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

View File

@ -9,7 +9,7 @@ export default function command(url, command, onmessage, onclose) {
url = removePrefix(url)
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.onmessage = onmessage
conn.onclose = onclose

View File

@ -2,13 +2,13 @@ import { fetchURL, removePrefix } from './utils'
import { baseURL } from '@/utils/constants'
import store from '@/store'
export async function fetch (url) {
export async function fetch(url) {
url = removePrefix(url)
const res = await fetchURL(`/api/resources${url}`, {})
if (res.status === 200) {
let data = await res.json()
const data = await res.json()
data.url = `/files${url}`
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)
let opts = { method }
const opts = { method }
if (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')
}
export async function put (url, content = '') {
export async function put(url, content = '') {
return resourceAction(url, 'PUT', content)
}
export function download (format, ...files) {
export function download(format, ...files) {
let url = `${baseURL}/api/raw`
if (files.length === 1) {
@ -65,7 +65,7 @@ export function download (format, ...files) {
} else {
let arg = ''
for (let file of files) {
for (const file of files) {
arg += removePrefix(file) + ','
}
@ -82,11 +82,11 @@ export function download (format, ...files) {
window.open(url)
}
export async function post (url, content = '', overwrite = false, onupload) {
export async function post(url, content = '', overwrite = false, onupload) {
url = removePrefix(url)
return new Promise((resolve, reject) => {
let request = new XMLHttpRequest()
const request = new XMLHttpRequest()
request.open('POST', `${baseURL}/api/resources${url}?override=${overwrite}`, true)
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
window.onbeforeunload = () => "Files are being uploaded."
window.onbeforeunload = () => 'Files are being uploaded.'
request.onload = () => {
if (request.status === 200) {
@ -112,14 +112,14 @@ export async function post (url, content = '', overwrite = false, onupload) {
}
request.send(content)
// Upload is done no more message before closing the tab
// Upload is done no more message before closing the tab
}).finally(() => { window.onbeforeunload = null })
}
function moveCopy (items, copy = false) {
let promises = []
function moveCopy(items, copy = false) {
const promises = []
for (let item of items) {
for (const item of items) {
const from = removePrefix(item.from)
const to = encodeURIComponent(removePrefix(item.to))
const url = `${from}?action=${copy ? 'copy' : 'rename'}&destination=${to}`
@ -129,15 +129,15 @@ function moveCopy (items, copy = false) {
return Promise.all(promises)
}
export function move (items) {
export function move(items) {
return moveCopy(items)
}
export function copy (items) {
export function copy(items) {
return moveCopy(items, true)
}
export async function checksum (url, algo) {
export async function checksum(url, algo) {
const data = await resourceAction(`${url}?checksum=${algo}`, 'GET')
return (await data.json()).checksums[algo]
}

View File

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

View File

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

View File

@ -1,14 +1,14 @@
import { fetchURL, fetchJSON } from './utils'
export async function getAll () {
export async function getAll() {
return fetchJSON(`/api/users`, {})
}
export async function get (id) {
export async function get(id) {
return fetchJSON(`/api/users/${id}`, {})
}
export async function create (user) {
export async function create(user) {
const res = await fetchURL(`/api/users`, {
method: 'POST',
body: JSON.stringify({
@ -23,10 +23,9 @@ export async function create (user) {
} else {
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}`, {
method: 'PUT',
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}`, {
method: 'DELETE'
})

View File

@ -2,11 +2,11 @@ import store from '@/store'
import { renew } from '@/utils/auth'
import { baseURL } from '@/utils/constants'
export async function fetchURL (url, opts) {
export async function fetchURL(url, opts) {
opts = opts || {}
opts.headers = opts.headers || {}
let { headers, ...rest } = opts
const { headers, ...rest } = opts
const res = await fetch(`${baseURL}${url}`, {
headers: {
@ -23,7 +23,7 @@ export async function fetchURL (url, opts) {
return res
}
export async function fetchJSON (url, opts) {
export async function fetchJSON(url, opts) {
const res = await fetchURL(url, opts)
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')) {
url = url.slice(6)
}

View File

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

View File

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

View File

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

View File

@ -7,12 +7,12 @@
</router-link>
<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>
<span>{{ $t('sidebar.newFolder') }}</span>
</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>
<span>{{ $t('sidebar.newFile') }}</span>
</button>
@ -24,7 +24,7 @@
<span>{{ $t('sidebar.settings') }}</span>
</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>
<span>{{ $t('sidebar.logout') }}</span>
</button>
@ -59,11 +59,11 @@ import * as auth from '@/utils/auth'
import { version, signup, disableExternal, noAuth, authMethod } from '@/utils/constants'
export default {
name: 'sidebar',
name: 'Sidebar',
computed: {
...mapState([ 'user' ]),
...mapGetters([ 'isLogged' ]),
active () {
...mapState(['user']),
...mapGetters(['isLogged']),
active() {
return this.$store.state.show === 'sidebar'
},
signup: () => signup,
@ -73,7 +73,7 @@ export default {
authMethod: () => authMethod
},
methods: {
help () {
help() {
this.$store.commit('showHover', 'help')
},
logout: auth.logout

View File

@ -1,5 +1,5 @@
<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>
<span>{{ $t('buttons.copyFile') }}</span>
</button>
@ -7,9 +7,9 @@
<script>
export default {
name: 'copy-button',
name: 'CopyButton',
methods: {
show: function () {
show: function() {
this.$store.commit('showHover', 'copy')
}
}

View File

@ -1,5 +1,5 @@
<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>
<span>{{ $t('buttons.delete') }}</span>
</button>
@ -7,9 +7,9 @@
<script>
export default {
name: 'delete-button',
name: 'DeleteButton',
methods: {
show: function () {
show: function() {
this.$store.commit('showHover', 'delete')
}
}

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<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>
<span>{{ $t('buttons.moveFile') }}</span>
</button>
@ -7,9 +7,9 @@
<script>
export default {
name: 'move-button',
name: 'MoveButton',
methods: {
show: function () {
show: function() {
this.$store.commit('showHover', 'move')
}
}

View File

@ -1,5 +1,5 @@
<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>
<span>{{ $t('buttons.rename') }}</span>
</button>
@ -7,9 +7,9 @@
<script>
export default {
name: 'rename-button',
name: 'RenameButton',
methods: {
show: function () {
show: function() {
this.$store.commit('showHover', 'rename')
}
}

View File

@ -1,5 +1,5 @@
<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>
<span>{{ $t('buttons.share') }}</span>
</button>
@ -7,9 +7,9 @@
<script>
export default {
name: 'share-button',
name: 'ShareButton',
methods: {
show () {
show() {
this.$store.commit('showHover', 'share')
}
}

View File

@ -1,5 +1,5 @@
<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>
<span>{{ $t('buttons.shell') }}</span>
</button>
@ -7,9 +7,9 @@
<script>
export default {
name: 'shell-button',
name: 'ShellButton',
methods: {
show: function () {
show: function() {
this.$store.commit('toggleShell')
}
}

View File

@ -1,5 +1,5 @@
<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>
<span>{{ $t('buttons.switchView') }}</span>
</button>
@ -10,17 +10,17 @@ import { mapState, mapMutations } from 'vuex'
import { users as api } from '@/api'
export default {
name: 'switch-button',
name: 'SwitchButton',
computed: {
...mapState(['user']),
icon: function () {
icon: function() {
if (this.user.viewMode === 'mosaic') return 'view_list'
return 'view_module'
}
},
methods: {
...mapMutations([ 'updateUser', 'closeHovers' ]),
change: async function () {
...mapMutations(['updateUser', 'closeHovers']),
change: async function() {
this.closeHovers()
const data = {

View File

@ -1,5 +1,5 @@
<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>
<span>{{ $t('buttons.upload') }}</span>
</button>
@ -7,9 +7,9 @@
<script>
export default {
name: 'upload-button',
name: 'UploadButton',
methods: {
upload: function () {
upload: function() {
document.getElementById('upload-input').click()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,16 +19,18 @@
</div>
<div class="card-action">
<button type="submit"
@click="$store.commit('closeHovers')"
<button
type="submit"
class="button button--flat"
: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>
</template>
<script>
export default { name: 'help' }
export default { name: 'Help' }
</script>

View File

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

View File

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

View File

@ -6,15 +6,15 @@
<div class="card-content">
<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 class="card-action">
<button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
@click="$store.commit('closeHovers')"
>{{ $t('buttons.cancel') }}</button>
<button
class="button button--flat"
@ -32,14 +32,14 @@ import { files as api } from '@/api'
import url from '@/utils/url'
export default {
name: 'new-dir',
name: 'NewDir',
data: function() {
return {
name: ''
};
}
},
computed: {
...mapGetters([ 'isFiles', 'isListing' ])
...mapGetters(['isFiles', 'isListing'])
},
methods: {
submit: async function(event) {
@ -66,6 +66,6 @@ export default {
this.$store.commit('closeHovers')
}
}
};
}
</script>

View File

@ -6,21 +6,21 @@
<div class="card-content">
<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 class="card-action">
<button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
@click="$store.commit('closeHovers')"
>{{ $t('buttons.cancel') }}</button>
<button
class="button button--flat"
@click="submit"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')"
@click="submit"
>{{ $t('buttons.create') }}</button>
</div>
</div>
@ -32,14 +32,14 @@ import { files as api } from '@/api'
import url from '@/utils/url'
export default {
name: 'new-file',
name: 'NewFile',
data: function() {
return {
name: ''
};
}
},
computed: {
...mapGetters([ 'isFiles', 'isListing' ])
...mapGetters(['isFiles', 'isListing'])
},
methods: {
submit: async function(event) {
@ -66,6 +66,6 @@ export default {
this.$store.commit('closeHovers')
}
}
};
}
</script>

View File

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

View File

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

View File

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

View File

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

View File

@ -2,20 +2,20 @@
<div>
<h3>{{ $t('settings.userCommands') }}</h3>
<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>
</template>
<script>
export default {
name: 'permissions',
name: 'Permissions',
props: ['commands'],
computed: {
raw: {
get () {
get() {
return this.commands.join(' ')
},
set (value) {
set(value) {
this.$emit('update:commands', value.split(' '))
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -2,26 +2,26 @@
<div>
<p v-if="!isDefault">
<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 v-if="!isDefault">
<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>
<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>
<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 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>
<permissions :perm.sync="user.perm" />
@ -42,21 +42,21 @@ import Permissions from './Permissions'
import Commands from './Commands'
export default {
name: 'user',
name: 'User',
components: {
Permissions,
Languages,
Rules,
Commands
},
props: [ 'user', 'isNew', 'isDefault' ],
props: ['user', 'isNew', 'isDefault'],
computed: {
passwordPlaceholder () {
passwordPlaceholder() {
return this.isNew ? '' : this.$t('settings.avoidChanges')
}
},
watch: {
'user.perm.admin': function () {
'user.perm.admin': function() {
if (!this.user.perm.admin) return
this.user.lockPassword = false
}

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ import router from '@/router'
import { Base64 } from 'js-base64'
import { baseURL } from '@/utils/constants'
export function parseToken (token) {
export function parseToken(token) {
const parts = token.split('.')
if (parts.length !== 3) {
@ -21,7 +21,7 @@ export function parseToken (token) {
store.commit('setUser', data.user)
}
export async function validateLogin () {
export async function validateLogin() {
try {
if (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 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`, {
method: 'POST',
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 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('setUser', null)
localStorage.setItem('jwt', null)
router.push({path: '/login'})
router.push({ path: '/login' })
}

View File

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

View File

@ -1,4 +1,4 @@
export default function (name) {
let re = new RegExp('(?:(?:^|.*;\\s*)' + name + '\\s*\\=\\s*([^;]*).*$)|^.*$')
export default function(name) {
const re = new RegExp('(?:(?:^|.*;\\s*)' + name + '\\s*\\=\\s*([^;]*).*$)|^.*$')
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++) {
rules[i] = rules[i].toLowerCase()
}
let result = null
let find = Array.prototype.find
const find = Array.prototype.find
find.call(document.styleSheets, styleSheet => {
result = find.call(styleSheet.cssRules, cssRule => {

View File

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

View File

@ -24,19 +24,19 @@ Vue.prototype.$showSuccess = (message) => {
}
Vue.prototype.$showError = (error) => {
let btns = [
Noty.button(i18n.t('buttons.close'), '', function () {
const btns = [
Noty.button(i18n.t('buttons.close'), '', function() {
n.close()
})
]
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')
}))
}
let n = new Noty(Object.assign({}, notyDefault, {
const n = new Noty(Object.assign({}, notyDefault, {
text: error.message || error,
type: 'error',
timeout: null,
@ -47,7 +47,7 @@ Vue.prototype.$showError = (error) => {
}
Vue.directive('focus', {
inserted: function (el) {
inserted: function(el) {
el.focus()
}
})

View File

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

View File

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

View File

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

View File

@ -1,12 +1,12 @@
<template>
<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/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>
</ul>
<router-view></router-view>
<router-view />
</div>
</template>
@ -14,7 +14,7 @@
import { mapState } from 'vuex'
export default {
name: 'settings',
computed: mapState([ 'user' ])
name: 'Settings',
computed: mapState(['user'])
}
</script>

View File

@ -1,20 +1,20 @@
<template>
<div class="share" v-if="loaded">
<div v-if="loaded" class="share">
<a target="_blank" :href="link">
<div class="share__box">
<div class="share__box__download" v-if="file.isDir">{{ $t('download.downloadFolder') }}</div>
<div class="share__box__download" v-else>{{ $t('download.downloadFile') }}</div>
<div v-if="file.isDir" class="share__box__download">{{ $t('download.downloadFolder') }}</div>
<div v-else class="share__box__download">{{ $t('download.downloadFile') }}</div>
<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">
<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="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" />
</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="M0 0h24v24H0z" fill="none"/>
<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" />
</svg>
<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>
</a>
@ -27,7 +27,7 @@ import { baseURL } from '@/utils/constants'
import QrcodeVue from 'qrcode.vue'
export default {
name: 'share',
name: 'Share',
components: {
QrcodeVue
},
@ -36,25 +36,25 @@ export default {
notFound: false,
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: {
'$route': 'fetchData'
},
created: function () {
created: function() {
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: {
fetchData: async function () {
fetchData: async function() {
try {
this.file = await api.getHash(this.hash)
this.loaded = true

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
module.exports = {
runtimeCompiler: true,
publicPath: '[{[ .StaticURL ]}]'
}
}

2
go.mod
View File

@ -8,6 +8,7 @@ require (
github.com/caddyserver/caddy v1.0.3
github.com/daaku/go.zipexe v1.0.1 // indirect
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/golang/snappy v0.0.1 // indirect
github.com/gorilla/mux v1.7.3
@ -29,6 +30,7 @@ require (
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
go.etcd.io/bbolt v1.3.3
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/sys v0.0.0-20200523222454-059865788121 // 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/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/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/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
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-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw=
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-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
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.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("/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)
}