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/
build/
# Vue distributable files
/frontend/dist/*
!/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"
"image"
"io"
"io/fs"
"log"
"mime"
"net/http"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"time"
@ -27,6 +29,11 @@ import (
const PermFile = 0644
const PermDir = 0755
var (
reSubDirs = regexp.MustCompile("(?i)^sub(s|titles)$")
reSubExts = regexp.MustCompile("(?i)(.vtt|.srt|.ass|.ssa)$")
)
// FileInfo describes a file.
type FileInfo struct {
*Listing
@ -277,8 +284,8 @@ func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
return nil
}
func calculateImageResolution(fs afero.Fs, filePath string) (*ImageResolution, error) {
file, err := fs.Open(filePath)
func calculateImageResolution(fs_ afero.Fs, filePath string) (*ImageResolution, error) {
file, err := fs_.Open(filePath)
if err != nil {
return nil, err
}
@ -343,12 +350,45 @@ func (i *FileInfo) detectSubtitles() {
base := strings.TrimSuffix(i.Name, ext)
for _, f := range dir {
if !f.IsDir() && strings.HasPrefix(f.Name(), base) && strings.HasSuffix(f.Name(), ".vtt") {
i.Subtitles = append(i.Subtitles, path.Join(parentDir, f.Name()))
// load all supported subtitles from subs directories
// 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 {
afs := &afero.Afero{Fs: i.Fs}
dir, err := afs.ReadDir(i.Path)

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,64 +1,71 @@
{
"name": "filebrowser-frontend",
"version": "2.0.0",
"version": "3.0.0",
"private": true,
"type": "module",
"engines": {
"npm": ">=7.0.0",
"node": ">=18.0.0"
},
"scripts": {
"dev": "vite dev",
"serve": "vite serve",
"build": "vite build",
"watch": "vite build --watch",
"build": "npm run typecheck && vite build",
"clean": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitkeep' -exec rm -r {} +",
"lint": "eslint --ext .vue,.js src/",
"lint:fix": "eslint --ext .vue,.js --fix src/",
"format": "prettier --write ."
"typecheck": "vue-tsc -p ./tsconfig.json --noEmit",
"lint": "npm run typecheck && eslint --ext .vue,.ts src/",
"lint:fix": "eslint --ext .vue,.ts --fix src/",
"format": "prettier --write .",
"test": "playwright test"
},
"dependencies": {
"ace-builds": "^1.23.4",
"clipboard": "^2.0.11",
"core-js": "^3.32.0",
"css-vars-ponyfill": "^2.4.8",
"filesize": "^10.0.8",
"js-base64": "^3.7.5",
"lodash.clonedeep": "^4.5.0",
"lodash.throttle": "^4.1.1",
"material-icons": "^1.13.9",
"moment": "^2.29.4",
"@chenfengyuan/vue-number-input": "^2.0.1",
"@vueuse/core": "^10.9.0",
"@vueuse/integrations": "^10.9.0",
"ace-builds": "^1.32.9",
"core-js": "^3.36.1",
"dayjs": "^1.11.10",
"filesize": "^10.1.1",
"js-base64": "^3.7.7",
"jwt-decode": "^4.0.0",
"lodash-es": "^4.17.21",
"material-icons": "^1.13.12",
"normalize.css": "^8.0.1",
"noty": "^3.2.0-beta",
"pinia": "^2.1.7",
"pretty-bytes": "^6.1.1",
"qrcode.vue": "^1.7.0",
"tus-js-client": "^3.1.1",
"qrcode.vue": "^3.4.1",
"tus-js-client": "^4.1.0",
"utif": "^3.1.0",
"vue": "^2.7.14",
"vue-async-computed": "^3.9.0",
"vue-i18n": "^8.28.2",
"vue-lazyload": "^1.3.5",
"vue-router": "^3.6.5",
"vue-simple-progress": "^1.1.1",
"vuex": "^3.6.2",
"vuex-router-sync": "^5.0.0",
"whatwg-fetch": "^3.6.17"
"video.js": "^8.10.0",
"videojs-hotkeys": "^0.2.28",
"videojs-mobile-ui": "^1.1.1",
"vue": "^3.4.21",
"vue-final-modal": "^4.5.4",
"vue-i18n": "^9.10.2",
"vue-lazyload": "^3.0.0",
"vue-router": "^4.3.0",
"vue-toastification": "^2.0.0-rc.5"
},
"devDependencies": {
"@vitejs/plugin-legacy": "^4.1.1",
"@vitejs/plugin-vue2": "^2.2.0",
"@vue/eslint-config-prettier": "^8.0.0",
"autoprefixer": "^10.4.14",
"eslint": "^8.46.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.16.1",
"jsdom": "^22.1.0",
"postcss": "^8.4.31",
"prettier": "^3.0.1",
"terser": "^5.19.2",
"vite": "^4.5.2",
"vite-plugin-compression2": "^0.10.3",
"vite-plugin-rewrite-all": "^1.0.1"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie < 11"
]
"@intlify/unplugin-vue-i18n": "^4.0.0",
"@playwright/test": "^1.42.1",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.12.2",
"@typescript-eslint/eslint-plugin": "^7.4.0",
"@vitejs/plugin-legacy": "^5.3.2",
"@vitejs/plugin-vue": "^5.0.4",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^13.0.0",
"autoprefixer": "^10.4.19",
"concurrently": "^8.2.2",
"eslint": "^8.57.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-vue": "^9.24.0",
"jsdom": "^24.0.0",
"postcss": "^8.4.38",
"prettier": "^3.2.5",
"terser": "^5.30.0",
"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,8 +16,8 @@
[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]
</title>
<meta name="robots" content="noindex,nofollow">
<meta name="robots" content="noindex,nofollow" />
<link
rel="icon"
type="image/png"
@ -181,14 +181,9 @@
</div>
</div>
<script type="module" src="/src/main.js"></script>
<script type="module" src="/src/main.ts"></script>
[{[ if .Theme -]}]
<link
rel="stylesheet"
href="[{[ .StaticURL ]}]/themes/[{[ .Theme ]}].css"
/>
[{[ end ]}] [{[ if .CSS -]}]
[{[ if .CSS -]}]
<link rel="stylesheet" href="[{[ .StaticURL ]}]/custom.css" />
[{[ end ]}]
</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>
</template>
<script>
// eslint-disable-next-line no-undef
// __webpack_public_path__ = window.FileBrowser.StaticURL + "/";
<script setup lang="ts">
import { ref, onMounted, watch } from "vue";
import { useI18n } from "vue-i18n";
import { setHtmlLocale } from "./i18n";
import { getMediaPreference, getTheme, setTheme } from "./utils/theme";
export default {
name: "app",
mounted() {
const loading = document.getElementById("loading");
loading.classList.add("done");
const { locale } = useI18n();
setTimeout(function () {
loading.parentNode.removeChild(loading);
}, 200);
},
};
const userTheme = ref<UserTheme>(getTheme() || getMediaPreference());
onMounted(() => {
setTheme(userTheme.value);
setHtmlLocale(locale.value);
// this might be null during HMR
const loading = document.getElementById("loading");
loading?.classList.add("done");
setTimeout(function () {
loading?.parentNode?.removeChild(loading);
}, 200);
});
// handles ltr/rtl changes
watch(locale, (newValue) => {
newValue && setHtmlLocale(newValue);
});
</script>
<style>
@import "./css/styles.css";
</style>

View File

@ -1,15 +1,22 @@
import { removePrefix } from "./utils";
import { baseURL } from "@/utils/constants";
import store from "@/store";
import { useAuthStore } from "@/stores/auth";
const ssl = window.location.protocol === "https:";
const protocol = ssl ? "wss:" : "ws:";
export default function command(url, command, onmessage, onclose) {
url = removePrefix(url);
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${store.state.jwt}`;
export default function command(
url: string,
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.onmessage = onmessage;
conn.onclose = onclose;

View File

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

View File

@ -1,7 +1,7 @@
import { fetchURL, removePrefix, createURL } from "./utils";
import { baseURL } from "@/utils/constants";
export async function fetch(url, password = "") {
export async function fetch(url: string, password: string = "") {
url = removePrefix(url);
const res = await fetchURL(
@ -12,12 +12,12 @@ export async function fetch(url, password = "") {
false
);
let data = await res.json();
const data = (await res.json()) as Resource;
data.url = `/share${url}`;
if (data.isDir) {
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.url = `${data.url}${encodeURIComponent(item.name)}`;
@ -32,7 +32,12 @@ export async function fetch(url, password = "") {
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}`;
if (files.length === 1) {
@ -40,7 +45,7 @@ export function download(format, hash, token, ...files) {
} else {
let arg = "";
for (let file of files) {
for (const file of files) {
arg += encodeURIComponent(file) + ",";
}
@ -60,11 +65,11 @@ export function download(format, hash, token, ...files) {
window.open(url);
}
export function getDownloadURL(share, inline = false) {
export function getDownloadURL(res: Resource, inline = false) {
const params = {
...(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 url from "../utils/url";
export default async function search(base, query) {
export default async function search(base: string, query: string) {
base = removePrefix(base);
query = encodeURIComponent(query);
@ -9,11 +9,11 @@ export default async function search(base, query) {
base += "/";
}
let res = await fetchURL(`/api/search${base}?query=${query}`, {});
const res = await fetchURL(`/api/search${base}?query=${query}`, {});
let data = await res.json();
data = data.map((item) => {
data = data.map((item: UploadItem) => {
item.url = `/files${base}` + url.encodePath(item.path);
if (item.dir) {

View File

@ -1,10 +1,10 @@
import { fetchURL, fetchJSON } from "./utils";
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`, {
method: "PUT",
body: JSON.stringify(settings),

View File

@ -1,21 +1,26 @@
import { fetchURL, fetchJSON, removePrefix, createURL } from "./utils";
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);
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}`, {
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 = `/api/share${url}`;
if (expires !== "") {
@ -23,7 +28,11 @@ export async function create(url, password = "", expires = "", unit = "hours") {
}
let body = "{}";
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, {
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);
}

View File

@ -1,6 +1,7 @@
import * as tus from "tus-js-client";
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 { fetchURL } from "./utils";
@ -11,13 +12,13 @@ const ALPHA = 0.2;
const ONE_MINUS_ALPHA = 1 - ALPHA;
const RECENT_SPEEDS_LIMIT = 5;
const MB_DIVISOR = 1024 * 1024;
const CURRENT_UPLOAD_LIST = {};
const CURRENT_UPLOAD_LIST: CurrentUploadList = {};
export async function upload(
filePath,
content = "",
filePath: string,
content: ApiContent = "",
overwrite = false,
onupload
onupload: any
) {
if (!tusSettings) {
// Shouldn't happen as we check for tus support before calling this function
@ -25,29 +26,35 @@ export async function upload(
}
filePath = removePrefix(filePath);
let resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`;
const resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`;
await createUpload(resourcePath);
return new Promise((resolve, reject) => {
let upload = new tus.Upload(content, {
const authStore = useAuthStore();
// 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}`,
chunkSize: tusSettings.chunkSize,
retryDelays: computeRetryDelays(tusSettings),
parallelUploads: 1,
storeFingerprintForResuming: false,
headers: {
"X-Auth": store.state.jwt,
"X-Auth": authStore.jwt,
},
onError: function (error) {
if (CURRENT_UPLOAD_LIST[filePath].interval) {
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
}
delete CURRENT_UPLOAD_LIST[filePath];
reject("Upload failed: " + error);
reject(new Error(`Upload failed: ${error.message}`));
},
onProgress: function (bytesUploaded) {
let fileData = CURRENT_UPLOAD_LIST[filePath];
const fileData = CURRENT_UPLOAD_LIST[filePath];
fileData.currentBytesUploaded = bytesUploaded;
if (!fileData.hasStarted) {
@ -79,14 +86,14 @@ export async function upload(
lastProgressTimestamp: null,
sumOfRecentSpeeds: 0,
hasStarted: false,
interval: null,
interval: undefined,
};
upload.start();
});
}
async function createUpload(resourcePath) {
let headResp = await fetchURL(resourcePath, {
async function createUpload(resourcePath: string) {
const headResp = await fetchURL(resourcePath, {
method: "POST",
});
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) {
// Disable retries altogether
return null;
return undefined;
}
// The tus client expects our retries as an array with computed backoffs
// E.g.: [0, 3000, 5000, 10000, 20000]
@ -115,7 +122,7 @@ function computeRetryDelays(tusSettings) {
return retryDelays;
}
export async function useTus(content) {
export async function useTus(content: ApiContent) {
return isTusSupported() && content instanceof Blob;
}
@ -123,25 +130,34 @@ function isTusSupported() {
return tus.isSupported === true;
}
function computeETA(state) {
function computeETA(state: ETAState, speed?: number) {
if (state.speedMbyte === 0) {
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(
(acc, progress) => acc + progress,
(acc: number, progress: Progress) => {
if (typeof progress === "number") {
return acc + progress;
}
return acc;
},
0
);
const remainingSize = totalSize - uploadedSize;
const speedBytesPerSecond = state.speedMbyte * 1024 * 1024;
const speedBytesPerSecond = (speed ?? state.speedMbyte) * 1024 * 1024;
return remainingSize / speedBytesPerSecond;
}
function computeGlobalSpeedAndETA() {
const uploadStore = useUploadStore();
let totalSpeed = 0;
let totalCount = 0;
for (let filePath in CURRENT_UPLOAD_LIST) {
for (const filePath in CURRENT_UPLOAD_LIST) {
totalSpeed += CURRENT_UPLOAD_LIST[filePath].currentAverageSpeed;
totalCount++;
}
@ -149,41 +165,43 @@ function computeGlobalSpeedAndETA() {
if (totalCount === 0) return { speed: 0, eta: Infinity };
const averageSpeed = totalSpeed / totalCount;
const averageETA = computeETA(store.state.upload, averageSpeed);
const averageETA = computeETA(uploadStore, averageSpeed);
return { speed: averageSpeed, eta: averageETA };
}
function calcProgress(filePath) {
let fileData = CURRENT_UPLOAD_LIST[filePath];
function calcProgress(filePath: string) {
const uploadStore = useUploadStore();
const fileData = CURRENT_UPLOAD_LIST[filePath];
let elapsedTime = (Date.now() - fileData.lastProgressTimestamp) / 1000;
let bytesSinceLastUpdate =
const elapsedTime =
(Date.now() - (fileData.lastProgressTimestamp ?? 0)) / 1000;
const bytesSinceLastUpdate =
fileData.currentBytesUploaded - fileData.initialBytesUploaded;
let currentSpeed = bytesSinceLastUpdate / MB_DIVISOR / elapsedTime;
const currentSpeed = bytesSinceLastUpdate / MB_DIVISOR / elapsedTime;
if (fileData.recentSpeeds.length >= RECENT_SPEEDS_LIMIT) {
fileData.sumOfRecentSpeeds -= fileData.recentSpeeds.shift();
fileData.sumOfRecentSpeeds -= fileData.recentSpeeds.shift() ?? 0;
}
fileData.recentSpeeds.push(currentSpeed);
fileData.sumOfRecentSpeeds += currentSpeed;
let avgRecentSpeed =
const avgRecentSpeed =
fileData.sumOfRecentSpeeds / fileData.recentSpeeds.length;
fileData.currentAverageSpeed =
ALPHA * avgRecentSpeed + ONE_MINUS_ALPHA * fileData.currentAverageSpeed;
const { speed, eta } = computeGlobalSpeedAndETA();
store.commit("setUploadSpeed", speed);
store.commit("setETA", eta);
uploadStore.setUploadSpeed(speed);
uploadStore.setETA(eta);
fileData.initialBytesUploaded = fileData.currentBytesUploaded;
fileData.lastProgressTimestamp = Date.now();
}
export function abortAllUploads() {
for (let filePath in CURRENT_UPLOAD_LIST) {
for (const filePath in CURRENT_UPLOAD_LIST) {
if (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() {
return fetchJSON(`/api/users`, {});
return fetchJSON<IUser[]>(`/api/users`, {});
}
export async function get(id) {
return fetchJSON(`/api/users/${id}`, {});
export async function get(id: number) {
return fetchJSON<IUser>(`/api/users/${id}`, {});
}
export async function create(user) {
export async function create(user: IUser) {
const res = await fetchURL(`/api/users`, {
method: "POST",
body: JSON.stringify({
@ -21,9 +21,11 @@ export async function create(user) {
if (res.status === 201) {
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}`, {
method: "PUT",
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}`, {
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
:is="element"
:to="base || ''"
:aria-label="$t('files.home')"
:title="$t('files.home')"
:aria-label="t('files.home')"
:title="t('files.home')"
>
<i class="material-icons">home</i>
</component>
@ -18,58 +18,66 @@
</div>
</template>
<script>
export default {
name: "breadcrumbs",
props: ["base", "noLink"],
computed: {
items() {
const relativePath = this.$route.path.replace(this.base, "");
let parts = relativePath.split("/");
<script setup lang="ts">
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
if (parts[0] === "") {
parts.shift();
}
const { t } = useI18n();
if (parts[parts.length - 1] === "") {
parts.pop();
}
const route = useRoute();
let breadcrumbs = [];
const props = defineProps<{
base: string;
noLink?: boolean;
}>();
for (let i = 0; i < parts.length; i++) {
if (i === 0) {
breadcrumbs.push({
name: decodeURIComponent(parts[i]),
url: this.base + "/" + parts[i] + "/",
});
} else {
breadcrumbs.push({
name: decodeURIComponent(parts[i]),
url: breadcrumbs[i - 1].url + parts[i] + "/",
});
}
}
const items = computed(() => {
const relativePath = route.path.replace(props.base, "");
let parts = relativePath.split("/");
if (breadcrumbs.length > 3) {
while (breadcrumbs.length !== 4) {
breadcrumbs.shift();
}
if (parts[0] === "") {
parts.shift();
}
breadcrumbs[0].name = "...";
}
if (parts[parts.length - 1] === "") {
parts.pop();
}
return breadcrumbs;
},
element() {
if (this.noLink !== undefined) {
return "span";
}
let breadcrumbs: BreadCrumb[] = [];
return "router-link";
},
},
};
for (let i = 0; i < parts.length; i++) {
if (i === 0) {
breadcrumbs.push({
name: decodeURIComponent(parts[i]),
url: props.base + "/" + parts[i] + "/",
});
} else {
breadcrumbs.push({
name: decodeURIComponent(parts[i]),
url: breadcrumbs[i - 1].url + parts[i] + "/",
});
}
}
if (breadcrumbs.length > 3) {
while (breadcrumbs.length !== 4) {
breadcrumbs.shift();
}
breadcrumbs[0].name = "...";
}
return breadcrumbs;
});
const element = computed(() => {
if (props.noLink) {
return "span";
}
return "router-link";
});
</script>
<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"
ref="input"
:autofocus="active"
v-model.trim="value"
v-model.trim="prompt"
:aria-label="$t('search.search')"
:placeholder="$t('search.search')"
/>
@ -28,7 +28,7 @@
<template v-if="isEmpty">
<p>{{ text }}</p>
<template v-if="value.length === 0">
<template v-if="prompt.length === 0">
<div class="boxes">
<h3>{{ $t("search.types") }}</h3>
<div>
@ -49,7 +49,7 @@
</template>
<ul v-show="results.length > 0">
<li v-for="(s, k) in filteredResults" :key="k">
<router-link @click.native="close" :to="s.url">
<router-link v-on:click="close" :to="s.url">
<i v-if="s.dir" class="material-icons">folder</i>
<i v-else class="material-icons">insert_drive_file</i>
<span>./{{ s.path }}</span>
@ -64,138 +64,155 @@
</div>
</template>
<script>
import { mapState, mapGetters, mapMutations } from "vuex";
<script setup lang="ts">
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import url from "@/utils/url";
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" },
audio: { label: "music", icon: "volume_up" },
video: { label: "video", icon: "movie" },
pdf: { label: "pdf", icon: "picture_as_pdf" },
};
export default {
name: "search",
data: function () {
return {
value: "",
active: false,
ongoing: false,
results: [],
reload: false,
resultsCount: 50,
scrollable: null,
};
},
watch: {
currentPrompt(val, old) {
this.active = val?.prompt === "search";
const layoutStore = useLayoutStore();
const fileStore = useFileStore();
if (old?.prompt === "search" && !this.active) {
if (this.reload) {
this.setReload(true);
}
const { currentPromptName } = storeToRefs(layoutStore);
document.body.style.overflow = "auto";
this.reset();
this.value = "";
this.active = false;
this.$refs.input.blur();
} else if (this.active) {
this.reload = false;
this.$refs.input.focus();
document.body.style.overflow = "hidden";
}
},
value() {
if (this.results.length) {
this.reset();
}
},
},
computed: {
...mapState(["user"]),
...mapGetters(["isListing", "currentPrompt"]),
boxes() {
return boxes;
},
isEmpty() {
return this.results.length === 0;
},
text() {
if (this.ongoing) {
return "";
}
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);
return this.value === ""
? this.$t("search.typeToSearch")
: this.$t("search.pressToSearch");
},
filteredResults() {
return this.results.slice(0, this.resultsCount);
},
},
mounted() {
this.$refs.result.addEventListener("scroll", (event) => {
if (
event.target.offsetHeight + event.target.scrollTop >=
event.target.scrollHeight - 100
) {
this.resultsCount += 50;
}
});
},
methods: {
...mapMutations(["showHover", "closeHovers", "setReload"]),
open() {
this.showHover("search");
},
close(event) {
event.stopPropagation();
event.preventDefault();
this.closeHovers();
},
keyup(event) {
if (event.keyCode === 27) {
this.close(event);
return;
}
const $showError = inject<IToastError>("$showError")!;
this.results.length = 0;
},
init(string) {
this.value = `${string} `;
this.$refs.input.focus();
},
reset() {
this.ongoing = false;
this.resultsCount = 50;
this.results = [];
},
async submit(event) {
event.preventDefault();
const input = ref<HTMLInputElement | null>(null);
const result = ref<HTMLElement | null>(null);
if (this.value === "") {
return;
}
const { t } = useI18n();
let path = this.$route.path;
if (!this.isListing) {
path = url.removeLastDir(path) + "/";
}
const route = useRoute();
this.ongoing = true;
watch(currentPromptName, (newVal, oldVal) => {
active.value = newVal === "search";
try {
this.results = await search(path, this.value);
} catch (error) {
this.$showError(error);
}
if (oldVal === "search" && !active.value) {
if (reload.value) {
fileStore.reload = true;
}
this.ongoing = false;
},
},
document.body.style.overflow = "auto";
reset();
prompt.value = "";
active.value = false;
input.value?.blur();
} else if (active.value) {
reload.value = false;
input.value?.focus();
document.body.style.overflow = "hidden";
}
});
watch(prompt, () => {
if (results.value.length) {
reset();
}
});
// ...mapState(useFileStore, ["isListing"]),
// ...mapState(useLayoutStore, ["show"]),
// ...mapWritableState(useFileStore, { sReload: "reload" }),
const isEmpty = computed(() => {
return results.value.length === 0;
});
const text = computed(() => {
if (ongoing.value) {
return "";
}
return prompt.value === ""
? t("search.typeToSearch")
: t("search.pressToSearch");
});
const filteredResults = computed(() => {
return results.value.slice(0, resultsCount.value);
});
onMounted(() => {
if (result.value === null) {
return;
}
result.value.addEventListener("scroll", (event: Event) => {
if (
(event.target as HTMLElement).offsetHeight +
(event.target as HTMLElement).scrollTop >=
(event.target as HTMLElement).scrollHeight - 100
) {
resultsCount.value += 50;
}
});
});
const open = () => {
!active.value && layoutStore.showHover("search");
};
const close = (event: Event) => {
event.stopPropagation();
event.preventDefault();
layoutStore.closeHovers();
};
const keyup = (event: KeyboardEvent) => {
if (event.key === "Escape") {
close(event);
return;
}
results.value.length = 0;
};
const init = (string: string) => {
prompt.value = `${string} `;
input.value !== null ? input.value.focus() : "";
};
const reset = () => {
ongoing.value = false;
resultsCount.value = 50;
results.value = [];
};
const submit = async (event: Event) => {
event.preventDefault();
if (prompt.value === "") {
return;
}
let path = route.path;
if (!fileStore.isListing) {
path = url.removeLastDir(path) + "/";
}
ongoing.value = true;
try {
results.value = await search(path, prompt.value);
} catch (error: any) {
$showError(error);
}
ongoing.value = false;
};
</script>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,62 +1,59 @@
<template>
<header>
<img v-if="showLogo !== undefined" :src="logoURL" />
<action
v-if="showMenu !== undefined"
<img v-if="showLogo" :src="logoURL" />
<Action
v-if="showMenu"
class="menu-button"
icon="menu"
:label="$t('buttons.toggleSidebar')"
@action="openSidebar()"
:label="t('buttons.toggleSidebar')"
@action="layoutStore.showHover('sidebar')"
/>
<slot />
<div id="dropdown" :class="{ active: this.currentPromptName === 'more' }">
<div
id="dropdown"
:class="{ active: layoutStore.currentPromptName === 'more' }"
>
<slot name="actions" />
</div>
<action
v-if="this.$slots.actions"
<Action
v-if="ifActionsSlot"
id="more"
icon="more_vert"
:label="$t('buttons.more')"
@action="$store.commit('showHover', 'more')"
:label="t('buttons.more')"
@action="layoutStore.showHover('more')"
/>
<div
class="overlay"
v-show="this.currentPromptName == 'more'"
@click="$store.commit('closeHovers')"
v-show="layoutStore.currentPromptName == 'more'"
@click="layoutStore.closeHovers"
/>
</header>
</template>
<script>
<script setup lang="ts">
import { useLayoutStore } from "@/stores/layout";
import { logoURL } from "@/utils/constants";
import Action from "@/components/header/Action.vue";
import { mapGetters } from "vuex";
import { computed, useSlots } from "vue";
import { useI18n } from "vue-i18n";
export default {
name: "header-bar",
props: ["showLogo", "showMenu"],
components: {
Action,
},
data: function () {
return {
logoURL,
};
},
methods: {
openSidebar() {
this.$store.commit("showHover", "sidebar");
},
},
computed: {
...mapGetters(["currentPromptName"]),
},
};
defineProps<{
showLogo?: boolean;
showMenu?: boolean;
}>();
const layoutStore = useLayoutStore();
const slots = useSlots();
const { t } = useI18n();
const ifActionsSlot = computed(() => (slots.actions ? true : false));
</script>
<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">
<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
@ -28,17 +31,20 @@
<div>
<button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
@click="closeHovers"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
tabindex="3"
>
{{ $t("buttons.cancel") }}
</button>
<button
id="focus-prompt"
class="button button--flat"
@click="copy"
:aria-label="$t('buttons.copy')"
:title="$t('buttons.copy')"
tabindex="2"
>
{{ $t("buttons.copy") }}
</button>
@ -48,7 +54,10 @@
</template>
<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 { files as api } from "@/api";
import buttons from "@/utils/buttons";
@ -63,8 +72,14 @@ export default {
dest: null,
};
},
computed: mapState(["req", "selected", "user"]),
inject: ["$showError"],
computed: {
...mapState(useFileStore, ["req", "selected"]),
...mapState(useAuthStore, ["user"]),
...mapWritableState(useFileStore, ["reload"]),
},
methods: {
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
copy: async function (event) {
event.preventDefault();
let items = [];
@ -87,7 +102,7 @@ export default {
buttons.success("copy");
if (this.$route.path === this.dest) {
this.$store.commit("setReload", true);
this.reload = true;
return;
}
@ -101,7 +116,7 @@ export default {
};
if (this.$route.path === this.dest) {
this.$store.commit("closeHovers");
this.closeHovers();
action(false, true);
return;
@ -114,14 +129,14 @@ export default {
let rename = false;
if (conflict) {
this.$store.commit("showHover", {
this.showHover({
prompt: "replace-rename",
confirm: (event, option) => {
overwrite = option == "overwrite";
rename = option == "rename";
event.preventDefault();
this.$store.commit("closeHovers");
this.closeHovers();
action(overwrite, rename);
},
});

View File

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

View File

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

View File

@ -25,7 +25,10 @@
</template>
<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 { files } from "@/api";
@ -42,8 +45,10 @@ export default {
current: window.location.pathname,
};
},
inject: ["$showError"],
computed: {
...mapState(["req", "user"]),
...mapState(useAuthStore, ["user"]),
...mapState(useFileStore, ["req"]),
nav() {
return decodeURIComponent(this.current);
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,22 +1,20 @@
<template>
<div>
<component
v-if="showOverlay"
:ref="currentPromptName"
:is="currentPromptName"
v-bind="currentPrompt.props"
>
</component>
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
</div>
<ModalsContainer />
</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 Info from "./Info.vue";
import Delete from "./Delete.vue";
import Rename from "./Rename.vue";
import DeleteUser from "./DeleteUser.vue";
import Download from "./Download.vue";
import Rename from "./Rename.vue";
import Move from "./Move.vue";
import Copy from "./Copy.vue";
import NewFile from "./NewFile.vue";
@ -24,87 +22,61 @@ import NewDir from "./NewDir.vue";
import Replace from "./Replace.vue";
import ReplaceRename from "./ReplaceRename.vue";
import Share from "./Share.vue";
import Upload from "./Upload.vue";
import ShareDelete from "./ShareDelete.vue";
import Sidebar from "../Sidebar.vue";
import Upload from "./Upload.vue";
import DiscardEditorChanges from "./DiscardEditorChanges.vue";
import { mapGetters, mapState } from "vuex";
import buttons from "@/utils/buttons";
export default {
name: "prompts",
components: {
Info,
Delete,
Rename,
Download,
Move,
Copy,
Share,
NewFile,
NewDir,
Help,
Replace,
ReplaceRename,
Upload,
ShareDelete,
Sidebar,
DiscardEditorChanges,
},
data: function () {
return {
pluginData: {
buttons,
store: this.$store,
router: this.$router,
},
};
},
created() {
window.addEventListener("keydown", (event) => {
if (this.currentPrompt == null) return;
const layoutStore = useLayoutStore();
const promptName = this.currentPrompt.prompt;
const prompt = this.$refs[promptName];
const { currentPromptName } = storeToRefs(layoutStore);
if (event.code === "Escape") {
event.stopImmediatePropagation();
this.$store.commit("closeHovers");
}
const closeModal = ref<() => Promise<string>>();
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"
);
const components = new Map<string, any>([
["info", Info],
["help", Help],
["delete", Delete],
["rename", Rename],
["move", Move],
["copy", Copy],
["newFile", NewFile],
["newDir", NewDir],
["download", Download],
["replace", Replace],
["replace-rename", ReplaceRename],
["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,
},
},
methods: {
resetPrompts() {
this.$store.commit("closeHovers");
},
},
};
});
closeModal.value = close;
open();
});
window.addEventListener("keydown", (event) => {
if (!layoutStore.currentPrompt) return;
if (event.key === "Escape") {
event.stopImmediatePropagation();
layoutStore.closeHovers();
}
});
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,38 +1,111 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t("prompts.upload") }}</h2>
<h2>{{ t("prompts.upload") }}</h2>
</div>
<div class="card-content">
<p>{{ $t("prompts.uploadMessage") }}</p>
<p>{{ t("prompts.uploadMessage") }}</p>
</div>
<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>
<div class="title">{{ $t("buttons.file") }}</div>
<div class="title">{{ t("buttons.file") }}</div>
</div>
<div @click="uploadFolder" class="action">
<div
@click="uploadFolder"
@keypress.enter="uploadFolder"
class="action"
tabindex="2"
>
<i class="material-icons">folder</i>
<div class="title">{{ $t("buttons.folder") }}</div>
<div class="title">{{ t("buttons.folder") }}</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "upload",
methods: {
uploadFile: function () {
document.getElementById("upload-input").value = "";
document.getElementById("upload-input").click();
},
uploadFolder: function () {
document.getElementById("upload-folder-input").value = "";
document.getElementById("upload-folder-input").click();
},
},
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import * as upload from "@/utils/upload";
const { t } = useI18n();
const route = useRoute();
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>

View File

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

View File

@ -1,5 +1,5 @@
<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">
{{ $t("languages." + language) }}
</option>
@ -7,40 +7,45 @@
</template>
<script>
import { markRaw } from "vue";
export default {
name: "languages",
props: ["locale"],
data() {
let dataObj = {
locales: {
he: "he",
hu: "hu",
ar: "ar",
de: "de",
el: "el",
en: "en",
es: "es",
fr: "fr",
is: "is",
it: "it",
ja: "ja",
ko: "ko",
"nl-be": "nlBE",
pl: "pl",
"pt-br": "ptBR",
pt: "pt",
ro: "ro",
ru: "ru",
sk: "sk",
"sv-se": "svSE",
tr: "tr",
ua: "ua",
"zh-cn": "zhCN",
"zh-tw": "zhTW",
},
let dataObj = {};
const locales = {
he: "he",
hu: "hu",
ar: "ar",
de: "de",
el: "el",
en: "en",
es: "es",
fr: "fr",
is: "is",
it: "it",
ja: "ja",
ko: "ko",
"nl-be": "nlBE",
pl: "pl",
"pt-br": "ptBR",
pt: "pt",
ro: "ro",
ru: "ru",
sk: "sk",
"sv-se": "svSE",
tr: "tr",
uk: "uk",
"zh-cn": "zhCN",
"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", {
value: markRaw(locales),
configurable: false,
writable: false,
});

View File

@ -1,18 +1,27 @@
<template>
<select v-on:change="change" :value="theme">
<option value="">{{ $t("settings.themes.light") }}</option>
<option value="dark">{{ $t("settings.themes.dark") }}</option>
<option value="">{{ t("settings.themes.default") }}</option>
<option value="light">{{ t("settings.themes.light") }}</option>
<option value="dark">{{ t("settings.themes.dark") }}</option>
</select>
</template>
<script>
export default {
name: "themes",
props: ["theme"],
methods: {
change(event) {
this.$emit("update:theme", event.target.value);
},
},
<script setup lang="ts">
import { SelectHTMLAttributes } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
:root {
--blue: #2196f3;
--dark-blue: #1E88E5;
--red: #F44336;
--dark-red: #D32F2F;
--dark-blue: #1e88e5;
--red: #f44336;
--dark-red: #d32f2f;
--moon-grey: #f2f2f2;
--icon-red: #da4453;
@ -11,4 +11,44 @@
--icon-green: #2ecc71;
--icon-blue: #1d99f3;
--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 {
font-family: "Roboto", sans-serif;
padding-top: 4em;
background-color: #fafafa;
color: #333333;
}
body.rtl {
direction: rtl;
background: var(--background);
color: var(--textSecondary);
}
* {
@ -62,8 +58,8 @@ nav {
left: 0;
}
body.rtl nav {
left: unset;
html[dir="rtl"] nav {
left: initial;
right: 0;
}
@ -78,13 +74,12 @@ nav .action {
text-overflow: ellipsis;
}
body.rtl .action {
direction: rtl;
html[dir="rtl"] nav .action {
text-align: right;
}
nav > div {
border-top: 1px solid rgba(0, 0, 0, 0.05);
border-top: 1px solid var(--divider);
}
nav .action > * {
@ -99,14 +94,15 @@ main {
.breadcrumbs {
height: 3em;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
background: var(--background);
border-bottom: 1px solid var(--divider);
}
.breadcrumbs span,
.breadcrumbs {
display: flex;
align-items: center;
color: #6f6f6f;
color: var(--textPrimary);
}
.breadcrumbs a {
@ -115,12 +111,12 @@ main {
border-radius: 0.125em;
}
body.rtl .breadcrumbs a {
html[dir="rtl"] .breadcrumbs a {
transform: translateX(-16em);
}
.breadcrumbs a:hover {
background-color: rgba(0, 0, 0, 0.05);
background-color: var(--divider);
}
.breadcrumbs span a {
@ -151,4 +147,34 @@ body.rtl .breadcrumbs a {
.break-word {
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 {
display: flex;
margin: 0 -.5em;
margin: 0 -0.5em;
flex-wrap: wrap;
}
body.rtl .dashboard .row {
html[dir="rtl"] .dashboard .row {
margin-right: 16em;
}
.dashboard .row .column {
display: flex;
padding: 0 .5em;
padding: 0 0.5em;
width: 50%;
}
@ -22,33 +22,33 @@ body.rtl .dashboard .row {
flex-grow: 1;
}
@media(max-width: 1200px) {
@media (max-width: 1200px) {
.dashboard .row .column {
width: 100%;
}
}
a {
color: inherit
color: inherit;
}
.dashboard p label {
margin-bottom: .2em;
margin-bottom: 0.2em;
display: block;
font-size: .8em;
font-size: 0.8em;
font-weight: 500;
color: rgba(0, 0, 0, 0.57);
color: var(--textPrimary);
}
li code,
p code {
background: rgba(0, 0, 0, 0.05);
padding: .1em;
border-radius: .2em;
background: var(--divider);
padding: 0.1em;
border-radius: 0.2em;
}
.small {
font-size: .8em;
font-size: 0.8em;
line-height: 1.5;
}
@ -61,21 +61,21 @@ p code {
.dashboard #nav .wrapper {
display: flex;
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;
}
.dashboard #nav ul {
list-style: none;
display: flex;
color: rgb(84, 110, 122);
color: var(--action);
font-weight: 500;
padding: 0;
margin: 0 0 -2px 0;
font-size: .8em;
font-size: 0.8em;
text-align: center;
justify-content: left;
}
@ -85,12 +85,11 @@ body.rtl #nav .wrapper {
padding: 1.5em 2em;
white-space: nowrap;
border-bottom: 2px solid transparent;
transition: .1s ease-in-out all;
transition: 0.1s ease-in-out all;
}
.dashboard #nav ul li:hover {
background: var(--moon-grey);
background: var(--surfaceSecondary);
}
.dashboard #nav ul li.active {
@ -120,7 +119,7 @@ table {
}
table tr {
border-bottom: 1px solid #ccc;
border-bottom: 1px solid var(--iconTertiary);
}
table tr:last-child {
@ -129,40 +128,44 @@ table tr:last-child {
table th {
font-weight: 500;
color: #757575;
color: var(--textSecondary);
text-align: left;
}
table th,
table td {
padding: .5em 0;
padding: 0.5em 0;
}
table td.small {
width: 1em;
}
table tr>*:first-child {
table tr > *:first-child {
padding-left: 1em;
}
body.rtl table tr>* {
html[dir="rtl"] table tr > * {
padding-left: unset;
padding-right: 1em;
text-align: right;
direction: ltr;
}
table tr>*:last-child {
table tr > *:last-child {
padding-right: 1em;
}
.card {
position: relative;
margin: 0 0 1rem 0;
background-color: #fff;
background: var(--surfacePrimary);
color: var(--textSecondary);
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;
}
@ -171,18 +174,18 @@ table tr>*:last-child {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 99999;
max-width: 25em;
width: 90%;
max-height: 95%;
animation: .1s show forwards;
/* animation-duration: 0.3s;
animation-fill-mode: forwards; */
}
.card>*>*:first-child {
.card > * > *:first-child {
margin-top: 0;
}
.card>*>*:last-child {
.card > * > *:last-child {
margin-bottom: 0;
}
@ -191,24 +194,24 @@ table tr>*:last-child {
display: flex;
}
.card .card-title>*:first-child {
.card .card-title > *:first-child {
margin-right: auto;
}
body.rtl .card .card-title>*:first-child {
html[dir="rtl"] .card .card-title > *:first-child {
margin-right: 0;
text-align: right;
}
.card>div {
.card > div {
padding: 1em 1em;
}
.card>div:first-child {
.card > div:first-child {
padding-top: 1.5em;
}
.card>div:last-child {
.card > div:last-child {
padding-bottom: 1.5em;
}
@ -234,7 +237,7 @@ body.rtl .card .card-action {
}
.card h3 {
color: rgba(0, 0, 0, 0.53);
color: var(--textPrimary);
font-size: 1em;
font-weight: 500;
margin: 2em 0 1em;
@ -253,6 +256,14 @@ body.rtl .card .card-action {
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 {
list-style: none;
padding: 0;
@ -277,24 +288,24 @@ body.rtl .card .card-action {
.card#share ul li input,
.card#share ul li select {
padding: .2em;
margin-right: .5em;
border: 1px solid #dadada;
padding: 0.2em;
margin-right: 0.5em;
border: 1px solid var(--borderPrimary);
}
.card#share .action.copy-clipboard::after {
content: 'Copied!';
content: "Copied!";
position: absolute;
left: -25%;
width: 150%;
font-size: .6em;
font-size: 0.6em;
text-align: center;
background: #44a6f5;
color: #fff;
padding: .5em .2em;
border-radius: .4em;
padding: 0.5em 0.2em;
border-radius: 0.4em;
top: -2em;
transition: .1s ease opacity;
transition: 0.1s ease opacity;
opacity: 0;
}
@ -324,10 +335,9 @@ body.rtl .card .card-action {
z-index: 9999;
visibility: hidden;
opacity: 0;
animation: .1s show forwards;
animation: 0.1s show forwards;
}
/* * * * * * * * * * * * * * * *
* PROMPT - MOVE *
* * * * * * * * * * * * * * * */
@ -344,33 +354,33 @@ body.rtl .card .card-action {
.file-list li {
width: 100%;
user-select: none;
border-radius: .2em;
padding: .3em;
border-radius: 0.2em;
padding: 0.3em;
}
.file-list li[aria-selected=true] {
.file-list li[aria-selected="true"] {
background: var(--blue) !important;
color: #fff !important;
transition: .1s ease all;
color: var(--iconSecondary) !important;
transition: 0.1s ease all;
}
.file-list li:hover {
background-color: #e9eaeb;
background: var(--surfaceSecondary);
cursor: pointer;
}
.file-list li:before {
content: "folder";
color: #6f6f6f;
color: var(--textPrimary);
vertical-align: middle;
line-height: 1.4;
font-family: 'Material Icons';
font-family: "Material Icons";
font-size: 1.75em;
margin-right: .25em;
margin-right: 0.25em;
}
.file-list li[aria-selected=true]:before {
color: white;
.file-list li[aria-selected="true"]:before {
color: var(--iconSecondary);
}
.help {
@ -399,11 +409,11 @@ body.rtl .card .card-action {
}
.collapsible {
border-top: 1px solid rgba(0,0,0,0.1);
border-top: 1px solid var(--borderPrimary);
}
.collapsible:last-of-type {
border-bottom: 1px solid rgba(0,0,0,0.1);
border-bottom: 1px solid var(--borderPrimary);
}
.collapsible > input {
@ -421,18 +431,18 @@ body.rtl .card .card-action {
.collapsible > label * {
margin: 0;
color: rgba(0,0,0,0.57);
color: var(--textPrimary);
}
.collapsible > label i {
transition: .2s ease transform;
transition: 0.2s ease transform;
user-select: none;
}
.collapsible .collapse {
max-height: 0;
overflow: hidden;
transition: .2s ease all;
transition: 0.2s ease all;
}
.collapsible > input:checked ~ .collapse {
@ -442,7 +452,7 @@ body.rtl .card .card-action {
}
.collapsible > input:checked ~ label i {
transform: rotate(180deg)
transform: rotate(180deg);
}
.card .collapsible {
@ -468,12 +478,12 @@ body.rtl .card .card-action {
flex: 1;
padding: 2em;
border-radius: 0.2em;
border: 1px solid rgba(0, 0, 0, 0.1);
border: 1px solid var(--borderPrimary);
text-align: center;
}
.card .card-action.full .action {
margin: 0 0.25em 0.50em;
margin: 0 0.25em 0.5em;
}
.card .card-action.full .action i {
@ -489,7 +499,7 @@ body.rtl .card .card-action {
}
/*** RTL - Fix disk usage information (in english) ***/
body.rtl .credits {
html[dir="rtl"] .credits {
text-align: right;
direction: ltr;
}
}

View File

@ -1,8 +1,8 @@
header {
z-index: 1000;
background-color: #fff;
border-bottom: 1px solid rgba(0, 0, 0, 0.075);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
background: var(--surfacePrimary);
border-bottom: 1px solid var(--divider);
box-shadow: 0 0 5px var(--borderPrimary);
position: fixed;
top: 0;
left: 0;
@ -37,7 +37,7 @@ header a:hover {
color: inherit;
}
header>div:first-child>.action,
header > div:first-child > .action,
header img {
margin-right: 1em;
}
@ -50,7 +50,7 @@ header .action span {
display: none;
}
header>div div {
header > div div {
vertical-align: middle;
position: relative;
}
@ -82,34 +82,39 @@ header .menu-button {
}
#search #input {
background-color: #f5f5f5;
background: var(--surfaceSecondary);
border-color: var(--surfacePrimary);
display: flex;
height: 100%;
padding: 0em 0.75em;
border-radius: 0.3em;
transition: .1s ease all;
transition: 0.1s ease all;
align-items: center;
z-index: 2;
}
#search #input input::placeholder {
color: var(--textSecondary);
}
#search.active #input {
border-bottom: 1px solid rgba(0, 0, 0, 0.075);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
background-color: #fff;
border-bottom: 1px solid var(--borderPrimary);
box-shadow: 0 0 5px var(--borderPrimary);
background: var(--surfacePrimary);
height: 4em;
}
#search.active>div {
#search.active > div {
border-radius: 0 !important;
}
#search.active i,
#search.active input {
color: #212121;
color: var(--textPrimary);
}
#search #input>.action,
#search #input>i {
#search #input > .action,
#search #input > i {
margin-right: 0.3em;
user-select: none;
}
@ -124,38 +129,39 @@ header .menu-button {
#search #result {
visibility: visible;
max-height: none;
background-color: #f8f8f8;
background: var(--background);
text-align: left;
padding: 0;
color: rgba(0, 0, 0, 0.6);
color: var(--textPrimary);
height: 0;
transition: .1s ease height, .1s ease padding;
transition:
0.1s ease height,
0.1s ease padding;
overflow-x: hidden;
overflow-y: auto;
z-index: 1;
}
body.rtl #search #result {
html[dir="rtl"] #search #result {
direction: ltr;
}
#search #result>div>*:first-child {
#search #result > div > *:first-child {
margin-top: 0;
}
body.rtl #search #result {
direction: rtl;
html[dir="rtl"] #search #result {
text-align: right;
}
/*** RTL - Keep search result LTR because it has paths (in english) ***/
body.rtl #search #result ul>* {
html[dir="rtl"] #search #result ul > * {
direction: ltr;
text-align: left;
}
#search.active #result {
padding: .5em;
padding: 0.5em;
height: calc(100% - 4em);
}
@ -166,10 +172,10 @@ body.rtl #search #result ul>* {
}
#search li {
margin-bottom: .5em;
margin-bottom: 0.5em;
}
#search #result>div {
#search #result > div {
max-width: 45em;
margin: 0 auto;
}
@ -187,10 +193,10 @@ body.rtl #search #result ul>* {
}
#search.active #result i {
color: #ccc;
color: var(--iconTertiary);
}
#search.active #result>p>i {
#search.active #result > p > i {
text-align: center;
margin: 0 auto;
display: table;
@ -199,35 +205,35 @@ body.rtl #search #result ul>* {
#search.active #result ul li a {
display: flex;
align-items: center;
padding: .3em 0;
padding: 0.3em 0;
}
#search.active #result ul li a i {
margin-right: .3em;
margin-right: 0.3em;
}
#search::-webkit-input-placeholder {
color: rgba(255, 255, 255, .5);
}
#search:-moz-placeholder {
opacity: 1;
color: rgba(255, 255, 255, .5);
/* I dont think we need these anymore */
/* #search::-webkit-input-placeholder {
color: var(--textPrimary);
}
#search::-moz-placeholder {
opacity: 1;
color: rgba(255, 255, 255, .5);
color: var(--textPrimary);
}
#search:-ms-input-placeholder {
color: rgba(255, 255, 255, .5);
color: var(--textPrimary);
}
#search #input input::placeholder {
color: var(--textPrimary);
} */
#search .boxes {
border: 1px solid rgba(0, 0, 0, 0.075);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
background: #fff;
border: 1px solid var(--borderPrimary);
box-shadow: 0 0 5px var(--borderPrimary);
background: var(--surfacePrimary);
margin: 1em 0;
}
@ -235,15 +241,15 @@ body.rtl #search #result ul>* {
margin: 0;
font-weight: 500;
font-size: 1em;
color: #212121;
padding: .5em;
color: var(--textSecondary);
padding: 0.5em;
}
body.rtl #search .boxes h3 {
html[dir="rtl"] #search .boxes h3 {
text-align: right;
}
#search .boxes>div {
#search .boxes > div {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
@ -251,7 +257,7 @@ body.rtl #search .boxes h3 {
margin-bottom: -1em;
}
#search .boxes>div>div {
#search .boxes > div > div {
background: var(--blue);
color: #fff;
text-align: center;

View File

@ -2,30 +2,50 @@
/* General */
.file-icons [aria-label^="."] { opacity: 0.33 }
.file-icons [aria-label$=".bak"] { opacity: 0.33 }
.file-icons [aria-label^="."] {
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=blob] i::before { content: 'insert_drive_file' }
.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' }
.file-icons [data-type="audio"] i::before {
content: "volume_up";
}
.file-icons [data-type="blob"] i::before {
content: "insert_drive_file";
}
.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 */
.file-icons [aria-label$=".ai"] i::before,
.file-icons [aria-label$=".odg"] i::before,
.file-icons [aria-label$=".xcf"] i::before
{ content: 'image' }
.file-icons [aria-label$=".xcf"] i::before {
content: "image";
}
/* #f90 - Presentation */
.file-icons [aria-label$=".odp"] i::before,
.file-icons [aria-label$=".ppt"] i::before,
.file-icons [aria-label$=".pptx"] i::before
{ content: 'slideshow' }
.file-icons [aria-label$=".pptx"] i::before {
content: "slideshow";
}
/* #0f0 - Spreadsheet/Database */
@ -34,8 +54,9 @@
.file-icons [aria-label$=".odb"] i::before,
.file-icons [aria-label$=".ods"] i::before,
.file-icons [aria-label$=".xls"] i::before,
.file-icons [aria-label$=".xlsx"] i::before
{ content: 'border_all' }
.file-icons [aria-label$=".xlsx"] i::before {
content: "border_all";
}
/* #00f - Document */
@ -43,8 +64,9 @@
.file-icons [aria-label$=".docx"] i::before,
.file-icons [aria-label$=".log"] i::before,
.file-icons [aria-label$=".odt"] i::before,
.file-icons [aria-label$=".rtf"] i::before
{ content: 'description' }
.file-icons [aria-label$=".rtf"] i::before {
content: "description";
}
/* #999 - Code */
@ -65,8 +87,9 @@
.file-icons [aria-label$=".rs"] i::before,
.file-icons [aria-label$=".vue"] i::before,
.file-icons [aria-label$=".xml"] i::before,
.file-icons [aria-label$=".yml"] i::before
{ content: 'code' }
.file-icons [aria-label$=".yml"] i::before {
content: "code";
}
/* #999 - Executable */
@ -75,16 +98,18 @@
.file-icons [aria-label$=".exe"] i::before,
.file-icons [aria-label$=".jar"] i::before,
.file-icons [aria-label$=".ps1"] i::before,
.file-icons [aria-label$=".sh"] i::before
{ content: 'web_asset' }
.file-icons [aria-label$=".sh"] i::before {
content: "web_asset";
}
/* #999 - Installer */
.file-icons [aria-label$=".deb"] i::before,
.file-icons [aria-label$=".msi"] i::before,
.file-icons [aria-label$=".pkg"] i::before,
.file-icons [aria-label$=".rpm"] i::before
{ content: 'archive' }
.file-icons [aria-label$=".rpm"] i::before {
content: "archive";
}
/* #999 - Compressed */
@ -96,8 +121,9 @@
.file-icons [aria-label$=".tar"] i::before,
.file-icons [aria-label$=".xz"] i::before,
.file-icons [aria-label$=".zip"] i::before,
.file-icons [aria-label$=".zst"] i::before
{ content: 'folder_zip' }
.file-icons [aria-label$=".zst"] i::before {
content: "folder_zip";
}
/* #999 - Disk */
@ -108,25 +134,35 @@
.file-icons [aria-label$=".vdi"] i::before,
.file-icons [aria-label$=".vhd"] i::before,
.file-icons [aria-label$=".vmdk"] i::before,
.file-icons [aria-label$=".wim"] i::before
{ content: 'album' }
.file-icons [aria-label$=".wim"] i::before {
content: "album";
}
/* #999 - Font */
.file-icons [aria-label$=".otf"] i::before,
.file-icons [aria-label$=".ttf"] i::before,
.file-icons [aria-label$=".woff"] i::before,
.file-icons [aria-label$=".woff2"] i::before
{ content: 'font_download' }
.file-icons [aria-label$=".woff2"] i::before {
content: "font_download";
}
/* Colors */
/* General */
.file-icons [data-type=audio] i { color: var(--icon-yellow) }
.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) }
.file-icons [data-type="audio"] i {
color: var(--icon-yellow);
}
.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 */
@ -135,8 +171,9 @@
.file-icons [aria-label$=".jar"] i,
.file-icons [aria-label$=".psd"] i,
.file-icons [aria-label$=".rb"] i,
.file-icons [data-type=pdf] i
{ color: var(--icon-red) }
.file-icons [data-type="pdf"] i {
color: var(--icon-red);
}
/* #f90 - Image/Presentation */
@ -146,16 +183,18 @@
.file-icons [aria-label$=".ppt"] i,
.file-icons [aria-label$=".pptx"] i,
.file-icons [aria-label$=".vue"] i,
.file-icons [aria-label$=".xcf"] i
{ color: var(--icon-orange) }
.file-icons [aria-label$=".xcf"] i {
color: var(--icon-orange);
}
/* #ff0 - Various */
.file-icons [aria-label$=".css"] i,
.file-icons [aria-label$=".js"] i,
.file-icons [aria-label$=".json"] i,
.file-icons [aria-label$=".zip"] i
{ color: var(--icon-yellow) }
.file-icons [aria-label$=".zip"] i {
color: var(--icon-yellow);
}
/* #0f0 - Spreadsheet/Google */
@ -164,8 +203,9 @@
.file-icons [aria-label$=".go"] i,
.file-icons [aria-label$=".ods"] i,
.file-icons [aria-label$=".xls"] i,
.file-icons [aria-label$=".xlsx"] i
{ color: var(--icon-green) }
.file-icons [aria-label$=".xlsx"] i {
color: var(--icon-green);
}
/* #00f - Document/Microsoft/Apple/Closed */
@ -188,18 +228,26 @@
.file-icons [aria-label$=".ps1"] i,
.file-icons [aria-label$=".rtf"] i,
.file-icons [aria-label$=".vob"] i,
.file-icons [aria-label$=".wim"] i
{ color: var(--icon-blue) }
.file-icons [aria-label$=".wim"] i {
color: var(--icon-blue);
}
/* #60f - Various */
.file-icons [aria-label$=".iso"] i,
.file-icons [aria-label$=".php"] i,
.file-icons [aria-label$=".rar"] i
{ color: var(--icon-violet) }
.file-icons [aria-label$=".rar"] i {
color: var(--icon-violet);
}
/* Overrides */
.file-icons [data-dir=true] i { color: var(--icon-blue) }
.file-icons [data-dir=true] i::before { content: 'folder' }
.file-icons [aria-selected=true] i { color: var(--item-selected) }
.file-icons [data-dir="true"] i {
color: var(--icon-blue);
}
.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 {
--item-selected: white;
}
body.rtl #listing {
html[dir="rtl"] #listing {
margin-right: 16em;
}
#listing h2 {
margin: 0 0 0 0.5em;
font-size: .9em;
color: rgba(0, 0, 0, 0.38);
font-size: 0.9em;
color: var(--textPrimary);
font-weight: 500;
}
@ -18,19 +14,22 @@ body.rtl #listing {
overflow: hidden;
}
#listing>div {
#listing > div {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
}
#listing .item {
background-color: #fff;
background: var(--surfacePrimary);
border-color: var(--divider);
position: relative;
display: flex;
flex-wrap: nowrap;
color: #6f6f6f;
transition: .1s ease background, .1s ease opacity;
color: var(--textPrimary);
transition:
0.1s ease background,
0.1s ease opacity;
align-items: center;
cursor: pointer;
user-select: none;
@ -75,13 +74,13 @@ body.rtl #listing {
margin: 1em auto;
display: block !important;
width: 95%;
color: rgba(0, 0, 0, 0.3);
color: var(--textPrimary);
font-weight: 500;
}
.message i {
font-size: 2.5em;
margin-bottom: .2em;
margin-bottom: 0.2em;
display: block;
}
@ -92,14 +91,18 @@ body.rtl #listing {
#listing.mosaic .item {
width: calc(33% - 1em);
margin: .5em;
margin: 0.5em;
padding: 0.5em;
border-radius: 0.2em;
box-shadow: 0 1px 3px rgba(0, 0, 0, .06), 0 1px 2px rgba(0, 0, 0, .12);
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.06),
0 1px 2px rgba(0, 0, 0, 0.12);
}
#listing.mosaic .item:hover {
box-shadow: 0 1px 3px rgba(0, 0, 0, .12), 0 1px 2px rgba(0, 0, 0, .24) !important;
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.12),
0 1px 2px rgba(0, 0, 0, 0.24) !important;
}
#listing.mosaic .header {
@ -127,16 +130,16 @@ body.rtl #listing {
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;
background: linear-gradient(#0000, #0009);
}
#listing.mosaic.gallery .item i {
width: 100%;
margin-right: 0;
font-size: 8em;
text-align: center;
width: 100%;
margin-right: 0;
font-size: 8em;
text-align: center;
}
#listing.mosaic.gallery .item img {
@ -159,7 +162,7 @@ body.rtl #listing {
#listing.list .item {
width: 100%;
margin: 0;
border: 1px solid rgba(0, 0, 0, 0.1);
border: 1px solid var(--borderPrimary);
padding: 1em;
border-top: 0;
}
@ -168,9 +171,9 @@ body.rtl #listing {
display: none;
}
#listing .item[aria-selected=true] {
#listing .item[aria-selected="true"] {
background: var(--blue) !important;
color: var(--item-selected) !important;
color: var(--iconSecondary) !important;
}
#listing.list .item div:first-of-type {
@ -202,25 +205,25 @@ body.rtl #listing {
#listing .item.header {
display: none !important;
background-color: #ccc;
background-color: var(--iconTertiary);
}
#listing.list .header i {
font-size: 1.5em;
vertical-align: middle;
margin-left: .2em;
margin-left: 0.2em;
}
#listing.list .item.header {
display: flex !important;
background: #fafafa;
background: var(--background);
z-index: 999;
padding: .85em;
padding: 0.85em;
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 {
width: 0;
}
@ -232,7 +235,7 @@ body.rtl #listing {
color: inherit;
}
#listing.list .item.header>div:first-child {
#listing.list .item.header > div:first-child {
width: 0;
}
@ -250,7 +253,7 @@ body.rtl #listing {
#listing.list .header i {
opacity: 0;
transition: .1s ease all;
transition: 0.1s ease all;
}
#listing.list .header p:hover i,
@ -272,7 +275,7 @@ body.rtl #listing {
height: 4em;
padding: 0.5em 0.5em 0.5em 1em;
justify-content: space-between;
transition: .2s ease bottom;
transition: 0.2s ease bottom;
}
#listing #multiple-selection.active {
@ -281,5 +284,5 @@ body.rtl #listing {
#listing #multiple-selection p,
#listing #multiple-selection i {
color: var(--item-selected);
color: var(--iconSecondary);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,282 +1,281 @@
{
"buttons": {
"cancel": "Ακύρωση",
"close": "Κλείσιμο",
"copy": "Αντιγραφή",
"copyFile": "Αντιγραφή αρχείου",
"copyToClipboard": "Αντιγραφή στο πρόχειρο",
"copyDownloadLinkToClipboard": "Αντιγραφή συνδέσμου λήψης στο πρόχειρο",
"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": "Κοινοποίηση",
"submit": "Υποβολή",
"switchView": "Εναλλαγή προβολής",
"toggleSidebar": "(Απ-)ενεργοποίησης της πλευρικής μπάρας",
"update": "Ενημέρωση",
"upload": "Μεταφόρτωση",
"openFile": "Άνοιγμα αρχείου",
"continue": "Συνέχεια"
"buttons": {
"cancel": "Ακύρωση",
"clear": "Καθαρισμός",
"close": "Κλείσιμο",
"copy": "Αντιγραφή",
"copyFile": "Αντιγραφή αρχείου",
"copyToClipboard": "Αντιγραφή στο πρόχειρο",
"copyDownloadLinkToClipboard": "Αντιγραφή συνδέσμου λήψης στο πρόχειρο",
"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": "Κοινοποίηση",
"submit": "Υποβολή",
"switchView": "Εναλλαγή προβολής",
"toggleSidebar": "(Απ-)ενεργοποίησης της πλευρικής μπάρας",
"update": "Ενημέρωση",
"upload": "Μεταφόρτωση",
"openFile": "Άνοιγμα αρχείου"
},
"download": {
"downloadFile": "Λήψη αρχείου",
"downloadFolder": "Λήψη φακέλου",
"downloadSelected": "Λήψη επιλεγμένων"
},
"upload": {
"abortUpload": "Είστε σίγουροι ότι θέλετε να διακόψετε τη μεταφόρτωση;"
},
"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": "αποθηκεύει ένα αρχείο ή εκκινεί λήψη του φακέλου στον οποίο βρίσκεστε"
},
"download": {
"downloadFile": "Λήψη αρχείου",
"downloadFolder": "Λήψη φακέλου",
"downloadSelected": "Λήψη επιλεγμένων"
"del": "διαγραφή επιλεγμένων στοιχείων",
"doubleClick": "ανοίγει ένα αρχείο ή φάκελο",
"esc": "καθαρίζει την επιλογή ή/και κλείνει το παράθυρο",
"f1": "αυτή η πληροφορία",
"f2": "μετονομασία αρχείου",
"help": "Βοήθεια"
},
"languages": {
"he": "עברית",
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"en": "English",
"es": "Español",
"el": "Ελληνικά",
"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": "Επιλέξτε τοποθεσία για αντιγραφή των αρχείων σας:",
"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": "Μεταφόρτωση",
"uploadFiles": "Μεταφόρτωση {files} αρχείων…",
"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": "Μπορείτε να προσαρμόσετε την εμφάνισης της εφαρμογής File Browser αλλάζοντας το όνομά της, αντικαθιστώντας το λογότυπό της, προσθέτοντας προσαρμοσμένα στυλ και ακόμα και απενεργοποιώντας εξωτερικούς συνδέσμους προς το GitHub.\nΓια περισσότερες πληροφορίες σχετικά με αυτές τις προσαρμογές, ελέγξτε το {0}.",
"changePassword": "Αλλαγή κωδικού πρόσβασης",
"commandRunner": "Εκτέλεση εντολών",
"commandRunnerHelp": "Εδώ μπορείτε να ορίσετε εντολές που εκτελούνται στα ονομασμένα γεγονότα και δραστηριότητες. Πρέπει να γράψετε μία εντολή ανά γραμμή. Οι μεταβλητές περιβάλλοντος {0} και {1} θα είναι διαθέσιμες, και θα είναι {0} σχετικές με το {1}. Για περισσότερες πληροφορίες σχετικά με αυτή τη λειτουργία και τις διαθέσιμες μεταβλητές περιβάλλοντος, παρακαλώ διαβάστε το {2}.",
"commandsUpdated": "Οι εντολές ενημερώθηκαν!",
"createUserDir": "Αυτόματη δημιουργία φακέλου χρήστη κατά την προσθήκη νέου χρήστη",
"tusUploads": "Τμηματικές μεταφορές αρχείων",
"tusUploadsHelp": "Η εφαρμογή File Browser υποστηρίζει τμηματικές μεταφορτώσεις αρχείων, επιτρέποντας την αποδοτική, αξιόπιστη και συνεχιζόμενη μεταφόρτωση αρχείων ακόμα και σε ασταθείς συνδέσεις δικτύου.",
"tusUploadsChunkSize": "Υποδεικνύει το μέγιστο μέγεθος ενός αιτήματος μεταφόρτωσης (για μικρότερες μεταφορές αρχείων θα χρησιμοποιηθούν απευθείας και όχι τμηματικές μεταφορτώσεις). Μπορείτε να εισάγετε έναν ακέραιο αριθμό που υποδηλώνει το μέγεθος σε bytes, ή κείμενο με αριθμό και μονάδα μέτρησης μεγέθους δεδομένων, όπως 10MB, 1GB κλπ.",
"tusUploadsRetryCount": "Αριθμός επαναληπτικών δοκιμών που θα πραγματοποιηθούν αν αποτύχει η μεταφόρτωση ενός τμήματος.",
"userHomeBasePath": "Βασική διαδρομή αρχείων για τους φακέλους των χρηστών",
"userScopeGenerationPlaceholder": "Η εμβέλεια εφαρμογής θα δημιουργηθεί αυτόματα",
"createUserHomeDirectory": "Δημιουργία φακέλου χρήστη",
"customStylesheet": "Προσαρμοσμένο στυλ εμφάνισης (stylesheet)",
"defaultUserDescription": "Αυτές είναι οι προεπιλεγμένες ρυθμίσεις για νέους χρήστες.",
"disableExternalLinks": "Απενεργοποίηση εξωτερικών συνδέσμων (εκτός από συνδέσμους προς τις οδηγίες χρήσης)",
"disableUsedDiskPercentage": "Απενεργοποίηση γραφήματος ποσοστού χρήσης χώρου αποθήκευσης",
"documentation": "οδηγίες χρήσης",
"examples": "Παραδείγματα",
"executeOnShell": "Εκτέλεση στο κέλυφος",
"executeOnShellDescription": "Από προεπιλογή, η εφαρμογή File Browser εκτελεί τις εντολές καλώντας τα προγράμματα των εντολών απευθείας. Αν θέλετε να τις εκτελέσετε σε ένα κέλυφος (όπως το Bash ή το PowerShell), μπορείτε να το καθορίσετε εδώ με τις απαιτούμενες παραμέτρους. Εάν οριστεί, η εντολή που εκτελείτε θα προστίθεται ως παράμετρος. Αυτό ισχύει τόσο για τις εντολές χρήστη όσο και για τους αγκίστρους συμβάντων (event hooks).",
"globalRules": "Πρόκειται για ένα γενικό σύνολο κανόνων που επιτρέπουν και απαγορεύουν διάφορες λειτουργίες και ισχύουν για κάθε χρήστη. Μπορείτε να καθορίσετε συγκεκριμένους κανόνες στις ρυθμίσεις κάθε χρήστη για να παρακάμψετε τους γενικούς κανόνες.",
"globalSettings": "Γενικές ρυθμίσεις",
"hideDotfiles": "Απόκρυψη κρυφών αρχείων (dotfiles)",
"insertPath": "Εισάγετε διαδρομή",
"insertRegex": "Εισάγετε έκφραση regex",
"instanceName": "Όνομα περιβάλλοντος",
"language": "Γλώσσα",
"lockPassword": "Αποτρέψτε τον χρήστη από την αλλαγή του κωδικού πρόσβασης",
"newPassword": "Νέος κωδικός πρόσβασης",
"newPasswordConfirm": "Επιβεβαιώστε τον νέο κωδικό πρόσβασης",
"newUser": "Νέος χρήστης",
"password": "Κωδικός πρόσβασης",
"passwordUpdated": "Ο κωδικός πρόσβασης ενημερώθηκε!",
"path": "Διαδρομή",
"perm": {
"create": "Δημιουργία αρχείων και φακέλων",
"delete": "Διαγραφή αρχείων και φακέλων",
"download": "Λήψη",
"execute": "Εκτέλεση εντολών",
"modify": "Επεξεργασία αρχείων",
"rename": "Μετονομασία ή μετακίνηση αρχείων και φακέλων",
"share": "Κοινοποίηση αρχείων"
},
"upload": {
"abortUpload": "Είστε σίγουροι ότι θέλετε να διακόψετε τη μεταφόρτωση;"
"permissions": "Δικαιώματα",
"permissionsHelp": "Μπορείτε να ορίσετε τον χρήστη ως διαχειριστή ή να επιλέξετε τα δικαιώματα μεμονωμένα. Αν επιλέξετε \"Διαχειριστής\", όλες οι υπόλοιπες επιλογές θα είναι αυτόματα επιλεγμένες. Η διαχείριση χρηστών παραμένει προνόμιο ενός χρήστη με τον ρόλο του διαχειριστή.\n",
"profileSettings": "Ρυθμίσεις προφίλ",
"ruleExample1": "αποκλείει την πρόσβαση σε οποιοδήποτε κρυφό αρχείο (όπως .git, .gitignore) σε κάθε φάκελο.\n",
"ruleExample2": "αποκλείει την πρόσβαση στο αρχείο με το όνομα Caddyfile στον ριζικό φάκελο της εμβέλειας του κανόνα.",
"rules": "Κανόνες",
"rulesHelp": "Εδώ μπορείτε να ορίσετε ένα σύνολο κανόνων που επιτρέπουν και απαγορεύουν διάφορες λειτουργίες για τον συγκεκριμένο χρήστη. Τα αποκλεισμένα αρχεία δεν θα εμφανίζονται στα περιεχόμενα των αντίστοιχων φακέλων και δεν θα είναι προσβάσιμα από τον χρήστη. Υποστηρίζονται εκφράσεις regex και διαδρομές σχετικές με την εμβέλεια αρχείων των χρηστών.\n",
"scope": "Εμβέλεια",
"setDateFormat": "Ορισμός ακριβούς μορφής ημερομηνίας",
"settingsUpdated": "Οι ρυθμίσεις ενημερώθηκαν!",
"shareDuration": "Διάρκεια κοινοποίησης",
"shareManagement": "Διαχείριση κοινοποίησης",
"shareDeleted": "Η κοινοποίηση διαγράφηκε!",
"singleClick": "Χρήση μονού κλικ για να ανοίξετε αρχεία και φακέλους",
"themes": {
"dark": "Σκοτεινό",
"light": "Φωτεινό",
"title": "Μοτίβο"
},
"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",
"el": "Ελληνικά",
"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": "Επιλέξτε τοποθεσία για αντιγραφή των αρχείων σας:",
"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": "Μεταφόρτωση",
"uploadFiles": "Μεταφόρτωση {files} αρχείων…",
"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": "Μπορείτε να προσαρμόσετε την εμφάνισης της εφαρμογής File Browser αλλάζοντας το όνομά της, αντικαθιστώντας το λογότυπό της, προσθέτοντας προσαρμοσμένα στυλ και ακόμα και απενεργοποιώντας εξωτερικούς συνδέσμους προς το GitHub.\nΓια περισσότερες πληροφορίες σχετικά με αυτές τις προσαρμογές, ελέγξτε το {0}.",
"changePassword": "Αλλαγή κωδικού πρόσβασης",
"commandRunner": "Εκτέλεση εντολών",
"commandRunnerHelp": "Εδώ μπορείτε να ορίσετε εντολές που εκτελούνται στα ονομασμένα γεγονότα και δραστηριότητες. Πρέπει να γράψετε μία εντολή ανά γραμμή. Οι μεταβλητές περιβάλλοντος {0} και {1} θα είναι διαθέσιμες, και θα είναι {0} σχετικές με το {1}. Για περισσότερες πληροφορίες σχετικά με αυτή τη λειτουργία και τις διαθέσιμες μεταβλητές περιβάλλοντος, παρακαλώ διαβάστε το {2}.",
"commandsUpdated": "Οι εντολές ενημερώθηκαν!",
"createUserDir": "Αυτόματη δημιουργία φακέλου χρήστη κατά την προσθήκη νέου χρήστη",
"tusUploads": "Τμηματικές μεταφορές αρχείων",
"tusUploadsHelp": "Η εφαρμογή File Browser υποστηρίζει τμηματικές μεταφορτώσεις αρχείων, επιτρέποντας την αποδοτική, αξιόπιστη και συνεχιζόμενη μεταφόρτωση αρχείων ακόμα και σε ασταθείς συνδέσεις δικτύου.",
"tusUploadsChunkSize": "Υποδεικνύει το μέγιστο μέγεθος ενός αιτήματος μεταφόρτωσης (για μικρότερες μεταφορές αρχείων θα χρησιμοποιηθούν απευθείας και όχι τμηματικές μεταφορτώσεις). Μπορείτε να εισάγετε έναν ακέραιο αριθμό που υποδηλώνει το μέγεθος σε bytes, ή κείμενο με αριθμό και μονάδα μέτρησης μεγέθους δεδομένων, όπως 10MB, 1GB κλπ.",
"tusUploadsRetryCount": "Αριθμός επαναληπτικών δοκιμών που θα πραγματοποιηθούν αν αποτύχει η μεταφόρτωση ενός τμήματος.",
"userHomeBasePath": "Βασική διαδρομή αρχείων για τους φακέλους των χρηστών",
"userScopeGenerationPlaceholder": "Η εμβέλεια εφαρμογής θα δημιουργηθεί αυτόματα",
"createUserHomeDirectory": "Δημιουργία φακέλου χρήστη",
"customStylesheet": "Προσαρμοσμένο στυλ εμφάνισης (stylesheet)",
"defaultUserDescription": "Αυτές είναι οι προεπιλεγμένες ρυθμίσεις για νέους χρήστες.",
"disableExternalLinks": "Απενεργοποίηση εξωτερικών συνδέσμων (εκτός από συνδέσμους προς τις οδηγίες χρήσης)",
"disableUsedDiskPercentage": "Απενεργοποίηση γραφήματος ποσοστού χρήσης χώρου αποθήκευσης",
"documentation": "οδηγίες χρήσης",
"examples": "Παραδείγματα",
"executeOnShell": "Εκτέλεση στο κέλυφος",
"executeOnShellDescription": "Από προεπιλογή, η εφαρμογή File Browser εκτελεί τις εντολές καλώντας τα προγράμματα των εντολών απευθείας. Αν θέλετε να τις εκτελέσετε σε ένα κέλυφος (όπως το Bash ή το PowerShell), μπορείτε να το καθορίσετε εδώ με τις απαιτούμενες παραμέτρους. Εάν οριστεί, η εντολή που εκτελείτε θα προστίθεται ως παράμετρος. Αυτό ισχύει τόσο για τις εντολές χρήστη όσο και για τους αγκίστρους συμβάντων (event hooks).",
"globalRules": "Πρόκειται για ένα γενικό σύνολο κανόνων που επιτρέπουν και απαγορεύουν διάφορες λειτουργίες και ισχύουν για κάθε χρήστη. Μπορείτε να καθορίσετε συγκεκριμένους κανόνες στις ρυθμίσεις κάθε χρήστη για να παρακάμψετε τους γενικούς κανόνες.",
"globalSettings": "Γενικές ρυθμίσεις",
"hideDotfiles": "Απόκρυψη κρυφών αρχείων (dotfiles)",
"insertPath": "Εισάγετε διαδρομή",
"insertRegex": "Εισάγετε έκφραση regex",
"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": "Εδώ μπορείτε να ορίσετε ένα σύνολο κανόνων που επιτρέπουν και απαγορεύουν διάφορες λειτουργίες για τον συγκεκριμένο χρήστη. Τα αποκλεισμένα αρχεία δεν θα εμφανίζονται στα περιεχόμενα των αντίστοιχων φακέλων και δεν θα είναι προσβάσιμα από τον χρήστη. Υποστηρίζονται εκφράσεις regex και διαδρομές σχετικές με την εμβέλεια αρχείων των χρηστών.\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",
"login": "Σύνδεση",
"logout": "Αποσύνδεση",
"myFiles": "Τα αρχεία μου",
"newFile": "Νέο αρχείο",
"newFolder": "Νέος φάκελος",
"preview": "Προεπισκόπηση",
"settings": "Ρυθμίσεις",
"signup": "Εγγραφή",
"siteSettings": "Ρυθμίσεις ιστότοπου"
},
"success": {
"linkCopied": "Ο σύνδεσμος αντιγράφηκε!"
},
"time": {
"days": "Ημέρες",
"hours": "Ώρες",
"minutes": "Λεπτά",
"seconds": "Δευτερόλεπτα",
"unit": "Μονάδα χρόνου"
}
"user": "Χρήστης",
"userCommands": "Εντολές χρήστη",
"userCommandsHelp": "Μια λίστα με τις διαθέσιμες εντολές για αυτόν το χρήστη, χωρισμένες μεταξύ τους με κενά. Παράδειγμα:\n",
"userCreated": "Ο χρήστης δημιουργήθηκε!",
"userDefaults": "Προεπιλεγμένες ρυθμίσεις χρήστη",
"userDeleted": "Ο χρήστης διαγράφηκε!",
"userManagement": "Διαχείριση χρηστών",
"userUpdated": "Ο χρήστης ενημερώθηκε!",
"username": "Όνομα χρήστη",
"users": "Χρήστες"
},
"sidebar": {
"help": "Βοήθεια",
"hugoNew": "Νέο Hugo",
"login": "Σύνδεση",
"logout": "Αποσύνδεση",
"myFiles": "Τα αρχεία μου",
"newFile": "Νέο αρχείο",
"newFolder": "Νέος φάκελος",
"preview": "Προεπισκόπηση",
"settings": "Ρυθμίσεις",
"signup": "Εγγραφή",
"siteSettings": "Ρυθμίσεις ιστότοπου"
},
"success": {
"linkCopied": "Ο σύνδεσμος αντιγράφηκε!"
},
"time": {
"days": "Ημέρες",
"hours": "Ώρες",
"minutes": "Λεπτά",
"seconds": "Δευτερόλεπτα",
"unit": "Μονάδα χρόνου"
}
}

View File

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

View File

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

View File

@ -1,15 +1,19 @@
{
"buttons": {
"cancel": "Annuler",
"clear": "Effacer",
"close": "Fermer",
"continue": "Continuer",
"copy": "Copier",
"copyFile": "Copier le fichier",
"copyToClipboard": "Copier dans le presse-papier",
"copyDownloadLinkToClipboard": "Copier le lien de téléchargement dans le presse-papier",
"create": "Créer",
"delete": "Supprimer",
"download": "Télécharger",
"file": "Fichier",
"folder": "Dossier",
"fullScreen": "Plein écran",
"hideDotfiles": "Masquer les dotfiles",
"info": "Info",
"more": "Plus",
@ -51,7 +55,6 @@
},
"files": {
"body": "Corps",
"clear": "Fermer",
"closePreview": "Fermer la prévisualisation",
"files": "Fichiers",
"folders": "Dossiers",
@ -87,23 +90,24 @@
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"fr": "Français",
"is": "",
"is": "Icelandic",
"it": "Italiano",
"ja": "日本語",
"ko": "한국어",
"nlBE": "",
"nlBE": "Dutch (Belgium)",
"pl": "Polski",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ro": "",
"ro": "Romanian",
"ru": "Русский",
"sk": "Slovenčina",
"svSE": "",
"tr" : "Türkçe",
"ua": "Українська",
"svSE": "Swedish (Sweden)",
"tr": "Türkçe",
"uk": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
},
@ -181,7 +185,7 @@
"brandingHelp": "Vous pouvez personnaliser l'apparence de votre instance de File Browser en changeant son nom, en remplaçant le logo, en ajoutant des styles personnalisés et même en désactivant les liens externes vers GitHub.\nPour plus d'informations sur la personnalisation de l'image de marque, veuillez consulter la {0}.",
"changePassword": "Modifier le mot de passe",
"commandRunner": "Command runner",
"commandRunnerHelp" : "Ici, vous pouvez définir les commandes qui sont exécutées pour les événements nommés précédemments. Vous devez en écrire une par ligne. Les variables d'environnement {0} et {1} seront disponibles, {0} étant relatif à {1}. Pour plus d'informations sur cette fonctionnalité et les variables d'environnement disponibles, veuillez lire la {2}.",
"commandRunnerHelp": "Ici, vous pouvez définir les commandes qui sont exécutées pour les événements nommés précédemments. Vous devez en écrire une par ligne. Les variables d'environnement {0} et {1} seront disponibles, {0} étant relatif à {1}. Pour plus d'informations sur cette fonctionnalité et les variables d'environnement disponibles, veuillez lire la {2}.",
"commandsUpdated": "Commandes mises à jour !",
"createUserDir": "Créer automatiquement un dossier pour l'utilisateur",
"customStylesheet": "Feuille de style personnalisée",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
{
"buttons": {
"cancel": "Annuleren",
"clear": "Wissen",
"close": "Sluiten",
"copy": "Kopiëren",
"copyFile": "Bestand kopiëren",
@ -46,7 +47,6 @@
},
"files": {
"body": "Body",
"clear": "Wissen",
"closePreview": "Voorvertoon sluiten",
"files": "Bestanden",
"folders": "Mappen",
@ -79,27 +79,28 @@
"languages": {
"he": "עברית",
"hu": "Magyar",
"ar": "Arabisch",
"de": "Duits",
"en": "Engels",
"es": "Spaans",
"fr": "Frans",
"is": "",
"it": "Italiaans",
"ja": "Japans",
"ko": "Koreaans",
"nlBE": "",
"pl": "Pools",
"pt": "Portugees",
"ptBR": "Portugees (Brazilië)",
"ro": "",
"ru": "Russisch",
"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": "",
"tr" : "Türkçe",
"ua": "Українська",
"zhCN": "Chinees (vereenvoudigd)",
"zhTW": "Chinees (traditioneel)"
"svSE": "Swedish (Sweden)",
"tr": "Türkçe",
"uk": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
},
"login": {
"createAnAccount": "Account aanmaken",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
{
"buttons": {
"cancel": "Vazgeç",
"clear": "Temizle",
"close": "Kapat",
"copy": "Kopyala",
"copyFile": "Dosyayı kopyala",
@ -49,7 +50,6 @@
},
"files": {
"body": "Sayfa",
"clear": "Temizle",
"closePreview": "Önizlemeyi kapat",
"files": "Dosyalar",
"folders": "Klasörler",
@ -85,6 +85,7 @@
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"fr": "Français",
@ -100,8 +101,8 @@
"ru": "Русский",
"sk": "Slovenčina",
"svSE": "Swedish (Sweden)",
"tr" : "Türkçe",
"ua": "Українська",
"tr": "Türkçe",
"uk": "Українська",
"zhCN": "中文 (简体)",
"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": {
"cancel": "取消",
"clear": "清空",
"close": "关闭",
"copy": "复制",
"copyFile": "复制文件",
@ -56,7 +57,6 @@
},
"files": {
"body": "内容",
"clear": "清空",
"closePreview": "关闭预览",
"files": "文件",
"folders": "文件夹",
@ -103,15 +103,15 @@
"nlBE": "Dutch (Belgium)",
"pl": "Polski",
"pt": "Português",
"ptBR": "PortuguêsBrasil",
"ptBR": "Português (Brasil)",
"ro": "Romanian",
"ru": "Русский",
"sk": "Slovenčina",
"svSE": "SwedishSweden",
"tr" : "Türkçe",
"ua": "Українська",
"zhCN": "中文(简体)",
"zhTW": "中文(繁體)"
"svSE": "Swedish (Sweden)",
"tr": "Türkçe",
"uk": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
},
"login": {
"createAnAccount": "创建用户",

View File

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