mirror of
https://github.com/filebrowser/filebrowser.git
synced 2025-05-08 19:22:57 +00:00
628 lines
19 KiB
Vue
628 lines
19 KiB
Vue
<template>
|
|
<div>
|
|
<header-bar showMenu showLogo>
|
|
<search /> <title />
|
|
<action class="search-button" icon="search" :label="$t('buttons.search')" @action="openSearch()" />
|
|
|
|
<template #actions>
|
|
<template v-if="!isMobile">
|
|
<action v-if="headerButtons.share" icon="share" :label="$t('buttons.share')" show="share" />
|
|
<action v-if="headerButtons.rename" icon="mode_edit" :label="$t('buttons.rename')" show="rename" />
|
|
<action v-if="headerButtons.copy" icon="content_copy" :label="$t('buttons.copyFile')" show="copy" />
|
|
<action v-if="headerButtons.move" icon="forward" :label="$t('buttons.moveFile')" show="move" />
|
|
<action v-if="headerButtons.delete" icon="delete" :label="$t('buttons.delete')" show="delete" />
|
|
</template>
|
|
|
|
<action v-if="headerButtons.shell" icon="code" :label="$t('buttons.shell')" @action="$store.commit('toggleShell')" />
|
|
<action :icon="user.viewMode === 'mosaic' ? 'view_list' : 'view_module'" :label="$t('buttons.switchView')" @action="switchView" />
|
|
<action icon="file_download" :label="$t('buttons.download')" @action="download" :counter="selectedCount" />
|
|
<action icon="file_upload" :label="$t('buttons.upload')" @action="upload" />
|
|
<action icon="info" :label="$t('buttons.info')" show="info" />
|
|
<action icon="check_circle" :label="$t('buttons.selectMultiple')" @action="toggleMultipleSelection" />
|
|
</template>
|
|
</header-bar>
|
|
|
|
<div v-if="isMobile" id="file-selection">
|
|
<span v-if="selectedCount > 0">{{ selectedCount }} selected</span>
|
|
<action v-if="headerButtons.share" icon="share" :label="$t('buttons.share')" show="share" />
|
|
<action v-if="headerButtons.rename" icon="mode_edit" :label="$t('buttons.rename')" show="rename" />
|
|
<action v-if="headerButtons.copy" icon="content_copy" :label="$t('buttons.copyFile')" show="copy" />
|
|
<action v-if="headerButtons.move" icon="forward" :label="$t('buttons.moveFile')" show="move" />
|
|
<action v-if="headerButtons.delete" icon="delete" :label="$t('buttons.delete')" show="delete" />
|
|
</div>
|
|
|
|
<div v-if="$store.state.loading">
|
|
<h2 class="message">
|
|
<span>{{ $t('files.loading') }}</span>
|
|
</h2>
|
|
</div>
|
|
<template v-else>
|
|
<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>
|
|
<input style="display:none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory multiple>
|
|
</div>
|
|
<div v-else id="listing"
|
|
:class="user.viewMode">
|
|
<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) in dirs"
|
|
:key="base64(item.name)"
|
|
v-bind:index="item.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) in files"
|
|
:key="base64(item.name)"
|
|
v-bind:index="item.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>
|
|
<input style="display:none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory multiple>
|
|
|
|
<div :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>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { mapState, mapGetters, mapMutations } from 'vuex'
|
|
import { users, files as api } from '@/api'
|
|
import { enableExec } from '@/utils/constants'
|
|
import * as upload from '@/utils/upload'
|
|
import css from '@/utils/css'
|
|
|
|
import HeaderBar from '@/components/header/HeaderBar'
|
|
import Action from '@/components/header/Action'
|
|
import Search from '@/components/Search'
|
|
import Item from '@/components/files/ListingItem'
|
|
|
|
export default {
|
|
name: 'listing',
|
|
components: {
|
|
HeaderBar,
|
|
Action,
|
|
Search,
|
|
Item
|
|
},
|
|
data: function () {
|
|
return {
|
|
showLimit: 50,
|
|
dragCounter: 0,
|
|
width: window.innerWidth
|
|
}
|
|
},
|
|
computed: {
|
|
...mapState([
|
|
'req',
|
|
'selected',
|
|
'user',
|
|
'show',
|
|
'multiple',
|
|
'selected'
|
|
]),
|
|
...mapGetters([
|
|
'selectedCount'
|
|
]),
|
|
nameSorted () {
|
|
return (this.req.sorting.by === 'name')
|
|
},
|
|
sizeSorted () {
|
|
return (this.req.sorting.by === 'size')
|
|
},
|
|
modifiedSorted () {
|
|
return (this.req.sorting.by === 'modified')
|
|
},
|
|
ascOrdered () {
|
|
return this.req.sorting.asc
|
|
},
|
|
items () {
|
|
const dirs = []
|
|
const files = []
|
|
|
|
this.req.items.forEach((item) => {
|
|
if (item.isDir) {
|
|
dirs.push(item)
|
|
} else {
|
|
files.push(item)
|
|
}
|
|
})
|
|
|
|
return { dirs, files }
|
|
},
|
|
dirs () {
|
|
return this.items.dirs.slice(0, this.showLimit)
|
|
},
|
|
files () {
|
|
let showLimit = this.showLimit - this.items.dirs.length
|
|
|
|
if (showLimit < 0) showLimit = 0
|
|
|
|
return this.items.files.slice(0, showLimit)
|
|
},
|
|
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'
|
|
},
|
|
headerButtons() {
|
|
return {
|
|
upload: this.user.perm.create,
|
|
download: this.user.perm.download,
|
|
shell: this.user.perm.execute && enableExec,
|
|
delete: this.selectedCount > 0 && this.user.perm.delete,
|
|
rename: this.selectedCount === 1 && this.user.perm.rename,
|
|
share: this.selectedCount === 1 && this.user.perm.share,
|
|
move: this.selectedCount > 0 && this.user.perm.rename,
|
|
copy: this.selectedCount > 0 && this.user.perm.create,
|
|
}
|
|
},
|
|
isMobile () {
|
|
return this.width <= 736
|
|
}
|
|
},
|
|
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)
|
|
window.addEventListener('scroll', this.scrollEvent)
|
|
window.addEventListener('resize', this.windowsResize)
|
|
document.addEventListener('dragover', this.preventDefault)
|
|
document.addEventListener('dragenter', this.dragEnter)
|
|
document.addEventListener('dragleave', this.dragLeave)
|
|
document.addEventListener('drop', this.drop)
|
|
},
|
|
beforeDestroy () {
|
|
// Remove event listeners before destroying this page.
|
|
window.removeEventListener('keydown', this.keyEvent)
|
|
window.removeEventListener('resize', this.resizeEvent)
|
|
window.removeEventListener('scroll', this.scrollEvent)
|
|
window.removeEventListener('resize', this.windowsResize)
|
|
document.removeEventListener('dragover', this.preventDefault)
|
|
document.removeEventListener('dragenter', this.dragEnter)
|
|
document.removeEventListener('dragleave', this.dragLeave)
|
|
document.removeEventListener('drop', this.drop)
|
|
},
|
|
methods: {
|
|
...mapMutations([ 'updateUser', 'addSelected' ]),
|
|
base64: function (name) {
|
|
return window.btoa(unescape(encodeURIComponent(name)))
|
|
},
|
|
keyEvent (event) {
|
|
// No prompts are shown
|
|
if (this.show !== null) {
|
|
return
|
|
}
|
|
|
|
// Esc!
|
|
if (event.keyCode === 27) {
|
|
// Reset files selection.
|
|
this.$store.commit('resetSelected')
|
|
}
|
|
|
|
// Del!
|
|
if (event.keyCode === 46) {
|
|
if (!this.user.perm.delete || this.selectedCount == 0) return
|
|
|
|
// Show delete prompt.
|
|
this.$store.commit('showHover', 'delete')
|
|
}
|
|
|
|
// F2!
|
|
if (event.keyCode === 113) {
|
|
if (!this.user.perm.rename || this.selectedCount !== 1) return
|
|
|
|
// Show rename prompt.
|
|
this.$store.commit('showHover', 'rename')
|
|
}
|
|
|
|
// Ctrl is pressed
|
|
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
|
|
case 'a':
|
|
event.preventDefault()
|
|
for (let file of this.items.files) {
|
|
if (this.$store.state.selected.indexOf(file.index) === -1) {
|
|
this.addSelected(file.index)
|
|
}
|
|
}
|
|
for (let dir of this.items.dirs) {
|
|
if (this.$store.state.selected.indexOf(dir.index) === -1) {
|
|
this.addSelected(dir.index)
|
|
}
|
|
}
|
|
break
|
|
case 's':
|
|
event.preventDefault()
|
|
document.getElementById('download-button').click()
|
|
break
|
|
}
|
|
},
|
|
preventDefault (event) {
|
|
// Wrapper around prevent default.
|
|
event.preventDefault()
|
|
},
|
|
copyCut (event, key) {
|
|
if (event.target.tagName.toLowerCase() === 'input') {
|
|
return
|
|
}
|
|
|
|
let items = []
|
|
|
|
for (let i of this.selected) {
|
|
items.push({
|
|
from: this.req.items[i].url,
|
|
name: encodeURIComponent(this.req.items[i].name)
|
|
})
|
|
}
|
|
|
|
if (items.length == 0) {
|
|
return
|
|
}
|
|
|
|
this.$store.commit('updateClipboard', {
|
|
key: key,
|
|
items: items,
|
|
path: this.$route.path
|
|
})
|
|
},
|
|
paste (event) {
|
|
if (event.target.tagName.toLowerCase() === 'input') {
|
|
return
|
|
}
|
|
|
|
let items = []
|
|
|
|
for (let item of this.$store.state.clipboard.items) {
|
|
const from = item.from.endsWith('/') ? item.from.slice(0, -1) : item.from
|
|
const to = this.$route.path + item.name
|
|
items.push({ from, to, name: item.name })
|
|
}
|
|
|
|
if (items.length === 0) {
|
|
return
|
|
}
|
|
|
|
let action = (overwrite, rename) => {
|
|
api.copy(items, overwrite, rename).then(() => {
|
|
this.$store.commit('setReload', true)
|
|
}).catch(this.$showError)
|
|
}
|
|
|
|
if (this.$store.state.clipboard.key === 'x') {
|
|
action = (overwrite, rename) => {
|
|
api.move(items, overwrite, rename).then(() => {
|
|
this.$store.commit('resetClipboard')
|
|
this.$store.commit('setReload', true)
|
|
}).catch(this.$showError)
|
|
}
|
|
}
|
|
|
|
if (this.$store.state.clipboard.path == this.$route.path) {
|
|
action(false, true)
|
|
|
|
return
|
|
}
|
|
|
|
let conflict = upload.checkConflict(items, this.req.items)
|
|
|
|
let overwrite = false
|
|
let rename = false
|
|
|
|
if (conflict) {
|
|
this.$store.commit('showHover', {
|
|
prompt: 'replace-rename',
|
|
confirm: (event, option) => {
|
|
overwrite = option == 'overwrite'
|
|
rename = option == 'rename'
|
|
|
|
event.preventDefault()
|
|
this.$store.commit('closeHovers')
|
|
action(overwrite, rename)
|
|
}
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
action(overwrite, rename)
|
|
},
|
|
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)`
|
|
},
|
|
scrollEvent () {
|
|
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
|
|
this.showLimit += 50
|
|
}
|
|
},
|
|
dragEnter () {
|
|
this.dragCounter++
|
|
|
|
// 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
|
|
})
|
|
},
|
|
dragLeave () {
|
|
this.dragCounter--
|
|
|
|
if (this.dragCounter == 0) {
|
|
this.resetOpacity()
|
|
}
|
|
},
|
|
drop: async function (event) {
|
|
event.preventDefault()
|
|
this.dragCounter = 0
|
|
this.resetOpacity()
|
|
|
|
let dt = event.dataTransfer
|
|
let el = event.target
|
|
|
|
if (dt.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 + '/'
|
|
}
|
|
|
|
let files = await upload.scanFiles(dt)
|
|
let path = this.$route.path.endsWith('/') ? this.$route.path + base : this.$route.path + '/' + base
|
|
let items = this.req.items
|
|
|
|
if (base !== '') {
|
|
try {
|
|
items = (await api.fetch(path)).items
|
|
} catch (error) {
|
|
this.$showError(error)
|
|
}
|
|
}
|
|
|
|
let conflict = upload.checkConflict(files, items)
|
|
|
|
if (conflict) {
|
|
this.$store.commit('showHover', {
|
|
prompt: 'replace',
|
|
confirm: (event) => {
|
|
event.preventDefault()
|
|
this.$store.commit('closeHovers')
|
|
upload.handleFiles(files, path, true)
|
|
}
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
upload.handleFiles(files, path)
|
|
},
|
|
uploadInput (event) {
|
|
this.$store.commit('closeHovers')
|
|
|
|
let files = event.currentTarget.files
|
|
let folder_upload = files[0].webkitRelativePath !== undefined && files[0].webkitRelativePath !== ''
|
|
|
|
if (folder_upload) {
|
|
for (let i = 0; i < files.length; i++) {
|
|
let file = files[i]
|
|
files[i].fullPath = file.webkitRelativePath
|
|
}
|
|
}
|
|
|
|
let path = this.$route.path.endsWith('/') ? this.$route.path : this.$route.path + '/'
|
|
let conflict = upload.checkConflict(files, this.req.items)
|
|
|
|
if (conflict) {
|
|
this.$store.commit('showHover', {
|
|
prompt: 'replace',
|
|
confirm: (event) => {
|
|
event.preventDefault()
|
|
this.$store.commit('closeHovers')
|
|
upload.handleFiles(files, path, true)
|
|
}
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
upload.handleFiles(files, path)
|
|
},
|
|
resetOpacity () {
|
|
let items = document.getElementsByClassName('item')
|
|
|
|
Array.from(items).forEach(file => {
|
|
file.style.opacity = 1
|
|
})
|
|
},
|
|
async sort (by) {
|
|
let asc = false
|
|
|
|
if (by === 'name') {
|
|
if (this.nameIcon === 'arrow_upward') {
|
|
asc = true
|
|
}
|
|
} else if (by === 'size') {
|
|
if (this.sizeIcon === 'arrow_upward') {
|
|
asc = true
|
|
}
|
|
} else if (by === 'modified') {
|
|
if (this.modifiedIcon === 'arrow_upward') {
|
|
asc = true
|
|
}
|
|
}
|
|
|
|
try {
|
|
await users.update({ id: this.user.id, sorting: { by, asc } }, ['sorting'])
|
|
} catch (e) {
|
|
this.$showError(e)
|
|
}
|
|
|
|
this.$store.commit('setReload', true)
|
|
},
|
|
openSearch () {
|
|
this.$store.commit('showHover', 'search')
|
|
},
|
|
toggleMultipleSelection () {
|
|
this.$store.commit('multiple', !this.multiple)
|
|
this.$store.commit('closeHovers')
|
|
},
|
|
windowsResize () {
|
|
this.width = window.innerWidth
|
|
},
|
|
download() {
|
|
if (this.selectedCount === 1 && !this.req.items[this.selected[0]].isDir) {
|
|
api.download(null, this.req.items[this.selected[0]].url)
|
|
return
|
|
}
|
|
|
|
this.$store.commit('showHover', {
|
|
prompt: 'download',
|
|
confirm: (format) => {
|
|
this.$store.commit('closeHovers')
|
|
|
|
let files = []
|
|
|
|
if (this.selectedCount > 0) {
|
|
for (let i of this.selected) {
|
|
files.push(this.req.items[i].url)
|
|
}
|
|
} else {
|
|
files.push(this.$route.path)
|
|
}
|
|
|
|
api.download(format, ...files)
|
|
}
|
|
})
|
|
},
|
|
switchView: async function () {
|
|
this.$store.commit('closeHovers')
|
|
|
|
const data = {
|
|
id: this.user.id,
|
|
viewMode: (this.user.viewMode === 'mosaic') ? 'list' : 'mosaic'
|
|
}
|
|
|
|
try {
|
|
await users.update(data, ['viewMode'])
|
|
this.$store.commit('updateUser', data)
|
|
} catch (e) {
|
|
this.$showError(e)
|
|
}
|
|
},
|
|
upload: function () {
|
|
if (typeof(DataTransferItem.prototype.webkitGetAsEntry) !== 'undefined') {
|
|
this.$store.commit('showHover', 'upload')
|
|
} else {
|
|
document.getElementById('upload-input').click();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|