mirror of
https://github.com/filebrowser/filebrowser.git
synced 2024-06-07 23:00:43 +00:00
Merge pull request #1307 from ramiresviana/tweaks-1
Frontend code quality changes
This commit is contained in:
commit
0fe34ad224
@ -69,13 +69,16 @@ nav > div {
|
||||
border-color: var(--divider);
|
||||
}
|
||||
|
||||
#breadcrumbs {
|
||||
.breadcrumbs {
|
||||
border-color: var(--divider);
|
||||
color: var(--textPrimary) !important;
|
||||
}
|
||||
#breadcrumbs span {
|
||||
.breadcrumbs span {
|
||||
color: var(--textPrimary) !important;
|
||||
}
|
||||
.breadcrumbs a:hover {
|
||||
background-color: rgba(255, 255, 255, .1);
|
||||
}
|
||||
|
||||
#listing .item {
|
||||
background: var(--surfacePrimary);
|
||||
@ -114,13 +117,20 @@ nav > div {
|
||||
background: var(--surfaceSecondary);
|
||||
}
|
||||
|
||||
.dashboard #nav ul li {
|
||||
color: var(--textSecondary);
|
||||
}
|
||||
.dashboard #nav ul li:hover {
|
||||
background: var(--surfaceSecondary);
|
||||
}
|
||||
|
||||
.card h3,
|
||||
.dashboard #nav,
|
||||
.dashboard p label {
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
.card#share ul li input,
|
||||
.card#share ul li select,
|
||||
.card#share input,
|
||||
.card#share select,
|
||||
.input {
|
||||
background: var(--surfaceSecondary);
|
||||
color: var(--textPrimary);
|
||||
@ -138,7 +148,7 @@ nav > div {
|
||||
background: #147A41;
|
||||
}
|
||||
|
||||
.dashboard #nav li,
|
||||
.dashboard #nav .wrapper,
|
||||
.collapsible {
|
||||
border-color: var(--divider);
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ export async function put (url, content = '') {
|
||||
}
|
||||
|
||||
export function download (format, ...files) {
|
||||
let url = store.getters['isSharing'] ? `${baseURL}/api/public/dl/${store.state.hash}` : `${baseURL}/api/raw`
|
||||
let url = `${baseURL}/api/raw`
|
||||
|
||||
if (files.length === 1) {
|
||||
url += removePrefix(files[0]) + '?'
|
||||
@ -74,15 +74,13 @@ export function download (format, ...files) {
|
||||
url += `/?files=${arg}&`
|
||||
}
|
||||
|
||||
if (format !== null) {
|
||||
if (format) {
|
||||
url += `algo=${format}&`
|
||||
}
|
||||
if (store.state.jwt !== ''){
|
||||
|
||||
if (store.state.jwt){
|
||||
url += `auth=${store.state.jwt}&`
|
||||
}
|
||||
if (store.state.token !== ''){
|
||||
url += `token=${store.state.token}`
|
||||
}
|
||||
|
||||
window.open(url)
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import * as files from './files'
|
||||
import * as share from './share'
|
||||
import * as users from './users'
|
||||
import * as settings from './settings'
|
||||
import * as pub from './pub'
|
||||
import search from './search'
|
||||
import commands from './commands'
|
||||
|
||||
@ -10,6 +11,7 @@ export {
|
||||
share,
|
||||
users,
|
||||
settings,
|
||||
pub,
|
||||
commands,
|
||||
search
|
||||
}
|
||||
|
37
frontend/src/api/pub.js
Normal file
37
frontend/src/api/pub.js
Normal file
@ -0,0 +1,37 @@
|
||||
import { fetchJSON, removePrefix } from './utils'
|
||||
import { baseURL } from '@/utils/constants'
|
||||
|
||||
export async function fetch(hash, password = "") {
|
||||
return fetchJSON(`/api/public/share/${hash}`, {
|
||||
headers: {'X-SHARE-PASSWORD': password},
|
||||
})
|
||||
}
|
||||
|
||||
export function download(format, hash, token, ...files) {
|
||||
let url = `${baseURL}/api/public/dl/${hash}`
|
||||
|
||||
const prefix = `/share/${hash}`
|
||||
if (files.length === 1) {
|
||||
url += removePrefix(files[0], prefix) + '?'
|
||||
} else {
|
||||
let arg = ''
|
||||
|
||||
for (let file of files) {
|
||||
arg += removePrefix(file, prefix) + ','
|
||||
}
|
||||
|
||||
arg = arg.substring(0, arg.length - 1)
|
||||
arg = encodeURIComponent(arg)
|
||||
url += `/?files=${arg}&`
|
||||
}
|
||||
|
||||
if (format) {
|
||||
url += `algo=${format}&`
|
||||
}
|
||||
|
||||
if (token) {
|
||||
url += `token=${token}&`
|
||||
}
|
||||
|
||||
window.open(url)
|
||||
}
|
@ -4,12 +4,6 @@ export async function list() {
|
||||
return fetchJSON('/api/shares')
|
||||
}
|
||||
|
||||
export async function getHash(hash, password = "") {
|
||||
return fetchJSON(`/api/public/share/${hash}`, {
|
||||
headers: {'X-SHARE-PASSWORD': password},
|
||||
})
|
||||
}
|
||||
|
||||
export async function get(url) {
|
||||
url = removePrefix(url)
|
||||
return fetchJSON(`/api/share${url}`)
|
||||
|
@ -33,11 +33,11 @@ export async function fetchJSON (url, opts) {
|
||||
}
|
||||
}
|
||||
|
||||
export function removePrefix (url) {
|
||||
export function removePrefix (url, prefix) {
|
||||
if (url.startsWith('/files')) {
|
||||
url = url.slice(6)
|
||||
} else if (store.getters['isSharing']) {
|
||||
url = url.slice(7 + store.state.hash.length)
|
||||
} else if (prefix) {
|
||||
url = url.replace(prefix, '')
|
||||
}
|
||||
|
||||
if (url === '') url = '/'
|
||||
|
67
frontend/src/components/Breadcrumbs.vue
Normal file
67
frontend/src/components/Breadcrumbs.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="breadcrumbs">
|
||||
<component :is="element" :to="base || ''" :aria-label="$t('files.home')" :title="$t('files.home')">
|
||||
<i class="material-icons">home</i>
|
||||
</component>
|
||||
|
||||
<span v-for="(link, index) in items" :key="index">
|
||||
<span class="chevron"><i class="material-icons">keyboard_arrow_right</i></span>
|
||||
<component :is="element" :to="link.url">{{ link.name }}</component>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'breadcrumbs',
|
||||
props: [
|
||||
'base',
|
||||
'noLink'
|
||||
],
|
||||
computed: {
|
||||
items () {
|
||||
const relativePath = this.$route.path.replace(this.base, '')
|
||||
let parts = relativePath.split('/')
|
||||
|
||||
if (parts[0] === '') {
|
||||
parts.shift()
|
||||
}
|
||||
|
||||
if (parts[parts.length - 1] === '') {
|
||||
parts.pop()
|
||||
}
|
||||
|
||||
let breadcrumbs = []
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (i === 0) {
|
||||
breadcrumbs.push({ name: decodeURIComponent(parts[i]), url: this.base + '/' + parts[i] + '/' })
|
||||
} else {
|
||||
breadcrumbs.push({ name: decodeURIComponent(parts[i]), url: breadcrumbs[i - 1].url + parts[i] + '/' })
|
||||
}
|
||||
}
|
||||
|
||||
if (breadcrumbs.length > 3) {
|
||||
while (breadcrumbs.length !== 4) {
|
||||
breadcrumbs.shift()
|
||||
}
|
||||
|
||||
breadcrumbs[0].name = '...'
|
||||
}
|
||||
|
||||
return breadcrumbs
|
||||
},
|
||||
element () {
|
||||
if (this.noLink !== undefined) {
|
||||
return 'span'
|
||||
}
|
||||
|
||||
return 'router-link'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
@ -1,185 +0,0 @@
|
||||
<template>
|
||||
<header v-if="!isEditor && !isPreview">
|
||||
<div>
|
||||
<button @click="openSidebar" :aria-label="$t('buttons.toggleSidebar')" :title="$t('buttons.toggleSidebar')" class="action">
|
||||
<i class="material-icons">menu</i>
|
||||
</button>
|
||||
<img :src="logoURL" alt="File Browser">
|
||||
<search v-if="isLogged"></search>
|
||||
</div>
|
||||
<div>
|
||||
<template v-if="isLogged || isSharing">
|
||||
<button v-show="!isSharing" @click="openSearch" :aria-label="$t('buttons.search')" :title="$t('buttons.search')" class="search-button action">
|
||||
<i class="material-icons">search</i>
|
||||
</button>
|
||||
|
||||
<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 && isListing && !isSharing">
|
||||
<span v-if="selectedCount > 0">{{ selectedCount }} selected</span>
|
||||
<share-button v-show="showShareButton"></share-button>
|
||||
<rename-button v-show="showRenameButton"></rename-button>
|
||||
<copy-button v-show="showCopyButton"></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="showShareButton"></share-button>
|
||||
<rename-button v-show="showRenameButton"></rename-button>
|
||||
<copy-button v-show="showCopyButton"></copy-button>
|
||||
<move-button v-show="showMoveButton"></move-button>
|
||||
<delete-button v-show="showDeleteButton"></delete-button>
|
||||
</div>
|
||||
|
||||
<shell-button v-if="isExecEnabled && !isSharing && user.perm.execute" />
|
||||
<switch-button v-show="isListing"></switch-button>
|
||||
<download-button v-show="showDownloadButton"></download-button>
|
||||
<upload-button v-show="showUpload"></upload-button>
|
||||
<info-button v-show="isFiles"></info-button>
|
||||
|
||||
<button v-show="isListing || (isSharing && req.isDir)" @click="toggleMultipleSelection" :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>
|
||||
</template>
|
||||
|
||||
<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 ShareButton from './buttons/Share'
|
||||
import ShellButton from './buttons/Shell'
|
||||
import {mapGetters, mapState} from 'vuex'
|
||||
import { logoURL, enableExec } from '@/utils/constants'
|
||||
import * as api from '@/api'
|
||||
import buttons from '@/utils/buttons'
|
||||
|
||||
export default {
|
||||
name: 'header-layout',
|
||||
components: {
|
||||
Search,
|
||||
InfoButton,
|
||||
DeleteButton,
|
||||
ShareButton,
|
||||
RenameButton,
|
||||
DownloadButton,
|
||||
CopyButton,
|
||||
UploadButton,
|
||||
SwitchButton,
|
||||
MoveButton,
|
||||
ShellButton
|
||||
},
|
||||
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',
|
||||
'isFiles',
|
||||
'isEditor',
|
||||
'isPreview',
|
||||
'isListing',
|
||||
'isLogged',
|
||||
'isSharing'
|
||||
]),
|
||||
...mapState([
|
||||
'req',
|
||||
'user',
|
||||
'loading',
|
||||
'reload',
|
||||
'multiple'
|
||||
]),
|
||||
logoURL: () => logoURL,
|
||||
isExecEnabled: () => enableExec,
|
||||
isMobile () {
|
||||
return this.width <= 736
|
||||
},
|
||||
showUpload () {
|
||||
return this.isListing && this.user.perm.create
|
||||
},
|
||||
showDownloadButton () {
|
||||
return (this.isFiles && this.user.perm.download) || (this.isSharing && this.selectedCount > 0)
|
||||
},
|
||||
showDeleteButton () {
|
||||
return this.isFiles && (this.isListing
|
||||
? (this.selectedCount !== 0 && this.user.perm.delete)
|
||||
: this.user.perm.delete)
|
||||
},
|
||||
showRenameButton () {
|
||||
return this.isFiles && (this.isListing
|
||||
? (this.selectedCount === 1 && this.user.perm.rename)
|
||||
: this.user.perm.rename)
|
||||
},
|
||||
showShareButton () {
|
||||
return this.isFiles && (this.isListing
|
||||
? (this.selectedCount === 1 && this.user.perm.share)
|
||||
: this.user.perm.share)
|
||||
},
|
||||
showMoveButton () {
|
||||
return this.isFiles && (this.isListing
|
||||
? (this.selectedCount > 0 && this.user.perm.rename)
|
||||
: this.user.perm.rename)
|
||||
},
|
||||
showCopyButton () {
|
||||
return this.isFiles && (this.isListing
|
||||
? (this.selectedCount > 0 && this.user.perm.create)
|
||||
: this.user.perm.create)
|
||||
},
|
||||
showMore () {
|
||||
return (this.isFiles || this.isSharing) && this.$store.state.show === 'more'
|
||||
},
|
||||
showOverlay () {
|
||||
return this.showMore
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openSidebar () {
|
||||
this.$store.commit('showHover', 'sidebar')
|
||||
},
|
||||
openMore () {
|
||||
this.$store.commit('showHover', 'more')
|
||||
},
|
||||
openSearch () {
|
||||
this.$store.commit('showHover', 'search')
|
||||
},
|
||||
toggleMultipleSelection () {
|
||||
this.$store.commit('multiple', !this.multiple)
|
||||
this.resetPrompts()
|
||||
},
|
||||
resetPrompts () {
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,17 +0,0 @@
|
||||
<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 () {
|
||||
this.$store.commit('showHover', 'copy')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,17 +0,0 @@
|
||||
<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 () {
|
||||
this.$store.commit('showHover', 'delete')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,35 +0,0 @@
|
||||
<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 { files as api } from '@/api'
|
||||
|
||||
export default {
|
||||
name: 'download-button',
|
||||
computed: {
|
||||
...mapState(['req', 'selected']),
|
||||
...mapGetters(['isListing', 'selectedCount', 'isSharing'])
|
||||
},
|
||||
methods: {
|
||||
download: function () {
|
||||
if (!this.isListing && !this.isSharing) {
|
||||
api.download(null, this.$route.path)
|
||||
return
|
||||
}
|
||||
|
||||
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', 'download')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,17 +0,0 @@
|
||||
<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 () {
|
||||
this.$store.commit('showHover', 'info')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,17 +0,0 @@
|
||||
<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 () {
|
||||
this.$store.commit('showHover', 'move')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,22 +0,0 @@
|
||||
<template>
|
||||
<button :title="$t('buttons.info')" :aria-label="$t('buttons.info')" class="action" @click="$emit('change-size')">
|
||||
<i class="material-icons">{{ this.icon }}</i>
|
||||
<span>{{ $t('buttons.info') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'preview-size-button',
|
||||
props: [ 'size' ],
|
||||
computed: {
|
||||
icon () {
|
||||
if (this.size) {
|
||||
return 'photo_size_select_large'
|
||||
}
|
||||
|
||||
return 'hd'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,17 +0,0 @@
|
||||
<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 () {
|
||||
this.$store.commit('showHover', 'rename')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,17 +0,0 @@
|
||||
<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 () {
|
||||
this.$store.commit('showHover', 'share')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,17 +0,0 @@
|
||||
<template>
|
||||
<button @click="show" :aria-label="$t('buttons.shell')" :title="$t('buttons.shell')" class="action">
|
||||
<i class="material-icons">code</i>
|
||||
<span>{{ $t('buttons.shell') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'shell-button',
|
||||
methods: {
|
||||
show: function () {
|
||||
this.$store.commit('toggleShell')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,40 +0,0 @@
|
||||
<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 { users as api } from '@/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', 'closeHovers' ]),
|
||||
change: async function () {
|
||||
this.closeHovers()
|
||||
|
||||
const data = {
|
||||
id: this.user.id,
|
||||
viewMode: (this.icon === 'view_list') ? 'list' : 'mosaic'
|
||||
}
|
||||
|
||||
try {
|
||||
await api.update(data, ['viewMode'])
|
||||
this.updateUser(data)
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,21 +0,0 @@
|
||||
<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 () {
|
||||
if (typeof(DataTransferItem.prototype.webkitGetAsEntry) !== 'undefined') {
|
||||
this.$store.commit('showHover', 'upload')
|
||||
} else {
|
||||
document.getElementById('upload-input').click();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -13,7 +13,7 @@
|
||||
:aria-label="name"
|
||||
:aria-selected="isSelected">
|
||||
<div>
|
||||
<img v-if="type==='image' && isThumbsEnabled && !isSharing" v-lazy="thumbnailUrl">
|
||||
<img v-if="readOnly == undefined && type==='image' && isThumbsEnabled" v-lazy="thumbnailUrl">
|
||||
<i v-else class="material-icons">{{ icon }}</i>
|
||||
</div>
|
||||
|
||||
@ -45,13 +45,12 @@ export default {
|
||||
touches: 0
|
||||
}
|
||||
},
|
||||
props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index'],
|
||||
props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index', 'readOnly'],
|
||||
computed: {
|
||||
...mapState(['user', 'selected', 'req', 'jwt']),
|
||||
...mapGetters(['selectedCount', 'isSharing']),
|
||||
...mapGetters(['selectedCount']),
|
||||
singleClick () {
|
||||
if (this.isSharing) return false
|
||||
return this.user.singleClick
|
||||
return this.readOnly == undefined && this.user.singleClick
|
||||
},
|
||||
isSelected () {
|
||||
return (this.selected.indexOf(this.index) !== -1)
|
||||
@ -64,10 +63,10 @@ export default {
|
||||
return 'insert_drive_file'
|
||||
},
|
||||
isDraggable () {
|
||||
return !this.isSharing && this.user.perm.rename
|
||||
return this.readOnly == undefined && this.user.perm.rename
|
||||
},
|
||||
canDrop () {
|
||||
if (!this.isDir || this.isSharing) return false
|
||||
if (!this.isDir || this.readOnly == undefined) return false
|
||||
|
||||
for (let i of this.selected) {
|
||||
if (this.req.items[i].url === this.url) {
|
||||
@ -139,7 +138,7 @@ export default {
|
||||
to: this.url + this.req.items[i].name,
|
||||
name: this.req.items[i].name
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let base = el.querySelector('.name').innerHTML + '/'
|
||||
let path = this.$route.path + base
|
||||
|
32
frontend/src/components/header/Action.vue
Normal file
32
frontend/src/components/header/Action.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<button @click="action" :aria-label="label" :title="label" class="action">
|
||||
<i class="material-icons">{{ icon }}</i>
|
||||
<span>{{ label }}</span>
|
||||
<span v-if="counter > 0" class="counter">{{ counter }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'action',
|
||||
props: [
|
||||
'icon',
|
||||
'label',
|
||||
'counter',
|
||||
'show'
|
||||
],
|
||||
methods: {
|
||||
action: function () {
|
||||
if (this.show) {
|
||||
this.$store.commit('showHover', this.show)
|
||||
}
|
||||
|
||||
this.$emit('action')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
47
frontend/src/components/header/HeaderBar.vue
Normal file
47
frontend/src/components/header/HeaderBar.vue
Normal file
@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<header>
|
||||
<img v-if="showLogo !== undefined" :src="logoURL" />
|
||||
<action v-if="showMenu !== undefined" class="menu-button" icon="menu" :label="$t('buttons.toggleSidebar')" @action="openSidebar()" />
|
||||
|
||||
<slot />
|
||||
|
||||
<div id="dropdown" :class="{ active: this.$store.state.show === 'more' }">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
|
||||
<action v-if="this.$slots.actions" id="more" icon="more_vert" :label="$t('buttons.more')" @action="$store.commit('showHover', 'more')" />
|
||||
|
||||
<div class="overlay" v-show="this.$store.state.show == 'more'" @click="$store.commit('closeHovers')" />
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { logoURL } from '@/utils/constants'
|
||||
|
||||
import Action from '@/components/header/Action'
|
||||
|
||||
export default {
|
||||
name: 'header-bar',
|
||||
props: [
|
||||
'showLogo',
|
||||
'showMenu',
|
||||
],
|
||||
components: {
|
||||
Action
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
logoURL
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openSidebar () {
|
||||
this.$store.commit('showHover', 'sidebar')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
@ -26,7 +26,7 @@ export default {
|
||||
name: 'delete',
|
||||
computed: {
|
||||
...mapGetters(['isListing', 'selectedCount']),
|
||||
...mapState(['req', 'selected'])
|
||||
...mapState(['req', 'selected', 'showConfirm'])
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(['closeHovers']),
|
||||
@ -38,7 +38,7 @@ export default {
|
||||
await api.remove(this.$route.path)
|
||||
buttons.success('delete')
|
||||
|
||||
this.$root.$emit('preview-deleted')
|
||||
this.showConfirm()
|
||||
this.closeHovers()
|
||||
return
|
||||
}
|
||||
|
@ -7,43 +7,29 @@
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.downloadMessage') }}</p>
|
||||
|
||||
<button class="button button--block" @click="download('zip')" v-focus>zip</button>
|
||||
<button class="button button--block" @click="download('tar')" v-focus>tar</button>
|
||||
<button class="button button--block" @click="download('targz')" v-focus>tar.gz</button>
|
||||
<button class="button button--block" @click="download('tarbz2')" v-focus>tar.bz2</button>
|
||||
<button class="button button--block" @click="download('tarxz')" v-focus>tar.xz</button>
|
||||
<button class="button button--block" @click="download('tarlz4')" v-focus>tar.lz4</button>
|
||||
<button class="button button--block" @click="download('tarsz')" v-focus>tar.sz</button>
|
||||
<button v-for="(ext, format) in formats" :key="format" class="button button--block" @click="showConfirm(format)" v-focus>{{ ext }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapGetters, mapState} from 'vuex'
|
||||
import { files as api } from '@/api'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
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)
|
||||
data: function () {
|
||||
return {
|
||||
formats: {
|
||||
zip: 'zip',
|
||||
tar: 'tar',
|
||||
targz: 'tar.gz',
|
||||
tarbz2: 'tar.bz2',
|
||||
tarxz: 'tar.xz',
|
||||
tarlz4: 'tar.lz4',
|
||||
tarsz: 'tar.sz'
|
||||
}
|
||||
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: mapState(['showConfirm'])
|
||||
}
|
||||
</script>
|
||||
|
@ -4,61 +4,82 @@
|
||||
<h2>{{ $t('buttons.share') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<ul>
|
||||
<template v-if="listing">
|
||||
<div class="card-content">
|
||||
<table>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>{{ $t('settings.shareDuration') }}</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
||||
<li v-for="link in links" :key="link.hash">
|
||||
<a :href="buildLink(link.hash)" target="_blank">
|
||||
<template v-if="link.expire !== 0">{{ humanTime(link.expire) }}</template>
|
||||
<template v-else>{{ $t('permanent') }}</template>
|
||||
</a>
|
||||
<tr v-for="link in links" :key="link.hash">
|
||||
<td>{{ link.hash }}</td>
|
||||
<td>
|
||||
<template v-if="link.expire !== 0">{{ humanTime(link.expire) }}</template>
|
||||
<template v-else>{{ $t('permanent') }}</template>
|
||||
</td>
|
||||
<td class="small">
|
||||
<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>
|
||||
</td>
|
||||
<td class="small">
|
||||
<button class="action"
|
||||
@click="deleteLink($event, link)"
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')"><i class="material-icons">delete</i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<button class="action"
|
||||
@click="deleteLink($event, link)"
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')"><i class="material-icons">delete</i></button>
|
||||
<div class="card-action">
|
||||
<button class="button button--flat button--grey"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.close')"
|
||||
:title="$t('buttons.close')">{{ $t('buttons.close') }}</button>
|
||||
<button class="button button--flat button--blue"
|
||||
@click="() => switchListing()"
|
||||
:aria-label="$t('buttons.new')"
|
||||
:title="$t('buttons.new')">{{ $t('buttons.new') }}</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
<template v-else>
|
||||
<div class="card-content">
|
||||
<p>{{ $t('settings.shareDuration') }}</p>
|
||||
<div class="input-group input">
|
||||
<input v-focus
|
||||
type="number"
|
||||
max="2147483647"
|
||||
min="1"
|
||||
@keyup.enter="submit"
|
||||
v-model.trim="time">
|
||||
<select class="right" 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>
|
||||
</div>
|
||||
<p>{{ $t('prompts.optionalPassword') }}</p>
|
||||
<input class="input input--block" type="password" v-model.trim="password">
|
||||
</div>
|
||||
|
||||
<li v-if="!hasPermanent">
|
||||
<div>
|
||||
<input type="password" :placeholder="$t('prompts.optionalPassword')" v-model="passwordPermalink">
|
||||
<a @click="getPermalink" :aria-label="$t('buttons.permalink')">{{ $t('buttons.permalink') }}</a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<input v-focus
|
||||
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>
|
||||
<input type="password" :placeholder="$t('prompts.optionalPassword')" v-model="password">
|
||||
<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="button button--flat"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.close')"
|
||||
:title="$t('buttons.close')">{{ $t('buttons.close') }}</button>
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<button class="button button--flat button--grey"
|
||||
@click="() => switchListing()"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="button button--flat button--blue"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.share')"
|
||||
:title="$t('buttons.share')">{{ $t('buttons.share') }}</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -75,11 +96,10 @@ export default {
|
||||
return {
|
||||
time: '',
|
||||
unit: 'hours',
|
||||
hasPermanent: false,
|
||||
links: [],
|
||||
clip: null,
|
||||
password: '',
|
||||
passwordPermalink: ''
|
||||
listing: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -104,11 +124,8 @@ export default {
|
||||
this.links = links
|
||||
this.sort()
|
||||
|
||||
for (let link of this.links) {
|
||||
if (link.expire === 0) {
|
||||
this.hasPermanent = true
|
||||
break
|
||||
}
|
||||
if (this.links.length == 0) {
|
||||
this.listing = false
|
||||
}
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
@ -125,22 +142,25 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
submit: async function () {
|
||||
if (!this.time) return
|
||||
let isPermanent = !this.time || this.time == 0
|
||||
|
||||
try {
|
||||
const res = await api.create(this.url, this.password, this.time, this.unit)
|
||||
let res = null
|
||||
|
||||
if (isPermanent) {
|
||||
res = await api.create(this.url, this.password)
|
||||
} else {
|
||||
res = await api.create(this.url, this.password, this.time, this.unit)
|
||||
}
|
||||
|
||||
this.links.push(res)
|
||||
this.sort()
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
}
|
||||
},
|
||||
getPermalink: async function () {
|
||||
try {
|
||||
const res = await api.create(this.url, this.passwordPermalink)
|
||||
this.links.push(res)
|
||||
this.sort()
|
||||
this.hasPermanent = true
|
||||
|
||||
this.time = ''
|
||||
this.unit = 'hours'
|
||||
this.password = ''
|
||||
|
||||
this.listing = true
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
}
|
||||
@ -149,8 +169,11 @@ export default {
|
||||
event.preventDefault()
|
||||
try {
|
||||
await api.remove(link.hash)
|
||||
if (link.expire === 0) this.hasPermanent = false
|
||||
this.links = this.links.filter(item => item.hash !== link.hash)
|
||||
|
||||
if (this.links.length == 0) {
|
||||
this.listing = false
|
||||
}
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
}
|
||||
@ -167,6 +190,13 @@ export default {
|
||||
if (b.expire === 0) return 1
|
||||
return new Date(a.expire) - new Date(b.expire)
|
||||
})
|
||||
},
|
||||
switchListing () {
|
||||
if (this.links.length == 0 && !this.listing) {
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
|
||||
this.listing = !this.listing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.deleteMessageShare', {path: hash.path}) }}</p>
|
||||
<p>{{ $t('prompts.deleteMessageShare', {path: ''}) }}</p>
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<button @click="$store.commit('closeHovers')"
|
||||
@ -17,30 +17,16 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapMutations, mapState} from 'vuex'
|
||||
import { share as api } from '@/api'
|
||||
import buttons from '@/utils/buttons'
|
||||
import {mapState} from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'share-delete',
|
||||
computed: {
|
||||
...mapState(['hash'])
|
||||
...mapState(['showConfirm'])
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(['closeHovers']),
|
||||
submit: async function () {
|
||||
buttons.loading('delete')
|
||||
|
||||
try {
|
||||
await api.remove(this.hash.hash)
|
||||
buttons.success('delete')
|
||||
|
||||
this.$root.$emit('share-deleted', this.hash.hash)
|
||||
this.closeHovers()
|
||||
} catch (e) {
|
||||
buttons.done('delete')
|
||||
this.$showError(e)
|
||||
}
|
||||
submit: function () {
|
||||
this.showConfirm()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -70,9 +70,4 @@
|
||||
padding: .5em;
|
||||
text-align: center;
|
||||
animation: .2s opac forwards;
|
||||
}
|
||||
|
||||
.share__promt__card {
|
||||
max-width: max-content !important;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
@ -83,29 +83,29 @@ main {
|
||||
width: calc(100% - 19em);
|
||||
}
|
||||
|
||||
#breadcrumbs {
|
||||
.breadcrumbs {
|
||||
height: 3em;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
#breadcrumbs span,
|
||||
#breadcrumbs {
|
||||
.breadcrumbs span,
|
||||
.breadcrumbs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #6f6f6f;
|
||||
}
|
||||
|
||||
#breadcrumbs a {
|
||||
.breadcrumbs a {
|
||||
color: inherit;
|
||||
transition: .1s ease-in;
|
||||
border-radius: .125em;
|
||||
}
|
||||
|
||||
#breadcrumbs a:hover {
|
||||
.breadcrumbs a:hover {
|
||||
background-color: rgba(0,0,0, 0.05);
|
||||
}
|
||||
|
||||
#breadcrumbs span a {
|
||||
.breadcrumbs span a {
|
||||
padding: .2em;
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,29 @@
|
||||
.dashboard {
|
||||
max-width: 600px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.dashboard .row {
|
||||
display: flex;
|
||||
margin: 0 -.5em;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dashboard .row .column {
|
||||
display: flex;
|
||||
padding: 0 .5em;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.dashboard .row .column .card {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@media(max-width: 1200px) {
|
||||
.dashboard .row .column {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit
|
||||
}
|
||||
@ -28,25 +49,56 @@ p code {
|
||||
}
|
||||
|
||||
.dashboard #nav {
|
||||
display: flex;
|
||||
padding-bottom: 1em;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.dashboard #nav .wrapper {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
border-bottom: 2px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.dashboard #nav ul {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
color: rgb(84, 110, 122);
|
||||
font-weight: 500;
|
||||
margin: 0 0 1em;
|
||||
padding: 0;
|
||||
margin: 0 0 -2px 0;
|
||||
font-size: .8em;
|
||||
text-align: center;
|
||||
justify-content: space-between;
|
||||
padding: 0;
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.dashboard #nav li {
|
||||
.dashboard #nav ul li {
|
||||
position: relative;
|
||||
padding: 1.5em 2em;
|
||||
white-space: nowrap;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: .1s ease-in-out all;
|
||||
|
||||
}
|
||||
|
||||
.dashboard #nav ul li:hover {
|
||||
background: var(--moon-grey);
|
||||
}
|
||||
|
||||
.dashboard #nav ul li.active {
|
||||
border-color: var(--blue);
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.dashboard #nav ul li.active::before {
|
||||
width: 100%;
|
||||
padding: 0 0 1em;
|
||||
border-bottom: 2px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.dashboard #nav li.active {
|
||||
border-color: var(--blue)
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
content: "";
|
||||
background: var(--blue);
|
||||
opacity: 0.08;
|
||||
}
|
||||
|
||||
.dashboard #nav i {
|
||||
@ -92,7 +144,7 @@ table tr>*:last-child {
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
margin: .5rem 0 1rem 0;
|
||||
margin: 0 0 1rem 0;
|
||||
background-color: #fff;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
|
||||
@ -151,6 +203,7 @@ table tr>*:last-child {
|
||||
|
||||
.card .card-content.full {
|
||||
padding-bottom: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
@ -226,6 +279,18 @@ table tr>*:last-child {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.card#share .input-group {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.card#share .input-group * {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.card#share .input-group input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
position: fixed;
|
||||
|
@ -6,9 +6,25 @@ header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 4em;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
padding: 0.5em 0.5em 0.5em 1em;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header > * {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
header title {
|
||||
display: block;
|
||||
flex: 1 1 auto;
|
||||
padding: 0 1em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
header .overlay {
|
||||
@ -30,17 +46,6 @@ header img {
|
||||
height: 2.5em;
|
||||
}
|
||||
|
||||
header>div:first-child>.action {
|
||||
display: none;
|
||||
}
|
||||
|
||||
header>div {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 0.5em 0.5em 0.5em 1em;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header .action span {
|
||||
display: none;
|
||||
}
|
||||
@ -50,19 +55,8 @@ header>div div {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
header>div:last-child div {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
header>div:first-child {
|
||||
height: 4em;
|
||||
}
|
||||
|
||||
header>div:last-child {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
header .search-button {
|
||||
header .search-button,
|
||||
header .menu-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
@ -70,6 +70,7 @@
|
||||
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
|
||||
width: 95%;
|
||||
max-width: 20em;
|
||||
z-index: 1;
|
||||
}
|
||||
#file-selection .action {
|
||||
border-radius: 50%;
|
||||
@ -81,6 +82,9 @@
|
||||
color: #6f6f6f;
|
||||
margin-right: auto;
|
||||
}
|
||||
#file-selection .action span {
|
||||
display: none;
|
||||
}
|
||||
nav {
|
||||
top: 0;
|
||||
z-index: 99999;
|
||||
@ -95,7 +99,7 @@
|
||||
left: 0;
|
||||
}
|
||||
header .search-button,
|
||||
header>div:first-child>.action {
|
||||
header .menu-button {
|
||||
display: inherit;
|
||||
}
|
||||
header img {
|
||||
|
@ -96,10 +96,11 @@
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
font-size: .75em;
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
width: 1.8em;
|
||||
height: 1.8em;
|
||||
text-align: center;
|
||||
line-height: 1.25em;
|
||||
line-height: 1.55em;
|
||||
font-weight: bold;
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
@ -117,25 +118,8 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#previewer .bar {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
padding: 0.5em;
|
||||
height: 3.7em;
|
||||
}
|
||||
|
||||
#previewer .bar > * {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
#previewer .bar .title {
|
||||
display: block;
|
||||
flex: 1 1 auto;
|
||||
padding: 0 1em;
|
||||
line-height: 2.3em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 1.2em;
|
||||
#previewer header {
|
||||
background: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@ -152,10 +136,9 @@
|
||||
}
|
||||
|
||||
#previewer .preview {
|
||||
margin: 2em auto 4em;
|
||||
max-width: 80%;
|
||||
margin-top: 4em;
|
||||
text-align: center;
|
||||
height: calc(100vh - 9.7em);
|
||||
height: calc(100vh - 4em);
|
||||
}
|
||||
|
||||
#previewer .preview pre {
|
||||
@ -170,6 +153,10 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#previewer .preview video {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#previewer .pdf {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@ -182,8 +169,25 @@
|
||||
#previewer>button {
|
||||
margin: 0;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
top: calc(50% + 1.85em);
|
||||
transform: translateY(-50%);
|
||||
background-color: rgba(80, 80, 80, .5);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
transition: 0.2s ease all;
|
||||
}
|
||||
|
||||
#previewer>button.hidden {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#previewer>button>i {
|
||||
padding: 0.4em;
|
||||
}
|
||||
|
||||
#previewer>button:first-of-type {
|
||||
@ -199,6 +203,7 @@
|
||||
#editor-container {
|
||||
background-color: #fafafa;
|
||||
position: fixed;
|
||||
margin-top: 4em;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
@ -206,43 +211,28 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#editor-container .bar {
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
padding: 0.5em;
|
||||
height: 3.7em;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.075);
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
#editor-container .title {
|
||||
margin-right: auto;
|
||||
padding: 0 1em;
|
||||
line-height: 2.7em;
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
#previewer .loading {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#editor-container #editor {
|
||||
height: calc(100vh - 8.2em);
|
||||
height: calc(100vh - 8.4em);
|
||||
}
|
||||
|
||||
#editor-container #breadcrumbs {
|
||||
#editor-container .breadcrumbs {
|
||||
height: 2.3em;
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
#editor-container #breadcrumbs span {
|
||||
#editor-container .breadcrumbs span {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#editor-container .breadcrumbs i {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* * * * * * * * * * * * * * * *
|
||||
* PROMPT *
|
||||
* * * * * * * * * * * * * * * */
|
||||
|
@ -10,9 +10,7 @@ import Settings from '@/views/Settings'
|
||||
import GlobalSettings from '@/views/settings/Global'
|
||||
import ProfileSettings from '@/views/settings/Profile'
|
||||
import Shares from '@/views/settings/Shares'
|
||||
import Error403 from '@/views/errors/403'
|
||||
import Error404 from '@/views/errors/404'
|
||||
import Error500 from '@/views/errors/500'
|
||||
import Errors from '@/views/Errors'
|
||||
import store from '@/store'
|
||||
import { baseURL } from '@/utils/constants'
|
||||
|
||||
@ -102,17 +100,29 @@ const router = new Router({
|
||||
{
|
||||
path: '/403',
|
||||
name: 'Forbidden',
|
||||
component: Error403
|
||||
component: Errors,
|
||||
props: {
|
||||
errorCode: 403,
|
||||
showHeader: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
name: 'Not Found',
|
||||
component: Error404
|
||||
component: Errors,
|
||||
props: {
|
||||
errorCode: 404,
|
||||
showHeader: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/500',
|
||||
name: 'Internal Server Error',
|
||||
component: Error500
|
||||
component: Errors,
|
||||
props: {
|
||||
errorCode: 500,
|
||||
showHeader: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/files',
|
||||
|
@ -2,9 +2,6 @@ const getters = {
|
||||
isLogged: state => state.user !== null,
|
||||
isFiles: state => !state.loading && state.route.name === 'Files',
|
||||
isListing: (state, getters) => getters.isFiles && state.req.isDir,
|
||||
isEditor: (state, getters) => getters.isFiles && (state.req.type === 'text' || state.req.type === 'textImmutable'),
|
||||
isPreview: state => state.previewMode,
|
||||
isSharing: state => !state.loading && state.route.name === 'Share',
|
||||
selectedCount: state => state.selected.length,
|
||||
progress : state => {
|
||||
if (state.upload.progress.length == 0) {
|
||||
|
@ -22,11 +22,7 @@ const state = {
|
||||
multiple: false,
|
||||
show: null,
|
||||
showShell: false,
|
||||
showMessage: null,
|
||||
showConfirm: null,
|
||||
previewMode: false,
|
||||
hash: '',
|
||||
token: '',
|
||||
showConfirm: null
|
||||
}
|
||||
|
||||
export default new Vuex.Store({
|
||||
|
@ -4,7 +4,7 @@ import moment from 'moment'
|
||||
const mutations = {
|
||||
closeHovers: state => {
|
||||
state.show = null
|
||||
state.showMessage = null
|
||||
state.showConfirm = null
|
||||
},
|
||||
toggleShell: (state) => {
|
||||
state.showShell = !state.showShell
|
||||
@ -16,16 +16,13 @@ const mutations = {
|
||||
}
|
||||
|
||||
state.show = value.prompt
|
||||
state.showMessage = value.message
|
||||
state.showConfirm = value.confirm
|
||||
},
|
||||
showError: (state, value) => {
|
||||
showError: (state) => {
|
||||
state.show = 'error'
|
||||
state.showMessage = value
|
||||
},
|
||||
showSuccess: (state, value) => {
|
||||
showSuccess: (state) => {
|
||||
state.show = 'success'
|
||||
state.showMessage = value
|
||||
},
|
||||
setLoading: (state, value) => { state.loading = value },
|
||||
setReload: (state, value) => { state.reload = value },
|
||||
@ -46,12 +43,8 @@ const mutations = {
|
||||
state.user = value
|
||||
},
|
||||
setJWT: (state, value) => (state.jwt = value),
|
||||
setToken: (state, value ) => (state.token = value),
|
||||
multiple: (state, value) => (state.multiple = value),
|
||||
addSelected: (state, value) => (state.selected.push(value)),
|
||||
addPlugin: (state, value) => {
|
||||
state.plugins.push(value)
|
||||
},
|
||||
removeSelected: (state, value) => {
|
||||
let i = state.selected.indexOf(value)
|
||||
if (i === -1) return
|
||||
@ -84,11 +77,7 @@ const mutations = {
|
||||
resetClipboard: (state) => {
|
||||
state.clipboard.key = ''
|
||||
state.clipboard.items = []
|
||||
},
|
||||
setPreviewMode(state, value) {
|
||||
state.previewMode = value
|
||||
},
|
||||
setHash: (state, value) => (state.hash = value),
|
||||
}
|
||||
}
|
||||
|
||||
export default mutations
|
||||
|
46
frontend/src/views/Errors.vue
Normal file
46
frontend/src/views/Errors.vue
Normal file
@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div>
|
||||
<header-bar v-if="showHeader" showMenu showLogo />
|
||||
|
||||
<h2 class="message">
|
||||
<i class="material-icons">{{ icon }}</i>
|
||||
<span>{{ message }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import HeaderBar from '@/components/header/HeaderBar'
|
||||
|
||||
const errors = {
|
||||
403: {
|
||||
icon: 'error',
|
||||
message: 'errors.forbidden'
|
||||
},
|
||||
404: {
|
||||
icon: 'gps_off',
|
||||
message: 'errors.notFound'
|
||||
},
|
||||
500: {
|
||||
icon: 'error_outline',
|
||||
message: 'errors.internal'
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'errors',
|
||||
components: {
|
||||
HeaderBar
|
||||
},
|
||||
props: [
|
||||
'errorCode', 'showHeader'
|
||||
],
|
||||
data: function () {
|
||||
return {
|
||||
icon: errors[this.errorCode].icon,
|
||||
message: this.$t(errors[this.errorCode].message)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,24 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<div id="breadcrumbs" v-if="isListing || error">
|
||||
<router-link to="/files/" :aria-label="$t('files.home')" :title="$t('files.home')">
|
||||
<i class="material-icons">home</i>
|
||||
</router-link>
|
||||
<header-bar v-if="error || !req.type" showMenu showLogo />
|
||||
|
||||
<span v-for="(link, index) in breadcrumbs" :key="index">
|
||||
<span class="chevron"><i class="material-icons">keyboard_arrow_right</i></span>
|
||||
<router-link :to="link.url">{{ link.name }}</router-link>
|
||||
</span>
|
||||
</div>
|
||||
<breadcrumbs base="/files" />
|
||||
|
||||
<div v-if="error">
|
||||
<not-found v-if="error.message === '404'"></not-found>
|
||||
<forbidden v-else-if="error.message === '403'"></forbidden>
|
||||
<internal-error v-else></internal-error>
|
||||
</div>
|
||||
<preview v-else-if="isPreview"></preview>
|
||||
<editor v-else-if="isEditor"></editor>
|
||||
<listing :class="{ multiple }" v-else-if="isListing"></listing>
|
||||
<errors v-if="error" :errorCode="errorCode" />
|
||||
<component v-else-if="currentView" :is="currentView"></component>
|
||||
<div v-else>
|
||||
<h2 class="message">
|
||||
<span>{{ $t('files.loading') }}</span>
|
||||
@ -28,13 +15,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Forbidden from './errors/403'
|
||||
import NotFound from './errors/404'
|
||||
import InternalError from './errors/500'
|
||||
import Preview from '@/components/files/Preview'
|
||||
import Listing from '@/components/files/Listing'
|
||||
import { files as api } from '@/api'
|
||||
import { mapGetters, mapState, mapMutations } from 'vuex'
|
||||
import { mapState, mapMutations } from 'vuex'
|
||||
|
||||
import HeaderBar from '@/components/header/HeaderBar'
|
||||
import Breadcrumbs from '@/components/Breadcrumbs'
|
||||
import Errors from '@/views/Errors'
|
||||
import Preview from '@/views/files/Preview'
|
||||
import Listing from '@/views/files/Listing'
|
||||
|
||||
function clean (path) {
|
||||
return path.endsWith('/') ? path.slice(0, -1) : path
|
||||
@ -43,68 +31,41 @@ function clean (path) {
|
||||
export default {
|
||||
name: 'files',
|
||||
components: {
|
||||
Forbidden,
|
||||
NotFound,
|
||||
InternalError,
|
||||
HeaderBar,
|
||||
Breadcrumbs,
|
||||
Errors,
|
||||
Preview,
|
||||
Listing,
|
||||
Editor: () => import('@/components/files/Editor')
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'selectedCount',
|
||||
'isListing',
|
||||
'isEditor',
|
||||
'isFiles'
|
||||
]),
|
||||
...mapState([
|
||||
'req',
|
||||
'user',
|
||||
'reload',
|
||||
'multiple',
|
||||
'loading',
|
||||
'show'
|
||||
]),
|
||||
isPreview () {
|
||||
return !this.loading && !this.isListing && !this.isEditor || this.loading && this.$store.state.previewMode
|
||||
},
|
||||
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++) {
|
||||
if (i === 0) {
|
||||
breadcrumbs.push({ name: decodeURIComponent(parts[i]), url: '/' + parts[i] + '/' })
|
||||
} else {
|
||||
breadcrumbs.push({ name: decodeURIComponent(parts[i]), url: breadcrumbs[i - 1].url + parts[i] + '/' })
|
||||
}
|
||||
}
|
||||
|
||||
breadcrumbs.shift()
|
||||
|
||||
if (breadcrumbs.length > 3) {
|
||||
while (breadcrumbs.length !== 4) {
|
||||
breadcrumbs.shift()
|
||||
}
|
||||
|
||||
breadcrumbs[0].name = '...'
|
||||
}
|
||||
|
||||
return breadcrumbs
|
||||
}
|
||||
Editor: () => import('@/views/files/Editor'),
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
error: null
|
||||
error: null,
|
||||
width: window.innerWidth
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'req',
|
||||
'reload',
|
||||
'loading',
|
||||
'show'
|
||||
]),
|
||||
currentView () {
|
||||
if (this.req.type == undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.req.isDir) {
|
||||
return 'listing'
|
||||
} else if(this.req.type === 'text' || this.req.type === 'textImmutable') {
|
||||
return 'editor'
|
||||
} else {
|
||||
return 'preview'
|
||||
}
|
||||
},
|
||||
errorCode() {
|
||||
return (this.error.message === '404' || this.error.message === '403') ? parseInt(this.error.message) : 500
|
||||
}
|
||||
},
|
||||
created () {
|
||||
@ -120,13 +81,14 @@ export default {
|
||||
},
|
||||
mounted () {
|
||||
window.addEventListener('keydown', this.keyEvent)
|
||||
window.addEventListener('scroll', this.scroll)
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.keyEvent)
|
||||
window.removeEventListener('scroll', this.scroll)
|
||||
},
|
||||
destroyed () {
|
||||
if (this.$store.state.showShell) {
|
||||
this.$store.commit('toggleShell')
|
||||
}
|
||||
this.$store.commit('updateRequest', {})
|
||||
},
|
||||
methods: {
|
||||
@ -171,74 +133,11 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
// Esc!
|
||||
if (event.keyCode === 27) {
|
||||
// If we're on a listing, unselect all
|
||||
// files and folders.
|
||||
if (this.isListing) {
|
||||
this.$store.commit('resetSelected')
|
||||
}
|
||||
}
|
||||
|
||||
// Del!
|
||||
if (event.keyCode === 46) {
|
||||
if (this.isEditor ||
|
||||
!this.isFiles ||
|
||||
this.loading ||
|
||||
!this.user.perm.delete ||
|
||||
(this.isListing && this.selectedCount === 0) ||
|
||||
this.$store.state.show != null) return
|
||||
|
||||
this.$store.commit('showHover', 'delete')
|
||||
}
|
||||
|
||||
// F1!
|
||||
if (event.keyCode === 112) {
|
||||
event.preventDefault()
|
||||
this.$store.commit('showHover', 'help')
|
||||
}
|
||||
|
||||
// F2!
|
||||
if (event.keyCode === 113) {
|
||||
if (this.isEditor ||
|
||||
!this.isFiles ||
|
||||
this.loading ||
|
||||
!this.user.perm.rename ||
|
||||
(this.isListing && this.selectedCount === 0) ||
|
||||
(this.isListing && this.selectedCount > 1)) return
|
||||
|
||||
this.$store.commit('showHover', 'rename')
|
||||
}
|
||||
|
||||
// CTRL + S
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
if (this.isEditor) return
|
||||
|
||||
if (String.fromCharCode(event.which).toLowerCase() === 's') {
|
||||
event.preventDefault()
|
||||
|
||||
if (this.req.kind !== 'editor') {
|
||||
document.getElementById('download-button').click()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scroll () {
|
||||
if (this.req.kind !== 'listing' || this.$store.state.user.viewMode === 'mosaic') return
|
||||
|
||||
let top = 112 - window.scrollY
|
||||
|
||||
if (top < 64) {
|
||||
top = 64
|
||||
}
|
||||
|
||||
document.querySelector('#listing.list .item.header').style.top = top + 'px'
|
||||
},
|
||||
openSidebar () {
|
||||
this.$store.commit('showHover', 'sidebar')
|
||||
},
|
||||
openSearch () {
|
||||
this.$store.commit('showHover', 'search')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
<div id="progress">
|
||||
<div v-bind:style="{ width: this.progress + '%' }"></div>
|
||||
</div>
|
||||
<site-header></site-header>
|
||||
<sidebar></sidebar>
|
||||
<main>
|
||||
<router-view></router-view>
|
||||
@ -17,7 +16,6 @@
|
||||
import { mapState, mapGetters } from 'vuex'
|
||||
import Sidebar from '@/components/Sidebar'
|
||||
import Prompts from '@/components/prompts/Prompts'
|
||||
import SiteHeader from '@/components/Header'
|
||||
import Shell from '@/components/Shell'
|
||||
import { enableExec } from '@/utils/constants'
|
||||
|
||||
@ -25,7 +23,6 @@ export default {
|
||||
name: 'layout',
|
||||
components: {
|
||||
Sidebar,
|
||||
SiteHeader,
|
||||
Prompts,
|
||||
Shell
|
||||
},
|
||||
|
@ -1,11 +1,17 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<ul id="nav">
|
||||
<li :class="{ active: $route.path === '/settings/profile' }"><router-link to="/settings/profile">{{ $t('settings.profileSettings') }}</router-link></li>
|
||||
<li :class="{ active: $route.path === '/settings/shares' }"><router-link to="/settings/shares">{{ $t('settings.shareManagement') }}</router-link></li>
|
||||
<li v-if="user.perm.admin" :class="{ active: $route.path === '/settings/global' }"><router-link to="/settings/global">{{ $t('settings.globalSettings') }}</router-link></li>
|
||||
<li v-if="user.perm.admin" :class="{ active: $route.path === '/settings/users' }"><router-link to="/settings/users">{{ $t('settings.userManagement') }}</router-link></li>
|
||||
</ul>
|
||||
<header-bar showMenu showLogo />
|
||||
|
||||
<div id="nav">
|
||||
<div class="wrapper">
|
||||
<ul>
|
||||
<router-link to="/settings/profile"><li :class="{ active: $route.path === '/settings/profile' }">{{ $t('settings.profileSettings') }}</li></router-link>
|
||||
<router-link to="/settings/shares"><li :class="{ active: $route.path === '/settings/shares' }">{{ $t('settings.shareManagement') }}</li></router-link>
|
||||
<router-link to="/settings/global"><li :class="{ active: $route.path === '/settings/global' }" v-if="user.perm.admin">{{ $t('settings.globalSettings') }}</li></router-link>
|
||||
<router-link to="/settings/users"><li :class="{ active: $route.path === '/settings/users' || $route.name === 'User' }" v-if="user.perm.admin">{{ $t('settings.userManagement') }}</li></router-link>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
@ -14,8 +20,15 @@
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
import HeaderBar from '@/components/header/HeaderBar'
|
||||
|
||||
export default {
|
||||
name: 'settings',
|
||||
computed: mapState([ 'user' ])
|
||||
components: {
|
||||
HeaderBar
|
||||
},
|
||||
computed: {
|
||||
...mapState([ 'user' ])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -1,135 +1,140 @@
|
||||
<template>
|
||||
<div v-if="!loading">
|
||||
<div id="breadcrumbs">
|
||||
<router-link :to="'/share/' + hash" :aria-label="$t('files.home')" :title="$t('files.home')">
|
||||
<i class="material-icons">home</i>
|
||||
</router-link>
|
||||
<div>
|
||||
<header-bar showMenu showLogo>
|
||||
<title />
|
||||
|
||||
<span v-for="(link, index) in breadcrumbs" :key="index">
|
||||
<span class="chevron"><i class="material-icons">keyboard_arrow_right</i></span>
|
||||
<router-link :to="link.url">{{ link.name }}</router-link>
|
||||
</span>
|
||||
</div>
|
||||
<div class="share">
|
||||
<div class="share__box share__box__info">
|
||||
<div class="share__box__header">
|
||||
{{ req.isDir ? $t('download.downloadFolder') : $t('download.downloadFile') }}
|
||||
</div>
|
||||
<div class="share__box__element share__box__center share__box__icon">
|
||||
<i class="material-icons">{{ icon }}</i>
|
||||
</div>
|
||||
<div class="share__box__element">
|
||||
<strong>{{ $t('prompts.displayName') }}</strong> {{ req.name }}
|
||||
</div>
|
||||
<div class="share__box__element">
|
||||
<strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime }}
|
||||
</div>
|
||||
<div class="share__box__element">
|
||||
<strong>{{ $t('prompts.size') }}:</strong> {{ humanSize }}
|
||||
</div>
|
||||
<div class="share__box__element share__box__center">
|
||||
<a target="_blank" :href="link" class="button button--flat">{{ $t('buttons.download') }}</a>
|
||||
</div>
|
||||
<div class="share__box__element share__box__center">
|
||||
<qrcode-vue :value="fullLink" size="200" level="M"></qrcode-vue>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="req.isDir && req.items.length > 0" class="share__box share__box__items">
|
||||
<div class="share__box__header" v-if="req.isDir">
|
||||
{{ $t('files.files') }}
|
||||
</div>
|
||||
<div id="listing" class="list">
|
||||
<item v-for="(item) in req.items.slice(0, this.showLimit)"
|
||||
: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 v-if="req.items.length > showLimit" class="item">
|
||||
<div>
|
||||
<p class="name"> + {{ req.items.length - showLimit }} </p>
|
||||
<action v-if="selectedCount" icon="file_download" :label="$t('buttons.download')" @action="download" :counter="selectedCount" />
|
||||
<action icon="check_circle" :label="$t('buttons.selectMultiple')" @action="toggleMultipleSelection" />
|
||||
</header-bar>
|
||||
|
||||
<breadcrumbs :base="'/share/' + hash" />
|
||||
|
||||
<div v-if="!loading">
|
||||
<div class="share">
|
||||
<div class="share__box share__box__info">
|
||||
<div class="share__box__header">
|
||||
{{ req.isDir ? $t('download.downloadFolder') : $t('download.downloadFile') }}
|
||||
</div>
|
||||
<div class="share__box__element share__box__center share__box__icon">
|
||||
<i class="material-icons">{{ icon }}</i>
|
||||
</div>
|
||||
<div class="share__box__element">
|
||||
<strong>{{ $t('prompts.displayName') }}</strong> {{ req.name }}
|
||||
</div>
|
||||
<div class="share__box__element">
|
||||
<strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime }}
|
||||
</div>
|
||||
<div class="share__box__element">
|
||||
<strong>{{ $t('prompts.size') }}:</strong> {{ humanSize }}
|
||||
</div>
|
||||
<div class="share__box__element share__box__center">
|
||||
<a target="_blank" :href="link" class="button button--flat">{{ $t('buttons.download') }}</a>
|
||||
</div>
|
||||
<div class="share__box__element share__box__center">
|
||||
<qrcode-vue :value="fullLink" size="200" level="M"></qrcode-vue>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="req.isDir && req.items.length > 0" class="share__box share__box__items">
|
||||
<div class="share__box__header" v-if="req.isDir">
|
||||
{{ $t('files.files') }}
|
||||
</div>
|
||||
<div id="listing" class="list">
|
||||
<item v-for="(item) in req.items.slice(0, this.showLimit)"
|
||||
: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"
|
||||
readOnly>
|
||||
</item>
|
||||
<div v-if="req.items.length > showLimit" class="item">
|
||||
<div>
|
||||
<p class="name"> + {{ req.items.length - showLimit }} </p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 :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>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="req.isDir && req.items.length === 0" class="share__box share__box__items">
|
||||
<h2 class="message">
|
||||
<i class="material-icons">sentiment_dissatisfied</i>
|
||||
<span>{{ $t('files.lonely') }}</span>
|
||||
</h2>
|
||||
<div v-else-if="req.isDir && req.items.length === 0" class="share__box share__box__items">
|
||||
<h2 class="message">
|
||||
<i class="material-icons">sentiment_dissatisfied</i>
|
||||
<span>{{ $t('files.lonely') }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="error">
|
||||
<not-found v-if="error.message === '404'"></not-found>
|
||||
<forbidden v-else-if="error.message === '403'"></forbidden>
|
||||
<div v-else-if="error.message === '401'">
|
||||
<div class="card floating" id="password">
|
||||
<div v-if="attemptedPasswordLogin" class="share__wrong__password">{{ $t('login.wrongCredentials') }}</div>
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('login.password') }}</h2>
|
||||
</div>
|
||||
<div v-if="error">
|
||||
<div v-if="error.message === '401'">
|
||||
<div class="card floating" id="password">
|
||||
<div v-if="attemptedPasswordLogin" class="share__wrong__password">{{ $t('login.wrongCredentials') }}</div>
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('login.password') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<input v-focus type="password" :placeholder="$t('login.password')" v-model="password" @keyup.enter="fetchData">
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<button class="button button--flat"
|
||||
@click="fetchData"
|
||||
:aria-label="$t('buttons.submit')"
|
||||
:title="$t('buttons.submit')">{{ $t('buttons.submit') }}</button>
|
||||
<div class="card-content">
|
||||
<input v-focus type="password" :placeholder="$t('login.password')" v-model="password" @keyup.enter="fetchData">
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<button class="button button--flat"
|
||||
@click="fetchData"
|
||||
:aria-label="$t('buttons.submit')"
|
||||
:title="$t('buttons.submit')">{{ $t('buttons.submit') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<errors v-else :errorCode="errorCode" />
|
||||
</div>
|
||||
<internal-error v-else></internal-error>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState, mapMutations, mapGetters} from 'vuex';
|
||||
import { share as api } from '@/api'
|
||||
import { pub as api } from '@/api'
|
||||
import { baseURL } from '@/utils/constants'
|
||||
import filesize from 'filesize'
|
||||
import moment from 'moment'
|
||||
|
||||
import HeaderBar from '@/components/header/HeaderBar'
|
||||
import Action from '@/components/header/Action'
|
||||
import Breadcrumbs from '@/components/Breadcrumbs'
|
||||
import Errors from '@/views/Errors'
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
import Item from "@/components/files/ListingItem"
|
||||
import Forbidden from './errors/403'
|
||||
import NotFound from './errors/404'
|
||||
import InternalError from './errors/500'
|
||||
|
||||
export default {
|
||||
name: 'share',
|
||||
components: {
|
||||
HeaderBar,
|
||||
Action,
|
||||
Breadcrumbs,
|
||||
Item,
|
||||
Forbidden,
|
||||
NotFound,
|
||||
InternalError,
|
||||
QrcodeVue
|
||||
QrcodeVue,
|
||||
Errors
|
||||
},
|
||||
data: () => ({
|
||||
error: null,
|
||||
path: '',
|
||||
showLimit: 500,
|
||||
password: '',
|
||||
attemptedPasswordLogin: false
|
||||
attemptedPasswordLogin: false,
|
||||
hash: null,
|
||||
token: null
|
||||
}),
|
||||
watch: {
|
||||
'$route': 'fetchData'
|
||||
},
|
||||
created: async function () {
|
||||
const hash = this.$route.params.pathMatch.split('/')[0]
|
||||
this.setHash(hash)
|
||||
this.hash = hash
|
||||
await this.fetchData()
|
||||
},
|
||||
mounted () {
|
||||
@ -139,8 +144,8 @@ export default {
|
||||
window.removeEventListener('keydown', this.keyEvent)
|
||||
},
|
||||
computed: {
|
||||
...mapState(['hash', 'req', 'loading', 'multiple']),
|
||||
...mapGetters(['selectedCount']),
|
||||
...mapState(['req', 'loading', 'multiple', 'selected']),
|
||||
...mapGetters(['selectedCount', 'selectedCount']),
|
||||
icon: function () {
|
||||
if (this.req.isDir) return 'folder'
|
||||
if (this.req.type === 'image') return 'insert_photo'
|
||||
@ -168,40 +173,12 @@ export default {
|
||||
humanTime: function () {
|
||||
return moment(this.req.modified).fromNow()
|
||||
},
|
||||
breadcrumbs () {
|
||||
let parts = this.path.split('/')
|
||||
|
||||
if (parts[0] === '') {
|
||||
parts.shift()
|
||||
}
|
||||
|
||||
if (parts[parts.length - 1] === '') {
|
||||
parts.pop()
|
||||
}
|
||||
|
||||
let breadcrumbs = []
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (i === 0) {
|
||||
breadcrumbs.push({ name: decodeURIComponent(parts[i]), url: '/share/' + this.hash + '/' + parts[i] + '/' })
|
||||
} else {
|
||||
breadcrumbs.push({ name: decodeURIComponent(parts[i]), url: breadcrumbs[i - 1].url + parts[i] + '/' })
|
||||
}
|
||||
}
|
||||
|
||||
if (breadcrumbs.length > 3) {
|
||||
while (breadcrumbs.length !== 4) {
|
||||
breadcrumbs.shift()
|
||||
}
|
||||
|
||||
breadcrumbs[0].name = '...'
|
||||
}
|
||||
|
||||
return breadcrumbs
|
||||
errorCode() {
|
||||
return (this.error.message === '404' || this.error.message === '403') ? parseInt(this.error.message) : 500
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([ 'setHash', 'resetSelected', 'updateRequest', 'setLoading' ]),
|
||||
...mapMutations([ 'resetSelected', 'updateRequest', 'setLoading' ]),
|
||||
base64: function (name) {
|
||||
return window.btoa(unescape(encodeURIComponent(name)))
|
||||
},
|
||||
@ -220,10 +197,11 @@ export default {
|
||||
if (this.password !== ''){
|
||||
this.attemptedPasswordLogin = true
|
||||
}
|
||||
let file = await api.getHash(encodeURIComponent(this.$route.params.pathMatch), this.password)
|
||||
let file = await api.fetch(encodeURIComponent(this.$route.params.pathMatch), this.password)
|
||||
this.path = file.path
|
||||
if (this.path.endsWith('/')) this.path = this.path.slice(0, -1)
|
||||
|
||||
this.token = file.token || ''
|
||||
this.$store.commit('setToken', this.token)
|
||||
if (file.isDir) file.items = file.items.map((item, index) => {
|
||||
item.index = index
|
||||
item.url = `/share/${this.hash}${this.path}/${encodeURIComponent(item.name)}`
|
||||
@ -247,6 +225,27 @@ export default {
|
||||
},
|
||||
toggleMultipleSelection () {
|
||||
this.$store.commit('multiple', !this.multiple)
|
||||
},
|
||||
download () {
|
||||
if (this.selectedCount === 1 && !this.req.items[this.selected[0]].isDir) {
|
||||
api.download(null, this.hash, this.token, this.req.items[this.selected[0]].url)
|
||||
return
|
||||
}
|
||||
|
||||
this.$store.commit('showHover', {
|
||||
prompt: 'download',
|
||||
confirm: (format) => {
|
||||
this.$store.commit('closeHovers')
|
||||
|
||||
let files = []
|
||||
|
||||
for (let i of this.selected) {
|
||||
files.push(this.req.items[i].url)
|
||||
}
|
||||
|
||||
api.download(format, this.hash, this.token, ...files)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="message">
|
||||
<i class="material-icons">error</i>
|
||||
<span>{{ $t('errors.forbidden') }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {name: 'forbidden'}
|
||||
</script>
|
||||
|
@ -1,13 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="message">
|
||||
<i class="material-icons">gps_off</i>
|
||||
<span>{{ $t('errors.notFound') }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {name: 'not-found'}
|
||||
</script>
|
||||
|
@ -1,13 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="message">
|
||||
<i class="material-icons">error_outline</i>
|
||||
<span>{{ $t('errors.internal') }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {name: 'internal-error'}
|
||||
</script>
|
||||
|
@ -1,27 +1,15 @@
|
||||
<template>
|
||||
<div id="editor-container">
|
||||
<div class="bar">
|
||||
<button @click="back" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close" class="action">
|
||||
<i class="material-icons">close</i>
|
||||
</button>
|
||||
<header-bar>
|
||||
<action icon="close" :label="$t('buttons.close')" @action="close()" />
|
||||
<title>{{ req.name }}</title>
|
||||
|
||||
<div class="title">
|
||||
<span>{{ req.name }}</span>
|
||||
</div>
|
||||
<template #actions>
|
||||
<action id="save-button" icon="save" :label="$t('buttons.save')" @action="save()" />
|
||||
</template>
|
||||
</header-bar>
|
||||
|
||||
<button @click="save" v-show="user.perm.modify" :aria-label="$t('buttons.save')" :title="$t('buttons.save')" id="save-button" class="action">
|
||||
<i class="material-icons">save</i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<breadcrumbs base="/files" noLink />
|
||||
|
||||
<form id="editor"></form>
|
||||
</div>
|
||||
@ -30,16 +18,25 @@
|
||||
<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 { theme } from '@/utils/constants'
|
||||
|
||||
import HeaderBar from '@/components/header/HeaderBar'
|
||||
import Action from '@/components/header/Action'
|
||||
import Breadcrumbs from '@/components/Breadcrumbs'
|
||||
|
||||
export default {
|
||||
name: 'editor',
|
||||
components: {
|
||||
HeaderBar,
|
||||
Action,
|
||||
Breadcrumbs
|
||||
},
|
||||
data: function () {
|
||||
return {}
|
||||
},
|
||||
@ -82,7 +79,7 @@ export default {
|
||||
window.removeEventListener('keydown', this.keyEvent)
|
||||
this.editor.destroy();
|
||||
},
|
||||
mounted: function () {
|
||||
mounted: function () {
|
||||
const fileContent = this.req.content || '';
|
||||
|
||||
this.editor = ace.edit('editor', {
|
||||
@ -126,6 +123,12 @@ export default {
|
||||
buttons.done(button)
|
||||
this.$showError(e)
|
||||
}
|
||||
},
|
||||
close () {
|
||||
this.$store.commit('updateRequest', {})
|
||||
|
||||
let uri = url.removeLastDir(this.$route.path) + '/'
|
||||
this.$router.push({ path: uri })
|
||||
}
|
||||
}
|
||||
}
|
@ -1,108 +1,169 @@
|
||||
<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>
|
||||
<div>
|
||||
<header-bar showMenu showLogo>
|
||||
<search /> <title />
|
||||
<action class="search-button" icon="search" :label="$t('buttons.search')" @action="openSearch()" />
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapMutations } from 'vuex'
|
||||
import Item from './ListingItem'
|
||||
import css from '@/utils/css'
|
||||
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: { Item },
|
||||
components: {
|
||||
HeaderBar,
|
||||
Action,
|
||||
Search,
|
||||
Item
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
showLimit: 50,
|
||||
dragCounter: 0
|
||||
dragCounter: 0,
|
||||
width: window.innerWidth
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['req', 'selected', 'user', 'show']),
|
||||
...mapState([
|
||||
'req',
|
||||
'selected',
|
||||
'user',
|
||||
'show',
|
||||
'multiple',
|
||||
'selected'
|
||||
]),
|
||||
...mapGetters([
|
||||
'selectedCount'
|
||||
]),
|
||||
nameSorted () {
|
||||
return (this.req.sorting.by === 'name')
|
||||
},
|
||||
@ -159,6 +220,21 @@ export default {
|
||||
}
|
||||
|
||||
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 () {
|
||||
@ -169,6 +245,7 @@ export default {
|
||||
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)
|
||||
@ -179,6 +256,7 @@ export default {
|
||||
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)
|
||||
@ -190,10 +268,34 @@ export default {
|
||||
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
|
||||
}
|
||||
@ -207,7 +309,7 @@ export default {
|
||||
break
|
||||
case 'c':
|
||||
case 'x':
|
||||
this.copyCut(event, key)
|
||||
this.copyCut(event, key)
|
||||
break
|
||||
case 'v':
|
||||
this.paste(event)
|
||||
@ -225,6 +327,10 @@ export default {
|
||||
}
|
||||
}
|
||||
break
|
||||
case 's':
|
||||
event.preventDefault()
|
||||
document.getElementById('download-button').click()
|
||||
break
|
||||
}
|
||||
},
|
||||
preventDefault (event) {
|
||||
@ -458,6 +564,59 @@ export default {
|
||||
}
|
||||
|
||||
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 = []
|
||||
|
||||
for (let i of this.selected) {
|
||||
files.push(this.req.items[i].url)
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +1,17 @@
|
||||
<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>
|
||||
<div id="previewer" @mousemove="toggleNavigation" @touchstart="toggleNavigation">
|
||||
<header-bar>
|
||||
<action icon="close" :label="$t('buttons.close')" @action="close()" />
|
||||
<title>{{ name }}</title>
|
||||
<action :disabled="loading" v-if="isResizeEnabled && req.type === 'image'" :icon="fullSize ? 'photo_size_select_large' : 'hd'" @action="toggleSize" />
|
||||
|
||||
<div class="title">{{ this.name }}</div>
|
||||
|
||||
<preview-size-button v-if="isResizeEnabled && this.req.type === 'image'" @change-size="toggleSize" v-bind:size="fullSize" :disabled="loading"></preview-size-button>
|
||||
<button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
|
||||
<i class="material-icons">more_vert</i>
|
||||
</button>
|
||||
|
||||
<div id="dropdown" :class="{ active : showMore }">
|
||||
<rename-button :disabled="loading" v-if="user.perm.rename"></rename-button>
|
||||
<delete-button :disabled="loading" v-if="user.perm.delete"></delete-button>
|
||||
<download-button :disabled="loading" v-if="user.perm.download"></download-button>
|
||||
<info-button :disabled="loading"></info-button>
|
||||
</div>
|
||||
</div>
|
||||
<template #actions>
|
||||
<action :disabled="loading" icon="mode_edit" :label="$t('buttons.rename')" show="rename" />
|
||||
<action :disabled="loading" icon="delete" :label="$t('buttons.delete')" @action="deleteFile" id="delete-button" />
|
||||
<action :disabled="loading" icon="file_download" :label="$t('buttons.download')" @action="download" />
|
||||
<action :disabled="loading" icon="info" :label="$t('buttons.info')" show="info" />
|
||||
</template>
|
||||
</header-bar>
|
||||
|
||||
<div class="loading" v-if="loading">
|
||||
<div class="spinner">
|
||||
@ -28,18 +21,11 @@
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<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" autoplay controls></audio>
|
||||
<video v-else-if="req.type == 'video'" :src="raw" autoplay controls>
|
||||
<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"
|
||||
@ -47,31 +33,35 @@
|
||||
: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>
|
||||
but don't worry, you can <a :href="downloadUrl">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">
|
||||
<a v-else-if="req.type == 'blob'" :href="downloadUrl">
|
||||
<h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-show="showMore" @click="resetPrompts" class="overlay"></div>
|
||||
<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 url from '@/utils/url'
|
||||
import { baseURL, resizePreview } from '@/utils/constants'
|
||||
import { files as api } from '@/api'
|
||||
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'
|
||||
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 ExtendedImage from '@/components/files/ExtendedImage'
|
||||
|
||||
const mediaTypes = [
|
||||
"image",
|
||||
@ -83,11 +73,8 @@ const mediaTypes = [
|
||||
export default {
|
||||
name: 'preview',
|
||||
components: {
|
||||
PreviewSizeButton,
|
||||
InfoButton,
|
||||
DeleteButton,
|
||||
RenameButton,
|
||||
DownloadButton,
|
||||
HeaderBar,
|
||||
Action,
|
||||
ExtendedImage
|
||||
},
|
||||
data: function () {
|
||||
@ -97,7 +84,10 @@ export default {
|
||||
listing: null,
|
||||
name: '',
|
||||
subtitles: [],
|
||||
fullSize: false
|
||||
fullSize: false,
|
||||
showNav: true,
|
||||
navTimeout: null,
|
||||
hoverNav: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -108,7 +98,7 @@ export default {
|
||||
hasNext () {
|
||||
return (this.nextLink !== '')
|
||||
},
|
||||
download () {
|
||||
downloadUrl () {
|
||||
return `${baseURL}/api/raw${url.encodePath(this.req.path)}?auth=${this.jwt}`
|
||||
},
|
||||
previewUrl () {
|
||||
@ -130,41 +120,40 @@ export default {
|
||||
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)
|
||||
deleteFile () {
|
||||
this.$store.commit('showHover', {
|
||||
prompt: 'delete',
|
||||
confirm: () => {
|
||||
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 })
|
||||
if (this.hasNext) {
|
||||
this.next()
|
||||
} else if (!this.hasPrevious && !this.hasNext) {
|
||||
this.close()
|
||||
} else {
|
||||
this.prev()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
prev () {
|
||||
this.hoverNav = false
|
||||
this.$router.push({ path: this.previousLink })
|
||||
},
|
||||
next () {
|
||||
this.hoverNav = false
|
||||
this.$router.push({ path: this.nextLink })
|
||||
},
|
||||
key (event) {
|
||||
@ -178,7 +167,7 @@ export default {
|
||||
} else if (event.which === 37) { // left arrow
|
||||
if (this.hasPrevious) this.prev()
|
||||
} else if (event.which === 27) { // esc
|
||||
this.back()
|
||||
this.close()
|
||||
}
|
||||
},
|
||||
async updatePreview () {
|
||||
@ -232,6 +221,27 @@ export default {
|
||||
},
|
||||
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 () {
|
||||
this.$store.commit('updateRequest', {})
|
||||
|
||||
let uri = url.removeLastDir(this.$route.path) + '/'
|
||||
this.$router.push({ path: uri })
|
||||
},
|
||||
download() {
|
||||
api.download(null, this.$route.path)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,102 +1,108 @@
|
||||
<template>
|
||||
<div class="dashboard" v-if="settings !== null">
|
||||
<form class="card" @submit.prevent="save">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('settings.globalSettings') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p><input type="checkbox" v-model="settings.signup"> {{ $t('settings.allowSignup') }}</p>
|
||||
|
||||
<p><input type="checkbox" v-model="settings.createUserDir"> {{ $t('settings.createUserDir') }}</p>
|
||||
|
||||
<h3>{{ $t('settings.rules') }}</h3>
|
||||
<p class="small">{{ $t('settings.globalRules') }}</p>
|
||||
<rules :rules.sync="settings.rules" />
|
||||
|
||||
<div v-if="isExecEnabled">
|
||||
<h3>{{ $t('settings.executeOnShell') }}</h3>
|
||||
<p class="small">{{ $t('settings.executeOnShellDescription') }}</p>
|
||||
<input class="input input--block" type="text" placeholder="bash -c, cmd /c, ..." v-model="settings.shell" />
|
||||
<div class="row" v-if="settings !== null">
|
||||
<div class="column">
|
||||
<form class="card" @submit.prevent="save">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('settings.globalSettings') }}</h2>
|
||||
</div>
|
||||
|
||||
<h3>{{ $t('settings.branding') }}</h3>
|
||||
<div class="card-content">
|
||||
<p><input type="checkbox" v-model="settings.signup"> {{ $t('settings.allowSignup') }}</p>
|
||||
|
||||
<i18n path="settings.brandingHelp" tag="p" class="small">
|
||||
<a class="link" target="_blank" href="https://filebrowser.org/configuration/custom-branding">{{ $t('settings.documentation') }}</a>
|
||||
</i18n>
|
||||
<p><input type="checkbox" v-model="settings.createUserDir"> {{ $t('settings.createUserDir') }}</p>
|
||||
|
||||
<p>
|
||||
<input type="checkbox" v-model="settings.branding.disableExternal" id="branding-links" />
|
||||
{{ $t('settings.disableExternalLinks') }}
|
||||
</p>
|
||||
<h3>{{ $t('settings.rules') }}</h3>
|
||||
<p class="small">{{ $t('settings.globalRules') }}</p>
|
||||
<rules :rules.sync="settings.rules" />
|
||||
|
||||
<p>
|
||||
<label for="theme">{{ $t('settings.themes.title') }}</label>
|
||||
<themes class="input input--block" :theme.sync="settings.branding.theme" id="theme"></themes>
|
||||
</p>
|
||||
<div v-if="isExecEnabled">
|
||||
<h3>{{ $t('settings.executeOnShell') }}</h3>
|
||||
<p class="small">{{ $t('settings.executeOnShellDescription') }}</p>
|
||||
<input class="input input--block" type="text" placeholder="bash -c, cmd /c, ..." v-model="settings.shell" />
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<label for="branding-name">{{ $t('settings.instanceName') }}</label>
|
||||
<input class="input input--block" type="text" v-model="settings.branding.name" id="branding-name" />
|
||||
</p>
|
||||
<h3>{{ $t('settings.branding') }}</h3>
|
||||
|
||||
<p>
|
||||
<label for="branding-files">{{ $t('settings.brandingDirectoryPath') }}</label>
|
||||
<input class="input input--block" type="text" v-model="settings.branding.files" id="branding-files" />
|
||||
</p>
|
||||
<i18n path="settings.brandingHelp" tag="p" class="small">
|
||||
<a class="link" target="_blank" href="https://filebrowser.org/configuration/custom-branding">{{ $t('settings.documentation') }}</a>
|
||||
</i18n>
|
||||
|
||||
</div>
|
||||
<p>
|
||||
<input type="checkbox" v-model="settings.branding.disableExternal" id="branding-links" />
|
||||
{{ $t('settings.disableExternalLinks') }}
|
||||
</p>
|
||||
|
||||
<div class="card-action">
|
||||
<input class="button button--flat" type="submit" :value="$t('buttons.update')">
|
||||
</div>
|
||||
</form>
|
||||
<p>
|
||||
<label for="theme">{{ $t('settings.themes.title') }}</label>
|
||||
<themes class="input input--block" :theme.sync="settings.branding.theme" id="theme"></themes>
|
||||
</p>
|
||||
|
||||
<form class="card" @submit.prevent="save">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('settings.userDefaults') }}</h2>
|
||||
</div>
|
||||
<p>
|
||||
<label for="branding-name">{{ $t('settings.instanceName') }}</label>
|
||||
<input class="input input--block" type="text" v-model="settings.branding.name" id="branding-name" />
|
||||
</p>
|
||||
|
||||
<div class="card-content">
|
||||
<p class="small">{{ $t('settings.defaultUserDescription') }}</p>
|
||||
<p>
|
||||
<label for="branding-files">{{ $t('settings.brandingDirectoryPath') }}</label>
|
||||
<input class="input input--block" type="text" v-model="settings.branding.files" id="branding-files" />
|
||||
</p>
|
||||
|
||||
<user-form :isNew="false" :isDefault="true" :user.sync="settings.defaults" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<input class="button button--flat" type="submit" :value="$t('buttons.update')">
|
||||
</div>
|
||||
</form>
|
||||
<div class="card-action">
|
||||
<input class="button button--flat" type="submit" :value="$t('buttons.update')">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<form v-if="isExecEnabled" class="card" @submit.prevent="save">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('settings.commandRunner') }}</h2>
|
||||
</div>
|
||||
<div class="column">
|
||||
<form class="card" @submit.prevent="save">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('settings.userDefaults') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<i18n path="settings.commandRunnerHelp" tag="p" class="small">
|
||||
<code>FILE</code>
|
||||
<code>SCOPE</code>
|
||||
<a class="link" target="_blank" href="https://filebrowser.org/configuration/command-runner">{{ $t('settings.documentation') }}</a>
|
||||
</i18n>
|
||||
<div class="card-content">
|
||||
<p class="small">{{ $t('settings.defaultUserDescription') }}</p>
|
||||
|
||||
<div v-for="command in settings.commands" :key="command.name" class="collapsible">
|
||||
<input :id="command.name" type="checkbox">
|
||||
<label :for="command.name">
|
||||
<p>{{ capitalize(command.name) }}</p>
|
||||
<i class="material-icons">arrow_drop_down</i>
|
||||
</label>
|
||||
<div class="collapse">
|
||||
<textarea class="input input--block input--textarea" v-model.trim="command.value"></textarea>
|
||||
<user-form :isNew="false" :isDefault="true" :user.sync="settings.defaults" />
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<input class="button button--flat" type="submit" :value="$t('buttons.update')">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<form v-if="isExecEnabled" class="card" @submit.prevent="save">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('settings.commandRunner') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<i18n path="settings.commandRunnerHelp" tag="p" class="small">
|
||||
<code>FILE</code>
|
||||
<code>SCOPE</code>
|
||||
<a class="link" target="_blank" href="https://filebrowser.org/configuration/command-runner">{{ $t('settings.documentation') }}</a>
|
||||
</i18n>
|
||||
|
||||
<div v-for="command in settings.commands" :key="command.name" class="collapsible">
|
||||
<input :id="command.name" type="checkbox">
|
||||
<label :for="command.name">
|
||||
<p>{{ capitalize(command.name) }}</p>
|
||||
<i class="material-icons">arrow_drop_down</i>
|
||||
</label>
|
||||
<div class="collapse">
|
||||
<textarea class="input input--block input--textarea" v-model.trim="command.value"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<input class="button button--flat" type="submit" :value="$t('buttons.update')">
|
||||
</div>
|
||||
</form>
|
||||
<div class="card-action">
|
||||
<input class="button button--flat" type="submit" :value="$t('buttons.update')">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -1,36 +1,40 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<form class="card" @submit="updateSettings">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('settings.profileSettings') }}</h2>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<form class="card" @submit="updateSettings">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('settings.profileSettings') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p><input type="checkbox" v-model="hideDotfiles"> {{ $t('settings.hideDotfiles') }}</p>
|
||||
<p><input type="checkbox" v-model="singleClick"> {{ $t('settings.singleClick') }}</p>
|
||||
<h3>{{ $t('settings.language') }}</h3>
|
||||
<languages class="input input--block" :locale.sync="locale"></languages>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p><input type="checkbox" v-model="hideDotfiles"> {{ $t('settings.hideDotfiles') }}</p>
|
||||
<p><input type="checkbox" v-model="singleClick"> {{ $t('settings.singleClick') }}</p>
|
||||
<h3>{{ $t('settings.language') }}</h3>
|
||||
<languages class="input input--block" :locale.sync="locale"></languages>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<input class="button button--flat" type="submit" :value="$t('buttons.update')">
|
||||
</div>
|
||||
</form>
|
||||
<div class="card-action">
|
||||
<input class="button button--flat" type="submit" :value="$t('buttons.update')">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<form class="card" v-if="!user.lockPassword" @submit="updatePassword">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('settings.changePassword') }}</h2>
|
||||
</div>
|
||||
<div class="column">
|
||||
<form class="card" v-if="!user.lockPassword" @submit="updatePassword">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('settings.changePassword') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<input :class="passwordClass" type="password" :placeholder="$t('settings.newPassword')" v-model="password" name="password">
|
||||
<input :class="passwordClass" type="password" :placeholder="$t('settings.newPasswordConfirm')" v-model="passwordConf" name="password">
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<input :class="passwordClass" type="password" :placeholder="$t('settings.newPassword')" v-model="password" name="password">
|
||||
<input :class="passwordClass" type="password" :placeholder="$t('settings.newPasswordConfirm')" v-model="passwordConf" name="password">
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<input class="button button--flat" type="submit" :value="$t('buttons.update')">
|
||||
</div>
|
||||
</form>
|
||||
<div class="card-action">
|
||||
<input class="button button--flat" type="submit" :value="$t('buttons.update')">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -1,40 +1,44 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('settings.shareManagement') }}</h2>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<div class="card">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('settings.shareManagement') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content full">
|
||||
<table>
|
||||
<tr>
|
||||
<th>{{ $t('settings.path') }}</th>
|
||||
<th>{{ $t('settings.shareDuration') }}</th>
|
||||
<th v-if="user.perm.admin">{{ $t('settings.username') }}</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<div class="card-content full">
|
||||
<table>
|
||||
<tr>
|
||||
<th>{{ $t('settings.path') }}</th>
|
||||
<th>{{ $t('settings.shareDuration') }}</th>
|
||||
<th v-if="user.perm.admin">{{ $t('settings.username') }}</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
||||
<tr v-for="link in links" :key="link.hash">
|
||||
<td><a :href="buildLink(link.hash)" target="_blank">{{ link.path }}</a></td>
|
||||
<td>
|
||||
<template v-if="link.expire !== 0">{{ humanTime(link.expire) }}</template>
|
||||
<template v-else>{{ $t('permanent') }}</template>
|
||||
</td>
|
||||
<td v-if="user.perm.admin">{{ link.username }}</td>
|
||||
<td class="small">
|
||||
<button class="action"
|
||||
@click="deleteLink($event, link)"
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')"><i class="material-icons">delete</i></button>
|
||||
</td>
|
||||
<td class="small">
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<tr v-for="link in links" :key="link.hash">
|
||||
<td><a :href="buildLink(link.hash)" target="_blank">{{ link.path }}</a></td>
|
||||
<td>
|
||||
<template v-if="link.expire !== 0">{{ humanTime(link.expire) }}</template>
|
||||
<template v-else>{{ $t('permanent') }}</template>
|
||||
</td>
|
||||
<td v-if="user.perm.admin">{{ link.username }}</td>
|
||||
<td class="small">
|
||||
<button class="action"
|
||||
@click="deleteLink($event, link)"
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')"><i class="material-icons">delete</i></button>
|
||||
</td>
|
||||
<td class="small">
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -73,23 +77,23 @@ export default {
|
||||
this.clip.on('success', () => {
|
||||
this.$showSuccess(this.$t('success.linkCopied'))
|
||||
})
|
||||
this.$root.$on('share-deleted', this.deleted)
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.clip.destroy()
|
||||
this.$root.$off('share-deleted', this.deleted)
|
||||
},
|
||||
methods: {
|
||||
deleteLink: async function (event, link) {
|
||||
event.preventDefault()
|
||||
this.$store.commit('setHash', {
|
||||
hash: link.hash,
|
||||
path: link.path
|
||||
|
||||
this.$store.commit('showHover', {
|
||||
prompt: 'share-delete',
|
||||
confirm: () => {
|
||||
this.$store.commit('closeHovers')
|
||||
|
||||
api.remove(link.hash)
|
||||
this.links = this.links.filter(item => item.hash !== link.hash)
|
||||
}
|
||||
})
|
||||
this.$store.commit('showHover', 'share-delete')
|
||||
},
|
||||
deleted (hash) {
|
||||
this.links = this.links.filter(item => item.hash !== hash)
|
||||
},
|
||||
humanTime (time) {
|
||||
return moment(time * 1000).fromNow()
|
||||
|
@ -1,29 +1,31 @@
|
||||
<template>
|
||||
<div>
|
||||
<form v-if="loaded" @submit="save" class="card">
|
||||
<div class="card-title">
|
||||
<h2 v-if="user.id === 0">{{ $t('settings.newUser') }}</h2>
|
||||
<h2 v-else>{{ $t('settings.user') }} {{ user.username }}</h2>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<form v-if="loaded" @submit="save" class="card">
|
||||
<div class="card-title">
|
||||
<h2 v-if="user.id === 0">{{ $t('settings.newUser') }}</h2>
|
||||
<h2 v-else>{{ $t('settings.user') }} {{ user.username }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<user-form :user.sync="user" :isDefault="false" :isNew="isNew" />
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<user-form :user.sync="user" :isDefault="false" :isNew="isNew" />
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button
|
||||
v-if="!isNew"
|
||||
@click.prevent="deletePrompt"
|
||||
type="button"
|
||||
class="button button--flat button--red"
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
|
||||
<input
|
||||
class="button button--flat"
|
||||
type="submit"
|
||||
:value="$t('buttons.save')">
|
||||
</div>
|
||||
</form>
|
||||
<div class="card-action">
|
||||
<button
|
||||
v-if="!isNew"
|
||||
@click.prevent="deletePrompt"
|
||||
type="button"
|
||||
class="button button--flat button--red"
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
|
||||
<input
|
||||
class="button button--flat"
|
||||
type="submit"
|
||||
:value="$t('buttons.save')">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div v-if="$store.state.show === 'deleteUser'" class="card floating">
|
||||
<div class="card-content">
|
||||
|
@ -1,28 +1,32 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('settings.users') }}</h2>
|
||||
<router-link to="/settings/users/new"><button class="button">{{ $t('buttons.new') }}</button></router-link>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<div class="card">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('settings.users') }}</h2>
|
||||
<router-link to="/settings/users/new"><button class="button">{{ $t('buttons.new') }}</button></router-link>
|
||||
</div>
|
||||
|
||||
<div class="card-content full">
|
||||
<table>
|
||||
<tr>
|
||||
<th>{{ $t('settings.username') }}</th>
|
||||
<th>{{ $t('settings.admin') }}</th>
|
||||
<th>{{ $t('settings.scope') }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<div class="card-content full">
|
||||
<table>
|
||||
<tr>
|
||||
<th>{{ $t('settings.username') }}</th>
|
||||
<th>{{ $t('settings.admin') }}</th>
|
||||
<th>{{ $t('settings.scope') }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
||||
<tr v-for="user in users" :key="user.id">
|
||||
<td>{{ user.username }}</td>
|
||||
<td><i v-if="user.perm.admin" class="material-icons">done</i><i v-else class="material-icons">close</i></td>
|
||||
<td>{{ user.scope }}</td>
|
||||
<td class="small">
|
||||
<router-link :to="'/settings/users/' + user.id"><i class="material-icons">mode_edit</i></router-link>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<tr v-for="user in users" :key="user.id">
|
||||
<td>{{ user.username }}</td>
|
||||
<td><i v-if="user.perm.admin" class="material-icons">done</i><i v-else class="material-icons">close</i></td>
|
||||
<td>{{ user.scope }}</td>
|
||||
<td class="small">
|
||||
<router-link :to="'/settings/users/' + user.id"><i class="material-icons">mode_edit</i></router-link>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -6,7 +6,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -91,17 +90,6 @@ var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request
|
||||
defer r.Body.Close()
|
||||
}
|
||||
|
||||
if body.Expires == "" {
|
||||
var err error
|
||||
s, err = d.store.Share.GetPermanent(r.URL.Path, d.user.ID)
|
||||
if err == nil {
|
||||
if _, err := w.Write([]byte(path.Join(d.server.BaseURL, "/share/", s.Hash))); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
bytes := make([]byte, 6)
|
||||
_, err := rand.Read(bytes)
|
||||
if err != nil {
|
||||
|
Loading…
Reference in New Issue
Block a user