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,12 +1,12 @@
<template>
<div>
<router-view></router-view>
<router-view />
</div>
</template>
<script>
export default {
name: 'app',
name: 'App',
mounted() {
const loading = document.getElementById('loading')
loading.classList.add('done')

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

@ -8,7 +8,7 @@ export async function fetch (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) {
@ -34,7 +34,7 @@ export async function fetch (url) {
async function resourceAction(url, method, content) {
url = removePrefix(url)
let opts = { method }
const opts = { method }
if (content) {
opts.body = content
@ -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) + ','
}
@ -86,7 +86,7 @@ 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) {
@ -117,9 +117,9 @@ export async function post (url, content = '', overwrite = false, onupload) {
}
function moveCopy(items, copy = false) {
let promises = []
const promises = []
for (let item of items) {
for (const item of items) {
const from = removePrefix(item.from)
const to = encodeURIComponent(removePrefix(item.to))
const url = `${from}?action=${copy ? 'copy' : 'rename'}&destination=${to}`

View File

@ -23,7 +23,6 @@ export async function create (user) {
} else {
throw new Error(res.status)
}
}
export async function update(user, which = ['all']) {

View File

@ -6,7 +6,7 @@ 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: {

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>
@ -77,7 +77,7 @@ import * as api from '@/api'
import buttons from '@/utils/buttons'
export default {
name: 'header-layout',
name: 'HeaderLayout',
components: {
Search,
InfoButton,

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,12 +33,12 @@
<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>
<p>{{ $t('search.'+v.label) }}</p>
@ -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: [],
@ -91,14 +91,14 @@ export default {
},
watch: {
show(val, old) {
this.active = val === "search"
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,7 +106,7 @@ export default {
} else if (this.active) {
this.reload = false
this.$refs.input.focus()
document.body.style.overflow = "hidden"
document.body.style.overflow = 'hidden'
}
},
value() {
@ -116,8 +116,8 @@ export default {
}
},
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() {
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()
@ -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,5 +1,5 @@
<template>
<div @click="focus" class="shell" ref="scrollable" :class="{ ['shell--hidden']: !showShell}">
<div ref="scrollable" class="shell" :class="{ ['shell--hidden']: !showShell}" @click="focus">
<div v-for="(c, index) in content" :key="index" class="shell__result">
<div class="shell__prompt"><i class="material-icons">chevron_right</i></div>
<pre class="shell__text">{{ c.text }}</pre>
@ -8,13 +8,14 @@
<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,7 +25,7 @@ import { mapMutations, mapState, mapGetters } from 'vuex'
import { commands } from '@/api'
export default {
name: 'shell',
name: 'Shell',
computed: {
...mapState(['user', 'showShell']),
...mapGetters(['isFiles', 'isLogged']),
@ -87,7 +88,7 @@ export default {
this.canInput = false
event.target.innerHTML = ''
let results = {
const results = {
text: `${cmd}\n\n`
}

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,7 +59,7 @@ 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']),

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,7 +7,7 @@
<script>
export default {
name: 'copy-button',
name: 'CopyButton',
methods: {
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,7 +7,7 @@
<script>
export default {
name: 'delete-button',
name: 'DeleteButton',
methods: {
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>
@ -11,7 +11,7 @@ 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'])

View File

@ -7,7 +7,7 @@
<script>
export default {
name: 'info-button',
name: 'InfoButton',
methods: {
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,7 +7,7 @@
<script>
export default {
name: 'move-button',
name: 'MoveButton',
methods: {
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,7 +7,7 @@
<script>
export default {
name: 'rename-button',
name: 'RenameButton',
methods: {
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,7 +7,7 @@
<script>
export default {
name: 'share-button',
name: 'ShareButton',
methods: {
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,7 +7,7 @@
<script>
export default {
name: 'shell-button',
name: 'ShellButton',
methods: {
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,7 +10,7 @@ import { mapState, mapMutations } from 'vuex'
import { users as api } from '@/api'
export default {
name: 'switch-button',
name: 'SwitchButton',
computed: {
...mapState(['user']),
icon: function() {

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,7 +7,7 @@
<script>
export default {
name: 'upload-button',
name: 'UploadButton',
methods: {
upload: function() {
document.getElementById('upload-input').click()

View File

@ -1,5 +1,5 @@
<template>
<form id="editor"></form>
<form id="editor" />
</template>
<script>
@ -13,7 +13,7 @@ import 'ace-builds/webpack-resolver'
import { theme } from '@/utils/constants'
export default {
name: 'editor',
name: 'Editor',
computed: {
...mapState(['req'])
},
@ -27,10 +27,10 @@ export default {
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 || '';
const fileContent = this.req.content || ''
this.editor = ace.edit('editor', {
maxLines: Infinity,
@ -43,8 +43,8 @@ export default {
wrap: true
})
if (theme == 'dark') {
this.editor.setTheme("ace/theme/twilight");
if (theme === 'dark') {
this.editor.setTheme('ace/theme/twilight')
}
},
methods: {

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">
<div id="multiple-selection" :class="{ active: $store.state.multiple }">
<p>{{ $t('files.multipleSelectionEnabled') }}</p>
<div @click="$store.commit('multiple', false)" tabindex="0" role="button" :title="$t('files.clear')" :aria-label="$t('files.clear')" class="action">
<div tabindex="0" role="button" :title="$t('files.clear')" :aria-label="$t('files.clear')" class="action" @click="$store.commit('multiple', false)">
<i class="material-icons">clear</i>
</div>
</div>
@ -94,7 +108,7 @@ import buttons from '@/utils/buttons'
import url from '@/utils/url'
export default {
name: 'listing',
name: 'Listing',
components: { Item },
data: function() {
return {
@ -190,7 +204,7 @@ export default {
return
}
let key = String.fromCharCode(event.which).toLowerCase()
const key = String.fromCharCode(event.which).toLowerCase()
switch (key) {
case 'f':
@ -215,16 +229,16 @@ export default {
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
}
@ -238,9 +252,9 @@ export default {
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 })
@ -264,7 +278,7 @@ export default {
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)`
},
@ -276,7 +290,7 @@ export default {
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
@ -289,8 +303,8 @@ export default {
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
@ -325,7 +339,7 @@ export default {
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)
@ -353,7 +367,7 @@ export default {
this.checkConflict(event.currentTarget.files, this.req.items, '')
},
resetOpacity() {
let items = document.getElementsByClassName('item')
const items = document.getElementsByClassName('item')
Array.from(items).forEach(file => {
file.style.opacity = 1
@ -361,10 +375,10 @@ export default {
},
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)
}

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,23 +32,24 @@
<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',
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)
return this.selected.indexOf(this.index) !== -1
},
icon() {
if (this.isDir) return 'folder'
@ -62,13 +64,17 @@ export default {
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: {
@ -110,16 +116,17 @@ export default {
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)
})

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,
@ -78,23 +96,32 @@ export default {
computed: {
...mapState(['req', 'user', 'oldReq', 'jwt']),
hasPrevious() {
return (this.previousLink !== '')
return this.previousLink !== ''
},
hasNext() {
return (this.nextLink !== '')
return this.nextLink !== ''
},
download() {
return `${baseURL}/api/raw${this.req.path}?auth=${this.jwt}`
},
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.download}&inline=true`
return `${this.thumbnail}&inline=true`
}
},
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 {
@ -114,7 +141,7 @@ export default {
},
methods: {
back() {
let uri = url.removeLastDir(this.$route.path) + '/'
const uri = url.removeLastDir(this.$route.path) + '/'
this.$router.push({ path: uri })
},
prev() {
@ -126,9 +153,11 @@ export default {
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()
}
},

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,7 +34,7 @@ import { files as api } from '@/api'
import buttons from '@/utils/buttons'
export default {
name: 'copy',
name: 'Copy',
components: { FileList },
data: function() {
return {
@ -43,10 +47,10 @@ export default {
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,14 +5,18 @@
<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>
@ -24,7 +28,7 @@ import url from '@/utils/url'
import buttons from '@/utils/buttons'
export default {
name: 'delete',
name: 'Delete',
computed: {
...mapGetters(['isListing', 'selectedCount']),
...mapState(['req', 'selected'])
@ -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,13 +7,13 @@
<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>
@ -23,7 +23,7 @@ import {mapGetters, mapState} from 'vuex'
import { files as api } from '@/api'
export default {
name: 'download',
name: 'Download',
computed: {
...mapState(['selected', 'req']),
...mapGetters(['selectedCount'])
@ -33,9 +33,9 @@ export default {
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,7 +25,7 @@ import url from '@/utils/url'
import { files } from '@/api'
export default {
name: 'file-list',
name: 'FileList',
data: function() {
return {
items: [],
@ -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({
@ -92,14 +95,14 @@ export default {
// 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
const url = event.currentTarget.dataset.url
// In 300 milliseconds, we shall reset the count.
setTimeout(() => {

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,11 +25,13 @@
</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>
@ -41,7 +43,7 @@ import moment from 'moment'
import { files as api } from '@/api'
export default {
name: 'info',
name: 'Info',
computed: {
...mapState(['req', 'selected']),
...mapGetters(['selectedCount', 'isListing']),
@ -52,7 +54,7 @@ export default {
let sum = 0
for (let selected of this.selected) {
for (const selected of this.selected) {
sum += this.req.items[selected].size
}

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,7 +33,7 @@ import { files as api } from '@/api'
import buttons from '@/utils/buttons'
export default {
name: 'move',
name: 'Move',
components: { FileList },
data: function() {
return {
@ -42,9 +46,9 @@ export default {
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,11 +32,11 @@ 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'])
@ -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,11 +32,11 @@ 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'])
@ -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,
@ -59,9 +59,9 @@ 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() {
return (this.show !== null && this.show !== 'search' && this.show !== 'more')

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,7 +33,7 @@ import url from '@/utils/url'
import { files as api } from '@/api'
export default {
name: 'rename',
name: 'Rename',
data: function() {
return {
name: ''

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,7 +75,7 @@ import moment from 'moment'
import Clipboard from 'clipboard'
export default {
name: 'share',
name: 'Share',
data: function() {
return {
time: '',
@ -97,7 +107,7 @@ export default {
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

View File

@ -2,13 +2,13 @@
<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: {

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',
name: 'Languages',
props: ['locale'],
data() {
let dataObj = {
const dataObj = {
locales: {
ar: 'ar',
de: 'de',
@ -31,11 +31,11 @@ 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) {

View File

@ -3,21 +3,21 @@
<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: {

View File

@ -1,39 +1,41 @@
<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) {
event.preventDefault()
let rules = [ ...this.rules ]
const rules = [...this.rules]
rules.splice(index, 1)
this.$emit('update:rules', [...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,7 +7,7 @@
<script>
export default {
name: 'themes',
name: 'Themes',
props: ['theme'],
methods: {
change(event) {

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,7 +42,7 @@ import Permissions from './Permissions'
import Commands from './Commands'
export default {
name: 'user',
name: 'User',
components: {
Permissions,
Languages,

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;
}
@ -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,14 +163,14 @@
#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);
}
@ -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

@ -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

@ -55,7 +55,7 @@ export async function renew (jwt) {
const res = await fetch(`${baseURL}/api/renew`, {
method: 'POST',
headers: {
'X-Auth': jwt,
'X-Auth': jwt
}
})

View File

@ -1,5 +1,5 @@
function loading(button) {
let el = document.querySelector(`#${button}-button > i`)
const el = document.querySelector(`#${button}-button > i`)
if (el === undefined || el === null) {
console.log('Error getting button ' + button) // eslint-disable-line
@ -17,7 +17,7 @@ function loading (button) {
}
function done(button) {
let el = document.querySelector(`#${button}-button > i`)
const el = document.querySelector(`#${button}-button > i`)
if (el === undefined || el === null) {
console.log('Error getting button ' + button) // eslint-disable-line
@ -34,7 +34,7 @@ function done (button) {
}
function success(button) {
let el = document.querySelector(`#${button}-button > i`)
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*([^;]*).*$)|^.*$')
const re = new RegExp('(?:(?:^|.*;\\s*)' + name + '\\s*\\=\\s*([^;]*).*$)|^.*$')
return document.cookie.replace(re, '$1')
}

View File

@ -4,7 +4,7 @@ export default function getRule (rules) {
}
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

@ -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).
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').
.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);
.replace(/%(?:7C|60|5E)/g, unescape)
}
export default {

View File

@ -24,7 +24,7 @@ Vue.prototype.$showSuccess = (message) => {
}
Vue.prototype.$showError = (error) => {
let btns = [
const btns = [
Noty.button(i18n.t('buttons.close'), '', function() {
n.close()
})
@ -36,7 +36,7 @@ Vue.prototype.$showError = (error) => {
}))
}
let n = new Noty(Object.assign({}, notyDefault, {
const n = new Noty(Object.assign({}, notyDefault, {
text: error.message || error,
type: 'error',
timeout: null,

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>
@ -40,7 +40,7 @@ function clean (path) {
}
export default {
name: 'files',
name: 'Files',
components: {
Forbidden,
NotFound,
@ -67,7 +67,7 @@ export default {
return !this.loading && !this.isListing && !this.isEditor
},
breadcrumbs() {
let parts = this.$route.path.split('/')
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) {
@ -105,15 +105,15 @@ export default {
error: null
}
},
created () {
this.fetchData()
},
watch: {
'$route': 'fetchData',
'reload': function() {
this.fetchData()
}
},
created() {
this.fetchData()
},
mounted() {
window.addEventListener('keydown', this.keyEvent)
window.addEventListener('scroll', this.scroll)

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,

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,12 +22,7 @@ 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
},
name: 'Login',
data: function() {
return {
createMode: false,
@ -38,6 +33,11 @@ export default {
passwordConfirm: ''
}
},
computed: {
signup: () => signup,
name: () => name,
logoURL: () => logoURL
},
mounted() {
if (!recaptcha) return
@ -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',
name: 'Settings',
computed: mapState(['user'])
}
</script>

View File

@ -1,9 +1,9 @@
<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" />
@ -14,7 +14,7 @@
<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,12 +36,6 @@ export default {
notFound: false,
file: null
}),
watch: {
'$route': 'fetchData'
},
created: function () {
this.fetchData()
},
computed: {
hash: function() {
return this.$route.params.pathMatch
@ -51,7 +45,13 @@ export default {
},
fullLink: function() {
return window.location.origin + this.link
}
},
watch: {
'$route': 'fetchData'
},
created: function() {
this.fetchData()
},
methods: {
fetchData: async function() {

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,7 +106,7 @@ import Rules from '@/components/settings/Rules'
import Themes from '@/components/settings/Themes'
export default {
name: 'settings',
name: 'Settings',
components: {
Themes,
UserForm,
@ -124,7 +124,7 @@ export default {
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({
@ -144,7 +144,7 @@ export default {
methods: {
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++) {
@ -154,7 +154,7 @@ export default {
return name.slice(0, -1)
},
async save() {
let settings = {
const settings = {
...this.settings,
shell: this.settings.shell.trim().split(' ').filter(s => s !== ''),
commands: {}

View File

@ -7,7 +7,7 @@
<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,7 +38,7 @@ import { users as api } from '@/api'
import Languages from '@/components/settings/Languages'
export default {
name: 'settings',
name: 'Settings',
components: {
Languages
},

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,9 +71,6 @@ export default {
loaded: false
}
},
created () {
this.fetchData()
},
computed: {
isNew() {
return this.$route.path === '/settings/users/new'
@ -80,12 +83,15 @@ export default {
this.user.lockPassword = false
}
},
created() {
this.fetchData()
},
methods: {
...mapMutations(['closeHovers', 'showHover', 'setUser']),
async fetchData() {
try {
if (this.isNew) {
let { defaults } = await settings.get()
const { defaults } = await settings.get()
this.user = {
...defaults,
username: '',
@ -120,7 +126,7 @@ export default {
},
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,7 +31,7 @@
import { users as api } from '@/api'
export default {
name: 'users',
name: 'Users',
data: function() {
return {
users: []

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)
}