chore: files pages logic responsability

This commit is contained in:
Ramires Viana
2021-02-26 15:10:21 +00:00
parent 95811e99bc
commit e503cb69f2
4 changed files with 216 additions and 232 deletions

View File

@@ -1,138 +0,0 @@
<template>
<div id="editor-container">
<header-bar>
<action icon="close" :label="$t('buttons.close')" @action="close()" />
<title>{{ req.name }}</title>
<template #actions>
<action id="save-button" icon="save" :label="$t('buttons.save')" @action="save()" />
</template>
</header-bar>
<div id="breadcrumbs">
<span><i class="material-icons">home</i></span>
<span v-for="(link, index) in breadcrumbs" :key="index">
<span class="chevron"><i class="material-icons">keyboard_arrow_right</i></span>
<span>{{ link.name }}</span>
</span>
</div>
<form id="editor"></form>
</div>
</template>
<script>
import { mapState } from 'vuex'
import { files as api } from '@/api'
import { theme } from '@/utils/constants'
import buttons from '@/utils/buttons'
import url from '@/utils/url'
import ace from 'ace-builds/src-min-noconflict/ace.js'
import modelist from 'ace-builds/src-min-noconflict/ext-modelist.js'
import 'ace-builds/webpack-resolver'
import HeaderBar from '@/components/header/HeaderBar'
import Action from '@/components/header/Action'
export default {
name: 'editor',
components: {
HeaderBar,
Action
},
data: function () {
return {}
},
computed: {
...mapState(['req', 'user']),
breadcrumbs () {
let parts = this.$route.path.split('/')
if (parts[0] === '') {
parts.shift()
}
if (parts[parts.length - 1] === '') {
parts.pop()
}
let breadcrumbs = []
for (let i = 0; i < parts.length; i++) {
breadcrumbs.push({ name: decodeURIComponent(parts[i]) })
}
breadcrumbs.shift()
if (breadcrumbs.length > 3) {
while (breadcrumbs.length !== 4) {
breadcrumbs.shift()
}
breadcrumbs[0].name = '...'
}
return breadcrumbs
}
},
created () {
window.addEventListener('keydown', this.keyEvent)
},
beforeDestroy () {
window.removeEventListener('keydown', this.keyEvent)
this.editor.destroy();
},
mounted: function () {
const fileContent = this.req.content || '';
this.editor = ace.edit('editor', {
value: fileContent,
showPrintMargin: false,
readOnly: this.req.type === 'textImmutable',
theme: 'ace/theme/chrome',
mode: modelist.getModeForPath(this.req.name).mode,
wrap: true
})
if (theme == 'dark') {
this.editor.setTheme("ace/theme/twilight");
}
},
methods: {
back () {
let uri = url.removeLastDir(this.$route.path) + '/'
this.$router.push({ path: uri })
},
keyEvent (event) {
if (!event.ctrlKey && !event.metaKey) {
return
}
if (String.fromCharCode(event.which).toLowerCase() !== 's') {
return
}
event.preventDefault()
this.save()
},
async save () {
const button = 'save'
buttons.loading('save')
try {
await api.put(this.$route.path, this.editor.getValue())
buttons.success(button)
} catch (e) {
buttons.done(button)
this.$showError(e)
}
},
close () {
let uri = url.removeLastDir(this.$route.path) + '/'
this.$router.push({ path: uri })
}
}
}
</script>

View File

@@ -1,464 +0,0 @@
<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>
<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>
<script>
import { mapState, mapMutations } from 'vuex'
import Item from './ListingItem'
import css from '@/utils/css'
import { users, files as api } from '@/api'
import * as upload from '@/utils/upload'
export default {
name: 'listing',
components: { Item },
data: function () {
return {
showLimit: 50,
dragCounter: 0
}
},
computed: {
...mapState(['req', 'selected', 'user', 'show']),
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'
}
},
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)
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)
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) {
if (this.show !== null) {
return
}
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
}
},
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)
}
}
}
</script>

View File

@@ -1,257 +0,0 @@
<template>
<div id="previewer" @mousemove="toggleNavigation" @touchstart="toggleNavigation">
<header-bar>
<action icon="close" :label="$t('buttons.close')" @action="close()" />
<title>{{ name }}</title>
<preview-size-button v-if="isResizeEnabled && req.type === 'image'" @change-size="toggleSize" v-bind:size="fullSize" :disabled="loading" />
<template #actions>
<rename-button :disabled="loading" v-if="user.perm.rename" />
<delete-button :disabled="loading" v-if="user.perm.delete" />
<download-button :disabled="loading" v-if="user.perm.download" />
<info-button :disabled="loading" />
</template>
</header-bar>
<div class="loading" v-if="loading">
<div class="spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
</div>
<template v-if="!loading">
<div class="preview">
<ExtendedImage v-if="req.type == 'image'" :src="raw"></ExtendedImage>
<audio v-else-if="req.type == 'audio'" :src="raw" controls></audio>
<video v-else-if="req.type == 'video'" :src="raw" controls>
<track
kind="captions"
v-for="(sub, index) in subtitles"
:key="index"
:src="sub"
:label="'Subtitle ' + index" :default="index === 0">
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.toLowerCase() == '.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>
</div>
</template>
<button @click="prev" @mouseover="hoverNav = true" @mouseleave="hoverNav = false" :class="{ hidden: !hasPrevious || !showNav }" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')">
<i class="material-icons">chevron_left</i>
</button>
<button @click="next" @mouseover="hoverNav = true" @mouseleave="hoverNav = false" :class="{ hidden: !hasNext || !showNav }" :aria-label="$t('buttons.next')" :title="$t('buttons.next')">
<i class="material-icons">chevron_right</i>
</button>
</div>
</template>
<script>
import { mapState } from 'vuex'
import { files as api } from '@/api'
import { baseURL, resizePreview } from '@/utils/constants'
import url from '@/utils/url'
import throttle from 'lodash.throttle'
import HeaderBar from '@/components/header/HeaderBar'
import Action from '@/components/header/Action'
import PreviewSizeButton from '@/components/buttons/PreviewSize'
import InfoButton from '@/components/buttons/Info'
import DeleteButton from '@/components/buttons/Delete'
import RenameButton from '@/components/buttons/Rename'
import DownloadButton from '@/components/buttons/Download'
import ExtendedImage from './ExtendedImage'
const mediaTypes = [
"image",
"video",
"audio",
"blob"
]
export default {
name: 'preview',
components: {
HeaderBar,
Action,
PreviewSizeButton,
InfoButton,
DeleteButton,
RenameButton,
DownloadButton,
ExtendedImage
},
data: function () {
return {
previousLink: '',
nextLink: '',
listing: null,
name: '',
subtitles: [],
fullSize: false,
showNav: true,
navTimeout: null,
hoverNav: false
}
},
computed: {
...mapState(['req', 'user', 'oldReq', 'jwt', 'loading', 'show']),
hasPrevious () {
return (this.previousLink !== '')
},
hasNext () {
return (this.nextLink !== '')
},
download () {
return `${baseURL}/api/raw${url.encodePath(this.req.path)}?auth=${this.jwt}`
},
previewUrl () {
if (this.req.type === 'image' && !this.fullSize) {
return `${baseURL}/api/preview/big${url.encodePath(this.req.path)}?auth=${this.jwt}`
}
return `${baseURL}/api/raw${url.encodePath(this.req.path)}?auth=${this.jwt}`
},
raw () {
return `${this.previewUrl}&inline=true`
},
showMore () {
return this.$store.state.show === 'more'
},
isResizeEnabled () {
return resizePreview
}
},
watch: {
$route: function () {
this.updatePreview()
this.toggleNavigation()
}
},
async mounted () {
window.addEventListener('keydown', this.key)
this.$store.commit('setPreviewMode', true)
this.listing = this.oldReq.items
this.$root.$on('preview-deleted', this.deleted)
this.updatePreview()
},
beforeDestroy () {
window.removeEventListener('keydown', this.key)
this.$store.commit('setPreviewMode', false)
this.$root.$off('preview-deleted', this.deleted)
},
methods: {
deleted () {
this.listing = this.listing.filter(item => item.name !== this.name)
if (this.hasNext) {
this.next()
} else if (!this.hasPrevious && !this.hasNext) {
this.back()
} else {
this.prev()
}
},
back () {
this.$store.commit('setPreviewMode', false)
let uri = url.removeLastDir(this.$route.path) + '/'
this.$router.push({ path: uri })
},
prev () {
this.hoverNav = false
this.$router.push({ path: this.previousLink })
},
next () {
this.hoverNav = false
this.$router.push({ path: this.nextLink })
},
key (event) {
if (this.show !== null) {
return
}
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()
} else if (event.which === 27) { // esc
this.back()
}
},
async updatePreview () {
if (this.req.subtitles) {
this.subtitles = this.req.subtitles.map(sub => `${baseURL}/api/raw${sub}?auth=${this.jwt}&inline=true`)
}
let dirs = this.$route.fullPath.split("/")
this.name = decodeURIComponent(dirs[dirs.length - 1])
if (!this.listing) {
try {
const path = url.removeLastDir(this.$route.path)
const res = await api.fetch(path)
this.listing = res.items
} catch (e) {
this.$showError(e)
}
}
this.previousLink = ''
this.nextLink = ''
for (let i = 0; i < this.listing.length; i++) {
if (this.listing[i].name !== this.name) {
continue
}
for (let j = i - 1; j >= 0; j--) {
if (mediaTypes.includes(this.listing[j].type)) {
this.previousLink = this.listing[j].url
break
}
}
for (let j = i + 1; j < this.listing.length; j++) {
if (mediaTypes.includes(this.listing[j].type)) {
this.nextLink = this.listing[j].url
break
}
}
return
}
},
openMore () {
this.$store.commit('showHover', 'more')
},
resetPrompts () {
this.$store.commit('closeHovers')
},
toggleSize () {
this.fullSize = !this.fullSize
},
toggleNavigation: throttle(function() {
this.showNav = true
if (this.navTimeout) {
clearTimeout(this.navTimeout)
}
this.navTimeout = setTimeout(() => {
this.showNav = false || this.hoverNav
this.navTimeout = null
}, 1500);
}, 500),
close () {
let uri = url.removeLastDir(this.$route.path) + '/'
this.$router.push({ path: uri })
}
}
}
</script>