chore: add circle
This commit is contained in:
220
src/components/Header.vue
Normal file
220
src/components/Header.vue
Normal file
@@ -0,0 +1,220 @@
|
||||
<template>
|
||||
<header>
|
||||
<div>
|
||||
<button @click="openSidebar" :aria-label="$t('buttons.toggleSidebar')" :title="$t('buttons.toggleSidebar')" class="action">
|
||||
<i class="material-icons">menu</i>
|
||||
</button>
|
||||
<img src="../assets/logo.svg" alt="File Browser">
|
||||
<search></search>
|
||||
</div>
|
||||
<div>
|
||||
<button @click="openSearch" :aria-label="$t('buttons.search')" :title="$t('buttons.search')" class="search-button action">
|
||||
<i class="material-icons">search</i>
|
||||
</button>
|
||||
|
||||
<button v-show="showSaveButton" :aria-label="$t('buttons.save')" :title="$t('buttons.save')" class="action" id="save-button">
|
||||
<i class="material-icons">save</i>
|
||||
</button>
|
||||
|
||||
<template v-if="staticGen.length > 0">
|
||||
<button v-show="showPublishButton" :aria-label="$t('buttons.publish')" :title="$t('buttons.publish')" class="action" id="publish-button">
|
||||
<i class="material-icons">send</i>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
|
||||
<i class="material-icons">more_vert</i>
|
||||
</button>
|
||||
|
||||
<!-- Menu that shows on listing AND mobile when there are files selected -->
|
||||
<div id="file-selection" v-if="isMobile && req.kind === 'listing'">
|
||||
<span v-if="selectedCount > 0">{{ selectedCount }} selected</span>
|
||||
<share-button v-show="showRenameButton"></share-button>
|
||||
<rename-button v-show="showRenameButton"></rename-button>
|
||||
<copy-button v-show="showMoveButton"></copy-button>
|
||||
<move-button v-show="showMoveButton"></move-button>
|
||||
<delete-button v-show="showDeleteButton"></delete-button>
|
||||
</div>
|
||||
|
||||
<!-- This buttons are shown on a dropdown on mobile phones -->
|
||||
<div id="dropdown" :class="{ active: showMore }">
|
||||
<div v-if="!isListing || !isMobile">
|
||||
<share-button v-show="showRenameButton"></share-button>
|
||||
<rename-button v-show="showRenameButton"></rename-button>
|
||||
<copy-button v-show="showMoveButton"></copy-button>
|
||||
<move-button v-show="showMoveButton"></move-button>
|
||||
<delete-button v-show="showDeleteButton"></delete-button>
|
||||
</div>
|
||||
|
||||
<template v-if="staticGen.length > 0">
|
||||
<schedule-button v-show="showPublishButton"></schedule-button>
|
||||
</template>
|
||||
|
||||
<switch-button v-show="showSwitchButton"></switch-button>
|
||||
<download-button v-show="showCommonButton"></download-button>
|
||||
<upload-button v-show="showUpload"></upload-button>
|
||||
<info-button v-show="showCommonButton"></info-button>
|
||||
|
||||
<button v-show="showSelectButton" @click="openSelect" :aria-label="$t('buttons.selectMultiple')" :title="$t('buttons.selectMultiple')" class="action">
|
||||
<i class="material-icons">check_circle</i>
|
||||
<span>{{ $t('buttons.select') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Search from './Search'
|
||||
import InfoButton from './buttons/Info'
|
||||
import DeleteButton from './buttons/Delete'
|
||||
import RenameButton from './buttons/Rename'
|
||||
import UploadButton from './buttons/Upload'
|
||||
import DownloadButton from './buttons/Download'
|
||||
import SwitchButton from './buttons/SwitchView'
|
||||
import MoveButton from './buttons/Move'
|
||||
import CopyButton from './buttons/Copy'
|
||||
import ScheduleButton from './buttons/Schedule'
|
||||
import ShareButton from './buttons/Share'
|
||||
import {mapGetters, mapState} from 'vuex'
|
||||
import * as api from '@/utils/api'
|
||||
import buttons from '@/utils/buttons'
|
||||
|
||||
export default {
|
||||
name: 'main',
|
||||
components: {
|
||||
Search,
|
||||
InfoButton,
|
||||
DeleteButton,
|
||||
ShareButton,
|
||||
RenameButton,
|
||||
DownloadButton,
|
||||
CopyButton,
|
||||
UploadButton,
|
||||
SwitchButton,
|
||||
MoveButton,
|
||||
ScheduleButton
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
width: window.innerWidth,
|
||||
pluginData: {
|
||||
api,
|
||||
buttons,
|
||||
'store': this.$store,
|
||||
'router': this.$router
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
window.addEventListener('resize', () => {
|
||||
this.width = window.innerWidth
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'selectedCount'
|
||||
]),
|
||||
...mapState([
|
||||
'req',
|
||||
'user',
|
||||
'loading',
|
||||
'reload',
|
||||
'multiple',
|
||||
'staticGen'
|
||||
]),
|
||||
isMobile () {
|
||||
return this.width <= 736
|
||||
},
|
||||
isListing () {
|
||||
return this.req.kind === 'listing'
|
||||
},
|
||||
showSelectButton () {
|
||||
return this.req.kind === 'listing' && !this.loading && this.$route.name === 'Files'
|
||||
},
|
||||
showSaveButton () {
|
||||
return (this.req.kind === 'editor' && !this.loading)
|
||||
},
|
||||
showPublishButton () {
|
||||
return (this.req.kind === 'editor' && !this.loading && this.user.allowPublish)
|
||||
},
|
||||
showSwitchButton () {
|
||||
return this.req.kind === 'listing' && this.$route.name === 'Files' && !this.loading
|
||||
},
|
||||
showCommonButton () {
|
||||
return !(this.$route.name !== 'Files' || this.loading)
|
||||
},
|
||||
showUpload () {
|
||||
if (this.$route.name !== 'Files' || this.loading) return false
|
||||
|
||||
if (this.req.kind === 'editor') return false
|
||||
return this.user.allowNew
|
||||
},
|
||||
showDeleteButton () {
|
||||
if (this.$route.name !== 'Files' || this.loading) return false
|
||||
|
||||
if (this.req.kind === 'listing') {
|
||||
if (this.selectedCount === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
return this.user.allowEdit
|
||||
}
|
||||
|
||||
return this.user.allowEdit
|
||||
},
|
||||
showRenameButton () {
|
||||
if (this.$route.name !== 'Files' || this.loading) return false
|
||||
|
||||
if (this.req.kind === 'listing') {
|
||||
if (this.selectedCount === 1) {
|
||||
return this.user.allowEdit
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return this.user.allowEdit
|
||||
},
|
||||
showMoveButton () {
|
||||
if (this.$route.name !== 'Files' || this.loading) return false
|
||||
|
||||
if (this.req.kind !== 'listing') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.selectedCount > 0) {
|
||||
return this.user.allowEdit
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
showMore () {
|
||||
if (this.$route.name !== 'Files' || this.loading) return false
|
||||
return (this.$store.state.show === 'more')
|
||||
},
|
||||
showOverlay () {
|
||||
return (this.$store.state.show === 'more')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openSidebar () {
|
||||
this.$store.commit('showHover', 'sidebar')
|
||||
},
|
||||
openMore () {
|
||||
this.$store.commit('showHover', 'more')
|
||||
},
|
||||
openSearch () {
|
||||
this.$store.commit('showHover', 'search')
|
||||
},
|
||||
openSelect () {
|
||||
this.$store.commit('multiple', true)
|
||||
this.resetPrompts()
|
||||
},
|
||||
resetPrompts () {
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
23
src/components/Languages.vue
Normal file
23
src/components/Languages.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<select v-on:change="change" :value="selected">
|
||||
<option value="en">{{ $t('languages.en') }}</option>
|
||||
<option value="fr">{{ $t('languages.fr') }}</option>
|
||||
<option value="pt">{{ $t('languages.pt') }}</option>
|
||||
<option value="ja">{{ $t('languages.ja') }}</option>
|
||||
<option value="zh-cn">{{ $t('languages.zhCN') }}</option>
|
||||
<option value="zh-tw">{{ $t('languages.zhTW') }}</option>
|
||||
<option value="es">{{ $t('languages.es') }}</option>
|
||||
</select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'languages',
|
||||
props: [ 'selected' ],
|
||||
methods: {
|
||||
change (event) {
|
||||
this.$emit('update:selected', event.target.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
265
src/components/Search.vue
Normal file
265
src/components/Search.vue
Normal file
@@ -0,0 +1,265 @@
|
||||
<template>
|
||||
<div id="search" @click="open" v-bind:class="{ active , ongoing }">
|
||||
<div id="input">
|
||||
<button v-if="active" class="action" @click="close" :aria-label="$t('buttons.close')" :title="$t('buttons.close')">
|
||||
<i class="material-icons">arrow_back</i>
|
||||
</button>
|
||||
<i v-else class="material-icons">search</i>
|
||||
<input type="text"
|
||||
@keyup="keyup"
|
||||
@keyup.enter="submit"
|
||||
ref="input"
|
||||
:autofocus="active"
|
||||
v-model.trim="value"
|
||||
:aria-label="$t('search.writeToSearch')"
|
||||
:placeholder="placeholder">
|
||||
</div>
|
||||
|
||||
<div id="result">
|
||||
<div>
|
||||
<template v-if="search.length === 0 && commands.length === 0">
|
||||
<p>{{ text }}</p>
|
||||
|
||||
<template v-if="value.length === 0">
|
||||
<div class="boxes">
|
||||
<h3>{{ $t('search.types') }}</h3>
|
||||
<div>
|
||||
<div tabindex="0"
|
||||
role="button"
|
||||
@click="init('type:image')"
|
||||
:aria-label="$t('search.images')">
|
||||
<i class="material-icons">insert_photo</i>
|
||||
<p>{{ $t('search.images') }}</p>
|
||||
</div>
|
||||
|
||||
<div tabindex="0"
|
||||
role="button"
|
||||
@click="init('type:audio')"
|
||||
:aria-label="$t('search.music')">
|
||||
<i class="material-icons">volume_up</i>
|
||||
<p>{{ $t('search.music') }}</p>
|
||||
</div>
|
||||
|
||||
<div tabindex="0"
|
||||
role="button"
|
||||
@click="init('type:video')"
|
||||
:aria-label="$t('search.video')">
|
||||
<i class="material-icons">movie</i>
|
||||
<p>{{ $t('search.video') }}</p>
|
||||
</div>
|
||||
|
||||
<div tabindex="0"
|
||||
role="button"
|
||||
@click="init('type:pdf')"
|
||||
:aria-label="$t('search.pdf')">
|
||||
<i class="material-icons">picture_as_pdf</i>
|
||||
<p>{{ $t('search.pdf') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
<ul v-else-if="search.length > 0">
|
||||
<li v-for="s in search">
|
||||
<router-link @click.native="close" :to="'./' + s.path">
|
||||
<i v-if="s.dir" class="material-icons">folder</i>
|
||||
<i v-else class="material-icons">insert_drive_file</i>
|
||||
<span>./{{ s.path }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<pre v-else-if="commands.length > 0">
|
||||
<template v-for="c in commands">{{ c }}</template>
|
||||
</pre>
|
||||
</div>
|
||||
<p id="renew"><i class="material-icons spin">autorenew</i></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import url from '@/utils/url'
|
||||
import * as api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'search',
|
||||
data: function () {
|
||||
return {
|
||||
value: '',
|
||||
active: false,
|
||||
ongoing: false,
|
||||
scrollable: null,
|
||||
search: [],
|
||||
commands: [],
|
||||
reload: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show (val, old) {
|
||||
this.active = (val === 'search')
|
||||
|
||||
// If the hover was search and now it's something else
|
||||
// we should blur the input.
|
||||
if (old === 'search' && val !== 'search') {
|
||||
if (this.reload) {
|
||||
this.$store.commit('setReload', true)
|
||||
}
|
||||
|
||||
document.body.style.overflow = 'auto'
|
||||
this.reset()
|
||||
this.$refs.input.blur()
|
||||
}
|
||||
|
||||
// If we are starting to show the search box, we should
|
||||
// focus the input.
|
||||
if (val === 'search') {
|
||||
this.reload = false
|
||||
this.$refs.input.focus()
|
||||
document.body.style.overflow = 'hidden'
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['user', 'show']),
|
||||
// Placeholder value.
|
||||
placeholder: function () {
|
||||
if (this.user.allowCommands && this.user.commands.length > 0) {
|
||||
return this.$t('search.searchOrCommand')
|
||||
}
|
||||
|
||||
return this.$t('search.search')
|
||||
},
|
||||
// The text that is shown on the results' box while
|
||||
// there is no search result or command output to show.
|
||||
text: function () {
|
||||
if (this.ongoing) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (this.value.length === 0) {
|
||||
if (this.user.allowCommands && this.user.commands.length > 0) {
|
||||
return `${this.$t('search.searchOrSupportedCommand')} ${this.user.commands.join(', ')}.`
|
||||
}
|
||||
|
||||
this.$t('search.type')
|
||||
}
|
||||
|
||||
if (!this.supported() || !this.user.allowCommands) {
|
||||
return this.$t('search.pressToSearch')
|
||||
} else {
|
||||
return this.$t('search.pressToExecute')
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
// Gets the result div which will be scrollable.
|
||||
this.scrollable = document.querySelector('#search #result')
|
||||
|
||||
// Adds the keydown event on window for the ESC key, so
|
||||
// when it's pressed, it closes the search window.
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if (event.keyCode === 27) {
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
// Sets the search to active.
|
||||
open (event) {
|
||||
this.$store.commit('showHover', 'search')
|
||||
},
|
||||
// Closes the search and prevents the event
|
||||
// of propagating so it doesn't trigger the
|
||||
// click event on #search.
|
||||
close (event) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
this.$store.commit('closeHovers')
|
||||
},
|
||||
// Checks if the current input is a supported command.
|
||||
supported () {
|
||||
let pieces = this.value.split(' ')
|
||||
|
||||
for (let i = 0; i < this.user.commands.length; i++) {
|
||||
if (pieces[0] === this.user.commands[i]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
// Initializes the search with a default value.
|
||||
init (string) {
|
||||
this.value = string + ' '
|
||||
this.$refs.input.focus()
|
||||
},
|
||||
// Resets the search box value.
|
||||
reset () {
|
||||
this.value = ''
|
||||
this.active = false
|
||||
this.ongoing = false
|
||||
this.search = []
|
||||
this.commands = []
|
||||
},
|
||||
// When the user presses a key, if it is ESC
|
||||
// then it will close the search box. Otherwise,
|
||||
// it will set the search box to active and clean
|
||||
// the search results, as well as commands'.
|
||||
keyup (event) {
|
||||
if (event.keyCode === 27) {
|
||||
this.close(event)
|
||||
return
|
||||
}
|
||||
|
||||
this.search.length = 0
|
||||
this.commands.length = 0
|
||||
},
|
||||
// Submits the input to the server and sets ongoing to true.
|
||||
submit (event) {
|
||||
this.ongoing = true
|
||||
|
||||
let path = this.$route.path
|
||||
if (this.$store.state.req.kind !== 'listing') {
|
||||
path = url.removeLastDir(path) + '/'
|
||||
}
|
||||
|
||||
// In case of being a command.
|
||||
if (this.supported() && this.user.allowCommands) {
|
||||
api.command(path, this.value,
|
||||
(event) => {
|
||||
this.commands.push(event.data)
|
||||
this.scrollable.scrollTop = this.scrollable.scrollHeight
|
||||
},
|
||||
(event) => {
|
||||
this.reload = true
|
||||
this.ongoing = false
|
||||
this.scrollable.scrollTop = this.scrollable.scrollHeight
|
||||
}
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// In case of being a search.
|
||||
api.search(path, this.value,
|
||||
(event) => {
|
||||
let response = JSON.parse(event.data)
|
||||
if (response.path[0] === '/') {
|
||||
response.path = response.path.substring(1)
|
||||
}
|
||||
|
||||
this.search.push(response)
|
||||
this.scrollable.scrollTop = this.scrollable.scrollHeight
|
||||
},
|
||||
(event) => {
|
||||
this.ongoing = false
|
||||
this.scrollable.scrollTop = this.scrollable.scrollHeight
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
90
src/components/Sidebar.vue
Normal file
90
src/components/Sidebar.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<nav :class="{active}">
|
||||
<router-link class="action" to="/files/" :aria-label="$t('sidebar.myFiles')" :title="$t('sidebar.myFiles')">
|
||||
<i class="material-icons">folder</i>
|
||||
<span>{{ $t('sidebar.myFiles') }}</span>
|
||||
</router-link>
|
||||
|
||||
<div v-if="user.allowNew">
|
||||
<button @click="$store.commit('showHover', 'newDir')" class="action" :aria-label="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')">
|
||||
<i class="material-icons">create_new_folder</i>
|
||||
<span>{{ $t('sidebar.newFolder') }}</span>
|
||||
</button>
|
||||
|
||||
<button @click="$store.commit('showHover', 'newFile')" class="action" :aria-label="$t('sidebar.newFile')" :title="$t('sidebar.newFile')">
|
||||
<i class="material-icons">note_add</i>
|
||||
<span>{{ $t('sidebar.newFile') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="staticGen.length > 0">
|
||||
<router-link to="/files/settings"
|
||||
:aria-label="$t('sidebar.siteSettings')"
|
||||
:title="$t('sidebar.siteSettings')"
|
||||
class="action">
|
||||
<i class="material-icons">settings</i>
|
||||
<span>{{ $t('sidebar.siteSettings') }}</span>
|
||||
</router-link>
|
||||
|
||||
<template v-if="staticGen === 'hugo'">
|
||||
<button class="action"
|
||||
:aria-label="$t('sidebar.hugoNew')"
|
||||
:title="$t('sidebar.hugoNew')"
|
||||
v-if="user.allowNew"
|
||||
@click="$store.commit('showHover', 'new-archetype')">
|
||||
<i class="material-icons">merge_type</i>
|
||||
<span>{{ $t('sidebar.hugoNew') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<button class="action"
|
||||
:aria-label="$t('sidebar.preview')"
|
||||
:title="$t('sidebar.preview')"
|
||||
@click="preview">
|
||||
<i class="material-icons">remove_red_eye</i>
|
||||
<span>{{ $t('sidebar.preview') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="!$store.state.noAuth">
|
||||
<router-link class="action" to="/settings" :aria-label="$t('sidebar.settings')" :title="$t('sidebar.settings')">
|
||||
<i class="material-icons">settings_applications</i>
|
||||
<span>{{ $t('sidebar.settings') }}</span>
|
||||
</router-link>
|
||||
|
||||
<button @click="logout" class="action" id="logout" :aria-label="$t('sidebar.logout')" :title="$t('sidebar.logout')">
|
||||
<i class="material-icons">exit_to_app</i>
|
||||
<span>{{ $t('sidebar.logout') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="credits">
|
||||
<span><a rel="noopener noreferrer" href="https://github.com/filebrowser/filebrowser">File Browser</a> v{{ version }}</span>
|
||||
<span><a @click="help">{{ $t('sidebar.help') }}</a></span>
|
||||
</p>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState} from 'vuex'
|
||||
import auth from '@/utils/auth'
|
||||
|
||||
export default {
|
||||
name: 'sidebar',
|
||||
computed: {
|
||||
...mapState(['user', 'staticGen', 'version']),
|
||||
active () {
|
||||
return this.$store.state.show === 'sidebar'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
help () {
|
||||
this.$store.commit('showHover', 'help')
|
||||
},
|
||||
preview () {
|
||||
window.open(this.$store.state.baseURL + '/preview/')
|
||||
},
|
||||
logout: auth.logout
|
||||
}
|
||||
}
|
||||
</script>
|
||||
17
src/components/buttons/Copy.vue
Normal file
17
src/components/buttons/Copy.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<button @click="show" :aria-label="$t('buttons.copy')" :title="$t('buttons.copy')" class="action" id="copy-button">
|
||||
<i class="material-icons">content_copy</i>
|
||||
<span>{{ $t('buttons.copyFile') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'copy-button',
|
||||
methods: {
|
||||
show: function (event) {
|
||||
this.$store.commit('showHover', 'copy')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
17
src/components/buttons/Delete.vue
Normal file
17
src/components/buttons/Delete.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<button @click="show" :aria-label="$t('buttons.delete')" :title="$t('buttons.delete')" class="action" id="delete-button">
|
||||
<i class="material-icons">delete</i>
|
||||
<span>{{ $t('buttons.delete') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'delete-button',
|
||||
methods: {
|
||||
show: function (event) {
|
||||
this.$store.commit('showHover', 'delete')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
39
src/components/buttons/Download.vue
Normal file
39
src/components/buttons/Download.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<button @click="download" :aria-label="$t('buttons.download')" :title="$t('buttons.download')" id="download-button" class="action">
|
||||
<i class="material-icons">file_download</i>
|
||||
<span>{{ $t('buttons.download') }}</span>
|
||||
<span v-if="selectedCount > 0" class="counter">{{ selectedCount }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapGetters, mapState} from 'vuex'
|
||||
import * as api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'download-button',
|
||||
computed: {
|
||||
...mapState(['req', 'selected']),
|
||||
...mapGetters(['selectedCount'])
|
||||
},
|
||||
methods: {
|
||||
download: function (event) {
|
||||
// If we are not on a listing, download the current file.
|
||||
if (this.req.kind !== 'listing') {
|
||||
api.download(null, this.$route.path)
|
||||
return
|
||||
}
|
||||
|
||||
// If we are on a listing and there is one element selected,
|
||||
// download it.
|
||||
if (this.selectedCount === 1 && !this.req.items[this.selected[0]].isDir) {
|
||||
api.download(null, this.req.items[this.selected[0]].url)
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise show the prompt to choose the formt of the download.
|
||||
this.$store.commit('showHover', 'download')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
17
src/components/buttons/Info.vue
Normal file
17
src/components/buttons/Info.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<button :title="$t('buttons.info')" :aria-label="$t('buttons.info')" class="action" @click="show">
|
||||
<i class="material-icons">info</i>
|
||||
<span>{{ $t('buttons.info') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'info-button',
|
||||
methods: {
|
||||
show: function (event) {
|
||||
this.$store.commit('showHover', 'info')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
17
src/components/buttons/Move.vue
Normal file
17
src/components/buttons/Move.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<button @click="show" :aria-label="$t('buttons.move')" :title="$t('buttons.move')" class="action" id="move-button">
|
||||
<i class="material-icons">forward</i>
|
||||
<span>{{ $t('buttons.moveFile') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'move-button',
|
||||
methods: {
|
||||
show: function (event) {
|
||||
this.$store.commit('showHover', 'move')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
17
src/components/buttons/Rename.vue
Normal file
17
src/components/buttons/Rename.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<button @click="show" :aria-label="$t('buttons.rename')" :title="$t('buttons.rename')" class="action" id="rename-button">
|
||||
<i class="material-icons">mode_edit</i>
|
||||
<span>{{ $t('buttons.rename') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'rename-button',
|
||||
methods: {
|
||||
show: function (event) {
|
||||
this.$store.commit('showHover', 'rename')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
21
src/components/buttons/Schedule.vue
Normal file
21
src/components/buttons/Schedule.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<button @click="show"
|
||||
:aria-label="$t('buttons.schedule')"
|
||||
:title="$t('buttons.schedule')"
|
||||
id="schedule-button"
|
||||
class="action">
|
||||
<i class="material-icons">alarm</i>
|
||||
<span>{{ $t('buttons.schedule') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'schedule-button',
|
||||
methods: {
|
||||
show: function (event) {
|
||||
this.$store.commit('showHover', 'schedule')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
17
src/components/buttons/Share.vue
Normal file
17
src/components/buttons/Share.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<button @click="show" :aria-label="$t('buttons.share')" :title="$t('buttons.share')" class="action">
|
||||
<i class="material-icons">share</i>
|
||||
<span>{{ $t('buttons.share') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'share-button',
|
||||
methods: {
|
||||
show (event) {
|
||||
this.$store.commit('showHover', 'share')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
36
src/components/buttons/SwitchView.vue
Normal file
36
src/components/buttons/SwitchView.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<button @click="change" :aria-label="$t('buttons.switchView')" :title="$t('buttons.switchView')" class="action" id="switch-view-button">
|
||||
<i class="material-icons">{{ icon }}</i>
|
||||
<span>{{ $t('buttons.switchView') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapMutations } from 'vuex'
|
||||
import { updateUser } from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'switch-button',
|
||||
computed: {
|
||||
...mapState(['user']),
|
||||
icon: function () {
|
||||
if (this.user.viewMode === 'mosaic') return 'view_list'
|
||||
return 'view_module'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(['updateUser']),
|
||||
change: function (event) {
|
||||
// If we are on mobile we should close the dropdown.
|
||||
this.$store.commit('closeHovers')
|
||||
|
||||
let user = {...this.user}
|
||||
user.viewMode = (this.icon === 'view_list') ? 'list' : 'mosaic'
|
||||
|
||||
updateUser(user, 'partial').then(() => {
|
||||
this.updateUser({ viewMode: user.viewMode })
|
||||
}).catch(this.$showError)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
17
src/components/buttons/Upload.vue
Normal file
17
src/components/buttons/Upload.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<button @click="upload" :aria-label="$t('buttons.upload')" :title="$t('buttons.upload')" class="action" id="upload-button">
|
||||
<i class="material-icons">file_upload</i>
|
||||
<span>{{ $t('buttons.upload') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'upload-button',
|
||||
methods: {
|
||||
upload: function (event) {
|
||||
document.getElementById('upload-input').click()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
143
src/components/files/Editor.vue
Normal file
143
src/components/files/Editor.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<form id="editor" :class="req.language">
|
||||
<div v-if="hasMetadata" id="metadata">
|
||||
<h2>{{ $t('files.metadata') }}</h2>
|
||||
</div>
|
||||
|
||||
<h2 v-if="hasMetadata">{{ $t('files.body') }}</h2>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import CodeMirror from '@/utils/codemirror'
|
||||
import * as api from '@/utils/api'
|
||||
import buttons from '@/utils/buttons'
|
||||
|
||||
export default {
|
||||
name: 'editor',
|
||||
computed: {
|
||||
...mapState(['req', 'schedule']),
|
||||
hasMetadata: function () {
|
||||
return (this.req.metadata !== undefined && this.req.metadata !== null)
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
metadata: null,
|
||||
metalang: null,
|
||||
content: null
|
||||
}
|
||||
},
|
||||
created () {
|
||||
window.addEventListener('keydown', this.keyEvent)
|
||||
document.getElementById('save-button').addEventListener('click', this.save)
|
||||
|
||||
let publish = document.getElementById('publish-button')
|
||||
if (publish !== null) {
|
||||
publish.addEventListener('click', this.publish)
|
||||
}
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.keyEvent)
|
||||
document.getElementById('save-button').removeEventListener('click', this.save)
|
||||
|
||||
let publish = document.getElementById('publish-button')
|
||||
if (publish !== null) {
|
||||
publish.removeEventListener('click', this.publish)
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
if (this.req.content === undefined || this.req.content === null) {
|
||||
this.req.content = ''
|
||||
}
|
||||
|
||||
// Set up the main content editor.
|
||||
this.content = CodeMirror(document.getElementById('editor'), {
|
||||
value: this.req.content,
|
||||
lineNumbers: (this.req.language !== 'markdown'),
|
||||
viewportMargin: 500,
|
||||
autofocus: true,
|
||||
mode: this.req.language,
|
||||
theme: (this.req.language === 'markdown') ? 'markdown' : 'ttcn',
|
||||
lineWrapping: (this.req.language === 'markdown')
|
||||
})
|
||||
|
||||
CodeMirror.autoLoadMode(this.content, this.req.language)
|
||||
|
||||
// Prevent of going on if there is no metadata.
|
||||
if (!this.hasMetadata) {
|
||||
return
|
||||
}
|
||||
|
||||
this.parseMetadata()
|
||||
|
||||
// Set up metadata editor.
|
||||
this.metadata = CodeMirror(document.getElementById('metadata'), {
|
||||
value: this.req.metadata,
|
||||
viewportMargin: Infinity,
|
||||
lineWrapping: true,
|
||||
theme: 'markdown',
|
||||
mode: this.metalang
|
||||
})
|
||||
|
||||
CodeMirror.autoLoadMode(this.metadata, this.metalang)
|
||||
},
|
||||
methods: {
|
||||
// Saves the content when the user presses CTRL-S.
|
||||
keyEvent (event) {
|
||||
if (!event.ctrlKey && !event.metaKey) {
|
||||
return
|
||||
}
|
||||
|
||||
if (String.fromCharCode(event.which).toLowerCase() !== 's') {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
this.save()
|
||||
},
|
||||
// Parses the metadata and gets the language in which
|
||||
// it is written.
|
||||
parseMetadata () {
|
||||
if (this.req.metadata.startsWith('{')) {
|
||||
this.metalang = 'json'
|
||||
}
|
||||
|
||||
if (this.req.metadata.startsWith('---')) {
|
||||
this.metalang = 'yaml'
|
||||
}
|
||||
|
||||
if (this.req.metadata.startsWith('+++')) {
|
||||
this.metalang = 'toml'
|
||||
}
|
||||
},
|
||||
// Publishes the file.
|
||||
publish (event) {
|
||||
this.save(event, true)
|
||||
},
|
||||
// Saves the file.
|
||||
save (event, regenerate = false) {
|
||||
let button = regenerate ? 'publish' : 'save'
|
||||
if (this.schedule !== '') button = 'schedule'
|
||||
let content = this.content.getValue()
|
||||
buttons.loading(button)
|
||||
|
||||
if (this.hasMetadata) {
|
||||
content = this.metadata.getValue() + '\n\n' + content
|
||||
}
|
||||
|
||||
api.put(this.$route.path, content, regenerate, this.schedule)
|
||||
.then(() => {
|
||||
buttons.success(button)
|
||||
this.$store.commit('setSchedule', '')
|
||||
})
|
||||
.catch(error => {
|
||||
buttons.done(button)
|
||||
this.$showError(error)
|
||||
this.$store.commit('setSchedule', '')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
381
src/components/files/Listing.vue
Normal file
381
src/components/files/Listing.vue
Normal file
@@ -0,0 +1,381 @@
|
||||
<template>
|
||||
<div v-if="(req.numDirs + req.numFiles) == 0">
|
||||
<h2 class="message">
|
||||
<i class="material-icons">sentiment_dissatisfied</i>
|
||||
<span>{{ $t('files.lonely') }}</span>
|
||||
</h2>
|
||||
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
|
||||
</div>
|
||||
<div v-else id="listing"
|
||||
:class="user.viewMode"
|
||||
@dragenter="dragEnter"
|
||||
@dragend="dragEnd">
|
||||
<div>
|
||||
<div class="item header">
|
||||
<div></div>
|
||||
<div>
|
||||
<p :class="{ active: nameSorted }" class="name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="sort('name')"
|
||||
:title="$t('files.sortByName')"
|
||||
:aria-label="$t('files.sortByName')">
|
||||
<span>{{ $t('files.name') }}</span>
|
||||
<i class="material-icons">{{ nameIcon }}</i>
|
||||
</p>
|
||||
|
||||
<p :class="{ active: sizeSorted }" class="size"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="sort('size')"
|
||||
:title="$t('files.sortBySize')"
|
||||
:aria-label="$t('files.sortBySize')">
|
||||
<span>{{ $t('files.size') }}</span>
|
||||
<i class="material-icons">{{ sizeIcon }}</i>
|
||||
</p>
|
||||
<p :class="{ active: modifiedSorted }" class="modified"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="sort('modified')"
|
||||
:title="$t('files.sortByLastModified')"
|
||||
:aria-label="$t('files.sortByLastModified')">
|
||||
<span>{{ $t('files.lastModified') }}</span>
|
||||
<i class="material-icons">{{ modifiedIcon }}</i>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 v-if="req.numDirs > 0">{{ $t('files.folders') }}</h2>
|
||||
<div v-if="req.numDirs > 0">
|
||||
<item v-for="(item, index) in req.items"
|
||||
v-if="item.isDir"
|
||||
:key="base64(item.name)"
|
||||
v-bind:index="index"
|
||||
v-bind:name="item.name"
|
||||
v-bind:isDir="item.isDir"
|
||||
v-bind:url="item.url"
|
||||
v-bind:modified="item.modified"
|
||||
v-bind:type="item.type"
|
||||
v-bind:size="item.size">
|
||||
</item>
|
||||
</div>
|
||||
|
||||
<h2 v-if="req.numFiles > 0">{{ $t('files.files') }}</h2>
|
||||
<div v-if="req.numFiles > 0">
|
||||
<item v-for="(item, index) in req.items"
|
||||
v-if="!item.isDir"
|
||||
:key="base64(item.name)"
|
||||
v-bind:index="index"
|
||||
v-bind:name="item.name"
|
||||
v-bind:isDir="item.isDir"
|
||||
v-bind:url="item.url"
|
||||
v-bind:modified="item.modified"
|
||||
v-bind:type="item.type"
|
||||
v-bind:size="item.size">
|
||||
</item>
|
||||
</div>
|
||||
|
||||
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
|
||||
|
||||
<div v-show="$store.state.multiple" :class="{ active: $store.state.multiple }" id="multiple-selection">
|
||||
<p>{{ $t('files.multipleSelectionEnabled') }}</p>
|
||||
<div @click="$store.commit('multiple', false)" tabindex="0" role="button" :title="$t('files.clear')" :aria-label="$t('files.clear')" class="action">
|
||||
<i class="material-icons">clear</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState} from 'vuex'
|
||||
import Item from './ListingItem'
|
||||
import css from '@/utils/css'
|
||||
import * as api from '@/utils/api'
|
||||
import buttons from '@/utils/buttons'
|
||||
|
||||
export default {
|
||||
name: 'listing',
|
||||
components: { Item },
|
||||
computed: {
|
||||
...mapState(['req', 'selected', 'user']),
|
||||
nameSorted () {
|
||||
return (this.req.sort === 'name')
|
||||
},
|
||||
sizeSorted () {
|
||||
return (this.req.sort === 'size')
|
||||
},
|
||||
modifiedSorted () {
|
||||
return (this.req.sort === 'modified')
|
||||
},
|
||||
ascOrdered () {
|
||||
return (this.req.order === 'asc')
|
||||
},
|
||||
nameIcon () {
|
||||
if (this.nameSorted && !this.ascOrdered) {
|
||||
return 'arrow_upward'
|
||||
}
|
||||
|
||||
return 'arrow_downward'
|
||||
},
|
||||
sizeIcon () {
|
||||
if (this.sizeSorted && this.ascOrdered) {
|
||||
return 'arrow_downward'
|
||||
}
|
||||
|
||||
return 'arrow_upward'
|
||||
},
|
||||
modifiedIcon () {
|
||||
if (this.modifiedSorted && this.ascOrdered) {
|
||||
return 'arrow_downward'
|
||||
}
|
||||
|
||||
return 'arrow_upward'
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
// Check the columns size for the first time.
|
||||
this.resizeEvent()
|
||||
|
||||
// Add the needed event listeners to the window and document.
|
||||
window.addEventListener('keydown', this.keyEvent)
|
||||
window.addEventListener('resize', this.resizeEvent)
|
||||
document.addEventListener('dragover', this.preventDefault)
|
||||
document.addEventListener('drop', this.drop)
|
||||
},
|
||||
beforeDestroy () {
|
||||
// Remove event listeners before destroying this page.
|
||||
window.removeEventListener('keydown', this.keyEvent)
|
||||
window.removeEventListener('resize', this.resizeEvent)
|
||||
document.removeEventListener('dragover', this.preventDefault)
|
||||
document.removeEventListener('drop', this.drop)
|
||||
},
|
||||
methods: {
|
||||
base64: function (name) {
|
||||
return window.btoa(unescape(encodeURIComponent(name)))
|
||||
},
|
||||
keyEvent (event) {
|
||||
if (!event.ctrlKey && !event.metaKey) {
|
||||
return
|
||||
}
|
||||
|
||||
let key = String.fromCharCode(event.which).toLowerCase()
|
||||
|
||||
switch (key) {
|
||||
case 'f':
|
||||
event.preventDefault()
|
||||
this.$store.commit('showHover', 'search')
|
||||
break
|
||||
case 'c':
|
||||
case 'x':
|
||||
this.copyCut(event, key)
|
||||
break
|
||||
case 'v':
|
||||
this.paste(event)
|
||||
break
|
||||
}
|
||||
},
|
||||
preventDefault (event) {
|
||||
// Wrapper around prevent default.
|
||||
event.preventDefault()
|
||||
},
|
||||
copyCut (event, key) {
|
||||
event.preventDefault()
|
||||
let items = []
|
||||
|
||||
for (let i of this.selected) {
|
||||
items.push({
|
||||
from: this.req.items[i].url,
|
||||
name: encodeURIComponent(this.req.items[i].name)
|
||||
})
|
||||
}
|
||||
|
||||
this.$store.commit('updateClipboard', {
|
||||
key: key,
|
||||
items: items
|
||||
})
|
||||
},
|
||||
paste (event) {
|
||||
if (event.target.tagName.toLowerCase() === 'input') {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
let items = []
|
||||
|
||||
for (let item of this.$store.state.clipboard.items) {
|
||||
items.push({
|
||||
from: item.from,
|
||||
to: this.$route.path + item.name
|
||||
})
|
||||
}
|
||||
|
||||
if (this.$store.state.clipboard.key === 'x') {
|
||||
api.move(items).then(() => {
|
||||
this.$store.commit('setReload', true)
|
||||
}).catch(this.$showError)
|
||||
return
|
||||
}
|
||||
|
||||
api.copy(items).then(() => {
|
||||
this.$store.commit('setReload', true)
|
||||
}).catch(this.$showError)
|
||||
},
|
||||
resizeEvent () {
|
||||
// Update the columns size based on the window width.
|
||||
let columns = Math.floor(document.querySelector('main').offsetWidth / 300)
|
||||
let items = css(['#listing.mosaic .item', '.mosaic#listing .item'])
|
||||
if (columns === 0) columns = 1
|
||||
items.style.width = `calc(${100 / columns}% - 1em)`
|
||||
},
|
||||
dragEnter (event) {
|
||||
// When the user starts dragging an item, put every
|
||||
// file on the listing with 50% opacity.
|
||||
let items = document.getElementsByClassName('item')
|
||||
|
||||
Array.from(items).forEach(file => {
|
||||
file.style.opacity = 0.5
|
||||
})
|
||||
},
|
||||
dragEnd (event) {
|
||||
this.resetOpacity()
|
||||
},
|
||||
drop: function (event) {
|
||||
event.preventDefault()
|
||||
this.resetOpacity()
|
||||
|
||||
let dt = event.dataTransfer
|
||||
let files = dt.files
|
||||
let el = event.target
|
||||
|
||||
if (files.length <= 0) return
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (el !== null && !el.classList.contains('item')) {
|
||||
el = el.parentElement
|
||||
}
|
||||
}
|
||||
|
||||
let base = ''
|
||||
if (el !== null && el.classList.contains('item') && el.dataset.dir === 'true') {
|
||||
base = el.querySelector('.name').innerHTML + '/'
|
||||
}
|
||||
|
||||
if (base !== '') {
|
||||
api.fetch(this.$route.path + base)
|
||||
.then(req => {
|
||||
this.checkConflict(files, req.items, base)
|
||||
})
|
||||
.catch(this.$showError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this.checkConflict(files, this.req.items, base)
|
||||
},
|
||||
checkConflict (files, items, base) {
|
||||
if (typeof items === 'undefined' || items === null) {
|
||||
items = []
|
||||
}
|
||||
|
||||
let conflict = false
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
let res = items.findIndex(function hasConflict (element) {
|
||||
return (element.name === this)
|
||||
}, files[i].name)
|
||||
|
||||
if (res >= 0) {
|
||||
conflict = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!conflict) {
|
||||
this.handleFiles(files, base)
|
||||
return
|
||||
}
|
||||
|
||||
this.$store.commit('showHover', {
|
||||
prompt: 'replace',
|
||||
confirm: (event) => {
|
||||
event.preventDefault()
|
||||
this.$store.commit('closeHovers')
|
||||
this.handleFiles(files, base, true)
|
||||
}
|
||||
})
|
||||
},
|
||||
uploadInput (event) {
|
||||
this.checkConflict(event.currentTarget.files, this.req.items, '')
|
||||
},
|
||||
resetOpacity () {
|
||||
let items = document.getElementsByClassName('item')
|
||||
|
||||
Array.from(items).forEach(file => {
|
||||
file.style.opacity = 1
|
||||
})
|
||||
},
|
||||
handleFiles (files, base, overwrite = false) {
|
||||
buttons.loading('upload')
|
||||
let promises = []
|
||||
let progress = new Array(files.length).fill(0)
|
||||
|
||||
let onupload = (id) => (event) => {
|
||||
progress[id] = (event.loaded / event.total) * 100
|
||||
|
||||
let sum = 0
|
||||
for (let i = 0; i < progress.length; i++) {
|
||||
sum += progress[i]
|
||||
}
|
||||
|
||||
this.$store.commit('setProgress', Math.ceil(sum / progress.length))
|
||||
}
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
let file = files[i]
|
||||
promises.push(api.post(this.$route.path + base + file.name, file, overwrite, onupload(i)))
|
||||
}
|
||||
|
||||
let finish = () => {
|
||||
buttons.success('upload')
|
||||
this.$store.commit('setProgress', 0)
|
||||
}
|
||||
|
||||
Promise.all(promises)
|
||||
.then(() => {
|
||||
finish()
|
||||
this.$store.commit('setReload', true)
|
||||
})
|
||||
.catch(error => {
|
||||
finish()
|
||||
this.$showError(error)
|
||||
})
|
||||
|
||||
return false
|
||||
},
|
||||
sort (sort) {
|
||||
let order = 'desc'
|
||||
|
||||
if (sort === 'name') {
|
||||
if (this.nameIcon === 'arrow_upward') {
|
||||
order = 'asc'
|
||||
}
|
||||
} else if (sort === 'size') {
|
||||
if (this.sizeIcon === 'arrow_upward') {
|
||||
order = 'asc'
|
||||
}
|
||||
} else if (sort === 'modified') {
|
||||
if (this.modifiedIcon === 'arrow_upward') {
|
||||
order = 'asc'
|
||||
}
|
||||
}
|
||||
|
||||
let path = this.$store.state.baseURL
|
||||
if (path === '') path = '/'
|
||||
document.cookie = `sort=${sort}; max-age=31536000; path=${path}`
|
||||
document.cookie = `order=${order}; max-age=31536000; path=${path}`
|
||||
this.$store.commit('setReload', true)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
158
src/components/files/ListingItem.vue
Normal file
158
src/components/files/ListingItem.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<div class="item"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
draggable="true"
|
||||
@dragstart="dragStart"
|
||||
@dragover="dragOver"
|
||||
@drop="drop"
|
||||
@click="click"
|
||||
@dblclick="open"
|
||||
@touchstart="touchstart"
|
||||
:data-dir="isDir"
|
||||
:aria-label="name"
|
||||
:aria-selected="isSelected">
|
||||
<div>
|
||||
<i class="material-icons">{{ icon }}</i>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="name">{{ name }}</p>
|
||||
|
||||
<p v-if="isDir" class="size" data-order="-1">—</p>
|
||||
<p v-else class="size" :data-order="humanSize()">{{ humanSize() }}</p>
|
||||
|
||||
<p class="modified">
|
||||
<time :datetime="modified">{{ humanTime() }}</time>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapMutations, mapGetters, mapState } from 'vuex'
|
||||
import filesize from 'filesize'
|
||||
import moment from 'moment'
|
||||
import * as api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'item',
|
||||
data: function () {
|
||||
return {
|
||||
touches: 0
|
||||
}
|
||||
},
|
||||
props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index'],
|
||||
computed: {
|
||||
...mapState(['selected', 'req']),
|
||||
...mapGetters(['selectedCount']),
|
||||
isSelected () {
|
||||
return (this.selected.indexOf(this.index) !== -1)
|
||||
},
|
||||
icon () {
|
||||
if (this.isDir) return 'folder'
|
||||
if (this.type === 'image') return 'insert_photo'
|
||||
if (this.type === 'audio') return 'volume_up'
|
||||
if (this.type === 'video') return 'movie'
|
||||
return 'insert_drive_file'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(['addSelected', 'removeSelected', 'resetSelected']),
|
||||
humanSize: function () {
|
||||
return filesize(this.size)
|
||||
},
|
||||
humanTime: function () {
|
||||
return moment(this.modified).fromNow()
|
||||
},
|
||||
dragStart: function (event) {
|
||||
if (this.selectedCount === 0) {
|
||||
this.addSelected(this.index)
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.isSelected) {
|
||||
this.resetSelected()
|
||||
this.addSelected(this.index)
|
||||
}
|
||||
},
|
||||
dragOver: function (event) {
|
||||
if (!this.isDir) return
|
||||
|
||||
event.preventDefault()
|
||||
let el = event.target
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (!el.classList.contains('item')) {
|
||||
el = el.parentElement
|
||||
}
|
||||
}
|
||||
|
||||
el.style.opacity = 1
|
||||
},
|
||||
drop: function (event) {
|
||||
if (!this.isDir) return
|
||||
event.preventDefault()
|
||||
|
||||
if (this.selectedCount === 0) return
|
||||
|
||||
let items = []
|
||||
|
||||
for (let i of this.selected) {
|
||||
items.push({
|
||||
from: this.req.items[i].url,
|
||||
to: this.url + encodeURIComponent(this.req.items[i].name)
|
||||
})
|
||||
}
|
||||
|
||||
api.move(items)
|
||||
.then(() => {
|
||||
this.$store.commit('setReload', true)
|
||||
})
|
||||
.catch(this.$showError)
|
||||
},
|
||||
click: function (event) {
|
||||
if (this.selectedCount !== 0) event.preventDefault()
|
||||
if (this.$store.state.selected.indexOf(this.index) !== -1) {
|
||||
this.removeSelected(this.index)
|
||||
return
|
||||
}
|
||||
|
||||
if (event.shiftKey && this.selected.length === 1) {
|
||||
let fi = 0
|
||||
let la = 0
|
||||
|
||||
if (this.index > this.selected[0]) {
|
||||
fi = this.selected[0] + 1
|
||||
la = this.index
|
||||
} else {
|
||||
fi = this.index
|
||||
la = this.selected[0] - 1
|
||||
}
|
||||
|
||||
for (; fi <= la; fi++) {
|
||||
this.addSelected(fi)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (!event.ctrlKey && !this.$store.state.multiple) this.resetSelected()
|
||||
this.addSelected(this.index)
|
||||
},
|
||||
touchstart (event) {
|
||||
setTimeout(() => {
|
||||
this.touches = 0
|
||||
}, 300)
|
||||
|
||||
this.touches++
|
||||
if (this.touches > 1) {
|
||||
this.open()
|
||||
}
|
||||
},
|
||||
open: function (event) {
|
||||
this.$router.push({path: this.url})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
139
src/components/files/Preview.vue
Normal file
139
src/components/files/Preview.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div id="previewer">
|
||||
<div class="bar">
|
||||
<button @click="back" class="action" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close">
|
||||
<i class="material-icons">close</i>
|
||||
</button>
|
||||
|
||||
<rename-button v-if="allowEdit()"></rename-button>
|
||||
<delete-button v-if="allowEdit()"></delete-button>
|
||||
<download-button></download-button>
|
||||
<info-button></info-button>
|
||||
</div>
|
||||
|
||||
<button class="action" @click="prev" v-show="hasPrevious" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')">
|
||||
<i class="material-icons">chevron_left</i>
|
||||
</button>
|
||||
<button class="action" @click="next" v-show="hasNext" :aria-label="$t('buttons.next')" :title="$t('buttons.next')">
|
||||
<i class="material-icons">chevron_right</i>
|
||||
</button>
|
||||
|
||||
<div class="preview">
|
||||
<img v-if="req.type == 'image'" :src="raw()">
|
||||
<audio v-else-if="req.type == 'audio'" :src="raw()" autoplay controls></audio>
|
||||
<video v-else-if="req.type == 'video'" :src="raw()" autoplay controls>
|
||||
Sorry, your browser doesn't support embedded videos,
|
||||
but don't worry, you can <a :href="download()">download it</a>
|
||||
and watch it with your favorite video player!
|
||||
</video>
|
||||
<object v-else-if="req.extension == '.pdf'" class="pdf" :data="raw()"></object>
|
||||
<a v-else-if="req.type == 'blob'" :href="download()">
|
||||
<h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2>
|
||||
</a>
|
||||
<pre v-else >{{ req.content }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import url from '@/utils/url'
|
||||
import * as api from '@/utils/api'
|
||||
import InfoButton from '@/components/buttons/Info'
|
||||
import DeleteButton from '@/components/buttons/Delete'
|
||||
import RenameButton from '@/components/buttons/Rename'
|
||||
import DownloadButton from '@/components/buttons/Download'
|
||||
|
||||
export default {
|
||||
name: 'preview',
|
||||
components: {
|
||||
InfoButton,
|
||||
DeleteButton,
|
||||
RenameButton,
|
||||
DownloadButton
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
previousLink: '',
|
||||
nextLink: '',
|
||||
listing: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['req', 'oldReq']),
|
||||
hasPrevious () {
|
||||
return (this.previousLink !== '')
|
||||
},
|
||||
hasNext () {
|
||||
return (this.nextLink !== '')
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
window.addEventListener('keyup', this.key)
|
||||
api.fetch(url.removeLastDir(this.$route.path))
|
||||
.then(req => {
|
||||
this.listing = req
|
||||
this.updateLinks()
|
||||
})
|
||||
.catch(this.$showError)
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keyup', this.key)
|
||||
},
|
||||
methods: {
|
||||
download () {
|
||||
let url = `${this.$store.state.baseURL}/api/download`
|
||||
url += this.req.url.slice(6)
|
||||
|
||||
return url
|
||||
},
|
||||
raw () {
|
||||
return `${this.download()}?&inline=true`
|
||||
},
|
||||
back (event) {
|
||||
let uri = url.removeLastDir(this.$route.path) + '/'
|
||||
this.$router.push({ path: uri })
|
||||
},
|
||||
prev () {
|
||||
this.$router.push({ path: this.previousLink })
|
||||
},
|
||||
next () {
|
||||
this.$router.push({ path: this.nextLink })
|
||||
},
|
||||
key (event) {
|
||||
event.preventDefault()
|
||||
|
||||
if (event.which === 13 || event.which === 39) { // right arrow
|
||||
if (this.hasNext) this.next()
|
||||
} else if (event.which === 37) { // left arrow
|
||||
if (this.hasPrevious) this.prev()
|
||||
}
|
||||
},
|
||||
updateLinks () {
|
||||
let pos = null
|
||||
|
||||
for (let i = 0; i < this.listing.items.length; i++) {
|
||||
if (this.listing.items[i].name === this.req.name) {
|
||||
pos = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (pos === null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (pos !== 0) {
|
||||
this.previousLink = this.listing.items[pos - 1].url
|
||||
}
|
||||
|
||||
if (pos !== this.listing.items.length - 1) {
|
||||
this.nextLink = this.listing.items[pos + 1].url
|
||||
}
|
||||
},
|
||||
allowEdit (event) {
|
||||
return this.$store.state.user.allowEdit
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
69
src/components/prompts/Copy.vue
Normal file
69
src/components/prompts/Copy.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.copy') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.copyMessage') }}</p>
|
||||
<file-list @update:selected="val => dest = val"></file-list>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="cancel flat"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="flat"
|
||||
@click="copy"
|
||||
:disabled="$route.path === dest"
|
||||
:aria-label="$t('buttons.copy')"
|
||||
:title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import FileList from './FileList'
|
||||
import * as api from '@/utils/api'
|
||||
import buttons from '@/utils/buttons'
|
||||
|
||||
export default {
|
||||
name: 'copy',
|
||||
components: { FileList },
|
||||
data: function () {
|
||||
return {
|
||||
current: window.location.pathname,
|
||||
dest: null
|
||||
}
|
||||
},
|
||||
computed: mapState(['req', 'selected']),
|
||||
methods: {
|
||||
copy: function (event) {
|
||||
event.preventDefault()
|
||||
buttons.loading('copy')
|
||||
let items = []
|
||||
|
||||
// Create a new promise for each file.
|
||||
for (let item of this.selected) {
|
||||
items.push({
|
||||
from: this.req.items[item].url,
|
||||
to: this.dest + encodeURIComponent(this.req.items[item].name)
|
||||
})
|
||||
}
|
||||
|
||||
// Execute the promises.
|
||||
api.copy(items)
|
||||
.then(() => {
|
||||
buttons.success('copy')
|
||||
this.$router.push({ path: this.dest })
|
||||
})
|
||||
.catch(error => {
|
||||
buttons.done('copy')
|
||||
this.$showError(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
80
src/components/prompts/Delete.vue
Normal file
80
src/components/prompts/Delete.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-content">
|
||||
<p v-if="req.kind !== 'listing'">{{ $t('prompts.deleteMessageSingle') }}</p>
|
||||
<p v-else>{{ $t('prompts.deleteMessageMultiple', { count: selectedCount}) }}</p>
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<button @click="$store.commit('closeHovers')"
|
||||
class="flat cancel"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button @click="submit"
|
||||
class="flat"
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapGetters, mapMutations, mapState} from 'vuex'
|
||||
import { remove } from '@/utils/api'
|
||||
import url from '@/utils/url'
|
||||
import buttons from '@/utils/buttons'
|
||||
|
||||
export default {
|
||||
name: 'delete',
|
||||
computed: {
|
||||
...mapGetters(['selectedCount']),
|
||||
...mapState(['req', 'selected'])
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(['closeHovers']),
|
||||
submit: function (event) {
|
||||
this.closeHovers()
|
||||
buttons.loading('delete')
|
||||
|
||||
// If we are not on a listing, delete the current
|
||||
// opened file.
|
||||
if (this.req.kind !== 'listing') {
|
||||
remove(this.$route.path)
|
||||
.then(() => {
|
||||
buttons.success('delete')
|
||||
this.$router.push({ path: url.removeLastDir(this.$route.path) + '/' })
|
||||
})
|
||||
.catch(error => {
|
||||
buttons.done('delete')
|
||||
this.$showError(error)
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (this.selectedCount === 0) {
|
||||
// This shouldn't happen...
|
||||
return
|
||||
}
|
||||
|
||||
// Create the promises array and fill it with
|
||||
// the delete request for every selected file.
|
||||
let promises = []
|
||||
|
||||
for (let index of this.selected) {
|
||||
promises.push(remove(this.req.items[index].url))
|
||||
}
|
||||
|
||||
Promise.all(promises)
|
||||
.then(() => {
|
||||
buttons.success('delete')
|
||||
this.$store.commit('setReload', true)
|
||||
})
|
||||
.catch(error => {
|
||||
buttons.done('delete')
|
||||
this.$store.commit('setReload', true)
|
||||
this.$showError(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
47
src/components/prompts/Download.vue
Normal file
47
src/components/prompts/Download.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="card floating" id="download">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.download') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.downloadMessage') }}</p>
|
||||
|
||||
<button class="block cancel" @click="download('zip')" autofocus>zip</button>
|
||||
<button class="block cancel" @click="download('tar')" autofocus>tar</button>
|
||||
<button class="block cancel" @click="download('targz')" autofocus>tar.gz</button>
|
||||
<button class="block cancel" @click="download('tarbz2')" autofocus>tar.bz2</button>
|
||||
<button class="block cancel" @click="download('tarxz')" autofocus>tar.xz</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapGetters, mapState} from 'vuex'
|
||||
import * as api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'download',
|
||||
computed: {
|
||||
...mapState(['selected', 'req']),
|
||||
...mapGetters(['selectedCount'])
|
||||
},
|
||||
methods: {
|
||||
download: function (format) {
|
||||
if (this.selectedCount === 0) {
|
||||
api.download(format, this.$route.path)
|
||||
} else {
|
||||
let files = []
|
||||
|
||||
for (let i of this.selected) {
|
||||
files.push(this.req.items[i].url)
|
||||
}
|
||||
|
||||
api.download(format, ...files)
|
||||
}
|
||||
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
140
src/components/prompts/FileList.vue
Normal file
140
src/components/prompts/FileList.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<div>
|
||||
<ul class="file-list">
|
||||
<li @click="select"
|
||||
@touchstart="touchstart"
|
||||
@dblclick="next"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:aria-label="item.name"
|
||||
:aria-selected="selected == item.url"
|
||||
:key="item.name" v-for="item in items"
|
||||
:data-url="item.url">{{ item.name }}</li>
|
||||
</ul>
|
||||
|
||||
<p>{{ $t('prompts.currentlyNavigating') }} <code>{{ nav }}</code>.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import url from '@/utils/url'
|
||||
import * as api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'file-list',
|
||||
data: function () {
|
||||
return {
|
||||
items: [],
|
||||
touches: {
|
||||
id: '',
|
||||
count: 0
|
||||
},
|
||||
selected: null,
|
||||
current: window.location.pathname
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['req']),
|
||||
nav () {
|
||||
return decodeURIComponent(this.current)
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
// If we're showing this on a listing,
|
||||
// we can use the current request object
|
||||
// to fill the move options.
|
||||
if (this.req.kind === 'listing') {
|
||||
this.fillOptions(this.req)
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, we must be on a preview or editor
|
||||
// so we fetch the data from the previous directory.
|
||||
api.fetch(url.removeLastDir(this.$route.path))
|
||||
.then(this.fillOptions)
|
||||
.catch(this.$showError)
|
||||
},
|
||||
methods: {
|
||||
fillOptions (req) {
|
||||
// Sets the current path and resets
|
||||
// the current items.
|
||||
this.current = req.url
|
||||
this.items = []
|
||||
|
||||
this.$emit('update:selected', this.current)
|
||||
|
||||
// If the path isn't the root path,
|
||||
// show a button to navigate to the previous
|
||||
// directory.
|
||||
if (req.url !== '/files/') {
|
||||
this.items.push({
|
||||
name: '..',
|
||||
url: url.removeLastDir(req.url) + '/'
|
||||
})
|
||||
}
|
||||
|
||||
// If this folder is empty, finish here.
|
||||
if (req.items === null) return
|
||||
|
||||
// Otherwise we add every directory to the
|
||||
// move options.
|
||||
for (let item of req.items) {
|
||||
if (!item.isDir) continue
|
||||
|
||||
this.items.push({
|
||||
name: item.name,
|
||||
url: item.url
|
||||
})
|
||||
}
|
||||
},
|
||||
next: function (event) {
|
||||
// Retrieves the URL of the directory the user
|
||||
// just clicked in and fill the options with its
|
||||
// content.
|
||||
let uri = event.currentTarget.dataset.url
|
||||
|
||||
api.fetch(uri)
|
||||
.then(this.fillOptions)
|
||||
.catch(this.$showError)
|
||||
},
|
||||
touchstart (event) {
|
||||
let url = event.currentTarget.dataset.url
|
||||
|
||||
// In 300 milliseconds, we shall reset the count.
|
||||
setTimeout(() => {
|
||||
this.touches.count = 0
|
||||
}, 300)
|
||||
|
||||
// If the element the user is touching
|
||||
// is different from the last one he touched,
|
||||
// reset the count.
|
||||
if (this.touches.id !== url) {
|
||||
this.touches.id = url
|
||||
this.touches.count = 1
|
||||
return
|
||||
}
|
||||
|
||||
this.touches.count++
|
||||
|
||||
// If there is more than one touch already,
|
||||
// open the next screen.
|
||||
if (this.touches.count > 1) {
|
||||
this.next(event)
|
||||
}
|
||||
},
|
||||
select: function (event) {
|
||||
// If the element is already selected, unselect it.
|
||||
if (this.selected === event.currentTarget.dataset.url) {
|
||||
this.selected = null
|
||||
this.$emit('update:selected', this.current)
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise select the element.
|
||||
this.selected = event.currentTarget.dataset.url
|
||||
this.$emit('update:selected', this.selected)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
34
src/components/prompts/Help.vue
Normal file
34
src/components/prompts/Help.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="card floating help">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('help.help') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<ul>
|
||||
<li><strong>F1</strong> - {{ $t('help.f1') }}</li>
|
||||
<li><strong>F2</strong> - {{ $t('help.f2') }}</li>
|
||||
<li><strong>DEL</strong> - {{ $t('help.del') }}</li>
|
||||
<li><strong>ESC</strong> - {{ $t('help.esc') }}</li>
|
||||
<li><strong>CTRL + S</strong> - {{ $t('help.ctrl.s') }}</li>
|
||||
<li><strong>CTRL + F</strong> - {{ $t('help.ctrl.f') }}</li>
|
||||
<li><strong>CTRL + Click</strong> - {{ $t('help.ctrl.click') }}</li>
|
||||
<li><strong>Click</strong> - {{ $t('help.click') }}</li>
|
||||
<li><strong>Double click</strong> - {{ $t('help.doubleClick') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button type="submit"
|
||||
@click="$store.commit('closeHovers')"
|
||||
class="flat"
|
||||
:aria-label="$t('buttons.ok')"
|
||||
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {name: 'help'}
|
||||
</script>
|
||||
|
||||
122
src/components/prompts/Info.vue
Normal file
122
src/components/prompts/Info.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.fileInfo') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p v-if="selected.length > 1">{{ $t('prompts.filesSelected', { count: selected.length }) }}</p>
|
||||
|
||||
<p v-if="selected.length < 2"><strong>{{ $t('prompts.displayName') }}</strong> {{ name() }}</p>
|
||||
<p><strong>{{ $t('prompts.size') }}:</strong> <span id="content_length"></span>{{ humanSize() }}</p>
|
||||
<p v-if="selected.length < 2"><strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime() }}</p>
|
||||
|
||||
<template v-if="dir() && selected.length === 0">
|
||||
<p><strong>{{ $t('prompts.numberFiles') }}:</strong> {{ req.numFiles }}</p>
|
||||
<p><strong>{{ $t('prompts.numberDirs') }}:</strong> {{ req.numDirs }}</p>
|
||||
</template>
|
||||
|
||||
<template v-if="!dir()">
|
||||
<p><strong>MD5:</strong> <code><a @click="checksum($event, 'md5')">{{ $t('prompts.show') }}</a></code></p>
|
||||
<p><strong>SHA1:</strong> <code><a @click="checksum($event, 'sha1')">{{ $t('prompts.show') }}</a></code></p>
|
||||
<p><strong>SHA256:</strong> <code><a @click="checksum($event, 'sha256')">{{ $t('prompts.show') }}</a></code></p>
|
||||
<p><strong>SHA512:</strong> <code><a @click="checksum($event, 'sha512')">{{ $t('prompts.show') }}</a></code></p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button type="submit"
|
||||
@click="$store.commit('closeHovers')"
|
||||
class="flat"
|
||||
:aria-label="$t('buttons.ok')"
|
||||
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState, mapGetters} from 'vuex'
|
||||
import filesize from 'filesize'
|
||||
import moment from 'moment'
|
||||
import * as api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'info',
|
||||
computed: {
|
||||
...mapState(['req', 'selected']),
|
||||
...mapGetters(['selectedCount'])
|
||||
},
|
||||
methods: {
|
||||
humanSize: function () {
|
||||
// If there are no files selected or this is not a listing
|
||||
// show the human file size of the current request.
|
||||
if (this.selectedCount === 0 || this.req.kind !== 'listing') {
|
||||
return filesize(this.req.size)
|
||||
}
|
||||
|
||||
// Otherwise, sum the sizes of each selected file and returns
|
||||
// its human form.
|
||||
var sum = 0
|
||||
|
||||
for (let i = 0; i < this.selectedCount; i++) {
|
||||
sum += this.req.items[this.selected[i]].size
|
||||
}
|
||||
|
||||
return filesize(sum)
|
||||
},
|
||||
humanTime: function () {
|
||||
// If there are no selected files, return the current request
|
||||
// modified time.
|
||||
if (this.selectedCount === 0) {
|
||||
return moment(this.req.modified).fromNow()
|
||||
}
|
||||
|
||||
// Otherwise return the modified time of the first item
|
||||
// that is selected since this should not appear when
|
||||
// there is more than one file selected.
|
||||
return moment(this.req.items[this.selected[0]]).fromNow()
|
||||
},
|
||||
name: function () {
|
||||
// Return the name of the current opened file if there
|
||||
// are no selected files.
|
||||
if (this.selectedCount === 0) {
|
||||
return this.req.name
|
||||
}
|
||||
|
||||
// Otherwise, just return the name of the selected file.
|
||||
// This field won't show when there is more than one
|
||||
// file selected.
|
||||
return this.req.items[this.selected[0]].name
|
||||
},
|
||||
dir: function () {
|
||||
if (this.selectedCount > 1) {
|
||||
// Don't show when multiple selected.
|
||||
return true
|
||||
}
|
||||
|
||||
if (this.selectedCount === 0) {
|
||||
return this.req.isDir
|
||||
}
|
||||
|
||||
return this.req.items[this.selected[0]].isDir
|
||||
},
|
||||
checksum: function (event, hash) {
|
||||
// Gets the checksum of the current selected or
|
||||
// opened file. Doesn't work for directories.
|
||||
event.preventDefault()
|
||||
|
||||
let link
|
||||
|
||||
if (this.selectedCount) {
|
||||
link = this.req.items[this.selected[0]].url
|
||||
} else {
|
||||
link = this.$route.path
|
||||
}
|
||||
|
||||
api.checksum(link, hash)
|
||||
.then((hash) => { event.target.innerHTML = hash })
|
||||
.catch(this.$showError)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
70
src/components/prompts/Move.vue
Normal file
70
src/components/prompts/Move.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.move') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<file-list @update:selected="val => dest = val"></file-list>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="flat cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="flat"
|
||||
@click="move"
|
||||
:disabled="$route.path === dest"
|
||||
:aria-label="$t('buttons.move')"
|
||||
:title="$t('buttons.move')">{{ $t('buttons.move') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import FileList from './FileList'
|
||||
import * as api from '@/utils/api'
|
||||
import buttons from '@/utils/buttons'
|
||||
|
||||
export default {
|
||||
name: 'move',
|
||||
components: { FileList },
|
||||
data: function () {
|
||||
return {
|
||||
current: window.location.pathname,
|
||||
dest: null
|
||||
}
|
||||
},
|
||||
computed: mapState(['req', 'selected']),
|
||||
methods: {
|
||||
move: function (event) {
|
||||
event.preventDefault()
|
||||
buttons.loading('move')
|
||||
let items = []
|
||||
|
||||
// Create a new promise for each file.
|
||||
for (let item of this.selected) {
|
||||
items.push({
|
||||
from: this.req.items[item].url,
|
||||
to: this.dest + encodeURIComponent(this.req.items[item].name)
|
||||
})
|
||||
}
|
||||
|
||||
// Execute the promises.
|
||||
api.move(items)
|
||||
.then(() => {
|
||||
buttons.success('move')
|
||||
this.$router.push({ path: this.dest })
|
||||
})
|
||||
.catch(error => {
|
||||
buttons.done('move')
|
||||
this.$showError(error)
|
||||
})
|
||||
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
76
src/components/prompts/NewArchetype.vue
Normal file
76
src/components/prompts/NewArchetype.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.newFile') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.newArchetype') }}</p>
|
||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||
<input type="text" @keyup.enter="submit" v-model.trim="archetype">
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="flat cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="flat"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { removePrefix } from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'new-archetype',
|
||||
data: function () {
|
||||
return {
|
||||
name: '',
|
||||
archetype: 'default'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit: function (event) {
|
||||
event.preventDefault()
|
||||
this.$store.commit('closeHovers')
|
||||
|
||||
this.new('/' + this.name, this.archetype)
|
||||
.then((url) => {
|
||||
this.$router.push({ path: url })
|
||||
})
|
||||
.catch(this.$showError)
|
||||
},
|
||||
new (url, type) {
|
||||
url = removePrefix(url)
|
||||
|
||||
if (!url.endsWith('.md') && !url.endsWith('.markdown')) {
|
||||
url += '.markdown'
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('POST', `${this.$store.state.baseURL}/api/resource${url}`, true)
|
||||
if (!this.$store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${this.$store.state.jwt}`)
|
||||
request.setRequestHeader('Archetype', encodeURIComponent(type))
|
||||
|
||||
request.onload = () => {
|
||||
if (request.status === 200) {
|
||||
resolve(request.getResponseHeader('Location'))
|
||||
} else {
|
||||
reject(request.responseText)
|
||||
}
|
||||
}
|
||||
|
||||
request.onerror = (error) => reject(error)
|
||||
request.send()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
60
src/components/prompts/NewDir.vue
Normal file
60
src/components/prompts/NewDir.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.newDir') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.newDirMessage') }}</p>
|
||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="cancel flat"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="flat"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')"
|
||||
@click="submit">{{ $t('buttons.create') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import url from '@/utils/url'
|
||||
import * as api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'new-dir',
|
||||
data: function () {
|
||||
return {
|
||||
name: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit: function (event) {
|
||||
event.preventDefault()
|
||||
if (this.new === '') return
|
||||
|
||||
// Build the path of the new directory.
|
||||
let uri = this.$route.path
|
||||
if (this.$store.state.req.kind !== 'listing') {
|
||||
uri = url.removeLastDir(uri) + '/'
|
||||
}
|
||||
|
||||
uri += this.name + '/'
|
||||
uri = uri.replace('//', '/')
|
||||
|
||||
api.post(uri)
|
||||
.then(() => { this.$router.push({ path: uri }) })
|
||||
.catch(this.$showError)
|
||||
|
||||
// Close the prompt
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
61
src/components/prompts/NewFile.vue
Normal file
61
src/components/prompts/NewFile.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.newFile') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.newFileMessage') }}</p>
|
||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="cancel flat"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="flat"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import url from '@/utils/url'
|
||||
import * as api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'new-file',
|
||||
data: function () {
|
||||
return {
|
||||
name: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit: function (event) {
|
||||
event.preventDefault()
|
||||
if (this.new === '') return
|
||||
|
||||
// Build the path of the new file.
|
||||
let uri = this.$route.path
|
||||
if (this.$store.state.req.kind !== 'listing') {
|
||||
uri = url.removeLastDir(uri) + '/'
|
||||
}
|
||||
|
||||
uri += this.name
|
||||
uri = uri.replace('//', '/')
|
||||
|
||||
// Create the new file.
|
||||
api.post(uri)
|
||||
.then(() => { this.$router.push({ path: uri }) })
|
||||
.catch(this.$showError)
|
||||
|
||||
// Close the prompt.
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
87
src/components/prompts/Prompts.vue
Normal file
87
src/components/prompts/Prompts.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div>
|
||||
<help v-if="showHelp" ></help>
|
||||
<download v-else-if="showDownload"></download>
|
||||
<new-file v-else-if="showNewFile"></new-file>
|
||||
<new-dir v-else-if="showNewDir"></new-dir>
|
||||
<rename v-else-if="showRename"></rename>
|
||||
<delete v-else-if="showDelete"></delete>
|
||||
<info v-else-if="showInfo"></info>
|
||||
<move v-else-if="showMove"></move>
|
||||
<copy v-else-if="showCopy"></copy>
|
||||
<replace v-else-if="showReplace"></replace>
|
||||
<schedule v-else-if="show === 'schedule'"></schedule>
|
||||
<new-archetype v-else-if="show === 'new-archetype'"></new-archetype>
|
||||
<share v-else-if="show === 'share'"></share>
|
||||
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Help from './Help'
|
||||
import Info from './Info'
|
||||
import Delete from './Delete'
|
||||
import Rename from './Rename'
|
||||
import Download from './Download'
|
||||
import Move from './Move'
|
||||
import Copy from './Copy'
|
||||
import NewFile from './NewFile'
|
||||
import NewDir from './NewDir'
|
||||
import NewArchetype from './NewArchetype'
|
||||
import Replace from './Replace'
|
||||
import Schedule from './Schedule'
|
||||
import Share from './Share'
|
||||
import { mapState } from 'vuex'
|
||||
import buttons from '@/utils/buttons'
|
||||
import * as api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'prompts',
|
||||
components: {
|
||||
Info,
|
||||
Delete,
|
||||
NewArchetype,
|
||||
Schedule,
|
||||
Rename,
|
||||
Download,
|
||||
Move,
|
||||
Copy,
|
||||
Share,
|
||||
NewFile,
|
||||
NewDir,
|
||||
Help,
|
||||
Replace
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
pluginData: {
|
||||
api,
|
||||
buttons,
|
||||
'store': this.$store,
|
||||
'router': this.$router
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['show', 'plugins']),
|
||||
showInfo: function () { return this.show === 'info' },
|
||||
showHelp: function () { return this.show === 'help' },
|
||||
showDelete: function () { return this.show === 'delete' },
|
||||
showRename: function () { return this.show === 'rename' },
|
||||
showMove: function () { return this.show === 'move' },
|
||||
showCopy: function () { return this.show === 'copy' },
|
||||
showNewFile: function () { return this.show === 'newFile' },
|
||||
showNewDir: function () { return this.show === 'newDir' },
|
||||
showDownload: function () { return this.show === 'download' },
|
||||
showReplace: function () { return this.show === 'replace' },
|
||||
showOverlay: function () {
|
||||
return (this.show !== null && this.show !== 'search' && this.show !== 'more')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetPrompts () {
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
84
src/components/prompts/Rename.vue
Normal file
84
src/components/prompts/Rename.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.rename') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.renameMessage') }} <code>{{ oldName() }}</code>:</p>
|
||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="cancel flat"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button @click="submit"
|
||||
class="flat"
|
||||
type="submit"
|
||||
:aria-label="$t('buttons.rename')"
|
||||
:title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import url from '@/utils/url'
|
||||
import * as api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'rename',
|
||||
data: function () {
|
||||
return {
|
||||
name: ''
|
||||
}
|
||||
},
|
||||
computed: mapState(['req', 'selected', 'selectedCount']),
|
||||
methods: {
|
||||
cancel: function (event) {
|
||||
this.$store.commit('closeHovers')
|
||||
},
|
||||
oldName: function () {
|
||||
// Get the current name of the file we are editing.
|
||||
if (this.req.kind !== 'listing') {
|
||||
return this.req.name
|
||||
}
|
||||
|
||||
if (this.selectedCount === 0 || this.selectedCount > 1) {
|
||||
// This shouldn't happen.
|
||||
return
|
||||
}
|
||||
|
||||
return this.req.items[this.selected[0]].name
|
||||
},
|
||||
submit: function (event) {
|
||||
let oldLink = ''
|
||||
let newLink = ''
|
||||
|
||||
if (this.req.kind !== 'listing') {
|
||||
oldLink = this.req.url
|
||||
} else {
|
||||
oldLink = this.req.items[this.selected[0]].url
|
||||
}
|
||||
|
||||
this.name = encodeURIComponent(this.name)
|
||||
newLink = url.removeLastDir(oldLink) + '/' + this.name
|
||||
|
||||
api.move([{ from: oldLink, to: newLink }])
|
||||
.then(() => {
|
||||
if (this.req.kind !== 'listing') {
|
||||
this.$router.push({ path: newLink })
|
||||
return
|
||||
}
|
||||
this.$store.commit('setReload', true)
|
||||
}).catch(error => {
|
||||
this.$showError(error)
|
||||
})
|
||||
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
31
src/components/prompts/Replace.vue
Normal file
31
src/components/prompts/Replace.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.replace') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.replaceMessage') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="flat cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="flat"
|
||||
@click="showConfirm"
|
||||
:aria-label="$t('buttons.replace')"
|
||||
:title="$t('buttons.replace')">{{ $t('buttons.replace') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'replace',
|
||||
computed: mapState(['showConfirm'])
|
||||
}
|
||||
</script>
|
||||
47
src/components/prompts/Schedule.vue
Normal file
47
src/components/prompts/Schedule.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.schedule') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.scheduleMessage') }}</p>
|
||||
<input autofocus type="datetime-local" v-model="date">
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="cancel flat"
|
||||
@click="close"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="falt"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.schedule')"
|
||||
:title="$t('buttons.schedule')">{{ $t('buttons.schedule') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'schedule',
|
||||
data: function () {
|
||||
return {
|
||||
date: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$store.commit('closeHovers')
|
||||
},
|
||||
submit: function (event) {
|
||||
event.preventDefault()
|
||||
if (this.date === '') return
|
||||
this.close()
|
||||
this.$store.commit('setSchedule', this.date)
|
||||
document.getElementById('save-button').click()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
162
src/components/prompts/Share.vue
Normal file
162
src/components/prompts/Share.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<div class="card floating" id="share">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('buttons.share') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<ul>
|
||||
<li v-if="!hasPermanent">
|
||||
<a @click="getPermalink" :aria-label="$t('buttons.permalink')">{{ $t('buttons.permalink') }}</a>
|
||||
</li>
|
||||
|
||||
<li v-for="link in links" :key="link.hash">
|
||||
<a :href="buildLink(link.hash)" target="_blank">
|
||||
<template v-if="link.expires">{{ humanTime(link.expireDate) }}</template>
|
||||
<template v-else>{{ $t('permanent') }}</template>
|
||||
</a>
|
||||
|
||||
<button class="action"
|
||||
@click="deleteLink($event, link)"
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')"><i class="material-icons">delete</i></button>
|
||||
|
||||
<button class="action copy-clipboard"
|
||||
:data-clipboard-text="buildLink(link.hash)"
|
||||
:aria-label="$t('buttons.copyToClipboard')"
|
||||
:title="$t('buttons.copyToClipboard')"><i class="material-icons">content_paste</i></button>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<input autofocus
|
||||
type="number"
|
||||
max="2147483647"
|
||||
min="0"
|
||||
@keyup.enter="submit"
|
||||
v-model.trim="time">
|
||||
<select v-model="unit" :aria-label="$t('time.unit')">
|
||||
<option value="seconds">{{ $t('time.seconds') }}</option>
|
||||
<option value="minutes">{{ $t('time.minutes') }}</option>
|
||||
<option value="hours">{{ $t('time.hours') }}</option>
|
||||
<option value="days">{{ $t('time.days') }}</option>
|
||||
</select>
|
||||
<button class="action"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')"><i class="material-icons">add</i></button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="flat"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.close')"
|
||||
:title="$t('buttons.close')">{{ $t('buttons.close') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import { getShare, deleteShare, share } from '@/utils/api'
|
||||
import moment from 'moment'
|
||||
import Clipboard from 'clipboard'
|
||||
|
||||
export default {
|
||||
name: 'share',
|
||||
data: function () {
|
||||
return {
|
||||
time: '',
|
||||
unit: 'hours',
|
||||
hasPermanent: false,
|
||||
links: [],
|
||||
clip: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState([ 'baseURL', 'req', 'selected', 'selectedCount' ]),
|
||||
url () {
|
||||
// Get the current name of the file we are editing.
|
||||
if (this.req.kind !== 'listing') {
|
||||
return this.$route.path
|
||||
}
|
||||
|
||||
if (this.selectedCount === 0 || this.selectedCount > 1) {
|
||||
// This shouldn't happen.
|
||||
return
|
||||
}
|
||||
|
||||
return this.req.items[this.selected[0]].url
|
||||
}
|
||||
},
|
||||
beforeMount () {
|
||||
getShare(this.url)
|
||||
.then(links => {
|
||||
this.links = links
|
||||
this.sort()
|
||||
|
||||
for (let link of this.links) {
|
||||
if (!link.expires) {
|
||||
this.hasPermanent = true
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (error === 404) return
|
||||
this.$showError(error)
|
||||
})
|
||||
},
|
||||
mounted () {
|
||||
this.clip = new Clipboard('.copy-clipboard')
|
||||
this.clip.on('success', (e) => {
|
||||
this.$showSuccess(this.$t('success.linkCopied'))
|
||||
})
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.clip.destroy()
|
||||
},
|
||||
methods: {
|
||||
submit: function (event) {
|
||||
if (!this.time) return
|
||||
|
||||
share(this.url, this.time, this.unit)
|
||||
.then(result => { this.links.push(result); this.sort() })
|
||||
.catch(this.$showError)
|
||||
},
|
||||
getPermalink (event) {
|
||||
share(this.url)
|
||||
.then(result => {
|
||||
this.links.push(result)
|
||||
this.sort()
|
||||
this.hasPermanent = true
|
||||
})
|
||||
.catch(this.$showError)
|
||||
},
|
||||
deleteLink (event, link) {
|
||||
event.preventDefault()
|
||||
deleteShare(link.hash)
|
||||
.then(() => {
|
||||
if (!link.expires) this.hasPermanent = false
|
||||
this.links = this.links.filter(item => item.hash !== link.hash)
|
||||
})
|
||||
.catch(this.$showError)
|
||||
},
|
||||
humanTime (time) {
|
||||
return moment(time).fromNow()
|
||||
},
|
||||
buildLink (hash) {
|
||||
return `${window.location.origin}${this.baseURL}/share/${hash}`
|
||||
},
|
||||
sort () {
|
||||
this.links = this.links.sort((a, b) => {
|
||||
if (!a.expires) return -1
|
||||
if (!b.expires) return 1
|
||||
return new Date(a.expireDate) - new Date(b.expireDate)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user