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

3
.gitignore vendored
View File

@@ -29,3 +29,6 @@ yarn-error.log*
*.sw* *.sw*
bin/ bin/
build/ 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. 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) ### [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. // FileInfo describes a file.
type FileInfo struct { type FileInfo struct {
*Listing *Listing
Fs afero.Fs `json:"-"` Fs afero.Fs `json:"-"`
Path string `json:"path"` Path string `json:"path"`
Name string `json:"name"` Name string `json:"name"`
Size int64 `json:"size"` Size int64 `json:"size"`
Extension string `json:"extension"` Extension string `json:"extension"`
ModTime time.Time `json:"modified"` ModTime time.Time `json:"modified"`
Mode os.FileMode `json:"mode"` Mode os.FileMode `json:"mode"`
IsDir bool `json:"isDir"` IsDir bool `json:"isDir"`
IsSymlink bool `json:"isSymlink"` IsSymlink bool `json:"isSymlink"`
Type string `json:"type"` Type string `json:"type"`
Subtitles []string `json:"subtitles,omitempty"` Subtitles []string `json:"subtitles,omitempty"`
Content string `json:"content,omitempty"` Content string `json:"content,omitempty"`
Checksums map[string]string `json:"checksums,omitempty"` Checksums map[string]string `json:"checksums,omitempty"`
Token string `json:"token,omitempty"` Token string `json:"token,omitempty"`
currentDir []os.FileInfo `json:"-"`
} }
// FileOptions are the options when getting a file info. // FileOptions are the options when getting a file info.
@@ -294,13 +295,21 @@ func (i *FileInfo) detectSubtitles() {
// detect multiple languages. Base*.vtt // detect multiple languages. Base*.vtt
// TODO: give subtitles descriptive names (lang) and track attributes // TODO: give subtitles descriptive names (lang) and track attributes
parentDir := strings.TrimRight(i.Path, i.Name) parentDir := strings.TrimRight(i.Path, i.Name)
dir, err := afero.ReadDir(i.Fs, parentDir) var dir []os.FileInfo
if err == nil { if len(i.currentDir) > 0 {
base := strings.TrimSuffix(i.Name, ext) dir = i.currentDir
for _, f := range dir { } else {
if !f.IsDir() && strings.HasPrefix(f.Name(), base) && strings.HasSuffix(f.Name(), ".vtt") { var err error
i.Subtitles = append(i.Subtitles, path.Join(parentDir, f.Name())) 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{ file := &FileInfo{
Fs: i.Fs, Fs: i.Fs,
Name: name, Name: name,
Size: f.Size(), Size: f.Size(),
ModTime: f.ModTime(), ModTime: f.ModTime(),
Mode: f.Mode(), Mode: f.Mode(),
IsDir: f.IsDir(), IsDir: f.IsDir(),
IsSymlink: isSymlink, IsSymlink: isSymlink,
Extension: filepath.Ext(name), Extension: filepath.Ext(name),
Path: fPath, Path: fPath,
currentDir: dir,
} }
if file.IsDir { 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", "name": "filebrowser-frontend",
"version": "2.0.0", "version": "2.0.0",
"private": true, "private": true,
"type": "module",
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "dev": "vite dev",
"build": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitignore' -exec rm -r {} + && vue-cli-service build --no-clean", "serve": "vite serve",
"lint": "npx vue-cli-service lint --no-fix --max-warnings=0", "build": "vite build",
"fix": "npx vue-cli-service lint", "watch": "vite build --watch",
"watch": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitignore' -exec rm -r {} + && vue-cli-service build --watch --no-clean" "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": { "dependencies": {
"ace-builds": "^1.4.7", "ace-builds": "^1.23.4",
"clipboard": "^2.0.4", "clipboard": "^2.0.11",
"core-js": "^3.9.1", "core-js": "^3.32.0",
"css-vars-ponyfill": "^2.4.3", "css-vars-ponyfill": "^2.4.8",
"js-base64": "^2.5.1", "filesize": "^10.0.8",
"js-base64": "^3.7.5",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"material-icons": "^1.10.5", "material-icons": "^1.13.9",
"moment": "^2.29.4", "moment": "^2.29.4",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"noty": "^3.2.0-beta", "noty": "^3.2.0-beta",
"pretty-bytes": "^6.0.0", "pretty-bytes": "^6.1.1",
"qrcode.vue": "^1.7.0", "qrcode.vue": "^1.7.0",
"tus-js-client": "^3.1.0", "tus-js-client": "^3.1.1",
"utif": "^3.1.0", "utif": "^3.1.0",
"vue": "^2.6.10", "vue": "^2.7.14",
"vue-async-computed": "^3.9.0", "vue-async-computed": "^3.9.0",
"vue-i18n": "^8.15.3", "vue-i18n": "^8.28.2",
"vue-lazyload": "^1.3.3", "vue-lazyload": "^1.3.5",
"vue-router": "^3.1.3", "vue-router": "^3.6.5",
"vue-simple-progress": "^1.1.1", "vue-simple-progress": "^1.1.1",
"vuex": "^3.1.2", "vuex": "^3.6.2",
"vuex-router-sync": "^5.0.0", "vuex-router-sync": "^5.0.0",
"whatwg-fetch": "^3.6.2" "whatwg-fetch": "^3.6.17"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "^4.1.2", "@vitejs/plugin-legacy": "^4.1.1",
"@vue/cli-plugin-eslint": "~4.5.0", "@vitejs/plugin-vue2": "^2.2.0",
"@vue/cli-service": "^4.1.2", "@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-prettier": "^6.0.0", "autoprefixer": "^10.4.14",
"babel-eslint": "^10.1.0", "eslint": "^8.46.0",
"compression-webpack-plugin": "^6.0.3", "eslint-plugin-prettier": "^5.0.0",
"eslint": "^6.7.2", "eslint-plugin-vue": "^9.16.1",
"eslint-plugin-prettier": "^3.3.1", "jsdom": "^22.1.0",
"eslint-plugin-vue": "^6.2.2", "postcss": "^8.4.27",
"prettier": "^2.2.1", "prettier": "^3.0.1",
"vue-template-compiler": "^2.6.10" "terser": "^5.19.2",
}, "vite": "^4.4.9",
"eslintConfig": { "vite-plugin-compression2": "^0.10.3",
"root": true, "vite-plugin-rewrite-all": "^1.0.1"
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended",
"@vue/prettier"
],
"rules": {},
"parserOptions": {
"parser": "babel-eslint"
}
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 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"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"> <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> <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
<link rel="icon" type="image/png" sizes="16x16" href="[{[ .StaticURL ]}]/img/icons/favicon-16x16.png"> 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 --> <!-- Add to home screen for Android and modern mobile browsers -->
<link rel="manifest" id="manifestPlaceholder" crossorigin="use-credentials"> <link
<meta name="theme-color" content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]"> 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 --> <!-- Add to home screen for Safari on iOS/iPadOS -->
<meta name="apple-mobile-web-app-capable" content="yes"> <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-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="assets"> <meta name="apple-mobile-web-app-title" content="assets" />
<link rel="apple-touch-icon" href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon.png"> <link
rel="apple-touch-icon"
href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon.png"
/>
<!-- Add to home screen for Windows --> <!-- Add to home screen for Windows -->
<meta name="msapplication-TileImage" content="[{[ .StaticURL ]}]/img/icons/mstile-144x144.png"> <meta
<meta name="msapplication-TileColor" content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]"> 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 --> <!-- Inject Some Variables and generate the manifest json -->
<script> <script>
window.FileBrowser = JSON.parse('[{[ .Json ]}]'); // 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; const stringManifest = JSON.stringify(dynamicManifest);
var dynamicManifest = { const blob = new Blob([stringManifest], { type: "application/json" });
"name": window.FileBrowser.Name || 'File Browser', const manifestURL = URL.createObjectURL(blob);
"short_name": window.FileBrowser.Name || 'File Browser', document
"icons": [ .querySelector("#manifestPlaceholder")
{ .setAttribute("href", manifestURL);
"src": fullStaticURL + "/img/icons/android-chrome-192x192.png", </script>
"sizes": "192x192",
"type": "image/png" <style>
}, #loading {
{ position: fixed;
"src": fullStaticURL + "/img/icons/android-chrome-512x512.png", top: 0;
"sizes": "512x512", left: 0;
"type": "image/png" 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% {
"start_url": window.location.origin + window.FileBrowser.BaseURL, -webkit-transform: scale(1);
"display": "standalone", }
"background_color": "#ffffff", }
"theme_color": window.FileBrowser.Color || "#455a64"
}
const stringManifest = JSON.stringify(dynamicManifest); @keyframes sk-bouncedelay {
const blob = new Blob([stringManifest], {type: 'application/json'}); 0%,
const manifestURL = URL.createObjectURL(blob); 80%,
document.querySelector('#manifestPlaceholder').setAttribute('href', manifestURL); 100% {
</script> -webkit-transform: scale(0);
transform: scale(0);
}
40% {
-webkit-transform: scale(1);
transform: scale(1);
}
}
</style>
</head>
<body>
<div id="app"></div>
<style> <div id="loading">
#loading { <div class="spinner">
position: fixed; <div class="bounce1"></div>
top: 0; <div class="bounce2"></div>
left: 0; <div class="bounce3"></div>
width: 100%; </div>
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> </div>
</div>
[{[ if .Theme -]}] <script type="module" src="/src/main.js"></script>
<link rel="stylesheet" href="[{[ .StaticURL ]}]/themes/[{[ .Theme ]}].css" />
[{[ end ]}] [{[ if .Theme -]}]
[{[ if .CSS -]}] <link
rel="stylesheet"
href="[{[ .StaticURL ]}]/themes/[{[ .Theme ]}].css"
/>
[{[ end ]}] [{[ if .CSS -]}]
<link rel="stylesheet" href="[{[ .StaticURL ]}]/custom.css" /> <link rel="stylesheet" href="[{[ .StaticURL ]}]/custom.css" />
[{[ end ]}] [{[ end ]}]
</body> </body>
</html> </html>

View File

@@ -174,6 +174,12 @@ table th {
background: var(--surfacePrimary); background: var(--surfacePrimary);
color: var(--textPrimary); color: var(--textPrimary);
} }
.shell__divider {
background: rgba(255, 255, 255, 0.1);
}
.shell__divider:hover {
background: rgba(255, 255, 255, 0.4);
}
.shell__result { .shell__result {
border-top: 1px solid var(--divider); border-top: 1px solid var(--divider);
} }

View File

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

View File

@@ -6,6 +6,12 @@ import { fetchURL } from "./utils";
const RETRY_BASE_DELAY = 1000; const RETRY_BASE_DELAY = 1000;
const RETRY_MAX_DELAY = 20000; 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( export async function upload(
filePath, filePath,
@@ -34,19 +40,47 @@ export async function upload(
"X-Auth": store.state.jwt, "X-Auth": store.state.jwt,
}, },
onError: function (error) { 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("Upload failed: " + error);
}, },
onProgress: function (bytesUploaded) { onProgress: function (bytesUploaded) {
// Emulate ProgressEvent.loaded which is used by calling functions let fileData = CURRENT_UPLOAD_LIST[filePath];
// loaded is specified in bytes (https://developer.mozilla.org/en-US/docs/Web/API/ProgressEvent/loaded) 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") { if (typeof onupload === "function") {
onupload({ loaded: bytesUploaded }); onupload({ loaded: bytesUploaded });
} }
}, },
onSuccess: function () { onSuccess: function () {
if (CURRENT_UPLOAD_LIST[filePath].interval) {
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
}
delete CURRENT_UPLOAD_LIST[filePath];
resolve(); resolve();
}, },
}); });
CURRENT_UPLOAD_LIST[filePath] = {
upload: upload,
recentSpeeds: [],
initialBytesUploaded: 0,
currentBytesUploaded: 0,
currentAverageSpeed: 0,
lastProgressTimestamp: null,
sumOfRecentSpeeds: 0,
hasStarted: false,
interval: null,
};
upload.start(); upload.start();
}); });
} }
@@ -88,3 +122,74 @@ export async function useTus(content) {
function isTusSupported() { function isTusSupported() {
return tus.isSupported === true; 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: { watch: {
show(val, old) { currentPrompt(val, old) {
this.active = val === "search"; this.active = val?.prompt === "search";
if (old === "search" && !this.active) { if (old?.prompt === "search" && !this.active) {
if (this.reload) { if (this.reload) {
this.setReload(true); this.setReload(true);
} }
@@ -116,8 +116,8 @@ export default {
}, },
}, },
computed: { computed: {
...mapState(["user", "show"]), ...mapState(["user"]),
...mapGetters(["isListing"]), ...mapGetters(["isListing", "currentPrompt"]),
boxes() { boxes() {
return boxes; return boxes;
}, },

View File

@@ -1,37 +1,54 @@
<template> <template>
<div <div
@click="focus"
class="shell" class="shell"
ref="scrollable"
:class="{ ['shell--hidden']: !showShell }" :class="{ ['shell--hidden']: !showShell }"
:style="{ height: `${this.shellHeight}em` }"
> >
<div v-for="(c, index) in content" :key="index" class="shell__result"> <div
<div class="shell__prompt"> @pointerdown="startDrag()"
<i class="material-icons">chevron_right</i> @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> </div>
<pre class="shell__text">{{ c.text }}</pre>
</div>
<div class="shell__result" :class="{ 'shell__result--hidden': !canInput }"> <div
<div class="shell__prompt"> class="shell__result"
<i class="material-icons">chevron_right</i> :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> </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>
<div
@pointerup="stopDrag()"
class="shell__overlay"
v-show="this.shellDrag"
></div>
</div> </div>
</template> </template>
<script> <script>
import { mapMutations, mapState, mapGetters } from "vuex"; import { mapMutations, mapState, mapGetters } from "vuex";
import { commands } from "@/api"; import { commands } from "@/api";
import { throttle } from "lodash";
import { theme } from "@/utils/constants";
export default { export default {
name: "shell", name: "shell",
@@ -51,9 +68,55 @@ export default {
history: [], history: [],
historyPos: 0, historyPos: 0,
canInput: true, 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: { methods: {
...mapMutations(["toggleShell"]), ...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 () { scroll: function () {
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight; this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight;
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -133,6 +133,17 @@ export default {
this.selected = event.currentTarget.dataset.url; this.selected = event.currentTarget.dataset.url;
this.$emit("update:selected", this.selected); 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> </script>

View File

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

View File

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

View File

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

View File

@@ -1,26 +1,33 @@
<template> <template>
<div> <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 v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
</div> </div>
</template> </template>
<script> <script>
import Help from "./Help"; import Help from "./Help.vue";
import Info from "./Info"; import Info from "./Info.vue";
import Delete from "./Delete"; import Delete from "./Delete.vue";
import Rename from "./Rename"; import Rename from "./Rename.vue";
import Download from "./Download"; import Download from "./Download.vue";
import Move from "./Move"; import Move from "./Move.vue";
import Copy from "./Copy"; import Copy from "./Copy.vue";
import NewFile from "./NewFile"; import NewFile from "./NewFile.vue";
import NewDir from "./NewDir"; import NewDir from "./NewDir.vue";
import Replace from "./Replace"; import Replace from "./Replace.vue";
import ReplaceRename from "./ReplaceRename"; import ReplaceRename from "./ReplaceRename.vue";
import Share from "./Share"; import Share from "./Share.vue";
import Upload from "./Upload"; import Upload from "./Upload.vue";
import ShareDelete from "./ShareDelete"; import ShareDelete from "./ShareDelete.vue";
import { mapState } from "vuex"; import Sidebar from "../Sidebar.vue";
import { mapGetters, mapState } from "vuex";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
export default { export default {
@@ -40,6 +47,7 @@ export default {
ReplaceRename, ReplaceRename,
Upload, Upload,
ShareDelete, ShareDelete,
Sidebar
}, },
data: function () { data: function () {
return { return {
@@ -52,7 +60,7 @@ export default {
}, },
created() { created() {
window.addEventListener("keydown", (event) => { window.addEventListener("keydown", (event) => {
if (this.show == null) return; if (this.currentPrompt == null) return;
let prompt = this.$refs.currentComponent; let prompt = this.$refs.currentComponent;
@@ -64,7 +72,7 @@ export default {
// Enter // Enter
if (event.keyCode == 13) { if (event.keyCode == 13) {
switch (this.show) { switch (this.currentPrompt.prompt) {
case "delete": case "delete":
prompt.submit(); prompt.submit();
break; break;
@@ -82,31 +90,13 @@ export default {
}); });
}, },
computed: { computed: {
...mapState(["show", "plugins"]), ...mapState(["plugins"]),
currentComponent: function () { ...mapGetters(["currentPrompt", "currentPromptName"]),
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;
},
showOverlay: function () { showOverlay: function () {
return ( 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>
<button <button
class="button button--flat button--blue" class="button button--flat button--blue"
@click="showAction" @click="currentPrompt.action"
:aria-label="$t('buttons.continue')" :aria-label="$t('buttons.continue')"
:title="$t('buttons.continue')" :title="$t('buttons.continue')"
> >
@@ -27,7 +27,7 @@
</button> </button>
<button <button
class="button button--flat button--red" class="button button--flat button--red"
@click="showConfirm" @click="currentPrompt.confirm"
:aria-label="$t('buttons.replace')" :aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')" :title="$t('buttons.replace')"
> >
@@ -38,10 +38,10 @@
</template> </template>
<script> <script>
import { mapState } from "vuex"; import { mapGetters } from "vuex";
export default { export default {
name: "replace", name: "replace",
computed: mapState(["showConfirm", "showAction"]), computed: mapGetters(["currentPrompt"]),
}; };
</script> </script>

View File

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

View File

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

View File

@@ -7,7 +7,18 @@
<div class="card floating"> <div class="card floating">
<div class="card-title"> <div class="card-title">
<h2>{{ $t("prompts.uploadFiles", { files: filesInUploadCount }) }}</h2> <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 <button
class="action" class="action"
@click="toggle" @click="toggle"
@@ -42,7 +53,9 @@
</template> </template>
<script> <script>
import { mapGetters } from "vuex"; import { mapGetters, mapMutations } from "vuex";
import { abortAllUploads } from "@/api/tus";
import buttons from "@/utils/buttons";
export default { export default {
name: "uploadFiles", name: "uploadFiles",
@@ -52,12 +65,42 @@ export default {
}; };
}, },
computed: { 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: { methods: {
toggle: function () { toggle: function () {
this.open = !this.open; 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> </script>

View File

@@ -67,10 +67,10 @@
</template> </template>
<script> <script>
import Languages from "./Languages"; import Languages from "./Languages.vue";
import Rules from "./Rules"; import Rules from "./Rules.vue";
import Permissions from "./Permissions"; import Permissions from "./Permissions.vue";
import Commands from "./Commands"; import Commands from "./Commands.vue";
import { enableExec } from "@/utils/constants"; import { enableExec } from "@/utils/constants";
export default { export default {

View File

@@ -2,21 +2,49 @@
position: fixed; position: fixed;
bottom: 0; bottom: 0;
left: 0; left: 0;
height: 25em;
max-height: calc(100% - 4em); max-height: calc(100% - 4em);
background: white; background: white;
color: #212121; color: #212121;
z-index: 9999; z-index: 9997;
width: 100%; width: 100%;
font-family: monospace;
overflow: auto;
font-size: 1rem;
cursor: text;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
transition: .2s ease transform; 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; direction: ltr;
} }

View File

@@ -1,173 +1,242 @@
@import "material-icons/iconfont/filled.css";
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 400; 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; unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 400; 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; unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 400; 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; unicode-range: U+1F00-1FFF;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 400; 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; unicode-range: U+0370-03FF;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 400; 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; unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-latin-ext.woff2) format('woff2'); src:
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; 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-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-latin.woff2) format('woff2'); src:
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; 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-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 500; 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; unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 500; 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; unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 500; 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; unicode-range: U+1F00-1FFF;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 500; 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; unicode-range: U+0370-03FF;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 500; 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; unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-latin-ext.woff2) format('woff2'); src:
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; 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-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-latin.woff2) format('woff2'); src:
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; 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-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 700; 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; unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 700; 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; unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 700; 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; unicode-range: U+1F00-1FFF;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 700; 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; unicode-range: U+0370-03FF;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 700; 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; unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'), url(../assets/fonts/roboto/bold-latin-ext.woff2) format('woff2'); src:
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; 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-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'), url(../assets/fonts/roboto/bold-latin.woff2) format('woff2'); src:
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; 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 { .material-icons {
font-size: 1.5rem; font-size: 1.5rem;
} }

View File

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

View File

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

View File

@@ -36,7 +36,8 @@
"toggleSidebar": "Seitenleiste anzeigen", "toggleSidebar": "Seitenleiste anzeigen",
"update": "Update", "update": "Update",
"upload": "Upload", "upload": "Upload",
"openFile": "Datei öffnen" "openFile": "Datei öffnen",
"continue": "Fortfahren"
}, },
"download": { "download": {
"downloadFile": "Download Datei", "downloadFile": "Download Datei",
@@ -147,7 +148,7 @@
"rename": "Umbenennen", "rename": "Umbenennen",
"renameMessage": "Fügen Sie einen Namen ein für", "renameMessage": "Fügen Sie einen Namen ein für",
"replace": "Ersetzen", "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", "schedule": "Plan",
"scheduleMessage": "Wählen Sie ein Datum und eine Zeit für die Veröffentlichung dieses Beitrags.", "scheduleMessage": "Wählen Sie ein Datum und eine Zeit für die Veröffentlichung dieses Beitrags.",
"show": "Anzeigen", "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}.", "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!", "commandsUpdated": "Befehle aktualisiert!",
"createUserDir": "Automatisches Erstellen des Home-Verzeichnisses beim Anlegen neuer Benutzer", "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", "customStylesheet": "Individuelles Stylesheet",
"defaultUserDescription": "Das sind die Standardeinstellung für Benutzer", "defaultUserDescription": "Das sind die Standardeinstellung für Benutzer",
"disableExternalLinks": "Externe Links deaktivieren (außer Dokumentation)", "disableExternalLinks": "Externe Links deaktivieren (außer Dokumentation)",
"disableUsedDiskPercentage": "Disable used disk percentage graph", "disableUsedDiskPercentage": "Diagramm zur Festplattennutzung deaktivieren",
"documentation": "Dokumentation", "documentation": "Dokumentation",
"examples": "Beispiele", "examples": "Beispiele",
"executeOnShell": "In Shell ausführen", "executeOnShell": "In Shell ausführen",

View File

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

View File

@@ -7,7 +7,7 @@ import i18n from "@/i18n";
import Vue from "@/utils/vue"; import Vue from "@/utils/vue";
import { recaptcha, loginPage } from "@/utils/constants"; import { recaptcha, loginPage } from "@/utils/constants";
import { login, validateLogin } from "@/utils/auth"; import { login, validateLogin } from "@/utils/auth";
import App from "@/App"; import App from "@/App.vue";
cssVars(); cssVars();

View File

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

View File

@@ -4,7 +4,7 @@ const getters = {
isListing: (state, getters) => getters.isFiles && state.req.isDir, isListing: (state, getters) => getters.isFiles && state.req.isDir,
selectedCount: (state) => state.selected.length, selectedCount: (state) => state.selected.length,
progress: (state) => { progress: (state) => {
if (state.upload.progress.length == 0) { if (state.upload.progress.length === 0) {
return 0; return 0;
} }
@@ -14,9 +14,7 @@ const getters = {
return Math.ceil((sum / totalSize) * 100); return Math.ceil((sum / totalSize) * 100);
}, },
filesInUploadCount: (state) => { filesInUploadCount: (state) => {
let total = return Object.keys(state.upload.uploads).length + state.upload.queue.length;
Object.keys(state.upload.uploads).length + state.upload.queue.length;
return total;
}, },
filesInUpload: (state) => { filesInUpload: (state) => {
let files = []; let files = [];
@@ -43,6 +41,16 @@ const getters = {
return files.sort((a, b) => a.progress - b.progress); 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; export default getters;

View File

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

View File

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

View File

@@ -3,30 +3,35 @@ import moment from "moment";
const mutations = { const mutations = {
closeHovers: (state) => { closeHovers: (state) => {
state.show = null; state.prompts.pop();
state.showConfirm = null;
state.showAction = null;
}, },
toggleShell: (state) => { toggleShell: (state) => {
state.show = null;
state.showShell = !state.showShell; state.showShell = !state.showShell;
}, },
showHover: (state, value) => { showHover: (state, value) => {
if (typeof value !== "object") { if (typeof value !== "object") {
state.show = value; state.prompts.push({
prompt: value,
confirm: null,
action: null,
props: null,
});
return; return;
} }
state.show = value.prompt; state.prompts.push({
state.showConfirm = value.confirm; prompt: value.prompt, // Should not be null
if (value.action !== undefined) { confirm: value?.confirm,
state.showAction = value.action; action: value?.action,
} props: value?.props,
});
}, },
showError: (state) => { showError: (state) => {
state.show = "error"; state.prompts.push("error");
}, },
showSuccess: (state) => { showSuccess: (state) => {
state.show = "success"; state.prompts.push("success");
}, },
setLoading: (state, value) => { setLoading: (state, value) => {
state.loading = value; state.loading = value;
@@ -74,8 +79,15 @@ const mutations = {
} }
}, },
updateRequest: (state, value) => { updateRequest: (state, value) => {
const selectedItems = state.selected.map((i) => state.req.items[i]);
state.oldReq = state.req; state.oldReq = state.req;
state.req = value; 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) => { updateClipboard: (state, value) => {
state.clipboard.key = value.key; state.clipboard.key = value.key;
@@ -86,6 +98,21 @@ const mutations = {
state.clipboard.key = ""; state.clipboard.key = "";
state.clipboard.items = []; 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; export default mutations;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,13 +26,13 @@ import { theme } from "@/utils/constants";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
import url from "@/utils/url"; import url from "@/utils/url";
import { version as ace_version } from "ace-builds";
import ace from "ace-builds/src-min-noconflict/ace.js"; import ace from "ace-builds/src-min-noconflict/ace.js";
import modelist from "ace-builds/src-min-noconflict/ext-modelist.js"; import modelist from "ace-builds/src-min-noconflict/ext-modelist.js";
import "ace-builds/webpack-resolver";
import HeaderBar from "@/components/header/HeaderBar"; import HeaderBar from "@/components/header/HeaderBar.vue";
import Action from "@/components/header/Action"; import Action from "@/components/header/Action.vue";
import Breadcrumbs from "@/components/Breadcrumbs"; import Breadcrumbs from "@/components/Breadcrumbs.vue";
export default { export default {
name: "editor", name: "editor",
@@ -86,6 +86,11 @@ export default {
mounted: function () { mounted: function () {
const fileContent = this.req.content || ""; 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", { this.editor = ace.edit("editor", {
value: fileContent, value: fileContent,
showPrintMargin: false, showPrintMargin: false,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -44,7 +44,7 @@
<script> <script>
import { mapState, mapMutations } from "vuex"; import { mapState, mapMutations } from "vuex";
import { users as api } from "@/api"; import { users as api } from "@/api";
import Errors from "@/views/Errors"; import Errors from "@/views/Errors.vue";
export default { export default {
name: "users", 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 { func handle(fn handleFunc, prefix string, store *storage.Storage, server *settings.Server) http.Handler {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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() settings, err := store.Settings.Get()
if err != nil { 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("/resources").Handler(monkey(resourcePatchHandler(fileCache), "/api/resources")).Methods("PATCH")
api.PathPrefix("/tus").Handler(monkey(tusPostHandler(), "/api/tus")).Methods("POST") 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("/tus").Handler(monkey(tusPatchHandler(), "/api/tus")).Methods("PATCH")
api.PathPrefix("/usage").Handler(monkey(diskUsage, "/api/usage")).Methods("GET") 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") 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) }, "", store, server)
static = handle(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { static = handle(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {