Compare commits

..

14 Commits

Author SHA1 Message Date
Oleg Lobanov
c1fb4004f7 chore(release): 2.25.0 2023-09-15 00:51:58 +02:00
Cameron
584b706b1e feat: added shell resizing (#2648) 2023-09-15 00:50:40 +02:00
M A E R Y O
ecdd684bf1 feat: implement upload speed calculation and ETA estimation (#2677) 2023-09-15 00:41:36 +02:00
Oleg Lobanov
36af01daa6 fix: tus upload with cloudflare proxy
closes #2593
2023-09-15 00:39:37 +02:00
Thomas
d0c3b8033d chore: update German translation (#2616) 2023-09-10 14:35:51 +02:00
kloon15
aa00c1c89c chore: fixes for vite dev server (#2678) 2023-08-29 20:14:45 +02:00
M A E R Y O
a404fb043d feat: implement abort upload functionality (#2673) 2023-08-27 23:59:49 +02:00
M A E R Y O
95fec7f694 fix: refactor path resolution logic for project root (#2674)
---------

Co-authored-by: MAERYO <maeryo@hanwha.com>
2023-08-27 23:52:33 +02:00
ArthurMousatov
5994224468 feat: add new folder button to move/create dialogs (#2667)
---------

Co-authored-by: Oleg Lobanov <oleg@lobanov.me>
2023-08-27 00:28:58 +02:00
古大羊
374bbd3ec1 perf(backend): optimize subtitles detection performance (#2637) 2023-08-26 14:53:18 +02:00
Oleg Lobanov
2c97573301 build: bump go version to 1.21.0 (#2672) 2023-08-26 14:16:10 +02:00
Oleg Lobanov
70eba7ecc9 build: bump node version to 18 (#2671) 2023-08-26 14:10:07 +02:00
Oleg Lobanov
7a4d0c0c08 chore: fix frontend dev proxy settings 2023-08-26 14:01:01 +02:00
kloon15
8838a09cf5 refactor: migrate frontend tooling to vite 4 (#2645)
---------

Co-authored-by: Oleg Lobanov <oleg@lobanov.me>
2023-08-26 13:55:51 +02:00
72 changed files with 4895 additions and 26204 deletions

View File

@@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
node-version: '18'
- run: make lint-frontend
lint-backend:
runs-on: ubuntu-latest
@@ -24,7 +24,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: 1.20.6
go-version: 1.21.0
- run: make lint-backend
lint-commits:
runs-on: ubuntu-latest
@@ -34,7 +34,7 @@ jobs:
fetch-depth: 0
- uses: actions/setup-node@v2
with:
node-version: '16'
node-version: '18'
- run: make lint-commits
lint:
runs-on: ubuntu-latest
@@ -49,7 +49,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
node-version: '18'
- run: make test-frontend
test-backend:
runs-on: ubuntu-latest
@@ -57,7 +57,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: 1.20.6
go-version: 1.21.0
- run: make test-backend
test:
runs-on: ubuntu-latest
@@ -76,10 +76,10 @@ jobs:
fetch-depth: 0
- uses: actions/setup-go@v2
with:
go-version: 1.20.6
go-version: 1.21.0
- uses: actions/setup-node@v2
with:
node-version: '16'
node-version: '18'
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx

3
.gitignore vendored
View File

@@ -29,3 +29,6 @@ yarn-error.log*
*.sw*
bin/
build/
/frontend/dist/*
!/frontend/dist/.gitkeep

View File

@@ -2,6 +2,38 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [2.25.0](https://github.com/filebrowser/filebrowser/compare/v2.24.2...v2.25.0) (2023-09-14)
### Features
* add new folder button to move/create dialogs ([#2667](https://github.com/filebrowser/filebrowser/issues/2667)) ([5994224](https://github.com/filebrowser/filebrowser/commit/599422446849fa37d5ab448bbf464afb7304b99d))
* added shell resizing ([#2648](https://github.com/filebrowser/filebrowser/issues/2648)) ([584b706](https://github.com/filebrowser/filebrowser/commit/584b706b1e310297acc2580c60442ff5c11ae432))
* implement abort upload functionality ([#2673](https://github.com/filebrowser/filebrowser/issues/2673)) ([a404fb0](https://github.com/filebrowser/filebrowser/commit/a404fb043da2573bf04385863b2d34b1f918b8e1))
* implement upload speed calculation and ETA estimation ([#2677](https://github.com/filebrowser/filebrowser/issues/2677)) ([ecdd684](https://github.com/filebrowser/filebrowser/commit/ecdd684bf1d537a4591caa38348102b61dd51e5d))
### Bug Fixes
* refactor path resolution logic for project root ([#2674](https://github.com/filebrowser/filebrowser/issues/2674)) ([95fec7f](https://github.com/filebrowser/filebrowser/commit/95fec7f69430c108e5cf95c428db9d671cd97a94))
* tus upload with cloudflare proxy ([36af01d](https://github.com/filebrowser/filebrowser/commit/36af01daa6e04005ce3d18985eebaeef06f7393d)), closes [#2593](https://github.com/filebrowser/filebrowser/issues/2593)
### Refactorings
* migrate frontend tooling to vite 4 ([#2645](https://github.com/filebrowser/filebrowser/issues/2645)) ([8838a09](https://github.com/filebrowser/filebrowser/commit/8838a09cf5104deac22b6143050588040c6825e6))
### Build
* bump go version to 1.21.0 ([#2672](https://github.com/filebrowser/filebrowser/issues/2672)) ([2c97573](https://github.com/filebrowser/filebrowser/commit/2c97573301a1b13179678fb7f9bd8316539ecdff))
* bump node version to 18 ([#2671](https://github.com/filebrowser/filebrowser/issues/2671)) ([70eba7e](https://github.com/filebrowser/filebrowser/commit/70eba7ecc9d19545c0899ae40eb3897a7c48562f))
### Performance improvements
* **backend:** optimize subtitles detection performance ([#2637](https://github.com/filebrowser/filebrowser/issues/2637)) ([374bbd3](https://github.com/filebrowser/filebrowser/commit/374bbd3ec199fddbe491ab2b74e520a10a73e54b))
### [2.24.2](https://github.com/filebrowser/filebrowser/compare/v2.24.1...v2.24.2) (2023-08-08)

View File

@@ -29,20 +29,21 @@ const PermDir = 0755
// FileInfo describes a file.
type FileInfo struct {
*Listing
Fs afero.Fs `json:"-"`
Path string `json:"path"`
Name string `json:"name"`
Size int64 `json:"size"`
Extension string `json:"extension"`
ModTime time.Time `json:"modified"`
Mode os.FileMode `json:"mode"`
IsDir bool `json:"isDir"`
IsSymlink bool `json:"isSymlink"`
Type string `json:"type"`
Subtitles []string `json:"subtitles,omitempty"`
Content string `json:"content,omitempty"`
Checksums map[string]string `json:"checksums,omitempty"`
Token string `json:"token,omitempty"`
Fs afero.Fs `json:"-"`
Path string `json:"path"`
Name string `json:"name"`
Size int64 `json:"size"`
Extension string `json:"extension"`
ModTime time.Time `json:"modified"`
Mode os.FileMode `json:"mode"`
IsDir bool `json:"isDir"`
IsSymlink bool `json:"isSymlink"`
Type string `json:"type"`
Subtitles []string `json:"subtitles,omitempty"`
Content string `json:"content,omitempty"`
Checksums map[string]string `json:"checksums,omitempty"`
Token string `json:"token,omitempty"`
currentDir []os.FileInfo `json:"-"`
}
// FileOptions are the options when getting a file info.
@@ -294,13 +295,21 @@ func (i *FileInfo) detectSubtitles() {
// detect multiple languages. Base*.vtt
// TODO: give subtitles descriptive names (lang) and track attributes
parentDir := strings.TrimRight(i.Path, i.Name)
dir, err := afero.ReadDir(i.Fs, parentDir)
if err == nil {
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()))
}
var dir []os.FileInfo
if len(i.currentDir) > 0 {
dir = i.currentDir
} else {
var err error
dir, err = afero.ReadDir(i.Fs, parentDir)
if err != nil {
return
}
}
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()))
}
}
}
@@ -340,15 +349,16 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
}
file := &FileInfo{
Fs: i.Fs,
Name: name,
Size: f.Size(),
ModTime: f.ModTime(),
Mode: f.Mode(),
IsDir: f.IsDir(),
IsSymlink: isSymlink,
Extension: filepath.Ext(name),
Path: fPath,
Fs: i.Fs,
Name: name,
Size: f.Size(),
ModTime: f.ModTime(),
Mode: f.Mode(),
IsDir: f.IsDir(),
IsSymlink: isSymlink,
Extension: filepath.Ext(name),
Path: fPath,
currentDir: dir,
}
if file.IsDir {

20
frontend/.eslintrc.json Normal file
View File

@@ -0,0 +1,20 @@
{
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended",
"@vue/eslint-config-prettier"
],
"rules": {
"vue/multi-word-component-names": "off",
"vue/no-reserved-component-names": "warn",
"vue/no-mutating-props": "warn"
},
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
}
}

2
frontend/.prettierignore Normal file
View File

@@ -0,0 +1,2 @@
# Ignore artifacts:
dist

View File

@@ -0,0 +1,3 @@
{
"trailingComma": "es5"
}

View File

@@ -1,3 +0,0 @@
module.exports = {
presets: ["@vue/app"],
};

View File

@@ -1,4 +0,0 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

0
frontend/dist/.gitkeep vendored Normal file
View File

192
frontend/index.html Normal file
View File

@@ -0,0 +1,192 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, user-scalable=no"
/>
<title>File Browser</title>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/img/icons/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/img/icons/favicon-16x16.png"
/>
<!-- Add to home screen for Android and modern mobile browsers -->
<link
rel="manifest"
id="manifestPlaceholder"
crossorigin="use-credentials"
/>
<meta name="theme-color" content="#2979ff" />
<!-- Add to home screen for Safari on iOS/iPadOS -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="assets" />
<link rel="apple-touch-icon" href="/img/icons/apple-touch-icon.png" />
<!-- Add to home screen for Windows -->
<meta
name="msapplication-TileImage"
content="/img/icons/mstile-144x144.png"
/>
<meta name="msapplication-TileColor" content="#2979ff" />
<!-- Inject Some Variables and generate the manifest json -->
<script>
// We can assign JSON directly
window.FileBrowser = {
AuthMethod: "json",
BaseURL: "",
CSS: false,
Color: "",
DisableExternal: false,
DisableUsedPercentage: false,
EnableExec: true,
EnableThumbs: true,
LoginPage: true,
Name: "",
NoAuth: false,
ReCaptcha: false,
ResizePreview: true,
Signup: false,
StaticURL: "",
Theme: "",
TusSettings: { chunkSize: 10485760, retryCount: 5 },
Version: "(untracked)",
};
// Global function to prepend static url
window.__prependStaticUrl = (url) => {
return `${window.FileBrowser.StaticURL}/${url.replace(/^\/+/, "")}`;
};
var dynamicManifest = {
name: window.FileBrowser.Name || "File Browser",
short_name: window.FileBrowser.Name || "File Browser",
icons: [
{
src: window.__prependStaticUrl(
"/img/icons/android-chrome-192x192.png"
),
sizes: "192x192",
type: "image/png",
},
{
src: window.__prependStaticUrl(
"/img/icons/android-chrome-512x512.png"
),
sizes: "512x512",
type: "image/png",
},
],
start_url: window.location.origin + window.FileBrowser.BaseURL,
display: "standalone",
background_color: "#ffffff",
theme_color: window.FileBrowser.Color || "#455a64",
};
const stringManifest = JSON.stringify(dynamicManifest);
const blob = new Blob([stringManifest], { type: "application/json" });
const manifestURL = URL.createObjectURL(blob);
document
.querySelector("#manifestPlaceholder")
.setAttribute("href", manifestURL);
</script>
<style>
#loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #fff;
z-index: 9999;
transition: 0.1s ease opacity;
-webkit-transition: 0.1s ease opacity;
}
#loading.done {
opacity: 0;
}
#loading .spinner {
width: 70px;
text-align: center;
position: fixed;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
#loading .spinner > div {
width: 18px;
height: 18px;
background-color: #333;
border-radius: 100%;
display: inline-block;
-webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
}
#loading .spinner .bounce1 {
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
#loading .spinner .bounce2 {
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
@-webkit-keyframes sk-bouncedelay {
0%,
80%,
100% {
-webkit-transform: scale(0);
}
40% {
-webkit-transform: scale(1);
}
}
@keyframes sk-bouncedelay {
0%,
80%,
100% {
-webkit-transform: scale(0);
transform: scale(0);
}
40% {
-webkit-transform: scale(1);
transform: scale(1);
}
}
</style>
</head>
<body>
<div id="app"></div>
<div id="loading">
<div class="spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

10
frontend/jsconfig.json Normal file
View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

29113
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,71 +2,59 @@
"name": "filebrowser-frontend",
"version": "2.0.0",
"private": true,
"type": "module",
"scripts": {
"serve": "vue-cli-service serve",
"build": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitignore' -exec rm -r {} + && vue-cli-service build --no-clean",
"lint": "npx vue-cli-service lint --no-fix --max-warnings=0",
"fix": "npx vue-cli-service lint",
"watch": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitignore' -exec rm -r {} + && vue-cli-service build --watch --no-clean"
"dev": "vite dev",
"serve": "vite serve",
"build": "vite build",
"watch": "vite build --watch",
"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 ."
},
"dependencies": {
"ace-builds": "^1.4.7",
"clipboard": "^2.0.4",
"core-js": "^3.9.1",
"css-vars-ponyfill": "^2.4.3",
"js-base64": "^2.5.1",
"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.10.5",
"material-icons": "^1.13.9",
"moment": "^2.29.4",
"normalize.css": "^8.0.1",
"noty": "^3.2.0-beta",
"pretty-bytes": "^6.0.0",
"pretty-bytes": "^6.1.1",
"qrcode.vue": "^1.7.0",
"tus-js-client": "^3.1.0",
"tus-js-client": "^3.1.1",
"utif": "^3.1.0",
"vue": "^2.6.10",
"vue": "^2.7.14",
"vue-async-computed": "^3.9.0",
"vue-i18n": "^8.15.3",
"vue-lazyload": "^1.3.3",
"vue-router": "^3.1.3",
"vue-i18n": "^8.28.2",
"vue-lazyload": "^1.3.5",
"vue-router": "^3.6.5",
"vue-simple-progress": "^1.1.1",
"vuex": "^3.1.2",
"vuex": "^3.6.2",
"vuex-router-sync": "^5.0.0",
"whatwg-fetch": "^3.6.2"
"whatwg-fetch": "^3.6.17"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.1.2",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-service": "^4.1.2",
"@vue/eslint-config-prettier": "^6.0.0",
"babel-eslint": "^10.1.0",
"compression-webpack-plugin": "^6.0.3",
"eslint": "^6.7.2",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-vue": "^6.2.2",
"prettier": "^2.2.1",
"vue-template-compiler": "^2.6.10"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended",
"@vue/prettier"
],
"rules": {},
"parserOptions": {
"parser": "babel-eslint"
}
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
"@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.27",
"prettier": "^3.0.1",
"terser": "^5.19.2",
"vite": "^4.4.9",
"vite-plugin-compression2": "^0.10.3",
"vite-plugin-rewrite-all": "^1.0.1"
},
"browserslist": [
"> 1%",

View File

@@ -0,0 +1,5 @@
module.exports = {
plugins: {
autoprefixer: {},
},
};

0
frontend/public/.gitkeep Normal file
View File

View File

@@ -1,144 +1,193 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, user-scalable=no"
/>
[{[ if .ReCaptcha -]}]
[{[ if .ReCaptcha -]}]
<script src="[{[ .ReCaptchaHost ]}]/recaptcha/api.js?render=explicit"></script>
[{[ end ]}]
[{[ end ]}]
<title>[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]</title>
<title>
[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]
</title>
<link rel="icon" type="image/png" sizes="32x32" href="[{[ .StaticURL ]}]/img/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="[{[ .StaticURL ]}]/img/icons/favicon-16x16.png">
<link
rel="icon"
type="image/png"
sizes="32x32"
href="[{[ .StaticURL ]}]/img/icons/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="[{[ .StaticURL ]}]/img/icons/favicon-16x16.png"
/>
<!-- Add to home screen for Android and modern mobile browsers -->
<link rel="manifest" id="manifestPlaceholder" crossorigin="use-credentials">
<meta name="theme-color" content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]">
<!-- Add to home screen for Android and modern mobile browsers -->
<link
rel="manifest"
id="manifestPlaceholder"
crossorigin="use-credentials"
/>
<meta
name="theme-color"
content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]"
/>
<!-- Add to home screen for Safari on iOS/iPadOS -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="assets">
<link rel="apple-touch-icon" href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon.png">
<!-- Add to home screen for Safari on iOS/iPadOS -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="assets" />
<link
rel="apple-touch-icon"
href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon.png"
/>
<!-- Add to home screen for Windows -->
<meta name="msapplication-TileImage" content="[{[ .StaticURL ]}]/img/icons/mstile-144x144.png">
<meta name="msapplication-TileColor" content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]">
<!-- Add to home screen for Windows -->
<meta
name="msapplication-TileImage"
content="[{[ .StaticURL ]}]/img/icons/mstile-144x144.png"
/>
<meta
name="msapplication-TileColor"
content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]"
/>
<!-- Inject Some Variables and generate the manifest json -->
<script>
window.FileBrowser = JSON.parse('[{[ .Json ]}]');
<!-- Inject Some Variables and generate the manifest json -->
<script>
// We can assign JSON directly
window.FileBrowser = [{[ .Json ]}];
// Global function to prepend static url
window.__prependStaticUrl = (url) => {
return `${window.FileBrowser.StaticURL}/${url.replace(/^\/+/, "")}`;
};
var dynamicManifest = {
name: window.FileBrowser.Name || "File Browser",
short_name: window.FileBrowser.Name || "File Browser",
icons: [
{
src: window.__prependStaticUrl("/img/icons/android-chrome-192x192.png"),
sizes: "192x192",
type: "image/png",
},
{
src: window.__prependStaticUrl("/img/icons/android-chrome-512x512.png"),
sizes: "512x512",
type: "image/png",
},
],
start_url: window.location.origin + window.FileBrowser.BaseURL,
display: "standalone",
background_color: "#ffffff",
theme_color: window.FileBrowser.Color || "#455a64",
};
var fullStaticURL = window.location.origin + window.FileBrowser.StaticURL;
var dynamicManifest = {
"name": window.FileBrowser.Name || 'File Browser',
"short_name": window.FileBrowser.Name || 'File Browser',
"icons": [
{
"src": fullStaticURL + "/img/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": fullStaticURL + "/img/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
const stringManifest = JSON.stringify(dynamicManifest);
const blob = new Blob([stringManifest], { type: "application/json" });
const manifestURL = URL.createObjectURL(blob);
document
.querySelector("#manifestPlaceholder")
.setAttribute("href", manifestURL);
</script>
<style>
#loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #fff;
z-index: 9999;
transition: 0.1s ease opacity;
-webkit-transition: 0.1s ease opacity;
}
#loading.done {
opacity: 0;
}
#loading .spinner {
width: 70px;
text-align: center;
position: fixed;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
#loading .spinner > div {
width: 18px;
height: 18px;
background-color: #333;
border-radius: 100%;
display: inline-block;
-webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
}
#loading .spinner .bounce1 {
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
#loading .spinner .bounce2 {
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
@-webkit-keyframes sk-bouncedelay {
0%,
80%,
100% {
-webkit-transform: scale(0);
}
],
"start_url": window.location.origin + window.FileBrowser.BaseURL,
"display": "standalone",
"background_color": "#ffffff",
"theme_color": window.FileBrowser.Color || "#455a64"
}
40% {
-webkit-transform: scale(1);
}
}
const stringManifest = JSON.stringify(dynamicManifest);
const blob = new Blob([stringManifest], {type: 'application/json'});
const manifestURL = URL.createObjectURL(blob);
document.querySelector('#manifestPlaceholder').setAttribute('href', manifestURL);
</script>
@keyframes sk-bouncedelay {
0%,
80%,
100% {
-webkit-transform: scale(0);
transform: scale(0);
}
40% {
-webkit-transform: scale(1);
transform: scale(1);
}
}
</style>
</head>
<body>
<div id="app"></div>
<style>
#loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #fff;
z-index: 9999;
transition: .1s ease opacity;
-webkit-transition: .1s ease opacity;
}
#loading.done {
opacity: 0;
}
#loading .spinner {
width: 70px;
text-align: center;
position: fixed;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
#loading .spinner > div {
width: 18px;
height: 18px;
background-color: #333;
border-radius: 100%;
display: inline-block;
-webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
}
#loading .spinner .bounce1 {
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
#loading .spinner .bounce2 {
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
@-webkit-keyframes sk-bouncedelay {
0%, 80%, 100% { -webkit-transform: scale(0) }
40% { -webkit-transform: scale(1.0) }
}
@keyframes sk-bouncedelay {
0%, 80%, 100% {
-webkit-transform: scale(0);
transform: scale(0);
} 40% {
-webkit-transform: scale(1.0);
transform: scale(1.0);
}
}
</style>
</head>
<body>
<div id="app"></div>
<div id="loading">
<div class="spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
<div id="loading">
<div class="spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
</div>
</div>
[{[ if .Theme -]}]
<link rel="stylesheet" href="[{[ .StaticURL ]}]/themes/[{[ .Theme ]}].css" />
[{[ end ]}]
[{[ if .CSS -]}]
<script type="module" src="/src/main.js"></script>
[{[ if .Theme -]}]
<link
rel="stylesheet"
href="[{[ .StaticURL ]}]/themes/[{[ .Theme ]}].css"
/>
[{[ end ]}] [{[ if .CSS -]}]
<link rel="stylesheet" href="[{[ .StaticURL ]}]/custom.css" />
[{[ end ]}]
</body>
[{[ end ]}]
</body>
</html>

View File

@@ -174,6 +174,12 @@ table th {
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);
}

View File

@@ -6,7 +6,7 @@
<script>
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.FileBrowser.StaticURL + "/";
// __webpack_public_path__ = window.FileBrowser.StaticURL + "/";
export default {
name: "app",

View File

@@ -6,6 +6,12 @@ import { fetchURL } from "./utils";
const RETRY_BASE_DELAY = 1000;
const RETRY_MAX_DELAY = 20000;
const SPEED_UPDATE_INTERVAL = 1000;
const ALPHA = 0.2;
const ONE_MINUS_ALPHA = 1 - ALPHA;
const RECENT_SPEEDS_LIMIT = 5;
const MB_DIVISOR = 1024 * 1024;
const CURRENT_UPLOAD_LIST = {};
export async function upload(
filePath,
@@ -34,19 +40,47 @@ export async function upload(
"X-Auth": store.state.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);
},
onProgress: function (bytesUploaded) {
// Emulate ProgressEvent.loaded which is used by calling functions
// loaded is specified in bytes (https://developer.mozilla.org/en-US/docs/Web/API/ProgressEvent/loaded)
let fileData = CURRENT_UPLOAD_LIST[filePath];
fileData.currentBytesUploaded = bytesUploaded;
if (!fileData.hasStarted) {
fileData.hasStarted = true;
fileData.lastProgressTimestamp = Date.now();
fileData.interval = setInterval(() => {
calcProgress(filePath);
}, SPEED_UPDATE_INTERVAL);
}
if (typeof onupload === "function") {
onupload({ loaded: bytesUploaded });
}
},
onSuccess: function () {
if (CURRENT_UPLOAD_LIST[filePath].interval) {
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
}
delete CURRENT_UPLOAD_LIST[filePath];
resolve();
},
});
CURRENT_UPLOAD_LIST[filePath] = {
upload: upload,
recentSpeeds: [],
initialBytesUploaded: 0,
currentBytesUploaded: 0,
currentAverageSpeed: 0,
lastProgressTimestamp: null,
sumOfRecentSpeeds: 0,
hasStarted: false,
interval: null,
};
upload.start();
});
}
@@ -88,3 +122,74 @@ export async function useTus(content) {
function isTusSupported() {
return tus.isSupported === true;
}
function computeETA(state) {
if (state.speedMbyte === 0) {
return Infinity;
}
const totalSize = state.sizes.reduce((acc, size) => acc + size, 0);
const uploadedSize = state.progress.reduce(
(acc, progress) => acc + progress,
0
);
const remainingSize = totalSize - uploadedSize;
const speedBytesPerSecond = state.speedMbyte * 1024 * 1024;
return remainingSize / speedBytesPerSecond;
}
function computeGlobalSpeedAndETA() {
let totalSpeed = 0;
let totalCount = 0;
for (let filePath in CURRENT_UPLOAD_LIST) {
totalSpeed += CURRENT_UPLOAD_LIST[filePath].currentAverageSpeed;
totalCount++;
}
if (totalCount === 0) return { speed: 0, eta: Infinity };
const averageSpeed = totalSpeed / totalCount;
const averageETA = computeETA(store.state.upload, averageSpeed);
return { speed: averageSpeed, eta: averageETA };
}
function calcProgress(filePath) {
let fileData = CURRENT_UPLOAD_LIST[filePath];
let elapsedTime = (Date.now() - fileData.lastProgressTimestamp) / 1000;
let bytesSinceLastUpdate =
fileData.currentBytesUploaded - fileData.initialBytesUploaded;
let currentSpeed = bytesSinceLastUpdate / MB_DIVISOR / elapsedTime;
if (fileData.recentSpeeds.length >= RECENT_SPEEDS_LIMIT) {
fileData.sumOfRecentSpeeds -= fileData.recentSpeeds.shift();
}
fileData.recentSpeeds.push(currentSpeed);
fileData.sumOfRecentSpeeds += currentSpeed;
let 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);
fileData.initialBytesUploaded = fileData.currentBytesUploaded;
fileData.lastProgressTimestamp = Date.now();
}
export function abortAllUploads() {
for (let filePath in CURRENT_UPLOAD_LIST) {
if (CURRENT_UPLOAD_LIST[filePath].interval) {
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
}
if (CURRENT_UPLOAD_LIST[filePath].upload) {
CURRENT_UPLOAD_LIST[filePath].upload.abort(true);
}
delete CURRENT_UPLOAD_LIST[filePath];
}
}

View File

@@ -90,10 +90,10 @@ export default {
};
},
watch: {
show(val, old) {
this.active = val === "search";
currentPrompt(val, old) {
this.active = val?.prompt === "search";
if (old === "search" && !this.active) {
if (old?.prompt === "search" && !this.active) {
if (this.reload) {
this.setReload(true);
}
@@ -116,8 +116,8 @@ export default {
},
},
computed: {
...mapState(["user", "show"]),
...mapGetters(["isListing"]),
...mapState(["user"]),
...mapGetters(["isListing", "currentPrompt"]),
boxes() {
return boxes;
},

View File

@@ -1,37 +1,54 @@
<template>
<div
@click="focus"
class="shell"
ref="scrollable"
:class="{ ['shell--hidden']: !showShell }"
:style="{ height: `${this.shellHeight}em` }"
>
<div v-for="(c, index) in content" :key="index" class="shell__result">
<div class="shell__prompt">
<i class="material-icons">chevron_right</i>
<div
@pointerdown="startDrag()"
@pointerup="stopDrag()"
class="shell__divider"
:style="this.shellDrag ? { background: `${checkTheme()}` } : ''"
></div>
<div @click="focus" class="shell__content" ref="scrollable">
<div v-for="(c, index) in content" :key="index" class="shell__result">
<div class="shell__prompt">
<i class="material-icons">chevron_right</i>
</div>
<pre class="shell__text">{{ c.text }}</pre>
</div>
<pre class="shell__text">{{ c.text }}</pre>
</div>
<div class="shell__result" :class="{ 'shell__result--hidden': !canInput }">
<div class="shell__prompt">
<i class="material-icons">chevron_right</i>
<div
class="shell__result"
:class="{ 'shell__result--hidden': !canInput }"
>
<div class="shell__prompt">
<i class="material-icons">chevron_right</i>
</div>
<pre
tabindex="0"
ref="input"
class="shell__text"
contenteditable="true"
@keydown.prevent.38="historyUp"
@keydown.prevent.40="historyDown"
@keypress.prevent.enter="submit"
/>
</div>
<pre
tabindex="0"
ref="input"
class="shell__text"
contenteditable="true"
@keydown.prevent.38="historyUp"
@keydown.prevent.40="historyDown"
@keypress.prevent.enter="submit"
/>
</div>
<div
@pointerup="stopDrag()"
class="shell__overlay"
v-show="this.shellDrag"
></div>
</div>
</template>
<script>
import { mapMutations, mapState, mapGetters } from "vuex";
import { commands } from "@/api";
import { throttle } from "lodash";
import { theme } from "@/utils/constants";
export default {
name: "shell",
@@ -51,9 +68,55 @@ export default {
history: [],
historyPos: 0,
canInput: true,
shellDrag: false,
shellHeight: 25,
fontsize: parseFloat(getComputedStyle(document.documentElement).fontSize),
}),
mounted() {
window.addEventListener("resize", this.resize);
},
beforeDestroy() {
window.removeEventListener("resize", this.resize);
},
methods: {
...mapMutations(["toggleShell"]),
checkTheme() {
if (theme == "dark") {
return "rgba(255, 255, 255, 0.4)";
}
return "rgba(127, 127, 127, 0.4)";
},
startDrag() {
document.addEventListener("pointermove", this.handleDrag);
this.shellDrag = true;
},
stopDrag() {
document.removeEventListener("pointermove", this.handleDrag);
this.shellDrag = false;
},
handleDrag: throttle(function (event) {
const top = window.innerHeight / this.fontsize - 4;
const userPos = (window.innerHeight - event.clientY) / this.fontsize;
const bottom =
2.25 +
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
if (userPos <= top && userPos >= bottom) {
this.shellHeight = userPos.toFixed(2);
}
}, 32),
resize: throttle(function () {
const top = window.innerHeight / this.fontsize - 4;
const bottom =
2.25 +
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
if (this.shellHeight > top) {
this.shellHeight = top;
} else if (this.shellHeight < bottom) {
this.shellHeight = bottom;
}
}, 32),
scroll: function () {
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight;
},

View File

@@ -133,9 +133,9 @@ export default {
},
computed: {
...mapState(["user"]),
...mapGetters(["isLogged"]),
...mapGetters(["isLogged", "currentPrompt"]),
active() {
return this.$store.state.show === "sidebar";
return this.currentPrompt?.prompt === "sidebar";
},
signup: () => signup,
version: () => version,

View File

@@ -10,12 +10,7 @@
@mouseup="mouseUp"
@wheel="wheelMove"
>
<img
src=""
class="image-ex-img image-ex-img-center"
ref="imgex"
@load="onLoad"
/>
<img class="image-ex-img image-ex-img-center" ref="imgex" @load="onLoad" />
</div>
</template>
<script>

View File

@@ -37,7 +37,7 @@
<script>
import { enableThumbs } from "@/utils/constants";
import { mapMutations, mapGetters, mapState } from "vuex";
import filesize from "filesize";
import { filesize } from "filesize";
import moment from "moment";
import { files as api } from "@/api";
import * as upload from "@/utils/upload";

View File

@@ -11,7 +11,7 @@
<slot />
<div id="dropdown" :class="{ active: this.$store.state.show === 'more' }">
<div id="dropdown" :class="{ active: this.currentPromptName === 'more' }">
<slot name="actions" />
</div>
@@ -25,7 +25,7 @@
<div
class="overlay"
v-show="this.$store.state.show == 'more'"
v-show="this.currentPromptName == 'more'"
@click="$store.commit('closeHovers')"
/>
</header>
@@ -34,7 +34,8 @@
<script>
import { logoURL } from "@/utils/constants";
import Action from "@/components/header/Action";
import Action from "@/components/header/Action.vue";
import { mapGetters } from "vuex";
export default {
name: "header-bar",
@@ -52,6 +53,9 @@ export default {
this.$store.commit("showHover", "sidebar");
},
},
computed: {
...mapGetters(["currentPromptName"]),
},
};
</script>

View File

@@ -6,33 +6,50 @@
<div class="card-content">
<p>{{ $t("prompts.copyMessage") }}</p>
<file-list @update:selected="(val) => (dest = val)"></file-list>
<file-list ref="fileList" @update:selected="(val) => (dest = val)">
</file-list>
</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')"
>
{{ $t("buttons.cancel") }}
</button>
<button
class="button button--flat"
@click="copy"
:aria-label="$t('buttons.copy')"
:title="$t('buttons.copy')"
>
{{ $t("buttons.copy") }}
</button>
<div
class="card-action"
style="display: flex; align-items: center; justify-content: space-between;"
>
<template v-if="user.perm.create">
<button
class="button button--flat"
@click="$refs.fileList.createDir()"
:aria-label="$t('sidebar.newFolder')"
:title="$t('sidebar.newFolder')"
style="justify-self: left;"
>
<span>{{ $t("sidebar.newFolder") }}</span>
</button>
</template>
<div>
<button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
>
{{ $t("buttons.cancel") }}
</button>
<button
class="button button--flat"
@click="copy"
:aria-label="$t('buttons.copy')"
:title="$t('buttons.copy')"
>
{{ $t("buttons.copy") }}
</button>
</div>
</div>
</div>
</template>
<script>
import { mapState } from "vuex";
import FileList from "./FileList";
import FileList from "./FileList.vue";
import { files as api } from "@/api";
import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload";
@@ -46,7 +63,7 @@ export default {
dest: null,
};
},
computed: mapState(["req", "selected"]),
computed: mapState(["req", "selected", "user"]),
methods: {
copy: async function (event) {
event.preventDefault();

View File

@@ -37,8 +37,8 @@ import buttons from "@/utils/buttons";
export default {
name: "delete",
computed: {
...mapGetters(["isListing", "selectedCount"]),
...mapState(["req", "selected", "showConfirm"]),
...mapGetters(["isListing", "selectedCount", "currentPrompt"]),
...mapState(["req", "selected"]),
},
methods: {
...mapMutations(["closeHovers"]),
@@ -50,7 +50,7 @@ export default {
await api.remove(this.$route.path);
buttons.success("delete");
this.showConfirm();
this.currentPrompt?.confirm();
this.closeHovers();
return;
}

View File

@@ -11,7 +11,7 @@
v-for="(ext, format) in formats"
:key="format"
class="button button--block"
@click="showConfirm(format)"
@click="currentPrompt.confirm(format)"
v-focus
>
{{ ext }}
@@ -21,7 +21,7 @@
</template>
<script>
import { mapState } from "vuex";
import { mapGetters } from "vuex";
export default {
name: "download",
@@ -38,6 +38,8 @@ export default {
},
};
},
computed: mapState(["showConfirm"]),
computed: {
...mapGetters(["currentPrompt"]),
},
};
</script>

View File

@@ -133,6 +133,17 @@ export default {
this.selected = event.currentTarget.dataset.url;
this.$emit("update:selected", this.selected);
},
createDir: async function () {
this.$store.commit("showHover", {
prompt: "newDir",
action: null,
confirm: null,
props: {
redirect: false,
base: this.current === this.$route.path ? null : this.current,
},
});
},
},
};
</script>

View File

@@ -81,7 +81,7 @@
<script>
import { mapState, mapGetters } from "vuex";
import filesize from "filesize";
import { filesize } from "filesize";
import moment from "moment";
import { files as api } from "@/api";

View File

@@ -5,34 +5,51 @@
</div>
<div class="card-content">
<file-list @update:selected="(val) => (dest = val)"></file-list>
<file-list ref="fileList" @update:selected="(val) => (dest = val)">
</file-list>
</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')"
>
{{ $t("buttons.cancel") }}
</button>
<button
class="button button--flat"
@click="move"
:disabled="$route.path === dest"
:aria-label="$t('buttons.move')"
:title="$t('buttons.move')"
>
{{ $t("buttons.move") }}
</button>
<div
class="card-action"
style="display: flex; align-items: center; justify-content: space-between;"
>
<template v-if="user.perm.create">
<button
class="button button--flat"
@click="$refs.fileList.createDir()"
:aria-label="$t('sidebar.newFolder')"
:title="$t('sidebar.newFolder')"
style="justify-self: left;"
>
<span>{{ $t("sidebar.newFolder") }}</span>
</button>
</template>
<div>
<button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
>
{{ $t("buttons.cancel") }}
</button>
<button
class="button button--flat"
@click="move"
:disabled="$route.path === dest"
:aria-label="$t('buttons.move')"
:title="$t('buttons.move')"
>
{{ $t("buttons.move") }}
</button>
</div>
</div>
</div>
</template>
<script>
import { mapState } from "vuex";
import FileList from "./FileList";
import FileList from "./FileList.vue";
import { files as api } from "@/api";
import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload";
@@ -46,7 +63,7 @@ export default {
dest: null,
};
},
computed: mapState(["req", "selected"]),
computed: mapState(["req", "selected", "user"]),
methods: {
move: async function (event) {
event.preventDefault();

View File

@@ -43,6 +43,16 @@ import url from "@/utils/url";
export default {
name: "new-dir",
props: {
redirect: {
type: Boolean,
default: true,
},
base: {
type: [String, null],
default: null,
},
},
data: function () {
return {
name: "",
@@ -57,7 +67,11 @@ export default {
if (this.new === "") return;
// Build the path of the new directory.
let uri = this.isFiles ? this.$route.path + "/" : "/";
let uri;
if (this.base) uri = this.base;
else if (this.isFiles) uri = this.$route.path + "/";
else uri = "/";
if (!this.isListing) {
uri = url.removeLastDir(uri) + "/";
@@ -65,10 +79,14 @@ export default {
uri += encodeURIComponent(this.name) + "/";
uri = uri.replace("//", "/");
try {
await api.post(uri);
this.$router.push({ path: 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);
}

View File

@@ -1,26 +1,33 @@
<template>
<div>
<component ref="currentComponent" :is="currentComponent"></component>
<component
v-if="showOverlay"
:ref="currentPromptName"
:is="currentPromptName"
v-bind="currentPrompt.props"
>
</component>
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
</div>
</template>
<script>
import Help from "./Help";
import Info from "./Info";
import Delete from "./Delete";
import Rename from "./Rename";
import Download from "./Download";
import Move from "./Move";
import Copy from "./Copy";
import NewFile from "./NewFile";
import NewDir from "./NewDir";
import Replace from "./Replace";
import ReplaceRename from "./ReplaceRename";
import Share from "./Share";
import Upload from "./Upload";
import ShareDelete from "./ShareDelete";
import { mapState } from "vuex";
import Help from "./Help.vue";
import Info from "./Info.vue";
import Delete from "./Delete.vue";
import Rename from "./Rename.vue";
import Download from "./Download.vue";
import Move from "./Move.vue";
import Copy from "./Copy.vue";
import NewFile from "./NewFile.vue";
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 { mapGetters, mapState } from "vuex";
import buttons from "@/utils/buttons";
export default {
@@ -40,6 +47,7 @@ export default {
ReplaceRename,
Upload,
ShareDelete,
Sidebar
},
data: function () {
return {
@@ -52,7 +60,7 @@ export default {
},
created() {
window.addEventListener("keydown", (event) => {
if (this.show == null) return;
if (this.currentPrompt == null) return;
let prompt = this.$refs.currentComponent;
@@ -64,7 +72,7 @@ export default {
// Enter
if (event.keyCode == 13) {
switch (this.show) {
switch (this.currentPrompt.prompt) {
case "delete":
prompt.submit();
break;
@@ -82,31 +90,13 @@ export default {
});
},
computed: {
...mapState(["show", "plugins"]),
currentComponent: function () {
const matched =
[
"info",
"help",
"delete",
"rename",
"move",
"copy",
"newFile",
"newDir",
"download",
"replace",
"replace-rename",
"share",
"upload",
"share-delete",
].indexOf(this.show) >= 0;
return (matched && this.show) || null;
},
...mapState(["plugins"]),
...mapGetters(["currentPrompt", "currentPromptName"]),
showOverlay: function () {
return (
this.show !== null && this.show !== "search" && this.show !== "more"
this.currentPrompt !== null &&
this.currentPrompt.prompt !== "search" &&
this.currentPrompt.prompt !== "more"
);
},
},

View File

@@ -19,7 +19,7 @@
</button>
<button
class="button button--flat button--blue"
@click="showAction"
@click="currentPrompt.action"
:aria-label="$t('buttons.continue')"
:title="$t('buttons.continue')"
>
@@ -27,7 +27,7 @@
</button>
<button
class="button button--flat button--red"
@click="showConfirm"
@click="currentPrompt.confirm"
:aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')"
>
@@ -38,10 +38,10 @@
</template>
<script>
import { mapState } from "vuex";
import { mapGetters } from "vuex";
export default {
name: "replace",
computed: mapState(["showConfirm", "showAction"]),
computed: mapGetters(["currentPrompt"]),
};
</script>

View File

@@ -19,7 +19,7 @@
</button>
<button
class="button button--flat button--blue"
@click="(event) => showConfirm(event, 'rename')"
@click="(event) => currentPrompt.confirm(event, 'rename')"
:aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')"
>
@@ -27,7 +27,7 @@
</button>
<button
class="button button--flat button--red"
@click="(event) => showConfirm(event, 'overwrite')"
@click="(event) => currentPrompt.confirm(event, 'overwrite')"
:aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')"
>
@@ -38,10 +38,10 @@
</template>
<script>
import { mapState } from "vuex";
import { mapGetters } from "vuex";
export default {
name: "replace-rename",
computed: mapState(["showConfirm"]),
computed: mapGetters(["currentPrompt"]),
};
</script>

View File

@@ -25,16 +25,16 @@
</template>
<script>
import { mapState } from "vuex";
import { mapGetters } from "vuex";
export default {
name: "share-delete",
computed: {
...mapState(["showConfirm"]),
...mapGetters(["currentPrompt"]),
},
methods: {
submit: function () {
this.showConfirm();
this.currentPrompt?.confirm();
},
},
};

View File

@@ -7,7 +7,18 @@
<div class="card floating">
<div class="card-title">
<h2>{{ $t("prompts.uploadFiles", { files: filesInUploadCount }) }}</h2>
<div class="upload-info">
<div class="upload-speed">{{ uploadSpeed.toFixed(2) }} MB/s</div>
<div class="upload-eta">{{ formattedETA }} remaining</div>
</div>
<button
class="action"
@click="abortAll"
aria-label="Abort upload"
title="Abort upload"
>
<i class="material-icons">{{ "cancel" }}</i>
</button>
<button
class="action"
@click="toggle"
@@ -42,7 +53,9 @@
</template>
<script>
import { mapGetters } from "vuex";
import { mapGetters, mapMutations } from "vuex";
import { abortAllUploads } from "@/api/tus";
import buttons from "@/utils/buttons";
export default {
name: "uploadFiles",
@@ -52,12 +65,42 @@ export default {
};
},
computed: {
...mapGetters(["filesInUpload", "filesInUploadCount"]),
...mapGetters([
"filesInUpload",
"filesInUploadCount",
"uploadSpeed",
"eta",
]),
...mapMutations(["resetUpload"]),
formattedETA() {
if (!this.eta || this.eta === Infinity) {
return "--:--:--";
}
let totalSeconds = this.eta;
const hours = Math.floor(totalSeconds / 3600);
totalSeconds %= 3600;
const minutes = Math.floor(totalSeconds / 60);
const seconds = Math.round(totalSeconds % 60);
return `${hours.toString().padStart(2, "0")}:${minutes
.toString()
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
},
},
methods: {
toggle: function () {
this.open = !this.open;
},
abortAll() {
if (confirm(this.$t("upload.abortUpload"))) {
abortAllUploads();
buttons.done("upload");
this.open = false;
this.$store.commit("resetUpload");
this.$store.commit("setReload", true);
}
},
},
};
</script>

View File

@@ -67,10 +67,10 @@
</template>
<script>
import Languages from "./Languages";
import Rules from "./Rules";
import Permissions from "./Permissions";
import Commands from "./Commands";
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";
export default {

View File

@@ -2,21 +2,49 @@
position: fixed;
bottom: 0;
left: 0;
height: 25em;
max-height: calc(100% - 4em);
background: white;
color: #212121;
z-index: 9999;
z-index: 9997;
width: 100%;
font-family: monospace;
overflow: auto;
font-size: 1rem;
cursor: text;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
transition: .2s ease transform;
}
body.rtl .shell {
.shell__divider {
position: relative;
height: 8px;
z-index: 9999;
background: rgba(127, 127, 127, 0.1);
transition: 0.2s ease background;
cursor: ns-resize;
touch-action: none;
user-select: none;
}
.shell__divider:hover {
background: rgba(127, 127, 127, 0.4);
}
.shell__content {
height: 100%;
font-family: monospace;
overflow: auto;
font-size: 1rem;
cursor: text;
}
.shell__overlay {
position: fixed;
width: 100%;
height: 100%;
top: 0px;
left: 0px;
z-index: 9998;
background-color: rgba(0, 0, 0, 0.05);
}
body.rtl .shell-content {
direction: ltr;
}

View File

@@ -1,173 +1,242 @@
@import "material-icons/iconfont/filled.css";
@font-face {
font-family: 'Roboto';
font-family: "Roboto";
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-cyrillic-ext.woff2) format('woff2');
src:
local("Roboto"),
local("Roboto-Regular"),
url(../assets/fonts/roboto/normal-cyrillic-ext.woff2) format("woff2");
unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
}
@font-face {
font-family: 'Roboto';
font-family: "Roboto";
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-cyrillic.woff2) format('woff2');
src:
local("Roboto"),
local("Roboto-Regular"),
url(../assets/fonts/roboto/normal-cyrillic.woff2) format("woff2");
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: 'Roboto';
font-family: "Roboto";
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-greek-ext.woff2) format('woff2');
src:
local("Roboto"),
local("Roboto-Regular"),
url(../assets/fonts/roboto/normal-greek-ext.woff2) format("woff2");
unicode-range: U+1F00-1FFF;
}
@font-face {
font-family: 'Roboto';
font-family: "Roboto";
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-greek.woff2) format('woff2');
src:
local("Roboto"),
local("Roboto-Regular"),
url(../assets/fonts/roboto/normal-greek.woff2) format("woff2");
unicode-range: U+0370-03FF;
}
@font-face {
font-family: 'Roboto';
font-family: "Roboto";
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-vietnamese.woff2) format('woff2');
src:
local("Roboto"),
local("Roboto-Regular"),
url(../assets/fonts/roboto/normal-vietnamese.woff2) format("woff2");
unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
}
@font-face {
font-family: 'Roboto';
font-family: "Roboto";
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-latin-ext.woff2) format('woff2');
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
src:
local("Roboto"),
local("Roboto-Regular"),
url(../assets/fonts/roboto/normal-latin-ext.woff2) format("woff2");
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F,
U+A720-A7FF;
}
@font-face {
font-family: 'Roboto';
font-family: "Roboto";
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
src:
local("Roboto"),
local("Roboto-Regular"),
url(../assets/fonts/roboto/normal-latin.woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC,
U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
}
@font-face {
font-family: 'Roboto';
font-family: "Roboto";
font-style: normal;
font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-cyrillic-ext.woff2) format('woff2');
src:
local("Roboto Medium"),
local("Roboto-Medium"),
url(../assets/fonts/roboto/medium-cyrillic-ext.woff2) format("woff2");
unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
}
@font-face {
font-family: 'Roboto';
font-family: "Roboto";
font-style: normal;
font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-cyrillic.woff2) format('woff2');
src:
local("Roboto Medium"),
local("Roboto-Medium"),
url(../assets/fonts/roboto/medium-cyrillic.woff2) format("woff2");
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: 'Roboto';
font-family: "Roboto";
font-style: normal;
font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-greek-ext.woff2) format('woff2');
src:
local("Roboto Medium"),
local("Roboto-Medium"),
url(../assets/fonts/roboto/medium-greek-ext.woff2) format("woff2");
unicode-range: U+1F00-1FFF;
}
@font-face {
font-family: 'Roboto';
font-family: "Roboto";
font-style: normal;
font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-greek.woff2) format('woff2');
src:
local("Roboto Medium"),
local("Roboto-Medium"),
url(../assets/fonts/roboto/medium-greek.woff2) format("woff2");
unicode-range: U+0370-03FF;
}
@font-face {
font-family: 'Roboto';
font-family: "Roboto";
font-style: normal;
font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-vietnamese.woff2) format('woff2');
src:
local("Roboto Medium"),
local("Roboto-Medium"),
url(../assets/fonts/roboto/medium-vietnamese.woff2) format("woff2");
unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
}
@font-face {
font-family: 'Roboto';
font-family: "Roboto";
font-style: normal;
font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-latin-ext.woff2) format('woff2');
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
src:
local("Roboto Medium"),
local("Roboto-Medium"),
url(../assets/fonts/roboto/medium-latin-ext.woff2) format("woff2");
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F,
U+A720-A7FF;
}
@font-face {
font-family: 'Roboto';
font-family: "Roboto";
font-style: normal;
font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
src:
local("Roboto Medium"),
local("Roboto-Medium"),
url(../assets/fonts/roboto/medium-latin.woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC,
U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
}
@font-face {
font-family: 'Roboto';
font-family: "Roboto";
font-style: normal;
font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'), url(../assets/fonts/roboto/bold-cyrillic-ext.woff2) format('woff2');
src:
local("Roboto Bold"),
local("Roboto-Bold"),
url(../assets/fonts/roboto/bold-cyrillic-ext.woff2) format("woff2");
unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
}
@font-face {
font-family: 'Roboto';
font-family: "Roboto";
font-style: normal;
font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'), url(../assets/fonts/roboto/bold-cyrillic.woff2) format('woff2');
src:
local("Roboto Bold"),
local("Roboto-Bold"),
url(../assets/fonts/roboto/bold-cyrillic.woff2) format("woff2");
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: 'Roboto';
font-family: "Roboto";
font-style: normal;
font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'), url(../assets/fonts/roboto/bold-greek-ext.woff2) format('woff2');
src:
local("Roboto Bold"),
local("Roboto-Bold"),
url(../assets/fonts/roboto/bold-greek-ext.woff2) format("woff2");
unicode-range: U+1F00-1FFF;
}
@font-face {
font-family: 'Roboto';
font-family: "Roboto";
font-style: normal;
font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'), url(../assets/fonts/roboto/bold-greek.woff2) format('woff2');
src:
local("Roboto Bold"),
local("Roboto-Bold"),
url(../assets/fonts/roboto/bold-greek.woff2) format("woff2");
unicode-range: U+0370-03FF;
}
@font-face {
font-family: 'Roboto';
font-family: "Roboto";
font-style: normal;
font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'), url(../assets/fonts/roboto/bold-vietnamese.woff2) format('woff2');
src:
local("Roboto Bold"),
local("Roboto-Bold"),
url(../assets/fonts/roboto/bold-vietnamese.woff2) format("woff2");
unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
}
@font-face {
font-family: 'Roboto';
font-family: "Roboto";
font-style: normal;
font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'), url(../assets/fonts/roboto/bold-latin-ext.woff2) format('woff2');
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
src:
local("Roboto Bold"),
local("Roboto-Bold"),
url(../assets/fonts/roboto/bold-latin-ext.woff2) format("woff2");
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F,
U+A720-A7FF;
}
@font-face {
font-family: 'Roboto';
font-family: "Roboto";
font-style: normal;
font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'), url(../assets/fonts/roboto/bold-latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
src:
local("Roboto Bold"),
local("Roboto-Bold"),
url(../assets/fonts/roboto/bold-latin.woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC,
U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
}
@import '~material-icons/iconfont/filled.css';
.material-icons {
font-size: 1.5rem;
}
}

View File

@@ -120,6 +120,10 @@
right: 0;
}
.shell__divider {
height: 12px;
}
header .search-button,
header .menu-button {
display: inherit;

View File

@@ -1,6 +1,6 @@
@import "~normalize.css/normalize.css";
@import "~noty/lib/noty.css";
@import "~noty/lib/themes/mint.css";
@import "normalize.css/normalize.css";
@import "noty/lib/noty.css";
@import "noty/lib/themes/mint.css";
@import "./_variables.css";
@import "./_buttons.css";
@import "./_inputs.css";
@@ -14,6 +14,7 @@
@import "./upload-files.css";
@import "./dashboard.css";
@import "./login.css";
@import "./mobile.css";
.link {
color: var(--blue);
@@ -27,9 +28,9 @@ main .spinner {
}
main .spinner > div {
width: .8em;
height: .8em;
margin: 0 .1em;
width: 0.8em;
height: 0.8em;
margin: 0 0.1em;
font-size: 1em;
background-color: rgba(0, 0, 0, 0.3);
border-radius: 100%;
@@ -71,7 +72,7 @@ main .spinner .bounce2 {
transition: 0.2s ease all;
border: 0;
margin: 0;
color: #546E7A;
color: #546e7a;
border-radius: 50%;
background: transparent;
padding: 0;
@@ -88,12 +89,12 @@ main .spinner .bounce2 {
.action i {
padding: 0.4em;
transition: .1s ease-in-out all;
transition: 0.1s ease-in-out all;
border-radius: 50%;
}
.action:hover {
background-color: rgba(0, 0, 0, .1);
background-color: rgba(0, 0, 0, 0.1);
}
.action ul {
@@ -109,8 +110,8 @@ main .spinner .bounce2 {
.action ul li {
line-height: 1;
padding: .7em;
transition: .1s ease background-color;
padding: 0.7em;
transition: 0.1s ease background-color;
}
.action ul li:hover {
@@ -139,7 +140,7 @@ main .spinner .bounce2 {
background: var(--blue);
color: #fff;
border-radius: 50%;
font-size: .75em;
font-size: 0.75em;
width: 1.8em;
height: 1.8em;
text-align: center;
@@ -148,7 +149,6 @@ main .spinner .bounce2 {
border: 2px solid white;
}
/* PREVIEWER */
#previewer {
@@ -179,7 +179,7 @@ main .spinner .bounce2 {
}
#previewer header .action:hover {
background-color: rgba(255, 255, 255, 0.3)
background-color: rgba(255, 255, 255, 0.3);
}
#previewer header .action span {
@@ -220,14 +220,14 @@ main .spinner .bounce2 {
}
#previewer .preview .info .title i {
display: block;
margin-bottom: .1em;
margin-bottom: 0.1em;
font-size: 4em;
}
#previewer .preview .info .button {
display: inline-block;
}
#previewer .preview .info .button:hover {
background-color: rgba(255, 255, 255, 0.2)
background-color: rgba(255, 255, 255, 0.2);
}
#previewer .preview .info .button i {
display: block;
@@ -241,15 +241,15 @@ main .spinner .bounce2 {
}
#previewer h2.message {
color: rgba(255, 255, 255, 0.5)
color: rgba(255, 255, 255, 0.5);
}
#previewer>button {
#previewer > button {
margin: 0;
position: fixed;
top: calc(50% + 1.85em);
transform: translateY(-50%);
background-color: rgba(80, 80, 80, .5);
background-color: rgba(80, 80, 80, 0.5);
color: white;
border-radius: 50%;
cursor: pointer;
@@ -259,20 +259,20 @@ main .spinner .bounce2 {
transition: 0.2s ease all;
}
#previewer>button.hidden {
#previewer > button.hidden {
opacity: 0;
visibility: hidden;
}
#previewer>button>i {
#previewer > button > i {
padding: 0.4em;
}
#previewer>button:first-of-type {
#previewer > button:first-of-type {
left: 0.5em;
}
#previewer>button:last-of-type {
#previewer > button:last-of-type {
right: 0.5em;
}
@@ -321,7 +321,7 @@ body.rtl .breadcrumbs .chevron {
}
#editor-container .breadcrumbs span {
font-size: .75rem;
font-size: 0.75rem;
}
#editor-container .breadcrumbs i {
@@ -339,7 +339,7 @@ body.rtl .breadcrumbs .chevron {
.noty_buttons button {
background: rgba(0, 0, 0, 0.05);
border: 1px solid rgba(0,0,0,0.1);
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 0 0 0;
font-size: 1rem;
}
@@ -356,7 +356,7 @@ body.rtl .breadcrumbs .chevron {
.credits > span {
display: block;
margin: .3em 0;
margin: 0.3em 0;
}
.credits a,
@@ -365,7 +365,6 @@ body.rtl .breadcrumbs .chevron {
cursor: pointer;
}
/* * * * * * * * * * * * * * * *
* ANIMATIONS *
* * * * * * * * * * * * * * * */
@@ -393,20 +392,20 @@ body.rtl .breadcrumbs .chevron {
.rules > div {
display: flex;
align-items: center;
margin: .5rem 0;
margin: 0.5rem 0;
}
.rules input[type="checkbox"] {
margin-right: .2rem;
margin-right: 0.2rem;
}
.rules input[type="text"] {
border: 1px solid#ddd;
padding: .2rem;
padding: 0.2rem;
}
.rules label {
margin-right: .5rem;
margin-right: 0.5rem;
}
.rules button {
@@ -414,12 +413,10 @@ body.rtl .breadcrumbs .chevron {
}
.rules button.delete {
padding: .2rem .5rem;
margin-left: .5rem;
padding: 0.2rem 0.5rem;
margin-left: 0.5rem;
}
@import './mobile.css';
/* * * * * * * * * * * * * * * *
* RTL overrides *
* * * * * * * * * * * * * * * */

View File

@@ -36,7 +36,8 @@
"toggleSidebar": "Seitenleiste anzeigen",
"update": "Update",
"upload": "Upload",
"openFile": "Datei öffnen"
"openFile": "Datei öffnen",
"continue": "Fortfahren"
},
"download": {
"downloadFile": "Download Datei",
@@ -147,7 +148,7 @@
"rename": "Umbenennen",
"renameMessage": "Fügen Sie einen Namen ein für",
"replace": "Ersetzen",
"replaceMessage": "Eine der Datei mit dem gleichen Namen, wie die Sie hochladen wollen, existiert bereits. Soll die vorhandene Datei ersetzt werden ?\n",
"replaceMessage": "Eine der Datei mit dem gleichen Namen, wie die Sie hochladen wollen, existiert bereits. Soll die vorhandene Datei übersprungen oder ersetzt werden?\n",
"schedule": "Plan",
"scheduleMessage": "Wählen Sie ein Datum und eine Zeit für die Veröffentlichung dieses Beitrags.",
"show": "Anzeigen",
@@ -184,10 +185,14 @@
"commandRunnerHelp": "Hier könne Sie Befehle eintragen, welche bei den benannten Aktionen ausgeführt werden. Sie müssen pro Zeile jeweils einen Befehl eingeben. Die Umgebungsvariable {0} und {1} sind verfügbar, wobei {0} relative zu {1} ist. Für mehr Informationen über diese Funktion und die verfügbaren Umgebungsvariablen, lesen Sie bitte die {2}.",
"commandsUpdated": "Befehle aktualisiert!",
"createUserDir": "Automatisches Erstellen des Home-Verzeichnisses beim Anlegen neuer Benutzer",
"tusUploads": "Gestückelter Upload",
"tusUploadsHelp": "File Browser unterstützt das Hochladen von gestückelten Dateien und ermöglicht so einen effizienten, zuverlässigen, fortsetzbaren und gestückelten Datei-Upload auch in unzuverlässigen Netzwerken.",
"tusUploadsChunkSize": "Gibt die maximale Größe pro Anfrage an (direkte Uploads werden für kleinere Uploads verwendet). Bitte geben Sie eine Byte-Angabe oder eine Zeichenfolge wie 10 MB, 1 GB usw. an",
"tusUploadsRetryCount": "Anzahl der Wiederholungsversuche, wenn das Hochladen eines Stückes fehlschlägt.",
"customStylesheet": "Individuelles Stylesheet",
"defaultUserDescription": "Das sind die Standardeinstellung für Benutzer",
"disableExternalLinks": "Externe Links deaktivieren (außer Dokumentation)",
"disableUsedDiskPercentage": "Disable used disk percentage graph",
"disableUsedDiskPercentage": "Diagramm zur Festplattennutzung deaktivieren",
"documentation": "Dokumentation",
"examples": "Beispiele",
"executeOnShell": "In Shell ausführen",

View File

@@ -45,6 +45,9 @@
"downloadFolder": "Download Folder",
"downloadSelected": "Download Selected"
},
"upload": {
"abortUpload": "Are you sure you want to abort?"
},
"errors": {
"forbidden": "You don't have permissions to access this.",
"internal": "Something really went wrong.",

View File

@@ -7,7 +7,7 @@ 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";
import App from "@/App.vue";
cssVars();

View File

@@ -1,16 +1,16 @@
import Vue from "vue";
import Router from "vue-router";
import Login from "@/views/Login";
import Layout from "@/views/Layout";
import Files from "@/views/Files";
import Share from "@/views/Share";
import Users from "@/views/settings/Users";
import User from "@/views/settings/User";
import Settings from "@/views/Settings";
import GlobalSettings from "@/views/settings/Global";
import ProfileSettings from "@/views/settings/Profile";
import Shares from "@/views/settings/Shares";
import Errors from "@/views/Errors";
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";
@@ -33,7 +33,7 @@ const titles = {
};
const router = new Router({
base: baseURL,
base: import.meta.env.PROD ? baseURL : "",
mode: "history",
routes: [
{

View File

@@ -4,7 +4,7 @@ const getters = {
isListing: (state, getters) => getters.isFiles && state.req.isDir,
selectedCount: (state) => state.selected.length,
progress: (state) => {
if (state.upload.progress.length == 0) {
if (state.upload.progress.length === 0) {
return 0;
}
@@ -14,9 +14,7 @@ const getters = {
return Math.ceil((sum / totalSize) * 100);
},
filesInUploadCount: (state) => {
let total =
Object.keys(state.upload.uploads).length + state.upload.queue.length;
return total;
return Object.keys(state.upload.uploads).length + state.upload.queue.length;
},
filesInUpload: (state) => {
let files = [];
@@ -43,6 +41,16 @@ const getters = {
return files.sort((a, b) => a.progress - b.progress);
},
currentPrompt: (state) => {
return state.prompts.length > 0
? state.prompts[state.prompts.length - 1]
: null;
},
currentPromptName: (_, getters) => {
return getters.currentPrompt?.prompt;
},
uploadSpeed: (state) => state.upload.speedMbyte,
eta: (state) => state.upload.eta,
};
export default getters;

View File

@@ -20,10 +20,8 @@ const state = {
reload: false,
selected: [],
multiple: false,
show: null,
prompts: [],
showShell: false,
showConfirm: null,
showAction: null,
};
export default new Vuex.Store({

View File

@@ -11,6 +11,8 @@ const state = {
progress: [],
queue: [],
uploads: {},
speedMbyte: 0,
eta: 0,
};
const mutations = {

View File

@@ -3,30 +3,35 @@ import moment from "moment";
const mutations = {
closeHovers: (state) => {
state.show = null;
state.showConfirm = null;
state.showAction = null;
state.prompts.pop();
},
toggleShell: (state) => {
state.show = null;
state.showShell = !state.showShell;
},
showHover: (state, value) => {
if (typeof value !== "object") {
state.show = value;
state.prompts.push({
prompt: value,
confirm: null,
action: null,
props: null,
});
return;
}
state.show = value.prompt;
state.showConfirm = value.confirm;
if (value.action !== undefined) {
state.showAction = value.action;
}
state.prompts.push({
prompt: value.prompt, // Should not be null
confirm: value?.confirm,
action: value?.action,
props: value?.props,
});
},
showError: (state) => {
state.show = "error";
state.prompts.push("error");
},
showSuccess: (state) => {
state.show = "success";
state.prompts.push("success");
},
setLoading: (state, value) => {
state.loading = value;
@@ -74,8 +79,15 @@ const mutations = {
}
},
updateRequest: (state, value) => {
const selectedItems = state.selected.map((i) => state.req.items[i]);
state.oldReq = state.req;
state.req = value;
state.selected = [];
if (!state.req?.items) return;
state.selected = state.req.items
.filter((item) => selectedItems.some((rItem) => rItem.url === item.url))
.map((item) => item.index);
},
updateClipboard: (state, value) => {
state.clipboard.key = value.key;
@@ -86,6 +98,21 @@ const mutations = {
state.clipboard.key = "";
state.clipboard.items = [];
},
setUploadSpeed: (state, value) => {
state.upload.speedMbyte = value;
},
setETA(state, value) {
state.upload.eta = value;
},
resetUpload(state) {
state.upload.uploads = {};
state.upload.queue = [];
state.upload.progress = [];
state.upload.sizes = [];
state.upload.id = 0;
state.upload.speedMbyte = 0;
state.upload.eta = 0;
},
};
export default mutations;

View File

@@ -1,7 +1,7 @@
import store from "@/store";
import router from "@/router";
import { Base64 } from "js-base64";
import { baseURL } from "@/utils/constants";
import { fetchURL } from "@/api/utils";
export function parseToken(token) {
const parts = token.split(".");
@@ -25,20 +25,24 @@ export async function validateLogin() {
await renew(localStorage.getItem("jwt"));
}
} catch (_) {
console.warn('Invalid JWT token in storage') // eslint-disable-line
console.warn("Invalid JWT token in storage"); // eslint-disable-line
}
}
export async function login(username, password, recaptcha) {
const data = { username, password, recaptcha };
const res = await fetch(`${baseURL}/api/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
const res = await fetchURL(
`/api/login`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
},
body: JSON.stringify(data),
});
false
);
const body = await res.text();
@@ -50,7 +54,7 @@ export async function login(username, password, recaptcha) {
}
export async function renew(jwt) {
const res = await fetch(`${baseURL}/api/renew`, {
const res = await fetchURL(`/api/renew`, {
method: "POST",
headers: {
"X-Auth": jwt,
@@ -69,13 +73,17 @@ export async function renew(jwt) {
export async function signup(username, password) {
const data = { username, password };
const res = await fetch(`${baseURL}/api/signup`, {
method: "POST",
headers: {
"Content-Type": "application/json",
const res = await fetchURL(
`/api/signup`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
},
body: JSON.stringify(data),
});
false
);
if (res.status !== 200) {
throw new Error(res.status);

View File

@@ -10,7 +10,7 @@
</template>
<script>
import HeaderBar from "@/components/header/HeaderBar";
import HeaderBar from "@/components/header/HeaderBar.vue";
const errors = {
0: {

View File

@@ -23,11 +23,11 @@
import { files as api } from "@/api";
import { mapState, mapMutations } from "vuex";
import HeaderBar from "@/components/header/HeaderBar";
import Breadcrumbs from "@/components/Breadcrumbs";
import Errors from "@/views/Errors";
import Preview from "@/views/files/Preview";
import Listing from "@/views/files/Listing";
import HeaderBar from "@/components/header/HeaderBar.vue";
import Breadcrumbs from "@/components/Breadcrumbs.vue";
import Errors from "@/views/Errors.vue";
import Preview from "@/views/files/Preview.vue";
import Listing from "@/views/files/Listing.vue";
function clean(path) {
return path.endsWith("/") ? path.slice(0, -1) : path;
@@ -41,7 +41,7 @@ export default {
Errors,
Preview,
Listing,
Editor: () => import("@/views/files/Editor"),
Editor: () => import("@/views/files/Editor.vue"),
},
data: function () {
return {
@@ -50,7 +50,7 @@ export default {
};
},
computed: {
...mapState(["req", "reload", "loading", "show"]),
...mapState(["req", "reload", "loading"]),
currentView() {
if (this.req.type == undefined) {
return null;

View File

@@ -15,10 +15,10 @@
<script>
import { mapState, mapGetters } from "vuex";
import Sidebar from "@/components/Sidebar";
import Prompts from "@/components/prompts/Prompts";
import Shell from "@/components/Shell";
import UploadFiles from "../components/prompts/UploadFiles";
import Sidebar from "@/components/Sidebar.vue";
import Prompts from "@/components/prompts/Prompts.vue";
import Shell from "@/components/Shell.vue";
import UploadFiles from "../components/prompts/UploadFiles.vue";
import { enableExec } from "@/utils/constants";
export default {
@@ -30,7 +30,7 @@ export default {
UploadFiles,
},
computed: {
...mapGetters(["isLogged", "progress"]),
...mapGetters(["isLogged", "progress", "currentPrompt"]),
...mapState(["user"]),
isExecEnabled: () => enableExec,
},
@@ -38,7 +38,7 @@ export default {
$route: function () {
this.$store.commit("resetSelected");
this.$store.commit("multiple", false);
if (this.$store.state.show !== "success")
if (this.currentPrompt?.prompt !== "success")
this.$store.commit("closeHovers");
},
},

View File

@@ -52,7 +52,7 @@
<script>
import { mapState } from "vuex";
import HeaderBar from "@/components/header/HeaderBar";
import HeaderBar from "@/components/header/HeaderBar.vue";
export default {
name: "settings",

View File

@@ -182,15 +182,15 @@
<script>
import { mapState, mapMutations, mapGetters } from "vuex";
import { pub as api } from "@/api";
import filesize from "filesize";
import { filesize } from "filesize";
import moment from "moment";
import HeaderBar from "@/components/header/HeaderBar";
import Action from "@/components/header/Action";
import Breadcrumbs from "@/components/Breadcrumbs";
import Errors from "@/views/Errors";
import HeaderBar from "@/components/header/HeaderBar.vue";
import Action from "@/components/header/Action.vue";
import Breadcrumbs from "@/components/Breadcrumbs.vue";
import Errors from "@/views/Errors.vue";
import QrcodeVue from "qrcode.vue";
import Item from "@/components/files/ListingItem";
import Item from "@/components/files/ListingItem.vue";
import Clipboard from "clipboard";
export default {

View File

@@ -26,13 +26,13 @@ import { theme } from "@/utils/constants";
import buttons from "@/utils/buttons";
import url from "@/utils/url";
import { version as ace_version } from "ace-builds";
import ace from "ace-builds/src-min-noconflict/ace.js";
import modelist from "ace-builds/src-min-noconflict/ext-modelist.js";
import "ace-builds/webpack-resolver";
import HeaderBar from "@/components/header/HeaderBar";
import Action from "@/components/header/Action";
import Breadcrumbs from "@/components/Breadcrumbs";
import HeaderBar from "@/components/header/HeaderBar.vue";
import Action from "@/components/header/Action.vue";
import Breadcrumbs from "@/components/Breadcrumbs.vue";
export default {
name: "editor",
@@ -86,6 +86,11 @@ export default {
mounted: function () {
const fileContent = this.req.content || "";
ace.config.set(
"basePath",
`https://cdn.jsdelivr.net/npm/ace-builds@${ace_version}/src-min-noconflict/`
);
this.editor = ace.edit("editor", {
value: fileContent,
showPrintMargin: false,

View File

@@ -1,7 +1,8 @@
<template>
<div>
<header-bar showMenu showLogo>
<search /> <title />
<search />
<title />
<action
class="search-button"
icon="search"
@@ -274,10 +275,10 @@ import * as upload from "@/utils/upload";
import css from "@/utils/css";
import throttle from "lodash.throttle";
import HeaderBar from "@/components/header/HeaderBar";
import Action from "@/components/header/Action";
import Search from "@/components/Search";
import Item from "@/components/files/ListingItem";
import HeaderBar from "@/components/header/HeaderBar.vue";
import Action from "@/components/header/Action.vue";
import Search from "@/components/Search.vue";
import Item from "@/components/files/ListingItem.vue";
export default {
name: "listing",
@@ -297,16 +298,8 @@ export default {
};
},
computed: {
...mapState([
"req",
"selected",
"user",
"show",
"multiple",
"selected",
"loading",
]),
...mapGetters(["selectedCount"]),
...mapState(["req", "selected", "user", "multiple", "selected", "loading"]),
...mapGetters(["selectedCount", "currentPrompt"]),
nameSorted() {
return this.req.sorting.by === "name";
},
@@ -443,7 +436,7 @@ export default {
},
keyEvent(event) {
// No prompts are shown
if (this.show !== null) {
if (this.currentPrompt !== null) {
return;
}

View File

@@ -143,14 +143,14 @@
</template>
<script>
import { mapState } from "vuex";
import { mapGetters, mapState } from "vuex";
import { files as api } from "@/api";
import { resizePreview } from "@/utils/constants";
import url from "@/utils/url";
import throttle from "lodash.throttle";
import HeaderBar from "@/components/header/HeaderBar";
import Action from "@/components/header/Action";
import ExtendedImage from "@/components/files/ExtendedImage";
import HeaderBar from "@/components/header/HeaderBar.vue";
import Action from "@/components/header/Action.vue";
import ExtendedImage from "@/components/files/ExtendedImage.vue";
const mediaTypes = ["image", "video", "audio", "blob"];
@@ -177,7 +177,8 @@ export default {
};
},
computed: {
...mapState(["req", "user", "oldReq", "jwt", "loading", "show"]),
...mapState(["req", "user", "oldReq", "jwt", "loading"]),
...mapGetters(["currentPrompt"]),
hasPrevious() {
return this.previousLink !== "";
},
@@ -195,7 +196,7 @@ export default {
return api.getDownloadURL(this.req, true);
},
showMore() {
return this.$store.state.show === "more";
return this.currentPrompt?.prompt === "more";
},
isResizeEnabled() {
return resizePreview;
@@ -247,7 +248,7 @@ export default {
this.$router.replace({ path: this.nextLink });
},
key(event) {
if (this.show !== null) {
if (this.currentPrompt !== null) {
return;
}

View File

@@ -223,10 +223,10 @@
import { mapState, mapMutations } from "vuex";
import { settings as api } from "@/api";
import { enableExec } from "@/utils/constants";
import UserForm from "@/components/settings/UserForm";
import Rules from "@/components/settings/Rules";
import Themes from "@/components/settings/Themes";
import Errors from "@/views/Errors";
import UserForm from "@/components/settings/UserForm.vue";
import Rules from "@/components/settings/Rules.vue";
import Themes from "@/components/settings/Themes.vue";
import Errors from "@/views/Errors.vue";
export default {
name: "settings",

View File

@@ -74,7 +74,7 @@
<script>
import { mapState, mapMutations } from "vuex";
import { users as api } from "@/api";
import Languages from "@/components/settings/Languages";
import Languages from "@/components/settings/Languages.vue";
import i18n, { rtlLanguages } from "@/i18n";
export default {

View File

@@ -65,7 +65,7 @@ import { share as api, users } from "@/api";
import { mapState, mapMutations } from "vuex";
import moment from "moment";
import Clipboard from "clipboard";
import Errors from "@/views/Errors";
import Errors from "@/views/Errors.vue";
export default {
name: "shares",

View File

@@ -37,7 +37,7 @@
</form>
</div>
<div v-if="$store.state.show === 'deleteUser'" class="card floating">
<div v-if="this.currentPromptName === 'deleteUser'" class="card floating">
<div class="card-content">
<p>Are you sure you want to delete this user?</p>
</div>
@@ -61,10 +61,10 @@
</template>
<script>
import { mapState, mapMutations } from "vuex";
import { mapState, mapMutations, mapGetters } from "vuex";
import { users as api, settings } from "@/api";
import UserForm from "@/components/settings/UserForm";
import Errors from "@/views/Errors";
import UserForm from "@/components/settings/UserForm.vue";
import Errors from "@/views/Errors.vue";
import deepClone from "lodash.clonedeep";
export default {
@@ -89,6 +89,7 @@ export default {
return this.$route.path === "/settings/users/new";
},
...mapState(["loading"]),
...mapGetters(["currentPrompt", "currentPromptName"]),
},
watch: {
$route: "fetchData",

View File

@@ -44,7 +44,7 @@
<script>
import { mapState, mapMutations } from "vuex";
import { users as api } from "@/api";
import Errors from "@/views/Errors";
import Errors from "@/views/Errors.vue";
export default {
name: "users",

70
frontend/vite.config.js Normal file
View File

@@ -0,0 +1,70 @@
import { fileURLToPath, URL } from "node:url";
import path from "node:path";
import { defineConfig } from "vite";
import legacy from "@vitejs/plugin-legacy";
import vue2 from "@vitejs/plugin-vue2";
import { compression } from "vite-plugin-compression2";
import pluginRewriteAll from "vite-plugin-rewrite-all";
const plugins = [
vue2(),
legacy({
targets: ["ie >= 11"],
additionalLegacyPolyfills: ["regenerator-runtime/runtime"],
}),
compression({ include: /\.js$/i, deleteOriginalAssets: true }),
pluginRewriteAll(), // fixes 404 error with paths containing dot in dev server
];
const resolve = {
alias: {
vue: "vue/dist/vue.esm.js",
"@/": `${path.resolve(__dirname, "src")}/`,
},
};
// https://vitejs.dev/config/
export default defineConfig(({ command }) => {
if (command === "serve") {
return {
plugins,
resolve,
server: {
proxy: {
"/api/command": {
target: "ws://127.0.0.1:8080",
ws: true,
},
"/api": "http://127.0.0.1:8080",
},
},
};
} else {
// command === 'build'
return {
plugins,
resolve,
base: "",
build: {
rollupOptions: {
input: {
index: fileURLToPath(
new URL(`./public/index.html`, import.meta.url)
),
},
},
},
experimental: {
renderBuiltUrl(filename, { hostType }) {
if (hostType === "js") {
return { runtime: `window.__prependStaticUrl("${filename}")` };
} else if (hostType === "html") {
return `[{[ .StaticURL ]}]/${filename}`;
} else {
return { relative: true };
}
},
},
};
}
});

View File

@@ -1,15 +0,0 @@
const CompressionPlugin = require("compression-webpack-plugin");
module.exports = {
runtimeCompiler: true,
publicPath: "[{[ .StaticURL ]}]",
parallel: 2,
configureWebpack: {
plugins: [
new CompressionPlugin({
include: /\.js$/,
deleteOriginalAssets: true,
}),
],
},
};

View File

@@ -49,7 +49,9 @@ func (d *data) Check(path string) bool {
func handle(fn handleFunc, prefix string, store *storage.Storage, server *settings.Server) http.Handler {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
for k, v := range globalHeaders {
w.Header().Set(k, v)
}
settings, err := store.Settings.Get()
if err != nil {

9
http/headers.go Normal file
View File

@@ -0,0 +1,9 @@
//go:build !dev
// +build !dev
package http
// global headers to append to every response
var globalHeaders = map[string]string{
"Cache-Control": "no-cache, no-store, must-revalidate",
}

15
http/headers_dev.go Normal file
View File

@@ -0,0 +1,15 @@
//go:build dev
// +build dev
package http
// global headers to append to every response
// cross-origin headers are necessary to be able to
// access them from a different URL during development
var globalHeaders = map[string]string{
"Cache-Control": "no-cache, no-store, must-revalidate",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "*",
"Access-Control-Allow-Methods": "*",
"Access-Control-Allow-Credentials": "true",
}

View File

@@ -66,7 +66,7 @@ func NewHandler(
api.PathPrefix("/resources").Handler(monkey(resourcePatchHandler(fileCache), "/api/resources")).Methods("PATCH")
api.PathPrefix("/tus").Handler(monkey(tusPostHandler(), "/api/tus")).Methods("POST")
api.PathPrefix("/tus").Handler(monkey(tusHeadHandler(), "/api/tus")).Methods("HEAD")
api.PathPrefix("/tus").Handler(monkey(tusHeadHandler(), "/api/tus")).Methods("HEAD", "GET")
api.PathPrefix("/tus").Handler(monkey(tusPatchHandler(), "/api/tus")).Methods("PATCH")
api.PathPrefix("/usage").Handler(monkey(diskUsage, "/api/usage")).Methods("GET")

View File

@@ -105,7 +105,7 @@ func getStaticHandlers(store *storage.Storage, server *settings.Server, assetsFs
}
w.Header().Set("x-xss-protection", "1; mode=block")
return handleWithStaticData(w, r, d, assetsFs, "index.html", "text/html; charset=utf-8")
return handleWithStaticData(w, r, d, assetsFs, "public/index.html", "text/html; charset=utf-8")
}, "", store, server)
static = handle(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {