├── .circleci └── config.yml ├── .gitignore ├── .tx └── config ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public ├── img │ ├── icons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── mstile-144x144.png │ │ ├── mstile-150x150.png │ │ ├── mstile-310x150.png │ │ ├── mstile-310x310.png │ │ ├── mstile-70x70.png │ │ └── safari-pinned-tab.svg │ └── logo.svg ├── index.html └── manifest.json ├── src ├── App.vue ├── api │ ├── commands.js │ ├── files.js │ ├── index.js │ ├── search.js │ ├── settings.js │ ├── share.js │ ├── users.js │ └── utils.js ├── assets │ └── fonts │ │ └── roboto │ │ ├── medium-cyrillic-ext.woff2 │ │ ├── medium-cyrillic.woff2 │ │ ├── medium-greek-ext.woff2 │ │ ├── medium-greek.woff2 │ │ ├── medium-latin-ext.woff2 │ │ ├── medium-latin.woff2 │ │ ├── medium-vietnamese.woff2 │ │ ├── normal-cyrillic-ext.woff2 │ │ ├── normal-cyrillic.woff2 │ │ ├── normal-greek-ext.woff2 │ │ ├── normal-greek.woff2 │ │ ├── normal-latin-ext.woff2 │ │ ├── normal-latin.woff2 │ │ └── normal-vietnamese.woff2 ├── components │ ├── Header.vue │ ├── Search.vue │ ├── Shell.vue │ ├── Sidebar.vue │ ├── buttons │ │ ├── Copy.vue │ │ ├── Delete.vue │ │ ├── Download.vue │ │ ├── Info.vue │ │ ├── Move.vue │ │ ├── Rename.vue │ │ ├── Share.vue │ │ ├── Shell.vue │ │ ├── SwitchView.vue │ │ └── Upload.vue │ ├── files │ │ ├── Editor.vue │ │ ├── Listing.vue │ │ ├── ListingItem.vue │ │ └── Preview.vue │ ├── prompts │ │ ├── Copy.vue │ │ ├── Delete.vue │ │ ├── Download.vue │ │ ├── FileList.vue │ │ ├── Help.vue │ │ ├── Info.vue │ │ ├── Move.vue │ │ ├── NewDir.vue │ │ ├── NewFile.vue │ │ ├── Prompts.vue │ │ ├── Rename.vue │ │ ├── Replace.vue │ │ └── Share.vue │ └── settings │ │ ├── Commands.vue │ │ ├── Languages.vue │ │ ├── Permissions.vue │ │ ├── Rules.vue │ │ └── UserForm.vue ├── css │ ├── _buttons.css │ ├── _inputs.css │ ├── _share.css │ ├── _shell.css │ ├── _variables.css │ ├── base.css │ ├── dashboard.css │ ├── fonts.css │ ├── header.css │ ├── listing.css │ ├── login.css │ ├── mobile.css │ └── styles.css ├── i18n │ ├── ar.json │ ├── de.json │ ├── en.json │ ├── es.json │ ├── fr.json │ ├── index.js │ ├── is.json │ ├── it.json │ ├── ja.json │ ├── ko.json │ ├── pl.json │ ├── pt-br.json │ ├── pt.json │ ├── ro.json │ ├── ru.json │ ├── zh-cn.json │ └── zh-tw.json ├── main.js ├── router │ └── index.js ├── store │ ├── getters.js │ ├── index.js │ └── mutations.js ├── utils │ ├── auth.js │ ├── buttons.js │ ├── constants.js │ ├── cookie.js │ ├── css.js │ ├── url.js │ └── vue.js └── views │ ├── Files.vue │ ├── Layout.vue │ ├── Login.vue │ ├── Settings.vue │ ├── Share.vue │ ├── errors │ ├── 403.vue │ ├── 404.vue │ └── 500.vue │ └── settings │ ├── Global.vue │ ├── Profile.vue │ ├── User.vue │ └── Users.vue └── vue.config.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node 6 | steps: 7 | - checkout 8 | - run: npm install 9 | - run: npm run lint 10 | - run: npm run build 11 | workflows: 12 | version: 2 13 | build: 14 | jobs: 15 | - build 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | lang_map = pt_BR: pt-br, zh_CN: zh-cn, zh_HK: zh-hk, zh_TW: zh-tw 4 | 5 | [file-browser.file-browser] 6 | file_filter = src/i18n/.json 7 | minimum_perc = 50 8 | source_file = src/i18n/en.json 9 | source_lang = en 10 | type = KEYVALUEJSON 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # File Browser Front-end 2 | 3 | [![CircleCI](https://img.shields.io/circleci/project/github/filebrowser/frontend.svg?style=flat-square)](https://circleci.com/gh/filebrowser/frontend) 4 | [![npm](https://img.shields.io/npm/v/filebrowser-frontend.svg?style=flat-square)]() 5 | [![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) 6 | [![Chat IRC](https://img.shields.io/badge/freenode-%23filebrowser-blue.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23filebrowser) 7 | 8 | > This is an example file with default selections. 9 | 10 | ## Install 11 | 12 | ``` 13 | npm install filebrowser-frontend 14 | ``` 15 | 16 | ## Usage 17 | 18 | This package is not prepared to be used by other projects than [File Browser](https://github.com/filebrowser/filebrowser) itself. 19 | 20 | ## Contribute 21 | 22 | Check the [community repository](https://github.com/filebrowser/community) for more information. 23 | 24 | ## License 25 | 26 | [Apache 2.0](./LICENSE) File Browser Contributors 27 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "filebrowser-frontend", 3 | "version": "2.0.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "watch": "vue-cli-service build --watch", 9 | "lint": "vue-cli-service lint --fix" 10 | }, 11 | "dependencies": { 12 | "ace-builds": "^1.4.4", 13 | "clipboard": "^2.0.4", 14 | "js-base64": "^2.5.1", 15 | "lodash.clonedeep": "^4.5.0", 16 | "material-design-icons": "^3.0.1", 17 | "moment": "^2.24.0", 18 | "normalize.css": "^8.0.1", 19 | "noty": "^3.2.0-beta", 20 | "qrcode.vue": "^1.6.1", 21 | "vue": "^2.6.10", 22 | "vue-i18n": "^8.11.2", 23 | "vue-router": "^3.0.6", 24 | "vuex": "^3.1.1", 25 | "vuex-router-sync": "^5.0.0" 26 | }, 27 | "devDependencies": { 28 | "@vue/cli-plugin-babel": "^3.7.0", 29 | "@vue/cli-plugin-eslint": "^3.7.0", 30 | "@vue/cli-service": "^3.7.0", 31 | "babel-eslint": "^10.0.1", 32 | "eslint": "^5.16.0", 33 | "eslint-plugin-vue": "^5.2.2", 34 | "vue-template-compiler": "^2.6.10" 35 | }, 36 | "eslintConfig": { 37 | "root": true, 38 | "env": { 39 | "node": true 40 | }, 41 | "extends": [ 42 | "plugin:vue/essential", 43 | "eslint:recommended" 44 | ], 45 | "rules": {}, 46 | "parserOptions": { 47 | "parser": "babel-eslint" 48 | } 49 | }, 50 | "postcss": { 51 | "plugins": { 52 | "autoprefixer": {} 53 | } 54 | }, 55 | "browserslist": [ 56 | "> 1%", 57 | "last 2 versions", 58 | "not ie <= 8" 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /public/img/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebrowser/frontend/d45d7f92fb16c05e3b4d67e86aebf1feb3413227/public/img/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/img/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebrowser/frontend/d45d7f92fb16c05e3b4d67e86aebf1feb3413227/public/img/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebrowser/frontend/d45d7f92fb16c05e3b4d67e86aebf1feb3413227/public/img/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/img/icons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #455a64 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebrowser/frontend/d45d7f92fb16c05e3b4d67e86aebf1feb3413227/public/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebrowser/frontend/d45d7f92fb16c05e3b4d67e86aebf1feb3413227/public/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/img/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebrowser/frontend/d45d7f92fb16c05e3b4d67e86aebf1feb3413227/public/img/icons/favicon.ico -------------------------------------------------------------------------------- /public/img/icons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebrowser/frontend/d45d7f92fb16c05e3b4d67e86aebf1feb3413227/public/img/icons/mstile-144x144.png -------------------------------------------------------------------------------- /public/img/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebrowser/frontend/d45d7f92fb16c05e3b4d67e86aebf1feb3413227/public/img/icons/mstile-150x150.png -------------------------------------------------------------------------------- /public/img/icons/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebrowser/frontend/d45d7f92fb16c05e3b4d67e86aebf1feb3413227/public/img/icons/mstile-310x150.png -------------------------------------------------------------------------------- /public/img/icons/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebrowser/frontend/d45d7f92fb16c05e3b4d67e86aebf1feb3413227/public/img/icons/mstile-310x310.png -------------------------------------------------------------------------------- /public/img/icons/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebrowser/frontend/d45d7f92fb16c05e3b4d67e86aebf1feb3413227/public/img/icons/mstile-70x70.png -------------------------------------------------------------------------------- /public/img/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 21 | 32 | 34 | 36 | 38 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /public/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 44 | 46 | 61 | 62 | 147 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | [{[ if .ReCaptcha -]}] 9 | 10 | [{[ end ]}] 11 | 12 | [{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}] 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 95 | 96 | 97 |
98 | 99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | 107 | [{[ if .CSS -]}] 108 | 109 | [{[ end ]}] 110 | 111 | 112 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "File Browser", 3 | "short_name": "File Browser", 4 | "icons": [ 5 | { 6 | "src": "./img/icons/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "./static/img/icons/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": "./", 17 | "display": "standalone", 18 | "background_color": "#ffffff", 19 | "theme_color": "#455a64" 20 | } 21 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | 21 | 24 | -------------------------------------------------------------------------------- /src/api/commands.js: -------------------------------------------------------------------------------- 1 | import { removePrefix } from './utils' 2 | import { baseURL } from '@/utils/constants' 3 | import store from '@/store' 4 | 5 | const ssl = (window.location.protocol === 'https:') 6 | const protocol = (ssl ? 'wss:' : 'ws:') 7 | 8 | export default function command(url, command, onmessage, onclose) { 9 | url = removePrefix(url) 10 | url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${store.state.jwt}` 11 | 12 | let conn = new window.WebSocket(url) 13 | conn.onopen = () => conn.send(command) 14 | conn.onmessage = onmessage 15 | conn.onclose = onclose 16 | } 17 | -------------------------------------------------------------------------------- /src/api/files.js: -------------------------------------------------------------------------------- 1 | import { fetchURL, removePrefix } from './utils' 2 | import { baseURL } from '@/utils/constants' 3 | import store from '@/store' 4 | 5 | export async function fetch (url) { 6 | url = removePrefix(url) 7 | 8 | const res = await fetchURL(`/api/resources${url}`, {}) 9 | 10 | if (res.status === 200) { 11 | let data = await res.json() 12 | data.url = `/files${url}` 13 | 14 | if (data.isDir) { 15 | if (!data.url.endsWith('/')) data.url += '/' 16 | data.items = data.items.map((item, index) => { 17 | item.index = index 18 | item.url = `${data.url}${encodeURIComponent(item.name)}` 19 | 20 | if (item.isDir) { 21 | item.url += '/' 22 | } 23 | 24 | return item 25 | }) 26 | } 27 | 28 | return data 29 | } else { 30 | throw new Error(res.status) 31 | } 32 | } 33 | 34 | async function resourceAction (url, method, content) { 35 | url = removePrefix(url) 36 | 37 | let opts = { method } 38 | 39 | if (content) { 40 | opts.body = content 41 | } 42 | 43 | const res = await fetchURL(`/api/resources${url}`, opts) 44 | 45 | if (res.status !== 200) { 46 | throw new Error(res.responseText) 47 | } else { 48 | return res 49 | } 50 | } 51 | 52 | export async function remove (url) { 53 | return resourceAction(url, 'DELETE') 54 | } 55 | 56 | export async function put (url, content = '') { 57 | return resourceAction(url, 'PUT', content) 58 | } 59 | 60 | export function download (format, ...files) { 61 | let url = `${baseURL}/api/raw` 62 | 63 | if (files.length === 1) { 64 | url += removePrefix(files[0]) + '?' 65 | } else { 66 | let arg = '' 67 | 68 | for (let file of files) { 69 | arg += removePrefix(file) + ',' 70 | } 71 | 72 | arg = arg.substring(0, arg.length - 1) 73 | arg = encodeURIComponent(arg) 74 | url += `/?files=${arg}&` 75 | } 76 | 77 | if (format !== null) { 78 | url += `algo=${format}&` 79 | } 80 | 81 | url += `auth=${store.state.jwt}` 82 | window.open(url) 83 | } 84 | 85 | export async function post (url, content = '', overwrite = false, onupload) { 86 | url = removePrefix(url) 87 | 88 | return new Promise((resolve, reject) => { 89 | let request = new XMLHttpRequest() 90 | request.open('POST', `${baseURL}/api/resources${url}?override=${overwrite}`, true) 91 | request.setRequestHeader('X-Auth', store.state.jwt) 92 | 93 | if (typeof onupload === 'function') { 94 | request.upload.onprogress = onupload 95 | } 96 | 97 | // Send a message to user before closing the tab during file upload 98 | window.onbeforeunload = () => "Files are being uploaded." 99 | 100 | request.onload = () => { 101 | if (request.status === 200) { 102 | resolve(request.responseText) 103 | } else if (request.status === 409) { 104 | reject(request.status) 105 | } else { 106 | reject(request.responseText) 107 | } 108 | } 109 | 110 | request.onerror = (error) => { 111 | reject(error) 112 | } 113 | 114 | request.send(content) 115 | // Upload is done no more message before closing the tab 116 | }).finally(() => { window.onbeforeunload = null }) 117 | } 118 | 119 | function moveCopy (items, copy = false) { 120 | let promises = [] 121 | 122 | for (let item of items) { 123 | const from = removePrefix(item.from) 124 | const to = encodeURIComponent(removePrefix(item.to)) 125 | const url = `${from}?action=${copy ? 'copy' : 'rename'}&destination=${to}` 126 | promises.push(resourceAction(url, 'PATCH')) 127 | } 128 | 129 | return Promise.all(promises) 130 | } 131 | 132 | export function move (items) { 133 | return moveCopy(items) 134 | } 135 | 136 | export function copy (items) { 137 | return moveCopy(items, true) 138 | } 139 | 140 | export async function checksum (url, algo) { 141 | const data = await resourceAction(`${url}?checksum=${algo}`, 'GET') 142 | return (await data.json()).checksums[algo] 143 | } 144 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | import * as files from './files' 2 | import * as share from './share' 3 | import * as users from './users' 4 | import * as settings from './settings' 5 | import search from './search' 6 | import commands from './commands' 7 | 8 | export { 9 | files, 10 | share, 11 | users, 12 | settings, 13 | commands, 14 | search 15 | } 16 | -------------------------------------------------------------------------------- /src/api/search.js: -------------------------------------------------------------------------------- 1 | import { fetchJSON, removePrefix } from './utils' 2 | 3 | export default async function search (url, query) { 4 | url = removePrefix(url) 5 | query = encodeURIComponent(query) 6 | 7 | return fetchJSON(`/api/search${url}?query=${query}`, {}) 8 | } 9 | -------------------------------------------------------------------------------- /src/api/settings.js: -------------------------------------------------------------------------------- 1 | import { fetchURL, fetchJSON } from './utils' 2 | 3 | export function get () { 4 | return fetchJSON(`/api/settings`, {}) 5 | } 6 | 7 | export async function update (settings) { 8 | const res = await fetchURL(`/api/settings`, { 9 | method: 'PUT', 10 | body: JSON.stringify(settings) 11 | }) 12 | 13 | if (res.status !== 200) { 14 | throw new Error(res.status) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/api/share.js: -------------------------------------------------------------------------------- 1 | import { fetchURL, fetchJSON, removePrefix } from './utils' 2 | 3 | export async function getHash(hash) { 4 | return fetchJSON(`/api/public/share/${hash}`) 5 | } 6 | 7 | export async function get(url) { 8 | url = removePrefix(url) 9 | return fetchJSON(`/api/share${url}`) 10 | } 11 | 12 | export async function remove(hash) { 13 | const res = await fetchURL(`/api/share/${hash}`, { 14 | method: 'DELETE' 15 | }) 16 | 17 | if (res.status !== 200) { 18 | throw new Error(res.status) 19 | } 20 | } 21 | 22 | export async function create(url, expires = '', unit = 'hours') { 23 | url = removePrefix(url) 24 | url = `/api/share${url}` 25 | if (expires !== '') { 26 | url += `?expires=${expires}&unit=${unit}` 27 | } 28 | 29 | return fetchJSON(url, { 30 | method: 'POST' 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /src/api/users.js: -------------------------------------------------------------------------------- 1 | import { fetchURL, fetchJSON } from './utils' 2 | 3 | export async function getAll () { 4 | return fetchJSON(`/api/users`, {}) 5 | } 6 | 7 | export async function get (id) { 8 | return fetchJSON(`/api/users/${id}`, {}) 9 | } 10 | 11 | export async function create (user) { 12 | const res = await fetchURL(`/api/users`, { 13 | method: 'POST', 14 | body: JSON.stringify({ 15 | what: 'user', 16 | which: [], 17 | data: user 18 | }) 19 | }) 20 | 21 | if (res.status === 201) { 22 | return res.headers.get('Location') 23 | } else { 24 | throw new Error(res.status) 25 | } 26 | 27 | } 28 | 29 | export async function update (user, which = ['all']) { 30 | const res = await fetchURL(`/api/users/${user.id}`, { 31 | method: 'PUT', 32 | body: JSON.stringify({ 33 | what: 'user', 34 | which: which, 35 | data: user 36 | }) 37 | }) 38 | 39 | if (res.status !== 200) { 40 | throw new Error(res.status) 41 | } 42 | } 43 | 44 | export async function remove (id) { 45 | const res = await fetchURL(`/api/users/${id}`, { 46 | method: 'DELETE' 47 | }) 48 | 49 | if (res.status !== 200) { 50 | throw new Error(res.status) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/api/utils.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | import { renew } from '@/utils/auth' 3 | import { baseURL } from '@/utils/constants' 4 | 5 | export async function fetchURL (url, opts) { 6 | opts = opts || {} 7 | opts.headers = opts.headers || {} 8 | 9 | let { headers, ...rest } = opts 10 | 11 | const res = await fetch(`${baseURL}${url}`, { 12 | headers: { 13 | 'X-Auth': store.state.jwt, 14 | ...headers 15 | }, 16 | ...rest 17 | }) 18 | 19 | if (res.headers.get('X-Renew-Token') === 'true') { 20 | await renew(store.state.jwt) 21 | } 22 | 23 | return res 24 | } 25 | 26 | export async function fetchJSON (url, opts) { 27 | const res = await fetchURL(url, opts) 28 | 29 | if (res.status === 200) { 30 | return res.json() 31 | } else { 32 | throw new Error(res.status) 33 | } 34 | } 35 | 36 | export function removePrefix (url) { 37 | if (url.startsWith('/files')) { 38 | url = url.slice(6) 39 | } 40 | 41 | if (url === '') url = '/' 42 | if (url[0] !== '/') url = '/' + url 43 | return url 44 | } 45 | 46 | -------------------------------------------------------------------------------- /src/assets/fonts/roboto/medium-cyrillic-ext.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebrowser/frontend/d45d7f92fb16c05e3b4d67e86aebf1feb3413227/src/assets/fonts/roboto/medium-cyrillic-ext.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/roboto/medium-cyrillic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebrowser/frontend/d45d7f92fb16c05e3b4d67e86aebf1feb3413227/src/assets/fonts/roboto/medium-cyrillic.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/roboto/medium-greek-ext.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebrowser/frontend/d45d7f92fb16c05e3b4d67e86aebf1feb3413227/src/assets/fonts/roboto/medium-greek-ext.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/roboto/medium-greek.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebrowser/frontend/d45d7f92fb16c05e3b4d67e86aebf1feb3413227/src/assets/fonts/roboto/medium-greek.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/roboto/medium-latin-ext.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebrowser/frontend/d45d7f92fb16c05e3b4d67e86aebf1feb3413227/src/assets/fonts/roboto/medium-latin-ext.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/roboto/medium-latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebrowser/frontend/d45d7f92fb16c05e3b4d67e86aebf1feb3413227/src/assets/fonts/roboto/medium-latin.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/roboto/medium-vietnamese.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebrowser/frontend/d45d7f92fb16c05e3b4d67e86aebf1feb3413227/src/assets/fonts/roboto/medium-vietnamese.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/roboto/normal-cyrillic-ext.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebrowser/frontend/d45d7f92fb16c05e3b4d67e86aebf1feb3413227/src/assets/fonts/roboto/normal-cyrillic-ext.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/roboto/normal-cyrillic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebrowser/frontend/d45d7f92fb16c05e3b4d67e86aebf1feb3413227/src/assets/fonts/roboto/normal-cyrillic.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/roboto/normal-greek-ext.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebrowser/frontend/d45d7f92fb16c05e3b4d67e86aebf1feb3413227/src/assets/fonts/roboto/normal-greek-ext.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/roboto/normal-greek.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebrowser/frontend/d45d7f92fb16c05e3b4d67e86aebf1feb3413227/src/assets/fonts/roboto/normal-greek.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/roboto/normal-latin-ext.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebrowser/frontend/d45d7f92fb16c05e3b4d67e86aebf1feb3413227/src/assets/fonts/roboto/normal-latin-ext.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/roboto/normal-latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebrowser/frontend/d45d7f92fb16c05e3b4d67e86aebf1feb3413227/src/assets/fonts/roboto/normal-latin.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/roboto/normal-vietnamese.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebrowser/frontend/d45d7f92fb16c05e3b4d67e86aebf1feb3413227/src/assets/fonts/roboto/normal-vietnamese.woff2 -------------------------------------------------------------------------------- /src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 190 | -------------------------------------------------------------------------------- /src/components/Search.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 199 | -------------------------------------------------------------------------------- /src/components/Shell.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 116 | -------------------------------------------------------------------------------- /src/components/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 82 | -------------------------------------------------------------------------------- /src/components/buttons/Copy.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /src/components/buttons/Delete.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /src/components/buttons/Download.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 36 | -------------------------------------------------------------------------------- /src/components/buttons/Info.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /src/components/buttons/Move.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /src/components/buttons/Rename.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /src/components/buttons/Share.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /src/components/buttons/Shell.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /src/components/buttons/SwitchView.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 41 | -------------------------------------------------------------------------------- /src/components/buttons/Upload.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /src/components/files/Editor.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 76 | -------------------------------------------------------------------------------- /src/components/files/ListingItem.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 170 | -------------------------------------------------------------------------------- /src/components/files/Preview.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 159 | -------------------------------------------------------------------------------- /src/components/prompts/Copy.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 68 | -------------------------------------------------------------------------------- /src/components/prompts/Delete.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 67 | -------------------------------------------------------------------------------- /src/components/prompts/Download.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 50 | -------------------------------------------------------------------------------- /src/components/prompts/FileList.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 141 | -------------------------------------------------------------------------------- /src/components/prompts/Help.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 34 | 35 | -------------------------------------------------------------------------------- /src/components/prompts/Info.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 99 | -------------------------------------------------------------------------------- /src/components/prompts/Move.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 68 | -------------------------------------------------------------------------------- /src/components/prompts/NewDir.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 71 | 72 | -------------------------------------------------------------------------------- /src/components/prompts/NewFile.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 71 | 72 | -------------------------------------------------------------------------------- /src/components/prompts/Prompts.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 80 | -------------------------------------------------------------------------------- /src/components/prompts/Rename.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 90 | -------------------------------------------------------------------------------- /src/components/prompts/Replace.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 32 | -------------------------------------------------------------------------------- /src/components/prompts/Share.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 167 | 168 | -------------------------------------------------------------------------------- /src/components/settings/Commands.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 25 | -------------------------------------------------------------------------------- /src/components/settings/Languages.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 31 | -------------------------------------------------------------------------------- /src/components/settings/Permissions.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 40 | -------------------------------------------------------------------------------- /src/components/settings/Rules.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 58 | -------------------------------------------------------------------------------- /src/components/settings/UserForm.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 66 | -------------------------------------------------------------------------------- /src/css/_buttons.css: -------------------------------------------------------------------------------- 1 | .button { 2 | outline: 0; 3 | border: 0; 4 | padding: .5em 1em; 5 | border-radius: .1em; 6 | cursor: pointer; 7 | background: var(--blue); 8 | color: white; 9 | border: 1px solid rgba(0, 0, 0, 0.05); 10 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.05); 11 | transition: .1s ease all; 12 | } 13 | 14 | .button:hover { 15 | background-color: var(--dark-blue); 16 | } 17 | 18 | .button--block { 19 | margin: 0 0 0.5em; 20 | display: block; 21 | width: 100%; 22 | } 23 | 24 | .button--red { 25 | background: var(--red); 26 | } 27 | 28 | .button--red:hover { 29 | background: var(--dark-red); 30 | } 31 | 32 | .button--flat { 33 | color: var(--dark-blue); 34 | background: transparent; 35 | box-shadow: 0 0 0; 36 | border: 0; 37 | text-transform: uppercase; 38 | } 39 | 40 | .button--flat:hover { 41 | background: var(--moon-grey); 42 | } 43 | 44 | .button--flat.button--red { 45 | color: var(--dark-red); 46 | } 47 | 48 | .button--flat.button--grey { 49 | color: #6f6f6f; 50 | } 51 | 52 | .button[disabled] { 53 | opacity: .5; 54 | cursor: not-allowed; 55 | } 56 | -------------------------------------------------------------------------------- /src/css/_inputs.css: -------------------------------------------------------------------------------- 1 | .input { 2 | border-radius: .1em; 3 | padding: .5em 1em; 4 | background: white; 5 | border: 1px solid rgba(0, 0, 0, 0.1); 6 | transition: .2s ease all; 7 | color: #333; 8 | margin: 0; 9 | } 10 | 11 | .input:hover, 12 | .input:focus { 13 | border-color: rgba(0, 0, 0, 0.2); 14 | } 15 | 16 | .input--block { 17 | margin-bottom: .5em; 18 | display: block; 19 | width: 100%; 20 | } 21 | 22 | .input--textarea { 23 | line-height: 1.15; 24 | font-family: monospace; 25 | min-height: 10em; 26 | resize: vertical; 27 | } 28 | 29 | .input--red { 30 | background: #fcd0cd; 31 | } 32 | 33 | .input--green { 34 | background: #c9f2da; 35 | } 36 | -------------------------------------------------------------------------------- /src/css/_share.css: -------------------------------------------------------------------------------- 1 | .share__box { 2 | text-align: center; 3 | box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px; 4 | background: #fff; 5 | display: block; 6 | border-radius: 0.2em; 7 | width: 90%; 8 | max-width: 25em; 9 | margin: 6em auto; 10 | } 11 | 12 | .share__box__download { 13 | width: 100%; 14 | padding: 1em; 15 | cursor: pointer; 16 | background: #ffffff; 17 | color: rgba(0, 0, 0, 0.5); 18 | border-bottom: 1px solid rgba(0, 0, 0, 0.05); 19 | } 20 | 21 | .share__box__info { 22 | padding: 2em 3em; 23 | } 24 | 25 | .share__box__title { 26 | margin-top: .2em; 27 | overflow: hidden; 28 | text-overflow: ellipsis; 29 | } 30 | -------------------------------------------------------------------------------- /src/css/_shell.css: -------------------------------------------------------------------------------- 1 | .shell { 2 | position: fixed; 3 | bottom: 0; 4 | left: 0; 5 | height: 25em; 6 | max-height: calc(100% - 4em); 7 | background: white; 8 | color: #212121; 9 | z-index: 9999; 10 | width: 100%; 11 | font-family: monospace; 12 | overflow: auto; 13 | font-size: 1rem; 14 | cursor: text; 15 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); 16 | transition: .2s ease transform; 17 | } 18 | 19 | .shell__result { 20 | display: flex; 21 | padding: 0.5em; 22 | align-items: flex-start; 23 | border-top: 1px solid rgba(0, 0, 0, 0.05); 24 | } 25 | 26 | .shell--hidden { 27 | transform: translateY(105%); 28 | } 29 | 30 | .shell__result--hidden { 31 | opacity: 0; 32 | } 33 | 34 | .shell__text, 35 | .shell__prompt, 36 | .shell__prompt i { 37 | font-size: inherit; 38 | } 39 | 40 | .shell__prompt { 41 | width: 1.2rem; 42 | } 43 | 44 | .shell__prompt i { 45 | color: var(--blue); 46 | } 47 | 48 | .shell__text { 49 | margin: 0; 50 | font-family: inherit; 51 | white-space: pre-wrap; 52 | width: 100%; 53 | } 54 | -------------------------------------------------------------------------------- /src/css/_variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --blue: #2196f3; 3 | --dark-blue: #1E88E5; 4 | --red: #F44336; 5 | --dark-red: #D32F2F; 6 | --moon-grey: #f2f2f2; 7 | } 8 | -------------------------------------------------------------------------------- /src/css/base.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Roboto', sans-serif; 3 | padding-top: 4em; 4 | background-color: #fafafa; 5 | color: #333333; 6 | } 7 | 8 | * { 9 | box-sizing: border-box; 10 | } 11 | 12 | *, 13 | *:hover, 14 | *:active, 15 | *:focus { 16 | outline: 0 17 | } 18 | 19 | a { 20 | text-decoration: none; 21 | } 22 | 23 | img { 24 | max-width: 100%; 25 | } 26 | 27 | audio, 28 | video { 29 | width: 100%; 30 | } 31 | 32 | .mobile-only { 33 | display: none !important; 34 | } 35 | 36 | .container { 37 | width: 95%; 38 | max-width: 960px; 39 | margin: 1em auto 0; 40 | } 41 | 42 | i.spin { 43 | animation: 1s spin linear infinite; 44 | } 45 | 46 | #app { 47 | transition: .2s ease padding; 48 | } 49 | 50 | #app.multiple { 51 | padding-bottom: 4em; 52 | } 53 | 54 | nav { 55 | width: 16em; 56 | position: fixed; 57 | top: 4em; 58 | left: 0; 59 | } 60 | 61 | nav .action { 62 | width: 100%; 63 | display: block; 64 | border-radius: 0; 65 | font-size: 1.1em; 66 | padding: .5em; 67 | white-space: nowrap; 68 | overflow: hidden; 69 | text-overflow: ellipsis; 70 | } 71 | 72 | nav>div { 73 | border-top: 1px solid rgba(0, 0, 0, 0.05); 74 | } 75 | 76 | nav .action>* { 77 | vertical-align: middle; 78 | } 79 | 80 | main { 81 | min-height: 1em; 82 | margin: 0 1em 1em auto; 83 | width: calc(100% - 19em); 84 | } 85 | 86 | #breadcrumbs { 87 | height: 3em; 88 | border-bottom: 1px solid rgba(0, 0, 0, 0.05); 89 | } 90 | 91 | #breadcrumbs span, 92 | #breadcrumbs { 93 | display: flex; 94 | align-items: center; 95 | color: #6f6f6f; 96 | } 97 | 98 | #breadcrumbs a { 99 | color: inherit; 100 | transition: .1s ease-in; 101 | border-radius: .125em; 102 | } 103 | 104 | #breadcrumbs a:hover { 105 | background-color: rgba(0,0,0, 0.05); 106 | } 107 | 108 | #breadcrumbs span a { 109 | padding: .2em; 110 | } 111 | 112 | #progress { 113 | position: fixed; 114 | top: 0; 115 | left: 0; 116 | width: 100%; 117 | height: 3px; 118 | z-index: 9999999999; 119 | } 120 | 121 | #progress div { 122 | height: 100%; 123 | background-color: #40c4ff; 124 | width: 0; 125 | transition: .2s ease width; 126 | } 127 | -------------------------------------------------------------------------------- /src/css/dashboard.css: -------------------------------------------------------------------------------- 1 | .dashboard { 2 | max-width: 600px; 3 | margin: 1em 0; 4 | } 5 | 6 | a { 7 | color: inherit 8 | } 9 | 10 | .dashboard p label { 11 | margin-bottom: .2em; 12 | display: block; 13 | font-size: .8em; 14 | font-weight: 500; 15 | color: rgba(0, 0, 0, 0.57); 16 | } 17 | 18 | li code, 19 | p code { 20 | background: rgba(0, 0, 0, 0.05); 21 | padding: .1em; 22 | border-radius: .2em; 23 | } 24 | 25 | .small { 26 | font-size: .8em; 27 | line-height: 1.5; 28 | } 29 | 30 | .dashboard #nav { 31 | list-style: none; 32 | display: flex; 33 | color: rgb(84, 110, 122); 34 | font-weight: 500; 35 | margin: 0 0 1em; 36 | font-size: .8em; 37 | text-align: center; 38 | justify-content: space-between; 39 | padding: 0; 40 | } 41 | 42 | .dashboard #nav li { 43 | width: 100%; 44 | padding: 0 0 1em; 45 | border-bottom: 2px solid rgba(0, 0, 0, 0.05); 46 | } 47 | 48 | .dashboard #nav li.active { 49 | border-color: var(--blue) 50 | } 51 | 52 | .dashboard #nav i { 53 | font-size: 1em; 54 | vertical-align: middle; 55 | } 56 | 57 | table { 58 | border-collapse: collapse; 59 | width: 100%; 60 | } 61 | 62 | table tr { 63 | border-bottom: 1px solid #ccc; 64 | } 65 | 66 | table tr:last-child { 67 | border: 0; 68 | } 69 | 70 | table th { 71 | font-weight: 500; 72 | color: #757575; 73 | text-align: left; 74 | } 75 | 76 | table th, 77 | table td { 78 | padding: .5em 0; 79 | } 80 | 81 | table td.small { 82 | width: 1em; 83 | } 84 | 85 | table tr>*:first-child { 86 | padding-left: 1em; 87 | } 88 | 89 | table tr>*:last-child { 90 | padding-right: 1em; 91 | } 92 | 93 | .card { 94 | position: relative; 95 | margin: .5rem 0 1rem 0; 96 | background-color: #fff; 97 | border-radius: 2px; 98 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); 99 | } 100 | 101 | .card.floating { 102 | position: fixed; 103 | top: 50%; 104 | left: 50%; 105 | transform: translate(-50%, -50%); 106 | z-index: 99999; 107 | max-width: 25em; 108 | width: 90%; 109 | max-height: 95%; 110 | z-index: 99999; 111 | animation: .1s show forwards; 112 | } 113 | 114 | .card>*>*:first-child { 115 | margin-top: 0; 116 | } 117 | 118 | .card>*>*:last-child { 119 | margin-bottom: 0; 120 | } 121 | 122 | .card .card-title { 123 | padding: 1.5em 1em 1em; 124 | display: flex; 125 | } 126 | 127 | .card .card-title>*:first-child { 128 | margin-right: auto; 129 | } 130 | 131 | .card>div { 132 | padding: 1em 1em; 133 | } 134 | 135 | .card>div:first-child { 136 | padding-top: 1.5em; 137 | } 138 | 139 | .card>div:last-child { 140 | padding-bottom: 1.5em; 141 | } 142 | 143 | .card .card-title * { 144 | margin: 0; 145 | } 146 | 147 | .card .card-action { 148 | text-align: right; 149 | } 150 | 151 | .card .card-content.full { 152 | padding-bottom: 0; 153 | } 154 | 155 | .card h2 { 156 | font-weight: 500; 157 | } 158 | 159 | .card h3 { 160 | color: rgba(0, 0, 0, 0.53); 161 | font-size: 1em; 162 | font-weight: 500; 163 | margin: 2em 0 1em; 164 | } 165 | 166 | .card-content table { 167 | margin: 0 -1em; 168 | width: calc(100% + 2em); 169 | } 170 | 171 | .card code { 172 | word-wrap: break-word; 173 | } 174 | 175 | .card#download { 176 | max-width: 15em; 177 | } 178 | 179 | .card#share ul { 180 | list-style: none; 181 | padding: 0; 182 | margin: 0; 183 | } 184 | 185 | .card#share ul li { 186 | display: flex; 187 | justify-content: space-between; 188 | align-items: center; 189 | } 190 | 191 | .card#share ul li a { 192 | color: var(--blue); 193 | cursor: pointer; 194 | margin-right: auto; 195 | } 196 | 197 | .card#share ul li .action i { 198 | font-size: 1em; 199 | } 200 | 201 | .card#share ul li input, 202 | .card#share ul li select { 203 | padding: .2em; 204 | margin-right: .5em; 205 | border: 1px solid #dadada; 206 | } 207 | 208 | .card#share .action.copy-clipboard::after { 209 | content: 'Copied!'; 210 | position: absolute; 211 | left: -25%; 212 | width: 150%; 213 | font-size: .6em; 214 | text-align: center; 215 | background: #44a6f5; 216 | color: #fff; 217 | padding: .5em .2em; 218 | border-radius: .4em; 219 | top: -2em; 220 | transition: .1s ease opacity; 221 | opacity: 0; 222 | } 223 | 224 | .card#share .action.copy-clipboard.active::after { 225 | opacity: 1; 226 | } 227 | 228 | .overlay { 229 | background-color: rgba(0, 0, 0, 0.5); 230 | position: fixed; 231 | top: 0; 232 | left: 0; 233 | height: 100%; 234 | width: 100%; 235 | z-index: 9999; 236 | animation: .1s show forwards; 237 | } 238 | 239 | 240 | /* * * * * * * * * * * * * * * * 241 | * PROMPT - MOVE * 242 | * * * * * * * * * * * * * * * */ 243 | 244 | .file-list { 245 | max-height: 50vh; 246 | overflow: auto; 247 | list-style: none; 248 | margin: 0; 249 | padding: 0; 250 | width: 100%; 251 | } 252 | 253 | .file-list li { 254 | width: 100%; 255 | user-select: none; 256 | border-radius: .2em; 257 | padding: .3em; 258 | } 259 | 260 | .file-list li[aria-selected=true] { 261 | background: var(--blue) !important; 262 | color: #fff !important; 263 | transition: .1s ease all; 264 | } 265 | 266 | .file-list li:hover { 267 | background-color: #e9eaeb; 268 | cursor: pointer; 269 | } 270 | 271 | .file-list li:before { 272 | content: "folder"; 273 | color: #6f6f6f; 274 | vertical-align: middle; 275 | line-height: 1.4; 276 | font-family: 'Material Icons'; 277 | font-size: 1.75em; 278 | margin-right: .25em; 279 | } 280 | 281 | .file-list li[aria-selected=true]:before { 282 | color: white; 283 | } 284 | 285 | .help { 286 | max-width: 24em; 287 | } 288 | 289 | .help ul { 290 | padding: 0; 291 | margin: 1em 0; 292 | list-style: none; 293 | } 294 | 295 | @keyframes show { 296 | 0% { 297 | display: none; 298 | opacity: 0; 299 | } 300 | 1% { 301 | display: block; 302 | opacity: 0; 303 | } 304 | 100% { 305 | display: block; 306 | opacity: 1; 307 | } 308 | } 309 | 310 | .collapsible { 311 | border-top: 1px solid rgba(0,0,0,0.1); 312 | } 313 | 314 | .collapsible:last-of-type { 315 | border-bottom: 1px solid rgba(0,0,0,0.1); 316 | } 317 | 318 | .collapsible > input { 319 | display: none; 320 | } 321 | 322 | .collapsible > label { 323 | padding: 1em 0; 324 | cursor: pointer; 325 | border-right: 0; 326 | border-left: 0; 327 | display: flex; 328 | justify-content: space-between; 329 | } 330 | 331 | .collapsible > label * { 332 | margin: 0; 333 | color: rgba(0,0,0,0.57); 334 | } 335 | 336 | .collapsible > label i { 337 | transition: .2s ease transform; 338 | user-select: none; 339 | } 340 | 341 | .collapsible .collapse { 342 | max-height: 0; 343 | overflow: hidden; 344 | transition: .2s ease all; 345 | } 346 | 347 | .collapsible > input:checked ~ .collapse { 348 | padding-top: 1em; 349 | padding-bottom: 1em; 350 | max-height: 20em; 351 | } 352 | 353 | .collapsible > input:checked ~ label i { 354 | transform: rotate(180deg) 355 | } 356 | 357 | .card .collapsible { 358 | width: calc(100% + 2em); 359 | margin: 0 -1em; 360 | } 361 | 362 | .card .collapsible > label { 363 | padding: 1em; 364 | } 365 | 366 | .card .collapsible .collapse { 367 | padding: 0 1em; 368 | } 369 | -------------------------------------------------------------------------------- /src/css/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Roboto'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-cyrillic-ext.woff2) format('woff2'); 6 | unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F; 7 | } 8 | 9 | @font-face { 10 | font-family: 'Roboto'; 11 | font-style: normal; 12 | font-weight: 400; 13 | src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-cyrillic.woff2) format('woff2'); 14 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 15 | } 16 | 17 | @font-face { 18 | font-family: 'Roboto'; 19 | font-style: normal; 20 | font-weight: 400; 21 | src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-greek-ext.woff2) format('woff2'); 22 | unicode-range: U+1F00-1FFF; 23 | } 24 | 25 | @font-face { 26 | font-family: 'Roboto'; 27 | font-style: normal; 28 | font-weight: 400; 29 | src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-greek.woff2) format('woff2'); 30 | unicode-range: U+0370-03FF; 31 | } 32 | 33 | @font-face { 34 | font-family: 'Roboto'; 35 | font-style: normal; 36 | font-weight: 400; 37 | src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-vietnamese.woff2) format('woff2'); 38 | unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB; 39 | } 40 | 41 | @font-face { 42 | font-family: 'Roboto'; 43 | font-style: normal; 44 | font-weight: 400; 45 | src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-latin-ext.woff2) format('woff2'); 46 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; 47 | } 48 | 49 | @font-face { 50 | font-family: 'Roboto'; 51 | font-style: normal; 52 | font-weight: 400; 53 | src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-latin.woff2) format('woff2'); 54 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; 55 | } 56 | 57 | @font-face { 58 | font-family: 'Roboto'; 59 | font-style: normal; 60 | font-weight: 500; 61 | src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-cyrillic-ext.woff2) format('woff2'); 62 | unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F; 63 | } 64 | 65 | @font-face { 66 | font-family: 'Roboto'; 67 | font-style: normal; 68 | font-weight: 500; 69 | src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-cyrillic.woff2) format('woff2'); 70 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 71 | } 72 | 73 | @font-face { 74 | font-family: 'Roboto'; 75 | font-style: normal; 76 | font-weight: 500; 77 | src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-greek-ext.woff2) format('woff2'); 78 | unicode-range: U+1F00-1FFF; 79 | } 80 | 81 | @font-face { 82 | font-family: 'Roboto'; 83 | font-style: normal; 84 | font-weight: 500; 85 | src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-greek.woff2) format('woff2'); 86 | unicode-range: U+0370-03FF; 87 | } 88 | 89 | @font-face { 90 | font-family: 'Roboto'; 91 | font-style: normal; 92 | font-weight: 500; 93 | src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-vietnamese.woff2) format('woff2'); 94 | unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB; 95 | } 96 | 97 | @font-face { 98 | font-family: 'Roboto'; 99 | font-style: normal; 100 | font-weight: 500; 101 | src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-latin-ext.woff2) format('woff2'); 102 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; 103 | } 104 | 105 | @font-face { 106 | font-family: 'Roboto'; 107 | font-style: normal; 108 | font-weight: 500; 109 | src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-latin.woff2) format('woff2'); 110 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; 111 | } 112 | 113 | @import "~material-design-icons/iconfont/material-icons.css"; 114 | -------------------------------------------------------------------------------- /src/css/header.css: -------------------------------------------------------------------------------- 1 | header { 2 | z-index: 1000; 3 | background-color: #fff; 4 | border-bottom: 1px solid rgba(0, 0, 0, 0.075); 5 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); 6 | position: fixed; 7 | top: 0; 8 | left: 0; 9 | width: 100%; 10 | padding: 0; 11 | display: flex; 12 | } 13 | 14 | header .overlay { 15 | width: 0; 16 | height: 0; 17 | } 18 | 19 | header a, 20 | header a:hover { 21 | color: inherit; 22 | } 23 | 24 | header>div:first-child>.action, 25 | header img { 26 | margin-right: 1em; 27 | } 28 | 29 | header img { 30 | height: 2.5em; 31 | } 32 | 33 | header>div:first-child>.action { 34 | display: none; 35 | } 36 | 37 | header>div { 38 | display: flex; 39 | width: 100%; 40 | padding: 0.5em 0.5em 0.5em 1em; 41 | align-items: center; 42 | } 43 | 44 | header .action span { 45 | display: none; 46 | } 47 | 48 | header>div div { 49 | vertical-align: middle; 50 | position: relative; 51 | } 52 | 53 | header>div:last-child div { 54 | display: flex; 55 | } 56 | 57 | header>div:first-child { 58 | height: 4em; 59 | } 60 | 61 | header>div:last-child { 62 | justify-content: flex-end; 63 | } 64 | 65 | header .search-button { 66 | display: none; 67 | } 68 | 69 | #more { 70 | display: none; 71 | } 72 | 73 | #search { 74 | position: relative; 75 | height: 100%; 76 | width: 100%; 77 | max-width: 25em; 78 | } 79 | 80 | #search.active { 81 | position: fixed; 82 | top: 0; 83 | right: 0; 84 | width: 100%; 85 | max-width: 100%; 86 | height: 100%; 87 | z-index: 9999; 88 | } 89 | 90 | #search #input { 91 | background-color: #f5f5f5; 92 | display: flex; 93 | padding: 0.75em; 94 | border-radius: 0.3em; 95 | transition: .1s ease all; 96 | align-items: center; 97 | z-index: 2; 98 | } 99 | 100 | #search.active #input { 101 | border-bottom: 1px solid rgba(0, 0, 0, 0.075); 102 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); 103 | background-color: #fff; 104 | height: 4em; 105 | } 106 | 107 | #search.active>div { 108 | border-radius: 0 !important; 109 | } 110 | 111 | #search.active i, 112 | #search.active input { 113 | color: #212121; 114 | } 115 | 116 | #search #input>.action, 117 | #search #input>i { 118 | margin-right: 0.3em; 119 | user-select: none; 120 | } 121 | 122 | #search input { 123 | width: 100%; 124 | border: 0; 125 | background-color: transparent; 126 | padding: 0; 127 | } 128 | 129 | #search #result { 130 | visibility: visible; 131 | max-height: none; 132 | background-color: #f8f8f8; 133 | text-align: left; 134 | padding: 0; 135 | color: rgba(0, 0, 0, 0.6); 136 | height: 0; 137 | transition: .1s ease height, .1s ease padding; 138 | overflow-x: hidden; 139 | overflow-y: auto; 140 | z-index: 1; 141 | } 142 | 143 | #search #result>div>*:first-child { 144 | margin-top: 0; 145 | } 146 | 147 | #search.active #result { 148 | padding: .5em; 149 | height: calc(100% - 4em); 150 | } 151 | 152 | #search ul { 153 | padding: 0; 154 | margin: 0; 155 | list-style: none; 156 | } 157 | 158 | #search li { 159 | margin-bottom: .5em; 160 | } 161 | 162 | #search #result>div { 163 | max-width: 45em; 164 | margin: 0 auto; 165 | } 166 | 167 | #search #result #renew { 168 | width: 100%; 169 | text-align: center; 170 | display: none; 171 | margin: 0; 172 | max-width: none; 173 | } 174 | 175 | #search.ongoing #result #renew { 176 | display: block; 177 | } 178 | 179 | #search.active #result i { 180 | color: #ccc; 181 | } 182 | 183 | #search.active #result>p>i { 184 | text-align: center; 185 | margin: 0 auto; 186 | display: table; 187 | } 188 | 189 | #search.active #result ul li a { 190 | display: flex; 191 | align-items: center; 192 | padding: .3em 0; 193 | } 194 | 195 | #search.active #result ul li a i { 196 | margin-right: .3em; 197 | } 198 | 199 | #search::-webkit-input-placeholder { 200 | color: rgba(255, 255, 255, .5); 201 | } 202 | 203 | #search:-moz-placeholder { 204 | opacity: 1; 205 | color: rgba(255, 255, 255, .5); 206 | } 207 | 208 | #search::-moz-placeholder { 209 | opacity: 1; 210 | color: rgba(255, 255, 255, .5); 211 | } 212 | 213 | #search:-ms-input-placeholder { 214 | color: rgba(255, 255, 255, .5); 215 | } 216 | 217 | #search .boxes { 218 | border: 1px solid rgba(0, 0, 0, 0.075); 219 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); 220 | background: #fff; 221 | margin: 1em 0; 222 | } 223 | 224 | #search .boxes h3 { 225 | margin: 0; 226 | font-weight: 500; 227 | font-size: 1em; 228 | color: #212121; 229 | padding: .5em; 230 | } 231 | 232 | #search .boxes>div { 233 | display: flex; 234 | flex-wrap: wrap; 235 | justify-content: space-between; 236 | margin-right: -1em; 237 | margin-bottom: -1em; 238 | } 239 | 240 | #search .boxes>div>div { 241 | background: var(--blue); 242 | color: #fff; 243 | text-align: center; 244 | width: 10em; 245 | padding: 1em; 246 | cursor: pointer; 247 | margin-bottom: 1em; 248 | margin-right: 1em; 249 | flex-grow: 1; 250 | } 251 | 252 | #search .boxes p { 253 | margin: 1em 0 0; 254 | } 255 | 256 | #search .boxes i { 257 | color: #fff !important; 258 | font-size: 3.5em; 259 | } 260 | -------------------------------------------------------------------------------- /src/css/listing.css: -------------------------------------------------------------------------------- 1 | #listing h2 { 2 | margin: 0 0 0 0.5em; 3 | font-size: .9em; 4 | color: rgba(0, 0, 0, 0.38); 5 | font-weight: 500; 6 | } 7 | 8 | #listing .item div:last-of-type * { 9 | text-overflow: ellipsis; 10 | overflow: hidden; 11 | } 12 | 13 | #listing>div { 14 | display: flex; 15 | padding: 0; 16 | flex-wrap: wrap; 17 | justify-content: flex-start; 18 | position: relative; 19 | } 20 | 21 | #listing .item { 22 | background-color: #fff; 23 | position: relative; 24 | display: flex; 25 | flex-wrap: nowrap; 26 | color: #6f6f6f; 27 | transition: .1s ease background, .1s ease opacity; 28 | align-items: center; 29 | cursor: pointer; 30 | } 31 | 32 | #listing .item div:last-of-type { 33 | text-overflow: ellipsis; 34 | white-space: nowrap; 35 | overflow: hidden; 36 | } 37 | 38 | #listing .item p { 39 | margin: 0; 40 | } 41 | 42 | #listing .item .size, 43 | #listing .item .modified { 44 | font-size: 0.9em; 45 | } 46 | 47 | #listing .item .name { 48 | font-weight: bold; 49 | } 50 | 51 | #listing .item i { 52 | font-size: 4em; 53 | margin-right: 0.1em; 54 | vertical-align: bottom; 55 | } 56 | 57 | .message { 58 | text-align: center; 59 | font-size: 2em; 60 | margin: 1em auto; 61 | display: block !important; 62 | width: 95%; 63 | color: rgba(0, 0, 0, 0.3); 64 | font-weight: 500; 65 | } 66 | 67 | .message i { 68 | font-size: 2.5em; 69 | margin-bottom: .2em; 70 | display: block; 71 | } 72 | 73 | #listing.mosaic { 74 | padding-top: 1em; 75 | margin: 0 -0.5em; 76 | } 77 | 78 | #listing.mosaic .item { 79 | width: calc(33% - 1em); 80 | margin: .5em; 81 | padding: 0.5em; 82 | border-radius: 0.2em; 83 | box-shadow: 0 1px 3px rgba(0, 0, 0, .06), 0 1px 2px rgba(0, 0, 0, .12); 84 | } 85 | 86 | #listing.mosaic .item:hover { 87 | box-shadow: 0 1px 3px rgba(0, 0, 0, .12), 0 1px 2px rgba(0, 0, 0, .24) !important; 88 | } 89 | 90 | #listing.mosaic .header { 91 | display: none; 92 | } 93 | 94 | #listing.mosaic .item div:first-of-type { 95 | width: 5em; 96 | } 97 | 98 | #listing.mosaic .item div:last-of-type { 99 | width: calc(100% - 5vw); 100 | } 101 | 102 | #listing.list { 103 | flex-direction: column; 104 | width: 100%; 105 | max-width: 100%; 106 | margin: 0; 107 | } 108 | 109 | #listing.list .item { 110 | width: 100%; 111 | margin: 0; 112 | border: 1px solid rgba(0, 0, 0, 0.1); 113 | padding: 1em; 114 | border-top: 0; 115 | } 116 | 117 | #listing.list h2 { 118 | display: none; 119 | } 120 | 121 | #listing .item[aria-selected=true] { 122 | background: var(--blue) !important; 123 | color: #fff !important; 124 | } 125 | 126 | #listing.list .item div:first-of-type { 127 | width: 3em; 128 | } 129 | 130 | #listing.list .item div:first-of-type i { 131 | font-size: 2em; 132 | } 133 | 134 | #listing.list .item div:last-of-type { 135 | width: calc(100% - 3em); 136 | display: flex; 137 | align-items: center; 138 | } 139 | 140 | #listing.list .item .name { 141 | width: 50%; 142 | } 143 | 144 | #listing.list .item .size { 145 | width: 25%; 146 | } 147 | 148 | #listing .item.header { 149 | display: none !important; 150 | background-color: #ccc; 151 | } 152 | 153 | #listing.list .header i { 154 | font-size: 1.5em; 155 | vertical-align: middle; 156 | margin-left: .2em; 157 | } 158 | 159 | #listing.list .item.header { 160 | display: flex !important; 161 | background: #fafafa; 162 | z-index: 999; 163 | padding: .85em; 164 | border: 0; 165 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 166 | } 167 | 168 | #listing.list .item.header>div:first-child { 169 | width: 0; 170 | } 171 | 172 | #listing.list .item.header .name { 173 | margin-right: 3em; 174 | } 175 | 176 | #listing.list .header a { 177 | color: inherit; 178 | } 179 | 180 | #listing.list .item.header>div:first-child { 181 | width: 0; 182 | } 183 | 184 | #listing.list .name { 185 | font-weight: normal; 186 | } 187 | 188 | #listing.list .item.header .name { 189 | margin-right: 3em; 190 | } 191 | 192 | #listing.list .header span { 193 | vertical-align: middle; 194 | } 195 | 196 | #listing.list .header i { 197 | opacity: 0; 198 | transition: .1s ease all; 199 | } 200 | 201 | #listing.list .header p:hover i, 202 | #listing.list .header .active i { 203 | opacity: 1; 204 | } 205 | 206 | #listing.list .item.header .active { 207 | font-weight: bold; 208 | } 209 | 210 | @keyframes slidein { 211 | from { 212 | bottom: -4em; 213 | } 214 | 215 | to { 216 | bottom: 0; 217 | } 218 | } 219 | 220 | #listing #multiple-selection { 221 | position: fixed; 222 | bottom: -4em; 223 | left: 0; 224 | z-index: 99999; 225 | width: 100%; 226 | background-color: var(--blue); 227 | height: 4em; 228 | display: none; 229 | padding: 0.5em 0.5em 0.5em 1em; 230 | justify-content: space-between; 231 | align-items: center; 232 | transition: .2s ease bottom; 233 | } 234 | 235 | #listing #multiple-selection.active { 236 | animation: slidein 0.2s forwards; 237 | display: flex; 238 | } 239 | 240 | #listing #multiple-selection p, 241 | #listing #multiple-selection i { 242 | color: #fff; 243 | } 244 | -------------------------------------------------------------------------------- /src/css/login.css: -------------------------------------------------------------------------------- 1 | #login { 2 | background: #fff; 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | width: 100%; 7 | height: 100%; 8 | } 9 | 10 | #login img { 11 | width: 4em; 12 | height: 4em; 13 | margin: 0 auto; 14 | display: block; 15 | } 16 | 17 | #login h1 { 18 | text-align: center; 19 | font-size: 2.5em; 20 | margin: .4em 0 .67em; 21 | } 22 | 23 | #login form { 24 | position: fixed; 25 | top: 50%; 26 | left: 50%; 27 | transform: translate(-50%, -50%); 28 | max-width: 16em; 29 | width: 90%; 30 | } 31 | 32 | #login.recaptcha form { 33 | min-width: 304px; 34 | } 35 | 36 | #login #recaptcha { 37 | margin: .5em 0 0; 38 | } 39 | 40 | #login .wrong { 41 | background: var(--red); 42 | color: #fff; 43 | padding: .5em; 44 | text-align: center; 45 | animation: .2s opac forwards; 46 | } 47 | 48 | @keyframes opac { 49 | 0% { 50 | opacity: 0; 51 | } 52 | 100% { 53 | opacity: 1; 54 | } 55 | } 56 | 57 | #login p { 58 | cursor: pointer; 59 | text-align: right; 60 | color: var(--blue); 61 | text-transform: lowercase; 62 | font-weight: 500; 63 | font-size: 0.9rem; 64 | margin: .5rem 0; 65 | } 66 | -------------------------------------------------------------------------------- /src/css/mobile.css: -------------------------------------------------------------------------------- 1 | @media (max-width: 1024px) { 2 | nav { 3 | width: 10em 4 | } 5 | } 6 | 7 | @media (max-width: 1024px) { 8 | main { 9 | width: calc(100% - 13em) 10 | } 11 | } 12 | 13 | @media (max-width: 736px) { 14 | body { 15 | padding-bottom: 5em; 16 | } 17 | #listing.list .item .size { 18 | display: none; 19 | } 20 | #listing.list .item .name { 21 | width: 60%; 22 | } 23 | #more { 24 | display: inherit 25 | } 26 | header .overlay { 27 | width: 100%; 28 | height: 100%; 29 | background-color: rgba(0, 0, 0, 0.1); 30 | } 31 | #dropdown { 32 | position: fixed; 33 | top: 1em; 34 | right: 1em; 35 | display: block; 36 | background-color: #fff; 37 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); 38 | transform: scale(0); 39 | transition: .1s ease-in-out transform; 40 | transform-origin: top right; 41 | z-index: 99999; 42 | } 43 | #dropdown > div { 44 | display: block; 45 | } 46 | #dropdown.active { 47 | transform: scale(1); 48 | } 49 | #dropdown .action { 50 | display: flex; 51 | align-items: center; 52 | border-radius: 0; 53 | width: 100%; 54 | } 55 | #dropdown .action span:not(.counter) { 56 | display: inline-block; 57 | padding: .4em; 58 | } 59 | #dropdown .counter { 60 | left: 2.25em; 61 | } 62 | #file-selection { 63 | position: fixed; 64 | bottom: 1em; 65 | left: 50%; 66 | transform: translateX(-50%); 67 | display: flex; 68 | align-items: center; 69 | background: #fff; 70 | box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px; 71 | width: 95%; 72 | max-width: 20em; 73 | } 74 | #file-selection .action { 75 | border-radius: 50%; 76 | width: auto; 77 | } 78 | #file-selection > span { 79 | display: inline-block; 80 | margin-left: 1em; 81 | color: #6f6f6f; 82 | margin-right: auto; 83 | } 84 | nav { 85 | top: 0; 86 | z-index: 99999; 87 | background: #fff; 88 | height: 100%; 89 | width: 16em; 90 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); 91 | transition: .1s ease left; 92 | left: -17em; 93 | } 94 | nav.active { 95 | left: 0; 96 | } 97 | header .search-button, 98 | header>div:first-child>.action { 99 | display: inherit; 100 | } 101 | header img { 102 | display: none; 103 | } 104 | #listing { 105 | margin-bottom: 5em; 106 | } 107 | main { 108 | margin: 0 1em; 109 | width: calc(100% - 2em); 110 | } 111 | #search { 112 | display: none; 113 | } 114 | #search.active { 115 | display: block; 116 | } 117 | } 118 | 119 | @media (max-width: 450px) { 120 | #listing.list .item .modified { 121 | display: none; 122 | } 123 | #listing.list .item .name { 124 | width: 100%; 125 | } 126 | } -------------------------------------------------------------------------------- /src/css/styles.css: -------------------------------------------------------------------------------- 1 | @import "~normalize.css/normalize.css"; 2 | @import "~noty/lib/noty.css"; 3 | @import "~noty/lib/themes/mint.css"; 4 | @import "./_variables.css"; 5 | @import "./_buttons.css"; 6 | @import "./_inputs.css"; 7 | @import "./_shell.css"; 8 | @import "./_share.css"; 9 | @import "./fonts.css"; 10 | @import "./base.css"; 11 | @import "./header.css"; 12 | @import "./listing.css"; 13 | @import "./dashboard.css"; 14 | @import "./login.css"; 15 | 16 | .link { 17 | color: var(--blue); 18 | } 19 | 20 | /* * * * * * * * * * * * * * * * 21 | * ACTION * 22 | * * * * * * * * * * * * * * * */ 23 | 24 | .action { 25 | display: inline-block; 26 | cursor: pointer; 27 | transition: 0.2s ease all; 28 | border: 0; 29 | margin: 0; 30 | color: #546E7A; 31 | border-radius: 50%; 32 | background: transparent; 33 | padding: 0; 34 | box-shadow: none; 35 | vertical-align: middle; 36 | text-align: left; 37 | position: relative; 38 | } 39 | 40 | .action.disabled { 41 | opacity: 0.2; 42 | cursor: not-allowed; 43 | } 44 | 45 | .action i { 46 | padding: 0.4em; 47 | transition: .1s ease-in-out all; 48 | border-radius: 50%; 49 | } 50 | 51 | .action:hover { 52 | background-color: rgba(0, 0, 0, .1); 53 | } 54 | 55 | .action ul { 56 | position: absolute; 57 | top: 0; 58 | color: #7d7d7d; 59 | list-style: none; 60 | margin: 0; 61 | padding: 0; 62 | flex-direction: column; 63 | display: flex; 64 | } 65 | 66 | .action ul li { 67 | line-height: 1; 68 | padding: .7em; 69 | transition: .1s ease background-color; 70 | } 71 | 72 | .action ul li:hover { 73 | background-color: rgba(0, 0, 0, 0.04); 74 | } 75 | 76 | #click-overlay { 77 | display: none; 78 | position: fixed; 79 | cursor: pointer; 80 | top: 0; 81 | left: 0; 82 | height: 100%; 83 | width: 100%; 84 | } 85 | 86 | #click-overlay.active { 87 | display: block; 88 | } 89 | 90 | .action .counter { 91 | display: block; 92 | position: absolute; 93 | bottom: 0; 94 | right: 0; 95 | background: var(--blue); 96 | color: #fff; 97 | border-radius: 50%; 98 | font-size: .75em; 99 | width: 1.5em; 100 | height: 1.5em; 101 | text-align: center; 102 | line-height: 1.25em; 103 | border: 2px solid white; 104 | } 105 | 106 | 107 | /* PREVIEWER */ 108 | 109 | #previewer { 110 | background-color: rgba(0, 0, 0, 0.9); 111 | position: fixed; 112 | top: 0; 113 | left: 0; 114 | width: 100%; 115 | height: 100%; 116 | z-index: 9999; 117 | overflow: hidden; 118 | } 119 | 120 | #previewer .bar { 121 | width: 100%; 122 | text-align: right; 123 | display: flex; 124 | padding: 0.5em; 125 | height: 3.7em; 126 | } 127 | 128 | #previewer .action:first-of-type { 129 | margin-right: auto; 130 | } 131 | 132 | #previewer .action i { 133 | color: #fff; 134 | } 135 | 136 | #previewer .action:hover { 137 | background-color: rgba(255, 255, 255, 0.3) 138 | } 139 | 140 | #previewer .action span { 141 | display: none; 142 | } 143 | 144 | #previewer .preview { 145 | margin: 2em auto 4em; 146 | max-width: 80%; 147 | text-align: center; 148 | height: calc(100vh - 9.7em); 149 | } 150 | 151 | #previewer .preview pre { 152 | text-align: left; 153 | overflow: auto; 154 | } 155 | 156 | #previewer .preview pre, 157 | #previewer .preview video, 158 | #previewer .preview img { 159 | max-height: 100%; 160 | margin: 0; 161 | } 162 | 163 | #previewer .pdf { 164 | width: 100%; 165 | height: 100%; 166 | } 167 | 168 | #previewer h2.message { 169 | color: rgba(255, 255, 255, 0.5) 170 | } 171 | 172 | #previewer>button { 173 | margin: 0; 174 | position: fixed; 175 | top: 50%; 176 | transform: translateY(-50%); 177 | } 178 | 179 | #previewer>button:first-of-type { 180 | left: 0.5em; 181 | } 182 | 183 | #previewer>button:last-of-type { 184 | right: 0.5em; 185 | } 186 | 187 | 188 | /* * * * * * * * * * * * * * * * 189 | * PROMPT * 190 | * * * * * * * * * * * * * * * */ 191 | 192 | .noty_buttons { 193 | text-align: right; 194 | padding: 0 10px 10px !important; 195 | } 196 | 197 | .noty_buttons button { 198 | background: rgba(0, 0, 0, 0.05); 199 | border: 1px solid rgba(0,0,0,0.1); 200 | box-shadow: 0 0 0 0; 201 | font-size: 14px; 202 | } 203 | 204 | /* * * * * * * * * * * * * * * * 205 | * FOOTER * 206 | * * * * * * * * * * * * * * * */ 207 | 208 | .credits { 209 | font-size: 0.6em; 210 | margin: 3em 2.5em; 211 | color: #a5a5a5; 212 | } 213 | 214 | .credits > span { 215 | display: block; 216 | margin: .3em 0; 217 | } 218 | 219 | .credits a, 220 | .credits a:hover { 221 | color: inherit; 222 | cursor: pointer; 223 | } 224 | 225 | 226 | /* * * * * * * * * * * * * * * * 227 | * ANIMATIONS * 228 | * * * * * * * * * * * * * * * */ 229 | 230 | @keyframes spin { 231 | 100% { 232 | -webkit-transform: rotate(-360deg); 233 | transform: rotate(-360deg); 234 | } 235 | } 236 | 237 | /* * * * * * * * * * * * * * * * 238 | * SETTINGS RULES * 239 | * * * * * * * * * * * * * * * */ 240 | 241 | .rules > div { 242 | display: flex; 243 | align-items: center; 244 | margin: .5rem 0; 245 | } 246 | 247 | .rules input[type="checkbox"] { 248 | margin-right: .2rem; 249 | } 250 | 251 | .rules input[type="text"] { 252 | border: 1px solid#ddd; 253 | padding: .2rem; 254 | } 255 | 256 | .rules label { 257 | margin-right: .5rem; 258 | } 259 | 260 | .rules button { 261 | margin-left: auto; 262 | } 263 | 264 | .rules button.delete { 265 | padding: .2rem .5rem; 266 | margin-left: .5rem; 267 | } 268 | 269 | @import './mobile.css'; 270 | -------------------------------------------------------------------------------- /src/i18n/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | 4 | import ar from './ar.json' 5 | import de from './de.json' 6 | import en from './en.json' 7 | import es from './es.json' 8 | import fr from './fr.json' 9 | import it from './it.json' 10 | import ja from './ja.json' 11 | import pl from './pl.json' 12 | import ko from './ko.json' 13 | import pt from './pt.json' 14 | import ptBR from './pt-br.json' 15 | import ru from './ru.json' 16 | import zhCN from './zh-cn.json' 17 | import zhTW from './zh-tw.json' 18 | 19 | Vue.use(VueI18n) 20 | 21 | export function detectLocale () { 22 | let locale = (navigator.language || navigator.browserLangugae).toLowerCase() 23 | switch (true) { 24 | case /^en.*/i.test(locale): 25 | locale = 'en' 26 | break 27 | case /^it.*/i.test(locale): 28 | locale = 'it' 29 | break 30 | case /^fr.*/i.test(locale): 31 | locale = 'fr' 32 | break 33 | case /^pt.*/i.test(locale): 34 | locale = 'pt' 35 | break 36 | case /^pt-BR.*/i.test(locale): 37 | locale = 'pt-br' 38 | break 39 | case /^ja.*/i.test(locale): 40 | locale = 'ja' 41 | break 42 | case /^zh-CN/i.test(locale): 43 | locale = 'zh-cn' 44 | break 45 | case /^zh-TW/i.test(locale): 46 | locale = 'zh-tw' 47 | break 48 | case /^zh.*/i.test(locale): 49 | locale = 'zh-cn' 50 | break 51 | case /^es.*/i.test(locale): 52 | locale = 'es' 53 | break 54 | case /^de.*/i.test(locale): 55 | locale = 'de' 56 | break 57 | case /^ru.*/i.test(locale): 58 | locale = 'ru' 59 | break 60 | case /^pl.*/i.test(locale): 61 | locale = 'pl' 62 | break 63 | case /^ko.*/i.test(locale): 64 | locale = 'ko' 65 | break 66 | default: 67 | locale = 'en' 68 | } 69 | 70 | return locale 71 | } 72 | 73 | const i18n = new VueI18n({ 74 | locale: detectLocale(), 75 | fallbackLocale: 'en', 76 | messages: { 77 | 'ar': ar, 78 | 'de': de, 79 | 'en': en, 80 | 'es': es, 81 | 'fr': fr, 82 | 'it': it, 83 | 'ja': ja, 84 | 'ko': ko, 85 | 'pl': pl, 86 | 'pt-br': ptBR, 87 | 'pt': pt, 88 | 'ru': ru, 89 | 'zh-cn': zhCN, 90 | 'zh-tw': zhTW 91 | } 92 | }) 93 | 94 | export default i18n 95 | -------------------------------------------------------------------------------- /src/i18n/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "permanent": "영구", 3 | "buttons": { 4 | "shell": "쉘 전환", 5 | "cancel": "취소", 6 | "close": "닫기", 7 | "copy": "복사", 8 | "copyFile": "파일 복사", 9 | "copyToClipboard": "클립보드 복사", 10 | "create": "생성", 11 | "delete": "삭제", 12 | "download": "다운로드", 13 | "info": "정보", 14 | "more": "더보기", 15 | "move": "이동", 16 | "moveFile": "파일 이동", 17 | "new": "신규", 18 | "next": "다음", 19 | "ok": "확인", 20 | "replace": "대체", 21 | "previous": "이전", 22 | "rename": "이름 바꾸기", 23 | "reportIssue": "이슈 보내기", 24 | "save": "저장", 25 | "search": "검색", 26 | "select": "선택", 27 | "share": "공유", 28 | "publish": "게시", 29 | "selectMultiple": "다중 선택", 30 | "schedule": "일정", 31 | "switchView": "보기 전환", 32 | "toggleSidebar": "사이드바 전환", 33 | "update": "업데이트", 34 | "upload": "업로드", 35 | "permalink": "링크 얻기" 36 | }, 37 | "success": { 38 | "linkCopied": "링크가 복사되었습니다!" 39 | }, 40 | "errors": { 41 | "forbidden": "접근 권한이 없습니다.", 42 | "internal": "오류가 발생하였습니다.", 43 | "notFound": "해당 경로를 찾을 수 없습니다." 44 | }, 45 | "files": { 46 | "folders": "폴더", 47 | "files": "파일", 48 | "body": "본문", 49 | "clear": "지우기", 50 | "closePreview": "미리보기 닫기", 51 | "home": "홈", 52 | "lastModified": "최종 수정", 53 | "loading": "로딩중...", 54 | "lonely": "폴더가 비어 있습니다...", 55 | "metadata": "메타데이터", 56 | "multipleSelectionEnabled": "다중 선택 켜짐", 57 | "name": "이름", 58 | "size": "크기", 59 | "sortByName": "이름순", 60 | "sortBySize": "크기순", 61 | "sortByLastModified": "수정시간순 정렬" 62 | }, 63 | "help": { 64 | "click": "파일이나 디렉토리를 선택해주세요.", 65 | "ctrl": { 66 | "click": "여러 개의 파일이나 디렉토리를 선택해주세요.", 67 | "f": "검색창 열기", 68 | "s": "파일 또는 디렉토리 다운로드" 69 | }, 70 | "del": "선택된 파일 삭제", 71 | "doubleClick": "파일 또는 디렉토리 열기", 72 | "esc": "선택 취소/프롬프트 닫기", 73 | "f1": "정보", 74 | "f2": "파일 이름 변경", 75 | "help": "도움말" 76 | }, 77 | "login": { 78 | "password": "비밀번호", 79 | "passwordConfirm": "비밀번호 확인", 80 | "submit": "로그인", 81 | "createAnAccount": "계정 생성", 82 | "loginInstead": "이미 계정이 있습니다", 83 | "passwordsDontMatch": "비밀번호가 일치하지 않습니다", 84 | "usernameTaken": "사용자 이름이 존재합니다", 85 | "signup": "가입하기", 86 | "username": "사용자 이름", 87 | "wrongCredentials": "사용자 이름 또는 비밀번호를 확인하십시오" 88 | }, 89 | "prompts": { 90 | "copy": "복사", 91 | "copyMessage": "복사할 디렉토리:", 92 | "currentlyNavigating": "현재 위치:", 93 | "deleteMessageMultiple": "{count} 개의 파일을 삭제하시겠습니까?", 94 | "deleteMessageSingle": "파일 혹은 디렉토리를 삭제하시겠습니까?", 95 | "deleteTitle": "파일 삭제", 96 | "displayName": "게시 이름:", 97 | "download": "파일 다운로드", 98 | "downloadMessage": "다운로드 포맷 설정.", 99 | "error": "에러 발생!", 100 | "fileInfo": "파일 정보", 101 | "filesSelected": "{count} 개의 파일이 선택되었습니다.", 102 | "lastModified": "최종 수정", 103 | "move": "이동", 104 | "moveMessage": "이동할 화일 또는 디렉토리를 선택하세요:", 105 | "newDir": "새 디렉토리", 106 | "newDirMessage": "새 디렉토리 이름을 입력해주세요.", 107 | "newFile": "새 파일", 108 | "newFileMessage": "새 파일 이름을 입력해주세요.", 109 | "numberDirs": "디렉토리 수", 110 | "numberFiles": "파일 수", 111 | "replace": "대체하기", 112 | "replaceMessage": "동일한 파일 이름이 존재합니다. 현재 파일을 덮어쓸까요?\n", 113 | "rename": "이름 변경", 114 | "renameMessage": "새로운 이름을 입력하세요.", 115 | "show": "보기", 116 | "size": "크기", 117 | "schedule": "일정", 118 | "scheduleMessage": "이 글을 공개할 시간을 알려주세요.", 119 | "newArchetype": "원형을 유지하는 포스트를 생성합니다. 파일은 컨텐트 폴더에 생성됩니다." 120 | }, 121 | "settings": { 122 | "instanceName": "인스턴스 이름", 123 | "brandingDirectoryPath": "브랜드 디렉토리 경로", 124 | "documentation": "문서", 125 | "branding": "브랜딩", 126 | "disableExternalLinks": "외부 링크 감추기", 127 | "brandingHelp": "File Browser 인스턴스는 이름, 로고, 스타일 등을 변경할 수 있습니다. 자세한 사항은 여기{0}에서 확인하세요.", 128 | "admin": "관리자", 129 | "administrator": "관리자", 130 | "allowCommands": "명령 실행", 131 | "allowEdit": "파일/디렉토리의 수정/변경/삭제 허용", 132 | "allowNew": "파일/디렉토리 생성 허용", 133 | "allowPublish": "새 포스트/페이지 생성 허용", 134 | "avoidChanges": "(수정하지 않으면 비워두세요)", 135 | "changePassword": "비밀번호 변경", 136 | "commandRunner": "명령 실행기", 137 | "commandRunnerHelp": "이벤트에 해당하는 명령을 설정하세요. 줄당 1개의 명령을 적으세요. 환경 변수{0} 와 {1}이 사용가능하며, {0} 은 {1}에 상대 경로 입니다. 자세한 사항은 {2} 를 참조하세요.", 138 | "commandsUpdated": "명령 수정됨!", 139 | "customStylesheet": "커스텀 스타일시트", 140 | "examples": "예", 141 | "globalSettings": "전역 설정", 142 | "language": "언어", 143 | "lockPassword": "사용자에 의한 비밀번호 변경을 허용하지 않음", 144 | "newPassword": "새로운 비밀번호", 145 | "newPasswordConfirm": "새로운 비밀번호 확인", 146 | "newUser": "새로운 사용자", 147 | "password": "비밀번호", 148 | "passwordUpdated": "비밀번호 수정 완료!", 149 | "permissions": "권한", 150 | "permissionsHelp": "사용자를 관리자로 만들거나 권한을 부여할 수 있습니다. 관리자를 선택하면, 모든 옵션이 자동으로 선택됩니다. 사용자 관리는 현재 관리자만 할 수 있습니다.\n", 151 | "profileSettings": "프로필 설정", 152 | "ruleExample1": "점(.)으로 시작하는 모든 파일의 접근을 방지합니다.(예 .git, .gitignore)\n", 153 | "ruleExample2": "Caddyfile파일의 접근을 방지합니다.", 154 | "rules": "룰", 155 | "rulesHelp": "사용자별로 규칙을 허용/방지를 지정할 수 있습니다. 방지된 파일은 보이지 않고 사용자들은 접근할 수 없습니다. 사용자의 접근 허용 범위와 관련해 정규표현식(regex)과 경로를 지원합니다.\n", 156 | "scope": "범위", 157 | "settingsUpdated": "설정 수정됨!", 158 | "user": "사용자", 159 | "userCommands": "명령어", 160 | "userCommandsHelp": "사용에게 허용할 명령어를 공백으로 구분하여 입력하세요. 예:\n", 161 | "userCreated": "사용자 생성됨!", 162 | "userDeleted": "사용자 삭제됨!", 163 | "userManagement": "사용자 관리", 164 | "username": "사용자 이름", 165 | "users": "사용자", 166 | "globalRules": "규칙에 대한 전역설정으로 모든 사용자에게 적용됩니다. 지정된 규칙은 사용자 설정을 덮어쓰기 합니다.", 167 | "allowSignup": "사용자 가입 허용", 168 | "createUserDir": "Auto create user home dir while adding new user", 169 | "insertRegex": "정규식 입력", 170 | "insertPath": "경로 입력", 171 | "userUpdated": "사용자 수정됨!", 172 | "userDefaults": "사용자 기본 설정", 173 | "defaultUserDescription": "아래 사항은 신규 사용자들에 대한 기본 설정입니다.", 174 | "executeOnShell": "쉘에서 실행", 175 | "executeOnShellDescription": "기본적으로 File Browser 는 바이너리를 명령어로 호출하여 실행합니다. 쉘을 통해 실행하기를 원한다면, Bash 또는 PowerShell 에 필요한 인수와 플래그를 설정하세요. 사용자 명령어와 이벤트 훅에 모두 적용됩니다.", 176 | "perm": { 177 | "create": "파일이나 디렉토리 생성하기", 178 | "delete": "화일이나 디렉토리 삭제하기", 179 | "download": "다운로드", 180 | "modify": "파일 편집", 181 | "execute": "명령 실행", 182 | "rename": "파일 이름 변경 또는 디렉토리 이동", 183 | "share": "파일 공유하기" 184 | } 185 | }, 186 | "sidebar": { 187 | "help": "도움말", 188 | "login": "로그인", 189 | "signup": "가입하기", 190 | "logout": "로그아웃", 191 | "myFiles": "내 파일", 192 | "newFile": "새로운 파일", 193 | "newFolder": "새로운 폴더", 194 | "settings": "설정", 195 | "siteSettings": "사이트 설정", 196 | "hugoNew": "Hugo New", 197 | "preview": "미리보기" 198 | }, 199 | "search": { 200 | "images": "이미지", 201 | "music": "음악", 202 | "pdf": "PDF", 203 | "types": "Types", 204 | "video": "비디오", 205 | "search": "검색...", 206 | "typeToSearch": "검색어 입력...", 207 | "pressToSearch": "검색하려면 엔터를 입력하세요" 208 | }, 209 | "languages": { 210 | "ar": "العربية", 211 | "en": "English", 212 | "it": "Italiano", 213 | "fr": "Français", 214 | "pt": "Português", 215 | "ptBR": "Português (Brasil)", 216 | "ja": "日本語", 217 | "zhCN": "中文 (简体)", 218 | "zhTW": "中文 (繁體)", 219 | "es": "Español", 220 | "de": "Deutsch", 221 | "ru": "Русский", 222 | "pl": "Polski", 223 | "ko": "한국어" 224 | }, 225 | "time": { 226 | "unit": "Time Unit", 227 | "seconds": "초", 228 | "minutes": "분", 229 | "hours": "시", 230 | "days": "일" 231 | }, 232 | "download": { 233 | "downloadFile": "파일 다운로드", 234 | "downloadFolder": "폴더 다운로드" 235 | } 236 | } -------------------------------------------------------------------------------- /src/i18n/zh-cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "permanent": "永久", 3 | "buttons": { 4 | "shell": "激活 shell", 5 | "cancel": "取消", 6 | "close": "关闭", 7 | "copy": "复制", 8 | "copyFile": "复制文件", 9 | "copyToClipboard": "复制到剪贴板", 10 | "create": "创建", 11 | "delete": "删除", 12 | "download": "下载", 13 | "info": "信息", 14 | "more": "更多", 15 | "move": "移动", 16 | "moveFile": "移动文件", 17 | "new": "新", 18 | "next": "下一个", 19 | "ok": "确定", 20 | "replace": "替换", 21 | "previous": "上一个", 22 | "rename": "重命名", 23 | "reportIssue": "报告问题", 24 | "save": "保存", 25 | "search": "搜索", 26 | "select": "选择", 27 | "share": "分享", 28 | "publish": "发布", 29 | "selectMultiple": "选择多个", 30 | "schedule": "计划", 31 | "switchView": "切换显示方式", 32 | "toggleSidebar": "切换侧边栏", 33 | "update": "更新", 34 | "upload": "上传", 35 | "permalink": "获取永久链接" 36 | }, 37 | "success": { 38 | "linkCopied": "链接已复制!" 39 | }, 40 | "errors": { 41 | "forbidden": "你无权限访问", 42 | "internal": "服务器出了点问题。", 43 | "notFound": "找不到文件。" 44 | }, 45 | "files": { 46 | "folders": "文件夹", 47 | "files": "文件", 48 | "body": "内容", 49 | "clear": "清空", 50 | "closePreview": "关闭预览", 51 | "home": "主页", 52 | "lastModified": "最后修改", 53 | "loading": "加载中...", 54 | "lonely": "这里没有任何文件...", 55 | "metadata": "元数据", 56 | "multipleSelectionEnabled": "多选模式已开启", 57 | "name": "名称", 58 | "size": "大小", 59 | "sortByName": "按名称排序", 60 | "sortBySize": "按大小排序", 61 | "sortByLastModified": "按最后修改时间排序" 62 | }, 63 | "help": { 64 | "click": "选择文件或目录", 65 | "ctrl": { 66 | "click": "选择多个文件或目录", 67 | "f": "打开搜索框", 68 | "s": "保存文件或下载当前文件夹" 69 | }, 70 | "del": "删除所选的文件/文件夹", 71 | "doubleClick": "打开文件/文件夹", 72 | "esc": "清除已选项或关闭提示信息", 73 | "f1": "显示该帮助信息", 74 | "f2": "重命名文件/文件夹", 75 | "help": "帮助" 76 | }, 77 | "login": { 78 | "password": "密码", 79 | "passwordConfirm": "确认密码", 80 | "submit": "登录", 81 | "createAnAccount": "创建用户", 82 | "loginInstead": "已有用户登录", 83 | "passwordsDontMatch": "密码不一致", 84 | "usernameTaken": "用户名已经被使用", 85 | "signup": "注册", 86 | "username": "用户名", 87 | "wrongCredentials": "用户名或密码错误" 88 | }, 89 | "prompts": { 90 | "copy": "复制", 91 | "copyMessage": "请选择欲复制至的目录:", 92 | "currentlyNavigating": "当前目录:", 93 | "deleteMessageMultiple": "你确定要删除这 {count} 个文件吗?", 94 | "deleteMessageSingle": "你确定要删除这个文件/文件夹吗?", 95 | "deleteTitle": "删除文件", 96 | "displayName": "名称:", 97 | "download": "下载文件", 98 | "downloadMessage": "请选择要下载的压缩格式。", 99 | "error": "出了一点问题...", 100 | "fileInfo": "文件信息", 101 | "filesSelected": "已选择 {count} 个文件。", 102 | "lastModified": "最后修改", 103 | "move": "移动", 104 | "moveMessage": "请选择欲移动至的目录:", 105 | "newDir": "新建目录", 106 | "newDirMessage": "请输入新目录的名称。", 107 | "newFile": "新建文件", 108 | "newFileMessage": "请输入新文件的名称。", 109 | "numberDirs": "目录数", 110 | "numberFiles": "文件数", 111 | "replace": "替换", 112 | "replaceMessage": "您尝试上传的文件中有一个与现有文件的名称存在冲突。是否替换现有的同名文件?", 113 | "rename": "重命名", 114 | "renameMessage": "请输入新名称,旧名称为:", 115 | "show": "揭示", 116 | "size": "大小", 117 | "schedule": "计划", 118 | "scheduleMessage": "请选择发布这篇帖子的日期。", 119 | "newArchetype": "创建一个基于原型的新帖子。您的文件将会创建在内容文件夹中。" 120 | }, 121 | "settings": { 122 | "instanceName": "Instance name", 123 | "brandingDirectoryPath": "品牌信息文件夹路径", 124 | "documentation": "帮助文档", 125 | "branding": "品牌", 126 | "disableExternalLinks": "禁止外部链接(帮助文档除外)", 127 | "brandingHelp": "您可以通过改变名称,更换商标,加入自定义样式,甚至禁用外部链接来自定义File Browser的外观和给人的感觉。\n想获得更多信息,请查看 {0} 。", 128 | "admin": "管理员", 129 | "administrator": "管理员", 130 | "allowCommands": "执行命令(Linux 代码)", 131 | "allowEdit": "编辑、重命名或删除文件/目录", 132 | "allowNew": "创建新文件和目录", 133 | "allowPublish": "发布新的帖子与页面", 134 | "avoidChanges": "(留空以避免更改)", 135 | "changePassword": "更改密码", 136 | "commandRunner": "命令执行器", 137 | "commandRunnerHelp": "Here you can set commands that are executed in the named events. You must write one per line. The environment variables {0} and {1} will be available, being {0} relative to {1}. For more information about this feature and the available environment variables, please read the {2}.", 138 | "commandsUpdated": "命令已更新!", 139 | "customStylesheet": "自定义样式表", 140 | "examples": "例子", 141 | "globalSettings": "全局设置", 142 | "language": "语言", 143 | "lockPassword": "禁止用户修改密码", 144 | "newPassword": "您的新密码", 145 | "newPasswordConfirm": "重输一遍新密码", 146 | "newUser": "新建用户", 147 | "password": "密码", 148 | "passwordUpdated": "密码已更新!", 149 | "permissions": "权限", 150 | "permissionsHelp": "您可以将该用户设置为管理员,也可以单独选择各项权限。如果选择了“管理员”,则其他的选项会被自动勾上,同时该用户可以管理其他用户。", 151 | "profileSettings": "个人设置", 152 | "ruleExample1": "阻止用户访问所有文件夹下任何以 . 开头的文件(隐藏文件, 例如: .git, .gitignore)。", 153 | "ruleExample2": "阻止用户访问其目录范围的根目录下名为 Caddyfile 的文件。", 154 | "rules": "规则", 155 | "rulesHelp": "您可以为该用户制定一组黑名单或白名单式的规则,被屏蔽的文件将不会显示在列表中,用户也无权限访问,支持相对于目录范围的路径。", 156 | "scope": "目录范围", 157 | "settingsUpdated": "设置已更新!", 158 | "user": "用户", 159 | "userCommands": "用户命令(Linux 代码)", 160 | "userCommandsHelp": "指定该用户可以执行的命令(Linux 代码),用空格分隔。例如:", 161 | "userCreated": "用户已创建!", 162 | "userDeleted": "用户已删除!", 163 | "userManagement": "用户管理", 164 | "username": "用户名", 165 | "users": "用户", 166 | "globalRules": "这是全局允许与禁止规则。它们作用于所有用户。您可以给每个用户定义单独的特殊规则来覆盖全局规则。", 167 | "allowSignup": "允许用户注册", 168 | "createUserDir": "在添加新用户的同时自动创建用户的个人目录", 169 | "insertRegex": "插入正则表达式", 170 | "insertPath": "Insert the path", 171 | "userUpdated": "用户已更新!", 172 | "userDefaults": "用户默认设置", 173 | "defaultUserDescription": "这些是新用户的默认设置", 174 | "executeOnShell": "在Shell中执行", 175 | "executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you want to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This apply to both user commands and event hooks.", 176 | "perm": { 177 | "create": "创建文件和文件夹", 178 | "delete": "删除文件和文件夹", 179 | "download": "下载", 180 | "modify": "编辑", 181 | "execute": "执行命令", 182 | "rename": "重命名或移动文件和文件夹", 183 | "share": "分享文件" 184 | } 185 | }, 186 | "sidebar": { 187 | "help": "帮助", 188 | "login": "登录", 189 | "signup": "注册", 190 | "logout": "登出", 191 | "myFiles": "我的文件", 192 | "newFile": "新建文件", 193 | "newFolder": "新建文件夹", 194 | "settings": "设置", 195 | "siteSettings": "网站设置", 196 | "hugoNew": "Hugo New", 197 | "preview": "预览" 198 | }, 199 | "search": { 200 | "images": "图像", 201 | "music": "音乐", 202 | "pdf": "PDF", 203 | "types": "类型", 204 | "video": "视频", 205 | "search": "搜索...", 206 | "typeToSearch": "输入搜索...", 207 | "pressToSearch": "回车搜索..." 208 | }, 209 | "languages": { 210 | "ar": "العربية", 211 | "en": "English", 212 | "it": "Italiano", 213 | "fr": "Français", 214 | "pt": "Português", 215 | "ptBR": "Português (Brasil)", 216 | "ja": "日本語", 217 | "zhCN": "中文 (简体)", 218 | "zhTW": "中文 (繁體)", 219 | "es": "Español", 220 | "de": "Deutsch", 221 | "ru": "Русский", 222 | "pl": "Polski", 223 | "ko": "한국어" 224 | }, 225 | "time": { 226 | "unit": "时间单位", 227 | "seconds": "秒", 228 | "minutes": "分钟", 229 | "hours": "小时", 230 | "days": "天" 231 | }, 232 | "download": { 233 | "downloadFile": "下载文件", 234 | "downloadFolder": "下载文件夹" 235 | } 236 | } -------------------------------------------------------------------------------- /src/i18n/zh-tw.json: -------------------------------------------------------------------------------- 1 | { 2 | "permanent": "永久", 3 | "buttons": { 4 | "shell": "切換 shell", 5 | "cancel": "取消", 6 | "close": "關閉", 7 | "copy": "複製", 8 | "copyFile": "複製檔案", 9 | "copyToClipboard": "複製到剪貼簿", 10 | "create": "建立", 11 | "delete": "刪除", 12 | "download": "下載", 13 | "info": "資訊", 14 | "more": "更多", 15 | "move": "移動", 16 | "moveFile": "移動檔案", 17 | "new": "新", 18 | "next": "下一個", 19 | "ok": "確認", 20 | "replace": "更換", 21 | "previous": "上一個", 22 | "rename": "重新命名", 23 | "reportIssue": "報告問題", 24 | "save": "儲存", 25 | "search": "搜尋", 26 | "select": "選擇", 27 | "share": "分享", 28 | "publish": "發佈", 29 | "selectMultiple": "選擇多個", 30 | "schedule": "計畫", 31 | "switchView": "切換顯示方式", 32 | "toggleSidebar": "切換側邊欄", 33 | "update": "更新", 34 | "upload": "上傳", 35 | "permalink": "獲取永久連結" 36 | }, 37 | "success": { 38 | "linkCopied": "連結已複製!" 39 | }, 40 | "errors": { 41 | "forbidden": "您無權訪問。", 42 | "internal": "伺服器出了點問題。", 43 | "notFound": "找不到檔案。" 44 | }, 45 | "files": { 46 | "folders": "資料夾", 47 | "files": "檔案", 48 | "body": "内容", 49 | "clear": "清空", 50 | "closePreview": "關閉預覽", 51 | "home": "主頁", 52 | "lastModified": "最後修改", 53 | "loading": "讀取中...", 54 | "lonely": "這裡沒有任何檔案...", 55 | "metadata": "詮釋資料", 56 | "multipleSelectionEnabled": "多選模式已開啟", 57 | "name": "名稱", 58 | "size": "大小", 59 | "sortByName": "按名稱排序", 60 | "sortBySize": "按大小排序", 61 | "sortByLastModified": "按最後修改時間排序" 62 | }, 63 | "help": { 64 | "click": "選擇檔案或目錄", 65 | "ctrl": { 66 | "click": "選擇多個檔案或目錄", 67 | "f": "打開搜尋列", 68 | "s": "儲存檔案或下載目前資料夾" 69 | }, 70 | "del": "刪除所選的檔案/資料夾", 71 | "doubleClick": "打開檔案/資料夾", 72 | "esc": "清除已選項或關閉提示資訊", 73 | "f1": "顯示該幫助資訊", 74 | "f2": "重新命名檔案/資料夾", 75 | "help": "幫助" 76 | }, 77 | "login": { 78 | "password": "密碼", 79 | "passwordConfirm": "確認密碼", 80 | "submit": "登入", 81 | "createAnAccount": "新建賬戶", 82 | "loginInstead": "已有賬戶登錄", 83 | "passwordsDontMatch": "密碼不匹配", 84 | "usernameTaken": "用戶名已存在", 85 | "signup": "註冊", 86 | "username": "帳號", 87 | "wrongCredentials": "帳號或密碼錯誤" 88 | }, 89 | "prompts": { 90 | "copy": "複製", 91 | "copyMessage": "請選擇欲複製至的目錄:", 92 | "currentlyNavigating": "目前目錄:", 93 | "deleteMessageMultiple": "你確定要刪除這 {count} 個檔案嗎?", 94 | "deleteMessageSingle": "你確定要刪除這個檔案/資料夾嗎?", 95 | "deleteTitle": "刪除檔案", 96 | "displayName": "名稱:", 97 | "download": "下載檔案", 98 | "downloadMessage": "請選擇要下載的壓縮格式。", 99 | "error": "發出了一點錯誤...", 100 | "fileInfo": "檔案資訊", 101 | "filesSelected": "已選擇 {count} 個檔案。", 102 | "lastModified": "最後修改", 103 | "move": "移動", 104 | "moveMessage": "請選擇欲移動至的目錄:", 105 | "newDir": "建立目錄", 106 | "newDirMessage": "請輸入新目錄的名稱。", 107 | "newFile": "建立檔案", 108 | "newFileMessage": "請輸入新檔案的名稱。", 109 | "numberDirs": "目錄數", 110 | "numberFiles": "檔案數", 111 | "replace": "替換", 112 | "replaceMessage": "您嘗試上傳的檔案中有一個與現有檔案的名稱存在衝突。是否取代現有的同名檔案?", 113 | "rename": "重新命名", 114 | "renameMessage": "請輸入新名稱,舊名稱為:", 115 | "show": "顯示", 116 | "size": "大小", 117 | "schedule": "計畫", 118 | "scheduleMessage": "請選擇發佈這篇貼文的日期。", 119 | "newArchetype": "建立一個基於原型的新貼文。您的檔案將會建立在內容資料夾中。" 120 | }, 121 | "settings": { 122 | "instanceName": "Instance name", 123 | "brandingDirectoryPath": "Branding directory path", 124 | "documentation": "documentation", 125 | "branding": "Branding", 126 | "disableExternalLinks": "Disable external links (except documentation)", 127 | "brandingHelp": "You can costumize how your File Browser instance looks and feels by changing its name, replacing the logo, adding custom styles and even disable external links to GitHub.\nFor more information about custom branding, please check out the {0}.", 128 | "admin": "管理員", 129 | "administrator": "管理員", 130 | "allowCommands": "執行命令", 131 | "allowEdit": "編輯、重命名或刪除檔案/目錄", 132 | "allowNew": "創建新檔案和目錄", 133 | "allowPublish": "發佈新的貼文與頁面", 134 | "avoidChanges": "(留空以避免更改)", 135 | "changePassword": "更改密碼", 136 | "commandRunner": "Command runner", 137 | "commandRunnerHelp": "Here you can set commands that are executed in the named events. You must write one per line. The environment variables {0} and {1} will be available, being {0} relative to {1}. For more information about this feature and the available environment variables, please read the {2}.", 138 | "commandsUpdated": "命令已更新!", 139 | "customStylesheet": "自定義樣式表", 140 | "examples": "範例", 141 | "globalSettings": "全域設定", 142 | "language": "語言", 143 | "lockPassword": "禁止使用者修改密碼", 144 | "newPassword": "您的新密碼", 145 | "newPasswordConfirm": "重輸一遍新密碼", 146 | "newUser": "建立使用者", 147 | "password": "密碼", 148 | "passwordUpdated": "密碼已更新!", 149 | "permissions": "權限", 150 | "permissionsHelp": "您可以將該使用者設置為管理員,也可以單獨選擇各項權限。如果選擇了“管理員”,則其他的選項會被自動勾上,同時該使用者可以管理其他使用者。", 151 | "profileSettings": "個人設定", 152 | "ruleExample1": "封鎖使用者存取所有資料夾下任何以 . 開頭的檔案(隱藏文件, 例如: .git, .gitignore)。", 153 | "ruleExample2": "封鎖使用者存取其目錄範圍的根目錄下名為 Caddyfile 的檔案。", 154 | "rules": "規則", 155 | "rulesHelp": "您可以為該使用者製定一組黑名單或白名單式的規則,被屏蔽的檔案將不會顯示在清單中,使用者也無權限存取,支持相對於目錄範圍的路徑。", 156 | "scope": "目錄範圍", 157 | "settingsUpdated": "設定已更新!", 158 | "user": "使用者", 159 | "userCommands": "使用者命令", 160 | "userCommandsHelp": "指定該使用者可以執行的命令,用空格分隔。例如:", 161 | "userCreated": "使用者已建立!", 162 | "userDeleted": "使用者已刪除!", 163 | "userManagement": "使用者管理", 164 | "username": "使用者名稱", 165 | "users": "使用者", 166 | "globalRules": "This is a global set of allow and disallow rules. They apply to every user. You can define specific rules on each user's settings to override this ones.", 167 | "allowSignup": "Allow users to signup", 168 | "createUserDir": "Auto create user home dir while adding new user", 169 | "insertRegex": "Insert regex expression", 170 | "insertPath": "Insert the path", 171 | "userUpdated": "使用者已更新!", 172 | "userDefaults": "User default settings", 173 | "defaultUserDescription": "This are the default settings for new users.", 174 | "executeOnShell": "Execute on shell", 175 | "executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you want to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This apply to both user commands and event hooks.", 176 | "perm": { 177 | "create": "建立檔案和資料夾", 178 | "delete": "刪除檔案和資料夾", 179 | "download": "下載", 180 | "modify": "編輯檔案", 181 | "execute": "Execute commands", 182 | "rename": "重命名或移動檔案/資料夾", 183 | "share": "分享檔案" 184 | } 185 | }, 186 | "sidebar": { 187 | "help": "幫助", 188 | "login": "登入", 189 | "signup": "註冊", 190 | "logout": "登出", 191 | "myFiles": "我的檔案", 192 | "newFile": "建立檔案", 193 | "newFolder": "建立資料夾", 194 | "settings": "設定", 195 | "siteSettings": "網站設定", 196 | "hugoNew": "Hugo New", 197 | "preview": "預覽" 198 | }, 199 | "search": { 200 | "images": "影像", 201 | "music": "音樂", 202 | "pdf": "PDF", 203 | "types": "類型", 204 | "video": "影片", 205 | "search": "搜尋...", 206 | "typeToSearch": "Type to search...", 207 | "pressToSearch": "Press enter to search..." 208 | }, 209 | "languages": { 210 | "ar": "العربية", 211 | "en": "English", 212 | "it": "Italiano", 213 | "fr": "Français", 214 | "pt": "Português", 215 | "ptBR": "Português (Brasil)", 216 | "ja": "日本語", 217 | "zhCN": "中文 (简体)", 218 | "zhTW": "中文 (繁體)", 219 | "es": "Español", 220 | "de": "Deutsch", 221 | "ru": "Русский", 222 | "pl": "Polski", 223 | "ko": "한국어" 224 | }, 225 | "time": { 226 | "unit": "時間單位", 227 | "seconds": "秒", 228 | "minutes": "分鐘", 229 | "hours": "小時", 230 | "days": "天" 231 | }, 232 | "download": { 233 | "downloadFile": "下載檔案", 234 | "downloadFolder": "下載資料夾" 235 | } 236 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { sync } from 'vuex-router-sync' 2 | import store from '@/store' 3 | import router from '@/router' 4 | import i18n from '@/i18n' 5 | import Vue from '@/utils/vue' 6 | import { recaptcha, loginPage } from '@/utils/constants' 7 | import { login, validateLogin } from '@/utils/auth' 8 | import App from '@/App' 9 | 10 | sync(store, router) 11 | 12 | async function start () { 13 | if (loginPage) { 14 | await validateLogin() 15 | } else { 16 | await login('', '', '') 17 | } 18 | 19 | if (recaptcha) { 20 | await new Promise (resolve => { 21 | const check = () => { 22 | if (typeof window.grecaptcha === 'undefined') { 23 | setTimeout(check, 100) 24 | } else { 25 | resolve() 26 | } 27 | } 28 | 29 | check() 30 | }) 31 | } 32 | 33 | new Vue({ 34 | el: '#app', 35 | store, 36 | router, 37 | i18n, 38 | template: '', 39 | components: { App } 40 | }) 41 | } 42 | 43 | start() 44 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Login from '@/views/Login' 4 | import Layout from '@/views/Layout' 5 | import Files from '@/views/Files' 6 | import Share from '@/views/Share' 7 | import Users from '@/views/settings/Users' 8 | import User from '@/views/settings/User' 9 | import Settings from '@/views/Settings' 10 | import GlobalSettings from '@/views/settings/Global' 11 | import ProfileSettings from '@/views/settings/Profile' 12 | import Error403 from '@/views/errors/403' 13 | import Error404 from '@/views/errors/404' 14 | import Error500 from '@/views/errors/500' 15 | import store from '@/store' 16 | import { baseURL } from '@/utils/constants' 17 | 18 | Vue.use(Router) 19 | 20 | const router = new Router({ 21 | base: baseURL, 22 | mode: 'history', 23 | routes: [ 24 | { 25 | path: '/login', 26 | name: 'Login', 27 | component: Login, 28 | beforeEnter: (to, from, next) => { 29 | if (store.getters.isLogged) { 30 | return next({ path: '/files' }) 31 | } 32 | 33 | document.title = 'Login' 34 | next() 35 | } 36 | }, 37 | { 38 | path: '/*', 39 | component: Layout, 40 | children: [ 41 | { 42 | path: '/share/*', 43 | name: 'Share', 44 | component: Share 45 | }, 46 | { 47 | path: '/files/*', 48 | name: 'Files', 49 | component: Files, 50 | meta: { 51 | requiresAuth: true 52 | } 53 | }, 54 | { 55 | path: '/settings', 56 | name: 'Settings', 57 | component: Settings, 58 | redirect: { 59 | path: '/settings/profile' 60 | }, 61 | meta: { 62 | requiresAuth: true 63 | }, 64 | children: [ 65 | { 66 | path: '/settings/profile', 67 | name: 'Profile Settings', 68 | component: ProfileSettings 69 | }, 70 | { 71 | path: '/settings/global', 72 | name: 'Global Settings', 73 | component: GlobalSettings, 74 | meta: { 75 | requiresAdmin: true 76 | } 77 | }, 78 | { 79 | path: '/settings/users', 80 | name: 'Users', 81 | component: Users, 82 | meta: { 83 | requiresAdmin: true 84 | } 85 | }, 86 | { 87 | path: '/settings/users/*', 88 | name: 'User', 89 | component: User, 90 | meta: { 91 | requiresAdmin: true 92 | } 93 | } 94 | ] 95 | }, 96 | { 97 | path: '/403', 98 | name: 'Forbidden', 99 | component: Error403 100 | }, 101 | { 102 | path: '/404', 103 | name: 'Not Found', 104 | component: Error404 105 | }, 106 | { 107 | path: '/500', 108 | name: 'Internal Server Error', 109 | component: Error500 110 | }, 111 | { 112 | path: '/files', 113 | redirect: { 114 | path: '/files/' 115 | } 116 | }, 117 | { 118 | path: '/*', 119 | redirect: to => `/files${to.path}` 120 | } 121 | ] 122 | } 123 | ] 124 | }) 125 | 126 | router.beforeEach((to, from, next) => { 127 | document.title = to.name 128 | 129 | if (to.matched.some(record => record.meta.requiresAuth)) { 130 | if (!store.getters.isLogged) { 131 | next({ 132 | path: '/login', 133 | query: { redirect: to.fullPath } 134 | }) 135 | 136 | return 137 | } 138 | 139 | if (to.matched.some(record => record.meta.requiresAdmin)) { 140 | if (!store.state.user.perm.admin) { 141 | next({ path: '/403' }) 142 | return 143 | } 144 | } 145 | } 146 | 147 | next() 148 | }) 149 | 150 | export default router 151 | -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | isLogged: state => state.user !== null, 3 | isFiles: state => !state.loading && state.route.name === 'Files', 4 | isListing: (state, getters) => getters.isFiles && state.req.isDir, 5 | isEditor: (state, getters) => getters.isFiles && (state.req.type === 'text' || state.req.type === 'textImmutable'), 6 | selectedCount: state => state.selected.length 7 | } 8 | 9 | export default getters 10 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import mutations from './mutations' 4 | import getters from './getters' 5 | 6 | Vue.use(Vuex) 7 | 8 | const state = { 9 | user: null, 10 | req: {}, 11 | oldReq: {}, 12 | clipboard: { 13 | key: '', 14 | items: [] 15 | }, 16 | jwt: '', 17 | progress: 0, 18 | loading: false, 19 | reload: false, 20 | selected: [], 21 | multiple: false, 22 | show: null, 23 | showShell: false, 24 | showMessage: null, 25 | showConfirm: null 26 | } 27 | 28 | export default new Vuex.Store({ 29 | strict: true, 30 | state, 31 | getters, 32 | mutations 33 | }) 34 | -------------------------------------------------------------------------------- /src/store/mutations.js: -------------------------------------------------------------------------------- 1 | import * as i18n from '@/i18n' 2 | import moment from 'moment' 3 | 4 | const mutations = { 5 | closeHovers: state => { 6 | state.show = null 7 | state.showMessage = null 8 | }, 9 | toggleShell: (state) => { 10 | state.showShell = !state.showShell 11 | }, 12 | showHover: (state, value) => { 13 | if (typeof value !== 'object') { 14 | state.show = value 15 | return 16 | } 17 | 18 | state.show = value.prompt 19 | state.showMessage = value.message 20 | state.showConfirm = value.confirm 21 | }, 22 | showError: (state, value) => { 23 | state.show = 'error' 24 | state.showMessage = value 25 | }, 26 | showSuccess: (state, value) => { 27 | state.show = 'success' 28 | state.showMessage = value 29 | }, 30 | setLoading: (state, value) => { state.loading = value }, 31 | setReload: (state, value) => { state.reload = value }, 32 | setUser: (state, value) => { 33 | if (value === null) { 34 | state.user = null 35 | return 36 | } 37 | 38 | let locale = value.locale 39 | 40 | if (locale === '') { 41 | locale = i18n.detectLocale() 42 | } 43 | 44 | moment.locale(locale) 45 | i18n.default.locale = locale 46 | state.user = value 47 | }, 48 | setJWT: (state, value) => (state.jwt = value), 49 | multiple: (state, value) => (state.multiple = value), 50 | addSelected: (state, value) => (state.selected.push(value)), 51 | addPlugin: (state, value) => { 52 | state.plugins.push(value) 53 | }, 54 | removeSelected: (state, value) => { 55 | let i = state.selected.indexOf(value) 56 | if (i === -1) return 57 | state.selected.splice(i, 1) 58 | }, 59 | resetSelected: (state) => { 60 | state.selected = [] 61 | }, 62 | updateUser: (state, value) => { 63 | if (typeof value !== 'object') return 64 | 65 | for (let field in value) { 66 | if (field === 'locale') { 67 | moment.locale(value[field]) 68 | i18n.default.locale = value[field] 69 | } 70 | 71 | state.user[field] = value[field] 72 | } 73 | }, 74 | updateRequest: (state, value) => { 75 | state.oldReq = state.req 76 | state.req = value 77 | }, 78 | updateClipboard: (state, value) => { 79 | state.clipboard.key = value.key 80 | state.clipboard.items = value.items 81 | }, 82 | resetClipboard: (state) => { 83 | state.clipboard.key = '' 84 | state.clipboard.items = [] 85 | }, 86 | setProgress: (state, value) => { 87 | state.progress = value 88 | } 89 | } 90 | 91 | export default mutations 92 | -------------------------------------------------------------------------------- /src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | import router from '@/router' 3 | import { Base64 } from 'js-base64' 4 | import { baseURL } from '@/utils/constants' 5 | 6 | export function parseToken (token) { 7 | const parts = token.split('.') 8 | 9 | if (parts.length !== 3) { 10 | throw new Error('token malformed') 11 | } 12 | 13 | const data = JSON.parse(Base64.decode(parts[1])) 14 | 15 | if (Math.round(new Date().getTime() / 1000) > data.exp) { 16 | throw new Error('token expired') 17 | } 18 | 19 | localStorage.setItem('jwt', token) 20 | store.commit('setJWT', token) 21 | store.commit('setUser', data.user) 22 | } 23 | 24 | export async function validateLogin () { 25 | try { 26 | if (localStorage.getItem('jwt')) { 27 | await renew(localStorage.getItem('jwt')) 28 | } 29 | } catch (_) { 30 | console.warn('Invalid JWT token in storage') // eslint-disable-line 31 | } 32 | } 33 | 34 | export async function login (username, password, recaptcha) { 35 | const data = { username, password, recaptcha } 36 | 37 | const res = await fetch(`${baseURL}/api/login`, { 38 | method: 'POST', 39 | headers: { 40 | 'Content-Type': 'application/json' 41 | }, 42 | body: JSON.stringify(data) 43 | }) 44 | 45 | const body = await res.text() 46 | 47 | if (res.status === 200) { 48 | parseToken(body) 49 | } else { 50 | throw new Error(body) 51 | } 52 | } 53 | 54 | export async function renew (jwt) { 55 | const res = await fetch(`${baseURL}/api/renew`, { 56 | method: 'POST', 57 | headers: { 58 | 'X-Auth': jwt, 59 | } 60 | }) 61 | 62 | const body = await res.text() 63 | 64 | if (res.status === 200) { 65 | parseToken(body) 66 | } else { 67 | throw new Error(body) 68 | } 69 | } 70 | 71 | export async function signup (username, password) { 72 | const data = { username, password } 73 | 74 | const res = await fetch(`${baseURL}/api/signup`, { 75 | method: 'POST', 76 | headers: { 77 | 'Content-Type': 'application/json' 78 | }, 79 | body: JSON.stringify(data) 80 | }) 81 | 82 | if (res.status !== 200) { 83 | throw new Error(res.status) 84 | } 85 | } 86 | 87 | export function logout () { 88 | store.commit('setJWT', '') 89 | store.commit('setUser', null) 90 | localStorage.setItem('jwt', null) 91 | router.push({path: '/login'}) 92 | } 93 | -------------------------------------------------------------------------------- /src/utils/buttons.js: -------------------------------------------------------------------------------- 1 | function loading (button) { 2 | let el = document.querySelector(`#${button}-button > i`) 3 | 4 | if (el === undefined || el === null) { 5 | console.log('Error getting button ' + button) // eslint-disable-line 6 | return 7 | } 8 | 9 | el.dataset.icon = el.innerHTML 10 | el.style.opacity = 0 11 | 12 | setTimeout(() => { 13 | el.classList.add('spin') 14 | el.innerHTML = 'autorenew' 15 | el.style.opacity = 1 16 | }, 100) 17 | } 18 | 19 | function done (button) { 20 | let el = document.querySelector(`#${button}-button > i`) 21 | 22 | if (el === undefined || el === null) { 23 | console.log('Error getting button ' + button) // eslint-disable-line 24 | return 25 | } 26 | 27 | el.style.opacity = 0 28 | 29 | setTimeout(() => { 30 | el.classList.remove('spin') 31 | el.innerHTML = el.dataset.icon 32 | el.style.opacity = 1 33 | }, 100) 34 | } 35 | 36 | function success (button) { 37 | let el = document.querySelector(`#${button}-button > i`) 38 | 39 | if (el === undefined || el === null) { 40 | console.log('Error getting button ' + button) // eslint-disable-line 41 | return 42 | } 43 | 44 | el.style.opacity = 0 45 | 46 | setTimeout(() => { 47 | el.classList.remove('spin') 48 | el.innerHTML = 'done' 49 | el.style.opacity = 1 50 | 51 | setTimeout(() => { 52 | el.style.opacity = 0 53 | 54 | setTimeout(() => { 55 | el.innerHTML = el.dataset.icon 56 | el.style.opacity = 1 57 | }, 100) 58 | }, 500) 59 | }, 100) 60 | } 61 | 62 | export default { 63 | loading, 64 | done, 65 | success 66 | } 67 | -------------------------------------------------------------------------------- /src/utils/constants.js: -------------------------------------------------------------------------------- 1 | const name = window.FileBrowser.Name || 'File Browser' 2 | const disableExternal = window.FileBrowser.DisableExternal 3 | const baseURL = window.FileBrowser.BaseURL 4 | const staticURL = window.FileBrowser.StaticURL 5 | const recaptcha = window.FileBrowser.ReCaptcha 6 | const recaptchaKey = window.FileBrowser.ReCaptchaKey 7 | const signup = window.FileBrowser.Signup 8 | const version = window.FileBrowser.Version 9 | const logoURL = `/${staticURL}/img/logo.svg` 10 | const noAuth = window.FileBrowser.NoAuth 11 | const loginPage = window.FileBrowser.LoginPage 12 | 13 | export { 14 | name, 15 | disableExternal, 16 | baseURL, 17 | logoURL, 18 | recaptcha, 19 | recaptchaKey, 20 | signup, 21 | version, 22 | noAuth, 23 | loginPage 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/cookie.js: -------------------------------------------------------------------------------- 1 | export default function (name) { 2 | let re = new RegExp('(?:(?:^|.*;\\s*)' + name + '\\s*\\=\\s*([^;]*).*$)|^.*$') 3 | return document.cookie.replace(re, '$1') 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/css.js: -------------------------------------------------------------------------------- 1 | export default function getRule (rules) { 2 | for (let i = 0; i < rules.length; i++) { 3 | rules[i] = rules[i].toLowerCase() 4 | } 5 | 6 | let result = null 7 | let find = Array.prototype.find 8 | 9 | find.call(document.styleSheets, styleSheet => { 10 | result = find.call(styleSheet.cssRules, cssRule => { 11 | let found = false 12 | 13 | if (cssRule instanceof window.CSSStyleRule) { 14 | for (let i = 0; i < rules.length; i++) { 15 | if (cssRule.selectorText.toLowerCase() === rules[i]) { 16 | found = true 17 | } 18 | } 19 | } 20 | 21 | return found 22 | }) 23 | 24 | return result != null 25 | }) 26 | 27 | return result 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/url.js: -------------------------------------------------------------------------------- 1 | function removeLastDir (url) { 2 | var arr = url.split('/') 3 | if (arr.pop() === '') { 4 | arr.pop() 5 | } 6 | 7 | return arr.join('/') 8 | } 9 | 10 | export default { 11 | removeLastDir: removeLastDir 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/vue.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Noty from 'noty' 3 | import i18n from '@/i18n' 4 | import { disableExternal } from '@/utils/constants' 5 | 6 | Vue.config.productionTip = true 7 | 8 | const notyDefault = { 9 | type: 'info', 10 | layout: 'bottomRight', 11 | timeout: 1000, 12 | progressBar: true 13 | } 14 | 15 | Vue.prototype.$noty = (opts) => { 16 | new Noty(Object.assign({}, notyDefault, opts)).show() 17 | } 18 | 19 | Vue.prototype.$showSuccess = (message) => { 20 | new Noty(Object.assign({}, notyDefault, { 21 | text: message, 22 | type: 'success' 23 | })).show() 24 | } 25 | 26 | Vue.prototype.$showError = (error) => { 27 | let btns = [ 28 | Noty.button(i18n.t('buttons.close'), '', function () { 29 | n.close() 30 | }) 31 | ] 32 | 33 | if (!disableExternal) { 34 | btns.unshift(Noty.button(i18n.t('buttons.reportIssue'), '', function () { 35 | window.open('https://github.com/filebrowser/filebrowser/issues/new/choose') 36 | })) 37 | } 38 | 39 | let n = new Noty(Object.assign({}, notyDefault, { 40 | text: error.message || error, 41 | type: 'error', 42 | timeout: null, 43 | buttons: btns 44 | })) 45 | 46 | n.show() 47 | } 48 | 49 | Vue.directive('focus', { 50 | inserted: function (el) { 51 | el.focus() 52 | } 53 | }) 54 | 55 | export default Vue 56 | -------------------------------------------------------------------------------- /src/views/Files.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 235 | -------------------------------------------------------------------------------- /src/views/Layout.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 44 | -------------------------------------------------------------------------------- /src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 96 | -------------------------------------------------------------------------------- /src/views/Settings.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | -------------------------------------------------------------------------------- /src/views/Share.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 68 | -------------------------------------------------------------------------------- /src/views/errors/403.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /src/views/errors/404.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /src/views/errors/500.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /src/views/settings/Global.vue: -------------------------------------------------------------------------------- 1 | 95 | 96 | 170 | -------------------------------------------------------------------------------- /src/views/settings/Profile.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 104 | -------------------------------------------------------------------------------- /src/views/settings/User.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 149 | -------------------------------------------------------------------------------- /src/views/settings/Users.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 49 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | runtimeCompiler: true, 3 | publicPath: '[{[ .StaticURL ]}]' 4 | } --------------------------------------------------------------------------------