feat: migrate to vue 3 (#2689)

---------

Co-authored-by: Joep <jcbuhre@gmail.com>
Co-authored-by: Omar Hussein <omarmohammad1951@gmail.com>
Co-authored-by: Oleg Lobanov <oleg@lobanov.me>
This commit is contained in:
kloon15 2024-04-01 17:18:22 +02:00 committed by GitHub
parent 0e3b35b30e
commit 5100e587d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
164 changed files with 12202 additions and 8047 deletions

9
.gitignore vendored
View File

@ -30,5 +30,14 @@ yarn-error.log*
bin/ bin/
build/ build/
# Vue distributable files
/frontend/dist/* /frontend/dist/*
!/frontend/dist/.gitkeep !/frontend/dist/.gitkeep
# Playwright files
/frontend/test-results/
/frontend/playwright-report/
/frontend/playwright/.cache/
default.nix
Dockerfile.dev

View File

@ -9,12 +9,14 @@ import (
"hash" "hash"
"image" "image"
"io" "io"
"io/fs"
"log" "log"
"mime" "mime"
"net/http" "net/http"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"time" "time"
@ -27,6 +29,11 @@ import (
const PermFile = 0644 const PermFile = 0644
const PermDir = 0755 const PermDir = 0755
var (
reSubDirs = regexp.MustCompile("(?i)^sub(s|titles)$")
reSubExts = regexp.MustCompile("(?i)(.vtt|.srt|.ass|.ssa)$")
)
// FileInfo describes a file. // FileInfo describes a file.
type FileInfo struct { type FileInfo struct {
*Listing *Listing
@ -277,8 +284,8 @@ func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
return nil return nil
} }
func calculateImageResolution(fs afero.Fs, filePath string) (*ImageResolution, error) { func calculateImageResolution(fs_ afero.Fs, filePath string) (*ImageResolution, error) {
file, err := fs.Open(filePath) file, err := fs_.Open(filePath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -343,12 +350,45 @@ func (i *FileInfo) detectSubtitles() {
base := strings.TrimSuffix(i.Name, ext) base := strings.TrimSuffix(i.Name, ext)
for _, f := range dir { for _, f := range dir {
if !f.IsDir() && strings.HasPrefix(f.Name(), base) && strings.HasSuffix(f.Name(), ".vtt") { // load all supported subtitles from subs directories
i.Subtitles = append(i.Subtitles, path.Join(parentDir, f.Name())) // should cover all instances of subtitle distributions
// like tv-shows with multiple episodes in single dir
if f.IsDir() && reSubDirs.MatchString(f.Name()) {
subsDir := path.Join(parentDir, f.Name())
i.loadSubtitles(subsDir, base, true)
} else if isSubtitleMatch(f, base) {
i.addSubtitle(path.Join(parentDir, f.Name()))
} }
} }
} }
func (i *FileInfo) loadSubtitles(subsPath, baseName string, recursive bool) {
dir, err := afero.ReadDir(i.Fs, subsPath)
if err == nil {
for _, f := range dir {
if isSubtitleMatch(f, "") {
i.addSubtitle(path.Join(subsPath, f.Name()))
} else if f.IsDir() && recursive && strings.HasPrefix(f.Name(), baseName) {
subsDir := path.Join(subsPath, f.Name())
i.loadSubtitles(subsDir, baseName, false)
}
}
}
}
func IsSupportedSubtitle(fileName string) bool {
return reSubExts.MatchString(fileName)
}
func isSubtitleMatch(f fs.FileInfo, baseName string) bool {
return !f.IsDir() && strings.HasPrefix(f.Name(), baseName) &&
IsSupportedSubtitle(f.Name())
}
func (i *FileInfo) addSubtitle(path_ string) {
i.Subtitles = append(i.Subtitles, path_)
}
func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error { func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
afs := &afero.Afero{Fs: i.Fs} afs := &afero.Afero{Fs: i.Fs}
dir, err := afs.ReadDir(i.Path) dir, err := afs.ReadDir(i.Path)

View File

@ -4,14 +4,21 @@
"node": true "node": true
}, },
"extends": [ "extends": [
"plugin:vue/essential", "plugin:vue/vue3-essential",
"eslint:recommended", "eslint:recommended",
"@vue/eslint-config-typescript",
"@vue/eslint-config-prettier" "@vue/eslint-config-prettier"
], ],
"rules": { "rules": {
"vue/multi-word-component-names": "off", "vue/multi-word-component-names": "off",
"vue/no-reserved-component-names": "warn", "vue/no-mutating-props": [
"vue/no-mutating-props": "warn" "error",
{
"shallowOnly": true
}
]
// no-undef is already included in
// @vue/eslint-config-typescript
}, },
"parserOptions": { "parserOptions": {
"ecmaVersion": "latest", "ecmaVersion": "latest",

View File

@ -187,6 +187,6 @@
</div> </div>
</div> </div>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>

File diff suppressed because it is too large Load Diff

View File

@ -1,64 +1,71 @@
{ {
"name": "filebrowser-frontend", "name": "filebrowser-frontend",
"version": "2.0.0", "version": "3.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"engines": {
"npm": ">=7.0.0",
"node": ">=18.0.0"
},
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"serve": "vite serve", "build": "npm run typecheck && vite build",
"build": "vite build",
"watch": "vite build --watch",
"clean": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitkeep' -exec rm -r {} +", "clean": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitkeep' -exec rm -r {} +",
"lint": "eslint --ext .vue,.js src/", "typecheck": "vue-tsc -p ./tsconfig.json --noEmit",
"lint:fix": "eslint --ext .vue,.js --fix src/", "lint": "npm run typecheck && eslint --ext .vue,.ts src/",
"format": "prettier --write ." "lint:fix": "eslint --ext .vue,.ts --fix src/",
"format": "prettier --write .",
"test": "playwright test"
}, },
"dependencies": { "dependencies": {
"ace-builds": "^1.23.4", "@chenfengyuan/vue-number-input": "^2.0.1",
"clipboard": "^2.0.11", "@vueuse/core": "^10.9.0",
"core-js": "^3.32.0", "@vueuse/integrations": "^10.9.0",
"css-vars-ponyfill": "^2.4.8", "ace-builds": "^1.32.9",
"filesize": "^10.0.8", "core-js": "^3.36.1",
"js-base64": "^3.7.5", "dayjs": "^1.11.10",
"lodash.clonedeep": "^4.5.0", "filesize": "^10.1.1",
"lodash.throttle": "^4.1.1", "js-base64": "^3.7.7",
"material-icons": "^1.13.9", "jwt-decode": "^4.0.0",
"moment": "^2.29.4", "lodash-es": "^4.17.21",
"material-icons": "^1.13.12",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"noty": "^3.2.0-beta", "pinia": "^2.1.7",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
"qrcode.vue": "^1.7.0", "qrcode.vue": "^3.4.1",
"tus-js-client": "^3.1.1", "tus-js-client": "^4.1.0",
"utif": "^3.1.0", "utif": "^3.1.0",
"vue": "^2.7.14", "video.js": "^8.10.0",
"vue-async-computed": "^3.9.0", "videojs-hotkeys": "^0.2.28",
"vue-i18n": "^8.28.2", "videojs-mobile-ui": "^1.1.1",
"vue-lazyload": "^1.3.5", "vue": "^3.4.21",
"vue-router": "^3.6.5", "vue-final-modal": "^4.5.4",
"vue-simple-progress": "^1.1.1", "vue-i18n": "^9.10.2",
"vuex": "^3.6.2", "vue-lazyload": "^3.0.0",
"vuex-router-sync": "^5.0.0", "vue-router": "^4.3.0",
"whatwg-fetch": "^3.6.17" "vue-toastification": "^2.0.0-rc.5"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-legacy": "^4.1.1", "@intlify/unplugin-vue-i18n": "^4.0.0",
"@vitejs/plugin-vue2": "^2.2.0", "@playwright/test": "^1.42.1",
"@vue/eslint-config-prettier": "^8.0.0", "@types/lodash-es": "^4.17.12",
"autoprefixer": "^10.4.14", "@types/node": "^20.12.2",
"eslint": "^8.46.0", "@typescript-eslint/eslint-plugin": "^7.4.0",
"eslint-plugin-prettier": "^5.0.0", "@vitejs/plugin-legacy": "^5.3.2",
"eslint-plugin-vue": "^9.16.1", "@vitejs/plugin-vue": "^5.0.4",
"jsdom": "^22.1.0", "@vue/eslint-config-prettier": "^9.0.0",
"postcss": "^8.4.31", "@vue/eslint-config-typescript": "^13.0.0",
"prettier": "^3.0.1", "autoprefixer": "^10.4.19",
"terser": "^5.19.2", "concurrently": "^8.2.2",
"vite": "^4.5.2", "eslint": "^8.57.0",
"vite-plugin-compression2": "^0.10.3", "eslint-plugin-prettier": "^5.1.3",
"vite-plugin-rewrite-all": "^1.0.1" "eslint-plugin-vue": "^9.24.0",
}, "jsdom": "^24.0.0",
"browserslist": [ "postcss": "^8.4.38",
"> 1%", "prettier": "^3.2.5",
"last 2 versions", "terser": "^5.30.0",
"not ie < 11" "vite": "^5.2.7",
] "vite-plugin-compression2": "^1.0.0",
"vue-tsc": "^2.0.7"
}
} }

View File

@ -0,0 +1,80 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://127.0.0.1:5173",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
/* Set default locale to English (US) */
locale: "en-US",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
// {
// name: "webkit",
// use: { ...devices["Desktop Safari"] },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: "npm run dev",
url: "http://127.0.0.1:5173",
reuseExistingServer: !process.env.CI,
},
});

View File

@ -16,7 +16,7 @@
[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}] [{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]
</title> </title>
<meta name="robots" content="noindex,nofollow"> <meta name="robots" content="noindex,nofollow" />
<link <link
rel="icon" rel="icon"
@ -181,14 +181,9 @@
</div> </div>
</div> </div>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.ts"></script>
[{[ if .Theme -]}] [{[ if .CSS -]}]
<link
rel="stylesheet"
href="[{[ .StaticURL ]}]/themes/[{[ .Theme ]}].css"
/>
[{[ end ]}] [{[ if .CSS -]}]
<link rel="stylesheet" href="[{[ .StaticURL ]}]/custom.css" /> <link rel="stylesheet" href="[{[ .StaticURL ]}]/custom.css" />
[{[ end ]}] [{[ end ]}]
</body> </body>

View File

@ -1,217 +0,0 @@
:root {
--background: #141D24;
--surfacePrimary: #20292F;
--surfaceSecondary: #3A4147;
--divider: rgba(255, 255, 255, 0.12);
--icon: #ffffff;
--textPrimary: rgba(255, 255, 255, 0.87);
--textSecondary: rgba(255, 255, 255, 0.6);
}
body {
background: var(--background);
color: var(--textPrimary);
}
#loading {
background: var(--background);
}
#loading .spinner div, main .spinner div {
background: var(--icon);
}
#login {
background: var(--background);
}
header {
background: var(--surfacePrimary);
}
#search #input {
background: var(--surfaceSecondary);
border-color: var(--surfacePrimary);
}
#search #input input::placeholder {
color: var(--textSecondary);
}
#search.active #input {
background: var(--surfacePrimary);
}
#search.active input {
color: var(--textPrimary);
}
#search #result {
background: var(--background);
color: var(--textPrimary);
}
#search .boxes {
background: var(--surfaceSecondary);
}
#search .boxes h3 {
color: var(--textPrimary);
}
.action {
color: var(--textPrimary) !important;
}
.action:hover {
background-color: rgba(255, 255, 255, .1);
}
.action i {
color: var(--icon) !important;
}
.action .counter {
border-color: var(--surfacePrimary);
}
nav > div {
border-color: var(--divider);
}
.breadcrumbs {
border-color: var(--divider);
color: var(--textPrimary) !important;
}
.breadcrumbs span {
color: var(--textPrimary) !important;
}
.breadcrumbs a:hover {
background-color: rgba(255, 255, 255, .1);
}
#listing .item {
background: var(--surfacePrimary);
color: var(--textPrimary);
border-color: var(--divider) !important;
}
#listing .item i {
color: var(--icon);
}
#listing .item .modified {
color: var(--textSecondary);
}
#listing h2,
#listing.list .header span {
color: var(--textPrimary) !important;
}
#listing.list .header span {
color: var(--textPrimary);
}
#listing.list .header i {
color: var(--icon);
}
#listing.list .item.header {
background: var(--background);
}
.message {
color: var(--textPrimary);
}
.card {
background: var(--surfacePrimary);
color: var(--textPrimary);
}
.button--flat:hover {
background: var(--surfaceSecondary);
}
.dashboard #nav ul li {
color: var(--textSecondary);
}
.dashboard #nav ul li:hover {
background: var(--surfaceSecondary);
}
.card h3,
.dashboard #nav,
.dashboard p label {
color: var(--textPrimary);
}
.card#share input,
.card#share select,
.input {
background: var(--surfaceSecondary);
color: var(--textPrimary);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.input:hover,
.input:focus {
border-color: rgba(255, 255, 255, 0.15);
}
.input--red {
background: #73302D;
}
.input--green {
background: #147A41;
}
.dashboard #nav .wrapper,
.collapsible {
border-color: var(--divider);
}
.collapsible > label * {
color: var(--textPrimary);
}
table th {
color: var(--textSecondary);
}
.file-list li:hover {
background: var(--surfaceSecondary);
}
.file-list li:before {
color: var(--textSecondary);
}
.file-list li[aria-selected=true]:before {
color: var(--icon);
}
.shell {
background: var(--surfacePrimary);
color: var(--textPrimary);
}
.shell__divider {
background: rgba(255, 255, 255, 0.1);
}
.shell__divider:hover {
background: rgba(255, 255, 255, 0.4);
}
.shell__result {
border-top: 1px solid var(--divider);
}
#editor-container {
background: var(--background);
}
#editor-container .bar {
background: var(--surfacePrimary);
}
@media (max-width: 736px) {
#file-selection {
background: var(--surfaceSecondary) !important;
}
#file-selection span {
color: var(--textPrimary) !important;
}
nav {
background: var(--surfaceSecondary) !important;
}
#dropdown {
background: var(--surfaceSecondary) !important;
}
}
.share__box {
background: var(--surfacePrimary) !important;
color: var(--textPrimary);
}
.share__box__element {
border-top-color: var(--divider);
}

View File

@ -4,23 +4,30 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
// eslint-disable-next-line no-undef import { ref, onMounted, watch } from "vue";
// __webpack_public_path__ = window.FileBrowser.StaticURL + "/"; import { useI18n } from "vue-i18n";
import { setHtmlLocale } from "./i18n";
import { getMediaPreference, getTheme, setTheme } from "./utils/theme";
export default { const { locale } = useI18n();
name: "app",
mounted() { const userTheme = ref<UserTheme>(getTheme() || getMediaPreference());
onMounted(() => {
setTheme(userTheme.value);
setHtmlLocale(locale.value);
// this might be null during HMR
const loading = document.getElementById("loading"); const loading = document.getElementById("loading");
loading.classList.add("done"); loading?.classList.add("done");
setTimeout(function () { setTimeout(function () {
loading.parentNode.removeChild(loading); loading?.parentNode?.removeChild(loading);
}, 200); }, 200);
}, });
};
</script>
<style> // handles ltr/rtl changes
@import "./css/styles.css"; watch(locale, (newValue) => {
</style> newValue && setHtmlLocale(newValue);
});
</script>

View File

@ -1,15 +1,22 @@
import { removePrefix } from "./utils"; import { removePrefix } from "./utils";
import { baseURL } from "@/utils/constants"; import { baseURL } from "@/utils/constants";
import store from "@/store"; import { useAuthStore } from "@/stores/auth";
const ssl = window.location.protocol === "https:"; const ssl = window.location.protocol === "https:";
const protocol = ssl ? "wss:" : "ws:"; const protocol = ssl ? "wss:" : "ws:";
export default function command(url, command, onmessage, onclose) { export default function command(
url = removePrefix(url); url: string,
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${store.state.jwt}`; command: string,
onmessage: WebSocket["onmessage"],
onclose: WebSocket["onclose"]
) {
const authStore = useAuthStore();
let conn = new window.WebSocket(url); url = removePrefix(url);
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${authStore.jwt}`;
const conn = new window.WebSocket(url);
conn.onopen = () => conn.send(command); conn.onopen = () => conn.send(command);
conn.onmessage = onmessage; conn.onmessage = onmessage;
conn.onclose = onclose; conn.onclose = onclose;

View File

@ -1,19 +1,20 @@
import { createURL, fetchURL, removePrefix } from "./utils"; import { createURL, fetchURL, removePrefix } from "./utils";
import { baseURL } from "@/utils/constants"; import { baseURL } from "@/utils/constants";
import store from "@/store"; import { useAuthStore } from "@/stores/auth";
import { upload as postTus, useTus } from "./tus"; import { upload as postTus, useTus } from "./tus";
export async function fetch(url) { export async function fetch(url: string) {
url = removePrefix(url); url = removePrefix(url);
const res = await fetchURL(`/api/resources${url}`, {}); const res = await fetchURL(`/api/resources${url}`, {});
let data = await res.json(); const data = (await res.json()) as Resource;
data.url = `/files${url}`; data.url = `/files${url}`;
if (data.isDir) { if (data.isDir) {
if (!data.url.endsWith("/")) data.url += "/"; if (!data.url.endsWith("/")) data.url += "/";
data.items = data.items.map((item, index) => { // Perhaps change the any
data.items = data.items.map((item: any, index: any) => {
item.index = index; item.index = index;
item.url = `${data.url}${encodeURIComponent(item.name)}`; item.url = `${data.url}${encodeURIComponent(item.name)}`;
@ -28,10 +29,12 @@ export async function fetch(url) {
return data; return data;
} }
async function resourceAction(url, method, content) { async function resourceAction(url: string, method: ApiMethod, content?: any) {
url = removePrefix(url); url = removePrefix(url);
let opts = { method }; const opts: ApiOpts = {
method,
};
if (content) { if (content) {
opts.body = content; opts.body = content;
@ -42,15 +45,15 @@ async function resourceAction(url, method, content) {
return res; return res;
} }
export async function remove(url) { export async function remove(url: string) {
return resourceAction(url, "DELETE"); return resourceAction(url, "DELETE");
} }
export async function put(url, content = "") { export async function put(url: string, content = "") {
return resourceAction(url, "PUT", content); return resourceAction(url, "PUT", content);
} }
export function download(format, ...files) { export function download(format: any, ...files: string[]) {
let url = `${baseURL}/api/raw`; let url = `${baseURL}/api/raw`;
if (files.length === 1) { if (files.length === 1) {
@ -58,7 +61,7 @@ export function download(format, ...files) {
} else { } else {
let arg = ""; let arg = "";
for (let file of files) { for (const file of files) {
arg += removePrefix(file) + ","; arg += removePrefix(file) + ",";
} }
@ -71,14 +74,20 @@ export function download(format, ...files) {
url += `algo=${format}&`; url += `algo=${format}&`;
} }
if (store.state.jwt) { const authStore = useAuthStore();
url += `auth=${store.state.jwt}&`; if (authStore.jwt) {
url += `auth=${authStore.jwt}&`;
} }
window.open(url); window.open(url);
} }
export async function post(url, content = "", overwrite = false, onupload) { export async function post(
url: string,
content: ApiContent = "",
overwrite = false,
onupload: any = () => {}
) {
// Use the pre-existing API if: // Use the pre-existing API if:
const useResourcesApi = const useResourcesApi =
// a folder is being created // a folder is being created
@ -93,10 +102,15 @@ export async function post(url, content = "", overwrite = false, onupload) {
: postTus(url, content, overwrite, onupload); : postTus(url, content, overwrite, onupload);
} }
async function postResources(url, content = "", overwrite = false, onupload) { async function postResources(
url: string,
content: ApiContent = "",
overwrite = false,
onupload: any
) {
url = removePrefix(url); url = removePrefix(url);
let bufferContent; let bufferContent: ArrayBuffer;
if ( if (
content instanceof Blob && content instanceof Blob &&
!["http:", "https:"].includes(window.location.protocol) !["http:", "https:"].includes(window.location.protocol)
@ -104,14 +118,15 @@ async function postResources(url, content = "", overwrite = false, onupload) {
bufferContent = await new Response(content).arrayBuffer(); bufferContent = await new Response(content).arrayBuffer();
} }
const authStore = useAuthStore();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let request = new XMLHttpRequest(); const request = new XMLHttpRequest();
request.open( request.open(
"POST", "POST",
`${baseURL}/api/resources${url}?override=${overwrite}`, `${baseURL}/api/resources${url}?override=${overwrite}`,
true true
); );
request.setRequestHeader("X-Auth", store.state.jwt); request.setRequestHeader("X-Auth", authStore.jwt);
if (typeof onupload === "function") { if (typeof onupload === "function") {
request.upload.onprogress = onupload; request.upload.onprogress = onupload;
@ -135,12 +150,17 @@ async function postResources(url, content = "", overwrite = false, onupload) {
}); });
} }
function moveCopy(items, copy = false, overwrite = false, rename = false) { function moveCopy(
let promises = []; items: any[],
copy = false,
overwrite = false,
rename = false
) {
const promises = [];
for (let item of items) { for (const item of items) {
const from = item.from; const from = item.from;
const to = encodeURIComponent(removePrefix(item.to)); const to = encodeURIComponent(removePrefix(item.to ?? ""));
const url = `${from}?action=${ const url = `${from}?action=${
copy ? "copy" : "rename" copy ? "copy" : "rename"
}&destination=${to}&override=${overwrite}&rename=${rename}`; }&destination=${to}&override=${overwrite}&rename=${rename}`;
@ -150,20 +170,20 @@ function moveCopy(items, copy = false, overwrite = false, rename = false) {
return Promise.all(promises); return Promise.all(promises);
} }
export function move(items, overwrite = false, rename = false) { export function move(items: any[], overwrite = false, rename = false) {
return moveCopy(items, false, overwrite, rename); return moveCopy(items, false, overwrite, rename);
} }
export function copy(items, overwrite = false, rename = false) { export function copy(items: any[], overwrite = false, rename = false) {
return moveCopy(items, true, overwrite, rename); return moveCopy(items, true, overwrite, rename);
} }
export async function checksum(url, algo) { export async function checksum(url: string, algo: ChecksumAlg) {
const data = await resourceAction(`${url}?checksum=${algo}`, "GET"); const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
return (await data.json()).checksums[algo]; return (await data.json()).checksums[algo];
} }
export function getDownloadURL(file, inline) { export function getDownloadURL(file: ResourceItem, inline: any) {
const params = { const params = {
...(inline && { inline: "true" }), ...(inline && { inline: "true" }),
}; };
@ -171,7 +191,7 @@ export function getDownloadURL(file, inline) {
return createURL("api/raw" + file.path, params); return createURL("api/raw" + file.path, params);
} }
export function getPreviewURL(file, size) { export function getPreviewURL(file: ResourceItem, size: string) {
const params = { const params = {
inline: "true", inline: "true",
key: Date.parse(file.modified), key: Date.parse(file.modified),
@ -180,20 +200,15 @@ export function getPreviewURL(file, size) {
return createURL("api/preview/" + size + file.path, params); return createURL("api/preview/" + size + file.path, params);
} }
export function getSubtitlesURL(file) { export function getSubtitlesURL(file: ResourceItem) {
const params = { const params = {
inline: "true", inline: "true",
}; };
const subtitles = []; return file.subtitles?.map((d) => createURL("api/subtitle" + d, params));
for (const sub of file.subtitles) {
subtitles.push(createURL("api/raw" + sub, params));
} }
return subtitles; export async function usage(url: string) {
}
export async function usage(url) {
url = removePrefix(url); url = removePrefix(url);
const res = await fetchURL(`/api/usage${url}`, {}); const res = await fetchURL(`/api/usage${url}`, {});

View File

@ -1,7 +1,7 @@
import { fetchURL, removePrefix, createURL } from "./utils"; import { fetchURL, removePrefix, createURL } from "./utils";
import { baseURL } from "@/utils/constants"; import { baseURL } from "@/utils/constants";
export async function fetch(url, password = "") { export async function fetch(url: string, password: string = "") {
url = removePrefix(url); url = removePrefix(url);
const res = await fetchURL( const res = await fetchURL(
@ -12,12 +12,12 @@ export async function fetch(url, password = "") {
false false
); );
let data = await res.json(); const data = (await res.json()) as Resource;
data.url = `/share${url}`; data.url = `/share${url}`;
if (data.isDir) { if (data.isDir) {
if (!data.url.endsWith("/")) data.url += "/"; if (!data.url.endsWith("/")) data.url += "/";
data.items = data.items.map((item, index) => { data.items = data.items.map((item: any, index: any) => {
item.index = index; item.index = index;
item.url = `${data.url}${encodeURIComponent(item.name)}`; item.url = `${data.url}${encodeURIComponent(item.name)}`;
@ -32,7 +32,12 @@ export async function fetch(url, password = "") {
return data; return data;
} }
export function download(format, hash, token, ...files) { export function download(
format: DownloadFormat,
hash: string,
token: string,
...files: string[]
) {
let url = `${baseURL}/api/public/dl/${hash}`; let url = `${baseURL}/api/public/dl/${hash}`;
if (files.length === 1) { if (files.length === 1) {
@ -40,7 +45,7 @@ export function download(format, hash, token, ...files) {
} else { } else {
let arg = ""; let arg = "";
for (let file of files) { for (const file of files) {
arg += encodeURIComponent(file) + ","; arg += encodeURIComponent(file) + ",";
} }
@ -60,11 +65,11 @@ export function download(format, hash, token, ...files) {
window.open(url); window.open(url);
} }
export function getDownloadURL(share, inline = false) { export function getDownloadURL(res: Resource, inline = false) {
const params = { const params = {
...(inline && { inline: "true" }), ...(inline && { inline: "true" }),
...(share.token && { token: share.token }), ...(res.token && { token: res.token }),
}; };
return createURL("api/public/dl/" + share.hash + share.path, params, false); return createURL("api/public/dl/" + res.hash + res.path, params, false);
} }

View File

@ -1,7 +1,7 @@
import { fetchURL, removePrefix } from "./utils"; import { fetchURL, removePrefix } from "./utils";
import url from "../utils/url"; import url from "../utils/url";
export default async function search(base, query) { export default async function search(base: string, query: string) {
base = removePrefix(base); base = removePrefix(base);
query = encodeURIComponent(query); query = encodeURIComponent(query);
@ -9,11 +9,11 @@ export default async function search(base, query) {
base += "/"; base += "/";
} }
let res = await fetchURL(`/api/search${base}?query=${query}`, {}); const res = await fetchURL(`/api/search${base}?query=${query}`, {});
let data = await res.json(); let data = await res.json();
data = data.map((item) => { data = data.map((item: UploadItem) => {
item.url = `/files${base}` + url.encodePath(item.path); item.url = `/files${base}` + url.encodePath(item.path);
if (item.dir) { if (item.dir) {

View File

@ -1,10 +1,10 @@
import { fetchURL, fetchJSON } from "./utils"; import { fetchURL, fetchJSON } from "./utils";
export function get() { export function get() {
return fetchJSON(`/api/settings`, {}); return fetchJSON<ISettings>(`/api/settings`, {});
} }
export async function update(settings) { export async function update(settings: ISettings) {
await fetchURL(`/api/settings`, { await fetchURL(`/api/settings`, {
method: "PUT", method: "PUT",
body: JSON.stringify(settings), body: JSON.stringify(settings),

View File

@ -1,21 +1,26 @@
import { fetchURL, fetchJSON, removePrefix, createURL } from "./utils"; import { fetchURL, fetchJSON, removePrefix, createURL } from "./utils";
export async function list() { export async function list() {
return fetchJSON("/api/shares"); return fetchJSON<Share[]>("/api/shares");
} }
export async function get(url) { export async function get(url: string) {
url = removePrefix(url); url = removePrefix(url);
return fetchJSON(`/api/share${url}`); return fetchJSON<Share>(`/api/share${url}`);
} }
export async function remove(hash) { export async function remove(hash: string) {
await fetchURL(`/api/share/${hash}`, { await fetchURL(`/api/share/${hash}`, {
method: "DELETE", method: "DELETE",
}); });
} }
export async function create(url, password = "", expires = "", unit = "hours") { export async function create(
url: string,
password = "",
expires = "",
unit = "hours"
) {
url = removePrefix(url); url = removePrefix(url);
url = `/api/share${url}`; url = `/api/share${url}`;
if (expires !== "") { if (expires !== "") {
@ -23,7 +28,11 @@ export async function create(url, password = "", expires = "", unit = "hours") {
} }
let body = "{}"; let body = "{}";
if (password != "" || expires !== "" || unit !== "hours") { if (password != "" || expires !== "" || unit !== "hours") {
body = JSON.stringify({ password: password, expires: expires, unit: unit }); body = JSON.stringify({
password: password,
expires: expires.toString(), // backend expects string not number
unit: unit,
});
} }
return fetchJSON(url, { return fetchJSON(url, {
method: "POST", method: "POST",
@ -31,6 +40,6 @@ export async function create(url, password = "", expires = "", unit = "hours") {
}); });
} }
export function getShareURL(share) { export function getShareURL(share: Share) {
return createURL("share/" + share.hash, {}, false); return createURL("share/" + share.hash, {}, false);
} }

View File

@ -1,6 +1,7 @@
import * as tus from "tus-js-client"; import * as tus from "tus-js-client";
import { baseURL, tusEndpoint, tusSettings } from "@/utils/constants"; import { baseURL, tusEndpoint, tusSettings } from "@/utils/constants";
import store from "@/store"; import { useAuthStore } from "@/stores/auth";
import { useUploadStore } from "@/stores/upload";
import { removePrefix } from "@/api/utils"; import { removePrefix } from "@/api/utils";
import { fetchURL } from "./utils"; import { fetchURL } from "./utils";
@ -11,13 +12,13 @@ const ALPHA = 0.2;
const ONE_MINUS_ALPHA = 1 - ALPHA; const ONE_MINUS_ALPHA = 1 - ALPHA;
const RECENT_SPEEDS_LIMIT = 5; const RECENT_SPEEDS_LIMIT = 5;
const MB_DIVISOR = 1024 * 1024; const MB_DIVISOR = 1024 * 1024;
const CURRENT_UPLOAD_LIST = {}; const CURRENT_UPLOAD_LIST: CurrentUploadList = {};
export async function upload( export async function upload(
filePath, filePath: string,
content = "", content: ApiContent = "",
overwrite = false, overwrite = false,
onupload onupload: any
) { ) {
if (!tusSettings) { if (!tusSettings) {
// Shouldn't happen as we check for tus support before calling this function // Shouldn't happen as we check for tus support before calling this function
@ -25,29 +26,35 @@ export async function upload(
} }
filePath = removePrefix(filePath); filePath = removePrefix(filePath);
let resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`; const resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`;
await createUpload(resourcePath); await createUpload(resourcePath);
return new Promise((resolve, reject) => { const authStore = useAuthStore();
let upload = new tus.Upload(content, {
// Exit early because of typescript, tus content can't be a string
if (content === "") {
return false;
}
return new Promise<void | string>((resolve, reject) => {
const upload = new tus.Upload(content, {
uploadUrl: `${baseURL}${resourcePath}`, uploadUrl: `${baseURL}${resourcePath}`,
chunkSize: tusSettings.chunkSize, chunkSize: tusSettings.chunkSize,
retryDelays: computeRetryDelays(tusSettings), retryDelays: computeRetryDelays(tusSettings),
parallelUploads: 1, parallelUploads: 1,
storeFingerprintForResuming: false, storeFingerprintForResuming: false,
headers: { headers: {
"X-Auth": store.state.jwt, "X-Auth": authStore.jwt,
}, },
onError: function (error) { onError: function (error) {
if (CURRENT_UPLOAD_LIST[filePath].interval) { if (CURRENT_UPLOAD_LIST[filePath].interval) {
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval); clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
} }
delete CURRENT_UPLOAD_LIST[filePath]; delete CURRENT_UPLOAD_LIST[filePath];
reject("Upload failed: " + error); reject(new Error(`Upload failed: ${error.message}`));
}, },
onProgress: function (bytesUploaded) { onProgress: function (bytesUploaded) {
let fileData = CURRENT_UPLOAD_LIST[filePath]; const fileData = CURRENT_UPLOAD_LIST[filePath];
fileData.currentBytesUploaded = bytesUploaded; fileData.currentBytesUploaded = bytesUploaded;
if (!fileData.hasStarted) { if (!fileData.hasStarted) {
@ -79,14 +86,14 @@ export async function upload(
lastProgressTimestamp: null, lastProgressTimestamp: null,
sumOfRecentSpeeds: 0, sumOfRecentSpeeds: 0,
hasStarted: false, hasStarted: false,
interval: null, interval: undefined,
}; };
upload.start(); upload.start();
}); });
} }
async function createUpload(resourcePath) { async function createUpload(resourcePath: string) {
let headResp = await fetchURL(resourcePath, { const headResp = await fetchURL(resourcePath, {
method: "POST", method: "POST",
}); });
if (headResp.status !== 201) { if (headResp.status !== 201) {
@ -96,10 +103,10 @@ async function createUpload(resourcePath) {
} }
} }
function computeRetryDelays(tusSettings) { function computeRetryDelays(tusSettings: TusSettings): number[] | undefined {
if (!tusSettings.retryCount || tusSettings.retryCount < 1) { if (!tusSettings.retryCount || tusSettings.retryCount < 1) {
// Disable retries altogether // Disable retries altogether
return null; return undefined;
} }
// The tus client expects our retries as an array with computed backoffs // The tus client expects our retries as an array with computed backoffs
// E.g.: [0, 3000, 5000, 10000, 20000] // E.g.: [0, 3000, 5000, 10000, 20000]
@ -115,7 +122,7 @@ function computeRetryDelays(tusSettings) {
return retryDelays; return retryDelays;
} }
export async function useTus(content) { export async function useTus(content: ApiContent) {
return isTusSupported() && content instanceof Blob; return isTusSupported() && content instanceof Blob;
} }
@ -123,25 +130,34 @@ function isTusSupported() {
return tus.isSupported === true; return tus.isSupported === true;
} }
function computeETA(state) { function computeETA(state: ETAState, speed?: number) {
if (state.speedMbyte === 0) { if (state.speedMbyte === 0) {
return Infinity; return Infinity;
} }
const totalSize = state.sizes.reduce((acc, size) => acc + size, 0); const totalSize = state.sizes.reduce(
(acc: number, size: number) => acc + size,
0
);
const uploadedSize = state.progress.reduce( const uploadedSize = state.progress.reduce(
(acc, progress) => acc + progress, (acc: number, progress: Progress) => {
if (typeof progress === "number") {
return acc + progress;
}
return acc;
},
0 0
); );
const remainingSize = totalSize - uploadedSize; const remainingSize = totalSize - uploadedSize;
const speedBytesPerSecond = state.speedMbyte * 1024 * 1024; const speedBytesPerSecond = (speed ?? state.speedMbyte) * 1024 * 1024;
return remainingSize / speedBytesPerSecond; return remainingSize / speedBytesPerSecond;
} }
function computeGlobalSpeedAndETA() { function computeGlobalSpeedAndETA() {
const uploadStore = useUploadStore();
let totalSpeed = 0; let totalSpeed = 0;
let totalCount = 0; let totalCount = 0;
for (let filePath in CURRENT_UPLOAD_LIST) { for (const filePath in CURRENT_UPLOAD_LIST) {
totalSpeed += CURRENT_UPLOAD_LIST[filePath].currentAverageSpeed; totalSpeed += CURRENT_UPLOAD_LIST[filePath].currentAverageSpeed;
totalCount++; totalCount++;
} }
@ -149,41 +165,43 @@ function computeGlobalSpeedAndETA() {
if (totalCount === 0) return { speed: 0, eta: Infinity }; if (totalCount === 0) return { speed: 0, eta: Infinity };
const averageSpeed = totalSpeed / totalCount; const averageSpeed = totalSpeed / totalCount;
const averageETA = computeETA(store.state.upload, averageSpeed); const averageETA = computeETA(uploadStore, averageSpeed);
return { speed: averageSpeed, eta: averageETA }; return { speed: averageSpeed, eta: averageETA };
} }
function calcProgress(filePath) { function calcProgress(filePath: string) {
let fileData = CURRENT_UPLOAD_LIST[filePath]; const uploadStore = useUploadStore();
const fileData = CURRENT_UPLOAD_LIST[filePath];
let elapsedTime = (Date.now() - fileData.lastProgressTimestamp) / 1000; const elapsedTime =
let bytesSinceLastUpdate = (Date.now() - (fileData.lastProgressTimestamp ?? 0)) / 1000;
const bytesSinceLastUpdate =
fileData.currentBytesUploaded - fileData.initialBytesUploaded; fileData.currentBytesUploaded - fileData.initialBytesUploaded;
let currentSpeed = bytesSinceLastUpdate / MB_DIVISOR / elapsedTime; const currentSpeed = bytesSinceLastUpdate / MB_DIVISOR / elapsedTime;
if (fileData.recentSpeeds.length >= RECENT_SPEEDS_LIMIT) { if (fileData.recentSpeeds.length >= RECENT_SPEEDS_LIMIT) {
fileData.sumOfRecentSpeeds -= fileData.recentSpeeds.shift(); fileData.sumOfRecentSpeeds -= fileData.recentSpeeds.shift() ?? 0;
} }
fileData.recentSpeeds.push(currentSpeed); fileData.recentSpeeds.push(currentSpeed);
fileData.sumOfRecentSpeeds += currentSpeed; fileData.sumOfRecentSpeeds += currentSpeed;
let avgRecentSpeed = const avgRecentSpeed =
fileData.sumOfRecentSpeeds / fileData.recentSpeeds.length; fileData.sumOfRecentSpeeds / fileData.recentSpeeds.length;
fileData.currentAverageSpeed = fileData.currentAverageSpeed =
ALPHA * avgRecentSpeed + ONE_MINUS_ALPHA * fileData.currentAverageSpeed; ALPHA * avgRecentSpeed + ONE_MINUS_ALPHA * fileData.currentAverageSpeed;
const { speed, eta } = computeGlobalSpeedAndETA(); const { speed, eta } = computeGlobalSpeedAndETA();
store.commit("setUploadSpeed", speed); uploadStore.setUploadSpeed(speed);
store.commit("setETA", eta); uploadStore.setETA(eta);
fileData.initialBytesUploaded = fileData.currentBytesUploaded; fileData.initialBytesUploaded = fileData.currentBytesUploaded;
fileData.lastProgressTimestamp = Date.now(); fileData.lastProgressTimestamp = Date.now();
} }
export function abortAllUploads() { export function abortAllUploads() {
for (let filePath in CURRENT_UPLOAD_LIST) { for (const filePath in CURRENT_UPLOAD_LIST) {
if (CURRENT_UPLOAD_LIST[filePath].interval) { if (CURRENT_UPLOAD_LIST[filePath].interval) {
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval); clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
} }

View File

@ -1,14 +1,14 @@
import { fetchURL, fetchJSON } from "./utils"; import { fetchURL, fetchJSON, StatusError } from "./utils";
export async function getAll() { export async function getAll() {
return fetchJSON(`/api/users`, {}); return fetchJSON<IUser[]>(`/api/users`, {});
} }
export async function get(id) { export async function get(id: number) {
return fetchJSON(`/api/users/${id}`, {}); return fetchJSON<IUser>(`/api/users/${id}`, {});
} }
export async function create(user) { export async function create(user: IUser) {
const res = await fetchURL(`/api/users`, { const res = await fetchURL(`/api/users`, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
@ -21,9 +21,11 @@ export async function create(user) {
if (res.status === 201) { if (res.status === 201) {
return res.headers.get("Location"); return res.headers.get("Location");
} }
throw new StatusError(await res.text(), res.status);
} }
export async function update(user, which = ["all"]) { export async function update(user: IUser, which = ["all"]) {
await fetchURL(`/api/users/${user.id}`, { await fetchURL(`/api/users/${user.id}`, {
method: "PUT", method: "PUT",
body: JSON.stringify({ body: JSON.stringify({
@ -34,7 +36,7 @@ export async function update(user, which = ["all"]) {
}); });
} }
export async function remove(id) { export async function remove(id: number) {
await fetchURL(`/api/users/${id}`, { await fetchURL(`/api/users/${id}`, {
method: "DELETE", method: "DELETE",
}); });

View File

@ -1,80 +0,0 @@
import store from "@/store";
import { renew, logout } from "@/utils/auth";
import { baseURL } from "@/utils/constants";
import { encodePath } from "@/utils/url";
export async function fetchURL(url, opts, auth = true) {
opts = opts || {};
opts.headers = opts.headers || {};
let { headers, ...rest } = opts;
let res;
try {
res = await fetch(`${baseURL}${url}`, {
headers: {
"X-Auth": store.state.jwt,
...headers,
},
...rest,
});
} catch {
const error = new Error("000 No connection");
error.status = 0;
throw error;
}
if (auth && res.headers.get("X-Renew-Token") === "true") {
await renew(store.state.jwt);
}
if (res.status < 200 || res.status > 299) {
const error = new Error(await res.text());
error.status = res.status;
if (auth && res.status == 401) {
logout();
}
throw error;
}
return res;
}
export async function fetchJSON(url, opts) {
const res = await fetchURL(url, opts);
if (res.status === 200) {
return res.json();
} else {
throw new Error(res.status);
}
}
export function removePrefix(url) {
url = url.split("/").splice(2).join("/");
if (url === "") url = "/";
if (url[0] !== "/") url = "/" + url;
return url;
}
export function createURL(endpoint, params = {}, auth = true) {
let prefix = baseURL;
if (!prefix.endsWith("/")) {
prefix = prefix + "/";
}
const url = new URL(prefix + encodePath(endpoint), origin);
const searchParams = {
...(auth && { auth: store.state.jwt }),
...params,
};
for (const key in searchParams) {
url.searchParams.set(key, searchParams[key]);
}
return url.toString();
}

98
frontend/src/api/utils.ts Normal file
View File

@ -0,0 +1,98 @@
import { useAuthStore } from "@/stores/auth";
import { renew, logout } from "@/utils/auth";
import { baseURL } from "@/utils/constants";
import { encodePath } from "@/utils/url";
export class StatusError extends Error {
constructor(
message: any,
public status?: number
) {
super(message);
this.name = "StatusError";
}
}
export async function fetchURL(
url: string,
opts: ApiOpts,
auth = true
): Promise<Response> {
const authStore = useAuthStore();
opts = opts || {};
opts.headers = opts.headers || {};
const { headers, ...rest } = opts;
let res;
try {
res = await fetch(`${baseURL}${url}`, {
headers: {
"X-Auth": authStore.jwt,
...headers,
},
...rest,
});
} catch {
throw new StatusError("000 No connection", 0);
}
if (auth && res.headers.get("X-Renew-Token") === "true") {
await renew(authStore.jwt);
}
if (res.status < 200 || res.status > 299) {
const body = await res.text();
const error = new StatusError(
body || `${res.status} ${res.statusText}`,
res.status
);
if (auth && res.status == 401) {
logout();
}
throw error;
}
return res;
}
export async function fetchJSON<T>(url: string, opts?: any): Promise<T> {
const res = await fetchURL(url, opts);
if (res.status === 200) {
return res.json() as Promise<T>;
}
throw new StatusError(`${res.status} ${res.statusText}`, res.status);
}
export function removePrefix(url: string): string {
url = url.split("/").splice(2).join("/");
if (url === "") url = "/";
if (url[0] !== "/") url = "/" + url;
return url;
}
export function createURL(endpoint: string, params = {}, auth = true): string {
const authStore = useAuthStore();
let prefix = baseURL;
if (!prefix.endsWith("/")) {
prefix = prefix + "/";
}
const url = new URL(prefix + encodePath(endpoint), origin);
const searchParams: SearchParams = {
...(auth && { auth: authStore.jwt }),
...params,
};
for (const key in searchParams) {
url.searchParams.set(key, searchParams[key]);
}
return url.toString();
}

View File

@ -3,8 +3,8 @@
<component <component
:is="element" :is="element"
:to="base || ''" :to="base || ''"
:aria-label="$t('files.home')" :aria-label="t('files.home')"
:title="$t('files.home')" :title="t('files.home')"
> >
<i class="material-icons">home</i> <i class="material-icons">home</i>
</component> </component>
@ -18,13 +18,22 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
export default { import { computed } from "vue";
name: "breadcrumbs", import { useI18n } from "vue-i18n";
props: ["base", "noLink"], import { useRoute } from "vue-router";
computed: {
items() { const { t } = useI18n();
const relativePath = this.$route.path.replace(this.base, "");
const route = useRoute();
const props = defineProps<{
base: string;
noLink?: boolean;
}>();
const items = computed(() => {
const relativePath = route.path.replace(props.base, "");
let parts = relativePath.split("/"); let parts = relativePath.split("/");
if (parts[0] === "") { if (parts[0] === "") {
@ -35,13 +44,13 @@ export default {
parts.pop(); parts.pop();
} }
let breadcrumbs = []; let breadcrumbs: BreadCrumb[] = [];
for (let i = 0; i < parts.length; i++) { for (let i = 0; i < parts.length; i++) {
if (i === 0) { if (i === 0) {
breadcrumbs.push({ breadcrumbs.push({
name: decodeURIComponent(parts[i]), name: decodeURIComponent(parts[i]),
url: this.base + "/" + parts[i] + "/", url: props.base + "/" + parts[i] + "/",
}); });
} else { } else {
breadcrumbs.push({ breadcrumbs.push({
@ -60,16 +69,15 @@ export default {
} }
return breadcrumbs; return breadcrumbs;
}, });
element() {
if (this.noLink !== undefined) { const element = computed(() => {
if (props.noLink) {
return "span"; return "span";
} }
return "router-link"; return "router-link";
}, });
},
};
</script> </script>
<style></style> <style></style>

View File

@ -0,0 +1,45 @@
<template>
<div class="t-container">
<span>{{ message }}</span>
<button v-if="isReport" class="action" @click.stop="clicked">
{{ reportText }}
</button>
</div>
</template>
<script setup lang="ts">
defineProps<{
message: string;
reportText?: string;
isReport?: boolean;
}>();
const clicked = () => {
window.open("https://github.com/filebrowser/filebrowser/issues/new/choose");
};
</script>
<style scoped>
.t-container {
width: 100%;
padding: 5px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.action {
text-align: center;
height: 40px;
padding: 0 10px;
margin-left: 20px;
border-radius: 5px;
color: white;
cursor: pointer;
border: thin solid currentColor;
}
html[dir="rtl"] .action {
margin-left: initial;
margin-right: 20px;
}
</style>

View File

@ -0,0 +1,224 @@
<!-- This component taken directly from vue-simple-progress
since it didnt support Vue 3 but the component itself does
https://raw.githubusercontent.com/dzwillia/vue-simple-progress/master/src/components/Progress.vue -->
<template>
<div>
<div
class="vue-simple-progress-text"
:style="text_style"
v-if="text.length > 0 && textPosition == 'top'"
>
{{ text }}
</div>
<div class="vue-simple-progress" :style="progress_style">
<div
class="vue-simple-progress-text"
:style="text_style"
v-if="text.length > 0 && textPosition == 'middle'"
>
{{ text }}
</div>
<div
style="position: relative; left: -9999px"
:style="text_style"
v-if="text.length > 0 && textPosition == 'inside'"
>
{{ text }}
</div>
<div class="vue-simple-progress-bar" :style="bar_style">
<div
:style="text_style"
v-if="text.length > 0 && textPosition == 'inside'"
>
{{ text }}
</div>
</div>
</div>
<div
class="vue-simple-progress-text"
:style="text_style"
v-if="text.length > 0 && textPosition == 'bottom'"
>
{{ text }}
</div>
</div>
</template>
<script>
// We're leaving this untouched as you can read in the beginning
var isNumber = function (n) {
return !isNaN(parseFloat(n)) && isFinite(n);
};
export default {
name: "progress-bar",
props: {
val: {
default: 0,
},
max: {
default: 100,
},
size: {
// either a number (pixel width/height) or 'tiny', 'small',
// 'medium', 'large', 'huge', 'massive' for common sizes
default: 3,
},
"bg-color": {
type: String,
default: "#eee",
},
"bar-color": {
type: String,
default: "#2196f3", // match .blue color to Material Design's 'Blue 500' color
},
"bar-transition": {
type: String,
default: "all 0.5s ease",
},
"bar-border-radius": {
type: Number,
default: 0,
},
spacing: {
type: Number,
default: 4,
},
text: {
type: String,
default: "",
},
"text-align": {
type: String,
default: "center", // 'left', 'right'
},
"text-position": {
type: String,
default: "bottom", // 'bottom', 'top', 'middle', 'inside'
},
"font-size": {
type: Number,
default: 13,
},
"text-fg-color": {
type: String,
default: "#222",
},
},
computed: {
pct() {
var pct = (this.val / this.max) * 100;
pct = pct.toFixed(2);
return Math.min(pct, this.max);
},
size_px() {
switch (this.size) {
case "tiny":
return 2;
case "small":
return 4;
case "medium":
return 8;
case "large":
return 12;
case "big":
return 16;
case "huge":
return 32;
case "massive":
return 64;
}
return isNumber(this.size) ? this.size : 32;
},
text_padding() {
switch (this.size) {
case "tiny":
case "small":
case "medium":
case "large":
case "big":
case "huge":
case "massive":
return Math.min(Math.max(Math.ceil(this.size_px / 8), 3), 12);
}
return isNumber(this.spacing) ? this.spacing : 4;
},
text_font_size() {
switch (this.size) {
case "tiny":
case "small":
case "medium":
case "large":
case "big":
case "huge":
case "massive":
return Math.min(Math.max(Math.ceil(this.size_px * 1.4), 11), 32);
}
return isNumber(this.fontSize) ? this.fontSize : 13;
},
progress_style() {
var style = {
background: this.bgColor,
};
if (this.textPosition == "middle" || this.textPosition == "inside") {
style["position"] = "relative";
style["min-height"] = this.size_px + "px";
style["z-index"] = "-2";
}
if (this.barBorderRadius > 0) {
style["border-radius"] = this.barBorderRadius + "px";
}
return style;
},
bar_style() {
var style = {
background: this.barColor,
width: this.pct + "%",
height: this.size_px + "px",
transition: this.barTransition,
};
if (this.barBorderRadius > 0) {
style["border-radius"] = this.barBorderRadius + "px";
}
if (this.textPosition == "middle" || this.textPosition == "inside") {
style["position"] = "absolute";
style["top"] = "0";
style["height"] = "100%";
(style["min-height"] = this.size_px + "px"), (style["z-index"] = "-1");
}
return style;
},
text_style() {
var style = {
color: this.textFgColor,
"font-size": this.text_font_size + "px",
"text-align": this.textAlign,
};
if (
this.textPosition == "top" ||
this.textPosition == "middle" ||
this.textPosition == "inside"
)
style["padding-bottom"] = this.text_padding + "px";
if (
this.textPosition == "bottom" ||
this.textPosition == "middle" ||
this.textPosition == "inside"
)
style["padding-top"] = this.text_padding + "px";
return style;
},
},
};
</script>

View File

@ -17,7 +17,7 @@
@keyup.enter="submit" @keyup.enter="submit"
ref="input" ref="input"
:autofocus="active" :autofocus="active"
v-model.trim="value" v-model.trim="prompt"
:aria-label="$t('search.search')" :aria-label="$t('search.search')"
:placeholder="$t('search.search')" :placeholder="$t('search.search')"
/> />
@ -28,7 +28,7 @@
<template v-if="isEmpty"> <template v-if="isEmpty">
<p>{{ text }}</p> <p>{{ text }}</p>
<template v-if="value.length === 0"> <template v-if="prompt.length === 0">
<div class="boxes"> <div class="boxes">
<h3>{{ $t("search.types") }}</h3> <h3>{{ $t("search.types") }}</h3>
<div> <div>
@ -49,7 +49,7 @@
</template> </template>
<ul v-show="results.length > 0"> <ul v-show="results.length > 0">
<li v-for="(s, k) in filteredResults" :key="k"> <li v-for="(s, k) in filteredResults" :key="k">
<router-link @click.native="close" :to="s.url"> <router-link v-on:click="close" :to="s.url">
<i v-if="s.dir" class="material-icons">folder</i> <i v-if="s.dir" class="material-icons">folder</i>
<i v-else class="material-icons">insert_drive_file</i> <i v-else class="material-icons">insert_drive_file</i>
<span>./{{ s.path }}</span> <span>./{{ s.path }}</span>
@ -64,138 +64,155 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapState, mapGetters, mapMutations } from "vuex"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import url from "@/utils/url"; import url from "@/utils/url";
import { search } from "@/api"; import { search } from "@/api";
import { computed, inject, onMounted, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { storeToRefs } from "pinia";
var boxes = { const boxes = {
image: { label: "images", icon: "insert_photo" }, image: { label: "images", icon: "insert_photo" },
audio: { label: "music", icon: "volume_up" }, audio: { label: "music", icon: "volume_up" },
video: { label: "video", icon: "movie" }, video: { label: "video", icon: "movie" },
pdf: { label: "pdf", icon: "picture_as_pdf" }, pdf: { label: "pdf", icon: "picture_as_pdf" },
}; };
export default { const layoutStore = useLayoutStore();
name: "search", const fileStore = useFileStore();
data: function () {
return {
value: "",
active: false,
ongoing: false,
results: [],
reload: false,
resultsCount: 50,
scrollable: null,
};
},
watch: {
currentPrompt(val, old) {
this.active = val?.prompt === "search";
if (old?.prompt === "search" && !this.active) { const { currentPromptName } = storeToRefs(layoutStore);
if (this.reload) {
this.setReload(true); const prompt = ref<string>("");
const active = ref<boolean>(false);
const ongoing = ref<boolean>(false);
const results = ref<any[]>([]);
const reload = ref<boolean>(false);
const resultsCount = ref<number>(50);
const $showError = inject<IToastError>("$showError")!;
const input = ref<HTMLInputElement | null>(null);
const result = ref<HTMLElement | null>(null);
const { t } = useI18n();
const route = useRoute();
watch(currentPromptName, (newVal, oldVal) => {
active.value = newVal === "search";
if (oldVal === "search" && !active.value) {
if (reload.value) {
fileStore.reload = true;
} }
document.body.style.overflow = "auto"; document.body.style.overflow = "auto";
this.reset(); reset();
this.value = ""; prompt.value = "";
this.active = false; active.value = false;
this.$refs.input.blur(); input.value?.blur();
} else if (this.active) { } else if (active.value) {
this.reload = false; reload.value = false;
this.$refs.input.focus(); input.value?.focus();
document.body.style.overflow = "hidden"; document.body.style.overflow = "hidden";
} }
}, });
value() {
if (this.results.length) { watch(prompt, () => {
this.reset(); if (results.value.length) {
reset();
} }
}, });
},
computed: { // ...mapState(useFileStore, ["isListing"]),
...mapState(["user"]), // ...mapState(useLayoutStore, ["show"]),
...mapGetters(["isListing", "currentPrompt"]), // ...mapWritableState(useFileStore, { sReload: "reload" }),
boxes() {
return boxes; const isEmpty = computed(() => {
}, return results.value.length === 0;
isEmpty() { });
return this.results.length === 0; const text = computed(() => {
}, if (ongoing.value) {
text() {
if (this.ongoing) {
return ""; return "";
} }
return this.value === "" return prompt.value === ""
? this.$t("search.typeToSearch") ? t("search.typeToSearch")
: this.$t("search.pressToSearch"); : t("search.pressToSearch");
}, });
filteredResults() { const filteredResults = computed(() => {
return this.results.slice(0, this.resultsCount); return results.value.slice(0, resultsCount.value);
}, });
},
mounted() { onMounted(() => {
this.$refs.result.addEventListener("scroll", (event) => { if (result.value === null) {
return;
}
result.value.addEventListener("scroll", (event: Event) => {
if ( if (
event.target.offsetHeight + event.target.scrollTop >= (event.target as HTMLElement).offsetHeight +
event.target.scrollHeight - 100 (event.target as HTMLElement).scrollTop >=
(event.target as HTMLElement).scrollHeight - 100
) { ) {
this.resultsCount += 50; resultsCount.value += 50;
} }
}); });
}, });
methods: {
...mapMutations(["showHover", "closeHovers", "setReload"]), const open = () => {
open() { !active.value && layoutStore.showHover("search");
this.showHover("search"); };
},
close(event) { const close = (event: Event) => {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
this.closeHovers(); layoutStore.closeHovers();
}, };
keyup(event) {
if (event.keyCode === 27) { const keyup = (event: KeyboardEvent) => {
this.close(event); if (event.key === "Escape") {
close(event);
return; return;
} }
results.value.length = 0;
};
this.results.length = 0; const init = (string: string) => {
}, prompt.value = `${string} `;
init(string) { input.value !== null ? input.value.focus() : "";
this.value = `${string} `; };
this.$refs.input.focus();
}, const reset = () => {
reset() { ongoing.value = false;
this.ongoing = false; resultsCount.value = 50;
this.resultsCount = 50; results.value = [];
this.results = []; };
},
async submit(event) { const submit = async (event: Event) => {
event.preventDefault(); event.preventDefault();
if (this.value === "") { if (prompt.value === "") {
return; return;
} }
let path = this.$route.path; let path = route.path;
if (!this.isListing) { if (!fileStore.isListing) {
path = url.removeLastDir(path) + "/"; path = url.removeLastDir(path) + "/";
} }
this.ongoing = true; ongoing.value = true;
try { try {
this.results = await search(path, this.value); results.value = await search(path, prompt.value);
} catch (error) { } catch (error: any) {
this.$showError(error); $showError(error);
} }
this.ongoing = false; ongoing.value = false;
},
},
}; };
</script> </script>

View File

@ -29,9 +29,9 @@
tabindex="0" tabindex="0"
ref="input" ref="input"
class="shell__text" class="shell__text"
contenteditable="true" :contenteditable="true"
@keydown.prevent.38="historyUp" @keydown.prevent.arrow-up="historyUp"
@keydown.prevent.40="historyDown" @keydown.prevent.arrow-down="historyDown"
@keypress.prevent.enter="submit" @keypress.prevent.enter="submit"
/> />
</div> </div>
@ -45,7 +45,10 @@
</template> </template>
<script> <script>
import { mapMutations, mapState, mapGetters } from "vuex"; import { mapState, mapActions } from "pinia";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { commands } from "@/api"; import { commands } from "@/api";
import { throttle } from "lodash"; import { throttle } from "lodash";
import { theme } from "@/utils/constants"; import { theme } from "@/utils/constants";
@ -53,8 +56,8 @@ import { theme } from "@/utils/constants";
export default { export default {
name: "shell", name: "shell",
computed: { computed: {
...mapState(["user", "showShell"]), ...mapState(useLayoutStore, ["showShell"]),
...mapGetters(["isFiles", "isLogged"]), ...mapState(useFileStore, ["isFiles"]),
path: function () { path: function () {
if (this.isFiles) { if (this.isFiles) {
return this.$route.path; return this.$route.path;
@ -75,11 +78,11 @@ export default {
mounted() { mounted() {
window.addEventListener("resize", this.resize); window.addEventListener("resize", this.resize);
}, },
beforeDestroy() { beforeUnmount() {
window.removeEventListener("resize", this.resize); window.removeEventListener("resize", this.resize);
}, },
methods: { methods: {
...mapMutations(["toggleShell"]), ...mapActions(useLayoutStore, ["toggleShell"]),
checkTheme() { checkTheme() {
if (theme == "dark") { if (theme == "dark") {
return "rgba(255, 255, 255, 0.4)"; return "rgba(255, 255, 255, 0.4)";

View File

@ -1,6 +1,6 @@
<template> <template>
<nav :class="{ active }"> <nav :class="{ active }">
<template v-if="isLogged"> <template v-if="isLoggedIn">
<button <button
class="action" class="action"
@click="toRoot" @click="toRoot"
@ -13,7 +13,7 @@
<div v-if="user.perm.create"> <div v-if="user.perm.create">
<button <button
@click="$store.commit('showHover', 'newDir')" @click="showHover('newDir')"
class="action" class="action"
:aria-label="$t('sidebar.newFolder')" :aria-label="$t('sidebar.newFolder')"
:title="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')"
@ -23,7 +23,7 @@
</button> </button>
<button <button
@click="$store.commit('showHover', 'newFile')" @click="showHover('newFile')"
class="action" class="action"
:aria-label="$t('sidebar.newFile')" :aria-label="$t('sidebar.newFile')"
:title="$t('sidebar.newFile')" :title="$t('sidebar.newFile')"
@ -82,9 +82,7 @@
<div <div
class="credits" class="credits"
v-if=" v-if="isFiles && !disableUsedPercentage"
$router.currentRoute.path.includes('/files/') && !disableUsedPercentage
"
style="width: 90%; margin: 2em 2.5em 3em 2.5em" style="width: 90%; margin: 2em 2.5em 3em 2.5em"
> >
<progress-bar :val="usage.usedPercentage" size="small"></progress-bar> <progress-bar :val="usage.usedPercentage" size="small"></progress-bar>
@ -112,7 +110,12 @@
</template> </template>
<script> <script>
import { mapState, mapGetters } from "vuex"; import { reactive } from "vue";
import { mapActions, mapState } from "pinia";
import { useAuthStore } from "@/stores/auth";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import * as auth from "@/utils/auth"; import * as auth from "@/utils/auth";
import { import {
version, version,
@ -123,19 +126,27 @@ import {
loginPage, loginPage,
} from "@/utils/constants"; } from "@/utils/constants";
import { files as api } from "@/api"; import { files as api } from "@/api";
import ProgressBar from "vue-simple-progress"; import ProgressBar from "@/components/ProgressBar.vue";
import prettyBytes from "pretty-bytes"; import prettyBytes from "pretty-bytes";
const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 };
export default { export default {
name: "sidebar", name: "sidebar",
setup() {
const usage = reactive(USAGE_DEFAULT);
return { usage };
},
components: { components: {
ProgressBar, ProgressBar,
}, },
inject: ["$showError"],
computed: { computed: {
...mapState(["user"]), ...mapState(useAuthStore, ["user", "isLoggedIn"]),
...mapGetters(["isLogged", "currentPrompt"]), ...mapState(useFileStore, ["isFiles", "reload"]),
...mapState(useLayoutStore, ["currentPromptName"]),
active() { active() {
return this.currentPrompt?.prompt === "sidebar"; return this.currentPromptName === "sidebar";
}, },
signup: () => signup, signup: () => signup,
version: () => version, version: () => version,
@ -143,15 +154,15 @@ export default {
disableUsedPercentage: () => disableUsedPercentage, disableUsedPercentage: () => disableUsedPercentage,
canLogout: () => !noAuth && loginPage, canLogout: () => !noAuth && loginPage,
}, },
asyncComputed: { methods: {
usage: { ...mapActions(useLayoutStore, ["closeHovers", "showHover"]),
async get() { async fetchUsage() {
let path = this.$route.path.endsWith("/") let path = this.$route.path.endsWith("/")
? this.$route.path ? this.$route.path
: this.$route.path + "/"; : this.$route.path + "/";
let usageStats = { used: 0, total: 0, usedPercentage: 0 }; let usageStats = USAGE_DEFAULT;
if (this.disableUsedPercentage) { if (this.disableUsedPercentage) {
return usageStats; return Object.assign(this.usage, usageStats);
} }
try { try {
let usage = await api.usage(path); let usage = await api.usage(path);
@ -163,27 +174,25 @@ export default {
} catch (error) { } catch (error) {
this.$showError(error); this.$showError(error);
} }
return usageStats; return Object.assign(this.usage, usageStats);
}, },
default: { used: "0 B", total: "0 B", usedPercentage: 0 },
shouldUpdate() {
return this.$router.currentRoute.path.includes("/files/");
},
},
},
methods: {
toRoot() { toRoot() {
this.$router.push({ path: "/files/" }, () => {}); this.$router.push({ path: "/files" });
this.$store.commit("closeHovers"); this.closeHovers();
}, },
toSettings() { toSettings() {
this.$router.push({ path: "/settings" }, () => {}); this.$router.push({ path: "/settings" });
this.$store.commit("closeHovers"); this.closeHovers();
}, },
help() { help() {
this.$store.commit("showHover", "help"); this.showHover("help");
}, },
logout: auth.logout, logout: auth.logout,
}, },
watch: {
isFiles(newValue) {
newValue && this.fetchUsage();
},
},
}; };
</script> </script>

View File

@ -13,205 +13,230 @@
<img class="image-ex-img image-ex-img-center" ref="imgex" @load="onLoad" /> <img class="image-ex-img image-ex-img-center" ref="imgex" @load="onLoad" />
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import throttle from "lodash.throttle"; import throttle from "lodash/throttle";
import UTIF from "utif"; import UTIF from "utif";
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
export default { interface IProps {
props: { src: string;
src: String, moveDisabledTime: number;
moveDisabledTime: { classList: any[];
type: Number, zoomStep: number;
default: () => 200, }
},
classList: { const props = withDefaults(defineProps<IProps>(), {
type: Array, moveDisabledTime: () => 200,
default: () => [], classList: () => [],
}, zoomStep: () => 0.25,
zoomStep: { });
type: Number,
default: () => 0.25, const scale = ref<number>(1);
}, const lastX = ref<number | null>(null);
}, const lastY = ref<number | null>(null);
data() { const inDrag = ref<boolean>(false);
return { const touches = ref<number>(0);
scale: 1, const lastTouchDistance = ref<number | null>(0);
lastX: null, const moveDisabled = ref<boolean>(false);
lastY: null, const disabledTimer = ref<number | null>(null);
inDrag: false, const imageLoaded = ref<boolean>(false);
touches: 0, const position = ref<{
lastTouchDistance: 0, center: { x: number; y: number };
moveDisabled: false, relative: { x: number; y: number };
disabledTimer: null, }>({
imageLoaded: false,
position: {
center: { x: 0, y: 0 }, center: { x: 0, y: 0 },
relative: { x: 0, y: 0 }, relative: { x: 0, y: 0 },
}, });
maxScale: 4, const maxScale = ref<number>(4);
minScale: 0.25, const minScale = ref<number>(0.25);
};
}, // Refs
mounted() { const imgex = ref<HTMLImageElement | null>(null);
if (!this.decodeUTIF()) { const container = ref<HTMLDivElement | null>(null);
this.$refs.imgex.src = this.src;
onMounted(() => {
if (!decodeUTIF() && imgex.value !== null) {
imgex.value.src = props.src;
} }
let container = this.$refs.container;
this.classList.forEach((className) => container.classList.add(className)); props.classList.forEach((className) =>
container.value !== null ? container.value.classList.add(className) : ""
);
if (container.value === null) {
return;
}
// set width and height if they are zero // set width and height if they are zero
if (getComputedStyle(container).width === "0px") { if (getComputedStyle(container.value).width === "0px") {
container.style.width = "100%"; container.value.style.width = "100%";
} }
if (getComputedStyle(container).height === "0px") { if (getComputedStyle(container.value).height === "0px") {
container.style.height = "100%"; container.value.style.height = "100%";
} }
window.addEventListener("resize", this.onResize); window.addEventListener("resize", onResize);
}, });
beforeDestroy() {
window.removeEventListener("resize", this.onResize); onBeforeUnmount(() => {
document.removeEventListener("mouseup", this.onMouseUp); window.removeEventListener("resize", onResize);
}, document.removeEventListener("mouseup", onMouseUp);
watch: { });
src: function () {
if (!this.decodeUTIF()) { watch(
this.$refs.imgex.src = this.src; () => props.src,
() => {
if (!decodeUTIF() && imgex.value !== null) {
imgex.value.src = props.src;
} }
this.scale = 1; scale.value = 1;
this.setZoom(); setZoom();
this.setCenter(); setCenter();
}, }
}, );
methods: {
// Modified from UTIF.replaceIMG // Modified from UTIF.replaceIMG
decodeUTIF() { const decodeUTIF = () => {
const sufs = ["tif", "tiff", "dng", "cr2", "nef"]; const sufs = ["tif", "tiff", "dng", "cr2", "nef"];
let suff = document.location.pathname.split(".").pop().toLowerCase(); if (document?.location?.pathname === undefined) {
return;
}
let suff = document.location.pathname.split(".")?.pop()?.toLowerCase() ?? "";
if (sufs.indexOf(suff) == -1) return false; if (sufs.indexOf(suff) == -1) return false;
let xhr = new XMLHttpRequest(); let xhr = new XMLHttpRequest();
UTIF._xhrs.push(xhr); UTIF._xhrs.push(xhr);
UTIF._imgs.push(this.$refs.imgex); UTIF._imgs.push(imgex.value);
xhr.open("GET", this.src); xhr.open("GET", props.src);
xhr.responseType = "arraybuffer"; xhr.responseType = "arraybuffer";
xhr.onload = UTIF._imgLoaded; xhr.onload = UTIF._imgLoaded;
xhr.send(); xhr.send();
return true; return true;
}, };
onLoad() {
let img = this.$refs.imgex;
this.imageLoaded = true; const onLoad = () => {
imageLoaded.value = true;
if (img === undefined) { if (imgex.value === null) {
return; return;
} }
img.classList.remove("image-ex-img-center"); imgex.value.classList.remove("image-ex-img-center");
this.setCenter(); setCenter();
img.classList.add("image-ex-img-ready"); imgex.value.classList.add("image-ex-img-ready");
document.addEventListener("mouseup", this.onMouseUp); document.addEventListener("mouseup", onMouseUp);
let realSize = img.naturalWidth; let realSize = imgex.value.naturalWidth;
let displaySize = img.offsetWidth; let displaySize = imgex.value.offsetWidth;
// Image is in portrait orientation // Image is in portrait orientation
if (img.naturalHeight > img.naturalWidth) { if (imgex.value.naturalHeight > imgex.value.naturalWidth) {
realSize = img.naturalHeight; realSize = imgex.value.naturalHeight;
displaySize = img.offsetHeight; displaySize = imgex.value.offsetHeight;
} }
// Scale needed to display the image on full size // Scale needed to display the image on full size
const fullScale = realSize / displaySize; const fullScale = realSize / displaySize;
// Full size plus additional zoom // Full size plus additional zoom
this.maxScale = fullScale + 4; maxScale.value = fullScale + 4;
}, };
onMouseUp() {
this.inDrag = false; const onMouseUp = () => {
}, inDrag.value = false;
onResize: throttle(function () { };
if (this.imageLoaded) {
this.setCenter(); const onResize = throttle(function () {
this.doMove(this.position.relative.x, this.position.relative.y); if (imageLoaded.value) {
setCenter();
doMove(position.value.relative.x, position.value.relative.y);
} }
}, 100), }, 100);
setCenter() {
let container = this.$refs.container;
let img = this.$refs.imgex;
this.position.center.x = Math.floor( const setCenter = () => {
(container.clientWidth - img.clientWidth) / 2 if (container.value === null || imgex.value === null) {
return;
}
position.value.center.x = Math.floor(
(container.value.clientWidth - imgex.value.clientWidth) / 2
); );
this.position.center.y = Math.floor( position.value.center.y = Math.floor(
(container.clientHeight - img.clientHeight) / 2 (container.value.clientHeight - imgex.value.clientHeight) / 2
); );
img.style.left = this.position.center.x + "px"; imgex.value.style.left = position.value.center.x + "px";
img.style.top = this.position.center.y + "px"; imgex.value.style.top = position.value.center.y + "px";
}, };
mousedownStart(event) {
this.lastX = null; const mousedownStart = (event: Event) => {
this.lastY = null; lastX.value = null;
this.inDrag = true; lastY.value = null;
inDrag.value = true;
event.preventDefault(); event.preventDefault();
}, };
mouseMove(event) { const mouseMove = (event: MouseEvent) => {
if (!this.inDrag) return; if (!inDrag.value) return;
this.doMove(event.movementX, event.movementY); doMove(event.movementX, event.movementY);
event.preventDefault(); event.preventDefault();
}, };
mouseUp(event) { const mouseUp = (event: Event) => {
this.inDrag = false; inDrag.value = false;
event.preventDefault(); event.preventDefault();
}, };
touchStart(event) { const touchStart = (event: TouchEvent) => {
this.lastX = null; lastX.value = null;
this.lastY = null; lastY.value = null;
this.lastTouchDistance = null; lastTouchDistance.value = null;
if (event.targetTouches.length < 2) { if (event.targetTouches.length < 2) {
setTimeout(() => { setTimeout(() => {
this.touches = 0; touches.value = 0;
}, 300); }, 300);
this.touches++; touches.value++;
if (this.touches > 1) { if (touches.value > 1) {
this.zoomAuto(event); zoomAuto(event);
} }
} }
event.preventDefault(); event.preventDefault();
}, };
zoomAuto(event) {
switch (this.scale) { const zoomAuto = (event: Event) => {
switch (scale.value) {
case 1: case 1:
this.scale = 2; scale.value = 2;
break; break;
case 2: case 2:
this.scale = 4; scale.value = 4;
break; break;
default: default:
case 4: case 4:
this.scale = 1; scale.value = 1;
this.setCenter(); setCenter();
break; break;
} }
this.setZoom(); setZoom();
event.preventDefault(); event.preventDefault();
}, };
touchMove(event) {
const touchMove = (event: TouchEvent) => {
event.preventDefault(); event.preventDefault();
if (this.lastX === null) { if (lastX.value === null) {
this.lastX = event.targetTouches[0].pageX; lastX.value = event.targetTouches[0].pageX;
this.lastY = event.targetTouches[0].pageY; lastY.value = event.targetTouches[0].pageY;
return; return;
} }
let step = this.$refs.imgex.width / 5; if (imgex.value === null) {
return;
}
let step = imgex.value.width / 5;
if (event.targetTouches.length === 2) { if (event.targetTouches.length === 2) {
this.moveDisabled = true; moveDisabled.value = true;
clearTimeout(this.disabledTimer); if (disabledTimer.value) clearTimeout(disabledTimer.value);
this.disabledTimer = setTimeout( disabledTimer.value = window.setTimeout(
() => (this.moveDisabled = false), () => (moveDisabled.value = false),
this.moveDisabledTime props.moveDisabledTime
); );
let p1 = event.targetTouches[0]; let p1 = event.targetTouches[0];
@ -219,55 +244,59 @@ export default {
let touchDistance = Math.sqrt( let touchDistance = Math.sqrt(
Math.pow(p2.pageX - p1.pageX, 2) + Math.pow(p2.pageY - p1.pageY, 2) Math.pow(p2.pageX - p1.pageX, 2) + Math.pow(p2.pageY - p1.pageY, 2)
); );
if (!this.lastTouchDistance) { if (!lastTouchDistance.value) {
this.lastTouchDistance = touchDistance; lastTouchDistance.value = touchDistance;
return; return;
} }
this.scale += (touchDistance - this.lastTouchDistance) / step; scale.value += (touchDistance - lastTouchDistance.value) / step;
this.lastTouchDistance = touchDistance; lastTouchDistance.value = touchDistance;
this.setZoom(); setZoom();
} else if (event.targetTouches.length === 1) { } else if (event.targetTouches.length === 1) {
if (this.moveDisabled) return; if (moveDisabled.value) return;
let x = event.targetTouches[0].pageX - this.lastX; let x = event.targetTouches[0].pageX - (lastX.value ?? 0);
let y = event.targetTouches[0].pageY - this.lastY; let y = event.targetTouches[0].pageY - (lastY.value ?? 0);
if (Math.abs(x) >= step && Math.abs(y) >= step) return; if (Math.abs(x) >= step && Math.abs(y) >= step) return;
this.lastX = event.targetTouches[0].pageX; lastX.value = event.targetTouches[0].pageX;
this.lastY = event.targetTouches[0].pageY; lastY.value = event.targetTouches[0].pageY;
this.doMove(x, y); doMove(x, y);
} }
}, };
doMove(x, y) {
let style = this.$refs.imgex.style; const doMove = (x: number, y: number) => {
let posX = this.pxStringToNumber(style.left) + x; if (imgex.value === null) {
let posY = this.pxStringToNumber(style.top) + y; return;
}
const style = imgex.value.style;
let posX = pxStringToNumber(style.left) + x;
let posY = pxStringToNumber(style.top) + y;
style.left = posX + "px"; style.left = posX + "px";
style.top = posY + "px"; style.top = posY + "px";
this.position.relative.x = Math.abs(this.position.center.x - posX); position.value.relative.x = Math.abs(position.value.center.x - posX);
this.position.relative.y = Math.abs(this.position.center.y - posY); position.value.relative.y = Math.abs(position.value.center.y - posY);
if (posX < this.position.center.x) { if (posX < position.value.center.x) {
this.position.relative.x = this.position.relative.x * -1; position.value.relative.x = position.value.relative.x * -1;
} }
if (posY < this.position.center.y) { if (posY < position.value.center.y) {
this.position.relative.y = this.position.relative.y * -1; position.value.relative.y = position.value.relative.y * -1;
} }
}, };
wheelMove(event) { const wheelMove = (event: WheelEvent) => {
this.scale += -Math.sign(event.deltaY) * this.zoomStep; scale.value += -Math.sign(event.deltaY) * props.zoomStep;
this.setZoom(); setZoom();
}, };
setZoom() { const setZoom = () => {
this.scale = this.scale < this.minScale ? this.minScale : this.scale; scale.value = scale.value < minScale.value ? minScale.value : scale.value;
this.scale = this.scale > this.maxScale ? this.maxScale : this.scale; scale.value = scale.value > maxScale.value ? maxScale.value : scale.value;
this.$refs.imgex.style.transform = `scale(${this.scale})`; if (imgex.value !== null)
}, imgex.value.style.transform = `scale(${scale.value})`;
pxStringToNumber(style) { };
const pxStringToNumber = (style: string) => {
return +style.replace("px", ""); return +style.replace("px", "");
},
},
}; };
</script> </script>
<style> <style>

View File

@ -15,7 +15,7 @@
> >
<div> <div>
<img <img
v-if="readOnly == undefined && type === 'image' && isThumbsEnabled" v-if="!readOnly && type === 'image' && isThumbsEnabled"
v-lazy="thumbnailUrl" v-lazy="thumbnailUrl"
/> />
<i v-else class="material-icons"></i> <i v-else class="material-icons"></i>
@ -34,137 +34,153 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { useAuthStore } from "@/stores/auth";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { enableThumbs } from "@/utils/constants"; import { enableThumbs } from "@/utils/constants";
import { mapMutations, mapGetters, mapState } from "vuex";
import { filesize } from "@/utils"; import { filesize } from "@/utils";
import moment from "moment/min/moment-with-locales"; import dayjs from "dayjs";
import { files as api } from "@/api"; import { files as api } from "@/api";
import * as upload from "@/utils/upload"; import * as upload from "@/utils/upload";
import { computed, inject, ref } from "vue";
import { useRouter } from "vue-router";
export default { const touches = ref<number>(0);
name: "item",
data: function () {
return {
touches: 0,
};
},
props: [
"name",
"isDir",
"url",
"type",
"size",
"modified",
"index",
"readOnly",
"path",
],
computed: {
...mapState(["user", "selected", "req", "jwt"]),
...mapGetters(["selectedCount"]),
singleClick() {
return this.readOnly == undefined && this.user.singleClick;
},
isSelected() {
return this.selected.indexOf(this.index) !== -1;
},
isDraggable() {
return this.readOnly == undefined && this.user.perm.rename;
},
canDrop() {
if (!this.isDir || this.readOnly !== undefined) return false;
for (let i of this.selected) { const $showError = inject<IToastError>("$showError")!;
if (this.req.items[i].url === this.url) { const router = useRouter();
const props = defineProps<{
name: string;
isDir: boolean;
url: string;
type: string;
size: number;
modified: string;
index: number;
readOnly?: boolean;
path?: string;
}>();
const authStore = useAuthStore();
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
const singleClick = computed(
() => !props.readOnly && authStore.user?.singleClick
);
const isSelected = computed(
() => fileStore.selected.indexOf(props.index) !== -1
);
const isDraggable = computed(
() => !props.readOnly && authStore.user?.perm.rename
);
const canDrop = computed(() => {
if (!props.isDir || props.readOnly) return false;
for (let i of fileStore.selected) {
if (fileStore.req?.items[i].url === props.url) {
return false; return false;
} }
} }
return true; return true;
}, });
thumbnailUrl() {
const thumbnailUrl = computed(() => {
const file = { const file = {
path: this.path, path: props.path,
modified: this.modified, modified: props.modified,
}; };
return api.getPreviewURL(file, "thumb"); return api.getPreviewURL(file as Resource, "thumb");
}, });
isThumbsEnabled() {
const isThumbsEnabled = computed(() => {
return enableThumbs; return enableThumbs;
}, });
},
methods: { const humanSize = () => {
...mapMutations(["addSelected", "removeSelected", "resetSelected"]), return props.type == "invalid_link" ? "invalid link" : filesize(props.size);
humanSize: function () { };
return this.type == "invalid_link" ? "invalid link" : filesize(this.size);
}, const humanTime = () => {
humanTime: function () { if (!props.readOnly && authStore.user?.dateFormat) {
if (this.readOnly == undefined && this.user.dateFormat) { return dayjs(props.modified).format("L LT");
return moment(this.modified).format("L LT");
} }
return moment(this.modified).fromNow(); return dayjs(props.modified).fromNow();
}, };
dragStart: function () {
if (this.selectedCount === 0) { const dragStart = () => {
this.addSelected(this.index); if (fileStore.selectedCount === 0) {
fileStore.selected.push(props.index);
return; return;
} }
if (!this.isSelected) { if (!isSelected.value) {
this.resetSelected(); fileStore.selected = [];
this.addSelected(this.index); fileStore.selected.push(props.index);
} }
}, };
dragOver: function (event) {
if (!this.canDrop) return; const dragOver = (event: Event) => {
if (!canDrop.value) return;
event.preventDefault(); event.preventDefault();
let el = event.target; let el = event.target as HTMLElement | null;
if (el !== null) {
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
if (!el.classList.contains("item")) { if (!el?.classList.contains("item")) {
el = el.parentElement; el = el?.parentElement ?? null;
} }
} }
el.style.opacity = 1; if (el !== null) el.style.opacity = "1";
}, }
drop: async function (event) { };
if (!this.canDrop) return;
const drop = async (event: Event) => {
if (!canDrop.value) return;
event.preventDefault(); event.preventDefault();
if (this.selectedCount === 0) return; if (fileStore.selectedCount === 0) return;
let el = event.target; let el = event.target as HTMLElement | null;
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
if (el !== null && !el.classList.contains("item")) { if (el !== null && !el.classList.contains("item")) {
el = el.parentElement; el = el.parentElement;
} }
} }
let items = []; let items: any[] = [];
for (let i of this.selected) { for (let i of fileStore.selected) {
if (fileStore.req) {
items.push({ items.push({
from: this.req.items[i].url, from: fileStore.req?.items[i].url,
to: this.url + encodeURIComponent(this.req.items[i].name), to: props.url + encodeURIComponent(fileStore.req?.items[i].name),
name: this.req.items[i].name, name: fileStore.req?.items[i].name,
}); });
} }
}
// Get url from ListingItem instance // Get url from ListingItem instance
if (el === null) {
return;
}
let path = el.__vue__.url; let path = el.__vue__.url;
let baseItems = (await api.fetch(path)).items; let baseItems = (await api.fetch(path)).items;
let action = (overwrite, rename) => { let action = (overwrite: boolean, rename: boolean) => {
api api
.move(items, overwrite, rename) .move(items, overwrite, rename)
.then(() => { .then(() => {
this.$store.commit("setReload", true); fileStore.reload = true;
}) })
.catch(this.$showError); .catch($showError);
}; };
let conflict = upload.checkConflict(items, baseItems); let conflict = upload.checkConflict(items, baseItems);
@ -173,14 +189,14 @@ export default {
let rename = false; let rename = false;
if (conflict) { if (conflict) {
this.$store.commit("showHover", { layoutStore.showHover({
prompt: "replace-rename", prompt: "replace-rename",
confirm: (event, option) => { confirm: (event: Event, option: any) => {
overwrite = option == "overwrite"; overwrite = option == "overwrite";
rename = option == "rename"; rename = option == "rename";
event.preventDefault(); event.preventDefault();
this.$store.commit("closeHovers"); layoutStore.closeHovers();
action(overwrite, rename); action(overwrite, rename);
}, },
}); });
@ -189,48 +205,51 @@ export default {
} }
action(overwrite, rename); action(overwrite, rename);
}, };
itemClick: function (event) {
const itemClick = (event: Event | KeyboardEvent) => {
if ( if (
!(event.ctrlKey || event.metaKey) && !((event as KeyboardEvent).ctrlKey || (event as KeyboardEvent).metaKey) &&
this.singleClick && singleClick.value &&
!this.$store.state.multiple !fileStore.multiple
) )
this.open(); open();
else this.click(event); else click(event);
}, };
click: function (event) {
if (!this.singleClick && this.selectedCount !== 0) event.preventDefault(); const click = (event: Event | KeyboardEvent) => {
if (!singleClick.value && fileStore.selectedCount !== 0)
event.preventDefault();
setTimeout(() => { setTimeout(() => {
this.touches = 0; touches.value = 0;
}, 300); }, 300);
this.touches++; touches.value++;
if (this.touches > 1) { if (touches.value > 1) {
this.open(); open();
} }
if (this.$store.state.selected.indexOf(this.index) !== -1) { if (fileStore.selected.indexOf(props.index) !== -1) {
this.removeSelected(this.index); fileStore.removeSelected(props.index);
return; return;
} }
if (event.shiftKey && this.selected.length > 0) { if ((event as KeyboardEvent).shiftKey && fileStore.selected.length > 0) {
let fi = 0; let fi = 0;
let la = 0; let la = 0;
if (this.index > this.selected[0]) { if (props.index > fileStore.selected[0]) {
fi = this.selected[0] + 1; fi = fileStore.selected[0] + 1;
la = this.index; la = props.index;
} else { } else {
fi = this.index; fi = props.index;
la = this.selected[0] - 1; la = fileStore.selected[0] - 1;
} }
for (; fi <= la; fi++) { for (; fi <= la; fi++) {
if (this.$store.state.selected.indexOf(fi) == -1) { if (fileStore.selected.indexOf(fi) == -1) {
this.addSelected(fi); fileStore.selected.push(fi);
} }
} }
@ -238,17 +257,17 @@ export default {
} }
if ( if (
!this.singleClick && !singleClick.value &&
!event.ctrlKey && !(event as KeyboardEvent).ctrlKey &&
!event.metaKey && !(event as KeyboardEvent).metaKey &&
!this.$store.state.multiple !fileStore.multiple
) ) {
this.resetSelected(); fileStore.selected = [];
this.addSelected(this.index); }
}, fileStore.selected.push(props.index);
open: function () { };
this.$router.push({ path: this.url });
}, const open = () => {
}, router.push({ path: props.url });
}; };
</script> </script>

View File

@ -0,0 +1,104 @@
<template>
<video ref="videoPlayer" class="video-max video-js" controls>
<source :src="source" />
<track
kind="subtitles"
v-for="(sub, index) in subtitles"
:key="index"
:src="sub"
:label="subLabel(sub)"
:default="index === 0"
/>
<p class="vjs-no-js">
Sorry, your browser doesn't support embedded videos, but don't worry, you
can <a :href="source">download it</a>
and watch it with your favorite video player!
</p>
</video>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from "vue";
import videojs from "video.js";
import type Player from "video.js/dist/types/player";
import "videojs-mobile-ui";
import "videojs-hotkeys";
import "video.js/dist/video-js.min.css";
import "videojs-mobile-ui/dist/videojs-mobile-ui.css";
const videoPlayer = ref<HTMLElement | null>(null);
const player = ref<Player | null>(null);
const props = withDefaults(
defineProps<{
source: string;
subtitles?: string[];
options?: any;
}>(),
{
options: {},
}
);
onMounted(() => {
player.value = videojs(
videoPlayer.value!,
{
html5: {
// needed for customizable subtitles
// TODO: add to user settings
nativeTextTracks: false,
},
plugins: {
hotkeys: {
volumeStep: 0.1,
seekStep: 10,
enableModifiersForNumbers: false,
},
},
...props.options,
},
// onReady callback
async () => {
// player.value!.log("onPlayerReady", this);
}
);
// TODO: need to test on mobile
// @ts-ignore
player.value!.mobileUi();
});
onBeforeUnmount(() => {
if (player.value) {
player.value.dispose();
player.value = null;
}
});
const subLabel = (subUrl: string) => {
let url: URL;
try {
url = new URL(subUrl);
} catch (_) {
// treat it as a relative url
// we only need this for filename
url = new URL(subUrl, window.location.origin);
}
const label = decodeURIComponent(
url.pathname
.split("/")
.pop()!
.replace(/\.[^/.]+$/, "")
);
return label;
};
</script>
<style scoped>
.video-max {
width: 100%;
height: 100%;
}
</style>

View File

@ -2,24 +2,31 @@
<button @click="action" :aria-label="label" :title="label" class="action"> <button @click="action" :aria-label="label" :title="label" class="action">
<i class="material-icons">{{ icon }}</i> <i class="material-icons">{{ icon }}</i>
<span>{{ label }}</span> <span>{{ label }}</span>
<span v-if="counter > 0" class="counter">{{ counter }}</span> <span v-if="counter && counter > 0" class="counter">{{ counter }}</span>
</button> </button>
</template> </template>
<script> <script setup lang="ts">
export default { import { useLayoutStore } from "@/stores/layout";
name: "action",
props: ["icon", "label", "counter", "show"], const props = defineProps<{
methods: { icon?: string;
action: function () { label?: string;
if (this.show) { counter?: number;
this.$store.commit("showHover", this.show); show?: string;
}>();
const emit = defineEmits<{
(e: "action"): any;
}>();
const layoutStore = useLayoutStore();
const action = () => {
if (props.show) {
layoutStore.showHover(props.show);
} }
this.$emit("action"); emit("action");
},
},
}; };
</script> </script>
<style></style>

View File

@ -1,62 +1,59 @@
<template> <template>
<header> <header>
<img v-if="showLogo !== undefined" :src="logoURL" /> <img v-if="showLogo" :src="logoURL" />
<action <Action
v-if="showMenu !== undefined" v-if="showMenu"
class="menu-button" class="menu-button"
icon="menu" icon="menu"
:label="$t('buttons.toggleSidebar')" :label="t('buttons.toggleSidebar')"
@action="openSidebar()" @action="layoutStore.showHover('sidebar')"
/> />
<slot /> <slot />
<div id="dropdown" :class="{ active: this.currentPromptName === 'more' }"> <div
id="dropdown"
:class="{ active: layoutStore.currentPromptName === 'more' }"
>
<slot name="actions" /> <slot name="actions" />
</div> </div>
<action <Action
v-if="this.$slots.actions" v-if="ifActionsSlot"
id="more" id="more"
icon="more_vert" icon="more_vert"
:label="$t('buttons.more')" :label="t('buttons.more')"
@action="$store.commit('showHover', 'more')" @action="layoutStore.showHover('more')"
/> />
<div <div
class="overlay" class="overlay"
v-show="this.currentPromptName == 'more'" v-show="layoutStore.currentPromptName == 'more'"
@click="$store.commit('closeHovers')" @click="layoutStore.closeHovers"
/> />
</header> </header>
</template> </template>
<script> <script setup lang="ts">
import { useLayoutStore } from "@/stores/layout";
import { logoURL } from "@/utils/constants"; import { logoURL } from "@/utils/constants";
import Action from "@/components/header/Action.vue"; import Action from "@/components/header/Action.vue";
import { mapGetters } from "vuex"; import { computed, useSlots } from "vue";
import { useI18n } from "vue-i18n";
export default { defineProps<{
name: "header-bar", showLogo?: boolean;
props: ["showLogo", "showMenu"], showMenu?: boolean;
components: { }>();
Action,
}, const layoutStore = useLayoutStore();
data: function () { const slots = useSlots();
return {
logoURL, const { t } = useI18n();
};
}, const ifActionsSlot = computed(() => (slots.actions ? true : false));
methods: {
openSidebar() {
this.$store.commit("showHover", "sidebar");
},
},
computed: {
...mapGetters(["currentPromptName"]),
},
};
</script> </script>
<style></style> <style></style>

View File

@ -0,0 +1,21 @@
<template>
<VueFinalModal
overlay-transition="vfm-fade"
content-transition="vfm-fade"
@closed="layoutStore.closeHovers"
:focus-trap="{
initialFocus: '#focus-prompt',
fallbackFocus: 'div.vfm__content',
}"
style="z-index: 9999999"
>
<slot />
</VueFinalModal>
</template>
<script setup lang="ts">
import { VueFinalModal } from "vue-final-modal";
import { useLayoutStore } from "@/stores/layout";
const layoutStore = useLayoutStore();
</script>

View File

@ -6,8 +6,11 @@
<div class="card-content"> <div class="card-content">
<p>{{ $t("prompts.copyMessage") }}</p> <p>{{ $t("prompts.copyMessage") }}</p>
<file-list ref="fileList" @update:selected="(val) => (dest = val)"> <file-list
</file-list> ref="fileList"
@update:selected="(val) => (dest = val)"
tabindex="1"
/>
</div> </div>
<div <div
@ -28,17 +31,20 @@
<div> <div>
<button <button
class="button button--flat button--grey" class="button button--flat button--grey"
@click="$store.commit('closeHovers')" @click="closeHovers"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
tabindex="3"
> >
{{ $t("buttons.cancel") }} {{ $t("buttons.cancel") }}
</button> </button>
<button <button
id="focus-prompt"
class="button button--flat" class="button button--flat"
@click="copy" @click="copy"
:aria-label="$t('buttons.copy')" :aria-label="$t('buttons.copy')"
:title="$t('buttons.copy')" :title="$t('buttons.copy')"
tabindex="2"
> >
{{ $t("buttons.copy") }} {{ $t("buttons.copy") }}
</button> </button>
@ -48,7 +54,10 @@
</template> </template>
<script> <script>
import { mapState } from "vuex"; import { mapActions, mapState, mapWritableState } from "pinia";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { useAuthStore } from "@/stores/auth";
import FileList from "./FileList.vue"; import FileList from "./FileList.vue";
import { files as api } from "@/api"; import { files as api } from "@/api";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
@ -63,8 +72,14 @@ export default {
dest: null, dest: null,
}; };
}, },
computed: mapState(["req", "selected", "user"]), inject: ["$showError"],
computed: {
...mapState(useFileStore, ["req", "selected"]),
...mapState(useAuthStore, ["user"]),
...mapWritableState(useFileStore, ["reload"]),
},
methods: { methods: {
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
copy: async function (event) { copy: async function (event) {
event.preventDefault(); event.preventDefault();
let items = []; let items = [];
@ -87,7 +102,7 @@ export default {
buttons.success("copy"); buttons.success("copy");
if (this.$route.path === this.dest) { if (this.$route.path === this.dest) {
this.$store.commit("setReload", true); this.reload = true;
return; return;
} }
@ -101,7 +116,7 @@ export default {
}; };
if (this.$route.path === this.dest) { if (this.$route.path === this.dest) {
this.$store.commit("closeHovers"); this.closeHovers();
action(false, true); action(false, true);
return; return;
@ -114,14 +129,14 @@ export default {
let rename = false; let rename = false;
if (conflict) { if (conflict) {
this.$store.commit("showHover", { this.showHover({
prompt: "replace-rename", prompt: "replace-rename",
confirm: (event, option) => { confirm: (event, option) => {
overwrite = option == "overwrite"; overwrite = option == "overwrite";
rename = option == "rename"; rename = option == "rename";
event.preventDefault(); event.preventDefault();
this.$store.commit("closeHovers"); this.closeHovers();
action(overwrite, rename); action(overwrite, rename);
}, },
}); });

View File

@ -10,18 +10,21 @@
</div> </div>
<div class="card-action"> <div class="card-action">
<button <button
@click="$store.commit('closeHovers')" @click="closeHovers"
class="button button--flat button--grey" class="button button--flat button--grey"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
tabindex="2"
> >
{{ $t("buttons.cancel") }} {{ $t("buttons.cancel") }}
</button> </button>
<button <button
id="focus-prompt"
@click="submit" @click="submit"
class="button button--flat button--red" class="button button--flat button--red"
:aria-label="$t('buttons.delete')" :aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')" :title="$t('buttons.delete')"
tabindex="1"
> >
{{ $t("buttons.delete") }} {{ $t("buttons.delete") }}
</button> </button>
@ -30,18 +33,27 @@
</template> </template>
<script> <script>
import { mapGetters, mapMutations, mapState } from "vuex"; import { mapActions, mapState, mapWritableState } from "pinia";
import { files as api } from "@/api"; import { files as api } from "@/api";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
export default { export default {
name: "delete", name: "delete",
inject: ["$showError"],
computed: { computed: {
...mapGetters(["isListing", "selectedCount", "currentPrompt"]), ...mapState(useFileStore, [
...mapState(["req", "selected"]), "isListing",
"selectedCount",
"req",
"selected",
"currentPrompt",
]),
...mapWritableState(useFileStore, ["reload"]),
}, },
methods: { methods: {
...mapMutations(["closeHovers"]), ...mapActions(useLayoutStore, ["closeHovers"]),
submit: async function () { submit: async function () {
buttons.loading("delete"); buttons.loading("delete");
@ -69,11 +81,11 @@ export default {
await Promise.all(promises); await Promise.all(promises);
buttons.success("delete"); buttons.success("delete");
this.$store.commit("setReload", true); this.reload = true;
} catch (e) { } catch (e) {
buttons.done("delete"); buttons.done("delete");
this.$showError(e); this.$showError(e);
if (this.isListing) this.$store.commit("setReload", true); if (this.isListing) this.reload = true;
} }
}, },
}, },

View File

@ -0,0 +1,40 @@
<template>
<div class="card floating">
<div class="card-content">
<p>{{ t("prompts.deleteUser") }}</p>
</div>
<div class="card-action">
<button
id="focus-prompt"
class="button button--flat button--grey"
@click="layoutStore.closeHovers"
:aria-label="t('buttons.cancel')"
:title="t('buttons.cancel')"
tabindex="1"
>
{{ t("buttons.cancel") }}
</button>
<button
class="button button--flat"
@click="layoutStore.currentPrompt?.confirm()"
tabindex="2"
>
{{ t("buttons.delete") }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { useLayoutStore } from "@/stores/layout";
import { useI18n } from "vue-i18n";
const layoutStore = useLayoutStore();
const { t } = useI18n();
// const emit = defineEmits<{
// (e: "confirm"): void;
// }>();
</script>

View File

@ -7,18 +7,21 @@
</div> </div>
<div class="card-action"> <div class="card-action">
<button <button
@click="$store.commit('closeHovers')"
class="button button--flat button--grey" class="button button--flat button--grey"
@click="closeHovers"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
tabindex="2"
> >
{{ $t("buttons.cancel") }} {{ $t("buttons.cancel") }}
</button> </button>
<button <button
id="focus-prompt"
@click="submit" @click="submit"
class="button button--flat button--red" class="button button--flat button--red"
:aria-label="$t('buttons.discardChanges')" :aria-label="$t('buttons.discardChanges')"
:title="$t('buttons.discardChanges')" :title="$t('buttons.discardChanges')"
tabindex="1"
> >
{{ $t("buttons.discardChanges") }} {{ $t("buttons.discardChanges") }}
</button> </button>
@ -27,15 +30,18 @@
</template> </template>
<script> <script>
import { mapMutations } from "vuex"; import { mapActions } from "pinia";
import url from "@/utils/url"; import url from "@/utils/url";
import { useLayoutStore } from "@/stores/layout";
import { useFileStore } from "@/stores/file";
export default { export default {
name: "discardEditorChanges", name: "discardEditorChanges",
methods: { methods: {
...mapMutations(["closeHovers"]), ...mapActions(useLayoutStore, ["closeHovers"]),
...mapActions(useFileStore, ["updateRequest"]),
submit: async function () { submit: async function () {
this.$store.commit("updateRequest", {}); this.updateRequest(null);
let uri = url.removeLastDir(this.$route.path) + "/"; let uri = url.removeLastDir(this.$route.path) + "/";
this.$router.push({ path: uri }); this.$router.push({ path: uri });

View File

@ -1,18 +1,18 @@
<template> <template>
<div class="card floating" id="download"> <div class="card floating" id="download">
<div class="card-title"> <div class="card-title">
<h2>{{ $t("prompts.download") }}</h2> <h2>{{ t("prompts.download") }}</h2>
</div> </div>
<div class="card-content"> <div class="card-content">
<p>{{ $t("prompts.downloadMessage") }}</p> <p>{{ t("prompts.downloadMessage") }}</p>
<button <button
id="focus-prompt"
v-for="(ext, format) in formats" v-for="(ext, format) in formats"
:key="format" :key="format"
class="button button--block" class="button button--block"
@click="currentPrompt.confirm(format)" @click="layoutStore.currentPrompt?.confirm(format)"
v-focus
> >
{{ ext }} {{ ext }}
</button> </button>
@ -20,14 +20,15 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapGetters } from "vuex"; import { useI18n } from "vue-i18n";
import { useLayoutStore } from "@/stores/layout";
export default { const layoutStore = useLayoutStore();
name: "download",
data: function () { const { t } = useI18n();
return {
formats: { const formats = {
zip: "zip", zip: "zip",
tar: "tar", tar: "tar",
targz: "tar.gz", targz: "tar.gz",
@ -35,11 +36,5 @@ export default {
tarxz: "tar.xz", tarxz: "tar.xz",
tarlz4: "tar.lz4", tarlz4: "tar.lz4",
tarsz: "tar.sz", tarsz: "tar.sz",
},
};
},
computed: {
...mapGetters(["currentPrompt"]),
},
}; };
</script> </script>

View File

@ -25,7 +25,10 @@
</template> </template>
<script> <script>
import { mapState } from "vuex"; import { mapState } from "pinia";
import { useAuthStore } from "@/stores/auth";
import { useFileStore } from "@/stores/file";
import url from "@/utils/url"; import url from "@/utils/url";
import { files } from "@/api"; import { files } from "@/api";
@ -42,8 +45,10 @@ export default {
current: window.location.pathname, current: window.location.pathname,
}; };
}, },
inject: ["$showError"],
computed: { computed: {
...mapState(["req", "user"]), ...mapState(useAuthStore, ["user"]),
...mapState(useFileStore, ["req"]),
nav() { nav() {
return decodeURIComponent(this.current); return decodeURIComponent(this.current);
}, },

View File

@ -20,11 +20,13 @@
<div class="card-action"> <div class="card-action">
<button <button
id="focus-prompt"
type="submit" type="submit"
@click="$store.commit('closeHovers')" @click="closeHovers"
class="button button--flat" class="button button--flat"
:aria-label="$t('buttons.ok')" :aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')" :title="$t('buttons.ok')"
tabindex="1"
> >
{{ $t("buttons.ok") }} {{ $t("buttons.ok") }}
</button> </button>
@ -33,5 +35,13 @@
</template> </template>
<script> <script>
export default { name: "help" }; import { mapActions } from "pinia";
import { useLayoutStore } from "@/stores/layout";
export default {
name: "help",
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
};
</script> </script>

View File

@ -40,33 +40,45 @@
<p> <p>
<strong>MD5: </strong <strong>MD5: </strong
><code ><code
><a @click="checksum($event, 'md5')">{{ ><a
$t("prompts.show") @click="checksum($event, 'md5')"
}}</a></code @keypress.enter="checksum($event, 'md5')"
tabindex="2"
>{{ $t("prompts.show") }}</a
></code
> >
</p> </p>
<p> <p>
<strong>SHA1: </strong <strong>SHA1: </strong
><code ><code
><a @click="checksum($event, 'sha1')">{{ ><a
$t("prompts.show") @click="checksum($event, 'sha1')"
}}</a></code @keypress.enter="checksum($event, 'sha1')"
tabindex="3"
>{{ $t("prompts.show") }}</a
></code
> >
</p> </p>
<p> <p>
<strong>SHA256: </strong <strong>SHA256: </strong
><code ><code
><a @click="checksum($event, 'sha256')">{{ ><a
$t("prompts.show") @click="checksum($event, 'sha256')"
}}</a></code @keypress.enter="checksum($event, 'sha256')"
tabindex="4"
>{{ $t("prompts.show") }}</a
></code
> >
</p> </p>
<p> <p>
<strong>SHA512: </strong <strong>SHA512: </strong
><code ><code
><a @click="checksum($event, 'sha512')">{{ ><a
$t("prompts.show") @click="checksum($event, 'sha512')"
}}</a></code @keypress.enter="checksum($event, 'sha512')"
tabindex="5"
>{{ $t("prompts.show") }}</a
></code
> >
</p> </p>
</template> </template>
@ -74,8 +86,9 @@
<div class="card-action"> <div class="card-action">
<button <button
id="focus-prompt"
type="submit" type="submit"
@click="$store.commit('closeHovers')" @click="closeHovers"
class="button button--flat" class="button button--flat"
:aria-label="$t('buttons.ok')" :aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')" :title="$t('buttons.ok')"
@ -87,16 +100,23 @@
</template> </template>
<script> <script>
import { mapState, mapGetters } from "vuex"; import { mapActions, mapState } from "pinia";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { filesize } from "@/utils"; import { filesize } from "@/utils";
import moment from "moment/min/moment-with-locales"; import dayjs from "dayjs";
import { files as api } from "@/api"; import { files as api } from "@/api";
export default { export default {
name: "info", name: "info",
inject: ["$showError"],
computed: { computed: {
...mapState(["req", "selected"]), ...mapState(useFileStore, [
...mapGetters(["selectedCount", "isListing"]), "req",
"selected",
"selectedCount",
"isListing",
]),
humanSize: function () { humanSize: function () {
if (this.selectedCount === 0 || !this.isListing) { if (this.selectedCount === 0 || !this.isListing) {
return filesize(this.req.size); return filesize(this.req.size);
@ -112,13 +132,19 @@ export default {
}, },
humanTime: function () { humanTime: function () {
if (this.selectedCount === 0) { if (this.selectedCount === 0) {
return moment(this.req.modified).fromNow(); return dayjs(this.req.modified).fromNow();
} }
return moment(this.req.items[this.selected[0]].modified).fromNow(); return dayjs(this.req.items[this.selected[0]].modified).fromNow();
}, },
modTime: function () { modTime: function () {
if (this.selectedCount === 0) {
return new Date(Date.parse(this.req.modified)).toLocaleString(); return new Date(Date.parse(this.req.modified)).toLocaleString();
}
return new Date(
Date.parse(this.req.items[this.selected[0]].modified)
).toLocaleString();
}, },
name: function () { name: function () {
return this.selectedCount === 0 return this.selectedCount === 0
@ -146,6 +172,7 @@ export default {
}, },
}, },
methods: { methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
checksum: async function (event, algo) { checksum: async function (event, algo) {
event.preventDefault(); event.preventDefault();
@ -159,8 +186,7 @@ export default {
try { try {
const hash = await api.checksum(link, algo); const hash = await api.checksum(link, algo);
// eslint-disable-next-line event.target.textContent = hash;
event.target.innerHTML = hash;
} catch (e) { } catch (e) {
this.$showError(e); this.$showError(e);
} }

View File

@ -5,8 +5,11 @@
</div> </div>
<div class="card-content"> <div class="card-content">
<file-list ref="fileList" @update:selected="(val) => (dest = val)"> <file-list
</file-list> ref="fileList"
@update:selected="(val) => (dest = val)"
tabindex="1"
/>
</div> </div>
<div <div
@ -27,18 +30,21 @@
<div> <div>
<button <button
class="button button--flat button--grey" class="button button--flat button--grey"
@click="$store.commit('closeHovers')" @click="closeHovers"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
tabindex="3"
> >
{{ $t("buttons.cancel") }} {{ $t("buttons.cancel") }}
</button> </button>
<button <button
id="focus-prompt"
class="button button--flat" class="button button--flat"
@click="move" @click="move"
:disabled="$route.path === dest" :disabled="$route.path === dest"
:aria-label="$t('buttons.move')" :aria-label="$t('buttons.move')"
:title="$t('buttons.move')" :title="$t('buttons.move')"
tabindex="2"
> >
{{ $t("buttons.move") }} {{ $t("buttons.move") }}
</button> </button>
@ -48,7 +54,10 @@
</template> </template>
<script> <script>
import { mapState } from "vuex"; import { mapActions, mapState } from "pinia";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { useAuthStore } from "@/stores/auth";
import FileList from "./FileList.vue"; import FileList from "./FileList.vue";
import { files as api } from "@/api"; import { files as api } from "@/api";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
@ -63,8 +72,13 @@ export default {
dest: null, dest: null,
}; };
}, },
computed: mapState(["req", "selected", "user"]), inject: ["$showError"],
computed: {
...mapState(useFileStore, ["req", "selected"]),
...mapState(useAuthStore, ["user"]),
},
methods: { methods: {
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
move: async function (event) { move: async function (event) {
event.preventDefault(); event.preventDefault();
let items = []; let items = [];
@ -99,14 +113,14 @@ export default {
let rename = false; let rename = false;
if (conflict) { if (conflict) {
this.$store.commit("showHover", { this.showHover({
prompt: "replace-rename", prompt: "replace-rename",
confirm: (event, option) => { confirm: (event, option) => {
overwrite = option == "overwrite"; overwrite = option == "overwrite";
rename = option == "rename"; rename = option == "rename";
event.preventDefault(); event.preventDefault();
this.$store.commit("closeHovers"); this.closeHovers();
action(overwrite, rename); action(overwrite, rename);
}, },
}); });

View File

@ -1,98 +1,104 @@
<template> <template>
<div class="card floating"> <div class="card floating">
<div class="card-title"> <div class="card-title">
<h2>{{ $t("prompts.newDir") }}</h2> <h2>{{ t("prompts.newDir") }}</h2>
</div> </div>
<div class="card-content"> <div class="card-content">
<p>{{ $t("prompts.newDirMessage") }}</p> <p>{{ t("prompts.newDirMessage") }}</p>
<input <input
id="focus-prompt"
class="input input--block" class="input input--block"
type="text" type="text"
@keyup.enter="submit" @keyup.enter="submit"
v-model.trim="name" v-model.trim="name"
v-focus tabindex="1"
/> />
</div> </div>
<div class="card-action"> <div class="card-action">
<button <button
class="button button--flat button--grey" class="button button--flat button--grey"
@click="$store.commit('closeHovers')" @click="layoutStore.closeHovers"
:aria-label="$t('buttons.cancel')" :aria-label="t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="t('buttons.cancel')"
tabindex="3"
> >
{{ $t("buttons.cancel") }} {{ t("buttons.cancel") }}
</button> </button>
<button <button
class="button button--flat" class="button button--flat"
:aria-label="$t('buttons.create')" :aria-label="$t('buttons.create')"
:title="$t('buttons.create')" :title="t('buttons.create')"
@click="submit" @click="submit"
tabindex="2"
> >
{{ $t("buttons.create") }} {{ t("buttons.create") }}
</button> </button>
</div> </div>
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapGetters } from "vuex"; import { inject, ref } from "vue";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { files as api } from "@/api"; import { files as api } from "@/api";
import url from "@/utils/url"; import url from "@/utils/url";
import { useRoute, useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
export default { const $showError = inject<IToastError>("$showError")!;
name: "new-dir",
props: { const props = defineProps({
base: String,
redirect: { redirect: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
base: { });
type: [String, null],
default: null, const fileStore = useFileStore();
}, const layoutStore = useLayoutStore();
},
data: function () { const route = useRoute();
return { const router = useRouter();
name: "", const { t } = useI18n();
};
}, const name = ref<string>("");
computed: {
...mapGetters(["isFiles", "isListing"]), const submit = async (event: Event) => {
},
methods: {
submit: async function (event) {
event.preventDefault(); event.preventDefault();
if (this.new === "") return; if (name.value === "") return;
// Build the path of the new directory. // Build the path of the new directory.
let uri; let uri: string;
if (props.base) uri = props.base;
if (this.base) uri = this.base; else if (fileStore.isFiles) uri = route.path + "/";
else if (this.isFiles) uri = this.$route.path + "/";
else uri = "/"; else uri = "/";
if (!this.isListing) { if (!fileStore.isListing) {
uri = url.removeLastDir(uri) + "/"; uri = url.removeLastDir(uri) + "/";
} }
uri += encodeURIComponent(this.name) + "/"; uri += encodeURIComponent(name.value) + "/";
uri = uri.replace("//", "/"); uri = uri.replace("//", "/");
try { try {
await api.post(uri); await api.post(uri);
if (this.redirect) { if (props.redirect) {
this.$router.push({ path: uri }); router.push({ path: uri });
} else if (!this.base) { } else if (!props.base) {
const res = await api.fetch(url.removeLastDir(uri) + "/"); const res = await api.fetch(url.removeLastDir(uri) + "/");
this.$store.commit("updateRequest", res); fileStore.updateRequest(res);
} }
} catch (e) { } catch (e) {
this.$showError(e); if (e instanceof Error) {
$showError(e);
}
} }
this.$store.commit("closeHovers"); layoutStore.closeHovers();
},
},
}; };
</script> </script>

View File

@ -1,14 +1,14 @@
<template> <template>
<div class="card floating"> <div class="card floating">
<div class="card-title"> <div class="card-title">
<h2>{{ $t("prompts.newFile") }}</h2> <h2>{{ t("prompts.newFile") }}</h2>
</div> </div>
<div class="card-content"> <div class="card-content">
<p>{{ $t("prompts.newFileMessage") }}</p> <p>{{ t("prompts.newFileMessage") }}</p>
<input <input
id="focus-prompt"
class="input input--block" class="input input--block"
v-focus
type="text" type="text"
@keyup.enter="submit" @keyup.enter="submit"
v-model.trim="name" v-model.trim="name"
@ -18,63 +18,68 @@
<div class="card-action"> <div class="card-action">
<button <button
class="button button--flat button--grey" class="button button--flat button--grey"
@click="$store.commit('closeHovers')" @click="layoutStore.closeHovers"
:aria-label="$t('buttons.cancel')" :aria-label="t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="t('buttons.cancel')"
> >
{{ $t("buttons.cancel") }} {{ t("buttons.cancel") }}
</button> </button>
<button <button
class="button button--flat" class="button button--flat"
@click="submit" @click="submit"
:aria-label="$t('buttons.create')" :aria-label="t('buttons.create')"
:title="$t('buttons.create')" :title="t('buttons.create')"
> >
{{ $t("buttons.create") }} {{ t("buttons.create") }}
</button> </button>
</div> </div>
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapGetters } from "vuex"; import { inject, ref } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute, useRouter } from "vue-router";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { files as api } from "@/api"; import { files as api } from "@/api";
import url from "@/utils/url"; import url from "@/utils/url";
export default { const $showError = inject<IToastError>("$showError")!;
name: "new-file",
data: function () { const fileStore = useFileStore();
return { const layoutStore = useLayoutStore();
name: "",
}; const route = useRoute();
}, const router = useRouter();
computed: { const { t } = useI18n();
...mapGetters(["isFiles", "isListing"]),
}, const name = ref<string>("");
methods: {
submit: async function (event) { const submit = async (event: Event) => {
event.preventDefault(); event.preventDefault();
if (this.new === "") return; if (name.value === "") return;
// Build the path of the new directory. // Build the path of the new directory.
let uri = this.isFiles ? this.$route.path + "/" : "/"; let uri = fileStore.isFiles ? route.path + "/" : "/";
if (!this.isListing) { if (!fileStore.isListing) {
uri = url.removeLastDir(uri) + "/"; uri = url.removeLastDir(uri) + "/";
} }
uri += encodeURIComponent(this.name); uri += encodeURIComponent(name.value);
uri = uri.replace("//", "/"); uri = uri.replace("//", "/");
try { try {
await api.post(uri); await api.post(uri);
this.$router.push({ path: uri }); router.push({ path: uri });
} catch (e) { } catch (e) {
this.$showError(e); if (e instanceof Error) {
$showError(e);
}
} }
this.$store.commit("closeHovers"); layoutStore.closeHovers();
},
},
}; };
</script> </script>

View File

@ -1,22 +1,20 @@
<template> <template>
<div> <ModalsContainer />
<component
v-if="showOverlay"
:ref="currentPromptName"
:is="currentPromptName"
v-bind="currentPrompt.props"
>
</component>
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
</div>
</template> </template>
<script> <script setup lang="ts">
import { ref, watch } from "vue";
import { ModalsContainer, useModal } from "vue-final-modal";
import { storeToRefs } from "pinia";
import { useLayoutStore } from "@/stores/layout";
import BaseModal from "./BaseModal.vue";
import Help from "./Help.vue"; import Help from "./Help.vue";
import Info from "./Info.vue"; import Info from "./Info.vue";
import Delete from "./Delete.vue"; import Delete from "./Delete.vue";
import Rename from "./Rename.vue"; import DeleteUser from "./DeleteUser.vue";
import Download from "./Download.vue"; import Download from "./Download.vue";
import Rename from "./Rename.vue";
import Move from "./Move.vue"; import Move from "./Move.vue";
import Copy from "./Copy.vue"; import Copy from "./Copy.vue";
import NewFile from "./NewFile.vue"; import NewFile from "./NewFile.vue";
@ -24,87 +22,61 @@ import NewDir from "./NewDir.vue";
import Replace from "./Replace.vue"; import Replace from "./Replace.vue";
import ReplaceRename from "./ReplaceRename.vue"; import ReplaceRename from "./ReplaceRename.vue";
import Share from "./Share.vue"; import Share from "./Share.vue";
import Upload from "./Upload.vue";
import ShareDelete from "./ShareDelete.vue"; import ShareDelete from "./ShareDelete.vue";
import Sidebar from "../Sidebar.vue"; import Upload from "./Upload.vue";
import DiscardEditorChanges from "./DiscardEditorChanges.vue"; import DiscardEditorChanges from "./DiscardEditorChanges.vue";
import { mapGetters, mapState } from "vuex";
import buttons from "@/utils/buttons";
export default { const layoutStore = useLayoutStore();
name: "prompts",
components: { const { currentPromptName } = storeToRefs(layoutStore);
Info,
Delete, const closeModal = ref<() => Promise<string>>();
Rename,
Download, const components = new Map<string, any>([
Move, ["info", Info],
Copy, ["help", Help],
Share, ["delete", Delete],
NewFile, ["rename", Rename],
NewDir, ["move", Move],
Help, ["copy", Copy],
Replace, ["newFile", NewFile],
ReplaceRename, ["newDir", NewDir],
Upload, ["download", Download],
ShareDelete, ["replace", Replace],
Sidebar, ["replace-rename", ReplaceRename],
DiscardEditorChanges, ["share", Share],
["upload", Upload],
["share-delete", ShareDelete],
["deleteUser", DeleteUser],
["discardEditorChanges", DiscardEditorChanges],
]);
watch(currentPromptName, (newValue) => {
if (closeModal.value) {
closeModal.value();
closeModal.value = undefined;
}
const modal = components.get(newValue!);
if (!modal) return;
const { open, close } = useModal({
component: BaseModal,
slots: {
default: modal,
}, },
data: function () { });
return {
pluginData: { closeModal.value = close;
buttons, open();
store: this.$store, });
router: this.$router,
},
};
},
created() {
window.addEventListener("keydown", (event) => { window.addEventListener("keydown", (event) => {
if (this.currentPrompt == null) return; if (!layoutStore.currentPrompt) return;
const promptName = this.currentPrompt.prompt; if (event.key === "Escape") {
const prompt = this.$refs[promptName];
if (event.code === "Escape") {
event.stopImmediatePropagation(); event.stopImmediatePropagation();
this.$store.commit("closeHovers"); layoutStore.closeHovers();
}
if (event.code === "Enter") {
switch (promptName) {
case "delete":
prompt.submit();
break;
case "copy":
prompt.copy(event);
break;
case "move":
prompt.move(event);
break;
case "replace":
prompt.showConfirm(event);
break;
}
} }
}); });
},
computed: {
...mapState(["plugins"]),
...mapGetters(["currentPrompt", "currentPromptName"]),
showOverlay: function () {
return (
this.currentPrompt !== null &&
this.currentPrompt.prompt !== "search" &&
this.currentPrompt.prompt !== "more"
);
},
},
methods: {
resetPrompts() {
this.$store.commit("closeHovers");
},
},
};
</script> </script>

View File

@ -10,8 +10,8 @@
>: >:
</p> </p>
<input <input
id="focus-prompt"
class="input input--block" class="input input--block"
v-focus
type="text" type="text"
@keyup.enter="submit" @keyup.enter="submit"
v-model.trim="name" v-model.trim="name"
@ -21,7 +21,7 @@
<div class="card-action"> <div class="card-action">
<button <button
class="button button--flat button--grey" class="button button--flat button--grey"
@click="$store.commit('closeHovers')" @click="closeHovers"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
> >
@ -41,7 +41,9 @@
</template> </template>
<script> <script>
import { mapState, mapGetters } from "vuex"; import { mapActions, mapState, mapWritableState } from "pinia";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import url from "@/utils/url"; import url from "@/utils/url";
import { files as api } from "@/api"; import { files as api } from "@/api";
@ -55,13 +57,20 @@ export default {
created() { created() {
this.name = this.oldName(); this.name = this.oldName();
}, },
inject: ["$showError"],
computed: { computed: {
...mapState(["req", "selected", "selectedCount"]), ...mapState(useFileStore, [
...mapGetters(["isListing"]), "req",
"selected",
"selectedCount",
"isListing",
]),
...mapWritableState(useFileStore, ["reload"]),
}, },
methods: { methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
cancel: function () { cancel: function () {
this.$store.commit("closeHovers"); this.closeHovers();
}, },
oldName: function () { oldName: function () {
if (!this.isListing) { if (!this.isListing) {
@ -96,12 +105,12 @@ export default {
return; return;
} }
this.$store.commit("setReload", true); this.reload = true;
} catch (e) { } catch (e) {
this.$showError(e); this.$showError(e);
} }
this.$store.commit("closeHovers"); this.closeHovers();
}, },
}, },
}; };

View File

@ -11,9 +11,10 @@
<div class="card-action"> <div class="card-action">
<button <button
class="button button--flat button--grey" class="button button--flat button--grey"
@click="$store.commit('closeHovers')" @click="closeHovers"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
tabindex="3"
> >
{{ $t("buttons.cancel") }} {{ $t("buttons.cancel") }}
</button> </button>
@ -22,14 +23,17 @@
@click="currentPrompt.action" @click="currentPrompt.action"
:aria-label="$t('buttons.continue')" :aria-label="$t('buttons.continue')"
:title="$t('buttons.continue')" :title="$t('buttons.continue')"
tabindex="2"
> >
{{ $t("buttons.continue") }} {{ $t("buttons.continue") }}
</button> </button>
<button <button
id="focus-prompt"
class="button button--flat button--red" class="button button--flat button--red"
@click="currentPrompt.confirm" @click="currentPrompt.confirm"
:aria-label="$t('buttons.replace')" :aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')" :title="$t('buttons.replace')"
tabindex="1"
> >
{{ $t("buttons.replace") }} {{ $t("buttons.replace") }}
</button> </button>
@ -38,10 +42,16 @@
</template> </template>
<script> <script>
import { mapGetters } from "vuex"; import { mapActions, mapState } from "pinia";
import { useLayoutStore } from "@/stores/layout";
export default { export default {
name: "replace", name: "replace",
computed: mapGetters(["currentPrompt"]), computed: {
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
}; };
</script> </script>

View File

@ -11,9 +11,10 @@
<div class="card-action"> <div class="card-action">
<button <button
class="button button--flat button--grey" class="button button--flat button--grey"
@click="$store.commit('closeHovers')" @click="closeHovers"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
tabindex="3"
> >
{{ $t("buttons.cancel") }} {{ $t("buttons.cancel") }}
</button> </button>
@ -22,14 +23,17 @@
@click="(event) => currentPrompt.confirm(event, 'rename')" @click="(event) => currentPrompt.confirm(event, 'rename')"
:aria-label="$t('buttons.rename')" :aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')" :title="$t('buttons.rename')"
tabindex="2"
> >
{{ $t("buttons.rename") }} {{ $t("buttons.rename") }}
</button> </button>
<button <button
id="focus-prompt"
class="button button--flat button--red" class="button button--flat button--red"
@click="(event) => currentPrompt.confirm(event, 'overwrite')" @click="(event) => currentPrompt.confirm(event, 'overwrite')"
:aria-label="$t('buttons.replace')" :aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')" :title="$t('buttons.replace')"
tabindex="1"
> >
{{ $t("buttons.replace") }} {{ $t("buttons.replace") }}
</button> </button>
@ -38,10 +42,16 @@
</template> </template>
<script> <script>
import { mapGetters } from "vuex"; import { mapActions, mapState } from "pinia";
import { useLayoutStore } from "@/stores/layout";
export default { export default {
name: "replace-rename", name: "replace-rename",
computed: mapGetters(["currentPrompt"]), computed: {
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
}; };
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="card floating share__promt__card" id="share"> <div class="card floating" id="share">
<div class="card-title"> <div class="card-title">
<h2>{{ $t("buttons.share") }}</h2> <h2>{{ $t("buttons.share") }}</h2>
</div> </div>
@ -25,9 +25,9 @@
<td class="small"> <td class="small">
<button <button
class="action copy-clipboard" class="action copy-clipboard"
:data-clipboard-text="buildLink(link)"
:aria-label="$t('buttons.copyToClipboard')" :aria-label="$t('buttons.copyToClipboard')"
:title="$t('buttons.copyToClipboard')" :title="$t('buttons.copyToClipboard')"
@click="copyToClipboard(buildLink(link))"
> >
<i class="material-icons">content_paste</i> <i class="material-icons">content_paste</i>
</button> </button>
@ -35,9 +35,9 @@
<td class="small" v-if="hasDownloadLink()"> <td class="small" v-if="hasDownloadLink()">
<button <button
class="action copy-clipboard" class="action copy-clipboard"
:data-clipboard-text="buildDownloadLink(link)"
:aria-label="$t('buttons.copyDownloadLinkToClipboard')" :aria-label="$t('buttons.copyDownloadLinkToClipboard')"
:title="$t('buttons.copyDownloadLinkToClipboard')" :title="$t('buttons.copyDownloadLinkToClipboard')"
@click="copyToClipboard(buildDownloadLink(link))"
> >
<i class="material-icons">content_paste_go</i> <i class="material-icons">content_paste_go</i>
</button> </button>
@ -59,17 +59,20 @@
<div class="card-action"> <div class="card-action">
<button <button
class="button button--flat button--grey" class="button button--flat button--grey"
@click="$store.commit('closeHovers')" @click="closeHovers"
:aria-label="$t('buttons.close')" :aria-label="$t('buttons.close')"
:title="$t('buttons.close')" :title="$t('buttons.close')"
tabindex="2"
> >
{{ $t("buttons.close") }} {{ $t("buttons.close") }}
</button> </button>
<button <button
id="focus-prompt"
class="button button--flat button--blue" class="button button--flat button--blue"
@click="() => switchListing()" @click="() => switchListing()"
:aria-label="$t('buttons.new')" :aria-label="$t('buttons.new')"
:title="$t('buttons.new')" :title="$t('buttons.new')"
tabindex="1"
> >
{{ $t("buttons.new") }} {{ $t("buttons.new") }}
</button> </button>
@ -80,15 +83,22 @@
<div class="card-content"> <div class="card-content">
<p>{{ $t("settings.shareDuration") }}</p> <p>{{ $t("settings.shareDuration") }}</p>
<div class="input-group input"> <div class="input-group input">
<input <vue-number-input
v-focus center
type="number" controls
max="2147483647" size="small"
min="1" :max="2147483647"
:min="0"
@keyup.enter="submit" @keyup.enter="submit"
v-model.trim="time" v-model="time"
tabindex="1"
/> />
<select class="right" v-model="unit" :aria-label="$t('time.unit')"> <select
class="right"
v-model="unit"
:aria-label="$t('time.unit')"
tabindex="2"
>
<option value="seconds">{{ $t("time.seconds") }}</option> <option value="seconds">{{ $t("time.seconds") }}</option>
<option value="minutes">{{ $t("time.minutes") }}</option> <option value="minutes">{{ $t("time.minutes") }}</option>
<option value="hours">{{ $t("time.hours") }}</option> <option value="hours">{{ $t("time.hours") }}</option>
@ -100,6 +110,7 @@
class="input input--block" class="input input--block"
type="password" type="password"
v-model.trim="password" v-model.trim="password"
tabindex="3"
/> />
</div> </div>
@ -109,14 +120,17 @@
@click="() => switchListing()" @click="() => switchListing()"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
tabindex="5"
> >
{{ $t("buttons.cancel") }} {{ $t("buttons.cancel") }}
</button> </button>
<button <button
id="focus-prompt"
class="button button--flat button--blue" class="button button--flat button--blue"
@click="submit" @click="submit"
:aria-label="$t('buttons.share')" :aria-label="$t('buttons.share')"
:title="$t('buttons.share')" :title="$t('buttons.share')"
tabindex="4"
> >
{{ $t("buttons.share") }} {{ $t("buttons.share") }}
</button> </button>
@ -126,16 +140,18 @@
</template> </template>
<script> <script>
import { mapState, mapGetters } from "vuex"; import { mapActions, mapState } from "pinia";
import { useFileStore } from "@/stores/file";
import { share as api, pub as pub_api } from "@/api"; import { share as api, pub as pub_api } from "@/api";
import moment from "moment/min/moment-with-locales"; import dayjs from "dayjs";
import Clipboard from "clipboard"; import { useLayoutStore } from "@/stores/layout";
import { copy } from "@/utils/clipboard";
export default { export default {
name: "share", name: "share",
data: function () { data: function () {
return { return {
time: "", time: 0,
unit: "hours", unit: "hours",
links: [], links: [],
clip: null, clip: null,
@ -143,9 +159,14 @@ export default {
listing: true, listing: true,
}; };
}, },
inject: ["$showError", "$showSuccess"],
computed: { computed: {
...mapState(["req", "selected", "selectedCount"]), ...mapState(useFileStore, [
...mapGetters(["isListing"]), "req",
"selected",
"selectedCount",
"isListing",
]),
url() { url() {
if (!this.isListing) { if (!this.isListing) {
return this.$route.path; return this.$route.path;
@ -172,23 +193,24 @@ export default {
this.$showError(e); this.$showError(e);
} }
}, },
mounted() {
this.clip = new Clipboard(".copy-clipboard");
this.clip.on("success", () => {
this.$showSuccess(this.$t("success.linkCopied"));
});
},
beforeDestroy() {
this.clip.destroy();
},
methods: { methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
copyToClipboard: function (text) {
copy(text).then(
() => {
// clipboard successfully set
this.$showSuccess(this.$t("success.linkCopied"));
},
() => {
// clipboard write failed
}
);
},
submit: async function () { submit: async function () {
let isPermanent = !this.time || this.time == 0;
try { try {
let res = null; let res = null;
if (isPermanent) { if (!this.time) {
res = await api.create(this.url, this.password); res = await api.create(this.url, this.password);
} else { } else {
res = await api.create(this.url, this.password, this.time, this.unit); res = await api.create(this.url, this.password, this.time, this.unit);
@ -197,7 +219,7 @@ export default {
this.links.push(res); this.links.push(res);
this.sort(); this.sort();
this.time = ""; this.time = 0;
this.unit = "hours"; this.unit = "hours";
this.password = ""; this.password = "";
@ -220,7 +242,7 @@ export default {
} }
}, },
humanTime(time) { humanTime(time) {
return moment(time * 1000).fromNow(); return dayjs(time * 1000).fromNow();
}, },
buildLink(share) { buildLink(share) {
return api.getShareURL(share); return api.getShareURL(share);
@ -242,7 +264,7 @@ export default {
}, },
switchListing() { switchListing() {
if (this.links.length == 0 && !this.listing) { if (this.links.length == 0 && !this.listing) {
this.$store.commit("closeHovers"); this.closeHovers();
} }
this.listing = !this.listing; this.listing = !this.listing;

View File

@ -5,18 +5,21 @@
</div> </div>
<div class="card-action"> <div class="card-action">
<button <button
@click="$store.commit('closeHovers')" @click="closeHovers"
class="button button--flat button--grey" class="button button--flat button--grey"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
tabindex="2"
> >
{{ $t("buttons.cancel") }} {{ $t("buttons.cancel") }}
</button> </button>
<button <button
id="focus-prompt"
@click="submit" @click="submit"
class="button button--flat button--red" class="button button--flat button--red"
:aria-label="$t('buttons.delete')" :aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')" :title="$t('buttons.delete')"
tabindex="1"
> >
{{ $t("buttons.delete") }} {{ $t("buttons.delete") }}
</button> </button>
@ -25,14 +28,16 @@
</template> </template>
<script> <script>
import { mapGetters } from "vuex"; import { mapActions, mapState } from "pinia";
import { useLayoutStore } from "@/stores/layout";
export default { export default {
name: "share-delete", name: "share-delete",
computed: { computed: {
...mapGetters(["currentPrompt"]), ...mapState(useLayoutStore, ["currentPrompt"]),
}, },
methods: { methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
submit: function () { submit: function () {
this.currentPrompt?.confirm(); this.currentPrompt?.confirm();
}, },

View File

@ -1,38 +1,111 @@
<template> <template>
<div class="card floating"> <div class="card floating">
<div class="card-title"> <div class="card-title">
<h2>{{ $t("prompts.upload") }}</h2> <h2>{{ t("prompts.upload") }}</h2>
</div> </div>
<div class="card-content"> <div class="card-content">
<p>{{ $t("prompts.uploadMessage") }}</p> <p>{{ t("prompts.uploadMessage") }}</p>
</div> </div>
<div class="card-action full"> <div class="card-action full">
<div @click="uploadFile" class="action"> <div
@click="uploadFile"
@keypress.enter="uploadFile"
class="action"
id="focus-prompt"
tabindex="1"
>
<i class="material-icons">insert_drive_file</i> <i class="material-icons">insert_drive_file</i>
<div class="title">{{ $t("buttons.file") }}</div> <div class="title">{{ t("buttons.file") }}</div>
</div> </div>
<div @click="uploadFolder" class="action"> <div
@click="uploadFolder"
@keypress.enter="uploadFolder"
class="action"
tabindex="2"
>
<i class="material-icons">folder</i> <i class="material-icons">folder</i>
<div class="title">{{ $t("buttons.folder") }}</div> <div class="title">{{ t("buttons.folder") }}</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script setup lang="ts">
export default { import { useI18n } from "vue-i18n";
name: "upload", import { useRoute } from "vue-router";
methods: { import { useFileStore } from "@/stores/file";
uploadFile: function () { import { useLayoutStore } from "@/stores/layout";
document.getElementById("upload-input").value = "";
document.getElementById("upload-input").click(); import * as upload from "@/utils/upload";
},
uploadFolder: function () { const { t } = useI18n();
document.getElementById("upload-folder-input").value = ""; const route = useRoute();
document.getElementById("upload-folder-input").click();
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
// TODO: this is a copy of the same function in FileListing.vue
const uploadInput = (event: Event) => {
layoutStore.closeHovers();
let files = (event.currentTarget as HTMLInputElement)?.files;
if (files === null) return;
let folder_upload = !!files[0].webkitRelativePath;
const uploadFiles: UploadList = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
const fullPath = folder_upload ? file.webkitRelativePath : undefined;
uploadFiles.push({
file,
name: file.name,
size: file.size,
isDir: false,
fullPath,
});
}
let path = route.path.endsWith("/") ? route.path : route.path + "/";
let conflict = upload.checkConflict(uploadFiles, fileStore.req!.items);
if (conflict) {
layoutStore.showHover({
prompt: "replace",
action: (event: Event) => {
event.preventDefault();
layoutStore.closeHovers();
upload.handleFiles(uploadFiles, path, false);
}, },
confirm: (event: Event) => {
event.preventDefault();
layoutStore.closeHovers();
upload.handleFiles(uploadFiles, path, true);
}, },
});
return;
}
upload.handleFiles(uploadFiles, path);
};
const openUpload = (isFolder: boolean) => {
const input = document.createElement("input");
input.type = "file";
input.multiple = true;
input.webkitdirectory = isFolder;
// TODO: call the function in FileListing.vue instead
input.onchange = uploadInput;
input.click();
};
const uploadFile = () => {
openUpload(false);
};
const uploadFolder = () => {
openUpload(true);
}; };
</script> </script>

View File

@ -53,7 +53,9 @@
</template> </template>
<script> <script>
import { mapGetters, mapMutations } from "vuex"; import { mapState, mapWritableState, mapActions } from "pinia";
import { useUploadStore } from "@/stores/upload";
import { useFileStore } from "@/stores/file";
import { abortAllUploads } from "@/api/tus"; import { abortAllUploads } from "@/api/tus";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
@ -65,19 +67,20 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters([ ...mapState(useUploadStore, [
"filesInUpload", "filesInUpload",
"filesInUploadCount", "filesInUploadCount",
"uploadSpeed", "uploadSpeed",
"eta", "getETA",
]), ]),
...mapMutations(["resetUpload"]), ...mapWritableState(useFileStore, ["reload"]),
...mapActions(useUploadStore, ["reset"]),
formattedETA() { formattedETA() {
if (!this.eta || this.eta === Infinity) { if (!this.getETA || this.getETA === Infinity) {
return "--:--:--"; return "--:--:--";
} }
let totalSeconds = this.eta; let totalSeconds = this.getETA;
const hours = Math.floor(totalSeconds / 3600); const hours = Math.floor(totalSeconds / 3600);
totalSeconds %= 3600; totalSeconds %= 3600;
const minutes = Math.floor(totalSeconds / 60); const minutes = Math.floor(totalSeconds / 60);
@ -97,8 +100,8 @@ export default {
abortAllUploads(); abortAllUploads();
buttons.done("upload"); buttons.done("upload");
this.open = false; this.open = false;
this.$store.commit("resetUpload"); this.reset();
this.$store.commit("setReload", true); this.reload = true;
} }
}, },
}, },

View File

@ -1,5 +1,5 @@
<template> <template>
<select v-on:change="change" :value="locale"> <select name="selectLanguage" v-on:change="change" :value="locale">
<option v-for="(language, value) in locales" :key="value" :value="value"> <option v-for="(language, value) in locales" :key="value" :value="value">
{{ $t("languages." + language) }} {{ $t("languages." + language) }}
</option> </option>
@ -7,12 +7,14 @@
</template> </template>
<script> <script>
import { markRaw } from "vue";
export default { export default {
name: "languages", name: "languages",
props: ["locale"], props: ["locale"],
data() { data() {
let dataObj = { let dataObj = {};
locales: { const locales = {
he: "he", he: "he",
hu: "hu", hu: "hu",
ar: "ar", ar: "ar",
@ -34,13 +36,16 @@ export default {
sk: "sk", sk: "sk",
"sv-se": "svSE", "sv-se": "svSE",
tr: "tr", tr: "tr",
ua: "ua", uk: "uk",
"zh-cn": "zhCN", "zh-cn": "zhCN",
"zh-tw": "zhTW", "zh-tw": "zhTW",
},
}; };
// Vue3 reactivity breaks with this configuration
// so we need to use markRaw as a workaround
// https://github.com/vuejs/core/issues/3024
Object.defineProperty(dataObj, "locales", { Object.defineProperty(dataObj, "locales", {
value: markRaw(locales),
configurable: false, configurable: false,
writable: false, writable: false,
}); });

View File

@ -1,18 +1,27 @@
<template> <template>
<select v-on:change="change" :value="theme"> <select v-on:change="change" :value="theme">
<option value="">{{ $t("settings.themes.light") }}</option> <option value="">{{ t("settings.themes.default") }}</option>
<option value="dark">{{ $t("settings.themes.dark") }}</option> <option value="light">{{ t("settings.themes.light") }}</option>
<option value="dark">{{ t("settings.themes.dark") }}</option>
</select> </select>
</template> </template>
<script> <script setup lang="ts">
export default { import { SelectHTMLAttributes } from "vue";
name: "themes", import { useI18n } from "vue-i18n";
props: ["theme"],
methods: { const { t } = useI18n();
change(event) {
this.$emit("update:theme", event.target.value); defineProps<{
}, theme: UserTheme;
}, }>();
const emit = defineEmits<{
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(e: "update:theme", val: string | null): void;
}>();
const change = (event: Event) => {
emit("update:theme", (event.target as SelectHTMLAttributes)?.value);
}; };
</script> </script>

View File

@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<p v-if="!isDefault"> <p v-if="!isDefault && props.user !== null">
<label for="username">{{ $t("settings.username") }}</label> <label for="username">{{ t("settings.username") }}</label>
<input <input
class="input input--block" class="input input--block"
type="text" type="text"
@ -11,7 +11,7 @@
</p> </p>
<p v-if="!isDefault"> <p v-if="!isDefault">
<label for="password">{{ $t("settings.password") }}</label> <label for="password">{{ t("settings.password") }}</label>
<input <input
class="input input--block" class="input input--block"
type="password" type="password"
@ -22,9 +22,9 @@
</p> </p>
<p> <p>
<label for="scope">{{ $t("settings.scope") }}</label> <label for="scope">{{ t("settings.scope") }}</label>
<input <input
:disabled="createUserDirData" :disabled="createUserDirData ?? false"
:placeholder="scopePlaceholder" :placeholder="scopePlaceholder"
class="input input--block" class="input input--block"
type="text" type="text"
@ -34,86 +34,89 @@
</p> </p>
<p class="small" v-if="displayHomeDirectoryCheckbox"> <p class="small" v-if="displayHomeDirectoryCheckbox">
<input type="checkbox" v-model="createUserDirData" /> <input type="checkbox" v-model="createUserDirData" />
{{ $t("settings.createUserHomeDirectory") }} {{ t("settings.createUserHomeDirectory") }}
</p> </p>
<p> <p>
<label for="locale">{{ $t("settings.language") }}</label> <label for="locale">{{ t("settings.language") }}</label>
<languages <languages
class="input input--block" class="input input--block"
id="locale" id="locale"
:locale.sync="user.locale" v-model:locale="user.locale"
></languages> ></languages>
</p> </p>
<p v-if="!isDefault"> <p v-if="!isDefault && user.perm">
<input <input
type="checkbox" type="checkbox"
:disabled="user.perm.admin" :disabled="user.perm.admin"
v-model="user.lockPassword" v-model="user.lockPassword"
/> />
{{ $t("settings.lockPassword") }} {{ t("settings.lockPassword") }}
</p> </p>
<permissions :perm.sync="user.perm" /> <permissions v-model:perm="user.perm" />
<commands v-if="isExecEnabled" :commands.sync="user.commands" /> <commands v-if="enableExec" v-model:commands="user.commands" />
<div v-if="!isDefault"> <div v-if="!isDefault">
<h3>{{ $t("settings.rules") }}</h3> <h3>{{ t("settings.rules") }}</h3>
<p class="small">{{ $t("settings.rulesHelp") }}</p> <p class="small">{{ t("settings.rulesHelp") }}</p>
<rules :rules.sync="user.rules" /> <rules v-model:rules="user.rules" />
</div> </div>
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import Languages from "./Languages.vue"; import Languages from "./Languages.vue";
import Rules from "./Rules.vue"; import Rules from "./Rules.vue";
import Permissions from "./Permissions.vue"; import Permissions from "./Permissions.vue";
import Commands from "./Commands.vue"; import Commands from "./Commands.vue";
import { enableExec } from "@/utils/constants"; import { enableExec } from "@/utils/constants";
import { computed, onMounted, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
export default { const { t } = useI18n();
name: "user",
data: () => { const createUserDirData = ref<boolean | null>(null);
return { const originalUserScope = ref<string | null>(null);
createUserDirData: false,
originalUserScope: "/", const props = defineProps<{
}; user: IUserForm;
}, isNew: boolean;
components: { isDefault: boolean;
Permissions, createUserDir?: boolean;
Languages, }>();
Rules,
Commands, onMounted(() => {
}, if (props.user.scope) {
props: ["user", "createUserDir", "isNew", "isDefault"], originalUserScope.value = props.user.scope;
created() { createUserDirData.value = props.createUserDir;
this.originalUserScope = this.user.scope; }
this.createUserDirData = this.createUserDir; });
},
computed: { const passwordPlaceholder = computed(() =>
passwordPlaceholder() { props.isNew ? "" : t("settings.avoidChanges")
return this.isNew ? "" : this.$t("settings.avoidChanges"); );
}, const scopePlaceholder = computed(() =>
scopePlaceholder() { createUserDirData.value ? t("settings.userScopeGenerationPlaceholder") : ""
return this.createUserDir );
? this.$t("settings.userScopeGenerationPlaceholder") const displayHomeDirectoryCheckbox = computed(
: ""; () => props.isNew && createUserDirData.value
}, );
displayHomeDirectoryCheckbox() {
return this.isNew && this.createUserDir; watch(
}, () => props.user,
isExecEnabled: () => enableExec, () => {
}, if (!props.user?.perm?.admin) return;
watch: { props.user.lockPassword = false;
"user.perm.admin": function () { }
if (!this.user.perm.admin) return; );
this.user.lockPassword = false;
}, watch(createUserDirData, () => {
createUserDirData() { if (props.user?.scope) {
this.user.scope = this.createUserDirData ? "" : this.originalUserScope; props.user.scope = createUserDirData.value
}, ? ""
}, : originalUserScope.value ?? "";
}; }
});
</script> </script>

View File

@ -1,14 +1,14 @@
.button { .button {
outline: 0; outline: 0;
border: 0; border: 0;
padding: .5em 1em; padding: 0.5em 1em;
border-radius: .1em; border-radius: 0.1em;
cursor: pointer; cursor: pointer;
background: var(--blue); background: var(--blue);
color: white; color: white;
border: 1px solid rgba(0, 0, 0, 0.05); border: 1px solid var(--divider);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.05); box-shadow: 0 0 5px var(--divider);
transition: .1s ease all; transition: 0.1s ease all;
} }
.button:hover { .button:hover {
@ -38,7 +38,7 @@
} }
.button--flat:hover { .button--flat:hover {
background: var(--moon-grey); background: var(--surfaceSecondary);
} }
.button--flat.button--red { .button--flat.button--red {
@ -50,6 +50,6 @@
} }
.button[disabled] { .button[disabled] {
opacity: .5; opacity: 0.5;
cursor: not-allowed; cursor: not-allowed;
} }

View File

@ -1,20 +1,20 @@
.input { .input {
border-radius: .1em; background: var(--surfacePrimary);
padding: .5em 1em; color: var(--textSecondary);
background: white; border: 1px solid var(--borderPrimary);
border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 0.1em;
transition: .2s ease all; padding: 0.5em 1em;
color: #333; transition: 0.2s ease all;
margin: 0; margin: 0;
} }
.input:hover, .input:hover,
.input:focus { .input:focus {
border-color: rgba(0, 0, 0, 0.2); border-color: var(--borderSecondary);
} }
.input--block { .input--block {
margin-bottom: .5em; margin-bottom: 0.5em;
display: block; display: block;
width: 100%; width: 100%;
} }
@ -27,9 +27,9 @@
} }
.input--red { .input--red {
background: #fcd0cd; background: var(--input-red) !important;
} }
.input--green { .input--green {
background: #c9f2da; background: var(--input-green) !important;
} }

View File

@ -12,8 +12,11 @@
} }
.share__box { .share__box {
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px; box-shadow:
background: #fff; rgba(0, 0, 0, 0.06) 0px 1px 3px,
rgba(0, 0, 0, 0.12) 0px 1px 2px;
background: var(--surfacePrimary);
color: var(--textPrimary);
border-radius: 0.2em; border-radius: 0.2em;
margin: 5px; margin: 5px;
overflow: hidden; overflow: hidden;
@ -39,7 +42,7 @@
.share__box__element { .share__box__element {
padding: 1em; padding: 1em;
border-top: 1px solid rgba(0, 0, 0, 0.1); border-top: 1px solid var(--borderPrimary);
word-break: break-all; word-break: break-all;
} }
@ -62,7 +65,7 @@
border-left: 0; border-left: 0;
border-right: 0; border-right: 0;
border-bottom: 0; border-bottom: 0;
border-top: 1px solid rgba(0, 0, 0, 0.1); border-top: 1px solid var(--borderPrimary);
} }
.share__box__items #listing.list .item .name { .share__box__items #listing.list .item .name {
@ -76,7 +79,7 @@
.share__wrong__password { .share__wrong__password {
background: var(--red); background: var(--red);
color: #fff; color: #fff;
padding: .5em; padding: 0.5em;
text-align: center; text-align: center;
animation: .2s opac forwards; animation: 0.2s opac forwards;
} }

View File

@ -3,17 +3,8 @@
bottom: 0; bottom: 0;
left: 0; left: 0;
max-height: calc(100% - 4em); max-height: calc(100% - 4em);
background: white; background: var(--surfacePrimary);
color: #212121; color: var(--textPrimary);
z-index: 9997;
width: 100%;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
transition: .2s ease transform;
}
.shell__divider {
position: relative;
height: 8px;
z-index: 9999; z-index: 9999;
background: rgba(127, 127, 127, 0.1); background: rgba(127, 127, 127, 0.1);
transition: 0.2s ease background; transition: 0.2s ease background;
@ -32,6 +23,8 @@
overflow: auto; overflow: auto;
font-size: 1rem; font-size: 1rem;
cursor: text; cursor: text;
box-shadow: 0 0 5px var(--borderPrimary);
transition: 0.2s ease transform;
} }
.shell__overlay { .shell__overlay {
@ -52,7 +45,7 @@ body.rtl .shell-content {
display: flex; display: flex;
padding: 0.5em; padding: 0.5em;
align-items: flex-start; align-items: flex-start;
border-top: 1px solid rgba(0, 0, 0, 0.05); border-top: 1px solid var(--divider);
} }
.shell--hidden { .shell--hidden {

View File

@ -1,8 +1,8 @@
:root { :root {
--blue: #2196f3; --blue: #2196f3;
--dark-blue: #1E88E5; --dark-blue: #1e88e5;
--red: #F44336; --red: #f44336;
--dark-red: #D32F2F; --dark-red: #d32f2f;
--moon-grey: #f2f2f2; --moon-grey: #f2f2f2;
--icon-red: #da4453; --icon-red: #da4453;
@ -11,4 +11,44 @@
--icon-green: #2ecc71; --icon-green: #2ecc71;
--icon-blue: #1d99f3; --icon-blue: #1d99f3;
--icon-violet: #9b59b6; --icon-violet: #9b59b6;
--input-red: rgb(252, 208, 205);
--input-green: rgb(201, 242, 218);
--item-selected: white;
--action: rgb(84, 110, 122);
--background: rgb(250, 250, 250);
--surfacePrimary: rgb(255, 255, 255);
--surfaceSecondary: rgb(230, 230, 230);
--divider: rgba(0, 0, 0, 0.05);
--iconPrimary: var(--icon-blue);
--iconSecondary: rgb(255, 255, 255);
--iconTertiary: rgb(204, 204, 204);
--textPrimary: rgb(111, 111, 111);
--textSecondary: rgb(51, 51, 51);
--hover: rgba(0, 0, 0, 0.1);
--borderPrimary: rgba(0, 0, 0, 0.1);
--borderSecondary: rgba(0, 0, 0, 0.2);
}
:root.dark {
--input-red: rgb(115, 48, 45);
--input-green: rgb(20, 122, 65);
--action: rgb(255, 255, 255);
--background: rgb(20, 29, 36);
--surfacePrimary: rgb(32, 41, 47);
--surfaceSecondary: rgb(58, 65, 71);
--textPrimary: rgba(255, 255, 255, 0.6);
--textSecondary: rgba(255, 255, 255, 0.87);
--divider: rgba(255, 255, 255, 0.12);
--iconPrimary: rgb(255, 255, 255);
--iconSecondary: rgb(255, 255, 255);
--iconTertiary: rgb(255, 255, 255);
--hover: rgba(255, 255, 255, 0.1);
--borderPrimary: rgba(255, 255, 255, 0.05);
--borderSecondary: rgba(255, 255, 255, 0.15);
} }

View File

@ -1,12 +1,8 @@
body { body {
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
padding-top: 4em; padding-top: 4em;
background-color: #fafafa; background: var(--background);
color: #333333; color: var(--textSecondary);
}
body.rtl {
direction: rtl;
} }
* { * {
@ -62,8 +58,8 @@ nav {
left: 0; left: 0;
} }
body.rtl nav { html[dir="rtl"] nav {
left: unset; left: initial;
right: 0; right: 0;
} }
@ -78,13 +74,12 @@ nav .action {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
body.rtl .action { html[dir="rtl"] nav .action {
direction: rtl;
text-align: right; text-align: right;
} }
nav > div { nav > div {
border-top: 1px solid rgba(0, 0, 0, 0.05); border-top: 1px solid var(--divider);
} }
nav .action > * { nav .action > * {
@ -99,14 +94,15 @@ main {
.breadcrumbs { .breadcrumbs {
height: 3em; height: 3em;
border-bottom: 1px solid rgba(0, 0, 0, 0.05); background: var(--background);
border-bottom: 1px solid var(--divider);
} }
.breadcrumbs span, .breadcrumbs span,
.breadcrumbs { .breadcrumbs {
display: flex; display: flex;
align-items: center; align-items: center;
color: #6f6f6f; color: var(--textPrimary);
} }
.breadcrumbs a { .breadcrumbs a {
@ -115,12 +111,12 @@ main {
border-radius: 0.125em; border-radius: 0.125em;
} }
body.rtl .breadcrumbs a { html[dir="rtl"] .breadcrumbs a {
transform: translateX(-16em); transform: translateX(-16em);
} }
.breadcrumbs a:hover { .breadcrumbs a:hover {
background-color: rgba(0, 0, 0, 0.05); background-color: var(--divider);
} }
.breadcrumbs span a { .breadcrumbs span a {
@ -152,3 +148,33 @@ body.rtl .breadcrumbs a {
.break-word { .break-word {
word-break: break-all; word-break: break-all;
} }
.vue-number-input > input {
background: var(--surfacePrimary) !important;
border-color: var(--surfaceSecondary) !important;
color: var(--textSecondary) !important;
}
.vue-number-input--small > input {
height: 1rem !important;
font-size: 1rem !important;
}
.vue-number-input :hover,
.vue-number-input :focus {
border-color: var(--borderSecondary) !important;
}
.vue-number-input__button {
background: var(--surfacePrimary) !important;
}
.vue-number-input__button--minus,
.vue-number-input__button--plus {
border-color: var(--surfaceSecondary) !important;
}
.vue-number-input__button::before,
.vue-number-input__button::after {
background: var(--textSecondary) !important;
}

View File

@ -4,17 +4,17 @@
.dashboard .row { .dashboard .row {
display: flex; display: flex;
margin: 0 -.5em; margin: 0 -0.5em;
flex-wrap: wrap; flex-wrap: wrap;
} }
body.rtl .dashboard .row { html[dir="rtl"] .dashboard .row {
margin-right: 16em; margin-right: 16em;
} }
.dashboard .row .column { .dashboard .row .column {
display: flex; display: flex;
padding: 0 .5em; padding: 0 0.5em;
width: 50%; width: 50%;
} }
@ -29,26 +29,26 @@ body.rtl .dashboard .row {
} }
a { a {
color: inherit color: inherit;
} }
.dashboard p label { .dashboard p label {
margin-bottom: .2em; margin-bottom: 0.2em;
display: block; display: block;
font-size: .8em; font-size: 0.8em;
font-weight: 500; font-weight: 500;
color: rgba(0, 0, 0, 0.57); color: var(--textPrimary);
} }
li code, li code,
p code { p code {
background: rgba(0, 0, 0, 0.05); background: var(--divider);
padding: .1em; padding: 0.1em;
border-radius: .2em; border-radius: 0.2em;
} }
.small { .small {
font-size: .8em; font-size: 0.8em;
line-height: 1.5; line-height: 1.5;
} }
@ -61,21 +61,21 @@ p code {
.dashboard #nav .wrapper { .dashboard #nav .wrapper {
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;
border-bottom: 2px solid rgba(0, 0, 0, 0.05); border-bottom: 2px solid var(--divider);
} }
body.rtl #nav .wrapper { html[dir="rtl"] .dashboard #nav .wrapper {
margin-right: 16em; margin-right: 16em;
} }
.dashboard #nav ul { .dashboard #nav ul {
list-style: none; list-style: none;
display: flex; display: flex;
color: rgb(84, 110, 122); color: var(--action);
font-weight: 500; font-weight: 500;
padding: 0; padding: 0;
margin: 0 0 -2px 0; margin: 0 0 -2px 0;
font-size: .8em; font-size: 0.8em;
text-align: center; text-align: center;
justify-content: left; justify-content: left;
} }
@ -85,12 +85,11 @@ body.rtl #nav .wrapper {
padding: 1.5em 2em; padding: 1.5em 2em;
white-space: nowrap; white-space: nowrap;
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
transition: .1s ease-in-out all; transition: 0.1s ease-in-out all;
} }
.dashboard #nav ul li:hover { .dashboard #nav ul li:hover {
background: var(--moon-grey); background: var(--surfaceSecondary);
} }
.dashboard #nav ul li.active { .dashboard #nav ul li.active {
@ -120,7 +119,7 @@ table {
} }
table tr { table tr {
border-bottom: 1px solid #ccc; border-bottom: 1px solid var(--iconTertiary);
} }
table tr:last-child { table tr:last-child {
@ -129,13 +128,13 @@ table tr:last-child {
table th { table th {
font-weight: 500; font-weight: 500;
color: #757575; color: var(--textSecondary);
text-align: left; text-align: left;
} }
table th, table th,
table td { table td {
padding: .5em 0; padding: 0.5em 0;
} }
table td.small { table td.small {
@ -146,7 +145,7 @@ table tr>*:first-child {
padding-left: 1em; padding-left: 1em;
} }
body.rtl table tr>* { html[dir="rtl"] table tr > * {
padding-left: unset; padding-left: unset;
padding-right: 1em; padding-right: 1em;
text-align: right; text-align: right;
@ -160,9 +159,13 @@ table tr>*:last-child {
.card { .card {
position: relative; position: relative;
margin: 0 0 1rem 0; margin: 0 0 1rem 0;
background-color: #fff; background: var(--surfacePrimary);
color: var(--textSecondary);
border-radius: 2px; border-radius: 2px;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); 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);
overflow: auto; overflow: auto;
} }
@ -171,11 +174,11 @@ table tr>*:last-child {
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
z-index: 99999;
max-width: 25em; max-width: 25em;
width: 90%; width: 90%;
max-height: 95%; max-height: 95%;
animation: .1s show forwards; /* animation-duration: 0.3s;
animation-fill-mode: forwards; */
} }
.card > * > *:first-child { .card > * > *:first-child {
@ -195,7 +198,7 @@ table tr>*:last-child {
margin-right: auto; margin-right: auto;
} }
body.rtl .card .card-title>*:first-child { html[dir="rtl"] .card .card-title > *:first-child {
margin-right: 0; margin-right: 0;
text-align: right; text-align: right;
} }
@ -234,7 +237,7 @@ body.rtl .card .card-action {
} }
.card h3 { .card h3 {
color: rgba(0, 0, 0, 0.53); color: var(--textPrimary);
font-size: 1em; font-size: 1em;
font-weight: 500; font-weight: 500;
margin: 2em 0 1em; margin: 2em 0 1em;
@ -253,6 +256,14 @@ body.rtl .card .card-action {
max-width: 15em; max-width: 15em;
} }
.card#share input,
.card#share select,
.card#share input::-webkit-inner-spin-button,
.card#share input::-webkit-outer-spin-button {
background: var(--surfacePrimary);
color: var(--textSecondary);
}
.card#share ul { .card#share ul {
list-style: none; list-style: none;
padding: 0; padding: 0;
@ -277,24 +288,24 @@ body.rtl .card .card-action {
.card#share ul li input, .card#share ul li input,
.card#share ul li select { .card#share ul li select {
padding: .2em; padding: 0.2em;
margin-right: .5em; margin-right: 0.5em;
border: 1px solid #dadada; border: 1px solid var(--borderPrimary);
} }
.card#share .action.copy-clipboard::after { .card#share .action.copy-clipboard::after {
content: 'Copied!'; content: "Copied!";
position: absolute; position: absolute;
left: -25%; left: -25%;
width: 150%; width: 150%;
font-size: .6em; font-size: 0.6em;
text-align: center; text-align: center;
background: #44a6f5; background: #44a6f5;
color: #fff; color: #fff;
padding: .5em .2em; padding: 0.5em 0.2em;
border-radius: .4em; border-radius: 0.4em;
top: -2em; top: -2em;
transition: .1s ease opacity; transition: 0.1s ease opacity;
opacity: 0; opacity: 0;
} }
@ -324,10 +335,9 @@ body.rtl .card .card-action {
z-index: 9999; z-index: 9999;
visibility: hidden; visibility: hidden;
opacity: 0; opacity: 0;
animation: .1s show forwards; animation: 0.1s show forwards;
} }
/* * * * * * * * * * * * * * * * /* * * * * * * * * * * * * * * *
* PROMPT - MOVE * * PROMPT - MOVE *
* * * * * * * * * * * * * * * */ * * * * * * * * * * * * * * * */
@ -344,33 +354,33 @@ body.rtl .card .card-action {
.file-list li { .file-list li {
width: 100%; width: 100%;
user-select: none; user-select: none;
border-radius: .2em; border-radius: 0.2em;
padding: .3em; padding: 0.3em;
} }
.file-list li[aria-selected=true] { .file-list li[aria-selected="true"] {
background: var(--blue) !important; background: var(--blue) !important;
color: #fff !important; color: var(--iconSecondary) !important;
transition: .1s ease all; transition: 0.1s ease all;
} }
.file-list li:hover { .file-list li:hover {
background-color: #e9eaeb; background: var(--surfaceSecondary);
cursor: pointer; cursor: pointer;
} }
.file-list li:before { .file-list li:before {
content: "folder"; content: "folder";
color: #6f6f6f; color: var(--textPrimary);
vertical-align: middle; vertical-align: middle;
line-height: 1.4; line-height: 1.4;
font-family: 'Material Icons'; font-family: "Material Icons";
font-size: 1.75em; font-size: 1.75em;
margin-right: .25em; margin-right: 0.25em;
} }
.file-list li[aria-selected=true]:before { .file-list li[aria-selected="true"]:before {
color: white; color: var(--iconSecondary);
} }
.help { .help {
@ -399,11 +409,11 @@ body.rtl .card .card-action {
} }
.collapsible { .collapsible {
border-top: 1px solid rgba(0,0,0,0.1); border-top: 1px solid var(--borderPrimary);
} }
.collapsible:last-of-type { .collapsible:last-of-type {
border-bottom: 1px solid rgba(0,0,0,0.1); border-bottom: 1px solid var(--borderPrimary);
} }
.collapsible > input { .collapsible > input {
@ -421,18 +431,18 @@ body.rtl .card .card-action {
.collapsible > label * { .collapsible > label * {
margin: 0; margin: 0;
color: rgba(0,0,0,0.57); color: var(--textPrimary);
} }
.collapsible > label i { .collapsible > label i {
transition: .2s ease transform; transition: 0.2s ease transform;
user-select: none; user-select: none;
} }
.collapsible .collapse { .collapsible .collapse {
max-height: 0; max-height: 0;
overflow: hidden; overflow: hidden;
transition: .2s ease all; transition: 0.2s ease all;
} }
.collapsible > input:checked ~ .collapse { .collapsible > input:checked ~ .collapse {
@ -442,7 +452,7 @@ body.rtl .card .card-action {
} }
.collapsible > input:checked ~ label i { .collapsible > input:checked ~ label i {
transform: rotate(180deg) transform: rotate(180deg);
} }
.card .collapsible { .card .collapsible {
@ -468,12 +478,12 @@ body.rtl .card .card-action {
flex: 1; flex: 1;
padding: 2em; padding: 2em;
border-radius: 0.2em; border-radius: 0.2em;
border: 1px solid rgba(0, 0, 0, 0.1); border: 1px solid var(--borderPrimary);
text-align: center; text-align: center;
} }
.card .card-action.full .action { .card .card-action.full .action {
margin: 0 0.25em 0.50em; margin: 0 0.25em 0.5em;
} }
.card .card-action.full .action i { .card .card-action.full .action i {
@ -489,7 +499,7 @@ body.rtl .card .card-action {
} }
/*** RTL - Fix disk usage information (in english) ***/ /*** RTL - Fix disk usage information (in english) ***/
body.rtl .credits { html[dir="rtl"] .credits {
text-align: right; text-align: right;
direction: ltr; direction: ltr;
} }

View File

@ -1,8 +1,8 @@
header { header {
z-index: 1000; z-index: 1000;
background-color: #fff; background: var(--surfacePrimary);
border-bottom: 1px solid rgba(0, 0, 0, 0.075); border-bottom: 1px solid var(--divider);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); box-shadow: 0 0 5px var(--borderPrimary);
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
@ -82,20 +82,25 @@ header .menu-button {
} }
#search #input { #search #input {
background-color: #f5f5f5; background: var(--surfaceSecondary);
border-color: var(--surfacePrimary);
display: flex; display: flex;
height: 100%; height: 100%;
padding: 0em 0.75em; padding: 0em 0.75em;
border-radius: 0.3em; border-radius: 0.3em;
transition: .1s ease all; transition: 0.1s ease all;
align-items: center; align-items: center;
z-index: 2; z-index: 2;
} }
#search #input input::placeholder {
color: var(--textSecondary);
}
#search.active #input { #search.active #input {
border-bottom: 1px solid rgba(0, 0, 0, 0.075); border-bottom: 1px solid var(--borderPrimary);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); box-shadow: 0 0 5px var(--borderPrimary);
background-color: #fff; background: var(--surfacePrimary);
height: 4em; height: 4em;
} }
@ -105,7 +110,7 @@ header .menu-button {
#search.active i, #search.active i,
#search.active input { #search.active input {
color: #212121; color: var(--textPrimary);
} }
#search #input > .action, #search #input > .action,
@ -124,18 +129,20 @@ header .menu-button {
#search #result { #search #result {
visibility: visible; visibility: visible;
max-height: none; max-height: none;
background-color: #f8f8f8; background: var(--background);
text-align: left; text-align: left;
padding: 0; padding: 0;
color: rgba(0, 0, 0, 0.6); color: var(--textPrimary);
height: 0; height: 0;
transition: .1s ease height, .1s ease padding; transition:
0.1s ease height,
0.1s ease padding;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
z-index: 1; z-index: 1;
} }
body.rtl #search #result { html[dir="rtl"] #search #result {
direction: ltr; direction: ltr;
} }
@ -143,19 +150,18 @@ body.rtl #search #result {
margin-top: 0; margin-top: 0;
} }
body.rtl #search #result { html[dir="rtl"] #search #result {
direction: rtl;
text-align: right; text-align: right;
} }
/*** RTL - Keep search result LTR because it has paths (in english) ***/ /*** RTL - Keep search result LTR because it has paths (in english) ***/
body.rtl #search #result ul>* { html[dir="rtl"] #search #result ul > * {
direction: ltr; direction: ltr;
text-align: left; text-align: left;
} }
#search.active #result { #search.active #result {
padding: .5em; padding: 0.5em;
height: calc(100% - 4em); height: calc(100% - 4em);
} }
@ -166,7 +172,7 @@ body.rtl #search #result ul>* {
} }
#search li { #search li {
margin-bottom: .5em; margin-bottom: 0.5em;
} }
#search #result > div { #search #result > div {
@ -187,7 +193,7 @@ body.rtl #search #result ul>* {
} }
#search.active #result i { #search.active #result i {
color: #ccc; color: var(--iconTertiary);
} }
#search.active #result > p > i { #search.active #result > p > i {
@ -199,35 +205,35 @@ body.rtl #search #result ul>* {
#search.active #result ul li a { #search.active #result ul li a {
display: flex; display: flex;
align-items: center; align-items: center;
padding: .3em 0; padding: 0.3em 0;
} }
#search.active #result ul li a i { #search.active #result ul li a i {
margin-right: .3em; margin-right: 0.3em;
} }
#search::-webkit-input-placeholder { /* I dont think we need these anymore */
color: rgba(255, 255, 255, .5); /* #search::-webkit-input-placeholder {
} color: var(--textPrimary);
#search:-moz-placeholder {
opacity: 1;
color: rgba(255, 255, 255, .5);
} }
#search::-moz-placeholder { #search::-moz-placeholder {
opacity: 1; opacity: 1;
color: rgba(255, 255, 255, .5); color: var(--textPrimary);
} }
#search:-ms-input-placeholder { #search:-ms-input-placeholder {
color: rgba(255, 255, 255, .5); color: var(--textPrimary);
} }
#search #input input::placeholder {
color: var(--textPrimary);
} */
#search .boxes { #search .boxes {
border: 1px solid rgba(0, 0, 0, 0.075); border: 1px solid var(--borderPrimary);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); box-shadow: 0 0 5px var(--borderPrimary);
background: #fff; background: var(--surfacePrimary);
margin: 1em 0; margin: 1em 0;
} }
@ -235,11 +241,11 @@ body.rtl #search #result ul>* {
margin: 0; margin: 0;
font-weight: 500; font-weight: 500;
font-size: 1em; font-size: 1em;
color: #212121; color: var(--textSecondary);
padding: .5em; padding: 0.5em;
} }
body.rtl #search .boxes h3 { html[dir="rtl"] #search .boxes h3 {
text-align: right; text-align: right;
} }

View File

@ -2,30 +2,50 @@
/* General */ /* General */
.file-icons [aria-label^="."] { opacity: 0.33 } .file-icons [aria-label^="."] {
.file-icons [aria-label$=".bak"] { opacity: 0.33 } opacity: 0.33;
}
.file-icons [aria-label$=".bak"] {
opacity: 0.33;
}
.file-icons [data-type=audio] i::before { content: 'volume_up' } .file-icons [data-type="audio"] i::before {
.file-icons [data-type=blob] i::before { content: 'insert_drive_file' } content: "volume_up";
.file-icons [data-type=image] i::before { content: 'image' } }
.file-icons [data-type=pdf] i::before { content: 'description' } .file-icons [data-type="blob"] i::before {
.file-icons [data-type=text] i::before { content: 'description' } content: "insert_drive_file";
.file-icons [data-type=video] i::before { content: 'movie' } }
.file-icons [data-type=invalid_link] i::before { content: 'link_off' } .file-icons [data-type="image"] i::before {
content: "image";
}
.file-icons [data-type="pdf"] i::before {
content: "description";
}
.file-icons [data-type="text"] i::before {
content: "description";
}
.file-icons [data-type="video"] i::before {
content: "movie";
}
.file-icons [data-type="invalid_link"] i::before {
content: "link_off";
}
/* #f90 - Image */ /* #f90 - Image */
.file-icons [aria-label$=".ai"] i::before, .file-icons [aria-label$=".ai"] i::before,
.file-icons [aria-label$=".odg"] i::before, .file-icons [aria-label$=".odg"] i::before,
.file-icons [aria-label$=".xcf"] i::before .file-icons [aria-label$=".xcf"] i::before {
{ content: 'image' } content: "image";
}
/* #f90 - Presentation */ /* #f90 - Presentation */
.file-icons [aria-label$=".odp"] i::before, .file-icons [aria-label$=".odp"] i::before,
.file-icons [aria-label$=".ppt"] i::before, .file-icons [aria-label$=".ppt"] i::before,
.file-icons [aria-label$=".pptx"] i::before .file-icons [aria-label$=".pptx"] i::before {
{ content: 'slideshow' } content: "slideshow";
}
/* #0f0 - Spreadsheet/Database */ /* #0f0 - Spreadsheet/Database */
@ -34,8 +54,9 @@
.file-icons [aria-label$=".odb"] i::before, .file-icons [aria-label$=".odb"] i::before,
.file-icons [aria-label$=".ods"] i::before, .file-icons [aria-label$=".ods"] i::before,
.file-icons [aria-label$=".xls"] i::before, .file-icons [aria-label$=".xls"] i::before,
.file-icons [aria-label$=".xlsx"] i::before .file-icons [aria-label$=".xlsx"] i::before {
{ content: 'border_all' } content: "border_all";
}
/* #00f - Document */ /* #00f - Document */
@ -43,8 +64,9 @@
.file-icons [aria-label$=".docx"] i::before, .file-icons [aria-label$=".docx"] i::before,
.file-icons [aria-label$=".log"] i::before, .file-icons [aria-label$=".log"] i::before,
.file-icons [aria-label$=".odt"] i::before, .file-icons [aria-label$=".odt"] i::before,
.file-icons [aria-label$=".rtf"] i::before .file-icons [aria-label$=".rtf"] i::before {
{ content: 'description' } content: "description";
}
/* #999 - Code */ /* #999 - Code */
@ -65,8 +87,9 @@
.file-icons [aria-label$=".rs"] i::before, .file-icons [aria-label$=".rs"] i::before,
.file-icons [aria-label$=".vue"] i::before, .file-icons [aria-label$=".vue"] i::before,
.file-icons [aria-label$=".xml"] i::before, .file-icons [aria-label$=".xml"] i::before,
.file-icons [aria-label$=".yml"] i::before .file-icons [aria-label$=".yml"] i::before {
{ content: 'code' } content: "code";
}
/* #999 - Executable */ /* #999 - Executable */
@ -75,16 +98,18 @@
.file-icons [aria-label$=".exe"] i::before, .file-icons [aria-label$=".exe"] i::before,
.file-icons [aria-label$=".jar"] i::before, .file-icons [aria-label$=".jar"] i::before,
.file-icons [aria-label$=".ps1"] i::before, .file-icons [aria-label$=".ps1"] i::before,
.file-icons [aria-label$=".sh"] i::before .file-icons [aria-label$=".sh"] i::before {
{ content: 'web_asset' } content: "web_asset";
}
/* #999 - Installer */ /* #999 - Installer */
.file-icons [aria-label$=".deb"] i::before, .file-icons [aria-label$=".deb"] i::before,
.file-icons [aria-label$=".msi"] i::before, .file-icons [aria-label$=".msi"] i::before,
.file-icons [aria-label$=".pkg"] i::before, .file-icons [aria-label$=".pkg"] i::before,
.file-icons [aria-label$=".rpm"] i::before .file-icons [aria-label$=".rpm"] i::before {
{ content: 'archive' } content: "archive";
}
/* #999 - Compressed */ /* #999 - Compressed */
@ -96,8 +121,9 @@
.file-icons [aria-label$=".tar"] i::before, .file-icons [aria-label$=".tar"] i::before,
.file-icons [aria-label$=".xz"] i::before, .file-icons [aria-label$=".xz"] i::before,
.file-icons [aria-label$=".zip"] i::before, .file-icons [aria-label$=".zip"] i::before,
.file-icons [aria-label$=".zst"] i::before .file-icons [aria-label$=".zst"] i::before {
{ content: 'folder_zip' } content: "folder_zip";
}
/* #999 - Disk */ /* #999 - Disk */
@ -108,25 +134,35 @@
.file-icons [aria-label$=".vdi"] i::before, .file-icons [aria-label$=".vdi"] i::before,
.file-icons [aria-label$=".vhd"] i::before, .file-icons [aria-label$=".vhd"] i::before,
.file-icons [aria-label$=".vmdk"] i::before, .file-icons [aria-label$=".vmdk"] i::before,
.file-icons [aria-label$=".wim"] i::before .file-icons [aria-label$=".wim"] i::before {
{ content: 'album' } content: "album";
}
/* #999 - Font */ /* #999 - Font */
.file-icons [aria-label$=".otf"] i::before, .file-icons [aria-label$=".otf"] i::before,
.file-icons [aria-label$=".ttf"] i::before, .file-icons [aria-label$=".ttf"] i::before,
.file-icons [aria-label$=".woff"] i::before, .file-icons [aria-label$=".woff"] i::before,
.file-icons [aria-label$=".woff2"] i::before .file-icons [aria-label$=".woff2"] i::before {
{ content: 'font_download' } content: "font_download";
}
/* Colors */ /* Colors */
/* General */ /* General */
.file-icons [data-type=audio] i { color: var(--icon-yellow) } .file-icons [data-type="audio"] i {
.file-icons [data-type=image] i { color: var(--icon-orange) } color: var(--icon-yellow);
.file-icons [data-type=video] i { color: var(--icon-violet) } }
.file-icons [data-type=invalid_link] i { color: var(--icon-red) } .file-icons [data-type="image"] i {
color: var(--icon-orange);
}
.file-icons [data-type="video"] i {
color: var(--icon-violet);
}
.file-icons [data-type="invalid_link"] i {
color: var(--icon-red);
}
/* #f00 - Adobe/Oracle */ /* #f00 - Adobe/Oracle */
@ -135,8 +171,9 @@
.file-icons [aria-label$=".jar"] i, .file-icons [aria-label$=".jar"] i,
.file-icons [aria-label$=".psd"] i, .file-icons [aria-label$=".psd"] i,
.file-icons [aria-label$=".rb"] i, .file-icons [aria-label$=".rb"] i,
.file-icons [data-type=pdf] i .file-icons [data-type="pdf"] i {
{ color: var(--icon-red) } color: var(--icon-red);
}
/* #f90 - Image/Presentation */ /* #f90 - Image/Presentation */
@ -146,16 +183,18 @@
.file-icons [aria-label$=".ppt"] i, .file-icons [aria-label$=".ppt"] i,
.file-icons [aria-label$=".pptx"] i, .file-icons [aria-label$=".pptx"] i,
.file-icons [aria-label$=".vue"] i, .file-icons [aria-label$=".vue"] i,
.file-icons [aria-label$=".xcf"] i .file-icons [aria-label$=".xcf"] i {
{ color: var(--icon-orange) } color: var(--icon-orange);
}
/* #ff0 - Various */ /* #ff0 - Various */
.file-icons [aria-label$=".css"] i, .file-icons [aria-label$=".css"] i,
.file-icons [aria-label$=".js"] i, .file-icons [aria-label$=".js"] i,
.file-icons [aria-label$=".json"] i, .file-icons [aria-label$=".json"] i,
.file-icons [aria-label$=".zip"] i .file-icons [aria-label$=".zip"] i {
{ color: var(--icon-yellow) } color: var(--icon-yellow);
}
/* #0f0 - Spreadsheet/Google */ /* #0f0 - Spreadsheet/Google */
@ -164,8 +203,9 @@
.file-icons [aria-label$=".go"] i, .file-icons [aria-label$=".go"] i,
.file-icons [aria-label$=".ods"] i, .file-icons [aria-label$=".ods"] i,
.file-icons [aria-label$=".xls"] i, .file-icons [aria-label$=".xls"] i,
.file-icons [aria-label$=".xlsx"] i .file-icons [aria-label$=".xlsx"] i {
{ color: var(--icon-green) } color: var(--icon-green);
}
/* #00f - Document/Microsoft/Apple/Closed */ /* #00f - Document/Microsoft/Apple/Closed */
@ -188,18 +228,26 @@
.file-icons [aria-label$=".ps1"] i, .file-icons [aria-label$=".ps1"] i,
.file-icons [aria-label$=".rtf"] i, .file-icons [aria-label$=".rtf"] i,
.file-icons [aria-label$=".vob"] i, .file-icons [aria-label$=".vob"] i,
.file-icons [aria-label$=".wim"] i .file-icons [aria-label$=".wim"] i {
{ color: var(--icon-blue) } color: var(--icon-blue);
}
/* #60f - Various */ /* #60f - Various */
.file-icons [aria-label$=".iso"] i, .file-icons [aria-label$=".iso"] i,
.file-icons [aria-label$=".php"] i, .file-icons [aria-label$=".php"] i,
.file-icons [aria-label$=".rar"] i .file-icons [aria-label$=".rar"] i {
{ color: var(--icon-violet) } color: var(--icon-violet);
}
/* Overrides */ /* Overrides */
.file-icons [data-dir=true] i { color: var(--icon-blue) } .file-icons [data-dir="true"] i {
.file-icons [data-dir=true] i::before { content: 'folder' } color: var(--icon-blue);
.file-icons [aria-selected=true] i { color: var(--item-selected) } }
.file-icons [data-dir="true"] i::before {
content: "folder";
}
.file-icons [aria-selected="true"] i {
color: var(--iconSecondary);
}

View File

@ -1,15 +1,11 @@
#listing { html[dir="rtl"] #listing {
--item-selected: white;
}
body.rtl #listing {
margin-right: 16em; margin-right: 16em;
} }
#listing h2 { #listing h2 {
margin: 0 0 0 0.5em; margin: 0 0 0 0.5em;
font-size: .9em; font-size: 0.9em;
color: rgba(0, 0, 0, 0.38); color: var(--textPrimary);
font-weight: 500; font-weight: 500;
} }
@ -25,12 +21,15 @@ body.rtl #listing {
} }
#listing .item { #listing .item {
background-color: #fff; background: var(--surfacePrimary);
border-color: var(--divider);
position: relative; position: relative;
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
color: #6f6f6f; color: var(--textPrimary);
transition: .1s ease background, .1s ease opacity; transition:
0.1s ease background,
0.1s ease opacity;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
@ -75,13 +74,13 @@ body.rtl #listing {
margin: 1em auto; margin: 1em auto;
display: block !important; display: block !important;
width: 95%; width: 95%;
color: rgba(0, 0, 0, 0.3); color: var(--textPrimary);
font-weight: 500; font-weight: 500;
} }
.message i { .message i {
font-size: 2.5em; font-size: 2.5em;
margin-bottom: .2em; margin-bottom: 0.2em;
display: block; display: block;
} }
@ -92,14 +91,18 @@ body.rtl #listing {
#listing.mosaic .item { #listing.mosaic .item {
width: calc(33% - 1em); width: calc(33% - 1em);
margin: .5em; margin: 0.5em;
padding: 0.5em; padding: 0.5em;
border-radius: 0.2em; border-radius: 0.2em;
box-shadow: 0 1px 3px rgba(0, 0, 0, .06), 0 1px 2px rgba(0, 0, 0, .12); box-shadow:
0 1px 3px rgba(0, 0, 0, 0.06),
0 1px 2px rgba(0, 0, 0, 0.12);
} }
#listing.mosaic .item:hover { #listing.mosaic .item:hover {
box-shadow: 0 1px 3px rgba(0, 0, 0, .12), 0 1px 2px rgba(0, 0, 0, .24) !important; box-shadow:
0 1px 3px rgba(0, 0, 0, 0.12),
0 1px 2px rgba(0, 0, 0, 0.24) !important;
} }
#listing.mosaic .header { #listing.mosaic .header {
@ -127,7 +130,7 @@ body.rtl #listing {
text-align: center; text-align: center;
} }
#listing.mosaic.gallery .item[data-type=image] div:last-of-type { #listing.mosaic.gallery .item[data-type="image"] div:last-of-type {
color: white; color: white;
background: linear-gradient(#0000, #0009); background: linear-gradient(#0000, #0009);
} }
@ -159,7 +162,7 @@ body.rtl #listing {
#listing.list .item { #listing.list .item {
width: 100%; width: 100%;
margin: 0; margin: 0;
border: 1px solid rgba(0, 0, 0, 0.1); border: 1px solid var(--borderPrimary);
padding: 1em; padding: 1em;
border-top: 0; border-top: 0;
} }
@ -168,9 +171,9 @@ body.rtl #listing {
display: none; display: none;
} }
#listing .item[aria-selected=true] { #listing .item[aria-selected="true"] {
background: var(--blue) !important; background: var(--blue) !important;
color: var(--item-selected) !important; color: var(--iconSecondary) !important;
} }
#listing.list .item div:first-of-type { #listing.list .item div:first-of-type {
@ -202,22 +205,22 @@ body.rtl #listing {
#listing .item.header { #listing .item.header {
display: none !important; display: none !important;
background-color: #ccc; background-color: var(--iconTertiary);
} }
#listing.list .header i { #listing.list .header i {
font-size: 1.5em; font-size: 1.5em;
vertical-align: middle; vertical-align: middle;
margin-left: .2em; margin-left: 0.2em;
} }
#listing.list .item.header { #listing.list .item.header {
display: flex !important; display: flex !important;
background: #fafafa; background: var(--background);
z-index: 999; z-index: 999;
padding: .85em; padding: 0.85em;
border: 0; border: 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.1); border-bottom: 1px solid var(--borderPrimary);
} }
#listing.list .item.header > div:first-child { #listing.list .item.header > div:first-child {
@ -250,7 +253,7 @@ body.rtl #listing {
#listing.list .header i { #listing.list .header i {
opacity: 0; opacity: 0;
transition: .1s ease all; transition: 0.1s ease all;
} }
#listing.list .header p:hover i, #listing.list .header p:hover i,
@ -272,7 +275,7 @@ body.rtl #listing {
height: 4em; height: 4em;
padding: 0.5em 0.5em 0.5em 1em; padding: 0.5em 0.5em 0.5em 1em;
justify-content: space-between; justify-content: space-between;
transition: .2s ease bottom; transition: 0.2s ease bottom;
} }
#listing #multiple-selection.active { #listing #multiple-selection.active {
@ -281,5 +284,5 @@ body.rtl #listing {
#listing #multiple-selection p, #listing #multiple-selection p,
#listing #multiple-selection i { #listing #multiple-selection i {
color: var(--item-selected); color: var(--iconSecondary);
} }

View File

@ -1,5 +1,5 @@
#login { #login {
background: #fff; background: var(--surfacePrimary);
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
@ -17,7 +17,7 @@
#login h1 { #login h1 {
text-align: center; text-align: center;
font-size: 2.5em; font-size: 2.5em;
margin: .4em 0 .67em; margin: 0.4em 0 0.67em;
} }
#login form { #login form {
@ -34,15 +34,15 @@
} }
#login #recaptcha { #login #recaptcha {
margin: .5em 0 0; margin: 0.5em 0 0;
} }
#login .wrong { #login .wrong {
background: var(--red); background: var(--red);
color: #fff; color: #fff;
padding: .5em; padding: 0.5em;
text-align: center; text-align: center;
animation: .2s opac forwards; animation: 0.2s opac forwards;
} }
@keyframes opac { @keyframes opac {
@ -61,5 +61,5 @@
text-transform: lowercase; text-transform: lowercase;
font-weight: 500; font-weight: 500;
font-size: 0.9rem; font-size: 0.9rem;
margin: .5rem 0; margin: 0.5rem 0;
} }

View File

@ -1,12 +1,12 @@
@media (max-width: 1024px) { @media (max-width: 1024px) {
nav { nav {
width: 10em width: 10em;
} }
} }
@media (max-width: 1024px) { @media (max-width: 1024px) {
main { main {
width: calc(100% - 13em) width: calc(100% - 13em);
} }
} }
@ -21,27 +21,27 @@
width: 60%; width: 60%;
} }
#more { #more {
display: inherit display: inherit;
} }
header .overlay { header .overlay {
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: rgba(0, 0, 0, 0.1); background-color: var(--borderPrimary);
} }
#dropdown { #dropdown {
position: fixed; position: fixed;
top: 1em; top: 1em;
right: 1em; right: 1em;
display: block; display: block;
background-color: #fff; background: var(--surfaceSecondary);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); box-shadow: 0 0 5px var(--borderPrimary);
transform: scale(0); transform: scale(0);
transition: .1s ease-in-out transform; transition: 0.1s ease-in-out transform;
transform-origin: top right; transform-origin: top right;
z-index: 99999; z-index: 99999;
} }
body.rtl #dropdown { html[dir="rtl"] #dropdown {
right: unset; right: unset;
left: 1em; left: 1em;
transform-origin: top left; transform-origin: top left;
@ -61,7 +61,7 @@
} }
#dropdown .action span:not(.counter) { #dropdown .action span:not(.counter) {
display: inline-block; display: inline-block;
padding: .4em; padding: 0.4em;
} }
#dropdown .counter { #dropdown .counter {
left: 2.25em; left: 2.25em;
@ -73,8 +73,10 @@
transform: translateX(-50%); transform: translateX(-50%);
display: flex; display: flex;
align-items: center; align-items: center;
background: #fff; background: var(--surfaceSecondary);
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px; box-shadow:
rgba(0, 0, 0, 0.06) 0px 1px 3px,
rgba(0, 0, 0, 0.12) 0px 1px 2px;
width: 95%; width: 95%;
max-width: 20em; max-width: 20em;
z-index: 1; z-index: 1;
@ -86,7 +88,7 @@
#file-selection > span { #file-selection > span {
display: inline-block; display: inline-block;
margin-left: 1em; margin-left: 1em;
color: #6f6f6f; color: var(--textPrimary);
margin-right: auto; margin-right: auto;
} }
#file-selection .action span { #file-selection .action span {
@ -95,15 +97,15 @@
nav { nav {
top: 0; top: 0;
z-index: 99999; z-index: 99999;
background: #fff; background: var(--surfaceSecondary);
height: 100%; height: 100%;
width: 16em; width: 16em;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); box-shadow: 0 0 5px var(--borderPrimary);
transition: .1s ease left; transition: 0.1s ease left;
left: -17em; left: -17em;
} }
body.rtl nav { html[dir="rtl"] nav {
left: unset; left: unset;
right: -17em; right: -17em;
} }
@ -111,7 +113,7 @@
left: 0; left: 0;
} }
body.rtl nav.active { html[dir="rtl"] nav.active {
left: unset; left: unset;
right: 0; right: 0;
} }
@ -131,19 +133,19 @@
margin-bottom: 5em; margin-bottom: 5em;
} }
body.rtl #listing { html[dir="rtl"] #listing {
margin-right: unset; margin-right: unset;
} }
body.rtl .breadcrumbs { html[dir="rtl"] .breadcrumbs {
transform: translateX(16em); transform: translateX(16em);
} }
body.rtl #nav .wrapper { html[dir="rtl"] #nav .wrapper {
margin-right: unset; margin-right: unset;
} }
body.rtl .dashboard .row { html[dir="rtl"] .dashboard .row {
margin-right: unset; margin-right: unset;
} }

View File

@ -1,6 +1,6 @@
@import "normalize.css/normalize.css"; @import "normalize.css/normalize.css";
@import "noty/lib/noty.css"; @import "vue-toastification/dist/index.css";
@import "noty/lib/themes/mint.css"; @import "vue-final-modal/style.css";
@import "./_variables.css"; @import "./_variables.css";
@import "./_buttons.css"; @import "./_buttons.css";
@import "./_inputs.css"; @import "./_inputs.css";
@ -16,10 +16,23 @@
@import "./login.css"; @import "./login.css";
@import "./mobile.css"; @import "./mobile.css";
/* For testing only
:focus {
outline: 2px solid crimson !important;
border-radius: 3px !important;
} */
.link { .link {
color: var(--blue); color: var(--blue);
} }
#loading {
background: var(--background);
}
#loading .spinner > div {
background: var(--iconPrimary);
}
main .spinner { main .spinner {
display: block; display: block;
text-align: center; text-align: center;
@ -32,7 +45,7 @@ main .spinner > div {
height: 0.8em; height: 0.8em;
margin: 0 0.1em; margin: 0 0.1em;
font-size: 1em; font-size: 1em;
background-color: rgba(0, 0, 0, 0.3); background: var(--iconPrimary);
border-radius: 100%; border-radius: 100%;
display: inline-block; display: inline-block;
animation: sk-bouncedelay 1.4s infinite ease-in-out both; animation: sk-bouncedelay 1.4s infinite ease-in-out both;
@ -72,7 +85,7 @@ main .spinner .bounce2 {
transition: 0.2s ease all; transition: 0.2s ease all;
border: 0; border: 0;
margin: 0; margin: 0;
color: #546e7a; color: var(--action);
border-radius: 50%; border-radius: 50%;
background: transparent; background: transparent;
padding: 0; padding: 0;
@ -94,7 +107,7 @@ main .spinner .bounce2 {
} }
.action:hover { .action:hover {
background-color: rgba(0, 0, 0, 0.1); background-color: var(--hover);
} }
.action ul { .action ul {
@ -115,7 +128,7 @@ main .spinner .bounce2 {
} }
.action ul li:hover { .action ul li:hover {
background-color: rgba(0, 0, 0, 0.04); background-color: var(--divider);
} }
#click-overlay { #click-overlay {
@ -138,7 +151,7 @@ main .spinner .bounce2 {
bottom: 0; bottom: 0;
right: 0; right: 0;
background: var(--blue); background: var(--blue);
color: #fff; color: var(--iconSecondary);
border-radius: 50%; border-radius: 50%;
font-size: 0.75em; font-size: 0.75em;
width: 1.8em; width: 1.8em;
@ -146,7 +159,7 @@ main .spinner .bounce2 {
text-align: center; text-align: center;
line-height: 1.55em; line-height: 1.55em;
font-weight: bold; font-weight: bold;
border: 2px solid white; border: 2px solid var(--borderPrimary);
} }
/* PREVIEWER */ /* PREVIEWER */
@ -217,7 +230,6 @@ main .spinner .bounce2 {
height: 88%; height: 88%;
} }
#previewer .preview video { #previewer .preview video {
height: 100%; height: 100%;
} }
@ -302,7 +314,7 @@ main .spinner .bounce2 {
#previewer .spinner > div { #previewer .spinner > div {
width: 18px; width: 18px;
height: 18px; height: 18px;
background-color: white; background: var(--iconPrimary);
} }
/* EDITOR */ /* EDITOR */
@ -310,17 +322,21 @@ main .spinner .bounce2 {
#editor-container { #editor-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: #fafafa; background-color: var(--background);
position: fixed; position: fixed;
padding-top: 4em; padding-top: 4em;
top: 0; top: 0;
left: 0; left: 0;
height: 100%; height: 100%;
width: 100%; width: 100%;
z-index: 9999; z-index: 9998;
overflow: hidden; overflow: hidden;
} }
#editor-container .bar {
background: var(--surfacePrimary);
}
#editor-container #editor { #editor-container #editor {
flex: 1; flex: 1;
} }
@ -331,7 +347,7 @@ main .spinner .bounce2 {
} }
/*** RTL - flip and position arrow of path ***/ /*** RTL - flip and position arrow of path ***/
body.rtl .breadcrumbs .chevron { html[dir="rtl"] .breadcrumbs .chevron {
transform: scaleX(-1) translateX(16em); transform: scaleX(-1) translateX(16em);
} }
@ -343,22 +359,6 @@ body.rtl .breadcrumbs .chevron {
font-size: 1rem; font-size: 1rem;
} }
/* * * * * * * * * * * * * * * *
* PROMPT *
* * * * * * * * * * * * * * * */
.noty_buttons {
text-align: right;
padding: 0 10px 10px !important;
}
.noty_buttons button {
background: rgba(0, 0, 0, 0.05);
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 0 0 0;
font-size: 1rem;
}
/* * * * * * * * * * * * * * * * /* * * * * * * * * * * * * * * *
* FOOTER * * FOOTER *
* * * * * * * * * * * * * * * */ * * * * * * * * * * * * * * * */
@ -436,17 +436,17 @@ body.rtl .breadcrumbs .chevron {
* RTL overrides * * RTL overrides *
* * * * * * * * * * * * * * * */ * * * * * * * * * * * * * * * */
body.rtl .card-content textarea { html[dir="rtl"] .card-content textarea {
direction: ltr; direction: ltr;
text-align: left; text-align: left;
} }
body.rtl .card-content .small + input { html[dir="rtl"] .card-content .small + input {
direction: ltr; direction: ltr;
text-align: left; text-align: left;
} }
body.rtl .card.floating .card-content .file-list { html[dir="rtl"] .card.floating .card-content .file-list {
direction: ltr; direction: ltr;
text-align: left; text-align: left;
} }

View File

@ -1,7 +1,9 @@
{ {
"buttons": { "buttons": {
"cancel": "إلغاء", "cancel": "إلغاء",
"clear": "مسح",
"close": "إغلاق", "close": "إغلاق",
"continue": "متابعة",
"copy": "نسخ", "copy": "نسخ",
"copyFile": "نسخ الملف", "copyFile": "نسخ الملف",
"copyToClipboard": "نسخ الى الحافظة", "copyToClipboard": "نسخ الى الحافظة",
@ -11,6 +13,7 @@
"download": "تحميل", "download": "تحميل",
"file": "ملف", "file": "ملف",
"folder": "مجلد", "folder": "مجلد",
"fullScreen": "تكبير/تصغير الشاشة",
"hideDotfiles": "إخفاء ملفات النقطة", "hideDotfiles": "إخفاء ملفات النقطة",
"info": "معلومات", "info": "معلومات",
"more": "المزيد", "more": "المزيد",
@ -38,7 +41,7 @@
"update": "تحديث", "update": "تحديث",
"upload": "رفع", "upload": "رفع",
"openFile": "فتح الملف", "openFile": "فتح الملف",
"continue": "متابعة" "discardChanges": "إلغاء التغييرات"
}, },
"download": { "download": {
"downloadFile": "تحميل الملف", "downloadFile": "تحميل الملف",
@ -56,7 +59,6 @@
}, },
"files": { "files": {
"body": "الصفحة", "body": "الصفحة",
"clear": "مسح",
"closePreview": "إغلاق العرض", "closePreview": "إغلاق العرض",
"files": "الملفات", "files": "الملفات",
"folders": "المجلدات", "folders": "المجلدات",
@ -133,6 +135,7 @@
"deleteMessageMultiple": "هل تريد بالتأكيد حذف {count} ملف؟", "deleteMessageMultiple": "هل تريد بالتأكيد حذف {count} ملف؟",
"deleteMessageSingle": "هل تريد بالتأكيد حذف هذا الملف/المجلد؟", "deleteMessageSingle": "هل تريد بالتأكيد حذف هذا الملف/المجلد؟",
"deleteMessageShare": "هل تريد بالتأكيد إلغاء مشاركة هذا الملف/المجلد ({path})؟", "deleteMessageShare": "هل تريد بالتأكيد إلغاء مشاركة هذا الملف/المجلد ({path})؟",
"deleteUser": "هل تريد بالتأكيد حذف هذا المستخدم؟",
"deleteTitle": "حذف الملفات", "deleteTitle": "حذف الملفات",
"displayName": "عرض اﻹسم:", "displayName": "عرض اﻹسم:",
"download": "تحميل الملفات", "download": "تحميل الملفات",
@ -161,7 +164,9 @@
"upload": "رفع", "upload": "رفع",
"uploadFiles": "يتم رفع {files} ملفات.", "uploadFiles": "يتم رفع {files} ملفات.",
"uploadMessage": "إختر الملفات التي تريد رفعها.", "uploadMessage": "إختر الملفات التي تريد رفعها.",
"optionalPassword": "كلمة مرور إختيارية" "optionalPassword": "كلمة مرور إختيارية",
"resolution": "الدقة",
"discardEditorChanges": "هل تريد بالتأكيد إلغاء التغييرات؟"
}, },
"search": { "search": {
"images": "الصور", "images": "الصور",
@ -243,6 +248,7 @@
"shareDeleted": "تم حذف المشاركة!", "shareDeleted": "تم حذف المشاركة!",
"singleClick": "استخدم النقرة الواحدة لفتح الملفات", "singleClick": "استخدم النقرة الواحدة لفتح الملفات",
"themes": { "themes": {
"default": "افتراضي (نظام التشغيل)",
"dark": "غامق", "dark": "غامق",
"light": "فاتح", "light": "فاتح",
"title": "موضوع" "title": "موضوع"
@ -282,4 +288,3 @@
"unit": "وحدة الوقت" "unit": "وحدة الوقت"
} }
} }

View File

@ -1,6 +1,7 @@
{ {
"buttons": { "buttons": {
"cancel": "Abbrechen", "cancel": "Abbrechen",
"clear": "Schließen",
"close": "Schließen", "close": "Schließen",
"copy": "Kopieren", "copy": "Kopieren",
"copyFile": "Kopiere Datei", "copyFile": "Kopiere Datei",
@ -36,8 +37,7 @@
"toggleSidebar": "Seitenleiste anzeigen", "toggleSidebar": "Seitenleiste anzeigen",
"update": "Update", "update": "Update",
"upload": "Upload", "upload": "Upload",
"openFile": "Datei öffnen", "openFile": "Datei öffnen"
"continue": "Fortfahren"
}, },
"download": { "download": {
"downloadFile": "Download Datei", "downloadFile": "Download Datei",
@ -52,7 +52,6 @@
}, },
"files": { "files": {
"body": "Body", "body": "Body",
"clear": "Schließen",
"closePreview": "Vorschau schließen", "closePreview": "Vorschau schließen",
"files": "Dateien", "files": "Dateien",
"folders": "Ordner", "folders": "Ordner",
@ -88,6 +87,7 @@
"hu": "Magyar", "hu": "Magyar",
"ar": "العربية", "ar": "العربية",
"de": "Deutsch", "de": "Deutsch",
"el": "Ελληνικά",
"en": "English", "en": "English",
"es": "Español", "es": "Español",
"fr": "Français", "fr": "Français",
@ -104,7 +104,7 @@
"sk": "Slovenčina", "sk": "Slovenčina",
"svSE": "Swedish (Sweden)", "svSE": "Swedish (Sweden)",
"tr": "Türkçe", "tr": "Türkçe",
"ua": "Українська", "uk": "Українська",
"zhCN": "中文 (简体)", "zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)" "zhTW": "中文 (繁體)"
}, },

View File

@ -1,6 +1,7 @@
{ {
"buttons": { "buttons": {
"cancel": "Ακύρωση", "cancel": "Ακύρωση",
"clear": "Καθαρισμός",
"close": "Κλείσιμο", "close": "Κλείσιμο",
"copy": "Αντιγραφή", "copy": "Αντιγραφή",
"copyFile": "Αντιγραφή αρχείου", "copyFile": "Αντιγραφή αρχείου",
@ -36,8 +37,7 @@
"toggleSidebar": "(Απ-)ενεργοποίησης της πλευρικής μπάρας", "toggleSidebar": "(Απ-)ενεργοποίησης της πλευρικής μπάρας",
"update": "Ενημέρωση", "update": "Ενημέρωση",
"upload": "Μεταφόρτωση", "upload": "Μεταφόρτωση",
"openFile": "Άνοιγμα αρχείου", "openFile": "Άνοιγμα αρχείου"
"continue": "Συνέχεια"
}, },
"download": { "download": {
"downloadFile": "Λήψη αρχείου", "downloadFile": "Λήψη αρχείου",
@ -55,7 +55,6 @@
}, },
"files": { "files": {
"body": "Περιεχόμενο", "body": "Περιεχόμενο",
"clear": "Καθαρισμός",
"closePreview": "Κλείσιμο προεπισκόπησης", "closePreview": "Κλείσιμο προεπισκόπησης",
"files": "Αρχεία", "files": "Αρχεία",
"folders": "Φάκελοι", "folders": "Φάκελοι",

View File

@ -1,7 +1,9 @@
{ {
"buttons": { "buttons": {
"cancel": "Cancel", "cancel": "Cancel",
"clear": "Clear",
"close": "Close", "close": "Close",
"continue": "Continue",
"copy": "Copy", "copy": "Copy",
"copyFile": "Copy file", "copyFile": "Copy file",
"copyToClipboard": "Copy to clipboard", "copyToClipboard": "Copy to clipboard",
@ -11,6 +13,7 @@
"download": "Download", "download": "Download",
"file": "File", "file": "File",
"folder": "Folder", "folder": "Folder",
"fullScreen": "Toggle full screen",
"hideDotfiles": "Hide dotfiles", "hideDotfiles": "Hide dotfiles",
"info": "Info", "info": "Info",
"more": "More", "more": "More",
@ -38,7 +41,6 @@
"update": "Update", "update": "Update",
"upload": "Upload", "upload": "Upload",
"openFile": "Open file", "openFile": "Open file",
"continue": "Continue",
"discardChanges": "Discard" "discardChanges": "Discard"
}, },
"download": { "download": {
@ -57,7 +59,6 @@
}, },
"files": { "files": {
"body": "Body", "body": "Body",
"clear": "Clear",
"closePreview": "Close preview", "closePreview": "Close preview",
"files": "Files", "files": "Files",
"folders": "Folders", "folders": "Folders",
@ -110,7 +111,7 @@
"sk": "Slovenčina", "sk": "Slovenčina",
"svSE": "Swedish (Sweden)", "svSE": "Swedish (Sweden)",
"tr": "Türkçe", "tr": "Türkçe",
"ua": "Українська", "uk": "Українська",
"zhCN": "中文 (简体)", "zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)" "zhTW": "中文 (繁體)"
}, },
@ -134,6 +135,7 @@
"deleteMessageMultiple": "Are you sure you wish to delete {count} file(s)?", "deleteMessageMultiple": "Are you sure you wish to delete {count} file(s)?",
"deleteMessageSingle": "Are you sure you wish to delete this file/folder?", "deleteMessageSingle": "Are you sure you wish to delete this file/folder?",
"deleteMessageShare": "Are you sure you wish to delete this share({path})?", "deleteMessageShare": "Are you sure you wish to delete this share({path})?",
"deleteUser": "Are you sure you want to delete this user?",
"deleteTitle": "Delete files", "deleteTitle": "Delete files",
"displayName": "Display Name:", "displayName": "Display Name:",
"download": "Download files", "download": "Download files",
@ -246,6 +248,7 @@
"shareDeleted": "Share deleted!", "shareDeleted": "Share deleted!",
"singleClick": "Use single clicks to open files and directories", "singleClick": "Use single clicks to open files and directories",
"themes": { "themes": {
"default": "System default",
"dark": "Dark", "dark": "Dark",
"light": "Light", "light": "Light",
"title": "Theme" "title": "Theme"

View File

@ -1,7 +1,9 @@
{ {
"buttons": { "buttons": {
"cancel": "Cancelar", "cancel": "Cancelar",
"clear": "Limpiar",
"close": "Cerrar", "close": "Cerrar",
"continue": "Continuar",
"copy": "Copiar", "copy": "Copiar",
"copyFile": "Copiar archivo", "copyFile": "Copiar archivo",
"copyToClipboard": "Copiar al portapapeles", "copyToClipboard": "Copiar al portapapeles",
@ -51,7 +53,6 @@
}, },
"files": { "files": {
"body": "Cuerpo", "body": "Cuerpo",
"clear": "Limpiar",
"closePreview": "Cerrar vista previa", "closePreview": "Cerrar vista previa",
"files": "Archivos", "files": "Archivos",
"folders": "Carpetas", "folders": "Carpetas",
@ -103,7 +104,7 @@
"sk": "Slovenčina", "sk": "Slovenčina",
"svSE": "Swedish (Sweden)", "svSE": "Swedish (Sweden)",
"tr": "Türkçe", "tr": "Türkçe",
"ua": "Українська", "uk": "Українська",
"zhCN": "中文 (简体)", "zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)" "zhTW": "中文 (繁體)"
}, },

View File

@ -1,15 +1,19 @@
{ {
"buttons": { "buttons": {
"cancel": "Annuler", "cancel": "Annuler",
"clear": "Effacer",
"close": "Fermer", "close": "Fermer",
"continue": "Continuer",
"copy": "Copier", "copy": "Copier",
"copyFile": "Copier le fichier", "copyFile": "Copier le fichier",
"copyToClipboard": "Copier dans le presse-papier", "copyToClipboard": "Copier dans le presse-papier",
"copyDownloadLinkToClipboard": "Copier le lien de téléchargement dans le presse-papier",
"create": "Créer", "create": "Créer",
"delete": "Supprimer", "delete": "Supprimer",
"download": "Télécharger", "download": "Télécharger",
"file": "Fichier", "file": "Fichier",
"folder": "Dossier", "folder": "Dossier",
"fullScreen": "Plein écran",
"hideDotfiles": "Masquer les dotfiles", "hideDotfiles": "Masquer les dotfiles",
"info": "Info", "info": "Info",
"more": "Plus", "more": "Plus",
@ -51,7 +55,6 @@
}, },
"files": { "files": {
"body": "Corps", "body": "Corps",
"clear": "Fermer",
"closePreview": "Fermer la prévisualisation", "closePreview": "Fermer la prévisualisation",
"files": "Fichiers", "files": "Fichiers",
"folders": "Dossiers", "folders": "Dossiers",
@ -87,23 +90,24 @@
"hu": "Magyar", "hu": "Magyar",
"ar": "العربية", "ar": "العربية",
"de": "Deutsch", "de": "Deutsch",
"el": "Ελληνικά",
"en": "English", "en": "English",
"es": "Español", "es": "Español",
"fr": "Français", "fr": "Français",
"is": "", "is": "Icelandic",
"it": "Italiano", "it": "Italiano",
"ja": "日本語", "ja": "日本語",
"ko": "한국어", "ko": "한국어",
"nlBE": "", "nlBE": "Dutch (Belgium)",
"pl": "Polski", "pl": "Polski",
"pt": "Português", "pt": "Português",
"ptBR": "Português (Brasil)", "ptBR": "Português (Brasil)",
"ro": "", "ro": "Romanian",
"ru": "Русский", "ru": "Русский",
"sk": "Slovenčina", "sk": "Slovenčina",
"svSE": "", "svSE": "Swedish (Sweden)",
"tr": "Türkçe", "tr": "Türkçe",
"ua": "Українська", "uk": "Українська",
"zhCN": "中文 (简体)", "zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)" "zhTW": "中文 (繁體)"
}, },

View File

@ -1,6 +1,7 @@
{ {
"buttons": { "buttons": {
"cancel": "ביטול", "cancel": "ביטול",
"clear": "נקה",
"close": "סגירה", "close": "סגירה",
"copy": "העתקה", "copy": "העתקה",
"copyFile": "העתק קובץ", "copyFile": "העתק קובץ",
@ -93,6 +94,7 @@
"hu": "Magyar", "hu": "Magyar",
"ar": "العربية", "ar": "العربية",
"de": "Deutsch", "de": "Deutsch",
"el": "Ελληνικά",
"en": "English", "en": "English",
"es": "Español", "es": "Español",
"fr": "Français", "fr": "Français",
@ -109,7 +111,7 @@
"sk": "Slovenčina", "sk": "Slovenčina",
"svSE": "Swedish (Sweden)", "svSE": "Swedish (Sweden)",
"tr": "Türkçe", "tr": "Türkçe",
"ua": "Українська", "uk": "Українська",
"zhCN": "中文 (简体)", "zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)" "zhTW": "中文 (繁體)"
}, },

View File

@ -1,6 +1,7 @@
{ {
"buttons": { "buttons": {
"cancel": "Mégse", "cancel": "Mégse",
"clear": "Törlése",
"close": "Bezárás", "close": "Bezárás",
"copy": "Másolás", "copy": "Másolás",
"copyFile": "Fájl másolása", "copyFile": "Fájl másolása",
@ -51,7 +52,6 @@
}, },
"files": { "files": {
"body": "Törzs", "body": "Törzs",
"clear": "Törlése",
"closePreview": "Előnézet bezárása", "closePreview": "Előnézet bezárása",
"files": "Fájlok", "files": "Fájlok",
"folders": "Mappák", "folders": "Mappák",
@ -87,6 +87,7 @@
"hu": "Magyar", "hu": "Magyar",
"ar": "العربية", "ar": "العربية",
"de": "Deutsch", "de": "Deutsch",
"el": "Ελληνικά",
"en": "English", "en": "English",
"es": "Español", "es": "Español",
"fr": "Français", "fr": "Français",
@ -103,7 +104,7 @@
"sk": "Slovenčina", "sk": "Slovenčina",
"svSE": "Swedish (Sweden)", "svSE": "Swedish (Sweden)",
"tr": "Türkçe", "tr": "Türkçe",
"ua": "Українська", "uk": "Українська",
"zhCN": "中文 (简体)", "zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)" "zhTW": "中文 (繁體)"
}, },

View File

@ -1,143 +0,0 @@
import Vue from "vue";
import VueI18n from "vue-i18n";
import he from "./he.json";
import hu from "./hu.json";
import ar from "./ar.json";
import de from "./de.json";
import el from "./el.json";
import en from "./en.json";
import es from "./es.json";
import fr from "./fr.json";
import is from "./is.json";
import it from "./it.json";
import ja from "./ja.json";
import ko from "./ko.json";
import nlBE from "./nl-be.json";
import pl from "./pl.json";
import pt from "./pt.json";
import ptBR from "./pt-br.json";
import ro from "./ro.json";
import ru from "./ru.json";
import sk from "./sk.json";
import ua from "./ua.json";
import svSE from "./sv-se.json";
import zhCN from "./zh-cn.json";
import zhTW from "./zh-tw.json";
Vue.use(VueI18n);
export function detectLocale() {
let locale = (navigator.language || navigator.browserLangugae).toLowerCase();
switch (true) {
case /^he.*/i.test(locale):
locale = "he";
break;
case /^hu.*/i.test(locale):
locale = "hu";
break;
case /^ar.*/i.test(locale):
locale = "ar";
break;
case /^el.*/i.test(locale):
locale = "el";
break;
case /^es.*/i.test(locale):
locale = "es";
break;
case /^en.*/i.test(locale):
locale = "en";
break;
case /^it.*/i.test(locale):
locale = "it";
break;
case /^fr.*/i.test(locale):
locale = "fr";
break;
case /^pt.*/i.test(locale):
locale = "pt";
break;
case /^pt-BR.*/i.test(locale):
locale = "pt-br";
break;
case /^ja.*/i.test(locale):
locale = "ja";
break;
case /^zh-CN/i.test(locale):
locale = "zh-cn";
break;
case /^zh-TW/i.test(locale):
locale = "zh-tw";
break;
case /^zh.*/i.test(locale):
locale = "zh-cn";
break;
case /^de.*/i.test(locale):
locale = "de";
break;
case /^ru.*/i.test(locale):
locale = "ru";
break;
case /^pl.*/i.test(locale):
locale = "pl";
break;
case /^ko.*/i.test(locale):
locale = "ko";
break;
case /^sk.*/i.test(locale):
locale = "sk";
break;
case /^ua.*/i.test(locale):
locale = "ua";
break;
default:
locale = "en";
}
return locale;
}
const removeEmpty = (obj) =>
Object.keys(obj)
.filter((k) => obj[k] !== null && obj[k] !== undefined && obj[k] !== "") // Remove undef. and null and empty.string.
.reduce(
(newObj, k) =>
typeof obj[k] === "object"
? Object.assign(newObj, { [k]: removeEmpty(obj[k]) }) // Recurse.
: Object.assign(newObj, { [k]: obj[k] }), // Copy value.
{}
);
export const rtlLanguages = ["he", "ar"];
const i18n = new VueI18n({
locale: detectLocale(),
fallbackLocale: "en",
messages: {
he: removeEmpty(he),
hu: removeEmpty(hu),
ar: removeEmpty(ar),
de: removeEmpty(de),
el: removeEmpty(el),
en: en,
es: removeEmpty(es),
fr: removeEmpty(fr),
is: removeEmpty(is),
it: removeEmpty(it),
ja: removeEmpty(ja),
ko: removeEmpty(ko),
"nl-be": removeEmpty(nlBE),
pl: removeEmpty(pl),
"pt-br": removeEmpty(ptBR),
pt: removeEmpty(pt),
ru: removeEmpty(ru),
ro: removeEmpty(ro),
sk: removeEmpty(sk),
"sv-se": removeEmpty(svSE),
ua: removeEmpty(ua),
"zh-cn": removeEmpty(zhCN),
"zh-tw": removeEmpty(zhTW),
},
});
export default i18n;

164
frontend/src/i18n/index.ts Normal file
View File

@ -0,0 +1,164 @@
import dayjs from "dayjs";
import { createI18n } from "vue-i18n";
import("dayjs/locale/ar");
import("dayjs/locale/de");
import("dayjs/locale/el");
import("dayjs/locale/en");
import("dayjs/locale/es");
import("dayjs/locale/fr");
import("dayjs/locale/he");
import("dayjs/locale/hu");
import("dayjs/locale/is");
import("dayjs/locale/it");
import("dayjs/locale/ja");
import("dayjs/locale/ko");
import("dayjs/locale/nl-be");
import("dayjs/locale/pl");
import("dayjs/locale/pt-br");
import("dayjs/locale/pt");
import("dayjs/locale/ro");
import("dayjs/locale/ru");
import("dayjs/locale/sk");
import("dayjs/locale/sv");
import("dayjs/locale/tr");
import("dayjs/locale/uk");
import("dayjs/locale/zh-cn");
import("dayjs/locale/zh-tw");
// All i18n resources specified in the plugin `include` option can be loaded
// at once using the import syntax
import messages from "@intlify/unplugin-vue-i18n/messages";
export function detectLocale() {
// locale is an RFC 5646 language tag
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/language
let locale = navigator.language.toLowerCase();
switch (true) {
case /^he\b/.test(locale):
locale = "he";
break;
case /^hu\b/.test(locale):
locale = "hu";
break;
case /^ar\b/.test(locale):
locale = "ar";
break;
case /^el.*/i.test(locale):
locale = "el";
break;
case /^es\b/.test(locale):
locale = "es";
break;
case /^en\b/.test(locale):
locale = "en";
break;
case /^is\b/.test(locale):
locale = "is";
break;
case /^it\b/.test(locale):
locale = "it";
break;
case /^fr\b/.test(locale):
locale = "fr";
break;
case /^pt-br\b/.test(locale):
locale = "pt-br";
break;
case /^pt\b/.test(locale):
locale = "pt";
break;
case /^ja\b/.test(locale):
locale = "ja";
break;
case /^zh-tw\b/.test(locale):
locale = "zh-tw";
break;
case /^zh-cn\b/.test(locale):
case /^zh\b/.test(locale):
locale = "zh-cn";
break;
case /^de\b/.test(locale):
locale = "de";
break;
case /^ro\b/.test(locale):
locale = "ro";
break;
case /^ru\b/.test(locale):
locale = "ru";
break;
case /^pl\b/.test(locale):
locale = "pl";
break;
case /^ko\b/.test(locale):
locale = "ko";
break;
case /^sk\b/.test(locale):
locale = "sk";
break;
case /^tr\b/.test(locale):
locale = "tr";
break;
// ua wasnt a valid locale for ukraine
case /^uk\b/.test(locale):
locale = "uk";
break;
case /^sv-se\b/.test(locale):
case /^sv\b/.test(locale):
locale = "sv";
break;
case /^nl-be\b/.test(locale):
locale = "nl-be";
break;
default:
locale = "en";
}
return locale;
}
// TODO: was this really necessary?
// function removeEmpty(obj: Record<string, any>): void {
// Object.keys(obj)
// .filter((k) => obj[k] !== null && obj[k] !== undefined && obj[k] !== "") // Remove undef. and null and empty.string.
// .reduce(
// (newObj, k) =>
// typeof obj[k] === "object"
// ? Object.assign(newObj, { [k]: removeEmpty(obj[k]) }) // Recurse.
// : Object.assign(newObj, { [k]: obj[k] }), // Copy value.
// {}
// );
// }
export const rtlLanguages = ["he", "ar"];
export const i18n = createI18n({
locale: detectLocale(),
fallbackLocale: "en",
messages,
// expose i18n.global for outside components
legacy: true,
});
export const isRtl = (locale?: string) => {
// see below
// @ts-ignore
return rtlLanguages.includes(locale || i18n.global.locale.value);
};
export function setLocale(locale: string) {
dayjs.locale(locale);
// according to doc u only need .value if legacy: false but they lied
// https://vue-i18n.intlify.dev/guide/essentials/scope.html#local-scope-1
//@ts-ignore
i18n.global.locale.value = locale;
}
export function setHtmlLocale(locale: string) {
const html = document.documentElement;
html.lang = locale;
if (isRtl(locale)) html.dir = "rtl";
else html.dir = "ltr";
}
export default i18n;

View File

@ -1,6 +1,7 @@
{ {
"buttons": { "buttons": {
"cancel": "Hætta við", "cancel": "Hætta við",
"clear": "Hreinsa",
"close": "Loka", "close": "Loka",
"copy": "Afrita", "copy": "Afrita",
"copyFile": "Afrita skjal", "copyFile": "Afrita skjal",
@ -46,7 +47,6 @@
}, },
"files": { "files": {
"body": "Meginmál", "body": "Meginmál",
"clear": "Hreinsa",
"closePreview": "Loka forskoðun", "closePreview": "Loka forskoðun",
"files": "Skjöl", "files": "Skjöl",
"folders": "Möppur", "folders": "Möppur",
@ -81,23 +81,24 @@
"hu": "Magyar", "hu": "Magyar",
"ar": "العربية", "ar": "العربية",
"de": "Deutsch", "de": "Deutsch",
"el": "Ελληνικά",
"en": "English", "en": "English",
"es": "Español", "es": "Español",
"fr": "Français", "fr": "Français",
"is": "", "is": "Icelandic",
"it": "Italiano", "it": "Italiano",
"ja": "日本語", "ja": "日本語",
"ko": "한국어", "ko": "한국어",
"nlBE": "", "nlBE": "Dutch (Belgium)",
"pl": "Polski", "pl": "Polski",
"pt": "Português", "pt": "Português",
"ptBR": "Português (Brasil)", "ptBR": "Português (Brasil)",
"ro": "", "ro": "Romanian",
"ru": "Русский", "ru": "Русский",
"sk": "Slovenčina", "sk": "Slovenčina",
"svSE": "", "svSE": "Swedish (Sweden)",
"tr": "Türkçe", "tr": "Türkçe",
"ua": "Українська", "uk": "Українська",
"zhCN": "中文 (简体)", "zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)" "zhTW": "中文 (繁體)"
}, },

View File

@ -1,7 +1,9 @@
{ {
"buttons": { "buttons": {
"cancel": "Annulla", "cancel": "Annulla",
"clear": "Cancella",
"close": "Chiudi", "close": "Chiudi",
"continue": "Continua",
"copy": "Copia", "copy": "Copia",
"copyFile": "Copia file", "copyFile": "Copia file",
"copyToClipboard": "Copia negli appunti", "copyToClipboard": "Copia negli appunti",
@ -46,7 +48,6 @@
}, },
"files": { "files": {
"body": "Contenuto", "body": "Contenuto",
"clear": "Cancella",
"closePreview": "Chiudi anteprima", "closePreview": "Chiudi anteprima",
"files": "File", "files": "File",
"folders": "Cartelle", "folders": "Cartelle",
@ -81,23 +82,24 @@
"hu": "Magyar", "hu": "Magyar",
"ar": "العربية", "ar": "العربية",
"de": "Deutsch", "de": "Deutsch",
"el": "Ελληνικά",
"en": "English", "en": "English",
"es": "Español", "es": "Español",
"fr": "Français", "fr": "Français",
"is": "", "is": "Icelandic",
"it": "Italiano", "it": "Italiano",
"ja": "日本語", "ja": "日本語",
"ko": "한국어", "ko": "한국어",
"nlBE": "", "nlBE": "Dutch (Belgium)",
"pl": "Polski", "pl": "Polski",
"pt": "Português", "pt": "Português",
"ptBR": "Português (Brasil)", "ptBR": "Português (Brasil)",
"ro": "", "ro": "Romanian",
"ru": "Русский", "ru": "Русский",
"sk": "Slovenčina", "sk": "Slovenčina",
"svSE": "", "svSE": "Swedish (Sweden)",
"tr": "Türkçe", "tr": "Türkçe",
"ua": "Українська", "uk": "Українська",
"zhCN": "中文 (简体)", "zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)" "zhTW": "中文 (繁體)"
}, },

View File

@ -1,6 +1,7 @@
{ {
"buttons": { "buttons": {
"cancel": "キャンセル", "cancel": "キャンセル",
"clear": "クリアー",
"close": "閉じる", "close": "閉じる",
"copy": "コピー", "copy": "コピー",
"copyFile": "ファイルのコピー", "copyFile": "ファイルのコピー",
@ -109,7 +110,7 @@
"sk": "Slovenčina", "sk": "Slovenčina",
"svSE": "Swedish (Sweden)", "svSE": "Swedish (Sweden)",
"tr": "Türkçe", "tr": "Türkçe",
"ua": "Українська", "uk": "Українська",
"zhCN": "中文 (简体)", "zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)" "zhTW": "中文 (繁體)"
}, },

View File

@ -1,6 +1,7 @@
{ {
"buttons": { "buttons": {
"cancel": "취소", "cancel": "취소",
"clear": "지우기",
"close": "닫기", "close": "닫기",
"copy": "복사", "copy": "복사",
"copyFile": "파일 복사", "copyFile": "파일 복사",
@ -46,7 +47,6 @@
}, },
"files": { "files": {
"body": "본문", "body": "본문",
"clear": "지우기",
"closePreview": "미리보기 닫기", "closePreview": "미리보기 닫기",
"files": "파일", "files": "파일",
"folders": "폴더", "folders": "폴더",
@ -81,23 +81,24 @@
"hu": "Magyar", "hu": "Magyar",
"ar": "العربية", "ar": "العربية",
"de": "Deutsch", "de": "Deutsch",
"el": "Ελληνικά",
"en": "English", "en": "English",
"es": "Español", "es": "Español",
"fr": "Français", "fr": "Français",
"is": "", "is": "Icelandic",
"it": "Italiano", "it": "Italiano",
"ja": "日本語", "ja": "日本語",
"ko": "한국어", "ko": "한국어",
"nlBE": "", "nlBE": "Dutch (Belgium)",
"pl": "Polski", "pl": "Polski",
"pt": "Português", "pt": "Português",
"ptBR": "Português (Brasil)", "ptBR": "Português (Brasil)",
"ro": "", "ro": "Romanian",
"ru": "Русский", "ru": "Русский",
"sk": "Slovenčina", "sk": "Slovenčina",
"svSE": "", "svSE": "Swedish (Sweden)",
"tr": "Türkçe", "tr": "Türkçe",
"ua": "Українська", "uk": "Українська",
"zhCN": "中文 (简体)", "zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)" "zhTW": "中文 (繁體)"
}, },

View File

@ -1,6 +1,7 @@
{ {
"buttons": { "buttons": {
"cancel": "Annuleren", "cancel": "Annuleren",
"clear": "Wissen",
"close": "Sluiten", "close": "Sluiten",
"copy": "Kopiëren", "copy": "Kopiëren",
"copyFile": "Bestand kopiëren", "copyFile": "Bestand kopiëren",
@ -46,7 +47,6 @@
}, },
"files": { "files": {
"body": "Body", "body": "Body",
"clear": "Wissen",
"closePreview": "Voorvertoon sluiten", "closePreview": "Voorvertoon sluiten",
"files": "Bestanden", "files": "Bestanden",
"folders": "Mappen", "folders": "Mappen",
@ -79,27 +79,28 @@
"languages": { "languages": {
"he": "עברית", "he": "עברית",
"hu": "Magyar", "hu": "Magyar",
"ar": "Arabisch", "ar": "العربية",
"de": "Duits", "de": "Deutsch",
"en": "Engels", "el": "Ελληνικά",
"es": "Spaans", "en": "English",
"fr": "Frans", "es": "Español",
"is": "", "fr": "Français",
"it": "Italiaans", "is": "Icelandic",
"ja": "Japans", "it": "Italiano",
"ko": "Koreaans", "ja": "日本語",
"nlBE": "", "ko": "한국어",
"pl": "Pools", "nlBE": "Dutch (Belgium)",
"pt": "Portugees", "pl": "Polski",
"ptBR": "Portugees (Brazilië)", "pt": "Português",
"ro": "", "ptBR": "Português (Brasil)",
"ru": "Russisch", "ro": "Romanian",
"ru": "Русский",
"sk": "Slovenčina", "sk": "Slovenčina",
"svSE": "", "svSE": "Swedish (Sweden)",
"tr": "Türkçe", "tr": "Türkçe",
"ua": "Українська", "uk": "Українська",
"zhCN": "Chinees (vereenvoudigd)", "zhCN": "中文 (简体)",
"zhTW": "Chinees (traditioneel)" "zhTW": "中文 (繁體)"
}, },
"login": { "login": {
"createAnAccount": "Account aanmaken", "createAnAccount": "Account aanmaken",

View File

@ -1,6 +1,7 @@
{ {
"buttons": { "buttons": {
"cancel": "Anuluj", "cancel": "Anuluj",
"clear": "Wyczyść",
"close": "Zamknij", "close": "Zamknij",
"copy": "Kopiuj", "copy": "Kopiuj",
"copyFile": "Kopiuj plik", "copyFile": "Kopiuj plik",
@ -46,7 +47,6 @@
}, },
"files": { "files": {
"body": "Body", "body": "Body",
"clear": "Wyczyść",
"closePreview": "Zamknij poprzednie", "closePreview": "Zamknij poprzednie",
"files": "Pliki", "files": "Pliki",
"folders": "Foldery", "folders": "Foldery",
@ -81,23 +81,24 @@
"hu": "Magyar", "hu": "Magyar",
"ar": "العربية", "ar": "العربية",
"de": "Deutsch", "de": "Deutsch",
"el": "Ελληνικά",
"en": "English", "en": "English",
"es": "Español", "es": "Español",
"fr": "Français", "fr": "Français",
"is": "Íslenska", "is": "Icelandic",
"it": "Italiano", "it": "Italiano",
"ja": "日本語", "ja": "日本語",
"ko": "한국어", "ko": "한국어",
"nlBE": "Nederlands (België)", "nlBE": "Dutch (Belgium)",
"pl": "Polski", "pl": "Polski",
"pt": "Português", "pt": "Português",
"ptBR": "Português (Brasil)", "ptBR": "Português (Brasil)",
"ro": "Română", "ro": "Romanian",
"ru": "Русский", "ru": "Русский",
"sk": "Slovenčina", "sk": "Slovenčina",
"svSE": "Svenska (Sverige)", "svSE": "Swedish (Sweden)",
"tr": "Türkçe", "tr": "Türkçe",
"ua": "Українська", "uk": "Українська",
"zhCN": "中文 (简体)", "zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)" "zhTW": "中文 (繁體)"
}, },
@ -147,7 +148,6 @@
"size": "Rozmiar", "size": "Rozmiar",
"upload": "Prześlij", "upload": "Prześlij",
"uploadMessage": "Proszę wybrać metodę przesyłania" "uploadMessage": "Proszę wybrać metodę przesyłania"
}, },
"search": { "search": {
"images": "Zdjęcia", "images": "Zdjęcia",

View File

@ -1,7 +1,9 @@
{ {
"buttons": { "buttons": {
"cancel": "Cancelar", "cancel": "Cancelar",
"clear": "Limpar",
"close": "Fechar", "close": "Fechar",
"continue": "Continuar",
"copy": "Copiar", "copy": "Copiar",
"copyFile": "Copiar arquivo", "copyFile": "Copiar arquivo",
"copyToClipboard": "Copiar", "copyToClipboard": "Copiar",
@ -51,7 +53,6 @@
}, },
"files": { "files": {
"body": "Corpo", "body": "Corpo",
"clear": "Limpar",
"closePreview": "Fechar pré-visualização", "closePreview": "Fechar pré-visualização",
"files": "Arquivos", "files": "Arquivos",
"folders": "Pastas", "folders": "Pastas",
@ -87,23 +88,24 @@
"hu": "Magyar", "hu": "Magyar",
"ar": "العربية", "ar": "العربية",
"de": "Deutsch", "de": "Deutsch",
"el": "Ελληνικά",
"en": "English", "en": "English",
"es": "Español", "es": "Español",
"fr": "Français", "fr": "Français",
"is": "", "is": "Icelandic",
"it": "Italiano", "it": "Italiano",
"ja": "日本語", "ja": "日本語",
"ko": "한국어", "ko": "한국어",
"nlBE": "", "nlBE": "Dutch (Belgium)",
"pl": "Polski", "pl": "Polski",
"pt": "Português", "pt": "Português",
"ptBR": "Português (Brasil)", "ptBR": "Português (Brasil)",
"ro": "", "ro": "Romanian",
"ru": "Русский", "ru": "Русский",
"sk": "Slovenčina", "sk": "Slovenčina",
"svSE": "", "svSE": "Swedish (Sweden)",
"tr": "Türkçe", "tr": "Türkçe",
"ua": "Українська", "uk": "Українська",
"zhCN": "中文 (简体)", "zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)" "zhTW": "中文 (繁體)"
}, },

View File

@ -1,7 +1,9 @@
{ {
"buttons": { "buttons": {
"cancel": "Cancelar", "cancel": "Cancelar",
"clear": "Limpar",
"close": "Fechar", "close": "Fechar",
"continue": "Continuar",
"copy": "Copiar", "copy": "Copiar",
"copyFile": "Copiar ficheiro", "copyFile": "Copiar ficheiro",
"copyToClipboard": "Copiar", "copyToClipboard": "Copiar",
@ -46,7 +48,6 @@
}, },
"files": { "files": {
"body": "Corpo", "body": "Corpo",
"clear": "Limpar",
"closePreview": "Fechar pré-visualização", "closePreview": "Fechar pré-visualização",
"files": "Ficheiros", "files": "Ficheiros",
"folders": "Pastas", "folders": "Pastas",
@ -79,27 +80,28 @@
"languages": { "languages": {
"he": "עברית", "he": "עברית",
"hu": "Magyar", "hu": "Magyar",
"ar": "Árabe", "ar": "العربية",
"de": "Alemão", "de": "Deutsch",
"en": "Inglês", "el": "Ελληνικά",
"es": "Espanhol", "en": "English",
"fr": "Francês", "es": "Español",
"is": "", "fr": "Français",
"is": "Icelandic",
"it": "Italiano", "it": "Italiano",
"ja": "Japonês", "ja": "日本語",
"ko": "Coreano", "ko": "한국어",
"nlBE": "", "nlBE": "Dutch (Belgium)",
"pl": "Polaco", "pl": "Polski",
"pt": "Português", "pt": "Português",
"ptBR": "Português (Brasil)", "ptBR": "Português (Brasil)",
"ro": "", "ro": "Romanian",
"ru": "Russo", "ru": "Русский",
"sk": "Slovenčina", "sk": "Slovenčina",
"svSE": "", "svSE": "Swedish (Sweden)",
"tr": "Türkçe", "tr": "Türkçe",
"ua": "Українська", "uk": "Українська",
"zhCN": "Chinês simplificado", "zhCN": "中文 (简体)",
"zhTW": "Chinês tradicional" "zhTW": "中文 (繁體)"
}, },
"login": { "login": {
"createAnAccount": "Criar uma conta", "createAnAccount": "Criar uma conta",

View File

@ -1,6 +1,7 @@
{ {
"buttons": { "buttons": {
"cancel": "Anulează", "cancel": "Anulează",
"clear": "Curăță",
"close": "Închide", "close": "Închide",
"copy": "Copiază", "copy": "Copiază",
"copyFile": "Copiază fișier", "copyFile": "Copiază fișier",
@ -46,7 +47,6 @@
}, },
"files": { "files": {
"body": "Corp", "body": "Corp",
"clear": "Curăță",
"closePreview": "Închide previzualizarea", "closePreview": "Închide previzualizarea",
"files": "Fișiere", "files": "Fișiere",
"folders": "Directoare", "folders": "Directoare",
@ -81,23 +81,24 @@
"hu": "Magyar", "hu": "Magyar",
"ar": "العربية", "ar": "العربية",
"de": "Deutsch", "de": "Deutsch",
"el": "Ελληνικά",
"en": "English", "en": "English",
"es": "Español", "es": "Español",
"fr": "Français", "fr": "Français",
"is": "", "is": "Icelandic",
"it": "Italiano", "it": "Italiano",
"ja": "日本語", "ja": "日本語",
"ko": "한국어", "ko": "한국어",
"nlBE": "", "nlBE": "Dutch (Belgium)",
"pl": "Polski", "pl": "Polski",
"pt": "Português", "pt": "Português",
"ptBR": "Português (Brasil)", "ptBR": "Português (Brasil)",
"ro": "", "ro": "Romanian",
"ru": "Русский", "ru": "Русский",
"sk": "Slovenčina", "sk": "Slovenčina",
"svSE": "", "svSE": "Swedish (Sweden)",
"tr": "Türkçe", "tr": "Türkçe",
"ua": "Українська", "uk": "Українська",
"zhCN": "中文 (简体)", "zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)" "zhTW": "中文 (繁體)"
}, },

View File

@ -1,6 +1,7 @@
{ {
"buttons": { "buttons": {
"cancel": "Отмена", "cancel": "Отмена",
"clear": "Очистить",
"close": "Закрыть", "close": "Закрыть",
"copy": "Копировать", "copy": "Копировать",
"copyFile": "Скопировать файл", "copyFile": "Скопировать файл",
@ -51,7 +52,6 @@
}, },
"files": { "files": {
"body": "Тело", "body": "Тело",
"clear": "Очистить",
"closePreview": "Закрыть", "closePreview": "Закрыть",
"files": "Файлы", "files": "Файлы",
"folders": "Папки", "folders": "Папки",
@ -87,6 +87,7 @@
"hu": "Magyar", "hu": "Magyar",
"ar": "العربية", "ar": "العربية",
"de": "Deutsch", "de": "Deutsch",
"el": "Ελληνικά",
"en": "English", "en": "English",
"es": "Español", "es": "Español",
"fr": "Français", "fr": "Français",
@ -103,7 +104,7 @@
"sk": "Slovenčina", "sk": "Slovenčina",
"svSE": "Swedish (Sweden)", "svSE": "Swedish (Sweden)",
"tr": "Türkçe", "tr": "Türkçe",
"ua": "Українська", "uk": "Українська",
"zhCN": "中文 (简体)", "zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)" "zhTW": "中文 (繁體)"
}, },

View File

@ -1,6 +1,7 @@
{ {
"buttons": { "buttons": {
"cancel": "Zrušiť", "cancel": "Zrušiť",
"clear": "Zrušiť výber",
"close": "Zavrieť", "close": "Zavrieť",
"copy": "Kopírovať", "copy": "Kopírovať",
"copyFile": "Kopírovať súbor", "copyFile": "Kopírovať súbor",
@ -51,7 +52,6 @@
}, },
"files": { "files": {
"body": "Telo", "body": "Telo",
"clear": "Zrušiť výber",
"closePreview": "Zavrieť náhľad", "closePreview": "Zavrieť náhľad",
"files": "Súbory", "files": "Súbory",
"folders": "Priečinky", "folders": "Priečinky",
@ -87,6 +87,7 @@
"hu": "Magyar", "hu": "Magyar",
"ar": "العربية", "ar": "العربية",
"de": "Deutsch", "de": "Deutsch",
"el": "Ελληνικά",
"en": "English", "en": "English",
"es": "Español", "es": "Español",
"fr": "Français", "fr": "Français",
@ -103,7 +104,7 @@
"sk": "Slovenčina", "sk": "Slovenčina",
"svSE": "Swedish (Sweden)", "svSE": "Swedish (Sweden)",
"tr": "Türkçe", "tr": "Türkçe",
"ua": "Українська", "uk": "Українська",
"zhCN": "中文 (简体)", "zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)" "zhTW": "中文 (繁體)"
}, },

View File

@ -1,6 +1,7 @@
{ {
"buttons": { "buttons": {
"cancel": "Avbryt", "cancel": "Avbryt",
"clear": "Rensa",
"close": "Stäng", "close": "Stäng",
"copy": "Kopiera", "copy": "Kopiera",
"copyFile": "Kopiera fil", "copyFile": "Kopiera fil",
@ -46,7 +47,6 @@
}, },
"files": { "files": {
"body": "Huvud", "body": "Huvud",
"clear": "Rensa",
"closePreview": "Stäng förhands granskningen", "closePreview": "Stäng förhands granskningen",
"files": "Filer", "files": "Filer",
"folders": "Mappar", "folders": "Mappar",
@ -81,23 +81,24 @@
"hu": "Magyar", "hu": "Magyar",
"ar": "العربية", "ar": "العربية",
"de": "Deutsch", "de": "Deutsch",
"el": "Ελληνικά",
"en": "English", "en": "English",
"es": "Español", "es": "Español",
"fr": "Français", "fr": "Français",
"is": "", "is": "Icelandic",
"it": "Italiano", "it": "Italiano",
"ja": "日本語", "ja": "日本語",
"ko": "한국어", "ko": "한국어",
"nlBE": "", "nlBE": "Dutch (Belgium)",
"pl": "Polski", "pl": "Polski",
"pt": "Português", "pt": "Português",
"ptBR": "Português (Brasil)", "ptBR": "Português (Brasil)",
"ro": "", "ro": "Romanian",
"ru": "Русский", "ru": "Русский",
"sk": "Slovenčina", "sk": "Slovenčina",
"svSE": "", "svSE": "Swedish (Sweden)",
"tr": "Türkçe", "tr": "Türkçe",
"ua": "Українська", "uk": "Українська",
"zhCN": "中文 (简体)", "zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)" "zhTW": "中文 (繁體)"
}, },

View File

@ -1,6 +1,7 @@
{ {
"buttons": { "buttons": {
"cancel": "Vazgeç", "cancel": "Vazgeç",
"clear": "Temizle",
"close": "Kapat", "close": "Kapat",
"copy": "Kopyala", "copy": "Kopyala",
"copyFile": "Dosyayı kopyala", "copyFile": "Dosyayı kopyala",
@ -49,7 +50,6 @@
}, },
"files": { "files": {
"body": "Sayfa", "body": "Sayfa",
"clear": "Temizle",
"closePreview": "Önizlemeyi kapat", "closePreview": "Önizlemeyi kapat",
"files": "Dosyalar", "files": "Dosyalar",
"folders": "Klasörler", "folders": "Klasörler",
@ -85,6 +85,7 @@
"hu": "Magyar", "hu": "Magyar",
"ar": "العربية", "ar": "العربية",
"de": "Deutsch", "de": "Deutsch",
"el": "Ελληνικά",
"en": "English", "en": "English",
"es": "Español", "es": "Español",
"fr": "Français", "fr": "Français",
@ -101,7 +102,7 @@
"sk": "Slovenčina", "sk": "Slovenčina",
"svSE": "Swedish (Sweden)", "svSE": "Swedish (Sweden)",
"tr": "Türkçe", "tr": "Türkçe",
"ua": "Українська", "uk": "Українська",
"zhCN": "中文 (简体)", "zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)" "zhTW": "中文 (繁體)"
}, },

View File

@ -1,270 +0,0 @@
{
"buttons": {
"cancel": "Відмінити",
"close": "Закрити",
"copy": "Копіювати",
"copyFile": "Копіювати файл",
"copyToClipboard": "Копіювати в буфер обміну",
"create": "Створити",
"delete": "Видалити",
"download": "Завантажити",
"file": "Файл",
"folder": "Папка",
"hideDotfiles": "Приховати точкові файли",
"info": "Інфо",
"more": "Більше",
"move": "Перемістити",
"moveFile": "Перемістити файл",
"new": "Новий",
"next": "Далі",
"ok": "ОК",
"permalink": "Отримати постійне посилання",
"previous": "Назад",
"publish": "Опублікувати",
"rename": "Перейменувати",
"replace": "Замінити",
"reportIssue": "Повідомити про помилку",
"save": "Зберегти",
"schedule": "Планування",
"search": "Пошук",
"select": "Вибрати",
"selectMultiple": "Мультивибір",
"share": "Поділитися",
"shell": "Командний рядок",
"submit": "Відправити",
"switchView": "Вид",
"toggleSidebar": "Бічна панель",
"update": "Оновити",
"upload": "Завантажити",
"openFile": "Відкрити файл"
},
"download": {
"downloadFile": "Завантажити файл",
"downloadFolder": "Завантажити папку",
"downloadSelected": "Завантажити вибране"
},
"errors": {
"forbidden": "У вас немає прав доступу до цього.",
"internal": "Щось пішло не так.",
"notFound": "Неправильне посилання.",
"connection": "Немає підключення до сервера."
},
"files": {
"body": "Тіло",
"clear": "Очистити",
"closePreview": "Закрити",
"files": "Файли",
"folders": "Папки",
"home": "Домівка",
"lastModified": "Останній раз змінено",
"loading": "Завантаження...",
"lonely": "Тут пусто...",
"metadata": "Метадані",
"multipleSelectionEnabled": "Мультивибір включений",
"name": "Ім'я",
"size": "Розмір",
"sortByLastModified": "Сортувати за останнім зміненням",
"sortByName": "Сортувати за іменем",
"sortBySize": "Сортувати за розміром",
"noPreview": "Попередній перегляд для цього файлу недоступний."
},
"help": {
"click": "вибрати файл чи каталог",
"ctrl": {
"click": "вибрати кілька файлів чи каталогів",
"f": "відкрити пошук",
"s": "скачати файл або поточний каталог"
},
"del": "видалити вибрані елементи",
"doubleClick": "відкрити файл чи каталог",
"esc": "очистити виділення та/або закрити вікно",
"f1": "допомога",
"f2": "перейменувати файл",
"help": "Допомога"
},
"languages": {
"he": "עברית",
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"en": "English",
"es": "Español",
"fr": "Français",
"is": "Icelandic",
"it": "Italiano",
"ja": "日本語",
"ko": "한국어",
"nlBE": "Dutch (Belgium)",
"pl": "Polski",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ro": "Romanian",
"ru": "Русский",
"sk": "Slovenčina",
"svSE": "Swedish (Sweden)",
"tr": "Türkçe",
"ua": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
},
"login": {
"createAnAccount": "Створити обліковий запис",
"loginInstead": "Вже є обліковий запис",
"password": "Пароль",
"passwordConfirm": "Підтвердження паролю",
"passwordsDontMatch": "Паролі не співпадають",
"signup": "Зареєструватися",
"submit": "Увійти",
"username": "Ім'я користувача",
"usernameTaken": "Ім'я користувача вже використовується",
"wrongCredentials": "Невірне ім'я користувача або пароль"
},
"permanent": "Постійний",
"prompts": {
"copy": "Копіювати",
"copyMessage": "Копіювати в:",
"currentlyNavigating": "Поточний каталог:",
"deleteMessageMultiple": "Видалити ці файли ({count})?",
"deleteMessageSingle": "Видалити цей файл/каталог?",
"deleteMessageShare": "Видалити цей спільний файл/каталог ({path})?",
"deleteTitle": "Видалити файлы",
"displayName": "Відображене ім'я:",
"download": "Завантажити файлы",
"downloadMessage": "Виберіть формат, в якому хочете завантажити.",
"error": "Помилка",
"fileInfo": "Інформація про файл",
"filesSelected": "Файлів вибрано: {count}.",
"lastModified": "Останній раз змінено",
"move": "Перемістити",
"moveMessage": "Перемістити в:",
"newArchetype": "Створіть новий запис на основі архетипу. Файл буде створено у каталозі.",
"newDir": "Новий каталог",
"newDirMessage": "Ім'я нового каталогу.",
"newFile": "Новий файл",
"newFileMessage": "Ім'я нового файлу.",
"numberDirs": "Кількість каталогів",
"numberFiles": "Кількість файлів",
"rename": "Перейменувати",
"renameMessage": "Нове ім'я",
"replace": "Замінити",
"replaceMessage": "Ім'я одного з файлів, що завантажуються, збігається з вже існуючим файлом. Ви бажаєте замінити існуючий?\n",
"schedule": "Планування",
"scheduleMessage": "Запланувати дату та час публікації.",
"show": "Показати",
"size": "Розмір",
"upload": "Завантажити",
"uploadMessage": "Виберіть варіант для завантаження.",
"optionalPassword": "Необов'язковий пароль"
},
"search": {
"images": "Зображення",
"music": "Музика",
"pdf": "PDF",
"pressToSearch": "Натисніть ENTER для пошуку",
"search": "Пошук...",
"typeToSearch": "Введіть ім'я файлу...",
"types": "Типи",
"video": "Відео"
},
"settings": {
"admin": "Адмін",
"administrator": "Адміністратор",
"allowCommands": "Запуск команд",
"allowEdit": "Редагування, перейменування та видалення файлів чи каталогів",
"allowNew": "Створення нових файлів або каталогів",
"allowPublish": "Публікація нових записів та сторінок",
"allowSignup": "Дозволити користувачам реєструватися",
"avoidChanges": "(залишіть поле порожнім, щоб уникнути змін)",
"branding": "Брендинг",
"brandingDirectoryPath": "Шлях до каталогу брендів",
"brandingHelp": "Ви можете налаштувати зовнішній вигляд файлового браузера, змінивши його ім'я, замінивши логотип, додавши власні стилі та навіть відключивши зовнішні посилання на GitHub.\nДодаткову інформацію про персоналізований брендинг можна знайти на сторінці {0}.",
"changePassword": "Зміна пароля",
"commandRunner": "Запуск команд",
"commandRunnerHelp": "Тут ви можете встановити команди, які будуть виконуватися у зазначених подіях. Ви повинні вказати по одній команді в кожному рядку. Змінні середовища {0} та {1} будуть доступні, будучи {0} щодо {1}. Додаткові відомості про цю функцію та доступні змінні середовища див. у {2}.",
"commandsUpdated": "Команди оновлені!",
"createUserDir": "Автоматичне створення домашнього каталогу користувача при додаванні нового користувача",
"customStylesheet": "Свій стиль",
"defaultUserDescription": "Це налаштування за замовчуванням для нових користувачів.",
"disableExternalLinks": "Вимкнути зовнішні посилання (крім документації)",
"disableUsedDiskPercentage": "Disable used disk percentage graph",
"documentation": "документація",
"examples": "Приклади",
"executeOnShell": "Виконати в командному рядку",
"executeOnShellDescription": "За замовчуванням File Browser виконує команди, безпосередньо викликаючи їх бінарні файли. Якщо ви хочете замість цього запускати їх в оболонці (наприклад, Bash або PowerShell), ви можете визначити їх тут з необхідними аргументами та прапорами. Якщо встановлено, виконуєма вами команда буде додана як аргумент. Це стосується як користувацьких команд, так і обробників подій.",
"globalRules": "Це глобальний набір дозволяючих та забороняючих правил. Вони застосовні до кожного користувача. Ви можете визначити певні правила для налаштувань кожного користувача, щоб перевизначити їх.",
"globalSettings": "Глобальні налаштування",
"hideDotfiles": "Приховати точкові файли",
"insertPath": "Вставте шлях",
"insertRegex": "Вставити регулярний вираз",
"instanceName": "Поточна назва програми",
"language": "Мова",
"lockPassword": "Заборонити користувачеві змінювати пароль",
"newPassword": "Новий пароль",
"newPasswordConfirm": "Підтвердження нового пароля",
"newUser": "Новий користувач",
"password": "Пароль",
"passwordUpdated": "Пароль оновлено!",
"path": "Шлях",
"perm": {
"create": "Створювати файли та каталоги",
"delete": "Видаляти файли та каталоги",
"download": "Завантажувати",
"execute": "Виконувати команди",
"modify": "Редагувати файли",
"rename": "Перейменовувати або переміщувати файли та каталоги",
"share": "Ділітися файлами"
},
"permissions": "Дозволи",
"permissionsHelp": "Можна настроїти користувача як адміністратора або вибрати індивідуальні дозволи. При виборі \"Адміністратор\" всі інші параметри будуть автоматично вибрані. Керування користувачами - привілей адміністратора.\n",
"profileSettings": "Налаштування профілю",
"ruleExample1": "запобігти доступу до будь-якого прихованого файлу (наприклад: .git, .gitignore) у кожній папці.\n",
"ruleExample2": "блокує доступ до файлу з ім'ям Caddyfile у кореневій області.",
"rules": "Права",
"rulesHelp": "Тут ви можете визначити набір дозволяючих та забороняючих правил для цього конкретного користувача. Блоковані файли не відображатимуться у списках, і не будуть доступні для користувача. Є підтримка регулярних виразів та відносних шляхів.\n",
"scope": "Корінь",
"setDateFormat": "Встановити точний формат дати",
"settingsUpdated": "Налаштування застосовані!",
"shareDuration": "Тривалість спільного посилання",
"shareManagement": "Управління спільними посиланнями",
"shareDeleted": "Спільне посилання видалено!",
"singleClick": "Відкриття файлів та каталогів одним кліком",
"themes": {
"dark": "Темна",
"light": "Світла",
"title": "Тема"
},
"user": "Користувач",
"userCommands": "Команди",
"userCommandsHelp": "Список команд, доступних користувачу, розділений пробілами. Приклад:\n",
"userCreated": "Користувач створений!",
"userDefaults": "Налаштування користувача за замовчуванням",
"userDeleted": "Користувач видалений!",
"userManagement": "Керування користувачами",
"userUpdated": "Користувач змінений!",
"username": "Ім'я користувача",
"users": "Користувачі"
},
"sidebar": {
"help": "Допомога",
"hugoNew": "Hugo New",
"login": "Увійти",
"logout": "Вийти",
"myFiles": "Файли",
"newFile": "Новий файл",
"newFolder": "Новий каталог",
"preview": "Перегляд",
"settings": "Налаштування",
"signup": "Зареєструватися",
"siteSettings": "Налаштування сайту"
},
"success": {
"linkCopied": "Посилання скопійоване!"
},
"time": {
"days": "Дні",
"hours": "Години",
"minutes": "Хвилини",
"seconds": "Секунди",
"unit": "Одиниця часу"
}
}

271
frontend/src/i18n/uk.json Normal file
View File

@ -0,0 +1,271 @@
{
"buttons": {
"cancel": "Відмінити",
"clear": "Очистити",
"close": "Закрити",
"copy": "Копіювати",
"copyFile": "Копіювати файл",
"copyToClipboard": "Копіювати в буфер обміну",
"create": "Створити",
"delete": "Видалити",
"download": "Завантажити",
"file": "Файл",
"folder": "Папка",
"hideDotfiles": "Приховати точкові файли",
"info": "Інфо",
"more": "Більше",
"move": "Перемістити",
"moveFile": "Перемістити файл",
"new": "Новий",
"next": "Далі",
"ok": "ОК",
"permalink": "Отримати постійне посилання",
"previous": "Назад",
"publish": "Опублікувати",
"rename": "Перейменувати",
"replace": "Замінити",
"reportIssue": "Повідомити про помилку",
"save": "Зберегти",
"schedule": "Планування",
"search": "Пошук",
"select": "Вибрати",
"selectMultiple": "Мультивибір",
"share": "Поділитися",
"shell": "Командний рядок",
"submit": "Відправити",
"switchView": "Вид",
"toggleSidebar": "Бічна панель",
"update": "Оновити",
"upload": "Завантажити",
"openFile": "Відкрити файл"
},
"download": {
"downloadFile": "Завантажити файл",
"downloadFolder": "Завантажити папку",
"downloadSelected": "Завантажити вибране"
},
"errors": {
"forbidden": "У вас немає прав доступу до цього.",
"internal": "Щось пішло не так.",
"notFound": "Неправильне посилання.",
"connection": "Немає підключення до сервера."
},
"files": {
"body": "Тіло",
"closePreview": "Закрити",
"files": "Файли",
"folders": "Папки",
"home": "Домівка",
"lastModified": "Останній раз змінено",
"loading": "Завантаження...",
"lonely": "Тут пусто...",
"metadata": "Метадані",
"multipleSelectionEnabled": "Мультивибір включений",
"name": "Ім'я",
"size": "Розмір",
"sortByLastModified": "Сортувати за останнім зміненням",
"sortByName": "Сортувати за іменем",
"sortBySize": "Сортувати за розміром",
"noPreview": "Попередній перегляд для цього файлу недоступний."
},
"help": {
"click": "вибрати файл чи каталог",
"ctrl": {
"click": "вибрати кілька файлів чи каталогів",
"f": "відкрити пошук",
"s": "скачати файл або поточний каталог"
},
"del": "видалити вибрані елементи",
"doubleClick": "відкрити файл чи каталог",
"esc": "очистити виділення та/або закрити вікно",
"f1": "допомога",
"f2": "перейменувати файл",
"help": "Допомога"
},
"languages": {
"he": "עברית",
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"fr": "Français",
"is": "Icelandic",
"it": "Italiano",
"ja": "日本語",
"ko": "한국어",
"nlBE": "Dutch (Belgium)",
"pl": "Polski",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ro": "Romanian",
"ru": "Русский",
"sk": "Slovenčina",
"svSE": "Swedish (Sweden)",
"tr": "Türkçe",
"uk": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
},
"login": {
"createAnAccount": "Створити обліковий запис",
"loginInstead": "Вже є обліковий запис",
"password": "Пароль",
"passwordConfirm": "Підтвердження паролю",
"passwordsDontMatch": "Паролі не співпадають",
"signup": "Зареєструватися",
"submit": "Увійти",
"username": "Ім'я користувача",
"usernameTaken": "Ім'я користувача вже використовується",
"wrongCredentials": "Невірне ім'я користувача або пароль"
},
"permanent": "Постійний",
"prompts": {
"copy": "Копіювати",
"copyMessage": "Копіювати в:",
"currentlyNavigating": "Поточний каталог:",
"deleteMessageMultiple": "Видалити ці файли ({count})?",
"deleteMessageSingle": "Видалити цей файл/каталог?",
"deleteMessageShare": "Видалити цей спільний файл/каталог ({path})?",
"deleteTitle": "Видалити файлы",
"displayName": "Відображене ім'я:",
"download": "Завантажити файлы",
"downloadMessage": "Виберіть формат, в якому хочете завантажити.",
"error": "Помилка",
"fileInfo": "Інформація про файл",
"filesSelected": "Файлів вибрано: {count}.",
"lastModified": "Останній раз змінено",
"move": "Перемістити",
"moveMessage": "Перемістити в:",
"newArchetype": "Створіть новий запис на основі архетипу. Файл буде створено у каталозі.",
"newDir": "Новий каталог",
"newDirMessage": "Ім'я нового каталогу.",
"newFile": "Новий файл",
"newFileMessage": "Ім'я нового файлу.",
"numberDirs": "Кількість каталогів",
"numberFiles": "Кількість файлів",
"rename": "Перейменувати",
"renameMessage": "Нове ім'я",
"replace": "Замінити",
"replaceMessage": "Ім'я одного з файлів, що завантажуються, збігається з вже існуючим файлом. Ви бажаєте замінити існуючий?\n",
"schedule": "Планування",
"scheduleMessage": "Запланувати дату та час публікації.",
"show": "Показати",
"size": "Розмір",
"upload": "Завантажити",
"uploadMessage": "Виберіть варіант для завантаження.",
"optionalPassword": "Необов'язковий пароль"
},
"search": {
"images": "Зображення",
"music": "Музика",
"pdf": "PDF",
"pressToSearch": "Натисніть ENTER для пошуку",
"search": "Пошук...",
"typeToSearch": "Введіть ім'я файлу...",
"types": "Типи",
"video": "Відео"
},
"settings": {
"admin": "Адмін",
"administrator": "Адміністратор",
"allowCommands": "Запуск команд",
"allowEdit": "Редагування, перейменування та видалення файлів чи каталогів",
"allowNew": "Створення нових файлів або каталогів",
"allowPublish": "Публікація нових записів та сторінок",
"allowSignup": "Дозволити користувачам реєструватися",
"avoidChanges": "(залишіть поле порожнім, щоб уникнути змін)",
"branding": "Брендинг",
"brandingDirectoryPath": "Шлях до каталогу брендів",
"brandingHelp": "Ви можете налаштувати зовнішній вигляд файлового браузера, змінивши його ім'я, замінивши логотип, додавши власні стилі та навіть відключивши зовнішні посилання на GitHub.\nДодаткову інформацію про персоналізований брендинг можна знайти на сторінці {0}.",
"changePassword": "Зміна пароля",
"commandRunner": "Запуск команд",
"commandRunnerHelp": "Тут ви можете встановити команди, які будуть виконуватися у зазначених подіях. Ви повинні вказати по одній команді в кожному рядку. Змінні середовища {0} та {1} будуть доступні, будучи {0} щодо {1}. Додаткові відомості про цю функцію та доступні змінні середовища див. у {2}.",
"commandsUpdated": "Команди оновлені!",
"createUserDir": "Автоматичне створення домашнього каталогу користувача при додаванні нового користувача",
"customStylesheet": "Свій стиль",
"defaultUserDescription": "Це налаштування за замовчуванням для нових користувачів.",
"disableExternalLinks": "Вимкнути зовнішні посилання (крім документації)",
"disableUsedDiskPercentage": "Disable used disk percentage graph",
"documentation": "документація",
"examples": "Приклади",
"executeOnShell": "Виконати в командному рядку",
"executeOnShellDescription": "За замовчуванням File Browser виконує команди, безпосередньо викликаючи їх бінарні файли. Якщо ви хочете замість цього запускати їх в оболонці (наприклад, Bash або PowerShell), ви можете визначити їх тут з необхідними аргументами та прапорами. Якщо встановлено, виконуєма вами команда буде додана як аргумент. Це стосується як користувацьких команд, так і обробників подій.",
"globalRules": "Це глобальний набір дозволяючих та забороняючих правил. Вони застосовні до кожного користувача. Ви можете визначити певні правила для налаштувань кожного користувача, щоб перевизначити їх.",
"globalSettings": "Глобальні налаштування",
"hideDotfiles": "Приховати точкові файли",
"insertPath": "Вставте шлях",
"insertRegex": "Вставити регулярний вираз",
"instanceName": "Поточна назва програми",
"language": "Мова",
"lockPassword": "Заборонити користувачеві змінювати пароль",
"newPassword": "Новий пароль",
"newPasswordConfirm": "Підтвердження нового пароля",
"newUser": "Новий користувач",
"password": "Пароль",
"passwordUpdated": "Пароль оновлено!",
"path": "Шлях",
"perm": {
"create": "Створювати файли та каталоги",
"delete": "Видаляти файли та каталоги",
"download": "Завантажувати",
"execute": "Виконувати команди",
"modify": "Редагувати файли",
"rename": "Перейменовувати або переміщувати файли та каталоги",
"share": "Ділітися файлами"
},
"permissions": "Дозволи",
"permissionsHelp": "Можна настроїти користувача як адміністратора або вибрати індивідуальні дозволи. При виборі \"Адміністратор\" всі інші параметри будуть автоматично вибрані. Керування користувачами - привілей адміністратора.\n",
"profileSettings": "Налаштування профілю",
"ruleExample1": "запобігти доступу до будь-якого прихованого файлу (наприклад: .git, .gitignore) у кожній папці.\n",
"ruleExample2": "блокує доступ до файлу з ім'ям Caddyfile у кореневій області.",
"rules": "Права",
"rulesHelp": "Тут ви можете визначити набір дозволяючих та забороняючих правил для цього конкретного користувача. Блоковані файли не відображатимуться у списках, і не будуть доступні для користувача. Є підтримка регулярних виразів та відносних шляхів.\n",
"scope": "Корінь",
"setDateFormat": "Встановити точний формат дати",
"settingsUpdated": "Налаштування застосовані!",
"shareDuration": "Тривалість спільного посилання",
"shareManagement": "Управління спільними посиланнями",
"shareDeleted": "Спільне посилання видалено!",
"singleClick": "Відкриття файлів та каталогів одним кліком",
"themes": {
"dark": "Темна",
"light": "Світла",
"title": "Тема"
},
"user": "Користувач",
"userCommands": "Команди",
"userCommandsHelp": "Список команд, доступних користувачу, розділений пробілами. Приклад:\n",
"userCreated": "Користувач створений!",
"userDefaults": "Налаштування користувача за замовчуванням",
"userDeleted": "Користувач видалений!",
"userManagement": "Керування користувачами",
"userUpdated": "Користувач змінений!",
"username": "Ім'я користувача",
"users": "Користувачі"
},
"sidebar": {
"help": "Допомога",
"hugoNew": "Hugo New",
"login": "Увійти",
"logout": "Вийти",
"myFiles": "Файли",
"newFile": "Новий файл",
"newFolder": "Новий каталог",
"preview": "Перегляд",
"settings": "Налаштування",
"signup": "Зареєструватися",
"siteSettings": "Налаштування сайту"
},
"success": {
"linkCopied": "Посилання скопійоване!"
},
"time": {
"days": "Дні",
"hours": "Години",
"minutes": "Хвилини",
"seconds": "Секунди",
"unit": "Одиниця часу"
}
}

View File

@ -1,6 +1,7 @@
{ {
"buttons": { "buttons": {
"cancel": "取消", "cancel": "取消",
"clear": "清空",
"close": "关闭", "close": "关闭",
"copy": "复制", "copy": "复制",
"copyFile": "复制文件", "copyFile": "复制文件",
@ -56,7 +57,6 @@
}, },
"files": { "files": {
"body": "内容", "body": "内容",
"clear": "清空",
"closePreview": "关闭预览", "closePreview": "关闭预览",
"files": "文件", "files": "文件",
"folders": "文件夹", "folders": "文件夹",
@ -103,15 +103,15 @@
"nlBE": "Dutch (Belgium)", "nlBE": "Dutch (Belgium)",
"pl": "Polski", "pl": "Polski",
"pt": "Português", "pt": "Português",
"ptBR": "PortuguêsBrasil", "ptBR": "Português (Brasil)",
"ro": "Romanian", "ro": "Romanian",
"ru": "Русский", "ru": "Русский",
"sk": "Slovenčina", "sk": "Slovenčina",
"svSE": "SwedishSweden", "svSE": "Swedish (Sweden)",
"tr": "Türkçe", "tr": "Türkçe",
"ua": "Українська", "uk": "Українська",
"zhCN": "中文(简体)", "zhCN": "中文 (简体)",
"zhTW": "中文(繁體)" "zhTW": "中文 (繁體)"
}, },
"login": { "login": {
"createAnAccount": "创建用户", "createAnAccount": "创建用户",

View File

@ -1,6 +1,7 @@
{ {
"buttons": { "buttons": {
"cancel": "取消", "cancel": "取消",
"clear": "清空",
"close": "關閉", "close": "關閉",
"copy": "複製", "copy": "複製",
"copyFile": "複製檔案", "copyFile": "複製檔案",
@ -46,7 +47,6 @@
}, },
"files": { "files": {
"body": "内容", "body": "内容",
"clear": "清空",
"closePreview": "關閉預覽", "closePreview": "關閉預覽",
"files": "檔案", "files": "檔案",
"folders": "資料夾", "folders": "資料夾",
@ -81,6 +81,7 @@
"hu": "Magyar", "hu": "Magyar",
"ar": "العربية", "ar": "العربية",
"de": "Deutsch", "de": "Deutsch",
"el": "Ελληνικά",
"en": "English", "en": "English",
"es": "Español", "es": "Español",
"fr": "Français", "fr": "Français",
@ -88,16 +89,16 @@
"it": "Italiano", "it": "Italiano",
"ja": "日本語", "ja": "日本語",
"ko": "한국어", "ko": "한국어",
"nlBE": "DutchBelgium", "nlBE": "Dutch (Belgium)",
"pl": "Polski", "pl": "Polski",
"pt": "Português", "pt": "Português",
"ptBR": "Português (Brasil)", "ptBR": "Português (Brasil)",
"ro": "Romanian", "ro": "Romanian",
"ru": "Русский", "ru": "Русский",
"sk": "Slovenčina", "sk": "Slovenčina",
"svSE": "SwedishSweden", "svSE": "Swedish (Sweden)",
"tr": "Türkçe", "tr": "Türkçe",
"ua": "Українська", "uk": "Українська",
"zhCN": "中文 (简体)", "zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)" "zhTW": "中文 (繁體)"
}, },

1
frontend/src/index.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module "*.vue";

View File

@ -1,51 +0,0 @@
import "whatwg-fetch";
import cssVars from "css-vars-ponyfill";
import { sync } from "vuex-router-sync";
import store from "@/store";
import router from "@/router";
import i18n from "@/i18n";
import Vue from "@/utils/vue";
import { recaptcha, loginPage } from "@/utils/constants";
import { login, validateLogin } from "@/utils/auth";
import App from "@/App.vue";
cssVars();
sync(store, router);
async function start() {
try {
if (loginPage) {
await validateLogin();
} else {
await login("", "", "");
}
} catch (e) {
console.log(e);
}
if (recaptcha) {
await new Promise((resolve) => {
const check = () => {
if (typeof window.grecaptcha === "undefined") {
setTimeout(check, 100);
} else {
resolve();
}
};
check();
});
}
new Vue({
el: "#app",
store,
router,
i18n,
template: "<App/>",
components: { App },
});
}
start();

109
frontend/src/main.ts Normal file
View File

@ -0,0 +1,109 @@
import { disableExternal } from "@/utils/constants";
import { createApp } from "vue";
import VueNumberInput from "@chenfengyuan/vue-number-input";
import VueLazyload from "vue-lazyload";
import { createVfm } from "vue-final-modal";
import Toast, { POSITION, useToast } from "vue-toastification";
import {
ToastOptions,
PluginOptions,
} from "vue-toastification/dist/types/types";
import createPinia from "@/stores";
import router from "@/router";
import i18n, { isRtl } from "@/i18n";
import App from "@/App.vue";
import CustomToast from "@/components/CustomToast.vue";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import relativeTime from "dayjs/plugin/relativeTime";
import duration from "dayjs/plugin/duration";
import "./css/styles.css";
// register dayjs plugins globally
dayjs.extend(localizedFormat);
dayjs.extend(relativeTime);
dayjs.extend(duration);
const pinia = createPinia(router);
const vfm = createVfm();
const app = createApp(App);
app.component(VueNumberInput.name || "vue-number-input", VueNumberInput);
app.use(VueLazyload);
app.use(Toast, {
transition: "Vue-Toastification__bounce",
maxToasts: 10,
newestOnTop: true,
} satisfies PluginOptions);
app.use(vfm);
app.use(i18n);
app.use(pinia);
app.use(router);
app.mixin({
mounted() {
// expose vue instance to components
this.$el.__vue__ = this;
},
});
// provide v-focus for components
app.directive("focus", {
mounted: async (el) => {
// initiate focus for the element
el.focus();
},
});
const toastConfig = {
position: POSITION.BOTTOM_CENTER,
timeout: 4000,
closeOnClick: true,
pauseOnFocusLoss: true,
pauseOnHover: true,
draggable: true,
draggablePercent: 0.6,
showCloseButtonOnHover: false,
hideProgressBar: false,
closeButton: "button",
icon: true,
} satisfies ToastOptions;
app.provide("$showSuccess", (message: string) => {
const $toast = useToast();
$toast.success(
{
component: CustomToast,
props: {
message: message,
},
},
{ ...toastConfig, rtl: isRtl() }
);
});
app.provide("$showError", (error: Error | string, displayReport = true) => {
const $toast = useToast();
$toast.error(
{
component: CustomToast,
props: {
message: (error as Error).message || error,
isReport: !disableExternal && displayReport,
// TODO: could you add this to the component itself?
reportText: i18n.global.t("buttons.reportIssue"),
},
},
{
...toastConfig,
timeout: 0,
rtl: isRtl(),
}
);
});
router.isReady().then(() => app.mount("#app"));

View File

@ -1,194 +0,0 @@
import Vue from "vue";
import Router from "vue-router";
import Login from "@/views/Login.vue";
import Layout from "@/views/Layout.vue";
import Files from "@/views/Files.vue";
import Share from "@/views/Share.vue";
import Users from "@/views/settings/Users.vue";
import User from "@/views/settings/User.vue";
import Settings from "@/views/Settings.vue";
import GlobalSettings from "@/views/settings/Global.vue";
import ProfileSettings from "@/views/settings/Profile.vue";
import Shares from "@/views/settings/Shares.vue";
import Errors from "@/views/Errors.vue";
import store from "@/store";
import { baseURL, name } from "@/utils/constants";
import i18n, { rtlLanguages } from "@/i18n";
Vue.use(Router);
const titles = {
Login: "sidebar.login",
Share: "buttons.share",
Files: "files.files",
Settings: "sidebar.settings",
ProfileSettings: "settings.profileSettings",
Shares: "settings.shareManagement",
GlobalSettings: "settings.globalSettings",
Users: "settings.users",
User: "settings.user",
Forbidden: "errors.forbidden",
NotFound: "errors.notFound",
InternalServerError: "errors.internal",
};
const router = new Router({
base: import.meta.env.PROD ? baseURL : "",
mode: "history",
routes: [
{
path: "/login",
name: "Login",
component: Login,
beforeEnter: (to, from, next) => {
if (store.getters.isLogged) {
return next({ path: "/files" });
}
next();
},
},
{
path: "/*",
component: Layout,
children: [
{
path: "/share/*",
name: "Share",
component: Share,
},
{
path: "/files/*",
name: "Files",
component: Files,
meta: {
requiresAuth: true,
},
},
{
path: "/settings",
name: "Settings",
component: Settings,
redirect: {
path: "/settings/profile",
},
meta: {
requiresAuth: true,
},
children: [
{
path: "/settings/profile",
name: "ProfileSettings",
component: ProfileSettings,
},
{
path: "/settings/shares",
name: "Shares",
component: Shares,
},
{
path: "/settings/global",
name: "GlobalSettings",
component: GlobalSettings,
meta: {
requiresAdmin: true,
},
},
{
path: "/settings/users",
name: "Users",
component: Users,
meta: {
requiresAdmin: true,
},
},
{
path: "/settings/users/*",
name: "User",
component: User,
meta: {
requiresAdmin: true,
},
},
],
},
{
path: "/403",
name: "Forbidden",
component: Errors,
props: {
errorCode: 403,
showHeader: true,
},
},
{
path: "/404",
name: "NotFound",
component: Errors,
props: {
errorCode: 404,
showHeader: true,
},
},
{
path: "/500",
name: "InternalServerError",
component: Errors,
props: {
errorCode: 500,
showHeader: true,
},
},
{
path: "/files",
redirect: {
path: "/files/",
},
},
{
path: "/*",
redirect: (to) => `/files${to.path}`,
},
],
},
],
});
router.beforeEach((to, from, next) => {
const title = i18n.t(titles[to.name]);
document.title = title + " - " + name;
/*** RTL related settings per route ****/
const rtlSet = document.querySelector("body").classList.contains("rtl");
const shouldSetRtl = rtlLanguages.includes(i18n.locale);
switch (true) {
case shouldSetRtl && !rtlSet:
document.querySelector("body").classList.add("rtl");
break;
case !shouldSetRtl && rtlSet:
document.querySelector("body").classList.remove("rtl");
break;
}
if (to.matched.some((record) => record.meta.requiresAuth)) {
if (!store.getters.isLogged) {
next({
path: "/login",
query: { redirect: to.fullPath },
});
return;
}
if (to.matched.some((record) => record.meta.requiresAdmin)) {
if (!store.state.user.perm.admin) {
next({ path: "/403" });
return;
}
}
}
next();
});
export default router;

View File

@ -0,0 +1,220 @@
import { RouteLocation, createRouter, createWebHistory } from "vue-router";
import Login from "@/views/Login.vue";
import Layout from "@/views/Layout.vue";
import Files from "@/views/Files.vue";
import Share from "@/views/Share.vue";
import Users from "@/views/settings/Users.vue";
import User from "@/views/settings/User.vue";
import Settings from "@/views/Settings.vue";
import GlobalSettings from "@/views/settings/Global.vue";
import ProfileSettings from "@/views/settings/Profile.vue";
import Shares from "@/views/settings/Shares.vue";
import Errors from "@/views/Errors.vue";
import { useAuthStore } from "@/stores/auth";
import { baseURL, name } from "@/utils/constants";
import i18n from "@/i18n";
import { recaptcha, loginPage } from "@/utils/constants";
import { login, validateLogin } from "@/utils/auth";
const titles = {
Login: "sidebar.login",
Share: "buttons.share",
Files: "files.files",
Settings: "sidebar.settings",
ProfileSettings: "settings.profileSettings",
Shares: "settings.shareManagement",
GlobalSettings: "settings.globalSettings",
Users: "settings.users",
User: "settings.user",
Forbidden: "errors.forbidden",
NotFound: "errors.notFound",
InternalServerError: "errors.internal",
};
const routes = [
{
path: "/login",
name: "Login",
component: Login,
},
{
path: "/share",
component: Layout,
children: [
{
path: ":path*",
name: "Share",
component: Share,
},
],
},
{
path: "/files",
component: Layout,
meta: {
requiresAuth: true,
},
children: [
{
path: ":path*",
name: "Files",
component: Files,
},
],
},
{
path: "/settings",
component: Layout,
meta: {
requiresAuth: true,
},
children: [
{
path: "",
name: "Settings",
component: Settings,
redirect: {
path: "/settings/profile",
},
children: [
{
path: "profile",
name: "ProfileSettings",
component: ProfileSettings,
},
{
path: "shares",
name: "Shares",
component: Shares,
},
{
path: "global",
name: "GlobalSettings",
component: GlobalSettings,
meta: {
requiresAdmin: true,
},
},
{
path: "users",
name: "Users",
component: Users,
meta: {
requiresAdmin: true,
},
},
{
path: "users/:id",
name: "User",
component: User,
meta: {
requiresAdmin: true,
},
},
],
},
],
},
{
path: "/403",
name: "Forbidden",
component: Errors,
props: {
errorCode: 403,
showHeader: true,
},
},
{
path: "/404",
name: "NotFound",
component: Errors,
props: {
errorCode: 404,
showHeader: true,
},
},
{
path: "/500",
name: "InternalServerError",
component: Errors,
props: {
errorCode: 500,
showHeader: true,
},
},
{
path: "/:catchAll(.*)*",
redirect: (to: RouteLocation) =>
`/files/${[...to.params.catchAll].join("/")}`,
},
];
async function initAuth() {
if (loginPage) {
await validateLogin();
} else {
await login("", "", "");
}
if (recaptcha) {
await new Promise<void>((resolve) => {
const check = () => {
if (typeof window.grecaptcha === "undefined") {
setTimeout(check, 100);
} else {
resolve();
}
};
check();
});
}
}
const router = createRouter({
history: createWebHistory(baseURL),
routes,
});
router.beforeResolve(async (to, from, next) => {
const title = i18n.global.t(titles[to.name as keyof typeof titles]);
document.title = title + " - " + name;
const authStore = useAuthStore();
// this will only be null on first route
if (from.name == null) {
try {
await initAuth();
} catch (error) {
console.error(error);
}
}
if (to.path.endsWith("/login") && authStore.isLoggedIn) {
next({ path: "/files/" });
return;
}
if (to.matched.some((record) => record.meta.requiresAuth)) {
if (!authStore.isLoggedIn) {
next({
path: "/login",
query: { redirect: to.fullPath },
});
return;
}
if (to.matched.some((record) => record.meta.requiresAdmin)) {
if (authStore.user === null || !authStore.user.perm.admin) {
next({ path: "/403" });
return;
}
}
}
next();
});
export { router, router as default };

Some files were not shown because too many files have changed in this diff Show More