feat:Support compress image view

This commit is contained in:
liwei 2020-06-06 18:55:01 +08:00
parent 6d899a6335
commit aadfce0e65
73 changed files with 1149 additions and 745 deletions

198
frontend/.eslintrc.js Normal file
View File

@ -0,0 +1,198 @@
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint',
sourceType: 'module'
},
env: {
browser: true,
node: true,
es6: true
},
extends: ['plugin:vue/recommended', 'eslint:recommended'],
// add your custom rules here
// it is base on https://github.com/vuejs/eslint-config-vue
rules: {
'vue/max-attributes-per-line': [2, {
'singleline': 10,
'multiline': {
'max': 1,
'allowFirstLine': false
}
}],
'vue/singleline-html-element-content-newline': 'off',
'vue/multiline-html-element-content-newline': 'off',
'vue/name-property-casing': ['error', 'PascalCase'],
'vue/no-v-html': 'off',
'accessor-pairs': 2,
'arrow-spacing': [2, {
'before': true,
'after': true
}],
'block-spacing': [2, 'always'],
'brace-style': [2, '1tbs', {
'allowSingleLine': true
}],
'camelcase': [0, {
'properties': 'always'
}],
'comma-dangle': [2, 'never'],
'comma-spacing': [2, {
'before': false,
'after': true
}],
'comma-style': [2, 'last'],
'constructor-super': 2,
'curly': [2, 'multi-line'],
'dot-location': [2, 'property'],
'eol-last': 2,
'eqeqeq': ['error', 'always', { 'null': 'ignore' }],
'generator-star-spacing': [2, {
'before': true,
'after': true
}],
'handle-callback-err': [2, '^(err|error)$'],
'indent': [2, 2, {
'SwitchCase': 1
}],
'jsx-quotes': [2, 'prefer-single'],
'key-spacing': [2, {
'beforeColon': false,
'afterColon': true
}],
'keyword-spacing': [2, {
'before': true,
'after': true
}],
'new-cap': [2, {
'newIsCap': true,
'capIsNew': false
}],
'new-parens': 2,
'no-array-constructor': 2,
'no-caller': 2,
'no-console': 'off',
'no-class-assign': 2,
'no-cond-assign': 2,
'no-const-assign': 2,
'no-control-regex': 0,
'no-delete-var': 2,
'no-dupe-args': 2,
'no-dupe-class-members': 2,
'no-dupe-keys': 2,
'no-duplicate-case': 2,
'no-empty-character-class': 2,
'no-empty-pattern': 2,
'no-eval': 2,
'no-ex-assign': 2,
'no-extend-native': 2,
'no-extra-bind': 2,
'no-extra-boolean-cast': 2,
'no-extra-parens': [2, 'functions'],
'no-fallthrough': 2,
'no-floating-decimal': 2,
'no-func-assign': 2,
'no-implied-eval': 2,
'no-inner-declarations': [2, 'functions'],
'no-invalid-regexp': 2,
'no-irregular-whitespace': 2,
'no-iterator': 2,
'no-label-var': 2,
'no-labels': [2, {
'allowLoop': false,
'allowSwitch': false
}],
'no-lone-blocks': 2,
'no-mixed-spaces-and-tabs': 2,
'no-multi-spaces': 2,
'no-multi-str': 2,
'no-multiple-empty-lines': [2, {
'max': 1
}],
'no-native-reassign': 2,
'no-negated-in-lhs': 2,
'no-new-object': 2,
'no-new-require': 2,
'no-new-symbol': 2,
'no-new-wrappers': 2,
'no-obj-calls': 2,
'no-octal': 2,
'no-octal-escape': 2,
'no-path-concat': 2,
'no-proto': 2,
'no-redeclare': 2,
'no-regex-spaces': 2,
'no-return-assign': [2, 'except-parens'],
'no-self-assign': 2,
'no-self-compare': 2,
'no-sequences': 2,
'no-shadow-restricted-names': 2,
'no-spaced-func': 2,
'no-sparse-arrays': 2,
'no-this-before-super': 2,
'no-throw-literal': 2,
'no-trailing-spaces': 2,
'no-undef': 2,
'no-undef-init': 2,
'no-unexpected-multiline': 2,
'no-unmodified-loop-condition': 2,
'no-unneeded-ternary': [2, {
'defaultAssignment': false
}],
'no-unreachable': 2,
'no-unsafe-finally': 2,
'no-unused-vars': [2, {
'vars': 'all',
'args': 'none'
}],
'no-useless-call': 2,
'no-useless-computed-key': 2,
'no-useless-constructor': 2,
'no-useless-escape': 0,
'no-whitespace-before-property': 2,
'no-with': 2,
'one-var': [2, {
'initialized': 'never'
}],
'operator-linebreak': [2, 'after', {
'overrides': {
'?': 'before',
':': 'before'
}
}],
'padded-blocks': [2, 'never'],
'quotes': [2, 'single', {
'avoidEscape': true,
'allowTemplateLiterals': true
}],
'semi': [2, 'never'],
'semi-spacing': [2, {
'before': false,
'after': true
}],
'space-before-blocks': [2, 'always'],
'space-before-function-paren': [2, 'never'],
'space-in-parens': [2, 'never'],
'space-infix-ops': 2,
'space-unary-ops': [2, {
'words': true,
'nonwords': false
}],
'spaced-comment': [2, 'always', {
'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
}],
'template-curly-spacing': [2, 'never'],
'use-isnan': 2,
'valid-typeof': 2,
'wrap-iife': [2, 'any'],
'yield-star-spacing': [2, 'both'],
'yoda': [2, 'never'],
'prefer-const': 2,
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'object-curly-spacing': [2, 'always', {
objectsInObjects: false
}],
'array-bracket-spacing': [2, 'never']
}
}

View File

@ -1,17 +1,17 @@
<template> <template>
<div> <div>
<router-view></router-view> <router-view />
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'app', name: 'App',
mounted () { mounted() {
const loading = document.getElementById('loading') const loading = document.getElementById('loading')
loading.classList.add('done') loading.classList.add('done')
setTimeout(function () { setTimeout(function() {
loading.parentNode.removeChild(loading) loading.parentNode.removeChild(loading)
}, 200) }, 200)
} }

View File

@ -9,7 +9,7 @@ export default function command(url, command, onmessage, onclose) {
url = removePrefix(url) url = removePrefix(url)
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${store.state.jwt}` url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${store.state.jwt}`
let conn = new window.WebSocket(url) const conn = new window.WebSocket(url)
conn.onopen = () => conn.send(command) conn.onopen = () => conn.send(command)
conn.onmessage = onmessage conn.onmessage = onmessage
conn.onclose = onclose conn.onclose = onclose

View File

@ -2,13 +2,13 @@ import { fetchURL, removePrefix } from './utils'
import { baseURL } from '@/utils/constants' import { baseURL } from '@/utils/constants'
import store from '@/store' import store from '@/store'
export async function fetch (url) { export async function fetch(url) {
url = removePrefix(url) url = removePrefix(url)
const res = await fetchURL(`/api/resources${url}`, {}) const res = await fetchURL(`/api/resources${url}`, {})
if (res.status === 200) { if (res.status === 200) {
let data = await res.json() const data = await res.json()
data.url = `/files${url}` data.url = `/files${url}`
if (data.isDir) { if (data.isDir) {
@ -31,10 +31,10 @@ export async function fetch (url) {
} }
} }
async function resourceAction (url, method, content) { async function resourceAction(url, method, content) {
url = removePrefix(url) url = removePrefix(url)
let opts = { method } const opts = { method }
if (content) { if (content) {
opts.body = content opts.body = content
@ -49,15 +49,15 @@ async function resourceAction (url, method, content) {
} }
} }
export async function remove (url) { export async function remove(url) {
return resourceAction(url, 'DELETE') return resourceAction(url, 'DELETE')
} }
export async function put (url, content = '') { export async function put(url, content = '') {
return resourceAction(url, 'PUT', content) return resourceAction(url, 'PUT', content)
} }
export function download (format, ...files) { export function download(format, ...files) {
let url = `${baseURL}/api/raw` let url = `${baseURL}/api/raw`
if (files.length === 1) { if (files.length === 1) {
@ -65,7 +65,7 @@ export function download (format, ...files) {
} else { } else {
let arg = '' let arg = ''
for (let file of files) { for (const file of files) {
arg += removePrefix(file) + ',' arg += removePrefix(file) + ','
} }
@ -82,11 +82,11 @@ export function download (format, ...files) {
window.open(url) window.open(url)
} }
export async function post (url, content = '', overwrite = false, onupload) { export async function post(url, content = '', overwrite = false, onupload) {
url = removePrefix(url) url = removePrefix(url)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let request = new XMLHttpRequest() const request = new XMLHttpRequest()
request.open('POST', `${baseURL}/api/resources${url}?override=${overwrite}`, true) request.open('POST', `${baseURL}/api/resources${url}?override=${overwrite}`, true)
request.setRequestHeader('X-Auth', store.state.jwt) request.setRequestHeader('X-Auth', store.state.jwt)
@ -95,7 +95,7 @@ export async function post (url, content = '', overwrite = false, onupload) {
} }
// Send a message to user before closing the tab during file upload // Send a message to user before closing the tab during file upload
window.onbeforeunload = () => "Files are being uploaded." window.onbeforeunload = () => 'Files are being uploaded.'
request.onload = () => { request.onload = () => {
if (request.status === 200) { if (request.status === 200) {
@ -116,10 +116,10 @@ export async function post (url, content = '', overwrite = false, onupload) {
}).finally(() => { window.onbeforeunload = null }) }).finally(() => { window.onbeforeunload = null })
} }
function moveCopy (items, copy = false) { function moveCopy(items, copy = false) {
let promises = [] const promises = []
for (let item of items) { for (const item of items) {
const from = removePrefix(item.from) const from = removePrefix(item.from)
const to = encodeURIComponent(removePrefix(item.to)) const to = encodeURIComponent(removePrefix(item.to))
const url = `${from}?action=${copy ? 'copy' : 'rename'}&destination=${to}` const url = `${from}?action=${copy ? 'copy' : 'rename'}&destination=${to}`
@ -129,15 +129,15 @@ function moveCopy (items, copy = false) {
return Promise.all(promises) return Promise.all(promises)
} }
export function move (items) { export function move(items) {
return moveCopy(items) return moveCopy(items)
} }
export function copy (items) { export function copy(items) {
return moveCopy(items, true) return moveCopy(items, true)
} }
export async function checksum (url, algo) { export async function checksum(url, algo) {
const data = await resourceAction(`${url}?checksum=${algo}`, 'GET') const data = await resourceAction(`${url}?checksum=${algo}`, 'GET')
return (await data.json()).checksums[algo] return (await data.json()).checksums[algo]
} }

View File

@ -1,6 +1,6 @@
import { fetchJSON, removePrefix } from './utils' import { fetchJSON, removePrefix } from './utils'
export default async function search (url, query) { export default async function search(url, query) {
url = removePrefix(url) url = removePrefix(url)
query = encodeURIComponent(query) query = encodeURIComponent(query)

View File

@ -1,10 +1,10 @@
import { fetchURL, fetchJSON } from './utils' import { fetchURL, fetchJSON } from './utils'
export function get () { export function get() {
return fetchJSON(`/api/settings`, {}) return fetchJSON(`/api/settings`, {})
} }
export async function update (settings) { export async function update(settings) {
const res = await fetchURL(`/api/settings`, { const res = await fetchURL(`/api/settings`, {
method: 'PUT', method: 'PUT',
body: JSON.stringify(settings) body: JSON.stringify(settings)

View File

@ -1,14 +1,14 @@
import { fetchURL, fetchJSON } from './utils' import { fetchURL, fetchJSON } from './utils'
export async function getAll () { export async function getAll() {
return fetchJSON(`/api/users`, {}) return fetchJSON(`/api/users`, {})
} }
export async function get (id) { export async function get(id) {
return fetchJSON(`/api/users/${id}`, {}) return fetchJSON(`/api/users/${id}`, {})
} }
export async function create (user) { export async function create(user) {
const res = await fetchURL(`/api/users`, { const res = await fetchURL(`/api/users`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
@ -23,10 +23,9 @@ export async function create (user) {
} else { } else {
throw new Error(res.status) throw new Error(res.status)
} }
} }
export async function update (user, which = ['all']) { export async function update(user, which = ['all']) {
const res = await fetchURL(`/api/users/${user.id}`, { const res = await fetchURL(`/api/users/${user.id}`, {
method: 'PUT', method: 'PUT',
body: JSON.stringify({ body: JSON.stringify({
@ -41,7 +40,7 @@ export async function update (user, which = ['all']) {
} }
} }
export async function remove (id) { export async function remove(id) {
const res = await fetchURL(`/api/users/${id}`, { const res = await fetchURL(`/api/users/${id}`, {
method: 'DELETE' method: 'DELETE'
}) })

View File

@ -2,11 +2,11 @@ import store from '@/store'
import { renew } from '@/utils/auth' import { renew } from '@/utils/auth'
import { baseURL } from '@/utils/constants' import { baseURL } from '@/utils/constants'
export async function fetchURL (url, opts) { export async function fetchURL(url, opts) {
opts = opts || {} opts = opts || {}
opts.headers = opts.headers || {} opts.headers = opts.headers || {}
let { headers, ...rest } = opts const { headers, ...rest } = opts
const res = await fetch(`${baseURL}${url}`, { const res = await fetch(`${baseURL}${url}`, {
headers: { headers: {
@ -23,7 +23,7 @@ export async function fetchURL (url, opts) {
return res return res
} }
export async function fetchJSON (url, opts) { export async function fetchJSON(url, opts) {
const res = await fetchURL(url, opts) const res = await fetchURL(url, opts)
if (res.status === 200) { if (res.status === 200) {
@ -33,7 +33,7 @@ export async function fetchJSON (url, opts) {
} }
} }
export function removePrefix (url) { export function removePrefix(url) {
if (url.startsWith('/files')) { if (url.startsWith('/files')) {
url = url.slice(6) url = url.slice(6)
} }

View File

@ -1,60 +1,60 @@
<template> <template>
<header> <header>
<div> <div>
<button @click="openSidebar" :aria-label="$t('buttons.toggleSidebar')" :title="$t('buttons.toggleSidebar')" class="action"> <button :aria-label="$t('buttons.toggleSidebar')" :title="$t('buttons.toggleSidebar')" class="action" @click="openSidebar">
<i class="material-icons">menu</i> <i class="material-icons">menu</i>
</button> </button>
<img :src="logoURL" alt="File Browser"> <img :src="logoURL" alt="File Browser">
<search v-if="isLogged"></search> <search v-if="isLogged" />
</div> </div>
<div> <div>
<template v-if="isLogged"> <template v-if="isLogged">
<button @click="openSearch" :aria-label="$t('buttons.search')" :title="$t('buttons.search')" class="search-button action"> <button :aria-label="$t('buttons.search')" :title="$t('buttons.search')" class="search-button action" @click="openSearch">
<i class="material-icons">search</i> <i class="material-icons">search</i>
</button> </button>
<button v-show="showSaveButton" :aria-label="$t('buttons.save')" :title="$t('buttons.save')" class="action" id="save-button"> <button v-show="showSaveButton" id="save-button" :aria-label="$t('buttons.save')" :title="$t('buttons.save')" class="action">
<i class="material-icons">save</i> <i class="material-icons">save</i>
</button> </button>
<button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action"> <button id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action" @click="openMore">
<i class="material-icons">more_vert</i> <i class="material-icons">more_vert</i>
</button> </button>
<!-- Menu that shows on listing AND mobile when there are files selected --> <!-- Menu that shows on listing AND mobile when there are files selected -->
<div id="file-selection" v-if="isMobile && isListing"> <div v-if="isMobile && isListing" id="file-selection">
<span v-if="selectedCount > 0">{{ selectedCount }} selected</span> <span v-if="selectedCount > 0">{{ selectedCount }} selected</span>
<share-button v-show="showShareButton"></share-button> <share-button v-show="showShareButton" />
<rename-button v-show="showRenameButton"></rename-button> <rename-button v-show="showRenameButton" />
<copy-button v-show="showCopyButton"></copy-button> <copy-button v-show="showCopyButton" />
<move-button v-show="showMoveButton"></move-button> <move-button v-show="showMoveButton" />
<delete-button v-show="showDeleteButton"></delete-button> <delete-button v-show="showDeleteButton" />
</div> </div>
<!-- This buttons are shown on a dropdown on mobile phones --> <!-- This buttons are shown on a dropdown on mobile phones -->
<div id="dropdown" :class="{ active: showMore }"> <div id="dropdown" :class="{ active: showMore }">
<div v-if="!isListing || !isMobile"> <div v-if="!isListing || !isMobile">
<share-button v-show="showShareButton"></share-button> <share-button v-show="showShareButton" />
<rename-button v-show="showRenameButton"></rename-button> <rename-button v-show="showRenameButton" />
<copy-button v-show="showCopyButton"></copy-button> <copy-button v-show="showCopyButton" />
<move-button v-show="showMoveButton"></move-button> <move-button v-show="showMoveButton" />
<delete-button v-show="showDeleteButton"></delete-button> <delete-button v-show="showDeleteButton" />
</div> </div>
<shell-button v-show="user.perm.execute" /> <shell-button v-show="user.perm.execute" />
<switch-button v-show="isListing"></switch-button> <switch-button v-show="isListing" />
<download-button v-show="showDownloadButton"></download-button> <download-button v-show="showDownloadButton" />
<upload-button v-show="showUpload"></upload-button> <upload-button v-show="showUpload" />
<info-button v-show="isFiles"></info-button> <info-button v-show="isFiles" />
<button v-show="isListing" @click="toggleMultipleSelection" :aria-label="$t('buttons.selectMultiple')" :title="$t('buttons.selectMultiple')" class="action" > <button v-show="isListing" :aria-label="$t('buttons.selectMultiple')" :title="$t('buttons.selectMultiple')" class="action" @click="toggleMultipleSelection">
<i class="material-icons">check_circle</i> <i class="material-icons">check_circle</i>
<span>{{ $t('buttons.select') }}</span> <span>{{ $t('buttons.select') }}</span>
</button> </button>
</div> </div>
</template> </template>
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div> <div v-show="showOverlay" class="overlay" @click="resetPrompts" />
</div> </div>
</header> </header>
</template> </template>
@ -71,13 +71,13 @@ import MoveButton from './buttons/Move'
import CopyButton from './buttons/Copy' import CopyButton from './buttons/Copy'
import ShareButton from './buttons/Share' import ShareButton from './buttons/Share'
import ShellButton from './buttons/Shell' import ShellButton from './buttons/Shell'
import {mapGetters, mapState} from 'vuex' import { mapGetters, mapState } from 'vuex'
import { logoURL } from '@/utils/constants' import { logoURL } from '@/utils/constants'
import * as api from '@/api' import * as api from '@/api'
import buttons from '@/utils/buttons' import buttons from '@/utils/buttons'
export default { export default {
name: 'header-layout', name: 'HeaderLayout',
components: { components: {
Search, Search,
InfoButton, InfoButton,
@ -91,7 +91,7 @@ export default {
MoveButton, MoveButton,
ShellButton ShellButton
}, },
data: function () { data: function() {
return { return {
width: window.innerWidth, width: window.innerWidth,
pluginData: { pluginData: {
@ -102,7 +102,7 @@ export default {
} }
} }
}, },
created () { created() {
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
this.width = window.innerWidth this.width = window.innerWidth
}) })
@ -123,65 +123,65 @@ export default {
'multiple' 'multiple'
]), ]),
logoURL: () => logoURL, logoURL: () => logoURL,
isMobile () { isMobile() {
return this.width <= 736 return this.width <= 736
}, },
showUpload () { showUpload() {
return this.isListing && this.user.perm.create return this.isListing && this.user.perm.create
}, },
showSaveButton () { showSaveButton() {
return this.isEditor && this.user.perm.modify return this.isEditor && this.user.perm.modify
}, },
showDownloadButton () { showDownloadButton() {
return this.isFiles && this.user.perm.download return this.isFiles && this.user.perm.download
}, },
showDeleteButton () { showDeleteButton() {
return this.isFiles && (this.isListing return this.isFiles && (this.isListing
? (this.selectedCount !== 0 && this.user.perm.delete) ? (this.selectedCount !== 0 && this.user.perm.delete)
: this.user.perm.delete) : this.user.perm.delete)
}, },
showRenameButton () { showRenameButton() {
return this.isFiles && (this.isListing return this.isFiles && (this.isListing
? (this.selectedCount === 1 && this.user.perm.rename) ? (this.selectedCount === 1 && this.user.perm.rename)
: this.user.perm.rename) : this.user.perm.rename)
}, },
showShareButton () { showShareButton() {
return this.isFiles && (this.isListing return this.isFiles && (this.isListing
? (this.selectedCount === 1 && this.user.perm.share) ? (this.selectedCount === 1 && this.user.perm.share)
: this.user.perm.share) : this.user.perm.share)
}, },
showMoveButton () { showMoveButton() {
return this.isFiles && (this.isListing return this.isFiles && (this.isListing
? (this.selectedCount > 0 && this.user.perm.rename) ? (this.selectedCount > 0 && this.user.perm.rename)
: this.user.perm.rename) : this.user.perm.rename)
}, },
showCopyButton () { showCopyButton() {
return this.isFiles && (this.isListing return this.isFiles && (this.isListing
? (this.selectedCount > 0 && this.user.perm.create) ? (this.selectedCount > 0 && this.user.perm.create)
: this.user.perm.create) : this.user.perm.create)
}, },
showMore () { showMore() {
return this.isFiles && this.$store.state.show === 'more' return this.isFiles && this.$store.state.show === 'more'
}, },
showOverlay () { showOverlay() {
return this.showMore return this.showMore
} }
}, },
methods: { methods: {
openSidebar () { openSidebar() {
this.$store.commit('showHover', 'sidebar') this.$store.commit('showHover', 'sidebar')
}, },
openMore () { openMore() {
this.$store.commit('showHover', 'more') this.$store.commit('showHover', 'more')
}, },
openSearch () { openSearch() {
this.$store.commit('showHover', 'search') this.$store.commit('showHover', 'search')
}, },
toggleMultipleSelection () { toggleMultipleSelection() {
this.$store.commit('multiple', !this.multiple) this.$store.commit('multiple', !this.multiple)
this.resetPrompts() this.resetPrompts()
}, },
resetPrompts () { resetPrompts() {
this.$store.commit('closeHovers') this.$store.commit('closeHovers')
} }
} }

View File

@ -1,25 +1,25 @@
<template> <template>
<div id="search" @click="open" v-bind:class="{ active , ongoing }"> <div id="search" :class="{ active , ongoing }" @click="open">
<div id="input"> <div id="input">
<button <button
v-if="active" v-if="active"
class="action" class="action"
@click="close"
:aria-label="$t('buttons.close')" :aria-label="$t('buttons.close')"
:title="$t('buttons.close')" :title="$t('buttons.close')"
@click="close"
> >
<i class="material-icons">arrow_back</i> <i class="material-icons">arrow_back</i>
</button> </button>
<i v-else class="material-icons">search</i> <i v-else class="material-icons">search</i>
<input <input
type="text"
@keyup.exact="keyup"
@keyup.enter="submit"
ref="input" ref="input"
:autofocus="active"
v-model.trim="value" v-model.trim="value"
type="text"
:autofocus="active"
:aria-label="$t('search.search')" :aria-label="$t('search.search')"
:placeholder="$t('search.search')" :placeholder="$t('search.search')"
@keyup.exact="keyup"
@keyup.enter="submit"
> >
</div> </div>
@ -33,14 +33,14 @@
<h3>{{ $t('search.types') }}</h3> <h3>{{ $t('search.types') }}</h3>
<div> <div>
<div <div
tabindex="0"
v-for="(v,k) in boxes" v-for="(v,k) in boxes"
:key="k" :key="k"
tabindex="0"
role="button" role="button"
@click="init('type:'+k)"
:aria-label="$t('search.'+v.label)" :aria-label="$t('search.'+v.label)"
@click="init('type:'+k)"
> >
<i class="material-icons">{{v.icon}}</i> <i class="material-icons">{{ v.icon }}</i>
<p>{{ $t('search.'+v.label) }}</p> <p>{{ $t('search.'+v.label) }}</p>
</div> </div>
</div> </div>
@ -49,7 +49,7 @@
</template> </template>
<ul v-show="results.length > 0"> <ul v-show="results.length > 0">
<li v-for="(s,k) in filteredResults" :key="k"> <li v-for="(s,k) in filteredResults" :key="k">
<router-link @click.native="close" :to="'./' + s.path"> <router-link :to="'./' + s.path" @click.native="close">
<i v-if="s.dir" class="material-icons">folder</i> <i v-if="s.dir" class="material-icons">folder</i>
<i v-else class="material-icons">insert_drive_file</i> <i v-else class="material-icons">insert_drive_file</i>
<span>./{{ s.path }}</span> <span>./{{ s.path }}</span>
@ -65,22 +65,22 @@
</template> </template>
<script> <script>
import { mapState, mapGetters, mapMutations } from "vuex" import { mapState, mapGetters, mapMutations } from 'vuex'
import url from "@/utils/url" import url from '@/utils/url'
import { search } from "@/api" import { search } from '@/api'
var boxes = { var boxes = {
image: { label: "images", icon: "insert_photo" }, image: { label: 'images', icon: 'insert_photo' },
audio: { label: "music", icon: "volume_up" }, audio: { label: 'music', icon: 'volume_up' },
video: { label: "video", icon: "movie" }, video: { label: 'video', icon: 'movie' },
pdf: { label: "pdf", icon: "picture_as_pdf" } pdf: { label: 'pdf', icon: 'picture_as_pdf' }
} }
export default { export default {
name: "search", name: 'Search',
data: function() { data: function() {
return { return {
value: "", value: '',
active: false, active: false,
ongoing: false, ongoing: false,
results: [], results: [],
@ -90,15 +90,15 @@ export default {
} }
}, },
watch: { watch: {
show (val, old) { show(val, old) {
this.active = val === "search" this.active = val === 'search'
if (old === "search" && !this.active) { if (old === 'search' && !this.active) {
if (this.reload) { if (this.reload) {
this.setReload(true) this.setReload(true)
} }
document.body.style.overflow = "auto" document.body.style.overflow = 'auto'
this.reset() this.reset()
this.value = '' this.value = ''
this.active = false this.active = false
@ -106,18 +106,18 @@ export default {
} else if (this.active) { } else if (this.active) {
this.reload = false this.reload = false
this.$refs.input.focus() this.$refs.input.focus()
document.body.style.overflow = "hidden" document.body.style.overflow = 'hidden'
} }
}, },
value () { value() {
if (this.results.length) { if (this.results.length) {
this.reset() this.reset()
} }
} }
}, },
computed: { computed: {
...mapState(["user", "show"]), ...mapState(['user', 'show']),
...mapGetters(["isListing"]), ...mapGetters(['isListing']),
boxes() { boxes() {
return boxes return boxes
}, },
@ -126,17 +126,17 @@ export default {
}, },
text() { text() {
if (this.ongoing) { if (this.ongoing) {
return "" return ''
} }
return this.value === '' ? this.$t("search.typeToSearch") : this.$t("search.pressToSearch") return this.value === '' ? this.$t('search.typeToSearch') : this.$t('search.pressToSearch')
}, },
filteredResults () { filteredResults() {
return this.results.slice(0, this.resultsCount) return this.results.slice(0, this.resultsCount)
} }
}, },
mounted() { mounted() {
window.addEventListener("keydown", event => { window.addEventListener('keydown', event => {
if (event.keyCode === 27) { if (event.keyCode === 27) {
this.closeHovers() this.closeHovers()
} }
@ -149,9 +149,9 @@ export default {
}) })
}, },
methods: { methods: {
...mapMutations(["showHover", "closeHovers", "setReload"]), ...mapMutations(['showHover', 'closeHovers', 'setReload']),
open() { open() {
this.showHover("search") this.showHover('search')
}, },
close(event) { close(event) {
event.stopPropagation() event.stopPropagation()
@ -166,11 +166,11 @@ export default {
this.results.length = 0 this.results.length = 0
}, },
init (string) { init(string) {
this.value = `${string} ` this.value = `${string} `
this.$refs.input.focus() this.$refs.input.focus()
}, },
reset () { reset() {
this.ongoing = false this.ongoing = false
this.resultsCount = 50 this.resultsCount = 50
this.results = [] this.results = []
@ -184,12 +184,11 @@ export default {
let path = this.$route.path let path = this.$route.path
if (!this.isListing) { if (!this.isListing) {
path = url.removeLastDir(path) + "/" path = url.removeLastDir(path) + '/'
} }
this.ongoing = true this.ongoing = true
this.results = await search(path, this.value) this.results = await search(path, this.value)
this.ongoing = false this.ongoing = false
} }

View File

@ -1,20 +1,21 @@
<template> <template>
<div @click="focus" class="shell" ref="scrollable" :class="{ ['shell--hidden']: !showShell}"> <div ref="scrollable" class="shell" :class="{ ['shell--hidden']: !showShell}" @click="focus">
<div v-for="(c, index) in content" :key="index" class="shell__result" > <div v-for="(c, index) in content" :key="index" class="shell__result">
<div class="shell__prompt"><i class="material-icons">chevron_right</i></div> <div class="shell__prompt"><i class="material-icons">chevron_right</i></div>
<pre class="shell__text">{{ c.text }}</pre> <pre class="shell__text">{{ c.text }}</pre>
</div> </div>
<div class="shell__result" :class="{ 'shell__result--hidden': !canInput }" > <div class="shell__result" :class="{ 'shell__result--hidden': !canInput }">
<div class="shell__prompt"><i class="material-icons">chevron_right</i></div> <div class="shell__prompt"><i class="material-icons">chevron_right</i></div>
<pre <pre
tabindex="0"
ref="input" ref="input"
tabindex="0"
class="shell__text" class="shell__text"
contenteditable="true" contenteditable="true"
@keydown.prevent.38="historyUp" @keydown.prevent.38="historyUp"
@keydown.prevent.40="historyDown" @keydown.prevent.40="historyDown"
@keypress.prevent.enter="submit" /> @keypress.prevent.enter="submit"
/>
</div> </div>
</div> </div>
</template> </template>
@ -24,11 +25,11 @@ import { mapMutations, mapState, mapGetters } from 'vuex'
import { commands } from '@/api' import { commands } from '@/api'
export default { export default {
name: 'shell', name: 'Shell',
computed: { computed: {
...mapState([ 'user', 'showShell' ]), ...mapState(['user', 'showShell']),
...mapGetters([ 'isFiles', 'isLogged' ]), ...mapGetters(['isFiles', 'isLogged']),
path: function () { path: function() {
if (this.isFiles) { if (this.isFiles) {
return this.$route.path return this.$route.path
} }
@ -43,20 +44,20 @@ export default {
canInput: true canInput: true
}), }),
methods: { methods: {
...mapMutations([ 'toggleShell' ]), ...mapMutations(['toggleShell']),
scroll: function () { scroll: function() {
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight
}, },
focus: function () { focus: function() {
this.$refs.input.focus() this.$refs.input.focus()
}, },
historyUp () { historyUp() {
if (this.historyPos > 0) { if (this.historyPos > 0) {
this.$refs.input.innerText = this.history[--this.historyPos] this.$refs.input.innerText = this.history[--this.historyPos]
this.focus() this.focus()
} }
}, },
historyDown () { historyDown() {
if (this.historyPos >= 0 && this.historyPos < this.history.length - 1) { if (this.historyPos >= 0 && this.historyPos < this.history.length - 1) {
this.$refs.input.innerText = this.history[++this.historyPos] this.$refs.input.innerText = this.history[++this.historyPos]
this.focus() this.focus()
@ -65,7 +66,7 @@ export default {
this.$refs.input.innerText = '' this.$refs.input.innerText = ''
} }
}, },
submit: function (event) { submit: function(event) {
const cmd = event.target.innerText.trim() const cmd = event.target.innerText.trim()
if (cmd === '') { if (cmd === '') {
@ -87,7 +88,7 @@ export default {
this.canInput = false this.canInput = false
event.target.innerHTML = '' event.target.innerHTML = ''
let results = { const results = {
text: `${cmd}\n\n` text: `${cmd}\n\n`
} }

View File

@ -7,12 +7,12 @@
</router-link> </router-link>
<div v-if="user.perm.create"> <div v-if="user.perm.create">
<button @click="$store.commit('showHover', 'newDir')" class="action" :aria-label="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')"> <button class="action" :aria-label="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')" @click="$store.commit('showHover', 'newDir')">
<i class="material-icons">create_new_folder</i> <i class="material-icons">create_new_folder</i>
<span>{{ $t('sidebar.newFolder') }}</span> <span>{{ $t('sidebar.newFolder') }}</span>
</button> </button>
<button @click="$store.commit('showHover', 'newFile')" class="action" :aria-label="$t('sidebar.newFile')" :title="$t('sidebar.newFile')"> <button class="action" :aria-label="$t('sidebar.newFile')" :title="$t('sidebar.newFile')" @click="$store.commit('showHover', 'newFile')">
<i class="material-icons">note_add</i> <i class="material-icons">note_add</i>
<span>{{ $t('sidebar.newFile') }}</span> <span>{{ $t('sidebar.newFile') }}</span>
</button> </button>
@ -24,7 +24,7 @@
<span>{{ $t('sidebar.settings') }}</span> <span>{{ $t('sidebar.settings') }}</span>
</router-link> </router-link>
<button v-if="authMethod == 'json'" @click="logout" class="action" id="logout" :aria-label="$t('sidebar.logout')" :title="$t('sidebar.logout')"> <button v-if="authMethod == 'json'" id="logout" class="action" :aria-label="$t('sidebar.logout')" :title="$t('sidebar.logout')" @click="logout">
<i class="material-icons">exit_to_app</i> <i class="material-icons">exit_to_app</i>
<span>{{ $t('sidebar.logout') }}</span> <span>{{ $t('sidebar.logout') }}</span>
</button> </button>
@ -59,11 +59,11 @@ import * as auth from '@/utils/auth'
import { version, signup, disableExternal, noAuth, authMethod } from '@/utils/constants' import { version, signup, disableExternal, noAuth, authMethod } from '@/utils/constants'
export default { export default {
name: 'sidebar', name: 'Sidebar',
computed: { computed: {
...mapState([ 'user' ]), ...mapState(['user']),
...mapGetters([ 'isLogged' ]), ...mapGetters(['isLogged']),
active () { active() {
return this.$store.state.show === 'sidebar' return this.$store.state.show === 'sidebar'
}, },
signup: () => signup, signup: () => signup,
@ -73,7 +73,7 @@ export default {
authMethod: () => authMethod authMethod: () => authMethod
}, },
methods: { methods: {
help () { help() {
this.$store.commit('showHover', 'help') this.$store.commit('showHover', 'help')
}, },
logout: auth.logout logout: auth.logout

View File

@ -1,5 +1,5 @@
<template> <template>
<button @click="show" :aria-label="$t('buttons.copy')" :title="$t('buttons.copy')" class="action" id="copy-button"> <button id="copy-button" :aria-label="$t('buttons.copy')" :title="$t('buttons.copy')" class="action" @click="show">
<i class="material-icons">content_copy</i> <i class="material-icons">content_copy</i>
<span>{{ $t('buttons.copyFile') }}</span> <span>{{ $t('buttons.copyFile') }}</span>
</button> </button>
@ -7,9 +7,9 @@
<script> <script>
export default { export default {
name: 'copy-button', name: 'CopyButton',
methods: { methods: {
show: function () { show: function() {
this.$store.commit('showHover', 'copy') this.$store.commit('showHover', 'copy')
} }
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<button @click="show" :aria-label="$t('buttons.delete')" :title="$t('buttons.delete')" class="action" id="delete-button"> <button id="delete-button" :aria-label="$t('buttons.delete')" :title="$t('buttons.delete')" class="action" @click="show">
<i class="material-icons">delete</i> <i class="material-icons">delete</i>
<span>{{ $t('buttons.delete') }}</span> <span>{{ $t('buttons.delete') }}</span>
</button> </button>
@ -7,9 +7,9 @@
<script> <script>
export default { export default {
name: 'delete-button', name: 'DeleteButton',
methods: { methods: {
show: function () { show: function() {
this.$store.commit('showHover', 'delete') this.$store.commit('showHover', 'delete')
} }
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<button @click="download" :aria-label="$t('buttons.download')" :title="$t('buttons.download')" id="download-button" class="action"> <button id="download-button" :aria-label="$t('buttons.download')" :title="$t('buttons.download')" class="action" @click="download">
<i class="material-icons">file_download</i> <i class="material-icons">file_download</i>
<span>{{ $t('buttons.download') }}</span> <span>{{ $t('buttons.download') }}</span>
<span v-if="selectedCount > 0" class="counter">{{ selectedCount }}</span> <span v-if="selectedCount > 0" class="counter">{{ selectedCount }}</span>
@ -7,17 +7,17 @@
</template> </template>
<script> <script>
import {mapGetters, mapState} from 'vuex' import { mapGetters, mapState } from 'vuex'
import { files as api } from '@/api' import { files as api } from '@/api'
export default { export default {
name: 'download-button', name: 'DownloadButton',
computed: { computed: {
...mapState(['req', 'selected']), ...mapState(['req', 'selected']),
...mapGetters(['isListing', 'selectedCount']) ...mapGetters(['isListing', 'selectedCount'])
}, },
methods: { methods: {
download: function () { download: function() {
if (!this.isListing) { if (!this.isListing) {
api.download(null, this.$route.path) api.download(null, this.$route.path)
return return

View File

@ -7,9 +7,9 @@
<script> <script>
export default { export default {
name: 'info-button', name: 'InfoButton',
methods: { methods: {
show: function () { show: function() {
this.$store.commit('showHover', 'info') this.$store.commit('showHover', 'info')
} }
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<button @click="show" :aria-label="$t('buttons.move')" :title="$t('buttons.move')" class="action" id="move-button"> <button id="move-button" :aria-label="$t('buttons.move')" :title="$t('buttons.move')" class="action" @click="show">
<i class="material-icons">forward</i> <i class="material-icons">forward</i>
<span>{{ $t('buttons.moveFile') }}</span> <span>{{ $t('buttons.moveFile') }}</span>
</button> </button>
@ -7,9 +7,9 @@
<script> <script>
export default { export default {
name: 'move-button', name: 'MoveButton',
methods: { methods: {
show: function () { show: function() {
this.$store.commit('showHover', 'move') this.$store.commit('showHover', 'move')
} }
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<button @click="show" :aria-label="$t('buttons.rename')" :title="$t('buttons.rename')" class="action" id="rename-button"> <button id="rename-button" :aria-label="$t('buttons.rename')" :title="$t('buttons.rename')" class="action" @click="show">
<i class="material-icons">mode_edit</i> <i class="material-icons">mode_edit</i>
<span>{{ $t('buttons.rename') }}</span> <span>{{ $t('buttons.rename') }}</span>
</button> </button>
@ -7,9 +7,9 @@
<script> <script>
export default { export default {
name: 'rename-button', name: 'RenameButton',
methods: { methods: {
show: function () { show: function() {
this.$store.commit('showHover', 'rename') this.$store.commit('showHover', 'rename')
} }
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<button @click="show" :aria-label="$t('buttons.share')" :title="$t('buttons.share')" class="action"> <button :aria-label="$t('buttons.share')" :title="$t('buttons.share')" class="action" @click="show">
<i class="material-icons">share</i> <i class="material-icons">share</i>
<span>{{ $t('buttons.share') }}</span> <span>{{ $t('buttons.share') }}</span>
</button> </button>
@ -7,9 +7,9 @@
<script> <script>
export default { export default {
name: 'share-button', name: 'ShareButton',
methods: { methods: {
show () { show() {
this.$store.commit('showHover', 'share') this.$store.commit('showHover', 'share')
} }
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<button @click="show" :aria-label="$t('buttons.shell')" :title="$t('buttons.shell')" class="action"> <button :aria-label="$t('buttons.shell')" :title="$t('buttons.shell')" class="action" @click="show">
<i class="material-icons">code</i> <i class="material-icons">code</i>
<span>{{ $t('buttons.shell') }}</span> <span>{{ $t('buttons.shell') }}</span>
</button> </button>
@ -7,9 +7,9 @@
<script> <script>
export default { export default {
name: 'shell-button', name: 'ShellButton',
methods: { methods: {
show: function () { show: function() {
this.$store.commit('toggleShell') this.$store.commit('toggleShell')
} }
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<button @click="change" :aria-label="$t('buttons.switchView')" :title="$t('buttons.switchView')" class="action" id="switch-view-button"> <button id="switch-view-button" :aria-label="$t('buttons.switchView')" :title="$t('buttons.switchView')" class="action" @click="change">
<i class="material-icons">{{ icon }}</i> <i class="material-icons">{{ icon }}</i>
<span>{{ $t('buttons.switchView') }}</span> <span>{{ $t('buttons.switchView') }}</span>
</button> </button>
@ -10,17 +10,17 @@ import { mapState, mapMutations } from 'vuex'
import { users as api } from '@/api' import { users as api } from '@/api'
export default { export default {
name: 'switch-button', name: 'SwitchButton',
computed: { computed: {
...mapState(['user']), ...mapState(['user']),
icon: function () { icon: function() {
if (this.user.viewMode === 'mosaic') return 'view_list' if (this.user.viewMode === 'mosaic') return 'view_list'
return 'view_module' return 'view_module'
} }
}, },
methods: { methods: {
...mapMutations([ 'updateUser', 'closeHovers' ]), ...mapMutations(['updateUser', 'closeHovers']),
change: async function () { change: async function() {
this.closeHovers() this.closeHovers()
const data = { const data = {

View File

@ -1,5 +1,5 @@
<template> <template>
<button @click="upload" :aria-label="$t('buttons.upload')" :title="$t('buttons.upload')" class="action" id="upload-button"> <button id="upload-button" :aria-label="$t('buttons.upload')" :title="$t('buttons.upload')" class="action" @click="upload">
<i class="material-icons">file_upload</i> <i class="material-icons">file_upload</i>
<span>{{ $t('buttons.upload') }}</span> <span>{{ $t('buttons.upload') }}</span>
</button> </button>
@ -7,9 +7,9 @@
<script> <script>
export default { export default {
name: 'upload-button', name: 'UploadButton',
methods: { methods: {
upload: function () { upload: function() {
document.getElementById('upload-input').click() document.getElementById('upload-input').click()
} }
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<form id="editor"></form> <form id="editor" />
</template> </template>
<script> <script>
@ -13,24 +13,24 @@ import 'ace-builds/webpack-resolver'
import { theme } from '@/utils/constants' import { theme } from '@/utils/constants'
export default { export default {
name: 'editor', name: 'Editor',
computed: { computed: {
...mapState(['req']) ...mapState(['req'])
}, },
data: function () { data: function() {
return {} return {}
}, },
created () { created() {
window.addEventListener('keydown', this.keyEvent) window.addEventListener('keydown', this.keyEvent)
document.getElementById('save-button').addEventListener('click', this.save) document.getElementById('save-button').addEventListener('click', this.save)
}, },
beforeDestroy () { beforeDestroy() {
window.removeEventListener('keydown', this.keyEvent) window.removeEventListener('keydown', this.keyEvent)
document.getElementById('save-button').removeEventListener('click', this.save) document.getElementById('save-button').removeEventListener('click', this.save)
this.editor.destroy(); this.editor.destroy()
}, },
mounted: function () { mounted: function() {
const fileContent = this.req.content || ''; const fileContent = this.req.content || ''
this.editor = ace.edit('editor', { this.editor = ace.edit('editor', {
maxLines: Infinity, maxLines: Infinity,
@ -43,12 +43,12 @@ export default {
wrap: true wrap: true
}) })
if (theme == 'dark') { if (theme === 'dark') {
this.editor.setTheme("ace/theme/twilight"); this.editor.setTheme('ace/theme/twilight')
} }
}, },
methods: { methods: {
keyEvent (event) { keyEvent(event) {
if (!event.ctrlKey && !event.metaKey) { if (!event.ctrlKey && !event.metaKey) {
return return
} }
@ -60,7 +60,7 @@ export default {
event.preventDefault() event.preventDefault()
this.save() this.save()
}, },
async save () { async save() {
const button = 'save' const button = 'save'
buttons.loading('save') buttons.loading('save')

View File

@ -1,7 +1,7 @@
<template> <template>
<div <div
class="image-ex-container"
ref="container" ref="container"
class="image-ex-container"
@touchstart="touchStart" @touchstart="touchStart"
@touchmove="touchMove" @touchmove="touchMove"
@dblclick="zoomAuto" @dblclick="zoomAuto"
@ -10,7 +10,7 @@
@mouseup="mouseUp" @mouseup="mouseUp"
@wheel="wheelMove" @wheel="wheelMove"
> >
<img :src="src" class="image-ex-img" ref="imgex" @load="setCenter"> <img ref="imgex" :src="src" class="image-ex-img" @load="setCenter">
</div> </div>
</template> </template>
<script> <script>
@ -54,20 +54,20 @@ export default {
} }
}, },
mounted() { mounted() {
let container = this.$refs.container const container = this.$refs.container
this.classList.forEach(className => container.classList.add(className)) this.classList.forEach(className => container.classList.add(className))
// set width and height if they are zero // set width and height if they are zero
if (getComputedStyle(container).width === "0px") { if (getComputedStyle(container).width === '0px') {
container.style.width = "100%" container.style.width = '100%'
} }
if (getComputedStyle(container).height === "0px") { if (getComputedStyle(container).height === '0px') {
container.style.height = "100%" container.style.height = '100%'
} }
}, },
methods: { methods: {
setCenter() { setCenter() {
let container = this.$refs.container const container = this.$refs.container
let img = this.$refs.imgex const img = this.$refs.imgex
let rate = Math.min( let rate = Math.min(
container.clientWidth / img.clientWidth, container.clientWidth / img.clientWidth,
@ -80,7 +80,7 @@ export default {
img.width = Math.floor(img.clientWidth * rate) img.width = Math.floor(img.clientWidth * rate)
img.style.top = `${Math.floor((container.clientHeight - img.clientHeight) / 2)}px` img.style.top = `${Math.floor((container.clientHeight - img.clientHeight) / 2)}px`
img.style.left = `${Math.floor((container.clientWidth - img.clientWidth) / 2)}px` img.style.left = `${Math.floor((container.clientWidth - img.clientWidth) / 2)}px`
document.addEventListener('mouseup', () => this.inDrag = false ) document.addEventListener('mouseup', () => { this.inDrag = false })
}, },
mousedownStart(event) { mousedownStart(event) {
this.lastX = null this.lastX = null
@ -126,7 +126,7 @@ export default {
this.lastY = event.targetTouches[0].pageY this.lastY = event.targetTouches[0].pageY
return return
} }
let step = this.$refs.imgex.width / 5 const step = this.$refs.imgex.width / 5
if (event.targetTouches.length === 2) { if (event.targetTouches.length === 2) {
this.moveDisabled = true this.moveDisabled = true
clearTimeout(this.disabledTimer) clearTimeout(this.disabledTimer)
@ -135,9 +135,9 @@ export default {
this.moveDisabledTime this.moveDisabledTime
) )
let p1 = event.targetTouches[0] const p1 = event.targetTouches[0]
let p2 = event.targetTouches[1] const p2 = event.targetTouches[1]
let touchDistance = Math.sqrt( const touchDistance = Math.sqrt(
Math.pow(p2.pageX - p1.pageX, 2) + Math.pow(p2.pageY - p1.pageY, 2) Math.pow(p2.pageX - p1.pageX, 2) + Math.pow(p2.pageY - p1.pageY, 2)
) )
if (!this.lastTouchDistance) { if (!this.lastTouchDistance) {
@ -149,8 +149,8 @@ export default {
this.setZoom() this.setZoom()
} else if (event.targetTouches.length === 1) { } else if (event.targetTouches.length === 1) {
if (this.moveDisabled) return if (this.moveDisabled) return
let x = event.targetTouches[0].pageX - this.lastX const x = event.targetTouches[0].pageX - this.lastX
let y = event.targetTouches[0].pageY - this.lastY const y = event.targetTouches[0].pageY - this.lastY
if (Math.abs(x) >= step && Math.abs(y) >= step) return if (Math.abs(x) >= step && Math.abs(y) >= step) return
this.lastX = event.targetTouches[0].pageX this.lastX = event.targetTouches[0].pageX
this.lastY = event.targetTouches[0].pageY this.lastY = event.targetTouches[0].pageY
@ -158,7 +158,7 @@ export default {
} }
}, },
doMove(x, y) { doMove(x, y) {
let style = this.$refs.imgex.style const style = this.$refs.imgex.style
style.left = `${this.pxStringToNumber(style.left) + x}px` style.left = `${this.pxStringToNumber(style.left) + x}px`
style.top = `${this.pxStringToNumber(style.top) + y}px` style.top = `${this.pxStringToNumber(style.top) + y}px`
}, },
@ -172,7 +172,7 @@ export default {
this.$refs.imgex.style.transform = `scale(${this.scale})` this.$refs.imgex.style.transform = `scale(${this.scale})`
}, },
pxStringToNumber(style) { pxStringToNumber(style) {
return +style.replace("px", "") return +style.replace('px', '')
} }
} }
} }

View File

@ -4,41 +4,53 @@
<i class="material-icons">sentiment_dissatisfied</i> <i class="material-icons">sentiment_dissatisfied</i>
<span>{{ $t('files.lonely') }}</span> <span>{{ $t('files.lonely') }}</span>
</h2> </h2>
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple> <input id="upload-input" style="display:none" type="file" multiple @change="uploadInput($event)">
</div> </div>
<div v-else id="listing" <div
v-else
id="listing"
:class="user.viewMode" :class="user.viewMode"
@dragenter="dragEnter" @dragenter="dragEnter"
@dragend="dragEnd"> @dragend="dragEnd"
>
<div> <div>
<div class="item header"> <div class="item header">
<div></div> <div />
<div> <div>
<p :class="{ active: nameSorted }" class="name" <p
:class="{ active: nameSorted }"
class="name"
role="button" role="button"
tabindex="0" tabindex="0"
@click="sort('name')"
:title="$t('files.sortByName')" :title="$t('files.sortByName')"
:aria-label="$t('files.sortByName')"> :aria-label="$t('files.sortByName')"
@click="sort('name')"
>
<span>{{ $t('files.name') }}</span> <span>{{ $t('files.name') }}</span>
<i class="material-icons">{{ nameIcon }}</i> <i class="material-icons">{{ nameIcon }}</i>
</p> </p>
<p :class="{ active: sizeSorted }" class="size" <p
:class="{ active: sizeSorted }"
class="size"
role="button" role="button"
tabindex="0" tabindex="0"
@click="sort('size')"
:title="$t('files.sortBySize')" :title="$t('files.sortBySize')"
:aria-label="$t('files.sortBySize')"> :aria-label="$t('files.sortBySize')"
@click="sort('size')"
>
<span>{{ $t('files.size') }}</span> <span>{{ $t('files.size') }}</span>
<i class="material-icons">{{ sizeIcon }}</i> <i class="material-icons">{{ sizeIcon }}</i>
</p> </p>
<p :class="{ active: modifiedSorted }" class="modified" <p
:class="{ active: modifiedSorted }"
class="modified"
role="button" role="button"
tabindex="0" tabindex="0"
@click="sort('modified')"
:title="$t('files.sortByLastModified')" :title="$t('files.sortByLastModified')"
:aria-label="$t('files.sortByLastModified')"> :aria-label="$t('files.sortByLastModified')"
@click="sort('modified')"
>
<span>{{ $t('files.lastModified') }}</span> <span>{{ $t('files.lastModified') }}</span>
<i class="material-icons">{{ modifiedIcon }}</i> <i class="material-icons">{{ modifiedIcon }}</i>
</p> </p>
@ -48,37 +60,39 @@
<h2 v-if="req.numDirs > 0">{{ $t('files.folders') }}</h2> <h2 v-if="req.numDirs > 0">{{ $t('files.folders') }}</h2>
<div v-if="req.numDirs > 0"> <div v-if="req.numDirs > 0">
<item v-for="(item) in dirs" <item
v-for="(item) in dirs"
:key="base64(item.name)" :key="base64(item.name)"
v-bind:index="item.index" :index="item.index"
v-bind:name="item.name" :name="item.name"
v-bind:isDir="item.isDir" :is-dir="item.isDir"
v-bind:url="item.url" :url="item.url"
v-bind:modified="item.modified" :modified="item.modified"
v-bind:type="item.type" :type="item.type"
v-bind:size="item.size"> :size="item.size"
</item> />
</div> </div>
<h2 v-if="req.numFiles > 0">{{ $t('files.files') }}</h2> <h2 v-if="req.numFiles > 0">{{ $t('files.files') }}</h2>
<div v-if="req.numFiles > 0"> <div v-if="req.numFiles > 0">
<item v-for="(item) in files" <item
v-for="(item) in files"
:key="base64(item.name)" :key="base64(item.name)"
v-bind:index="item.index" :index="item.index"
v-bind:name="item.name" :name="item.name"
v-bind:isDir="item.isDir" :is-dir="item.isDir"
v-bind:url="item.url" :url="item.url"
v-bind:modified="item.modified" :modified="item.modified"
v-bind:type="item.type" :type="item.type"
v-bind:size="item.size"> :size="item.size"
</item> />
</div> </div>
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple> <input id="upload-input" style="display:none" type="file" multiple @change="uploadInput($event)">
<div :class="{ active: $store.state.multiple }" id="multiple-selection"> <div id="multiple-selection" :class="{ active: $store.state.multiple }">
<p>{{ $t('files.multipleSelectionEnabled') }}</p> <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"> <div tabindex="0" role="button" :title="$t('files.clear')" :aria-label="$t('files.clear')" class="action" @click="$store.commit('multiple', false)">
<i class="material-icons">clear</i> <i class="material-icons">clear</i>
</div> </div>
</div> </div>
@ -94,28 +108,28 @@ import buttons from '@/utils/buttons'
import url from '@/utils/url' import url from '@/utils/url'
export default { export default {
name: 'listing', name: 'Listing',
components: { Item }, components: { Item },
data: function () { data: function() {
return { return {
show: 50 show: 50
} }
}, },
computed: { computed: {
...mapState(['req', 'selected', 'user']), ...mapState(['req', 'selected', 'user']),
nameSorted () { nameSorted() {
return (this.req.sorting.by === 'name') return (this.req.sorting.by === 'name')
}, },
sizeSorted () { sizeSorted() {
return (this.req.sorting.by === 'size') return (this.req.sorting.by === 'size')
}, },
modifiedSorted () { modifiedSorted() {
return (this.req.sorting.by === 'modified') return (this.req.sorting.by === 'modified')
}, },
ascOrdered () { ascOrdered() {
return this.req.sorting.asc return this.req.sorting.asc
}, },
items () { items() {
const dirs = [] const dirs = []
const files = [] const files = []
@ -129,31 +143,31 @@ export default {
return { dirs, files } return { dirs, files }
}, },
dirs () { dirs() {
return this.items.dirs.slice(0, this.show) return this.items.dirs.slice(0, this.show)
}, },
files () { files() {
let show = this.show - this.items.dirs.length let show = this.show - this.items.dirs.length
if (show < 0) show = 0 if (show < 0) show = 0
return this.items.files.slice(0, show) return this.items.files.slice(0, show)
}, },
nameIcon () { nameIcon() {
if (this.nameSorted && !this.ascOrdered) { if (this.nameSorted && !this.ascOrdered) {
return 'arrow_upward' return 'arrow_upward'
} }
return 'arrow_downward' return 'arrow_downward'
}, },
sizeIcon () { sizeIcon() {
if (this.sizeSorted && this.ascOrdered) { if (this.sizeSorted && this.ascOrdered) {
return 'arrow_downward' return 'arrow_downward'
} }
return 'arrow_upward' return 'arrow_upward'
}, },
modifiedIcon () { modifiedIcon() {
if (this.modifiedSorted && this.ascOrdered) { if (this.modifiedSorted && this.ascOrdered) {
return 'arrow_downward' return 'arrow_downward'
} }
@ -161,7 +175,7 @@ export default {
return 'arrow_upward' return 'arrow_upward'
} }
}, },
mounted: function () { mounted: function() {
// Check the columns size for the first time. // Check the columns size for the first time.
this.resizeEvent() this.resizeEvent()
@ -172,7 +186,7 @@ export default {
document.addEventListener('dragover', this.preventDefault) document.addEventListener('dragover', this.preventDefault)
document.addEventListener('drop', this.drop) document.addEventListener('drop', this.drop)
}, },
beforeDestroy () { beforeDestroy() {
// Remove event listeners before destroying this page. // Remove event listeners before destroying this page.
window.removeEventListener('keydown', this.keyEvent) window.removeEventListener('keydown', this.keyEvent)
window.removeEventListener('resize', this.resizeEvent) window.removeEventListener('resize', this.resizeEvent)
@ -181,16 +195,16 @@ export default {
document.removeEventListener('drop', this.drop) document.removeEventListener('drop', this.drop)
}, },
methods: { methods: {
...mapMutations([ 'updateUser' ]), ...mapMutations(['updateUser']),
base64: function (name) { base64: function(name) {
return window.btoa(unescape(encodeURIComponent(name))) return window.btoa(unescape(encodeURIComponent(name)))
}, },
keyEvent (event) { keyEvent(event) {
if (!event.ctrlKey && !event.metaKey) { if (!event.ctrlKey && !event.metaKey) {
return return
} }
let key = String.fromCharCode(event.which).toLowerCase() const key = String.fromCharCode(event.which).toLowerCase()
switch (key) { switch (key) {
case 'f': case 'f':
@ -206,25 +220,25 @@ export default {
break break
} }
}, },
preventDefault (event) { preventDefault(event) {
// Wrapper around prevent default. // Wrapper around prevent default.
event.preventDefault() event.preventDefault()
}, },
copyCut (event, key) { copyCut(event, key) {
if (event.target.tagName.toLowerCase() === 'input') { if (event.target.tagName.toLowerCase() === 'input') {
return return
} }
let items = [] const items = []
for (let i of this.selected) { for (const i of this.selected) {
items.push({ items.push({
from: this.req.items[i].url, from: this.req.items[i].url,
name: encodeURIComponent(this.req.items[i].name) name: encodeURIComponent(this.req.items[i].name)
}) })
} }
if (items.length == 0) { if (items.length === 0) {
return return
} }
@ -233,14 +247,14 @@ export default {
items: items items: items
}) })
}, },
paste (event) { paste(event) {
if (event.target.tagName.toLowerCase() === 'input') { if (event.target.tagName.toLowerCase() === 'input') {
return return
} }
let items = [] const items = []
for (let item of this.$store.state.clipboard.items) { for (const item of this.$store.state.clipboard.items) {
const from = item.from.endsWith('/') ? item.from.slice(0, -1) : item.from const from = item.from.endsWith('/') ? item.from.slice(0, -1) : item.from
const to = this.$route.path + item.name const to = this.$route.path + item.name
items.push({ from, to }) items.push({ from, to })
@ -261,36 +275,36 @@ export default {
this.$store.commit('setReload', true) this.$store.commit('setReload', true)
}).catch(this.$showError) }).catch(this.$showError)
}, },
resizeEvent () { resizeEvent() {
// Update the columns size based on the window width. // Update the columns size based on the window width.
let columns = Math.floor(document.querySelector('main').offsetWidth / 300) let columns = Math.floor(document.querySelector('main').offsetWidth / 300)
let items = css(['#listing.mosaic .item', '.mosaic#listing .item']) const items = css(['#listing.mosaic .item', '.mosaic#listing .item'])
if (columns === 0) columns = 1 if (columns === 0) columns = 1
items.style.width = `calc(${100 / columns}% - 1em)` items.style.width = `calc(${100 / columns}% - 1em)`
}, },
scrollEvent () { scrollEvent() {
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) { if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
this.show += 50 this.show += 50
} }
}, },
dragEnter () { dragEnter() {
// When the user starts dragging an item, put every // When the user starts dragging an item, put every
// file on the listing with 50% opacity. // file on the listing with 50% opacity.
let items = document.getElementsByClassName('item') const items = document.getElementsByClassName('item')
Array.from(items).forEach(file => { Array.from(items).forEach(file => {
file.style.opacity = 0.5 file.style.opacity = 0.5
}) })
}, },
dragEnd () { dragEnd() {
this.resetOpacity() this.resetOpacity()
}, },
drop: function (event) { drop: function(event) {
event.preventDefault() event.preventDefault()
this.resetOpacity() this.resetOpacity()
let dt = event.dataTransfer const dt = event.dataTransfer
let files = dt.files const files = dt.files
let el = event.target let el = event.target
if (files.length <= 0) return if (files.length <= 0) return
@ -318,14 +332,14 @@ export default {
this.checkConflict(files, this.req.items, base) this.checkConflict(files, this.req.items, base)
}, },
checkConflict (files, items, base) { checkConflict(files, items, base) {
if (typeof items === 'undefined' || items === null) { if (typeof items === 'undefined' || items === null) {
items = [] items = []
} }
let conflict = false let conflict = false
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
let res = items.findIndex(function hasConflict (element) { const res = items.findIndex(function hasConflict(element) {
return (element.name === this) return (element.name === this)
}, files[i].name) }, files[i].name)
@ -349,22 +363,22 @@ export default {
} }
}) })
}, },
uploadInput (event) { uploadInput(event) {
this.checkConflict(event.currentTarget.files, this.req.items, '') this.checkConflict(event.currentTarget.files, this.req.items, '')
}, },
resetOpacity () { resetOpacity() {
let items = document.getElementsByClassName('item') const items = document.getElementsByClassName('item')
Array.from(items).forEach(file => { Array.from(items).forEach(file => {
file.style.opacity = 1 file.style.opacity = 1
}) })
}, },
handleFiles (files, base, overwrite = false) { handleFiles(files, base, overwrite = false) {
buttons.loading('upload') buttons.loading('upload')
let promises = [] const promises = []
let progress = new Array(files.length).fill(0) const progress = new Array(files.length).fill(0)
let onupload = (id) => (event) => { const onupload = (id) => (event) => {
progress[id] = (event.loaded / event.total) * 100 progress[id] = (event.loaded / event.total) * 100
let sum = 0 let sum = 0
@ -376,12 +390,12 @@ export default {
} }
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
let file = files[i] const file = files[i]
let filenameEncoded = url.encodeRFC5987ValueChars(file.name) const filenameEncoded = url.encodeRFC5987ValueChars(file.name)
promises.push(api.post(this.$route.path + base + filenameEncoded, file, overwrite, onupload(i))) promises.push(api.post(this.$route.path + base + filenameEncoded, file, overwrite, onupload(i)))
} }
let finish = () => { const finish = () => {
buttons.success('upload') buttons.success('upload')
this.$store.commit('setProgress', 0) this.$store.commit('setProgress', 0)
} }
@ -398,7 +412,7 @@ export default {
return false return false
}, },
async sort (by) { async sort(by) {
let asc = false let asc = false
if (by === 'name') { if (by === 'name') {
@ -416,7 +430,7 @@ export default {
} }
try { try {
await users.update({ id: this.user.id, sorting: { by, asc } }, ['sorting']) await users.update({ id: this.user.id, sorting: { by, asc }}, ['sorting'])
} catch (e) { } catch (e) {
this.$showError(e) this.$showError(e)
} }

View File

@ -13,7 +13,8 @@
:aria-label="name" :aria-label="name"
:aria-selected="isSelected"> :aria-selected="isSelected">
<div> <div>
<i class="material-icons">{{ icon }}</i> <img v-if="type==='image'" :src="thumbnailUrl">
<i v-else class="material-icons">{{ icon }}</i>
</div> </div>
<div> <div>
@ -31,25 +32,26 @@
<script> <script>
import { mapMutations, mapGetters, mapState } from 'vuex' import { mapMutations, mapGetters, mapState } from 'vuex'
import { baseURL } from '@/utils/constants'
import filesize from 'filesize' import filesize from 'filesize'
import moment from 'moment' import moment from 'moment'
import { files as api } from '@/api' import { files as api } from '@/api'
export default { export default {
name: 'item', name: 'Item',
data: function () { props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index'],
data: function() {
return { return {
touches: 0 touches: 0
} }
}, },
props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index'],
computed: { computed: {
...mapState(['selected', 'req', 'user']), ...mapState(['selected', 'req', 'jwt']),
...mapGetters(['selectedCount']), ...mapGetters(['selectedCount']),
isSelected () { isSelected() {
return (this.selected.indexOf(this.index) !== -1) return this.selected.indexOf(this.index) !== -1
}, },
icon () { icon() {
if (this.isDir) return 'folder' if (this.isDir) return 'folder'
if (this.type === 'image') return 'insert_photo' if (this.type === 'image') return 'insert_photo'
if (this.type === 'audio') return 'volume_up' if (this.type === 'audio') return 'volume_up'
@ -59,27 +61,31 @@ export default {
isDraggable () { isDraggable () {
return this.user.perm.rename return this.user.perm.rename
}, },
canDrop () { canDrop() {
if (!this.isDir) return false if (!this.isDir) return false
for (let i of this.selected) { for (const i of this.selected) {
if (this.req.items[i].url === this.url) { if (this.req.items[i].url === this.url) {
return false return false
} }
} }
return true return true
},
thumbnailUrl() {
const path = this.url.replace(/^\/files\//, '')
return `${baseURL}/api/thumbnail/${path}?auth=${this.jwt}&inline=true`
} }
}, },
methods: { methods: {
...mapMutations(['addSelected', 'removeSelected', 'resetSelected']), ...mapMutations(['addSelected', 'removeSelected', 'resetSelected']),
humanSize: function () { humanSize: function() {
return filesize(this.size) return filesize(this.size)
}, },
humanTime: function () { humanTime: function() {
return moment(this.modified).fromNow() return moment(this.modified).fromNow()
}, },
dragStart: function () { dragStart: function() {
if (this.selectedCount === 0) { if (this.selectedCount === 0) {
this.addSelected(this.index) this.addSelected(this.index)
return return
@ -90,7 +96,7 @@ export default {
this.addSelected(this.index) this.addSelected(this.index)
} }
}, },
dragOver: function (event) { dragOver: function(event) {
if (!this.canDrop) return if (!this.canDrop) return
event.preventDefault() event.preventDefault()
@ -104,28 +110,29 @@ export default {
el.style.opacity = 1 el.style.opacity = 1
}, },
drop: function (event) { drop: function(event) {
if (!this.canDrop) return if (!this.canDrop) return
event.preventDefault() event.preventDefault()
if (this.selectedCount === 0) return if (this.selectedCount === 0) return
let items = [] const items = []
for (let i of this.selected) { for (const i of this.selected) {
items.push({ items.push({
from: this.req.items[i].url, from: this.req.items[i].url,
to: this.url + this.req.items[i].name to: this.url + this.req.items[i].name
}) })
} }
api.move(items) api
.move(items)
.then(() => { .then(() => {
this.$store.commit('setReload', true) this.$store.commit('setReload', true)
}) })
.catch(this.$showError) .catch(this.$showError)
}, },
click: function (event) { click: function(event) {
if (this.selectedCount !== 0) event.preventDefault() if (this.selectedCount !== 0) event.preventDefault()
if (this.$store.state.selected.indexOf(this.index) !== -1) { if (this.$store.state.selected.indexOf(this.index) !== -1) {
this.removeSelected(this.index) this.removeSelected(this.index)
@ -154,7 +161,7 @@ export default {
if (!event.ctrlKey && !this.$store.state.multiple) this.resetSelected() if (!event.ctrlKey && !this.$store.state.multiple) this.resetSelected()
this.addSelected(this.index) this.addSelected(this.index)
}, },
touchstart () { touchstart() {
setTimeout(() => { setTimeout(() => {
this.touches = 0 this.touches = 0
}, 300) }, 300)
@ -164,8 +171,8 @@ export default {
this.open() this.open()
} }
}, },
open: function () { open: function() {
this.$router.push({path: this.url}) this.$router.push({ path: this.url })
} }
} }
} }

View File

@ -1,40 +1,63 @@
<template> <template>
<div id="previewer"> <div id="previewer">
<div class="bar"> <div class="bar">
<button @click="back" class="action" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close"> <button
id="close"
class="action"
:title="$t('files.closePreview')"
:aria-label="$t('files.closePreview')"
@click="back"
>
<i class="material-icons">close</i> <i class="material-icons">close</i>
</button> </button>
<rename-button v-if="user.perm.rename"></rename-button> <rename-button v-if="user.perm.rename" />
<delete-button v-if="user.perm.delete"></delete-button> <delete-button v-if="user.perm.delete" />
<download-button v-if="user.perm.download"></download-button> <download-button v-if="user.perm.download" />
<info-button></info-button> <info-button />
</div> </div>
<button class="action" @click="prev" v-show="hasPrevious" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')"> <button
v-show="hasPrevious"
class="action"
:aria-label="$t('buttons.previous')"
:title="$t('buttons.previous')"
@click="prev"
>
<i class="material-icons">chevron_left</i> <i class="material-icons">chevron_left</i>
</button> </button>
<button class="action" @click="next" v-show="hasNext" :aria-label="$t('buttons.next')" :title="$t('buttons.next')"> <button
v-show="hasNext"
class="action"
:aria-label="$t('buttons.next')"
:title="$t('buttons.next')"
@click="next"
>
<i class="material-icons">chevron_right</i> <i class="material-icons">chevron_right</i>
</button> </button>
<div class="preview"> <div class="preview">
<ExtendedImage v-if="req.type == 'image'" :src="raw"></ExtendedImage> <ExtendedImage v-if="req.type == 'image'" :src="raw" />
<audio v-else-if="req.type == 'audio'" :src="raw" autoplay controls></audio> <audio v-else-if="req.type == 'audio'" :src="raw" autoplay controls />
<video v-else-if="req.type == 'video'" :src="raw" autoplay controls> <video v-else-if="req.type == 'video'" :src="raw" autoplay controls>
<track <track
kind="captions"
v-for="(sub, index) in subtitles" v-for="(sub, index) in subtitles"
:key="index" :key="index"
kind="captions"
:src="sub" :src="sub"
:label="'Subtitle ' + index" :default="index === 0"> :label="'Subtitle ' + index"
Sorry, your browser doesn't support embedded videos, :default="index === 0"
but don't worry, you can <a :href="download">download it</a> >Sorry, your browser doesn't support embedded videos,
but don't worry, you can
<a :href="download">download it</a>
and watch it with your favorite video player! and watch it with your favorite video player!
</video> </video>
<object v-else-if="req.extension == '.pdf'" class="pdf" :data="raw"></object> <object v-else-if="req.extension == '.pdf'" class="pdf" :data="raw" />
<a v-else-if="req.type == 'blob'" :href="download"> <a v-else-if="req.type == 'blob'" :href="download">
<h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2> <h2 class="message">
{{ $t('buttons.download') }}
<i class="material-icons">file_download</i>
</h2>
</a> </a>
</div> </div>
</div> </div>
@ -51,15 +74,10 @@ import RenameButton from '@/components/buttons/Rename'
import DownloadButton from '@/components/buttons/Download' import DownloadButton from '@/components/buttons/Download'
import ExtendedImage from './ExtendedImage' import ExtendedImage from './ExtendedImage'
const mediaTypes = [ const mediaTypes = ['image', 'video', 'audio', 'blob']
"image",
"video",
"audio",
"blob"
]
export default { export default {
name: 'preview', name: 'Preview',
components: { components: {
InfoButton, InfoButton,
DeleteButton, DeleteButton,
@ -67,7 +85,7 @@ export default {
DownloadButton, DownloadButton,
ExtendedImage ExtendedImage
}, },
data: function () { data: function() {
return { return {
previousLink: '', previousLink: '',
nextLink: '', nextLink: '',
@ -77,24 +95,33 @@ export default {
}, },
computed: { computed: {
...mapState(['req', 'user', 'oldReq', 'jwt']), ...mapState(['req', 'user', 'oldReq', 'jwt']),
hasPrevious () { hasPrevious() {
return (this.previousLink !== '') return this.previousLink !== ''
}, },
hasNext () { hasNext() {
return (this.nextLink !== '') return this.nextLink !== ''
}, },
download () { download() {
return `${baseURL}/api/raw${this.req.path}?auth=${this.jwt}` return `${baseURL}/api/raw${this.req.path}?auth=${this.jwt}`
}, },
raw () { thumbnail() {
return `${this.download}&inline=true` if (this.req.type === 'image') {
return `${baseURL}/api/thumbnail${this.req.path}?auth=${this.jwt}`
} else {
return `${baseURL}/api/raw${this.req.path}?auth=${this.jwt}`
} }
}, },
async mounted () { raw() {
return `${this.thumbnail}&inline=true`
}
},
async mounted() {
window.addEventListener('keyup', this.key) window.addEventListener('keyup', this.key)
if (this.req.subtitles) { if (this.req.subtitles) {
this.subtitles = this.req.subtitles.map(sub => `${baseURL}/api/raw${sub}?auth=${this.jwt}&inline=true`) this.subtitles = this.req.subtitles.map(
sub => `${baseURL}/api/raw${sub}?auth=${this.jwt}&inline=true`
)
} }
try { try {
@ -109,30 +136,32 @@ export default {
this.$showError(e) this.$showError(e)
} }
}, },
beforeDestroy () { beforeDestroy() {
window.removeEventListener('keyup', this.key) window.removeEventListener('keyup', this.key)
}, },
methods: { methods: {
back () { back() {
let uri = url.removeLastDir(this.$route.path) + '/' const uri = url.removeLastDir(this.$route.path) + '/'
this.$router.push({ path: uri }) this.$router.push({ path: uri })
}, },
prev () { prev() {
this.$router.push({ path: this.previousLink }) this.$router.push({ path: this.previousLink })
}, },
next () { next() {
this.$router.push({ path: this.nextLink }) this.$router.push({ path: this.nextLink })
}, },
key (event) { key(event) {
event.preventDefault() event.preventDefault()
if (event.which === 13 || event.which === 39) { // right arrow if (event.which === 13 || event.which === 39) {
// right arrow
if (this.hasNext) this.next() if (this.hasNext) this.next()
} else if (event.which === 37) { // left arrow } else if (event.which === 37) {
// left arrow
if (this.hasPrevious) this.prev() if (this.hasPrevious) this.prev()
} }
}, },
updateLinks (items) { updateLinks(items) {
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
if (items[i].name !== this.req.name) { if (items[i].name !== this.req.name) {
continue continue

View File

@ -6,19 +6,23 @@
<div class="card-content"> <div class="card-content">
<p>{{ $t('prompts.copyMessage') }}</p> <p>{{ $t('prompts.copyMessage') }}</p>
<file-list @update:selected="val => dest = val"></file-list> <file-list @update:selected="val => dest = val" />
</div> </div>
<div class="card-action"> <div class="card-action">
<button class="button button--flat button--grey" <button
@click="$store.commit('closeHovers')" class="button button--flat button--grey"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> :title="$t('buttons.cancel')"
<button class="button button--flat" @click="$store.commit('closeHovers')"
@click="copy" >{{ $t('buttons.cancel') }}</button>
<button
class="button button--flat"
:disabled="$route.path === dest" :disabled="$route.path === dest"
:aria-label="$t('buttons.copy')" :aria-label="$t('buttons.copy')"
:title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button> :title="$t('buttons.copy')"
@click="copy"
>{{ $t('buttons.copy') }}</button>
</div> </div>
</div> </div>
</template> </template>
@ -30,9 +34,9 @@ import { files as api } from '@/api'
import buttons from '@/utils/buttons' import buttons from '@/utils/buttons'
export default { export default {
name: 'copy', name: 'Copy',
components: { FileList }, components: { FileList },
data: function () { data: function() {
return { return {
current: window.location.pathname, current: window.location.pathname,
dest: null dest: null
@ -40,13 +44,13 @@ export default {
}, },
computed: mapState(['req', 'selected']), computed: mapState(['req', 'selected']),
methods: { methods: {
copy: async function (event) { copy: async function(event) {
event.preventDefault() event.preventDefault()
buttons.loading('copy') buttons.loading('copy')
let items = [] const items = []
// Create a new promise for each file. // Create a new promise for each file.
for (let item of this.selected) { for (const item of this.selected) {
items.push({ items.push({
from: this.req.items[item].url, from: this.req.items[item].url,
to: this.dest + encodeURIComponent(this.req.items[item].name) to: this.dest + encodeURIComponent(this.req.items[item].name)

View File

@ -5,33 +5,37 @@
<p v-else>{{ $t('prompts.deleteMessageMultiple', { count: selectedCount}) }}</p> <p v-else>{{ $t('prompts.deleteMessageMultiple', { count: selectedCount}) }}</p>
</div> </div>
<div class="card-action"> <div class="card-action">
<button @click="$store.commit('closeHovers')" <button
class="button button--flat button--grey" class="button button--flat button--grey"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> :title="$t('buttons.cancel')"
<button @click="submit" @click="$store.commit('closeHovers')"
>{{ $t('buttons.cancel') }}</button>
<button
class="button button--flat button--red" class="button button--flat button--red"
:aria-label="$t('buttons.delete')" :aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button> :title="$t('buttons.delete')"
@click="submit"
>{{ $t('buttons.delete') }}</button>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import {mapGetters, mapMutations, mapState} from 'vuex' import { mapGetters, mapMutations, mapState } from 'vuex'
import { files as api } from '@/api' import { files as api } from '@/api'
import url from '@/utils/url' import url from '@/utils/url'
import buttons from '@/utils/buttons' import buttons from '@/utils/buttons'
export default { export default {
name: 'delete', name: 'Delete',
computed: { computed: {
...mapGetters(['isListing', 'selectedCount']), ...mapGetters(['isListing', 'selectedCount']),
...mapState(['req', 'selected']) ...mapState(['req', 'selected'])
}, },
methods: { methods: {
...mapMutations(['closeHovers']), ...mapMutations(['closeHovers']),
submit: async function () { submit: async function() {
this.closeHovers() this.closeHovers()
buttons.loading('delete') buttons.loading('delete')
@ -47,8 +51,8 @@ export default {
return return
} }
let promises = [] const promises = []
for (let index of this.selected) { for (const index of this.selected) {
promises.push(api.remove(this.req.items[index].url)) promises.push(api.remove(this.req.items[index].url))
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="card floating" id="download"> <div id="download" class="card floating">
<div class="card-title"> <div class="card-title">
<h2>{{ $t('prompts.download') }}</h2> <h2>{{ $t('prompts.download') }}</h2>
</div> </div>
@ -7,35 +7,35 @@
<div class="card-content"> <div class="card-content">
<p>{{ $t('prompts.downloadMessage') }}</p> <p>{{ $t('prompts.downloadMessage') }}</p>
<button class="button button--block" @click="download('zip')" v-focus>zip</button> <button v-focus class="button button--block" @click="download('zip')">zip</button>
<button class="button button--block" @click="download('tar')" v-focus>tar</button> <button v-focus class="button button--block" @click="download('tar')">tar</button>
<button class="button button--block" @click="download('targz')" v-focus>tar.gz</button> <button v-focus class="button button--block" @click="download('targz')">tar.gz</button>
<button class="button button--block" @click="download('tarbz2')" v-focus>tar.bz2</button> <button v-focus class="button button--block" @click="download('tarbz2')">tar.bz2</button>
<button class="button button--block" @click="download('tarxz')" v-focus>tar.xz</button> <button v-focus class="button button--block" @click="download('tarxz')">tar.xz</button>
<button class="button button--block" @click="download('tarlz4')" v-focus>tar.lz4</button> <button v-focus class="button button--block" @click="download('tarlz4')">tar.lz4</button>
<button class="button button--block" @click="download('tarsz')" v-focus>tar.sz</button> <button v-focus class="button button--block" @click="download('tarsz')">tar.sz</button>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import {mapGetters, mapState} from 'vuex' import { mapGetters, mapState } from 'vuex'
import { files as api } from '@/api' import { files as api } from '@/api'
export default { export default {
name: 'download', name: 'Download',
computed: { computed: {
...mapState(['selected', 'req']), ...mapState(['selected', 'req']),
...mapGetters(['selectedCount']) ...mapGetters(['selectedCount'])
}, },
methods: { methods: {
download: function (format) { download: function(format) {
if (this.selectedCount === 0) { if (this.selectedCount === 0) {
api.download(format, this.$route.path) api.download(format, this.$route.path)
} else { } else {
let files = [] const files = []
for (let i of this.selected) { for (const i of this.selected) {
files.push(this.req.items[i].url) files.push(this.req.items[i].url)
} }

View File

@ -1,15 +1,18 @@
<template> <template>
<div> <div>
<ul class="file-list"> <ul class="file-list">
<li @click="select" <li
@touchstart="touchstart" v-for="item in items"
@dblclick="next" :key="item.name"
role="button" role="button"
tabindex="0" tabindex="0"
:aria-label="item.name" :aria-label="item.name"
:aria-selected="selected == item.url" :aria-selected="selected == item.url"
:key="item.name" v-for="item in items" :data-url="item.url"
:data-url="item.url">{{ item.name }}</li> @click="select"
@touchstart="touchstart"
@dblclick="next"
>{{ item.name }}</li>
</ul> </ul>
<p>{{ $t('prompts.currentlyNavigating') }} <code>{{ nav }}</code>.</p> <p>{{ $t('prompts.currentlyNavigating') }} <code>{{ nav }}</code>.</p>
@ -22,8 +25,8 @@ import url from '@/utils/url'
import { files } from '@/api' import { files } from '@/api'
export default { export default {
name: 'file-list', name: 'FileList',
data: function () { data: function() {
return { return {
items: [], items: [],
touches: { touches: {
@ -35,12 +38,12 @@ export default {
} }
}, },
computed: { computed: {
...mapState([ 'req' ]), ...mapState(['req']),
nav () { nav() {
return decodeURIComponent(this.current) return decodeURIComponent(this.current)
} }
}, },
mounted () { mounted() {
// If we're showing this on a listing, // If we're showing this on a listing,
// we can use the current request object // we can use the current request object
// to fill the move options. // to fill the move options.
@ -56,7 +59,7 @@ export default {
.catch(this.$showError) .catch(this.$showError)
}, },
methods: { methods: {
fillOptions (req) { fillOptions(req) {
// Sets the current path and resets // Sets the current path and resets
// the current items. // the current items.
this.current = req.url this.current = req.url
@ -79,7 +82,7 @@ export default {
// Otherwise we add every directory to the // Otherwise we add every directory to the
// move options. // move options.
for (let item of req.items) { for (const item of req.items) {
if (!item.isDir) continue if (!item.isDir) continue
this.items.push({ this.items.push({
@ -88,18 +91,18 @@ export default {
}) })
} }
}, },
next: function (event) { next: function(event) {
// Retrieves the URL of the directory the user // Retrieves the URL of the directory the user
// just clicked in and fill the options with its // just clicked in and fill the options with its
// content. // content.
let uri = event.currentTarget.dataset.url const uri = event.currentTarget.dataset.url
files.fetch(uri) files.fetch(uri)
.then(this.fillOptions) .then(this.fillOptions)
.catch(this.$showError) .catch(this.$showError)
}, },
touchstart (event) { touchstart(event) {
let url = event.currentTarget.dataset.url const url = event.currentTarget.dataset.url
// In 300 milliseconds, we shall reset the count. // In 300 milliseconds, we shall reset the count.
setTimeout(() => { setTimeout(() => {
@ -123,7 +126,7 @@ export default {
this.next(event) this.next(event)
} }
}, },
select: function (event) { select: function(event) {
// If the element is already selected, unselect it. // If the element is already selected, unselect it.
if (this.selected === event.currentTarget.dataset.url) { if (this.selected === event.currentTarget.dataset.url) {
this.selected = null this.selected = null

View File

@ -19,16 +19,18 @@
</div> </div>
<div class="card-action"> <div class="card-action">
<button type="submit" <button
@click="$store.commit('closeHovers')" type="submit"
class="button button--flat" class="button button--flat"
:aria-label="$t('buttons.ok')" :aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button> :title="$t('buttons.ok')"
@click="$store.commit('closeHovers')"
>{{ $t('buttons.ok') }}</button>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
export default { name: 'help' } export default { name: 'Help' }
</script> </script>

View File

@ -7,8 +7,8 @@
<div class="card-content"> <div class="card-content">
<p v-if="selected.length > 1">{{ $t('prompts.filesSelected', { count: selected.length }) }}</p> <p v-if="selected.length > 1">{{ $t('prompts.filesSelected', { count: selected.length }) }}</p>
<p class="break-word" v-if="selected.length < 2"><strong>{{ $t('prompts.displayName') }}</strong> {{ name }}</p> <p v-if="selected.length < 2" class="break-word"><strong>{{ $t('prompts.displayName') }}</strong> {{ name }}</p>
<p v-if="!dir || selected.length > 1"><strong>{{ $t('prompts.size') }}:</strong> <span id="content_length"></span> {{ humanSize }}</p> <p v-if="!dir || selected.length > 1"><strong>{{ $t('prompts.size') }}:</strong> <span id="content_length" /> {{ humanSize }}</p>
<p v-if="selected.length < 2"><strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime }}</p> <p v-if="selected.length < 2"><strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime }}</p>
<template v-if="dir && selected.length === 0"> <template v-if="dir && selected.length === 0">
@ -25,57 +25,59 @@
</div> </div>
<div class="card-action"> <div class="card-action">
<button type="submit" <button
@click="$store.commit('closeHovers')" type="submit"
class="button button--flat" class="button button--flat"
:aria-label="$t('buttons.ok')" :aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button> :title="$t('buttons.ok')"
@click="$store.commit('closeHovers')"
>{{ $t('buttons.ok') }}</button>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import {mapState, mapGetters} from 'vuex' import { mapState, mapGetters } from 'vuex'
import filesize from 'filesize' import filesize from 'filesize'
import moment from 'moment' import moment from 'moment'
import { files as api } from '@/api' import { files as api } from '@/api'
export default { export default {
name: 'info', name: 'Info',
computed: { computed: {
...mapState(['req', 'selected']), ...mapState(['req', 'selected']),
...mapGetters(['selectedCount', 'isListing']), ...mapGetters(['selectedCount', 'isListing']),
humanSize: function () { humanSize: function() {
if (this.selectedCount === 0 || !this.isListing) { if (this.selectedCount === 0 || !this.isListing) {
return filesize(this.req.size) return filesize(this.req.size)
} }
let sum = 0 let sum = 0
for (let selected of this.selected) { for (const selected of this.selected) {
sum += this.req.items[selected].size sum += this.req.items[selected].size
} }
return filesize(sum) return filesize(sum)
}, },
humanTime: function () { humanTime: function() {
if (this.selectedCount === 0) { if (this.selectedCount === 0) {
return moment(this.req.modified).fromNow() return moment(this.req.modified).fromNow()
} }
return moment(this.req.items[this.selected[0]]).fromNow() return moment(this.req.items[this.selected[0]]).fromNow()
}, },
name: function () { name: function() {
return this.selectedCount === 0 ? this.req.name : this.req.items[this.selected[0]].name return this.selectedCount === 0 ? this.req.name : this.req.items[this.selected[0]].name
}, },
dir: function () { dir: function() {
return this.selectedCount > 1 || (this.selectedCount === 0 return this.selectedCount > 1 || (this.selectedCount === 0
? this.req.isDir ? this.req.isDir
: this.req.items[this.selected[0]].isDir) : this.req.items[this.selected[0]].isDir)
} }
}, },
methods: { methods: {
checksum: async function (event, algo) { checksum: async function(event, algo) {
event.preventDefault() event.preventDefault()
let link let link

View File

@ -5,19 +5,23 @@
</div> </div>
<div class="card-content"> <div class="card-content">
<file-list @update:selected="val => dest = val"></file-list> <file-list @update:selected="val => dest = val" />
</div> </div>
<div class="card-action"> <div class="card-action">
<button class="button button--flat button--grey" <button
@click="$store.commit('closeHovers')" class="button button--flat button--grey"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> :title="$t('buttons.cancel')"
<button class="button button--flat" @click="$store.commit('closeHovers')"
@click="move" >{{ $t('buttons.cancel') }}</button>
<button
class="button button--flat"
:disabled="$route.path === dest" :disabled="$route.path === dest"
:aria-label="$t('buttons.move')" :aria-label="$t('buttons.move')"
:title="$t('buttons.move')">{{ $t('buttons.move') }}</button> :title="$t('buttons.move')"
@click="move"
>{{ $t('buttons.move') }}</button>
</div> </div>
</div> </div>
</template> </template>
@ -29,9 +33,9 @@ import { files as api } from '@/api'
import buttons from '@/utils/buttons' import buttons from '@/utils/buttons'
export default { export default {
name: 'move', name: 'Move',
components: { FileList }, components: { FileList },
data: function () { data: function() {
return { return {
current: window.location.pathname, current: window.location.pathname,
dest: null dest: null
@ -39,12 +43,12 @@ export default {
}, },
computed: mapState(['req', 'selected']), computed: mapState(['req', 'selected']),
methods: { methods: {
move: async function (event) { move: async function(event) {
event.preventDefault() event.preventDefault()
buttons.loading('move') buttons.loading('move')
let items = [] const items = []
for (let item of this.selected) { for (const item of this.selected) {
items.push({ items.push({
from: this.req.items[item].url, from: this.req.items[item].url,
to: this.dest + encodeURIComponent(this.req.items[item].name) to: this.dest + encodeURIComponent(this.req.items[item].name)

View File

@ -6,15 +6,15 @@
<div class="card-content"> <div class="card-content">
<p>{{ $t('prompts.newDirMessage') }}</p> <p>{{ $t('prompts.newDirMessage') }}</p>
<input class="input input--block" type="text" @keyup.enter="submit" v-model.trim="name" v-focus> <input v-model.trim="name" v-focus class="input input--block" type="text" @keyup.enter="submit">
</div> </div>
<div class="card-action"> <div class="card-action">
<button <button
class="button button--flat button--grey" class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
@click="$store.commit('closeHovers')"
>{{ $t('buttons.cancel') }}</button> >{{ $t('buttons.cancel') }}</button>
<button <button
class="button button--flat" class="button button--flat"
@ -32,14 +32,14 @@ import { files as api } from '@/api'
import url from '@/utils/url' import url from '@/utils/url'
export default { export default {
name: 'new-dir', name: 'NewDir',
data: function() { data: function() {
return { return {
name: '' name: ''
}; }
}, },
computed: { computed: {
...mapGetters([ 'isFiles', 'isListing' ]) ...mapGetters(['isFiles', 'isListing'])
}, },
methods: { methods: {
submit: async function(event) { submit: async function(event) {
@ -66,6 +66,6 @@ export default {
this.$store.commit('closeHovers') this.$store.commit('closeHovers')
} }
} }
}; }
</script> </script>

View File

@ -6,21 +6,21 @@
<div class="card-content"> <div class="card-content">
<p>{{ $t('prompts.newFileMessage') }}</p> <p>{{ $t('prompts.newFileMessage') }}</p>
<input class="input input--block" v-focus type="text" @keyup.enter="submit" v-model.trim="name"> <input v-model.trim="name" v-focus class="input input--block" type="text" @keyup.enter="submit">
</div> </div>
<div class="card-action"> <div class="card-action">
<button <button
class="button button--flat button--grey" class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
@click="$store.commit('closeHovers')"
>{{ $t('buttons.cancel') }}</button> >{{ $t('buttons.cancel') }}</button>
<button <button
class="button button--flat" class="button button--flat"
@click="submit"
:aria-label="$t('buttons.create')" :aria-label="$t('buttons.create')"
:title="$t('buttons.create')" :title="$t('buttons.create')"
@click="submit"
>{{ $t('buttons.create') }}</button> >{{ $t('buttons.create') }}</button>
</div> </div>
</div> </div>
@ -32,14 +32,14 @@ import { files as api } from '@/api'
import url from '@/utils/url' import url from '@/utils/url'
export default { export default {
name: 'new-file', name: 'NewFile',
data: function() { data: function() {
return { return {
name: '' name: ''
}; }
}, },
computed: { computed: {
...mapGetters([ 'isFiles', 'isListing' ]) ...mapGetters(['isFiles', 'isListing'])
}, },
methods: { methods: {
submit: async function(event) { submit: async function(event) {
@ -66,6 +66,6 @@ export default {
this.$store.commit('closeHovers') this.$store.commit('closeHovers')
} }
} }
}; }
</script> </script>

View File

@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<component :is="currentComponent"></component> <component :is="currentComponent" />
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div> <div v-show="showOverlay" class="overlay" @click="resetPrompts" />
</div> </div>
</template> </template>
@ -21,7 +21,7 @@ import { mapState } from 'vuex'
import buttons from '@/utils/buttons' import buttons from '@/utils/buttons'
export default { export default {
name: 'prompts', name: 'Prompts',
components: { components: {
Info, Info,
Delete, Delete,
@ -35,7 +35,7 @@ export default {
Help, Help,
Replace Replace
}, },
data: function () { data: function() {
return { return {
pluginData: { pluginData: {
buttons, buttons,
@ -46,7 +46,7 @@ export default {
}, },
computed: { computed: {
...mapState(['show', 'plugins']), ...mapState(['show', 'plugins']),
currentComponent: function () { currentComponent: function() {
const matched = [ const matched = [
'info', 'info',
'help', 'help',
@ -59,16 +59,16 @@ export default {
'download', 'download',
'replace', 'replace',
'share' 'share'
].indexOf(this.show) >= 0; ].indexOf(this.show) >= 0
return matched && this.show || null; return matched && this.show || null
}, },
showOverlay: function () { showOverlay: function() {
return (this.show !== null && this.show !== 'search' && this.show !== 'more') return (this.show !== null && this.show !== 'search' && this.show !== 'more')
} }
}, },
methods: { methods: {
resetPrompts () { resetPrompts() {
this.$store.commit('closeHovers') this.$store.commit('closeHovers')
} }
} }

View File

@ -6,19 +6,23 @@
<div class="card-content"> <div class="card-content">
<p>{{ $t('prompts.renameMessage') }} <code>{{ oldName() }}</code>:</p> <p>{{ $t('prompts.renameMessage') }} <code>{{ oldName() }}</code>:</p>
<input class="input input--block" v-focus type="text" @keyup.enter="submit" v-model.trim="name"> <input v-model.trim="name" v-focus class="input input--block" type="text" @keyup.enter="submit">
</div> </div>
<div class="card-action"> <div class="card-action">
<button class="button button--flat button--grey" <button
@click="$store.commit('closeHovers')" class="button button--flat button--grey"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> :title="$t('buttons.cancel')"
<button @click="submit" @click="$store.commit('closeHovers')"
>{{ $t('buttons.cancel') }}</button>
<button
class="button button--flat" class="button button--flat"
type="submit" type="submit"
:aria-label="$t('buttons.rename')" :aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button> :title="$t('buttons.rename')"
@click="submit"
>{{ $t('buttons.rename') }}</button>
</div> </div>
</div> </div>
</template> </template>
@ -29,13 +33,13 @@ import url from '@/utils/url'
import { files as api } from '@/api' import { files as api } from '@/api'
export default { export default {
name: 'rename', name: 'Rename',
data: function () { data: function() {
return { return {
name: '' name: ''
} }
}, },
created () { created() {
this.name = this.oldName() this.name = this.oldName()
}, },
computed: { computed: {
@ -43,10 +47,10 @@ export default {
...mapGetters(['isListing']) ...mapGetters(['isListing'])
}, },
methods: { methods: {
cancel: function () { cancel: function() {
this.$store.commit('closeHovers') this.$store.commit('closeHovers')
}, },
oldName: function () { oldName: function() {
if (!this.isListing) { if (!this.isListing) {
return this.req.name return this.req.name
} }
@ -58,7 +62,7 @@ export default {
return this.req.items[this.selected[0]].name return this.req.items[this.selected[0]].name
}, },
submit: async function () { submit: async function() {
let oldLink = '' let oldLink = ''
let newLink = '' let newLink = ''

View File

@ -9,14 +9,18 @@
</div> </div>
<div class="card-action"> <div class="card-action">
<button class="button button--flat button--grey" <button
@click="$store.commit('closeHovers')" class="button button--flat button--grey"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> :title="$t('buttons.cancel')"
<button class="button button--flat button--red" @click="$store.commit('closeHovers')"
@click="showConfirm" >{{ $t('buttons.cancel') }}</button>
<button
class="button button--flat button--red"
:aria-label="$t('buttons.replace')" :aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')">{{ $t('buttons.replace') }}</button> :title="$t('buttons.replace')"
@click="showConfirm"
>{{ $t('buttons.replace') }}</button>
</div> </div>
</div> </div>
</template> </template>
@ -25,7 +29,7 @@
import { mapState } from 'vuex' import { mapState } from 'vuex'
export default { export default {
name: 'replace', name: 'Replace',
computed: mapState(['showConfirm']) computed: mapState(['showConfirm'])
} }
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="card floating" id="share"> <div id="share" class="card floating">
<div class="card-title"> <div class="card-title">
<h2>{{ $t('buttons.share') }}</h2> <h2>{{ $t('buttons.share') }}</h2>
</div> </div>
@ -7,7 +7,7 @@
<div class="card-content"> <div class="card-content">
<ul> <ul>
<li v-if="!hasPermanent"> <li v-if="!hasPermanent">
<a @click="getPermalink" :aria-label="$t('buttons.permalink')">{{ $t('buttons.permalink') }}</a> <a :aria-label="$t('buttons.permalink')" @click="getPermalink">{{ $t('buttons.permalink') }}</a>
</li> </li>
<li v-for="link in links" :key="link.hash"> <li v-for="link in links" :key="link.hash">
@ -16,43 +16,53 @@
<template v-else>{{ $t('permanent') }}</template> <template v-else>{{ $t('permanent') }}</template>
</a> </a>
<button class="action" <button
@click="deleteLink($event, link)" class="action"
:aria-label="$t('buttons.delete')" :aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')"><i class="material-icons">delete</i></button> :title="$t('buttons.delete')"
@click="deleteLink($event, link)"
><i class="material-icons">delete</i></button>
<button class="action copy-clipboard" <button
class="action copy-clipboard"
:data-clipboard-text="buildLink(link.hash)" :data-clipboard-text="buildLink(link.hash)"
:aria-label="$t('buttons.copyToClipboard')" :aria-label="$t('buttons.copyToClipboard')"
:title="$t('buttons.copyToClipboard')"><i class="material-icons">content_paste</i></button> :title="$t('buttons.copyToClipboard')"
><i class="material-icons">content_paste</i></button>
</li> </li>
<li> <li>
<input v-focus <input
v-model.trim="time"
v-focus
type="number" type="number"
max="2147483647" max="2147483647"
min="0" min="0"
@keyup.enter="submit" @keyup.enter="submit"
v-model.trim="time"> >
<select v-model="unit" :aria-label="$t('time.unit')"> <select v-model="unit" :aria-label="$t('time.unit')">
<option value="seconds">{{ $t('time.seconds') }}</option> <option value="seconds">{{ $t('time.seconds') }}</option>
<option value="minutes">{{ $t('time.minutes') }}</option> <option value="minutes">{{ $t('time.minutes') }}</option>
<option value="hours">{{ $t('time.hours') }}</option> <option value="hours">{{ $t('time.hours') }}</option>
<option value="days">{{ $t('time.days') }}</option> <option value="days">{{ $t('time.days') }}</option>
</select> </select>
<button class="action" <button
@click="submit" class="action"
:aria-label="$t('buttons.create')" :aria-label="$t('buttons.create')"
:title="$t('buttons.create')"><i class="material-icons">add</i></button> :title="$t('buttons.create')"
@click="submit"
><i class="material-icons">add</i></button>
</li> </li>
</ul> </ul>
</div> </div>
<div class="card-action"> <div class="card-action">
<button class="button button--flat" <button
@click="$store.commit('closeHovers')" class="button button--flat"
:aria-label="$t('buttons.close')" :aria-label="$t('buttons.close')"
:title="$t('buttons.close')">{{ $t('buttons.close') }}</button> :title="$t('buttons.close')"
@click="$store.commit('closeHovers')"
>{{ $t('buttons.close') }}</button>
</div> </div>
</div> </div>
</template> </template>
@ -65,8 +75,8 @@ import moment from 'moment'
import Clipboard from 'clipboard' import Clipboard from 'clipboard'
export default { export default {
name: 'share', name: 'Share',
data: function () { data: function() {
return { return {
time: '', time: '',
unit: 'hours', unit: 'hours',
@ -76,9 +86,9 @@ export default {
} }
}, },
computed: { computed: {
...mapState([ 'req', 'selected', 'selectedCount' ]), ...mapState(['req', 'selected', 'selectedCount']),
...mapGetters([ 'isListing' ]), ...mapGetters(['isListing']),
url () { url() {
if (!this.isListing) { if (!this.isListing) {
return this.$route.path return this.$route.path
} }
@ -91,13 +101,13 @@ export default {
return this.req.items[this.selected[0]].url return this.req.items[this.selected[0]].url
} }
}, },
async beforeMount () { async beforeMount() {
try { try {
const links = await api.get(this.url) const links = await api.get(this.url)
this.links = links this.links = links
this.sort() this.sort()
for (let link of this.links) { for (const link of this.links) {
if (link.expire === 0) { if (link.expire === 0) {
this.hasPermanent = true this.hasPermanent = true
break break
@ -107,17 +117,17 @@ export default {
this.$showError(e) this.$showError(e)
} }
}, },
mounted () { mounted() {
this.clip = new Clipboard('.copy-clipboard') this.clip = new Clipboard('.copy-clipboard')
this.clip.on('success', () => { this.clip.on('success', () => {
this.$showSuccess(this.$t('success.linkCopied')) this.$showSuccess(this.$t('success.linkCopied'))
}) })
}, },
beforeDestroy () { beforeDestroy() {
this.clip.destroy() this.clip.destroy()
}, },
methods: { methods: {
submit: async function () { submit: async function() {
if (!this.time) return if (!this.time) return
try { try {
@ -128,7 +138,7 @@ export default {
this.$showError(e) this.$showError(e)
} }
}, },
getPermalink: async function () { getPermalink: async function() {
try { try {
const res = await api.create(this.url) const res = await api.create(this.url)
this.links.push(res) this.links.push(res)
@ -138,7 +148,7 @@ export default {
this.$showError(e) this.$showError(e)
} }
}, },
deleteLink: async function (event, link) { deleteLink: async function(event, link) {
event.preventDefault() event.preventDefault()
try { try {
await api.remove(link.hash) await api.remove(link.hash)
@ -148,13 +158,13 @@ export default {
this.$showError(e) this.$showError(e)
} }
}, },
humanTime (time) { humanTime(time) {
return moment(time * 1000).fromNow() return moment(time * 1000).fromNow()
}, },
buildLink (hash) { buildLink(hash) {
return `${window.location.origin}${baseURL}/share/${hash}` return `${window.location.origin}${baseURL}/share/${hash}`
}, },
sort () { sort() {
this.links = this.links.sort((a, b) => { this.links = this.links.sort((a, b) => {
if (a.expire === 0) return -1 if (a.expire === 0) return -1
if (b.expire === 0) return 1 if (b.expire === 0) return 1

View File

@ -2,20 +2,20 @@
<div> <div>
<h3>{{ $t('settings.userCommands') }}</h3> <h3>{{ $t('settings.userCommands') }}</h3>
<p class="small">{{ $t('settings.userCommandsHelp') }} <i>git svn hg</i>.</p> <p class="small">{{ $t('settings.userCommandsHelp') }} <i>git svn hg</i>.</p>
<input class="input input--block" type="text" v-model.trim="raw"> <input v-model.trim="raw" class="input input--block" type="text">
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'permissions', name: 'Permissions',
props: ['commands'], props: ['commands'],
computed: { computed: {
raw: { raw: {
get () { get() {
return this.commands.join(' ') return this.commands.join(' ')
}, },
set (value) { set(value) {
this.$emit('update:commands', value.split(' ')) this.$emit('update:commands', value.split(' '))
} }
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<select v-on:change="change" :value="locale"> <select :value="locale" @change="change">
<option v-for="(language, value) in locales" :key="value" :value="value">{{ $t('languages.' + language) }}</option> <option v-for="(language, value) in locales" :key="value" :value="value">{{ $t('languages.' + language) }}</option>
</select> </select>
</template> </template>
@ -7,10 +7,10 @@
<script> <script>
export default { export default {
name: 'languages', name: 'Languages',
props: [ 'locale' ], props: ['locale'],
data() { data() {
let dataObj = { const dataObj = {
locales: { locales: {
ar: 'ar', ar: 'ar',
de: 'de', de: 'de',
@ -31,14 +31,14 @@ export default {
'zh-cn': 'zhCN', 'zh-cn': 'zhCN',
'zh-tw': 'zhTW' 'zh-tw': 'zhTW'
} }
}; }
Object.defineProperty(dataObj, "locales", { configurable: false, writable: false }); Object.defineProperty(dataObj, 'locales', { configurable: false, writable: false })
return dataObj; return dataObj
}, },
methods: { methods: {
change (event) { change(event) {
this.$emit('update:locale', event.target.value) this.$emit('update:locale', event.target.value)
} }
} }

View File

@ -3,28 +3,28 @@
<h3>{{ $t('settings.permissions') }}</h3> <h3>{{ $t('settings.permissions') }}</h3>
<p class="small">{{ $t('settings.permissionsHelp') }}</p> <p class="small">{{ $t('settings.permissionsHelp') }}</p>
<p><input type="checkbox" v-model="admin"> {{ $t('settings.administrator') }}</p> <p><input v-model="admin" type="checkbox"> {{ $t('settings.administrator') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.create"> {{ $t('settings.perm.create') }}</p> <p><input v-model="perm.create" type="checkbox" :disabled="admin"> {{ $t('settings.perm.create') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.delete"> {{ $t('settings.perm.delete') }}</p> <p><input v-model="perm.delete" type="checkbox" :disabled="admin"> {{ $t('settings.perm.delete') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.download"> {{ $t('settings.perm.download') }}</p> <p><input v-model="perm.download" type="checkbox" :disabled="admin"> {{ $t('settings.perm.download') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.modify"> {{ $t('settings.perm.modify') }}</p> <p><input v-model="perm.modify" type="checkbox" :disabled="admin"> {{ $t('settings.perm.modify') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.execute"> {{ $t('settings.perm.execute') }}</p> <p><input v-model="perm.execute" type="checkbox" :disabled="admin"> {{ $t('settings.perm.execute') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.rename"> {{ $t('settings.perm.rename') }}</p> <p><input v-model="perm.rename" type="checkbox" :disabled="admin"> {{ $t('settings.perm.rename') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.share"> {{ $t('settings.perm.share') }}</p> <p><input v-model="perm.share" type="checkbox" :disabled="admin"> {{ $t('settings.perm.share') }}</p>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'permissions', name: 'Permissions',
props: ['perm'], props: ['perm'],
computed: { computed: {
admin: { admin: {
get () { get() {
return this.perm.admin return this.perm.admin
}, },
set (value) { set(value) {
if (value) { if (value) {
for (const key in this.perm) { for (const key in this.perm) {
this.perm[key] = true this.perm[key] = true

View File

@ -1,43 +1,45 @@
<template> <template>
<form class="rules small"> <form class="rules small">
<div v-for="(rule, index) in rules" :key="index"> <div v-for="(rule, index) in rules" :key="index">
<input type="checkbox" v-model="rule.regex"><label>Regex</label> <input v-model="rule.regex" type="checkbox"><label>Regex</label>
<input type="checkbox" v-model="rule.allow"><label>Allow</label> <input v-model="rule.allow" type="checkbox"><label>Allow</label>
<input <input
@keypress.enter.prevent
type="text"
v-if="rule.regex" v-if="rule.regex"
v-model="rule.regexp.raw" v-model="rule.regexp.raw"
:placeholder="$t('settings.insertRegex')" />
<input
@keypress.enter.prevent
type="text" type="text"
:placeholder="$t('settings.insertRegex')"
@keypress.enter.prevent
>
<input
v-else v-else
v-model="rule.path" v-model="rule.path"
:placeholder="$t('settings.insertPath')" /> type="text"
:placeholder="$t('settings.insertPath')"
@keypress.enter.prevent
>
<button class="button button--red" @click="remove($event, index)">-</button> <button class="button button--red" @click="remove($event, index)">-</button>
</div> </div>
<div> <div>
<button class="button" @click="create" default="false">{{ $t('buttons.new') }}</button> <button class="button" default="false" @click="create">{{ $t('buttons.new') }}</button>
</div> </div>
</form> </form>
</template> </template>
<script> <script>
export default { export default {
name: 'rules-textarea', name: 'RulesTextarea',
props: ['rules'], props: ['rules'],
methods: { methods: {
remove (event, index) { remove(event, index) {
event.preventDefault() event.preventDefault()
let rules = [ ...this.rules ] const rules = [...this.rules]
rules.splice(index, 1) rules.splice(index, 1)
this.$emit('update:rules', [ ...rules ]) this.$emit('update:rules', [...rules])
}, },
create (event) { create(event) {
event.preventDefault() event.preventDefault()
this.$emit('update:rules', [ this.$emit('update:rules', [

View File

@ -1,5 +1,5 @@
<template> <template>
<select v-on:change="change" :value="theme"> <select :value="theme" @change="change">
<option value="">{{ $t('settings.themes.light') }}</option> <option value="">{{ $t('settings.themes.light') }}</option>
<option value="dark">{{ $t('settings.themes.dark') }}</option> <option value="dark">{{ $t('settings.themes.dark') }}</option>
</select> </select>
@ -7,10 +7,10 @@
<script> <script>
export default { export default {
name: 'themes', name: 'Themes',
props: [ 'theme' ], props: ['theme'],
methods: { methods: {
change (event) { change(event) {
this.$emit('update:theme', event.target.value) this.$emit('update:theme', event.target.value)
} }
} }

View File

@ -2,26 +2,26 @@
<div> <div>
<p v-if="!isDefault"> <p v-if="!isDefault">
<label for="username">{{ $t('settings.username') }}</label> <label for="username">{{ $t('settings.username') }}</label>
<input class="input input--block" type="text" v-model="user.username" id="username"> <input id="username" v-model="user.username" class="input input--block" type="text">
</p> </p>
<p v-if="!isDefault"> <p v-if="!isDefault">
<label for="password">{{ $t('settings.password') }}</label> <label for="password">{{ $t('settings.password') }}</label>
<input class="input input--block" type="password" :placeholder="passwordPlaceholder" v-model="user.password" id="password"> <input id="password" v-model="user.password" class="input input--block" type="password" :placeholder="passwordPlaceholder">
</p> </p>
<p> <p>
<label for="scope">{{ $t('settings.scope') }}</label> <label for="scope">{{ $t('settings.scope') }}</label>
<input class="input input--block" type="text" v-model="user.scope" id="scope"> <input id="scope" v-model="user.scope" class="input input--block" type="text">
</p> </p>
<p> <p>
<label for="locale">{{ $t('settings.language') }}</label> <label for="locale">{{ $t('settings.language') }}</label>
<languages class="input input--block" id="locale" :locale.sync="user.locale"></languages> <languages id="locale" class="input input--block" :locale.sync="user.locale" />
</p> </p>
<p v-if="!isDefault"> <p v-if="!isDefault">
<input type="checkbox" :disabled="user.perm.admin" v-model="user.lockPassword"> {{ $t('settings.lockPassword') }} <input v-model="user.lockPassword" type="checkbox" :disabled="user.perm.admin"> {{ $t('settings.lockPassword') }}
</p> </p>
<permissions :perm.sync="user.perm" /> <permissions :perm.sync="user.perm" />
@ -42,21 +42,21 @@ import Permissions from './Permissions'
import Commands from './Commands' import Commands from './Commands'
export default { export default {
name: 'user', name: 'User',
components: { components: {
Permissions, Permissions,
Languages, Languages,
Rules, Rules,
Commands Commands
}, },
props: [ 'user', 'isNew', 'isDefault' ], props: ['user', 'isNew', 'isDefault'],
computed: { computed: {
passwordPlaceholder () { passwordPlaceholder() {
return this.isNew ? '' : this.$t('settings.avoidChanges') return this.isNew ? '' : this.$t('settings.avoidChanges')
} }
}, },
watch: { watch: {
'user.perm.admin': function () { 'user.perm.admin': function() {
if (!this.user.perm.admin) return if (!this.user.perm.admin) return
this.user.lockPassword = false this.user.lockPassword = false
} }

View File

@ -1,6 +1,6 @@
#listing h2 { #listing h2 {
margin: 0 0 0 0.5em; margin: 0 0 0 0.5em;
font-size: .9em; font-size: 0.9em;
color: rgba(0, 0, 0, 0.38); color: rgba(0, 0, 0, 0.38);
font-weight: 500; font-weight: 500;
} }
@ -10,7 +10,7 @@
overflow: hidden; overflow: hidden;
} }
#listing>div { #listing > div {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: flex-start; justify-content: flex-start;
@ -22,7 +22,7 @@
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
color: #6f6f6f; color: #6f6f6f;
transition: .1s ease background, .1s ease opacity; transition: 0.1s ease background, 0.1s ease opacity;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
} }
@ -52,6 +52,13 @@
vertical-align: bottom; vertical-align: bottom;
} }
#listing .item img {
width: 4em;
height: 4em;
margin-right: 0.1em;
vertical-align: bottom;
}
.message { .message {
text-align: center; text-align: center;
font-size: 2em; font-size: 2em;
@ -64,7 +71,7 @@
.message i { .message i {
font-size: 2.5em; font-size: 2.5em;
margin-bottom: .2em; margin-bottom: 0.2em;
display: block; display: block;
} }
@ -75,14 +82,14 @@
#listing.mosaic .item { #listing.mosaic .item {
width: calc(33% - 1em); width: calc(33% - 1em);
margin: .5em; margin: 0.5em;
padding: 0.5em; padding: 0.5em;
border-radius: 0.2em; border-radius: 0.2em;
box-shadow: 0 1px 3px rgba(0, 0, 0, .06), 0 1px 2px rgba(0, 0, 0, .12); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.12);
} }
#listing.mosaic .item:hover { #listing.mosaic .item:hover {
box-shadow: 0 1px 3px rgba(0, 0, 0, .12), 0 1px 2px rgba(0, 0, 0, .24) !important; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24) !important;
} }
#listing.mosaic .header { #listing.mosaic .header {
@ -116,7 +123,7 @@
display: none; display: none;
} }
#listing .item[aria-selected=true] { #listing .item[aria-selected="true"] {
background: var(--blue) !important; background: var(--blue) !important;
color: #fff !important; color: #fff !important;
} }
@ -129,6 +136,11 @@
font-size: 2em; font-size: 2em;
} }
#listing.list .item div:first-of-type img {
width: 2em;
height: 2em;
}
#listing.list .item div:last-of-type { #listing.list .item div:last-of-type {
width: calc(100% - 3em); width: calc(100% - 3em);
display: flex; display: flex;
@ -151,19 +163,19 @@
#listing.list .header i { #listing.list .header i {
font-size: 1.5em; font-size: 1.5em;
vertical-align: middle; vertical-align: middle;
margin-left: .2em; margin-left: 0.2em;
} }
#listing.list .item.header { #listing.list .item.header {
display: flex !important; display: flex !important;
background: #fafafa; background: #fafafa;
z-index: 999; z-index: 999;
padding: .85em; padding: 0.85em;
border: 0; border: 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.1); border-bottom: 1px solid rgba(0, 0, 0, 0.1);
} }
#listing.list .item.header>div:first-child { #listing.list .item.header > div:first-child {
width: 0; width: 0;
} }
@ -175,7 +187,7 @@
color: inherit; color: inherit;
} }
#listing.list .item.header>div:first-child { #listing.list .item.header > div:first-child {
width: 0; width: 0;
} }
@ -193,7 +205,7 @@
#listing.list .header i { #listing.list .header i {
opacity: 0; opacity: 0;
transition: .1s ease all; transition: 0.1s ease all;
} }
#listing.list .header p:hover i, #listing.list .header p:hover i,
@ -215,7 +227,7 @@
height: 4em; height: 4em;
padding: 0.5em 0.5em 0.5em 1em; padding: 0.5em 0.5em 0.5em 1em;
justify-content: space-between; justify-content: space-between;
transition: .2s ease bottom; transition: 0.2s ease bottom;
} }
#listing #multiple-selection.active { #listing #multiple-selection.active {

View File

@ -22,7 +22,7 @@ import zhTW from './zh-tw.json'
Vue.use(VueI18n) Vue.use(VueI18n)
export function detectLocale () { export function detectLocale() {
let locale = (navigator.language || navigator.browserLangugae).toLowerCase() let locale = (navigator.language || navigator.browserLangugae).toLowerCase()
switch (true) { switch (true) {
case /^ar.*/i.test(locale): case /^ar.*/i.test(locale):

View File

@ -9,7 +9,7 @@ import App from '@/App'
sync(store, router) sync(store, router)
async function start () { async function start() {
if (loginPage) { if (loginPage) {
await validateLogin() await validateLogin()
} else { } else {
@ -17,7 +17,7 @@ async function start () {
} }
if (recaptcha) { if (recaptcha) {
await new Promise (resolve => { await new Promise(resolve => {
const check = () => { const check = () => {
if (typeof window.grecaptcha === 'undefined') { if (typeof window.grecaptcha === 'undefined') {
setTimeout(check, 100) setTimeout(check, 100)
@ -35,8 +35,8 @@ async function start () {
store, store,
router, router,
i18n, i18n,
template: '<App/>', components: { App },
components: { App } template: '<App/>'
}) })
} }

View File

@ -52,7 +52,7 @@ const mutations = {
state.plugins.push(value) state.plugins.push(value)
}, },
removeSelected: (state, value) => { removeSelected: (state, value) => {
let i = state.selected.indexOf(value) const i = state.selected.indexOf(value)
if (i === -1) return if (i === -1) return
state.selected.splice(i, 1) state.selected.splice(i, 1)
}, },
@ -62,7 +62,7 @@ const mutations = {
updateUser: (state, value) => { updateUser: (state, value) => {
if (typeof value !== 'object') return if (typeof value !== 'object') return
for (let field in value) { for (const field in value) {
if (field === 'locale') { if (field === 'locale') {
moment.locale(value[field]) moment.locale(value[field])
i18n.default.locale = value[field] i18n.default.locale = value[field]

View File

@ -3,7 +3,7 @@ import router from '@/router'
import { Base64 } from 'js-base64' import { Base64 } from 'js-base64'
import { baseURL } from '@/utils/constants' import { baseURL } from '@/utils/constants'
export function parseToken (token) { export function parseToken(token) {
const parts = token.split('.') const parts = token.split('.')
if (parts.length !== 3) { if (parts.length !== 3) {
@ -21,7 +21,7 @@ export function parseToken (token) {
store.commit('setUser', data.user) store.commit('setUser', data.user)
} }
export async function validateLogin () { export async function validateLogin() {
try { try {
if (localStorage.getItem('jwt')) { if (localStorage.getItem('jwt')) {
await renew(localStorage.getItem('jwt')) await renew(localStorage.getItem('jwt'))
@ -31,7 +31,7 @@ export async function validateLogin () {
} }
} }
export async function login (username, password, recaptcha) { export async function login(username, password, recaptcha) {
const data = { username, password, recaptcha } const data = { username, password, recaptcha }
const res = await fetch(`${baseURL}/api/login`, { const res = await fetch(`${baseURL}/api/login`, {
@ -51,11 +51,11 @@ export async function login (username, password, recaptcha) {
} }
} }
export async function renew (jwt) { export async function renew(jwt) {
const res = await fetch(`${baseURL}/api/renew`, { const res = await fetch(`${baseURL}/api/renew`, {
method: 'POST', method: 'POST',
headers: { headers: {
'X-Auth': jwt, 'X-Auth': jwt
} }
}) })
@ -68,7 +68,7 @@ export async function renew (jwt) {
} }
} }
export async function signup (username, password) { export async function signup(username, password) {
const data = { username, password } const data = { username, password }
const res = await fetch(`${baseURL}/api/signup`, { const res = await fetch(`${baseURL}/api/signup`, {
@ -84,9 +84,9 @@ export async function signup (username, password) {
} }
} }
export function logout () { export function logout() {
store.commit('setJWT', '') store.commit('setJWT', '')
store.commit('setUser', null) store.commit('setUser', null)
localStorage.setItem('jwt', null) localStorage.setItem('jwt', null)
router.push({path: '/login'}) router.push({ path: '/login' })
} }

View File

@ -1,5 +1,5 @@
function loading (button) { function loading(button) {
let el = document.querySelector(`#${button}-button > i`) const el = document.querySelector(`#${button}-button > i`)
if (el === undefined || el === null) { if (el === undefined || el === null) {
console.log('Error getting button ' + button) // eslint-disable-line console.log('Error getting button ' + button) // eslint-disable-line
@ -16,8 +16,8 @@ function loading (button) {
}, 100) }, 100)
} }
function done (button) { function done(button) {
let el = document.querySelector(`#${button}-button > i`) const el = document.querySelector(`#${button}-button > i`)
if (el === undefined || el === null) { if (el === undefined || el === null) {
console.log('Error getting button ' + button) // eslint-disable-line console.log('Error getting button ' + button) // eslint-disable-line
@ -33,8 +33,8 @@ function done (button) {
}, 100) }, 100)
} }
function success (button) { function success(button) {
let el = document.querySelector(`#${button}-button > i`) const el = document.querySelector(`#${button}-button > i`)
if (el === undefined || el === null) { if (el === undefined || el === null) {
console.log('Error getting button ' + button) // eslint-disable-line console.log('Error getting button ' + button) // eslint-disable-line

View File

@ -1,4 +1,4 @@
export default function (name) { export default function(name) {
let re = new RegExp('(?:(?:^|.*;\\s*)' + name + '\\s*\\=\\s*([^;]*).*$)|^.*$') const re = new RegExp('(?:(?:^|.*;\\s*)' + name + '\\s*\\=\\s*([^;]*).*$)|^.*$')
return document.cookie.replace(re, '$1') return document.cookie.replace(re, '$1')
} }

View File

@ -1,10 +1,10 @@
export default function getRule (rules) { export default function getRule(rules) {
for (let i = 0; i < rules.length; i++) { for (let i = 0; i < rules.length; i++) {
rules[i] = rules[i].toLowerCase() rules[i] = rules[i].toLowerCase()
} }
let result = null let result = null
let find = Array.prototype.find const find = Array.prototype.find
find.call(document.styleSheets, styleSheet => { find.call(document.styleSheets, styleSheet => {
result = find.call(styleSheet.cssRules, cssRule => { result = find.call(styleSheet.cssRules, cssRule => {

View File

@ -1,4 +1,4 @@
function removeLastDir (url) { function removeLastDir(url) {
var arr = url.split('/') var arr = url.split('/')
if (arr.pop() === '') { if (arr.pop() === '') {
arr.pop() arr.pop()
@ -10,14 +10,14 @@ function removeLastDir (url) {
// this code borrow from mozilla // this code borrow from mozilla
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#Examples // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#Examples
function encodeRFC5987ValueChars(str) { function encodeRFC5987ValueChars(str) {
return encodeURIComponent(str). return encodeURIComponent(str)
// Note that although RFC3986 reserves "!", RFC5987 does not, // Note that although RFC3986 reserves "!", RFC5987 does not,
// so we do not need to escape it // so we do not need to escape it
replace(/['()]/g, escape). // i.e., %27 %28 %29 .replace(/['()]/g, escape) // i.e., %27 %28 %29
replace(/\*/g, '%2A'). .replace(/\*/g, '%2A')
// The following are not required for percent-encoding per RFC5987, // The following are not required for percent-encoding per RFC5987,
// so we can allow for a little better readability over the wire: |`^ // so we can allow for a little better readability over the wire: |`^
replace(/%(?:7C|60|5E)/g, unescape); .replace(/%(?:7C|60|5E)/g, unescape)
} }
export default { export default {

View File

@ -24,19 +24,19 @@ Vue.prototype.$showSuccess = (message) => {
} }
Vue.prototype.$showError = (error) => { Vue.prototype.$showError = (error) => {
let btns = [ const btns = [
Noty.button(i18n.t('buttons.close'), '', function () { Noty.button(i18n.t('buttons.close'), '', function() {
n.close() n.close()
}) })
] ]
if (!disableExternal) { if (!disableExternal) {
btns.unshift(Noty.button(i18n.t('buttons.reportIssue'), '', function () { btns.unshift(Noty.button(i18n.t('buttons.reportIssue'), '', function() {
window.open('https://github.com/filebrowser/filebrowser/issues/new/choose') window.open('https://github.com/filebrowser/filebrowser/issues/new/choose')
})) }))
} }
let n = new Noty(Object.assign({}, notyDefault, { const n = new Noty(Object.assign({}, notyDefault, {
text: error.message || error, text: error.message || error,
type: 'error', type: 'error',
timeout: null, timeout: null,
@ -47,7 +47,7 @@ Vue.prototype.$showError = (error) => {
} }
Vue.directive('focus', { Vue.directive('focus', {
inserted: function (el) { inserted: function(el) {
el.focus() el.focus()
} }
}) })

View File

@ -11,13 +11,13 @@
</span> </span>
</div> </div>
<div v-if="error"> <div v-if="error">
<not-found v-if="error.message === '404'"></not-found> <not-found v-if="error.message === '404'" />
<forbidden v-else-if="error.message === '403'"></forbidden> <forbidden v-else-if="error.message === '403'" />
<internal-error v-else></internal-error> <internal-error v-else />
</div> </div>
<editor v-else-if="isEditor"></editor> <editor v-else-if="isEditor" />
<listing :class="{ multiple }" v-else-if="isListing"></listing> <listing v-else-if="isListing" :class="{ multiple }" />
<preview v-else-if="isPreview"></preview> <preview v-else-if="isPreview" />
<div v-else> <div v-else>
<h2 class="message"> <h2 class="message">
<span>{{ $t('files.loading') }}</span> <span>{{ $t('files.loading') }}</span>
@ -35,12 +35,12 @@ import Listing from '@/components/files/Listing'
import { files as api } from '@/api' import { files as api } from '@/api'
import { mapGetters, mapState, mapMutations } from 'vuex' import { mapGetters, mapState, mapMutations } from 'vuex'
function clean (path) { function clean(path) {
return path.endsWith('/') ? path.slice(0, -1) : path return path.endsWith('/') ? path.slice(0, -1) : path
} }
export default { export default {
name: 'files', name: 'Files',
components: { components: {
Forbidden, Forbidden,
NotFound, NotFound,
@ -63,11 +63,11 @@ export default {
'multiple', 'multiple',
'loading' 'loading'
]), ]),
isPreview () { isPreview() {
return !this.loading && !this.isListing && !this.isEditor return !this.loading && !this.isListing && !this.isEditor
}, },
breadcrumbs () { breadcrumbs() {
let parts = this.$route.path.split('/') const parts = this.$route.path.split('/')
if (parts[0] === '') { if (parts[0] === '') {
parts.shift() parts.shift()
@ -77,7 +77,7 @@ export default {
parts.pop() parts.pop()
} }
let breadcrumbs = [] const breadcrumbs = []
for (let i = 0; i < parts.length; i++) { for (let i = 0; i < parts.length; i++) {
if (i === 0) { if (i === 0) {
@ -100,34 +100,34 @@ export default {
return breadcrumbs return breadcrumbs
} }
}, },
data: function () { data: function() {
return { return {
error: null error: null
} }
}, },
created () {
this.fetchData()
},
watch: { watch: {
'$route': 'fetchData', '$route': 'fetchData',
'reload': function () { 'reload': function() {
this.fetchData() this.fetchData()
} }
}, },
mounted () { created() {
this.fetchData()
},
mounted() {
window.addEventListener('keydown', this.keyEvent) window.addEventListener('keydown', this.keyEvent)
window.addEventListener('scroll', this.scroll) window.addEventListener('scroll', this.scroll)
}, },
beforeDestroy () { beforeDestroy() {
window.removeEventListener('keydown', this.keyEvent) window.removeEventListener('keydown', this.keyEvent)
window.removeEventListener('scroll', this.scroll) window.removeEventListener('scroll', this.scroll)
}, },
destroyed () { destroyed() {
this.$store.commit('updateRequest', {}) this.$store.commit('updateRequest', {})
}, },
methods: { methods: {
...mapMutations([ 'setLoading' ]), ...mapMutations(['setLoading']),
async fetchData () { async fetchData() {
// Reset view information. // Reset view information.
this.$store.commit('setReload', false) this.$store.commit('setReload', false)
this.$store.commit('resetSelected') this.$store.commit('resetSelected')
@ -157,7 +157,7 @@ export default {
this.setLoading(false) this.setLoading(false)
} }
}, },
keyEvent (event) { keyEvent(event) {
// Esc! // Esc!
if (event.keyCode === 27) { if (event.keyCode === 27) {
this.$store.commit('closeHovers') this.$store.commit('closeHovers')
@ -212,7 +212,7 @@ export default {
} }
} }
}, },
scroll () { scroll() {
if (this.req.kind !== 'listing' || this.$store.state.user.viewMode === 'mosaic') return if (this.req.kind !== 'listing' || this.$store.state.user.viewMode === 'mosaic') return
let top = 112 - window.scrollY let top = 112 - window.scrollY
@ -223,10 +223,10 @@ export default {
document.querySelector('#listing.list .item.header').style.top = top + 'px' document.querySelector('#listing.list .item.header').style.top = top + 'px'
}, },
openSidebar () { openSidebar() {
this.$store.commit('showHover', 'sidebar') this.$store.commit('showHover', 'sidebar')
}, },
openSearch () { openSearch() {
this.$store.commit('showHover', 'search') this.$store.commit('showHover', 'search')
} }
} }

View File

@ -1,15 +1,15 @@
<template> <template>
<div> <div>
<div id="progress"> <div id="progress">
<div v-bind:style="{ width: $store.state.progress + '%' }"></div> <div :style="{ width: $store.state.progress + '%' }" />
</div> </div>
<site-header></site-header> <site-header />
<sidebar></sidebar> <sidebar />
<main> <main>
<router-view></router-view> <router-view />
<shell v-if="isLogged && user.perm.execute" /> <shell v-if="isLogged && user.perm.execute" />
</main> </main>
<prompts></prompts> <prompts />
</div> </div>
</template> </template>
@ -21,7 +21,7 @@ import SiteHeader from '@/components/Header'
import Shell from '@/components/Shell' import Shell from '@/components/Shell'
export default { export default {
name: 'layout', name: 'Layout',
components: { components: {
Sidebar, Sidebar,
SiteHeader, SiteHeader,
@ -29,11 +29,11 @@ export default {
Shell Shell
}, },
computed: { computed: {
...mapGetters([ 'isLogged' ]), ...mapGetters(['isLogged']),
...mapState([ 'user' ]) ...mapState(['user'])
}, },
watch: { watch: {
'$route': function () { '$route': function() {
this.$store.commit('resetSelected') this.$store.commit('resetSelected')
this.$store.commit('multiple', false) this.$store.commit('multiple', false)
if (this.$store.state.show !== 'success') this.$store.commit('closeHovers') if (this.$store.state.show !== 'success') this.$store.commit('closeHovers')

View File

@ -5,14 +5,14 @@
<h1>{{ name }}</h1> <h1>{{ name }}</h1>
<div v-if="error !== ''" class="wrong">{{ error }}</div> <div v-if="error !== ''" class="wrong">{{ error }}</div>
<input class="input input--block" type="text" v-model="username" :placeholder="$t('login.username')"> <input v-model="username" class="input input--block" type="text" :placeholder="$t('login.username')">
<input class="input input--block" type="password" v-model="password" :placeholder="$t('login.password')"> <input v-model="password" class="input input--block" type="password" :placeholder="$t('login.password')">
<input class="input input--block" v-if="createMode" type="password" v-model="passwordConfirm" :placeholder="$t('login.passwordConfirm')" /> <input v-if="createMode" v-model="passwordConfirm" class="input input--block" type="password" :placeholder="$t('login.passwordConfirm')">
<div v-if="recaptcha" id="recaptcha"></div> <div v-if="recaptcha" id="recaptcha" />
<input class="button button--block" type="submit" :value="createMode ? $t('login.signup') : $t('login.submit')"> <input class="button button--block" type="submit" :value="createMode ? $t('login.signup') : $t('login.submit')">
<p @click="toggleMode" v-if="signup">{{ createMode ? $t('login.loginInstead') : $t('login.createAnAccount') }}</p> <p v-if="signup" @click="toggleMode">{{ createMode ? $t('login.loginInstead') : $t('login.createAnAccount') }}</p>
</form> </form>
</div> </div>
</template> </template>
@ -22,13 +22,8 @@ import * as auth from '@/utils/auth'
import { name, logoURL, recaptcha, recaptchaKey, signup } from '@/utils/constants' import { name, logoURL, recaptcha, recaptchaKey, signup } from '@/utils/constants'
export default { export default {
name: 'login', name: 'Login',
computed: { data: function() {
signup: () => signup,
name: () => name,
logoURL: () => logoURL
},
data: function () {
return { return {
createMode: false, createMode: false,
error: '', error: '',
@ -38,7 +33,12 @@ export default {
passwordConfirm: '' passwordConfirm: ''
} }
}, },
mounted () { computed: {
signup: () => signup,
name: () => name,
logoURL: () => logoURL
},
mounted() {
if (!recaptcha) return if (!recaptcha) return
window.grecaptcha.render('recaptcha', { window.grecaptcha.render('recaptcha', {
@ -46,10 +46,10 @@ export default {
}) })
}, },
methods: { methods: {
toggleMode () { toggleMode() {
this.createMode = !this.createMode this.createMode = !this.createMode
}, },
async submit (event) { async submit(event) {
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
@ -83,7 +83,7 @@ export default {
await auth.login(this.username, this.password, captcha) await auth.login(this.username, this.password, captcha)
this.$router.push({ path: redirect }) this.$router.push({ path: redirect })
} catch (e) { } catch (e) {
if (e.message == 409) { if (e.message === 409) {
this.error = this.$t('login.usernameTaken') this.error = this.$t('login.usernameTaken')
} else { } else {
this.error = this.$t('login.wrongCredentials') this.error = this.$t('login.wrongCredentials')

View File

@ -1,12 +1,12 @@
<template> <template>
<div class="dashboard"> <div class="dashboard">
<ul id="nav" v-if="user.perm.admin"> <ul v-if="user.perm.admin" 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/profile' }"><router-link to="/settings/profile">{{ $t('settings.profileSettings') }}</router-link></li>
<li :class="{ active: $route.path === '/settings/global' }"><router-link to="/settings/global">{{ $t('settings.globalSettings') }}</router-link></li> <li :class="{ active: $route.path === '/settings/global' }"><router-link to="/settings/global">{{ $t('settings.globalSettings') }}</router-link></li>
<li :class="{ active: $route.path === '/settings/users' }"><router-link to="/settings/users">{{ $t('settings.userManagement') }}</router-link></li> <li :class="{ active: $route.path === '/settings/users' }"><router-link to="/settings/users">{{ $t('settings.userManagement') }}</router-link></li>
</ul> </ul>
<router-view></router-view> <router-view />
</div> </div>
</template> </template>
@ -14,7 +14,7 @@
import { mapState } from 'vuex' import { mapState } from 'vuex'
export default { export default {
name: 'settings', name: 'Settings',
computed: mapState([ 'user' ]) computed: mapState(['user'])
} }
</script> </script>

View File

@ -1,20 +1,20 @@
<template> <template>
<div class="share" v-if="loaded"> <div v-if="loaded" class="share">
<a target="_blank" :href="link"> <a target="_blank" :href="link">
<div class="share__box"> <div class="share__box">
<div class="share__box__download" v-if="file.isDir">{{ $t('download.downloadFolder') }}</div> <div v-if="file.isDir" class="share__box__download">{{ $t('download.downloadFolder') }}</div>
<div class="share__box__download" v-else>{{ $t('download.downloadFile') }}</div> <div v-else class="share__box__download">{{ $t('download.downloadFile') }}</div>
<div class="share__box__info"> <div class="share__box__info">
<svg v-if="file.isDir" fill="#40c4ff" height="150" viewBox="0 0 24 24" width="150" xmlns="http://www.w3.org/2000/svg"> <svg v-if="file.isDir" fill="#40c4ff" height="150" viewBox="0 0 24 24" width="150" xmlns="http://www.w3.org/2000/svg">
<path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/> <path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z" />
<path d="M0 0h24v24H0z" fill="none"/> <path d="M0 0h24v24H0z" fill="none" />
</svg> </svg>
<svg v-else fill="#40c4ff" height="150" viewBox="0 0 24 24" width="150" xmlns="http://www.w3.org/2000/svg"> <svg v-else fill="#40c4ff" height="150" viewBox="0 0 24 24" width="150" xmlns="http://www.w3.org/2000/svg">
<path d="M6 2c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6H6zm7 7V3.5L18.5 9H13z"/> <path d="M6 2c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6H6zm7 7V3.5L18.5 9H13z" />
<path d="M0 0h24v24H0z" fill="none"/> <path d="M0 0h24v24H0z" fill="none" />
</svg> </svg>
<h1 class="share__box__title">{{ file.name }}</h1> <h1 class="share__box__title">{{ file.name }}</h1>
<qrcode-vue :value="fullLink" size="200" level="M"></qrcode-vue> <qrcode-vue :value="fullLink" size="200" level="M" />
</div> </div>
</div> </div>
</a> </a>
@ -27,7 +27,7 @@ import { baseURL } from '@/utils/constants'
import QrcodeVue from 'qrcode.vue' import QrcodeVue from 'qrcode.vue'
export default { export default {
name: 'share', name: 'Share',
components: { components: {
QrcodeVue QrcodeVue
}, },
@ -36,25 +36,25 @@ export default {
notFound: false, notFound: false,
file: null file: null
}), }),
computed: {
hash: function() {
return this.$route.params.pathMatch
},
link: function() {
return `${baseURL}/api/public/dl/${this.hash}/${encodeURI(this.file.name)}`
},
fullLink: function() {
return window.location.origin + this.link
}
},
watch: { watch: {
'$route': 'fetchData' '$route': 'fetchData'
}, },
created: function () { created: function() {
this.fetchData() this.fetchData()
}, },
computed: {
hash: function () {
return this.$route.params.pathMatch
},
link: function () {
return `${baseURL}/api/public/dl/${this.hash}/${encodeURI(this.file.name)}`
},
fullLink: function () {
return window.location.origin + this.link
},
},
methods: { methods: {
fetchData: async function () { fetchData: async function() {
try { try {
this.file = await api.getHash(this.hash) this.file = await api.getHash(this.hash)
this.loaded = true this.loaded = true

View File

@ -8,6 +8,6 @@
</template> </template>
<script> <script>
export default {name: 'forbidden'} export default { name: 'Forbidden' }
</script> </script>

View File

@ -8,6 +8,6 @@
</template> </template>
<script> <script>
export default {name: 'not-found'} export default { name: 'NotFound' }
</script> </script>

View File

@ -8,6 +8,6 @@
</template> </template>
<script> <script>
export default {name: 'internal-error'} export default { name: 'InternalError' }
</script> </script>

View File

@ -1,14 +1,14 @@
<template> <template>
<div class="dashboard" v-if="settings !== null"> <div v-if="settings !== null" class="dashboard">
<form class="card" @submit.prevent="save"> <form class="card" @submit.prevent="save">
<div class="card-title"> <div class="card-title">
<h2>{{ $t('settings.globalSettings') }}</h2> <h2>{{ $t('settings.globalSettings') }}</h2>
</div> </div>
<div class="card-content"> <div class="card-content">
<p><input type="checkbox" v-model="settings.signup"> {{ $t('settings.allowSignup') }}</p> <p><input v-model="settings.signup" type="checkbox"> {{ $t('settings.allowSignup') }}</p>
<p><input type="checkbox" v-model="settings.createUserDir"> {{ $t('settings.createUserDir') }}</p> <p><input v-model="settings.createUserDir" type="checkbox"> {{ $t('settings.createUserDir') }}</p>
<h3>{{ $t('settings.rules') }}</h3> <h3>{{ $t('settings.rules') }}</h3>
<p class="small">{{ $t('settings.globalRules') }}</p> <p class="small">{{ $t('settings.globalRules') }}</p>
@ -16,7 +16,7 @@
<h3>{{ $t('settings.executeOnShell') }}</h3> <h3>{{ $t('settings.executeOnShell') }}</h3>
<p class="small">{{ $t('settings.executeOnShellDescription') }}</p> <p class="small">{{ $t('settings.executeOnShellDescription') }}</p>
<input class="input input--block" type="text" placeholder="bash -c, cmd /c, ..." v-model="settings.shell" /> <input v-model="settings.shell" class="input input--block" type="text" placeholder="bash -c, cmd /c, ...">
<h3>{{ $t('settings.branding') }}</h3> <h3>{{ $t('settings.branding') }}</h3>
@ -25,23 +25,23 @@
</i18n> </i18n>
<p> <p>
<input type="checkbox" v-model="settings.branding.disableExternal" id="branding-links" /> <input id="branding-links" v-model="settings.branding.disableExternal" type="checkbox">
{{ $t('settings.disableExternalLinks') }} {{ $t('settings.disableExternalLinks') }}
</p> </p>
<p> <p>
<label for="theme">{{ $t('settings.themes.title') }}</label> <label for="theme">{{ $t('settings.themes.title') }}</label>
<themes class="input input--block" :theme.sync="settings.branding.theme" id="theme"></themes> <themes id="theme" class="input input--block" :theme.sync="settings.branding.theme" />
</p> </p>
<p> <p>
<label for="branding-name">{{ $t('settings.instanceName') }}</label> <label for="branding-name">{{ $t('settings.instanceName') }}</label>
<input class="input input--block" type="text" v-model="settings.branding.name" id="branding-name" /> <input id="branding-name" v-model="settings.branding.name" class="input input--block" type="text">
</p> </p>
<p> <p>
<label for="branding-files">{{ $t('settings.brandingDirectoryPath') }}</label> <label for="branding-files">{{ $t('settings.brandingDirectoryPath') }}</label>
<input class="input input--block" type="text" v-model="settings.branding.files" id="branding-files" /> <input id="branding-files" v-model="settings.branding.files" class="input input--block" type="text">
</p> </p>
</div> </div>
@ -59,7 +59,7 @@
<div class="card-content"> <div class="card-content">
<p class="small">{{ $t('settings.defaultUserDescription') }}</p> <p class="small">{{ $t('settings.defaultUserDescription') }}</p>
<user-form :isNew="false" :isDefault="true" :user.sync="settings.defaults" /> <user-form :is-new="false" :is-default="true" :user.sync="settings.defaults" />
</div> </div>
<div class="card-action"> <div class="card-action">
@ -86,7 +86,7 @@
<i class="material-icons">arrow_drop_down</i> <i class="material-icons">arrow_drop_down</i>
</label> </label>
<div class="collapse"> <div class="collapse">
<textarea class="input input--block input--textarea" v-model.trim="command.value"></textarea> <textarea v-model.trim="command.value" class="input input--block input--textarea" />
</div> </div>
</div> </div>
</div> </div>
@ -106,25 +106,25 @@ import Rules from '@/components/settings/Rules'
import Themes from '@/components/settings/Themes' import Themes from '@/components/settings/Themes'
export default { export default {
name: 'settings', name: 'Settings',
components: { components: {
Themes, Themes,
UserForm, UserForm,
Rules Rules
}, },
data: function () { data: function() {
return { return {
originalSettings: null, originalSettings: null,
settings: null settings: null
} }
}, },
computed: { computed: {
...mapState([ 'user' ]) ...mapState(['user'])
}, },
async created () { async created() {
try { try {
const original = await api.get() const original = await api.get()
let settings = { ...original, commands: [] } const settings = { ...original, commands: [] }
for (const key in original.commands) { for (const key in original.commands) {
settings.commands.push({ settings.commands.push({
@ -142,9 +142,9 @@ export default {
} }
}, },
methods: { methods: {
capitalize (name, where = '_') { capitalize(name, where = '_') {
if (where === 'caps') where = /(?=[A-Z])/ if (where === 'caps') where = /(?=[A-Z])/
let splitted = name.split(where) const splitted = name.split(where)
name = '' name = ''
for (let i = 0; i < splitted.length; i++) { for (let i = 0; i < splitted.length; i++) {
@ -153,8 +153,8 @@ export default {
return name.slice(0, -1) return name.slice(0, -1)
}, },
async save () { async save() {
let settings = { const settings = {
...this.settings, ...this.settings,
shell: this.settings.shell.trim().split(' ').filter(s => s !== ''), shell: this.settings.shell.trim().split(' ').filter(s => s !== ''),
commands: {} commands: {}

View File

@ -7,7 +7,7 @@
<div class="card-content"> <div class="card-content">
<h3>{{ $t('settings.language') }}</h3> <h3>{{ $t('settings.language') }}</h3>
<languages class="input input--block" :locale.sync="locale"></languages> <languages class="input input--block" :locale.sync="locale" />
</div> </div>
<div class="card-action"> <div class="card-action">
@ -15,14 +15,14 @@
</div> </div>
</form> </form>
<form class="card" v-if="!user.lockPassword" @submit="updatePassword"> <form v-if="!user.lockPassword" class="card" @submit="updatePassword">
<div class="card-title"> <div class="card-title">
<h2>{{ $t('settings.changePassword') }}</h2> <h2>{{ $t('settings.changePassword') }}</h2>
</div> </div>
<div class="card-content"> <div class="card-content">
<input :class="passwordClass" type="password" :placeholder="$t('settings.newPassword')" v-model="password" name="password"> <input v-model="password" :class="passwordClass" type="password" :placeholder="$t('settings.newPassword')" name="password">
<input :class="passwordClass" type="password" :placeholder="$t('settings.newPasswordConfirm')" v-model="passwordConf" name="password"> <input v-model="passwordConf" :class="passwordClass" type="password" :placeholder="$t('settings.newPasswordConfirm')" name="password">
</div> </div>
<div class="card-action"> <div class="card-action">
@ -38,11 +38,11 @@ import { users as api } from '@/api'
import Languages from '@/components/settings/Languages' import Languages from '@/components/settings/Languages'
export default { export default {
name: 'settings', name: 'Settings',
components: { components: {
Languages Languages
}, },
data: function () { data: function() {
return { return {
password: '', password: '',
passwordConf: '', passwordConf: '',
@ -50,8 +50,8 @@ export default {
} }
}, },
computed: { computed: {
...mapState([ 'user' ]), ...mapState(['user']),
passwordClass () { passwordClass() {
const baseClass = 'input input--block' const baseClass = 'input input--block'
if (this.password === '' && this.passwordConf === '') { if (this.password === '' && this.passwordConf === '') {
@ -65,12 +65,12 @@ export default {
return `${baseClass} input--red` return `${baseClass} input--red`
} }
}, },
created () { created() {
this.locale = this.user.locale this.locale = this.user.locale
}, },
methods: { methods: {
...mapMutations([ 'updateUser' ]), ...mapMutations(['updateUser']),
async updatePassword (event) { async updatePassword(event) {
event.preventDefault() event.preventDefault()
if (this.password !== this.passwordConf || this.password === '') { if (this.password !== this.passwordConf || this.password === '') {
@ -86,7 +86,7 @@ export default {
this.$showError(e) this.$showError(e)
} }
}, },
async updateSettings (event) { async updateSettings(event) {
event.preventDefault() event.preventDefault()
try { try {

View File

@ -1,27 +1,29 @@
<template> <template>
<div> <div>
<form v-if="loaded" @submit="save" class="card"> <form v-if="loaded" class="card" @submit="save">
<div class="card-title"> <div class="card-title">
<h2 v-if="user.id === 0">{{ $t('settings.newUser') }}</h2> <h2 v-if="user.id === 0">{{ $t('settings.newUser') }}</h2>
<h2 v-else>{{ $t('settings.user') }} {{ user.username }}</h2> <h2 v-else>{{ $t('settings.user') }} {{ user.username }}</h2>
</div> </div>
<div class="card-content"> <div class="card-content">
<user-form :user.sync="user" :isDefault="false" :isNew="isNew" /> <user-form :user.sync="user" :is-default="false" :is-new="isNew" />
</div> </div>
<div class="card-action"> <div class="card-action">
<button <button
v-if="!isNew" v-if="!isNew"
@click.prevent="deletePrompt"
type="button" type="button"
class="button button--flat button--red" class="button button--flat button--red"
:aria-label="$t('buttons.delete')" :aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button> :title="$t('buttons.delete')"
@click.prevent="deletePrompt"
>{{ $t('buttons.delete') }}</button>
<input <input
class="button button--flat" class="button button--flat"
type="submit" type="submit"
:value="$t('buttons.save')"> :value="$t('buttons.save')"
>
</div> </div>
</form> </form>
@ -31,15 +33,19 @@
</div> </div>
<div class="card-action"> <div class="card-action">
<button class="button button--flat button--grey" <button
@click="closeHovers"
v-focus v-focus
class="button button--flat button--grey"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"> :title="$t('buttons.cancel')"
@click="closeHovers"
>
{{ $t('buttons.cancel') }} {{ $t('buttons.cancel') }}
</button> </button>
<button class="button button--flat" <button
@click="deleteUser"> class="button button--flat"
@click="deleteUser"
>
{{ $t('buttons.delete') }} {{ $t('buttons.delete') }}
</button> </button>
</div> </div>
@ -54,7 +60,7 @@ import UserForm from '@/components/settings/UserForm'
import deepClone from 'lodash.clonedeep' import deepClone from 'lodash.clonedeep'
export default { export default {
name: 'user', name: 'User',
components: { components: {
UserForm UserForm
}, },
@ -65,27 +71,27 @@ export default {
loaded: false loaded: false
} }
}, },
created () {
this.fetchData()
},
computed: { computed: {
isNew () { isNew() {
return this.$route.path === '/settings/users/new' return this.$route.path === '/settings/users/new'
} }
}, },
watch: { watch: {
'$route': 'fetchData', '$route': 'fetchData',
'user.perm.admin': function () { 'user.perm.admin': function() {
if (!this.user.perm.admin) return if (!this.user.perm.admin) return
this.user.lockPassword = false this.user.lockPassword = false
} }
}, },
created() {
this.fetchData()
},
methods: { methods: {
...mapMutations([ 'closeHovers', 'showHover', 'setUser' ]), ...mapMutations(['closeHovers', 'showHover', 'setUser']),
async fetchData () { async fetchData() {
try { try {
if (this.isNew) { if (this.isNew) {
let { defaults } = await settings.get() const { defaults } = await settings.get()
this.user = { this.user = {
...defaults, ...defaults,
username: '', username: '',
@ -104,10 +110,10 @@ export default {
this.$router.push({ path: '/settings/users/new' }) this.$router.push({ path: '/settings/users/new' })
} }
}, },
deletePrompt () { deletePrompt() {
this.showHover('deleteUser') this.showHover('deleteUser')
}, },
async deleteUser (event) { async deleteUser(event) {
event.preventDefault() event.preventDefault()
try { try {
@ -118,9 +124,9 @@ export default {
this.$showError(e) this.$showError(e)
} }
}, },
async save (event) { async save(event) {
event.preventDefault() event.preventDefault()
let user = { const user = {
...this.originalUser, ...this.originalUser,
...this.user ...this.user
} }

View File

@ -11,7 +11,7 @@
<th>{{ $t('settings.username') }}</th> <th>{{ $t('settings.username') }}</th>
<th>{{ $t('settings.admin') }}</th> <th>{{ $t('settings.admin') }}</th>
<th>{{ $t('settings.scope') }}</th> <th>{{ $t('settings.scope') }}</th>
<th></th> <th />
</tr> </tr>
<tr v-for="user in users" :key="user.id"> <tr v-for="user in users" :key="user.id">
@ -31,13 +31,13 @@
import { users as api } from '@/api' import { users as api } from '@/api'
export default { export default {
name: 'users', name: 'Users',
data: function () { data: function() {
return { return {
users: [] users: []
} }
}, },
async created () { async created() {
try { try {
this.users = await api.getAll() this.users = await api.getAll()
} catch (e) { } catch (e) {

2
go.mod
View File

@ -8,6 +8,7 @@ require (
github.com/caddyserver/caddy v1.0.3 github.com/caddyserver/caddy v1.0.3
github.com/daaku/go.zipexe v1.0.1 // indirect github.com/daaku/go.zipexe v1.0.1 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/disintegration/imaging v1.6.2
github.com/dsnet/compress v0.0.1 // indirect github.com/dsnet/compress v0.0.1 // indirect
github.com/golang/snappy v0.0.1 // indirect github.com/golang/snappy v0.0.1 // indirect
github.com/gorilla/mux v1.7.3 github.com/gorilla/mux v1.7.3
@ -29,6 +30,7 @@ require (
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
go.etcd.io/bbolt v1.3.3 go.etcd.io/bbolt v1.3.3
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37
golang.org/x/image v0.0.0-20200430140353-33d19683fad8 // indirect
golang.org/x/net v0.0.0-20200528225125-3c3fba18258b // indirect golang.org/x/net v0.0.0-20200528225125-3c3fba18258b // indirect
golang.org/x/sys v0.0.0-20200523222454-059865788121 // indirect golang.org/x/sys v0.0.0-20200523222454-059865788121 // indirect
golang.org/x/text v0.3.2 // indirect golang.org/x/text v0.3.2 // indirect

5
go.sum
View File

@ -43,6 +43,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q= github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
@ -239,6 +241,9 @@ golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw=
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225 h1:kNX+jCowfMYzvlSvJu5pQWEmyWFrBXJ3PBy10xKMXK8= golang.org/x/net v0.0.0-20180724234803-3673e40ba225 h1:kNX+jCowfMYzvlSvJu5pQWEmyWFrBXJ3PBy10xKMXK8=

View File

@ -59,6 +59,7 @@ func NewHandler(store *storage.Storage, server *settings.Server) (http.Handler,
api.Handle("/settings", monkey(settingsPutHandler, "")).Methods("PUT") api.Handle("/settings", monkey(settingsPutHandler, "")).Methods("PUT")
api.PathPrefix("/raw").Handler(monkey(rawHandler, "/api/raw")).Methods("GET") api.PathPrefix("/raw").Handler(monkey(rawHandler, "/api/raw")).Methods("GET")
api.PathPrefix("/thumbnail").Handler(monkey(thumbnailHandler, "/api/thumbnail")).Methods("GET")
api.PathPrefix("/command").Handler(monkey(commandsHandler, "/api/command")).Methods("GET") api.PathPrefix("/command").Handler(monkey(commandsHandler, "/api/command")).Methods("GET")
api.PathPrefix("/search").Handler(monkey(searchHandler, "/api/search")).Methods("GET") api.PathPrefix("/search").Handler(monkey(searchHandler, "/api/search")).Methods("GET")

92
http/thumbnail.go Normal file
View File

@ -0,0 +1,92 @@
package http
import (
"github.com/disintegration/imaging"
"github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/files"
"image"
"image/gif"
"image/jpeg"
"image/png"
"mime"
"net/http"
"net/url"
)
var thumbnailHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
if !d.user.Perm.Download {
return http.StatusAccepted, nil
}
file, err := files.NewFileInfo(files.FileOptions{
Fs: d.user.Fs,
Path: r.URL.Path,
Modify: d.user.Perm.Modify,
Expand: true,
Checker: d,
})
if err != nil {
return errToStatus(err), err
}
if file.IsDir || file.Type != "image" {
return http.StatusNotFound, nil
}
return thumbnailFileHandler(w, r, file)
})
func thumbnailFileHandler(w http.ResponseWriter, r *http.Request, file *files.FileInfo) (int, error) {
fd, err := file.Fs.Open(file.Path)
if err != nil {
return errToStatus(err), err
}
defer fd.Close()
if r.URL.Query().Get("inline") == "true" {
w.Header().Set("Content-Disposition", "inline")
} else {
// As per RFC6266 section 4.3
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(file.Name))
}
srcImg, err := imaging.Decode(fd, imaging.AutoOrientation(true))
if err != nil {
return errToStatus(err), err
}
dstImg := fitResizeImage(srcImg)
w.Header().Add("Content-Type", mime.TypeByExtension(file.Extension))
err = func() error {
switch file.Extension {
case ".jpg", ".jpeg":
return jpeg.Encode(w, dstImg, nil)
case ".png":
return png.Encode(w, dstImg)
case ".gif":
return gif.Encode(w, dstImg, nil)
default:
return errors.ErrNotExist
}
}()
if err != nil {
return errToStatus(err), err
}
return 0, nil
}
const maxSize = 1080
func fitResizeImage(srcImage image.Image) image.Image {
width := srcImage.Bounds().Dx()
height := srcImage.Bounds().Dy()
if width > maxSize && width > height {
width = maxSize
height = 0
} else if height > maxSize && height > width {
width = 0
height = maxSize
} else {
return srcImage
}
return imaging.Resize(srcImage, width, height, imaging.NearestNeighbor)
}