mirror of
https://github.com/filebrowser/filebrowser.git
synced 2025-05-08 19:22:57 +00:00
Compare commits
No commits in common. "master" and "v2.28.0" have entirely different histories.
49
.github/workflows/main.yaml
vendored
49
.github/workflows/main.yaml
vendored
@ -3,25 +3,20 @@ name: main
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "master"
|
- 'master'
|
||||||
tags:
|
tags:
|
||||||
- "v*"
|
- 'v*'
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# linters
|
# linters
|
||||||
lint-frontend:
|
lint-frontend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
package_json_file: "frontend/package.json"
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "22.x"
|
node-version: '18'
|
||||||
cache: "pnpm"
|
|
||||||
cache-dependency-path: "frontend/pnpm-lock.yaml"
|
|
||||||
- run: make lint-frontend
|
- run: make lint-frontend
|
||||||
lint-backend:
|
lint-backend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -29,27 +24,32 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.23.0
|
go-version: 1.21.0
|
||||||
- run: make lint-backend
|
- run: make lint-backend
|
||||||
|
lint-commits:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
- run: make lint-commits
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [lint-frontend, lint-backend]
|
needs: [lint-frontend, lint-backend, lint-commits]
|
||||||
steps:
|
steps:
|
||||||
- run: echo "done"
|
- run: echo "done"
|
||||||
|
|
||||||
# tests
|
# tests
|
||||||
test-frontend:
|
test-frontend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
package_json_file: "frontend/package.json"
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "22.x"
|
node-version: '18'
|
||||||
cache: "pnpm"
|
|
||||||
cache-dependency-path: "frontend/pnpm-lock.yaml"
|
|
||||||
- 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@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.23.0
|
go-version: 1.21.0
|
||||||
- run: make test-backend
|
- run: make test-backend
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -65,7 +65,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- run: echo "done"
|
- run: echo "done"
|
||||||
|
|
||||||
# release
|
# release
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [lint, test]
|
needs: [lint, test]
|
||||||
@ -76,15 +76,10 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.23.0
|
go-version: 1.21.0
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
package_json_file: "frontend/package.json"
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "22.x"
|
node-version: '18'
|
||||||
cache: "pnpm"
|
|
||||||
cache-dependency-path: "frontend/pnpm-lock.yaml"
|
|
||||||
- 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
|
||||||
|
46
.github/workflows/pr-lint.yaml
vendored
46
.github/workflows/pr-lint.yaml
vendored
@ -1,46 +0,0 @@
|
|||||||
name: "Lint PR"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
- reopened
|
|
||||||
- edited
|
|
||||||
- synchronize
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
main:
|
|
||||||
name: Validate PR title
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: amannn/action-semantic-pull-request@v5
|
|
||||||
id: lint_pr_title
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- uses: marocchino/sticky-pull-request-comment@v2
|
|
||||||
# When the previous steps fails, the workflow would stop. By adding this
|
|
||||||
# condition you can continue the execution with the populated error message.
|
|
||||||
if: always() && (steps.lint_pr_title.outputs.error_message != null)
|
|
||||||
with:
|
|
||||||
header: pr-title-lint-error
|
|
||||||
message: |
|
|
||||||
Hey there and thank you for opening this pull request! 👋🏼
|
|
||||||
|
|
||||||
We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
|
|
||||||
|
|
||||||
Details:
|
|
||||||
|
|
||||||
```
|
|
||||||
${{ steps.lint_pr_title.outputs.error_message }}
|
|
||||||
```
|
|
||||||
|
|
||||||
# Delete a previous comment when the issue has been resolved
|
|
||||||
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
|
|
||||||
uses: marocchino/sticky-pull-request-comment@v2
|
|
||||||
with:
|
|
||||||
header: pr-title-lint-error
|
|
||||||
delete: true
|
|
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@ -11,7 +11,7 @@ jobs:
|
|||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v9
|
- uses: actions/stale@v5
|
||||||
with:
|
with:
|
||||||
stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
|
stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
|
||||||
close-pr-message: 'This PR was closed because it has been stalled for 5 days with no activity.'
|
close-pr-message: 'This PR was closed because it has been stalled for 5 days with no activity.'
|
||||||
|
9
.gitignore
vendored
9
.gitignore
vendored
@ -30,14 +30,5 @@ yarn-error.log*
|
|||||||
bin/
|
bin/
|
||||||
build/
|
build/
|
||||||
|
|
||||||
# Vue distributable files
|
|
||||||
/frontend/dist/*
|
/frontend/dist/*
|
||||||
!/frontend/dist/.gitkeep
|
!/frontend/dist/.gitkeep
|
||||||
|
|
||||||
# Playwright files
|
|
||||||
/frontend/test-results/
|
|
||||||
/frontend/playwright-report/
|
|
||||||
/frontend/playwright/.cache/
|
|
||||||
|
|
||||||
default.nix
|
|
||||||
Dockerfile.dev
|
|
||||||
|
@ -6,6 +6,8 @@ linters-settings:
|
|||||||
funlen:
|
funlen:
|
||||||
lines: 100
|
lines: 100
|
||||||
statements: 50
|
statements: 50
|
||||||
|
gci:
|
||||||
|
local-prefixes: github.com/filebrowser/filebrowser
|
||||||
goconst:
|
goconst:
|
||||||
min-len: 2
|
min-len: 2
|
||||||
min-occurrences: 2
|
min-occurrences: 2
|
||||||
@ -27,31 +29,23 @@ linters-settings:
|
|||||||
goimports:
|
goimports:
|
||||||
local-prefixes: github.com/filebrowser/filebrowser
|
local-prefixes: github.com/filebrowser/filebrowser
|
||||||
gomnd:
|
gomnd:
|
||||||
# don't include the "operation" and "assign"
|
settings:
|
||||||
checks:
|
mnd:
|
||||||
- argument
|
# don't include the "operation" and "assign"
|
||||||
- case
|
checks: argument,case,condition,return
|
||||||
- condition
|
|
||||||
- return
|
|
||||||
ignored-numbers:
|
|
||||||
- '0'
|
|
||||||
- '1'
|
|
||||||
- '2'
|
|
||||||
- '3'
|
|
||||||
ignored-functions:
|
|
||||||
- strings.SplitN
|
|
||||||
govet:
|
govet:
|
||||||
enable:
|
check-shadowing: true
|
||||||
- nilness
|
|
||||||
- shadow
|
|
||||||
lll:
|
lll:
|
||||||
line-length: 140
|
line-length: 140
|
||||||
|
maligned:
|
||||||
|
suggest-new: true
|
||||||
misspell:
|
misspell:
|
||||||
locale: US
|
locale: US
|
||||||
nolintlint:
|
nolintlint:
|
||||||
|
allow-leading-space: true # don't require machine-readable nolint directives (i.e. with no leading space)
|
||||||
allow-unused: false # report any unused nolint directives
|
allow-unused: false # report any unused nolint directives
|
||||||
require-explanation: false # require an explanation for nolint directives
|
require-explanation: false # don't require an explanation for nolint directives
|
||||||
require-specific: true # require nolint directives to be specific about which linter is being skipped
|
require-specific: false # don't require nolint directives to be specific about which linter is being skipped
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
# please, do not use `enable-all`: it's deprecated and will be removed soon.
|
# please, do not use `enable-all`: it's deprecated and will be removed soon.
|
||||||
@ -59,19 +53,17 @@ linters:
|
|||||||
disable-all: true
|
disable-all: true
|
||||||
enable:
|
enable:
|
||||||
- bodyclose
|
- bodyclose
|
||||||
|
- deadcode
|
||||||
- dogsled
|
- dogsled
|
||||||
- dupl
|
- dupl
|
||||||
- errcheck
|
- errcheck
|
||||||
- errorlint
|
|
||||||
- exportloopref
|
- exportloopref
|
||||||
- exhaustive
|
- exhaustive
|
||||||
- funlen
|
- funlen
|
||||||
- gocheckcompilerdirectives
|
|
||||||
- gochecknoinits
|
- gochecknoinits
|
||||||
- goconst
|
- goconst
|
||||||
- gocritic
|
- gocritic
|
||||||
- gocyclo
|
- gocyclo
|
||||||
- godox
|
|
||||||
- goimports
|
- goimports
|
||||||
- gomnd
|
- gomnd
|
||||||
- goprintffuncname
|
- goprintffuncname
|
||||||
@ -83,21 +75,19 @@ linters:
|
|||||||
- misspell
|
- misspell
|
||||||
- nakedret
|
- nakedret
|
||||||
- nolintlint
|
- nolintlint
|
||||||
- prealloc
|
|
||||||
- revive
|
|
||||||
- rowserrcheck
|
- rowserrcheck
|
||||||
- staticcheck
|
- staticcheck
|
||||||
|
- structcheck
|
||||||
- stylecheck
|
- stylecheck
|
||||||
- testifylint
|
|
||||||
- typecheck
|
- typecheck
|
||||||
- unconvert
|
- unconvert
|
||||||
- unparam
|
- unparam
|
||||||
- unused
|
- unused
|
||||||
|
- varcheck
|
||||||
- whitespace
|
- whitespace
|
||||||
|
- prealloc
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
exclude-dirs:
|
|
||||||
- frontend/
|
|
||||||
exclude-rules:
|
exclude-rules:
|
||||||
- path: cmd/.*.go
|
- path: cmd/.*.go
|
||||||
linters:
|
linters:
|
||||||
@ -118,4 +108,13 @@ issues:
|
|||||||
- gomnd
|
- gomnd
|
||||||
|
|
||||||
run:
|
run:
|
||||||
timeout: 5m
|
go: '1.18'
|
||||||
|
skip-dirs:
|
||||||
|
- frontend/
|
||||||
|
skip-files:
|
||||||
|
- http/rice-box.go
|
||||||
|
|
||||||
|
# golangci.com configuration
|
||||||
|
# https://github.com/golangci/golangci/wiki/Configuration
|
||||||
|
service:
|
||||||
|
golangci-lint-version: 1.27.x # use the fixed version to not introduce new linters unexpectedly
|
@ -1,5 +1,3 @@
|
|||||||
version: 2
|
|
||||||
|
|
||||||
project_name: filebrowser
|
project_name: filebrowser
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@ -36,10 +34,10 @@ builds:
|
|||||||
archives:
|
archives:
|
||||||
-
|
-
|
||||||
name_template: "{{.Os}}-{{.Arch}}{{if .Arm}}v{{.Arm}}{{end}}-{{ .ProjectName }}"
|
name_template: "{{.Os}}-{{.Arch}}{{if .Arm}}v{{.Arm}}{{end}}-{{ .ProjectName }}"
|
||||||
formats: [ 'tar.gz' ]
|
format: tar.gz
|
||||||
format_overrides:
|
format_overrides:
|
||||||
- goos: windows
|
- goos: windows
|
||||||
formats: [ 'zip' ]
|
format: zip
|
||||||
|
|
||||||
dockers:
|
dockers:
|
||||||
-
|
-
|
||||||
@ -139,7 +137,6 @@ dockers:
|
|||||||
- "filebrowser/filebrowser:v{{ .Major }}-amd64-s6"
|
- "filebrowser/filebrowser:v{{ .Major }}-amd64-s6"
|
||||||
extra_files:
|
extra_files:
|
||||||
- docker/root
|
- docker/root
|
||||||
- healthcheck.sh
|
|
||||||
-
|
-
|
||||||
dockerfile: Dockerfile.s6.aarch64
|
dockerfile: Dockerfile.s6.aarch64
|
||||||
use: buildx
|
use: buildx
|
||||||
@ -158,7 +155,6 @@ dockers:
|
|||||||
- "filebrowser/filebrowser:v{{ .Major }}-arm64-s6"
|
- "filebrowser/filebrowser:v{{ .Major }}-arm64-s6"
|
||||||
extra_files:
|
extra_files:
|
||||||
- docker/root
|
- docker/root
|
||||||
- healthcheck.sh
|
|
||||||
docker_manifests:
|
docker_manifests:
|
||||||
- name_template: "filebrowser/filebrowser:latest"
|
- name_template: "filebrowser/filebrowser:latest"
|
||||||
image_templates:
|
image_templates:
|
||||||
@ -193,7 +189,7 @@ brews:
|
|||||||
repository:
|
repository:
|
||||||
owner: filebrowser
|
owner: filebrowser
|
||||||
name: homebrew-tap
|
name: homebrew-tap
|
||||||
directory: Formula
|
folder: Formula
|
||||||
homepage: https://filebrowser.org
|
homepage: https://filebrowser.org
|
||||||
commit_author:
|
commit_author:
|
||||||
name: FileBrowser Robot
|
name: FileBrowser Robot
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[main]
|
[main]
|
||||||
host = https://www.transifex.com
|
host = https://www.transifex.com
|
||||||
lang_map = pt_BR: pt-br, zh_CN: zh-cn, zh_HK: zh-hk, zh_TW: zh-tw, nl_BE: nl-be, sv_SE: sv-se, cz-CS: cz_cs
|
lang_map = pt_BR: pt-br, zh_CN: zh-cn, zh_HK: zh-hk, zh_TW: zh-tw, nl_BE: nl-be, sv_SE: sv-se
|
||||||
|
|
||||||
[file-browser.file-browser]
|
[file-browser.file-browser]
|
||||||
file_filter = frontend/src/i18n/<lang>.json
|
file_filter = frontend/src/i18n/<lang>.json
|
||||||
|
136
CHANGELOG.md
136
CHANGELOG.md
@ -2,140 +2,6 @@
|
|||||||
|
|
||||||
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.32.0](https://github.com/filebrowser/filebrowser/compare/v2.31.2...v2.32.0) (2025-01-31)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* create user on proxy authentication if user does not exist ([#3569](https://github.com/filebrowser/filebrowser/issues/3569)) ([209acf2](https://github.com/filebrowser/filebrowser/commit/209acf2429b06e2e8d78218937c59fd7e7edd1be))
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* add proper healthcheck for S6 containers ([#3691](https://github.com/filebrowser/filebrowser/issues/3691)) ([045064f](https://github.com/filebrowser/filebrowser/commit/045064f8b8bf9f86058e877448085e38da8b3f2e))
|
|
||||||
* disk usage refreshing ([#3692](https://github.com/filebrowser/filebrowser/issues/3692)) ([bbdd313](https://github.com/filebrowser/filebrowser/commit/bbdd313705b8d253f0c47ad717a6e47b2f46e719))
|
|
||||||
* Fix user creation on proxy auth ([#3666](https://github.com/filebrowser/filebrowser/issues/3666)) ([5300d00](https://github.com/filebrowser/filebrowser/commit/5300d00d2e7dbb80a252aff57e100113f02506c3))
|
|
||||||
* prompts disappearing on copy / move / upload ([#3537](https://github.com/filebrowser/filebrowser/issues/3537)) ([d1c84a8](https://github.com/filebrowser/filebrowser/commit/d1c84a84123c77dede05c023b3697a432b56122c))
|
|
||||||
|
|
||||||
|
|
||||||
### Refactorings
|
|
||||||
|
|
||||||
* Fix eslint warnings ([#3698](https://github.com/filebrowser/filebrowser/issues/3698)) ([0201f9c](https://github.com/filebrowser/filebrowser/commit/0201f9c5c4dd2a4d5a3503e59cdb8045e8d3a91f)), closes [#3407](https://github.com/filebrowser/filebrowser/issues/3407)
|
|
||||||
|
|
||||||
|
|
||||||
### Build
|
|
||||||
|
|
||||||
* **deps:** bump cross-spawn from 7.0.3 to 7.0.6 in /tools ([#3601](https://github.com/filebrowser/filebrowser/issues/3601)) ([25372ed](https://github.com/filebrowser/filebrowser/commit/25372edb5c0e616e82b76b5f523633af57d347e0))
|
|
||||||
* **deps:** bump github.com/golang-jwt/jwt/v4 from 4.5.0 to 4.5.1 ([#3574](https://github.com/filebrowser/filebrowser/issues/3574)) ([2fdea73](https://github.com/filebrowser/filebrowser/commit/2fdea73430011846276a1cda52458f1d670f5ea7))
|
|
||||||
* **deps:** bump golang.org/x/crypto from 0.26.0 to 0.31.0 ([#3634](https://github.com/filebrowser/filebrowser/issues/3634)) ([e92dbb4](https://github.com/filebrowser/filebrowser/commit/e92dbb4bb8b7894264fbf0a48a641712c3b68766))
|
|
||||||
* **deps:** bump golang.org/x/net from 0.23.0 to 0.33.0 ([#3712](https://github.com/filebrowser/filebrowser/issues/3712)) ([1194cfe](https://github.com/filebrowser/filebrowser/commit/1194cfe0097a70399c1f06cf0f514b9d70fa463c))
|
|
||||||
* **deps:** bump vue-i18n from 9.10.2 to 9.14.2 in /frontend ([#3618](https://github.com/filebrowser/filebrowser/issues/3618)) ([0659594](https://github.com/filebrowser/filebrowser/commit/065959451d3ba12019c6151274aa4e6904cdca99))
|
|
||||||
* fix go releaser ([ba797cd](https://github.com/filebrowser/filebrowser/commit/ba797cda3135eddb9b7165dc5ceb932399cb54df))
|
|
||||||
* update to node 22 and pnpm ([#3616](https://github.com/filebrowser/filebrowser/issues/3616)) ([d51a343](https://github.com/filebrowser/filebrowser/commit/d51a3438201274a1b826be1b775ca1035ade20c5))
|
|
||||||
|
|
||||||
### [2.31.2](https://github.com/filebrowser/filebrowser/compare/v2.31.1...v2.31.2) (2024-10-03)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* added whitespace before version ([#3510](https://github.com/filebrowser/filebrowser/issues/3510)) ([2b37e69](https://github.com/filebrowser/filebrowser/commit/2b37e696c9bde4d0c453de236a3555d982346bbb))
|
|
||||||
* change location of custom init scripts ([#3493](https://github.com/filebrowser/filebrowser/issues/3493)) ([406d4f7](https://github.com/filebrowser/filebrowser/commit/406d4f78845a1684df7c9c457b208f4dd9b2a930))
|
|
||||||
* files list alignment ([#3494](https://github.com/filebrowser/filebrowser/issues/3494)) ([64400ff](https://github.com/filebrowser/filebrowser/commit/64400ffda8b09f66b8662a3c9400235139800a4d))
|
|
||||||
* german translation spelling typos ([#3469](https://github.com/filebrowser/filebrowser/issues/3469)) ([1e7c415](https://github.com/filebrowser/filebrowser/commit/1e7c41505fb6a3b9baa1534787492a186e09bcfb))
|
|
||||||
|
|
||||||
|
|
||||||
### Build
|
|
||||||
|
|
||||||
* **deps-dev:** bump vite from 5.2.7 to 5.4.6 in /frontend ([#3496](https://github.com/filebrowser/filebrowser/issues/3496)) ([ec7b643](https://github.com/filebrowser/filebrowser/commit/ec7b643e8e9499f7ff226ec7f8e63a9df9890352))
|
|
||||||
* **deps:** bump rollup from 4.21.3 to 4.22.4 in /frontend ([#3504](https://github.com/filebrowser/filebrowser/issues/3504)) ([03d74ee](https://github.com/filebrowser/filebrowser/commit/03d74ee7582196c09720f8d488056339f06c446c))
|
|
||||||
|
|
||||||
### [2.31.1](https://github.com/filebrowser/filebrowser/compare/v2.31.0...v2.31.1) (2024-08-30)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* command not found in shell ([#3438](https://github.com/filebrowser/filebrowser/issues/3438)) ([121d9ab](https://github.com/filebrowser/filebrowser/commit/121d9abecdc7d4e923cfc5023519995938a6ccae))
|
|
||||||
|
|
||||||
|
|
||||||
### Build
|
|
||||||
|
|
||||||
* update to alpine 3.20 ([#3447](https://github.com/filebrowser/filebrowser/issues/3447)) ([7de6bc4](https://github.com/filebrowser/filebrowser/commit/7de6bc4a912b5734dd0df02ed8391e78619e2615))
|
|
||||||
|
|
||||||
## [2.31.0](https://github.com/filebrowser/filebrowser/compare/v2.30.0...v2.31.0) (2024-08-29)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* add Czech translation ([#3416](https://github.com/filebrowser/filebrowser/issues/3416)) ([8e67a12](https://github.com/filebrowser/filebrowser/commit/8e67a12f260caefcbe419c2281025b9b15f02bf3))
|
|
||||||
* Added epub preview. Resolves [#3375](https://github.com/filebrowser/filebrowser/issues/3375) ([#3376](https://github.com/filebrowser/filebrowser/issues/3376)) ([99a6382](https://github.com/filebrowser/filebrowser/commit/99a6382b320874e94f9bd74708f46dd9a7485d3c))
|
|
||||||
* implement markdown file preview in Ace editor ([#3431](https://github.com/filebrowser/filebrowser/issues/3431)) ([b0f4604](https://github.com/filebrowser/filebrowser/commit/b0f4604f44e6a35e07df3000f106f523cd942cfc))
|
|
||||||
* support mime type for epub extension ([#3425](https://github.com/filebrowser/filebrowser/issues/3425)) ([f6f7e5f](https://github.com/filebrowser/filebrowser/commit/f6f7e5fea3ff7073ee652008a51cb5445a6f3d5d))
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* clipboard copy in safari ([#3261](https://github.com/filebrowser/filebrowser/issues/3261)) ([1fccc5d](https://github.com/filebrowser/filebrowser/commit/1fccc5d649add2a56c55e75cf9dec4851e6d7cbf))
|
|
||||||
* CSS selectors for listing icons ([#3277](https://github.com/filebrowser/filebrowser/issues/3277)) ([2a90cdf](https://github.com/filebrowser/filebrowser/commit/2a90cdfdaff8655c7cb1167c01994a0978dece8f))
|
|
||||||
* fix catalan i18n file ([090272e](https://github.com/filebrowser/filebrowser/commit/090272e3b7c56a940c4aa2d28f860c574aa17d53))
|
|
||||||
* fixing an issue where the upload indicator would "jump" around in the UI ([#3354](https://github.com/filebrowser/filebrowser/issues/3354)) ([7be5644](https://github.com/filebrowser/filebrowser/commit/7be564495226bc6846289a56edb8893511036c6e))
|
|
||||||
* **frontend:** N files selected hint use i18n ([#3390](https://github.com/filebrowser/filebrowser/issues/3390)) ([10bf3cf](https://github.com/filebrowser/filebrowser/commit/10bf3cffbf8eb7d95fe4e1cc6acf1012329744b9))
|
|
||||||
* pdf preview header ([#3274](https://github.com/filebrowser/filebrowser/issues/3274)) ([a838868](https://github.com/filebrowser/filebrowser/commit/a8388689f3019083f263845900f683ddc13884dc))
|
|
||||||
* pull down to refresh within editor ([#3378](https://github.com/filebrowser/filebrowser/issues/3378)) ([21783ed](https://github.com/filebrowser/filebrowser/commit/21783ed91a13ad52afdb411e43faf14fb6ef6e42))
|
|
||||||
|
|
||||||
|
|
||||||
### Build
|
|
||||||
|
|
||||||
* bump go libs ([b596567](https://github.com/filebrowser/filebrowser/commit/b596567c6163d57eaefbf3e30d84cfca65c24cdf))
|
|
||||||
* bump go version to 1.23.0 ([364fdaa](https://github.com/filebrowser/filebrowser/commit/364fdaaf0c1eace82ff8637d337cc1b32e5e9972))
|
|
||||||
* bump golangci-lint to 1.60.3 ([a6347c8](https://github.com/filebrowser/filebrowser/commit/a6347c88586e584b4565277b0010fa9ff2576b1f))
|
|
||||||
* **deps-dev:** bump braces from 3.0.2 to 3.0.3 in /frontend ([#3316](https://github.com/filebrowser/filebrowser/issues/3316)) ([e8589be](https://github.com/filebrowser/filebrowser/commit/e8589be6409a2b29edd44ee2edd3fbf6b2d72724))
|
|
||||||
* **deps-dev:** bump ws from 8.16.0 to 8.17.1 in /frontend ([#3321](https://github.com/filebrowser/filebrowser/issues/3321)) ([c3465f9](https://github.com/filebrowser/filebrowser/commit/c3465f99136506d51b813be4f31b289e708da0ce))
|
|
||||||
* **deps:** bump golang.org/x/image from 0.15.0 to 0.18.0 ([#3335](https://github.com/filebrowser/filebrowser/issues/3335)) ([30a8ddf](https://github.com/filebrowser/filebrowser/commit/30a8ddf113862e3de2c09547662b7f2af8a30dfe))
|
|
||||||
* fix goreleaser file ([056cfa8](https://github.com/filebrowser/filebrowser/commit/056cfa8facdca4c397a6b245028d4c9d3f0ca518))
|
|
||||||
|
|
||||||
## [2.30.0](https://github.com/filebrowser/filebrowser/compare/v2.29.0...v2.30.0) (2024-05-19)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* allow multi-select with SHIFT key in singleClick mode ([#3185](https://github.com/filebrowser/filebrowser/issues/3185)) ([2e47a03](https://github.com/filebrowser/filebrowser/commit/2e47a038d63de8f848b070578c1d71f765438a24))
|
|
||||||
* Enhance MIME Type Detection for Additional File Extensions ([#3183](https://github.com/filebrowser/filebrowser/issues/3183)) ([be62f56](https://github.com/filebrowser/filebrowser/commit/be62f56782551e17d6d5dc23bc29cc56ef961a66))
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* add overlay for sidebar on mobile ([#3197](https://github.com/filebrowser/filebrowser/issues/3197)) ([3b48f75](https://github.com/filebrowser/filebrowser/commit/3b48f75301287fe94cbbacff184b4db03f37f7ea))
|
|
||||||
* current folder name in page title ([#3200](https://github.com/filebrowser/filebrowser/issues/3200)) ([e336a25](https://github.com/filebrowser/filebrowser/commit/e336a25ad29ed8b956169d426992860a877ee551))
|
|
||||||
* Fixing the inability to play MKV video files online and enhancing the auxiliary features of the VideoPlayer. ([#3181](https://github.com/filebrowser/filebrowser/issues/3181)) ([782375b](https://github.com/filebrowser/filebrowser/commit/782375b1cb4c4f954468c30ec277ce021c82b40d))
|
|
||||||
* shell window size ([#3198](https://github.com/filebrowser/filebrowser/issues/3198)) ([4c5b612](https://github.com/filebrowser/filebrowser/commit/4c5b612cb2563817f9da50413c7cf9e89b4c4d4a))
|
|
||||||
* The file type icon in the file list is sensitive to the case of the suffix name ([#3187](https://github.com/filebrowser/filebrowser/issues/3187)) ([a9c327c](https://github.com/filebrowser/filebrowser/commit/a9c327cc0687796a3c7bfafd4ddabf4342859e31))
|
|
||||||
|
|
||||||
## [2.29.0](https://github.com/filebrowser/filebrowser/compare/v2.28.0...v2.29.0) (2024-04-30)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* Display Upload Progress as Percentage and File Size / Total File Size ([#3111](https://github.com/filebrowser/filebrowser/issues/3111)) ([236ca63](https://github.com/filebrowser/filebrowser/commit/236ca637f99e373adfeaaefc5db6af50bd15b6bf))
|
|
||||||
* migrate to vue 3 ([#2689](https://github.com/filebrowser/filebrowser/issues/2689)) ([5100e58](https://github.com/filebrowser/filebrowser/commit/5100e587d73831ecdb5e3bd35a78fef96ad248a4))
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* abort upload behavior to properly handle server-side deletion and frontend state reset ([#3114](https://github.com/filebrowser/filebrowser/issues/3114)) ([434e49b](https://github.com/filebrowser/filebrowser/commit/434e49bf59e4ddf7ec90893fa3fd53faee8c9cbb))
|
|
||||||
* apply proper zindex to modal dialogs ([#3172](https://github.com/filebrowser/filebrowser/issues/3172)) ([821f51e](https://github.com/filebrowser/filebrowser/commit/821f51ea5ad1f5c2eb72441bc761031cacee43e1))
|
|
||||||
* correct list item selector ([#3126](https://github.com/filebrowser/filebrowser/issues/3126)) ([#3147](https://github.com/filebrowser/filebrowser/issues/3147)) ([22a05e1](https://github.com/filebrowser/filebrowser/commit/22a05e1f02a083cf7b630e16873dad0de89b7854))
|
|
||||||
* don't redirect to login when no auth ([#3165](https://github.com/filebrowser/filebrowser/issues/3165)) ([da5a6e0](https://github.com/filebrowser/filebrowser/commit/da5a6e051faa80134c2adf4e621426cbdf046c88))
|
|
||||||
* Frontend bug, administrators unable to delete users ([#3170](https://github.com/filebrowser/filebrowser/issues/3170)) ([bee71d9](https://github.com/filebrowser/filebrowser/commit/bee71d93fee137cdd807cd8f7716c7da0830fae7))
|
|
||||||
* handle quotes in healthcheck.sh ([#3130](https://github.com/filebrowser/filebrowser/issues/3130)) ([18f04a7](https://github.com/filebrowser/filebrowser/commit/18f04a7d26186927f51f46354f3b2164a68f1b41))
|
|
||||||
* the copy method in clipboard.ts ([#3177](https://github.com/filebrowser/filebrowser/issues/3177)) ([4786187](https://github.com/filebrowser/filebrowser/commit/4786187852b8eef07e40aa00cd159ccc1e7e79dc))
|
|
||||||
|
|
||||||
|
|
||||||
### Build
|
|
||||||
|
|
||||||
* bump go version to 1.22.1 ([bbd0abb](https://github.com/filebrowser/filebrowser/commit/bbd0abbdfdbb3ddf3326247b7c6d925751dfabcb))
|
|
||||||
* bump go version to 1.22.2 ([#3158](https://github.com/filebrowser/filebrowser/issues/3158)) ([a9da7fd](https://github.com/filebrowser/filebrowser/commit/a9da7fd56c849b5a13133136b35ef5ebee622962))
|
|
||||||
* **deps:** bump golang.org/x/net from 0.22.0 to 0.23.0 ([#3133](https://github.com/filebrowser/filebrowser/issues/3133)) ([6b77b8d](https://github.com/filebrowser/filebrowser/commit/6b77b8d683f7357ef71af678550e78910c10ddeb))
|
|
||||||
|
|
||||||
## [2.28.0](https://github.com/filebrowser/filebrowser/compare/v2.27.0...v2.28.0) (2024-04-01)
|
## [2.28.0](https://github.com/filebrowser/filebrowser/compare/v2.27.0...v2.28.0) (2024-04-01)
|
||||||
|
|
||||||
|
|
||||||
@ -146,7 +12,7 @@ All notable changes to this project will be documented in this file. See [standa
|
|||||||
* close editor when click escape key ([#2947](https://github.com/filebrowser/filebrowser/issues/2947)) ([70c8261](https://github.com/filebrowser/filebrowser/commit/70c826133b8578b8712e6db8f762a15a076cd9a9))
|
* close editor when click escape key ([#2947](https://github.com/filebrowser/filebrowser/issues/2947)) ([70c8261](https://github.com/filebrowser/filebrowser/commit/70c826133b8578b8712e6db8f762a15a076cd9a9))
|
||||||
* enable preview in shared folder ([#3055](https://github.com/filebrowser/filebrowser/issues/3055)) ([4c233c3](https://github.com/filebrowser/filebrowser/commit/4c233c3db39ea5a00d6e602ec0ecbddecb590877))
|
* enable preview in shared folder ([#3055](https://github.com/filebrowser/filebrowser/issues/3055)) ([4c233c3](https://github.com/filebrowser/filebrowser/commit/4c233c3db39ea5a00d6e602ec0ecbddecb590877))
|
||||||
* focus editor when opened ([#2946](https://github.com/filebrowser/filebrowser/issues/2946)) ([b19710e](https://github.com/filebrowser/filebrowser/commit/b19710efca6daa7af56dc211d0051d500d2eea22))
|
* focus editor when opened ([#2946](https://github.com/filebrowser/filebrowser/issues/2946)) ([b19710e](https://github.com/filebrowser/filebrowser/commit/b19710efca6daa7af56dc211d0051d500d2eea22))
|
||||||
* freezing the list in the background while previewing a file ([#3004](https://github.com/filebrowser/filebrowser/issues/3004)) ([e167c3e](https://github.com/filebrowser/filebrowser/commit/e167c3e1efed8b16be45d994a8d443fda1d8cf49))
|
* freezing the list in the backgroud while previewing a file ([#3004](https://github.com/filebrowser/filebrowser/issues/3004)) ([e167c3e](https://github.com/filebrowser/filebrowser/commit/e167c3e1efed8b16be45d994a8d443fda1d8cf49))
|
||||||
* prompt to confirm discard editor changes ([#2948](https://github.com/filebrowser/filebrowser/issues/2948)) ([fb1a09c](https://github.com/filebrowser/filebrowser/commit/fb1a09c7c172b913c12b30975ca545e505df0c05))
|
* prompt to confirm discard editor changes ([#2948](https://github.com/filebrowser/filebrowser/issues/2948)) ([fb1a09c](https://github.com/filebrowser/filebrowser/commit/fb1a09c7c172b913c12b30975ca545e505df0c05))
|
||||||
* select multiple files with ctrl even with singleClick option ([#2953](https://github.com/filebrowser/filebrowser/issues/2953)) ([d49c3df](https://github.com/filebrowser/filebrowser/commit/d49c3dfacfc0ff07e620b3ad2700e64927b06235))
|
* select multiple files with ctrl even with singleClick option ([#2953](https://github.com/filebrowser/filebrowser/issues/2953)) ([d49c3df](https://github.com/filebrowser/filebrowser/commit/d49c3dfacfc0ff07e620b3ad2700e64927b06235))
|
||||||
|
|
||||||
|
@ -1,19 +1,14 @@
|
|||||||
FROM ghcr.io/linuxserver/baseimage-alpine:3.20
|
FROM ghcr.io/linuxserver/baseimage-alpine:3.17
|
||||||
|
|
||||||
RUN apk --update add ca-certificates \
|
RUN apk --update add ca-certificates \
|
||||||
mailcap \
|
mailcap \
|
||||||
curl \
|
curl
|
||||||
jq
|
|
||||||
|
|
||||||
COPY healthcheck.sh /healthcheck.sh
|
|
||||||
RUN chmod +x /healthcheck.sh # Make the script executable
|
|
||||||
|
|
||||||
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
|
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
|
||||||
CMD /healthcheck.sh || exit 1
|
CMD curl -f http://localhost/health || exit 1
|
||||||
|
|
||||||
# copy local files
|
# copy local files
|
||||||
COPY docker/root/ /
|
COPY docker/root/ /
|
||||||
RUN ln -s /config/settings.json /.filebrowser.json
|
|
||||||
COPY filebrowser /usr/bin/filebrowser
|
COPY filebrowser /usr/bin/filebrowser
|
||||||
|
|
||||||
# ports and volumes
|
# ports and volumes
|
||||||
|
@ -1,19 +1,14 @@
|
|||||||
FROM ghcr.io/linuxserver/baseimage-alpine:arm64v8-3.20
|
FROM ghcr.io/linuxserver/baseimage-alpine:arm64v8-3.17
|
||||||
|
|
||||||
RUN apk --update add ca-certificates \
|
RUN apk --update add ca-certificates \
|
||||||
mailcap \
|
mailcap \
|
||||||
curl \
|
curl
|
||||||
jq
|
|
||||||
|
|
||||||
COPY healthcheck.sh /healthcheck.sh
|
|
||||||
RUN chmod +x /healthcheck.sh # Make the script executable
|
|
||||||
|
|
||||||
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
|
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
|
||||||
CMD /healthcheck.sh || exit 1
|
CMD curl -f http://localhost/health || exit 1
|
||||||
|
|
||||||
# copy local files
|
# copy local files
|
||||||
COPY docker/root/ /
|
COPY docker/root/ /
|
||||||
RUN ln -s /config/settings.json /.filebrowser.json
|
|
||||||
COPY filebrowser /usr/bin/filebrowser
|
COPY filebrowser /usr/bin/filebrowser
|
||||||
|
|
||||||
# ports and volumes
|
# ports and volumes
|
||||||
|
16
Dockerfile.s6.armhf
Normal file
16
Dockerfile.s6.armhf
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
FROM ghcr.io/linuxserver/baseimage-alpine:arm32v7-3.17
|
||||||
|
|
||||||
|
RUN apk --update add ca-certificates \
|
||||||
|
mailcap \
|
||||||
|
curl
|
||||||
|
|
||||||
|
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
|
||||||
|
CMD curl -f http://localhost/health || exit 1
|
||||||
|
|
||||||
|
# copy local files
|
||||||
|
COPY docker/root/ /
|
||||||
|
COPY filebrowser /usr/bin/filebrowser
|
||||||
|
|
||||||
|
# ports and volumes
|
||||||
|
VOLUME /srv /config /database
|
||||||
|
EXPOSE 80
|
7
Makefile
7
Makefile
@ -10,7 +10,7 @@ build: | build-frontend build-backend ## Build binary
|
|||||||
|
|
||||||
.PHONY: build-frontend
|
.PHONY: build-frontend
|
||||||
build-frontend: ## Build frontend
|
build-frontend: ## Build frontend
|
||||||
$Q cd frontend && pnpm install --frozen-lockfile && pnpm run build
|
$Q cd frontend && npm ci && npm run build
|
||||||
|
|
||||||
.PHONY: build-backend
|
.PHONY: build-backend
|
||||||
build-backend: ## Build backend
|
build-backend: ## Build backend
|
||||||
@ -21,18 +21,17 @@ test: | test-frontend test-backend ## Run all tests
|
|||||||
|
|
||||||
.PHONY: test-frontend
|
.PHONY: test-frontend
|
||||||
test-frontend: ## Run frontend tests
|
test-frontend: ## Run frontend tests
|
||||||
$Q cd frontend && pnpm install --frozen-lockfile && pnpm run typecheck
|
|
||||||
|
|
||||||
.PHONY: test-backend
|
.PHONY: test-backend
|
||||||
test-backend: ## Run backend tests
|
test-backend: ## Run backend tests
|
||||||
$Q $(go) test -v ./...
|
$Q $(go) test -v ./...
|
||||||
|
|
||||||
.PHONY: lint
|
.PHONY: lint
|
||||||
lint: lint-frontend lint-backend ## Run all linters
|
lint: lint-frontend lint-backend lint-commits ## Run all linters
|
||||||
|
|
||||||
.PHONY: lint-frontend
|
.PHONY: lint-frontend
|
||||||
lint-frontend: ## Run frontend linters
|
lint-frontend: ## Run frontend linters
|
||||||
$Q cd frontend && pnpm install --frozen-lockfile && pnpm run lint
|
$Q cd frontend && npm ci && npm run lint
|
||||||
|
|
||||||
.PHONY: lint-backend
|
.PHONY: lint-backend
|
||||||
lint-backend: | $(golangci-lint) ## Run backend linters
|
lint-backend: | $(golangci-lint) ## Run backend linters
|
||||||
|
@ -2,7 +2,6 @@ package auth
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -10,7 +9,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
|
"github.com/filebrowser/filebrowser/v2/errors"
|
||||||
"github.com/filebrowser/filebrowser/v2/files"
|
"github.com/filebrowser/filebrowser/v2/files"
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
@ -124,10 +123,10 @@ func (a *HookAuth) GetValues(s string) {
|
|||||||
|
|
||||||
// iterate input lines
|
// iterate input lines
|
||||||
for _, val := range strings.Split(s, "\n") {
|
for _, val := range strings.Split(s, "\n") {
|
||||||
v := strings.SplitN(val, "=", 2)
|
v := strings.SplitN(val, "=", 2) //nolint: gomnd
|
||||||
|
|
||||||
// skips non key and value format
|
// skips non key and value format
|
||||||
if len(v) != 2 {
|
if len(v) != 2 { //nolint: gomnd
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,7 +144,7 @@ func (a *HookAuth) GetValues(s string) {
|
|||||||
// SaveUser updates the existing user or creates a new one when not found
|
// SaveUser updates the existing user or creates a new one when not found
|
||||||
func (a *HookAuth) SaveUser() (*users.User, error) {
|
func (a *HookAuth) SaveUser() (*users.User, error) {
|
||||||
u, err := a.Users.Get(a.Server.Root, a.Cred.Username)
|
u, err := a.Users.Get(a.Server.Root, a.Cred.Username)
|
||||||
if err != nil && !errors.Is(err, fbErrors.ErrNotExist) {
|
if err != nil && err != errors.ErrNotExist {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ type JSONAuth struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Auth authenticates the user via a json in content body.
|
// Auth authenticates the user via a json in content body.
|
||||||
func (a JSONAuth) Auth(r *http.Request, usr users.Store, _ *settings.Settings, srv *settings.Server) (*users.User, error) {
|
func (a JSONAuth) Auth(r *http.Request, usr users.Store, stg *settings.Settings, srv *settings.Server) (*users.User, error) {
|
||||||
var cred jsonCred
|
var cred jsonCred
|
||||||
|
|
||||||
if r.Body == nil {
|
if r.Body == nil {
|
||||||
@ -39,7 +39,7 @@ func (a JSONAuth) Auth(r *http.Request, usr users.Store, _ *settings.Settings, s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If ReCaptcha is enabled, check the code.
|
// If ReCaptcha is enabled, check the code.
|
||||||
if a.ReCaptcha != nil && a.ReCaptcha.Secret != "" {
|
if a.ReCaptcha != nil && len(a.ReCaptcha.Secret) > 0 {
|
||||||
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:govet
|
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:govet
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -14,7 +14,7 @@ const MethodNoAuth settings.AuthMethod = "noauth"
|
|||||||
type NoAuth struct{}
|
type NoAuth struct{}
|
||||||
|
|
||||||
// Auth uses authenticates user 1.
|
// Auth uses authenticates user 1.
|
||||||
func (a NoAuth) Auth(_ *http.Request, usr users.Store, _ *settings.Settings, srv *settings.Server) (*users.User, error) {
|
func (a NoAuth) Auth(r *http.Request, usr users.Store, stg *settings.Settings, srv *settings.Server) (*users.User, error) {
|
||||||
return usr.Get(srv.Root, uint(1))
|
return usr.Get(srv.Root, uint(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
|
"github.com/filebrowser/filebrowser/v2/errors"
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
)
|
)
|
||||||
@ -19,51 +18,16 @@ type ProxyAuth struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Auth authenticates the user via an HTTP header.
|
// Auth authenticates the user via an HTTP header.
|
||||||
func (a ProxyAuth) Auth(r *http.Request, usr users.Store, setting *settings.Settings, srv *settings.Server) (*users.User, error) {
|
func (a ProxyAuth) Auth(r *http.Request, usr users.Store, stg *settings.Settings, srv *settings.Server) (*users.User, error) {
|
||||||
username := r.Header.Get(a.Header)
|
username := r.Header.Get(a.Header)
|
||||||
user, err := usr.Get(srv.Root, username)
|
user, err := usr.Get(srv.Root, username)
|
||||||
if errors.Is(err, fbErrors.ErrNotExist) {
|
if err == errors.ErrNotExist {
|
||||||
return a.createUser(usr, setting, srv, username)
|
return nil, os.ErrPermission
|
||||||
}
|
}
|
||||||
|
|
||||||
return user, err
|
return user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a ProxyAuth) createUser(usr users.Store, setting *settings.Settings, srv *settings.Server, username string) (*users.User, error) {
|
|
||||||
const passwordSize = 32
|
|
||||||
randomPasswordBytes := make([]byte, passwordSize)
|
|
||||||
_, err := rand.Read(randomPasswordBytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var hashedRandomPassword string
|
|
||||||
hashedRandomPassword, err = users.HashPwd(string(randomPasswordBytes))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
user := &users.User{
|
|
||||||
Username: username,
|
|
||||||
Password: hashedRandomPassword,
|
|
||||||
LockPassword: true,
|
|
||||||
}
|
|
||||||
setting.Defaults.Apply(user)
|
|
||||||
|
|
||||||
var userHome string
|
|
||||||
userHome, err = setting.MakeUserDir(user.Username, user.Scope, srv.Root)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
user.Scope = userHome
|
|
||||||
|
|
||||||
err = usr.Save(user)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoginPage tells that proxy auth doesn't require a login page.
|
// LoginPage tells that proxy auth doesn't require a login page.
|
||||||
func (a ProxyAuth) LoginPage() bool {
|
func (a ProxyAuth) LoginPage() bool {
|
||||||
return false
|
return false
|
||||||
|
@ -14,8 +14,8 @@ var cmdsAddCmd = &cobra.Command{
|
|||||||
Use: "add <event> <command>",
|
Use: "add <event> <command>",
|
||||||
Short: "Add a command to run on a specific event",
|
Short: "Add a command to run on a specific event",
|
||||||
Long: `Add a command to run on a specific event.`,
|
Long: `Add a command to run on a specific event.`,
|
||||||
Args: cobra.MinimumNArgs(2),
|
Args: cobra.MinimumNArgs(2), //nolint:gomnd
|
||||||
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||||
s, err := d.store.Settings.Get()
|
s, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
command := strings.Join(args[1:], " ")
|
command := strings.Join(args[1:], " ")
|
||||||
|
@ -14,7 +14,7 @@ var cmdsLsCmd = &cobra.Command{
|
|||||||
Short: "List all commands for each event",
|
Short: "List all commands for each event",
|
||||||
Long: `List all commands for each event.`,
|
Long: `List all commands for each event.`,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
|
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||||
s, err := d.store.Settings.Get()
|
s, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
evt := mustGetString(cmd.Flags(), "event")
|
evt := mustGetString(cmd.Flags(), "event")
|
||||||
|
@ -23,7 +23,7 @@ You can also specify an optional parameter (index_end) so
|
|||||||
you can remove all commands from 'index' to 'index_end',
|
you can remove all commands from 'index' to 'index_end',
|
||||||
including 'index_end'.`,
|
including 'index_end'.`,
|
||||||
Args: func(cmd *cobra.Command, args []string) error {
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
if err := cobra.RangeArgs(2, 3)(cmd, args); err != nil {
|
if err := cobra.RangeArgs(2, 3)(cmd, args); err != nil { //nolint:gomnd
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ including 'index_end'.`,
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||||
s, err := d.store.Settings.Get()
|
s, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
evt := args[0]
|
evt := args[0]
|
||||||
@ -43,7 +43,7 @@ including 'index_end'.`,
|
|||||||
i, err := strconv.Atoi(args[1])
|
i, err := strconv.Atoi(args[1])
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
f := i
|
f := i
|
||||||
if len(args) == 3 {
|
if len(args) == 3 { //nolint:gomnd
|
||||||
f, err = strconv.Atoi(args[2])
|
f, err = strconv.Atoi(args[2])
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
}
|
}
|
||||||
|
@ -140,7 +140,7 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Auther) {
|
func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Auther) {
|
||||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) //nolint:gomnd
|
||||||
|
|
||||||
fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup)
|
fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup)
|
||||||
fmt.Fprintf(w, "Create User Dir:\t%t\n", set.CreateUserDir)
|
fmt.Fprintf(w, "Create User Dir:\t%t\n", set.CreateUserDir)
|
||||||
|
@ -13,7 +13,7 @@ var configCatCmd = &cobra.Command{
|
|||||||
Short: "Prints the configuration",
|
Short: "Prints the configuration",
|
||||||
Long: `Prints the configuration.`,
|
Long: `Prints the configuration.`,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: python(func(_ *cobra.Command, _ []string, d pythonData) {
|
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||||
set, err := d.store.Settings.Get()
|
set, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
ser, err := d.store.Settings.GetServer()
|
ser, err := d.store.Settings.GetServer()
|
||||||
|
@ -15,7 +15,7 @@ var configExportCmd = &cobra.Command{
|
|||||||
json or yaml file. This exported configuration can be changed,
|
json or yaml file. This exported configuration can be changed,
|
||||||
and imported again with 'config import' command.`,
|
and imported again with 'config import' command.`,
|
||||||
Args: jsonYamlArg,
|
Args: jsonYamlArg,
|
||||||
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||||
settings, err := d.store.Settings.Get()
|
settings, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ database.
|
|||||||
|
|
||||||
The path must be for a json or yaml file.`,
|
The path must be for a json or yaml file.`,
|
||||||
Args: jsonYamlArg,
|
Args: jsonYamlArg,
|
||||||
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||||
var key []byte
|
var key []byte
|
||||||
if d.hadDB {
|
if d.hadDB {
|
||||||
settings, err := d.store.Settings.Get()
|
settings, err := d.store.Settings.Get()
|
||||||
|
@ -22,7 +22,7 @@ this options can be changed in the future with the command
|
|||||||
to the defaults when creating new users and you don't
|
to the defaults when creating new users and you don't
|
||||||
override the options.`,
|
override the options.`,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
|
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||||
defaults := settings.UserDefaults{}
|
defaults := settings.UserDefaults{}
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
getUserDefaults(flags, &defaults, true)
|
getUserDefaults(flags, &defaults, true)
|
||||||
|
@ -16,7 +16,7 @@ var configSetCmd = &cobra.Command{
|
|||||||
Long: `Updates the configuration. Set the flags for the options
|
Long: `Updates the configuration. Set the flags for the options
|
||||||
you want to change. Other options will remain unchanged.`,
|
you want to change. Other options will remain unchanged.`,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
|
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
set, err := d.store.Settings.Get()
|
set, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
|
@ -39,12 +39,12 @@ var docsCmd = &cobra.Command{
|
|||||||
Use: "docs",
|
Use: "docs",
|
||||||
Hidden: true,
|
Hidden: true,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: func(cmd *cobra.Command, _ []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
dir := mustGetString(cmd.Flags(), "path")
|
dir := mustGetString(cmd.Flags(), "path")
|
||||||
generateDocs(rootCmd, dir)
|
generateDocs(rootCmd, dir)
|
||||||
names := []string{}
|
names := []string{}
|
||||||
|
|
||||||
err := filepath.Walk(dir, func(_ string, info os.FileInfo, err error) error {
|
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||||
if err != nil || info.IsDir() {
|
if err != nil || info.IsDir() {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -101,7 +101,7 @@ func generateMarkdown(cmd *cobra.Command, w io.Writer) {
|
|||||||
_, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.UseLine())
|
_, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.UseLine())
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmd.Example != "" {
|
if len(cmd.Example) > 0 {
|
||||||
buf.WriteString("## Examples\n\n")
|
buf.WriteString("## Examples\n\n")
|
||||||
_, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.Example)
|
_, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.Example)
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ var hashCmd = &cobra.Command{
|
|||||||
Short: "Hashes a password",
|
Short: "Hashes a password",
|
||||||
Long: `Hashes a password using bcrypt algorithm.`,
|
Long: `Hashes a password using bcrypt algorithm.`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Run: func(_ *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
pwd, err := users.HashPwd(args[0])
|
pwd, err := users.HashPwd(args[0])
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
fmt.Println(pwd)
|
fmt.Println(pwd)
|
||||||
|
@ -76,7 +76,7 @@ var rootCmd = &cobra.Command{
|
|||||||
Use: "filebrowser",
|
Use: "filebrowser",
|
||||||
Short: "A stylish web-based file browser",
|
Short: "A stylish web-based file browser",
|
||||||
Long: `File Browser CLI lets you create the database to use with File Browser,
|
Long: `File Browser CLI lets you create the database to use with File Browser,
|
||||||
manage your users and all the configurations without accessing the
|
manage your users and all the configurations without acessing the
|
||||||
web interface.
|
web interface.
|
||||||
|
|
||||||
If you've never run File Browser, you'll need to have a database for
|
If you've never run File Browser, you'll need to have a database for
|
||||||
@ -108,9 +108,9 @@ name in caps. So to set "database" via an env variable, you should
|
|||||||
set FB_DATABASE.
|
set FB_DATABASE.
|
||||||
|
|
||||||
Also, if the database path doesn't exist, File Browser will enter into
|
Also, if the database path doesn't exist, File Browser will enter into
|
||||||
the quick setup mode and a new database will be bootstrapped and a new
|
the quick setup mode and a new database will be bootstraped and a new
|
||||||
user created with the credentials from options "username" and "password".`,
|
user created with the credentials from options "username" and "password".`,
|
||||||
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
|
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||||
log.Println(cfgFile)
|
log.Println(cfgFile)
|
||||||
|
|
||||||
if !d.hadDB {
|
if !d.hadDB {
|
||||||
@ -416,8 +416,7 @@ func initConfig() {
|
|||||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||||
|
|
||||||
if err := v.ReadInConfig(); err != nil {
|
if err := v.ReadInConfig(); err != nil {
|
||||||
var configParseError v.ConfigParseError
|
if _, ok := err.(v.ConfigParseError); ok {
|
||||||
if errors.As(err, &configParseError) {
|
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
cfgFile = "No config file used"
|
cfgFile = "No config file used"
|
||||||
|
@ -28,7 +28,7 @@ You can also specify an optional parameter (index_end) so
|
|||||||
you can remove all commands from 'index' to 'index_end',
|
you can remove all commands from 'index' to 'index_end',
|
||||||
including 'index_end'.`,
|
including 'index_end'.`,
|
||||||
Args: func(cmd *cobra.Command, args []string) error {
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
if err := cobra.RangeArgs(1, 2)(cmd, args); err != nil {
|
if err := cobra.RangeArgs(1, 2)(cmd, args); err != nil { //nolint:gomnd
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,7 +44,7 @@ including 'index_end'.`,
|
|||||||
i, err := strconv.Atoi(args[0])
|
i, err := strconv.Atoi(args[0])
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
f := i
|
f := i
|
||||||
if len(args) == 2 {
|
if len(args) == 2 { //nolint:gomnd
|
||||||
f, err = strconv.Atoi(args[1])
|
f, err = strconv.Atoi(args[1])
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ var rulesLsCommand = &cobra.Command{
|
|||||||
Short: "List global rules or user specific rules",
|
Short: "List global rules or user specific rules",
|
||||||
Long: `List global rules or user specific rules.`,
|
Long: `List global rules or user specific rules.`,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
|
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||||
runRules(d.store, cmd, nil, nil)
|
runRules(d.store, cmd, nil, nil)
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ var upgradeCmd = &cobra.Command{
|
|||||||
import share links because they are incompatible with
|
import share links because they are incompatible with
|
||||||
this version.`,
|
this version.`,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: func(cmd *cobra.Command, _ []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
oldDB := mustGetString(flags, "old.database")
|
oldDB := mustGetString(flags, "old.database")
|
||||||
oldConf := mustGetString(flags, "old.config")
|
oldConf := mustGetString(flags, "old.config")
|
||||||
|
@ -26,7 +26,7 @@ var usersCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func printUsers(usrs []*users.User) {
|
func printUsers(usrs []*users.User) {
|
||||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) //nolint:gomnd
|
||||||
fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tS.Click\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock")
|
fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tS.Click\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock")
|
||||||
|
|
||||||
for _, u := range usrs {
|
for _, u := range usrs {
|
||||||
|
@ -15,7 +15,7 @@ var usersAddCmd = &cobra.Command{
|
|||||||
Use: "add <username> <password>",
|
Use: "add <username> <password>",
|
||||||
Short: "Create a new user",
|
Short: "Create a new user",
|
||||||
Long: `Create a new user and add it to the database.`,
|
Long: `Create a new user and add it to the database.`,
|
||||||
Args: cobra.ExactArgs(2),
|
Args: cobra.ExactArgs(2), //nolint:gomnd
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||||
s, err := d.store.Settings.Get()
|
s, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
|
@ -14,7 +14,7 @@ var usersExportCmd = &cobra.Command{
|
|||||||
Long: `Export all users to a json or yaml file. Please indicate the
|
Long: `Export all users to a json or yaml file. Please indicate the
|
||||||
path to the file where you want to write the users.`,
|
path to the file where you want to write the users.`,
|
||||||
Args: jsonYamlArg,
|
Args: jsonYamlArg,
|
||||||
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||||
list, err := d.store.Users.Gets("")
|
list, err := d.store.Users.Gets("")
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ var usersLsCmd = &cobra.Command{
|
|||||||
Run: findUsers,
|
Run: findUsers,
|
||||||
}
|
}
|
||||||
|
|
||||||
var findUsers = python(func(_ *cobra.Command, args []string, d pythonData) {
|
var findUsers = python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||||
var (
|
var (
|
||||||
list []*users.User
|
list []*users.User
|
||||||
user *users.User
|
user *users.User
|
||||||
|
@ -60,7 +60,7 @@ list or set it to 0.`,
|
|||||||
// User exists in DB.
|
// User exists in DB.
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if !overwrite {
|
if !overwrite {
|
||||||
checkErr(errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registered"))
|
checkErr(errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registred"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the usernames mismatch, check if there is another one in the DB
|
// If the usernames mismatch, check if there is another one in the DB
|
||||||
@ -84,6 +84,6 @@ list or set it to 0.`,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func usernameConflictError(username string, originalID, newID uint) error {
|
func usernameConflictError(username string, originalID, newID uint) error {
|
||||||
return fmt.Errorf(`can't import user with ID %d and username "%s" because the username is already registered with the user %d`,
|
return fmt.Errorf(`can't import user with ID %d and username "%s" because the username is already registred with the user %d`,
|
||||||
newID, username, originalID)
|
newID, username, originalID)
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ var usersRmCmd = &cobra.Command{
|
|||||||
Short: "Delete a user by username or id",
|
Short: "Delete a user by username or id",
|
||||||
Long: `Delete a user by username or id`,
|
Long: `Delete a user by username or id`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||||
username, id := parseUsernameOrID(args[0])
|
username, id := parseUsernameOrID(args[0])
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
@ -188,7 +188,7 @@ func cleanUpMapValue(v interface{}) interface{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// convertCmdStrToCmdArray checks if cmd string is blank (whitespace included)
|
// convertCmdStrToCmdArray checks if cmd string is blank (whitespace included)
|
||||||
// then returns empty string array, else returns the split word array of cmd.
|
// then returns empty string array, else returns the splitted word array of cmd.
|
||||||
// This is to ensure the result will never be []string{""}
|
// This is to ensure the result will never be []string{""}
|
||||||
func convertCmdStrToCmdArray(cmd string) []string {
|
func convertCmdStrToCmdArray(cmd string) []string {
|
||||||
var cmdArray []string
|
var cmdArray []string
|
||||||
|
@ -15,7 +15,7 @@ func init() {
|
|||||||
var versionCmd = &cobra.Command{
|
var versionCmd = &cobra.Command{
|
||||||
Use: "version",
|
Use: "version",
|
||||||
Short: "Print the version number",
|
Short: "Print the version number",
|
||||||
Run: func(_ *cobra.Command, _ []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
fmt.Println("File Browser v" + version.Version + "/" + version.CommitSHA)
|
fmt.Println("File Browser v" + version.Version + "/" + version.CommitSHA)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ func New(fs afero.Fs, root string) *FileCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FileCache) Store(_ context.Context, key string, value []byte) error {
|
func (f *FileCache) Store(ctx context.Context, key string, value []byte) error {
|
||||||
mu := f.getScopedLocks(key)
|
mu := f.getScopedLocks(key)
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
@ -48,7 +48,7 @@ func (f *FileCache) Store(_ context.Context, key string, value []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FileCache) Load(_ context.Context, key string) (value []byte, exist bool, err error) {
|
func (f *FileCache) Load(ctx context.Context, key string) (value []byte, exist bool, err error) {
|
||||||
r, ok, err := f.open(key)
|
r, ok, err := f.open(key)
|
||||||
if err != nil || !ok {
|
if err != nil || !ok {
|
||||||
return nil, ok, err
|
return nil, ok, err
|
||||||
@ -62,7 +62,7 @@ func (f *FileCache) Load(_ context.Context, key string) (value []byte, exist boo
|
|||||||
return value, true, nil
|
return value, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FileCache) Delete(_ context.Context, key string) error {
|
func (f *FileCache) Delete(ctx context.Context, key string) error {
|
||||||
mu := f.getScopedLocks(key)
|
mu := f.getScopedLocks(key)
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
|
@ -40,7 +40,7 @@ func TestFileCache(t *testing.T) {
|
|||||||
require.False(t, exists)
|
require.False(t, exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkValue(t *testing.T, ctx context.Context, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) { //nolint:revive
|
func checkValue(t *testing.T, ctx context.Context, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) { //nolint:golint
|
||||||
t.Helper()
|
t.Helper()
|
||||||
// check actual file content
|
// check actual file content
|
||||||
b, err := afero.ReadFile(fs, fileFullPath)
|
b, err := afero.ReadFile(fs, fileFullPath)
|
||||||
|
@ -11,14 +11,14 @@ func NewNoOp() *NoOp {
|
|||||||
return &NoOp{}
|
return &NoOp{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NoOp) Store(_ context.Context, _ string, _ []byte) error {
|
func (n *NoOp) Store(ctx context.Context, key string, value []byte) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NoOp) Load(_ context.Context, _ string) (value []byte, exist bool, err error) {
|
func (n *NoOp) Load(ctx context.Context, key string) (value []byte, exist bool, err error) {
|
||||||
return nil, false, nil
|
return nil, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NoOp) Delete(_ context.Context, _ string) error {
|
func (n *NoOp) Delete(ctx context.Context, key string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
0
docker/root/custom-cont-init.d/20-config → docker/root/etc/cont-init.d/20-config
Executable file → Normal file
0
docker/root/custom-cont-init.d/20-config → docker/root/etc/cont-init.d/20-config
Executable file → Normal file
0
docker/root/etc/services.d/filebrowser/run
Executable file → Normal file
0
docker/root/etc/services.d/filebrowser/run
Executable file → Normal file
@ -6,35 +6,27 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
|
||||||
"hash"
|
"hash"
|
||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
|
||||||
"log"
|
"log"
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
|
|
||||||
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
|
"github.com/filebrowser/filebrowser/v2/errors"
|
||||||
"github.com/filebrowser/filebrowser/v2/rules"
|
"github.com/filebrowser/filebrowser/v2/rules"
|
||||||
)
|
)
|
||||||
|
|
||||||
const PermFile = 0644
|
const PermFile = 0644
|
||||||
const PermDir = 0755
|
const PermDir = 0755
|
||||||
|
|
||||||
var (
|
|
||||||
reSubDirs = regexp.MustCompile("(?i)^sub(s|titles)$")
|
|
||||||
reSubExts = regexp.MustCompile("(?i)(.vtt|.srt|.ass|.ssa)$")
|
|
||||||
)
|
|
||||||
|
|
||||||
// FileInfo describes a file.
|
// FileInfo describes a file.
|
||||||
type FileInfo struct {
|
type FileInfo struct {
|
||||||
*Listing
|
*Listing
|
||||||
@ -76,7 +68,7 @@ type ImageResolution struct {
|
|||||||
// NewFileInfo creates a File object from a path and a given user. This File
|
// NewFileInfo creates a File object from a path and a given user. This File
|
||||||
// object will be automatically filled depending on if it is a directory
|
// object will be automatically filled depending on if it is a directory
|
||||||
// or a file. If it's a video file, it will also detect any subtitles.
|
// or a file. If it's a video file, it will also detect any subtitles.
|
||||||
func NewFileInfo(opts *FileOptions) (*FileInfo, error) {
|
func NewFileInfo(opts FileOptions) (*FileInfo, error) {
|
||||||
if !opts.Checker.Check(opts.Path) {
|
if !opts.Checker.Check(opts.Path) {
|
||||||
return nil, os.ErrPermission
|
return nil, os.ErrPermission
|
||||||
}
|
}
|
||||||
@ -103,7 +95,7 @@ func NewFileInfo(opts *FileOptions) (*FileInfo, error) {
|
|||||||
return file, err
|
return file, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func stat(opts *FileOptions) (*FileInfo, error) {
|
func stat(opts FileOptions) (*FileInfo, error) {
|
||||||
var file *FileInfo
|
var file *FileInfo
|
||||||
|
|
||||||
if lstaterFs, ok := opts.Fs.(afero.Lstater); ok {
|
if lstaterFs, ok := opts.Fs.(afero.Lstater); ok {
|
||||||
@ -166,7 +158,7 @@ func stat(opts *FileOptions) (*FileInfo, error) {
|
|||||||
// algorithm. The checksums data is saved on File object.
|
// algorithm. The checksums data is saved on File object.
|
||||||
func (i *FileInfo) Checksum(algo string) error {
|
func (i *FileInfo) Checksum(algo string) error {
|
||||||
if i.IsDir {
|
if i.IsDir {
|
||||||
return fbErrors.ErrIsDirectory
|
return errors.ErrIsDirectory
|
||||||
}
|
}
|
||||||
|
|
||||||
if i.Checksums == nil {
|
if i.Checksums == nil {
|
||||||
@ -192,7 +184,7 @@ func (i *FileInfo) Checksum(algo string) error {
|
|||||||
case "sha512":
|
case "sha512":
|
||||||
h = sha512.New()
|
h = sha512.New()
|
||||||
default:
|
default:
|
||||||
return fbErrors.ErrInvalidOption
|
return errors.ErrInvalidOption
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = io.Copy(h, reader)
|
_, err = io.Copy(h, reader)
|
||||||
@ -217,6 +209,8 @@ func (i *FileInfo) RealPath() string {
|
|||||||
return i.Path
|
return i.Path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: use constants
|
||||||
|
//
|
||||||
//nolint:goconst
|
//nolint:goconst
|
||||||
func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
|
func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
|
||||||
if IsNamedPipe(i.Mode) {
|
if IsNamedPipe(i.Mode) {
|
||||||
@ -283,8 +277,8 @@ func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculateImageResolution(fSys afero.Fs, filePath string) (*ImageResolution, error) {
|
func calculateImageResolution(fs afero.Fs, filePath string) (*ImageResolution, error) {
|
||||||
file, err := fSys.Open(filePath)
|
file, err := fs.Open(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -316,7 +310,7 @@ func (i *FileInfo) readFirstBytes() []byte {
|
|||||||
|
|
||||||
buffer := make([]byte, 512) //nolint:gomnd
|
buffer := make([]byte, 512) //nolint:gomnd
|
||||||
n, err := reader.Read(buffer)
|
n, err := reader.Read(buffer)
|
||||||
if err != nil && !errors.Is(err, io.EOF) {
|
if err != nil && err != io.EOF {
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
i.Type = "blob"
|
i.Type = "blob"
|
||||||
return nil
|
return nil
|
||||||
@ -334,6 +328,7 @@ func (i *FileInfo) detectSubtitles() {
|
|||||||
ext := filepath.Ext(i.Path)
|
ext := filepath.Ext(i.Path)
|
||||||
|
|
||||||
// detect multiple languages. Base*.vtt
|
// detect multiple languages. Base*.vtt
|
||||||
|
// TODO: give subtitles descriptive names (lang) and track attributes
|
||||||
parentDir := strings.TrimRight(i.Path, i.Name)
|
parentDir := strings.TrimRight(i.Path, i.Name)
|
||||||
var dir []os.FileInfo
|
var dir []os.FileInfo
|
||||||
if len(i.currentDir) > 0 {
|
if len(i.currentDir) > 0 {
|
||||||
@ -348,45 +343,12 @@ func (i *FileInfo) detectSubtitles() {
|
|||||||
|
|
||||||
base := strings.TrimSuffix(i.Name, ext)
|
base := strings.TrimSuffix(i.Name, ext)
|
||||||
for _, f := range dir {
|
for _, f := range dir {
|
||||||
// load all supported subtitles from subs directories
|
if !f.IsDir() && strings.HasPrefix(f.Name(), base) && strings.HasSuffix(f.Name(), ".vtt") {
|
||||||
// should cover all instances of subtitle distributions
|
i.Subtitles = append(i.Subtitles, path.Join(parentDir, f.Name()))
|
||||||
// like tv-shows with multiple episodes in single dir
|
|
||||||
if f.IsDir() && reSubDirs.MatchString(f.Name()) {
|
|
||||||
subsDir := path.Join(parentDir, f.Name())
|
|
||||||
i.loadSubtitles(subsDir, base, true)
|
|
||||||
} else if isSubtitleMatch(f, base) {
|
|
||||||
i.addSubtitle(path.Join(parentDir, f.Name()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *FileInfo) loadSubtitles(subsPath, baseName string, recursive bool) {
|
|
||||||
dir, err := afero.ReadDir(i.Fs, subsPath)
|
|
||||||
if err == nil {
|
|
||||||
for _, f := range dir {
|
|
||||||
if isSubtitleMatch(f, "") {
|
|
||||||
i.addSubtitle(path.Join(subsPath, f.Name()))
|
|
||||||
} else if f.IsDir() && recursive && strings.HasPrefix(f.Name(), baseName) {
|
|
||||||
subsDir := path.Join(subsPath, f.Name())
|
|
||||||
i.loadSubtitles(subsDir, baseName, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsSupportedSubtitle(fileName string) bool {
|
|
||||||
return reSubExts.MatchString(fileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isSubtitleMatch(f fs.FileInfo, baseName string) bool {
|
|
||||||
return !f.IsDir() && strings.HasPrefix(f.Name(), baseName) &&
|
|
||||||
IsSupportedSubtitle(f.Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *FileInfo) addSubtitle(fPath string) {
|
|
||||||
i.Subtitles = append(i.Subtitles, fPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
|
func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
|
||||||
afs := &afero.Afero{Fs: i.Fs}
|
afs := &afero.Afero{Fs: i.Fs}
|
||||||
dir, err := afs.ReadDir(i.Path)
|
dir, err := afs.ReadDir(i.Path)
|
||||||
|
@ -20,6 +20,7 @@ type Listing struct {
|
|||||||
//nolint:goconst
|
//nolint:goconst
|
||||||
func (l Listing) ApplySort() {
|
func (l Listing) ApplySort() {
|
||||||
// Check '.Order' to know how to sort
|
// Check '.Order' to know how to sort
|
||||||
|
// TODO: use enum
|
||||||
if !l.Sorting.Asc {
|
if !l.Sorting.Asc {
|
||||||
switch l.Sorting.By {
|
switch l.Sorting.By {
|
||||||
case "name":
|
case "name":
|
||||||
|
609
files/mime.go
609
files/mime.go
@ -1,609 +0,0 @@
|
|||||||
package files
|
|
||||||
|
|
||||||
// This file contains code primarily sourced from::
|
|
||||||
// github.com/kataras/iris
|
|
||||||
|
|
||||||
import (
|
|
||||||
"mime"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ContentBinaryHeaderValue header value for binary data.
|
|
||||||
ContentBinaryHeaderValue = "application/octet-stream"
|
|
||||||
// ContentWebassemblyHeaderValue header value for web assembly files.
|
|
||||||
ContentWebassemblyHeaderValue = "application/wasm"
|
|
||||||
// ContentHTMLHeaderValue is the string of text/html response header's content type value.
|
|
||||||
ContentHTMLHeaderValue = "text/html"
|
|
||||||
// ContentJSONHeaderValue header value for JSON data.
|
|
||||||
ContentJSONHeaderValue = "application/json"
|
|
||||||
// ContentJSONProblemHeaderValue header value for JSON API problem error.
|
|
||||||
// Read more at: https://tools.ietf.org/html/rfc7807
|
|
||||||
ContentJSONProblemHeaderValue = "application/problem+json"
|
|
||||||
// ContentXMLProblemHeaderValue header value for XML API problem error.
|
|
||||||
// Read more at: https://tools.ietf.org/html/rfc7807
|
|
||||||
ContentXMLProblemHeaderValue = "application/problem+xml"
|
|
||||||
// ContentJavascriptHeaderValue header value for JSONP & Javascript data.
|
|
||||||
ContentJavascriptHeaderValue = "text/javascript"
|
|
||||||
// ContentTextHeaderValue header value for Text data.
|
|
||||||
ContentTextHeaderValue = "text/plain"
|
|
||||||
// ContentXMLHeaderValue header value for XML data.
|
|
||||||
ContentXMLHeaderValue = "text/xml"
|
|
||||||
// ContentXMLUnreadableHeaderValue obsolete header value for XML.
|
|
||||||
ContentXMLUnreadableHeaderValue = "application/xml"
|
|
||||||
// ContentMarkdownHeaderValue custom key/content type, the real is the text/html.
|
|
||||||
ContentMarkdownHeaderValue = "text/markdown"
|
|
||||||
// ContentYAMLHeaderValue header value for YAML data.
|
|
||||||
ContentYAMLHeaderValue = "application/x-yaml"
|
|
||||||
// ContentYAMLTextHeaderValue header value for YAML plain text.
|
|
||||||
ContentYAMLTextHeaderValue = "text/yaml"
|
|
||||||
// ContentProtobufHeaderValue header value for Protobuf messages data.
|
|
||||||
ContentProtobufHeaderValue = "application/x-protobuf"
|
|
||||||
// ContentMsgPackHeaderValue header value for MsgPack data.
|
|
||||||
ContentMsgPackHeaderValue = "application/msgpack"
|
|
||||||
// ContentMsgPack2HeaderValue alternative header value for MsgPack data.
|
|
||||||
ContentMsgPack2HeaderValue = "application/x-msgpack"
|
|
||||||
// ContentFormHeaderValue header value for post form data.
|
|
||||||
ContentFormHeaderValue = "application/x-www-form-urlencoded"
|
|
||||||
// ContentFormMultipartHeaderValue header value for post multipart form data.
|
|
||||||
ContentFormMultipartHeaderValue = "multipart/form-data"
|
|
||||||
// ContentMultipartRelatedHeaderValue header value for multipart related data.
|
|
||||||
ContentMultipartRelatedHeaderValue = "multipart/related"
|
|
||||||
// ContentGRPCHeaderValue Content-Type header value for gRPC.
|
|
||||||
ContentGRPCHeaderValue = "application/grpc"
|
|
||||||
)
|
|
||||||
|
|
||||||
var types = map[string]string{
|
|
||||||
".3dm": "x-world/x-3dmf",
|
|
||||||
".3dmf": "x-world/x-3dmf",
|
|
||||||
".7z": "application/x-7z-compressed",
|
|
||||||
".a": "application/octet-stream",
|
|
||||||
".aab": "application/x-authorware-bin",
|
|
||||||
".aam": "application/x-authorware-map",
|
|
||||||
".aas": "application/x-authorware-seg",
|
|
||||||
".abc": "text/vndabc",
|
|
||||||
".ace": "application/x-ace-compressed",
|
|
||||||
".acgi": "text/html",
|
|
||||||
".afl": "video/animaflex",
|
|
||||||
".ai": "application/postscript",
|
|
||||||
".aif": "audio/aiff",
|
|
||||||
".aifc": "audio/aiff",
|
|
||||||
".aiff": "audio/aiff",
|
|
||||||
".aim": "application/x-aim",
|
|
||||||
".aip": "text/x-audiosoft-intra",
|
|
||||||
".alz": "application/x-alz-compressed",
|
|
||||||
".ani": "application/x-navi-animation",
|
|
||||||
".aos": "application/x-nokia-9000-communicator-add-on-software",
|
|
||||||
".aps": "application/mime",
|
|
||||||
".apk": "application/vnd.android.package-archive",
|
|
||||||
".arc": "application/x-arc-compressed",
|
|
||||||
".arj": "application/arj",
|
|
||||||
".art": "image/x-jg",
|
|
||||||
".asf": "video/x-ms-asf",
|
|
||||||
".asm": "text/x-asm",
|
|
||||||
".asp": "text/asp",
|
|
||||||
".asx": "application/x-mplayer2",
|
|
||||||
".au": "audio/basic",
|
|
||||||
".avi": "video/x-msvideo",
|
|
||||||
".avs": "video/avs-video",
|
|
||||||
".bcpio": "application/x-bcpio",
|
|
||||||
".bin": "application/mac-binary",
|
|
||||||
".bmp": "image/bmp",
|
|
||||||
".boo": "application/book",
|
|
||||||
".book": "application/book",
|
|
||||||
".boz": "application/x-bzip2",
|
|
||||||
".bsh": "application/x-bsh",
|
|
||||||
".bz2": "application/x-bzip2",
|
|
||||||
".bz": "application/x-bzip",
|
|
||||||
".c++": ContentTextHeaderValue,
|
|
||||||
".c": "text/x-c",
|
|
||||||
".cab": "application/vnd.ms-cab-compressed",
|
|
||||||
".cat": "application/vndms-pkiseccat",
|
|
||||||
".cc": "text/x-c",
|
|
||||||
".ccad": "application/clariscad",
|
|
||||||
".cco": "application/x-cocoa",
|
|
||||||
".cdf": "application/cdf",
|
|
||||||
".cer": "application/pkix-cert",
|
|
||||||
".cha": "application/x-chat",
|
|
||||||
".chat": "application/x-chat",
|
|
||||||
".chrt": "application/vnd.kde.kchart",
|
|
||||||
".class": "application/java",
|
|
||||||
".com": ContentTextHeaderValue,
|
|
||||||
".conf": ContentTextHeaderValue,
|
|
||||||
".cpio": "application/x-cpio",
|
|
||||||
".cpp": "text/x-c",
|
|
||||||
".cpt": "application/mac-compactpro",
|
|
||||||
".crl": "application/pkcs-crl",
|
|
||||||
".crt": "application/pkix-cert",
|
|
||||||
".crx": "application/x-chrome-extension",
|
|
||||||
".csh": "text/x-scriptcsh",
|
|
||||||
".css": "text/css",
|
|
||||||
".csv": "text/csv",
|
|
||||||
".cxx": ContentTextHeaderValue,
|
|
||||||
".dar": "application/x-dar",
|
|
||||||
".dcr": "application/x-director",
|
|
||||||
".deb": "application/x-debian-package",
|
|
||||||
".deepv": "application/x-deepv",
|
|
||||||
".def": ContentTextHeaderValue,
|
|
||||||
".der": "application/x-x509-ca-cert",
|
|
||||||
".dif": "video/x-dv",
|
|
||||||
".dir": "application/x-director",
|
|
||||||
".divx": "video/divx",
|
|
||||||
".dl": "video/dl",
|
|
||||||
".dmg": "application/x-apple-diskimage",
|
|
||||||
".doc": "application/msword",
|
|
||||||
".dot": "application/msword",
|
|
||||||
".dp": "application/commonground",
|
|
||||||
".drw": "application/drafting",
|
|
||||||
".dump": "application/octet-stream",
|
|
||||||
".dv": "video/x-dv",
|
|
||||||
".dvi": "application/x-dvi",
|
|
||||||
".dwf": "drawing/x-dwf=(old)",
|
|
||||||
".dwg": "application/acad",
|
|
||||||
".dxf": "application/dxf",
|
|
||||||
".dxr": "application/x-director",
|
|
||||||
".el": "text/x-scriptelisp",
|
|
||||||
".elc": "application/x-bytecodeelisp=(compiled=elisp)",
|
|
||||||
".eml": "message/rfc822",
|
|
||||||
".env": "application/x-envoy",
|
|
||||||
".eps": "application/postscript",
|
|
||||||
".es": "application/x-esrehber",
|
|
||||||
".etx": "text/x-setext",
|
|
||||||
".evy": "application/envoy",
|
|
||||||
".exe": "application/octet-stream",
|
|
||||||
".f77": "text/x-fortran",
|
|
||||||
".f90": "text/x-fortran",
|
|
||||||
".f": "text/x-fortran",
|
|
||||||
".fdf": "application/vndfdf",
|
|
||||||
".fif": "application/fractals",
|
|
||||||
".fli": "video/fli",
|
|
||||||
".flo": "image/florian",
|
|
||||||
".flv": "video/x-flv",
|
|
||||||
".flx": "text/vndfmiflexstor",
|
|
||||||
".fmf": "video/x-atomic3d-feature",
|
|
||||||
".for": "text/x-fortran",
|
|
||||||
".fpx": "image/vndfpx",
|
|
||||||
".frl": "application/freeloader",
|
|
||||||
".funk": "audio/make",
|
|
||||||
".g3": "image/g3fax",
|
|
||||||
".g": ContentTextHeaderValue,
|
|
||||||
".gif": "image/gif",
|
|
||||||
".gl": "video/gl",
|
|
||||||
".gsd": "audio/x-gsm",
|
|
||||||
".gsm": "audio/x-gsm",
|
|
||||||
".gsp": "application/x-gsp",
|
|
||||||
".gss": "application/x-gss",
|
|
||||||
".gtar": "application/x-gtar",
|
|
||||||
".gz": "application/x-compressed",
|
|
||||||
".gzip": "application/x-gzip",
|
|
||||||
".h": "text/x-h",
|
|
||||||
".hdf": "application/x-hdf",
|
|
||||||
".help": "application/x-helpfile",
|
|
||||||
".hgl": "application/vndhp-hpgl",
|
|
||||||
".hh": "text/x-h",
|
|
||||||
".hlb": "text/x-script",
|
|
||||||
".hlp": "application/hlp",
|
|
||||||
".hpg": "application/vndhp-hpgl",
|
|
||||||
".hpgl": "application/vndhp-hpgl",
|
|
||||||
".hqx": "application/binhex",
|
|
||||||
".hta": "application/hta",
|
|
||||||
".htc": "text/x-component",
|
|
||||||
".htm": "text/html",
|
|
||||||
".html": "text/html",
|
|
||||||
".htmls": "text/html",
|
|
||||||
".htt": "text/webviewhtml",
|
|
||||||
".htx": "text/html",
|
|
||||||
".ice": "x-conference/x-cooltalk",
|
|
||||||
".ico": "image/x-icon",
|
|
||||||
".ics": "text/calendar",
|
|
||||||
".icz": "text/calendar",
|
|
||||||
".idc": ContentTextHeaderValue,
|
|
||||||
".ief": "image/ief",
|
|
||||||
".iefs": "image/ief",
|
|
||||||
".iges": "application/iges",
|
|
||||||
".igs": "application/iges",
|
|
||||||
".ima": "application/x-ima",
|
|
||||||
".imap": "application/x-httpd-imap",
|
|
||||||
".inf": "application/inf",
|
|
||||||
".ins": "application/x-internett-signup",
|
|
||||||
".ip": "application/x-ip2",
|
|
||||||
".isu": "video/x-isvideo",
|
|
||||||
".it": "audio/it",
|
|
||||||
".iv": "application/x-inventor",
|
|
||||||
".ivr": "i-world/i-vrml",
|
|
||||||
".ivy": "application/x-livescreen",
|
|
||||||
".jam": "audio/x-jam",
|
|
||||||
".jav": "text/x-java-source",
|
|
||||||
".java": "text/x-java-source",
|
|
||||||
".jcm": "application/x-java-commerce",
|
|
||||||
".jfif-tbnl": "image/jpeg",
|
|
||||||
".jfif": "image/jpeg",
|
|
||||||
".jnlp": "application/x-java-jnlp-file",
|
|
||||||
".jpe": "image/jpeg",
|
|
||||||
".jpeg": "image/jpeg",
|
|
||||||
".jpg": "image/jpeg",
|
|
||||||
".jps": "image/x-jps",
|
|
||||||
".js": ContentJavascriptHeaderValue,
|
|
||||||
".mjs": ContentJavascriptHeaderValue,
|
|
||||||
".json": ContentJSONHeaderValue,
|
|
||||||
".vue": ContentJavascriptHeaderValue,
|
|
||||||
".jut": "image/jutvision",
|
|
||||||
".kar": "audio/midi",
|
|
||||||
".karbon": "application/vnd.kde.karbon",
|
|
||||||
".kfo": "application/vnd.kde.kformula",
|
|
||||||
".flw": "application/vnd.kde.kivio",
|
|
||||||
".kml": "application/vnd.google-earth.kml+xml",
|
|
||||||
".kmz": "application/vnd.google-earth.kmz",
|
|
||||||
".kon": "application/vnd.kde.kontour",
|
|
||||||
".kpr": "application/vnd.kde.kpresenter",
|
|
||||||
".kpt": "application/vnd.kde.kpresenter",
|
|
||||||
".ksp": "application/vnd.kde.kspread",
|
|
||||||
".kwd": "application/vnd.kde.kword",
|
|
||||||
".kwt": "application/vnd.kde.kword",
|
|
||||||
".ksh": "text/x-scriptksh",
|
|
||||||
".la": "audio/nspaudio",
|
|
||||||
".lam": "audio/x-liveaudio",
|
|
||||||
".latex": "application/x-latex",
|
|
||||||
".lha": "application/lha",
|
|
||||||
".lhx": "application/octet-stream",
|
|
||||||
".list": ContentTextHeaderValue,
|
|
||||||
".lma": "audio/nspaudio",
|
|
||||||
".log": ContentTextHeaderValue,
|
|
||||||
".lsp": "text/x-scriptlisp",
|
|
||||||
".lst": ContentTextHeaderValue,
|
|
||||||
".lsx": "text/x-la-asf",
|
|
||||||
".ltx": "application/x-latex",
|
|
||||||
".lzh": "application/octet-stream",
|
|
||||||
".lzx": "application/lzx",
|
|
||||||
".m1v": "video/mpeg",
|
|
||||||
".m2a": "audio/mpeg",
|
|
||||||
".m2v": "video/mpeg",
|
|
||||||
".m3u": "audio/x-mpegurl",
|
|
||||||
".m": "text/x-m",
|
|
||||||
".man": "application/x-troff-man",
|
|
||||||
".manifest": "text/cache-manifest",
|
|
||||||
".map": "application/x-navimap",
|
|
||||||
".mar": ContentTextHeaderValue,
|
|
||||||
".mbd": "application/mbedlet",
|
|
||||||
".mc$": "application/x-magic-cap-package-10",
|
|
||||||
".mcd": "application/mcad",
|
|
||||||
".mcf": "text/mcf",
|
|
||||||
".mcp": "application/netmc",
|
|
||||||
".me": "application/x-troff-me",
|
|
||||||
".mht": "message/rfc822",
|
|
||||||
".mhtml": "message/rfc822",
|
|
||||||
".mid": "application/x-midi",
|
|
||||||
".midi": "application/x-midi",
|
|
||||||
".mif": "application/x-frame",
|
|
||||||
".mime": "message/rfc822",
|
|
||||||
".mjf": "audio/x-vndaudioexplosionmjuicemediafile",
|
|
||||||
".mjpg": "video/x-motion-jpeg",
|
|
||||||
".mm": "application/base64",
|
|
||||||
".mme": "application/base64",
|
|
||||||
".mod": "audio/mod",
|
|
||||||
".moov": "video/quicktime",
|
|
||||||
".mov": "video/quicktime",
|
|
||||||
".movie": "video/x-sgi-movie",
|
|
||||||
".mp2": "audio/mpeg",
|
|
||||||
".mp3": "audio/mpeg",
|
|
||||||
".mp4": "video/mp4",
|
|
||||||
".mpa": "audio/mpeg",
|
|
||||||
".mpc": "application/x-project",
|
|
||||||
".mpe": "video/mpeg",
|
|
||||||
".mpeg": "video/mpeg",
|
|
||||||
".mpg": "video/mpeg",
|
|
||||||
".mpga": "audio/mpeg",
|
|
||||||
".mpp": "application/vndms-project",
|
|
||||||
".mpt": "application/x-project",
|
|
||||||
".mpv": "application/x-project",
|
|
||||||
".mpx": "application/x-project",
|
|
||||||
".mrc": "application/marc",
|
|
||||||
".ms": "application/x-troff-ms",
|
|
||||||
".mv": "video/x-sgi-movie",
|
|
||||||
".my": "audio/make",
|
|
||||||
".mzz": "application/x-vndaudioexplosionmzz",
|
|
||||||
".nap": "image/naplps",
|
|
||||||
".naplps": "image/naplps",
|
|
||||||
".nc": "application/x-netcdf",
|
|
||||||
".ncm": "application/vndnokiaconfiguration-message",
|
|
||||||
".nif": "image/x-niff",
|
|
||||||
".niff": "image/x-niff",
|
|
||||||
".nix": "application/x-mix-transfer",
|
|
||||||
".nsc": "application/x-conference",
|
|
||||||
".nvd": "application/x-navidoc",
|
|
||||||
".o": "application/octet-stream",
|
|
||||||
".oda": "application/oda",
|
|
||||||
".odb": "application/vnd.oasis.opendocument.database",
|
|
||||||
".odc": "application/vnd.oasis.opendocument.chart",
|
|
||||||
".odf": "application/vnd.oasis.opendocument.formula",
|
|
||||||
".odg": "application/vnd.oasis.opendocument.graphics",
|
|
||||||
".odi": "application/vnd.oasis.opendocument.image",
|
|
||||||
".odm": "application/vnd.oasis.opendocument.text-master",
|
|
||||||
".odp": "application/vnd.oasis.opendocument.presentation",
|
|
||||||
".ods": "application/vnd.oasis.opendocument.spreadsheet",
|
|
||||||
".odt": "application/vnd.oasis.opendocument.text",
|
|
||||||
".oga": "audio/ogg",
|
|
||||||
".ogg": "audio/ogg",
|
|
||||||
".ogv": "video/ogg",
|
|
||||||
".omc": "application/x-omc",
|
|
||||||
".omcd": "application/x-omcdatamaker",
|
|
||||||
".omcr": "application/x-omcregerator",
|
|
||||||
".otc": "application/vnd.oasis.opendocument.chart-template",
|
|
||||||
".otf": "application/vnd.oasis.opendocument.formula-template",
|
|
||||||
".otg": "application/vnd.oasis.opendocument.graphics-template",
|
|
||||||
".oth": "application/vnd.oasis.opendocument.text-web",
|
|
||||||
".oti": "application/vnd.oasis.opendocument.image-template",
|
|
||||||
".otm": "application/vnd.oasis.opendocument.text-master",
|
|
||||||
".otp": "application/vnd.oasis.opendocument.presentation-template",
|
|
||||||
".ots": "application/vnd.oasis.opendocument.spreadsheet-template",
|
|
||||||
".ott": "application/vnd.oasis.opendocument.text-template",
|
|
||||||
".p10": "application/pkcs10",
|
|
||||||
".p12": "application/pkcs-12",
|
|
||||||
".p7a": "application/x-pkcs7-signature",
|
|
||||||
".p7c": "application/pkcs7-mime",
|
|
||||||
".p7m": "application/pkcs7-mime",
|
|
||||||
".p7r": "application/x-pkcs7-certreqresp",
|
|
||||||
".p7s": "application/pkcs7-signature",
|
|
||||||
".p": "text/x-pascal",
|
|
||||||
".part": "application/pro_eng",
|
|
||||||
".pas": "text/pascal",
|
|
||||||
".pbm": "image/x-portable-bitmap",
|
|
||||||
".pcl": "application/vndhp-pcl",
|
|
||||||
".pct": "image/x-pict",
|
|
||||||
".pcx": "image/x-pcx",
|
|
||||||
".pdb": "chemical/x-pdb",
|
|
||||||
".pdf": "application/pdf",
|
|
||||||
".pfunk": "audio/make",
|
|
||||||
".pgm": "image/x-portable-graymap",
|
|
||||||
".pic": "image/pict",
|
|
||||||
".pict": "image/pict",
|
|
||||||
".pkg": "application/x-newton-compatible-pkg",
|
|
||||||
".pko": "application/vndms-pkipko",
|
|
||||||
".pl": "text/x-scriptperl",
|
|
||||||
".plx": "application/x-pixclscript",
|
|
||||||
".pm4": "application/x-pagemaker",
|
|
||||||
".pm5": "application/x-pagemaker",
|
|
||||||
".pm": "text/x-scriptperl-module",
|
|
||||||
".png": "image/png",
|
|
||||||
".pnm": "application/x-portable-anymap",
|
|
||||||
".pot": "application/mspowerpoint",
|
|
||||||
".pov": "model/x-pov",
|
|
||||||
".ppa": "application/vndms-powerpoint",
|
|
||||||
".ppm": "image/x-portable-pixmap",
|
|
||||||
".pps": "application/mspowerpoint",
|
|
||||||
".ppt": "application/mspowerpoint",
|
|
||||||
".ppz": "application/mspowerpoint",
|
|
||||||
".pre": "application/x-freelance",
|
|
||||||
".prt": "application/pro_eng",
|
|
||||||
".ps": "application/postscript",
|
|
||||||
".psd": "application/octet-stream",
|
|
||||||
".pvu": "paleovu/x-pv",
|
|
||||||
".pwz": "application/vndms-powerpoint",
|
|
||||||
".py": "text/x-scriptphyton",
|
|
||||||
".pyc": "application/x-bytecodepython",
|
|
||||||
".qcp": "audio/vndqcelp",
|
|
||||||
".qd3": "x-world/x-3dmf",
|
|
||||||
".qd3d": "x-world/x-3dmf",
|
|
||||||
".qif": "image/x-quicktime",
|
|
||||||
".qt": "video/quicktime",
|
|
||||||
".qtc": "video/x-qtc",
|
|
||||||
".qti": "image/x-quicktime",
|
|
||||||
".qtif": "image/x-quicktime",
|
|
||||||
".ra": "audio/x-pn-realaudio",
|
|
||||||
".ram": "audio/x-pn-realaudio",
|
|
||||||
".rar": "application/x-rar-compressed",
|
|
||||||
".ras": "application/x-cmu-raster",
|
|
||||||
".rast": "image/cmu-raster",
|
|
||||||
".rexx": "text/x-scriptrexx",
|
|
||||||
".rf": "image/vndrn-realflash",
|
|
||||||
".rgb": "image/x-rgb",
|
|
||||||
".rm": "application/vndrn-realmedia",
|
|
||||||
".rmi": "audio/mid",
|
|
||||||
".rmm": "audio/x-pn-realaudio",
|
|
||||||
".rmp": "audio/x-pn-realaudio",
|
|
||||||
".rng": "application/ringing-tones",
|
|
||||||
".rnx": "application/vndrn-realplayer",
|
|
||||||
".roff": "application/x-troff",
|
|
||||||
".rp": "image/vndrn-realpix",
|
|
||||||
".rpm": "audio/x-pn-realaudio-plugin",
|
|
||||||
".rt": "text/vndrn-realtext",
|
|
||||||
".rtf": "text/richtext",
|
|
||||||
".rtx": "text/richtext",
|
|
||||||
".rv": "video/vndrn-realvideo",
|
|
||||||
".s": "text/x-asm",
|
|
||||||
".s3m": "audio/s3m",
|
|
||||||
".s7z": "application/x-7z-compressed",
|
|
||||||
".saveme": "application/octet-stream",
|
|
||||||
".sbk": "application/x-tbook",
|
|
||||||
".scm": "text/x-scriptscheme",
|
|
||||||
".sdml": ContentTextHeaderValue,
|
|
||||||
".sdp": "application/sdp",
|
|
||||||
".sdr": "application/sounder",
|
|
||||||
".sea": "application/sea",
|
|
||||||
".set": "application/set",
|
|
||||||
".sgm": "text/x-sgml",
|
|
||||||
".sgml": "text/x-sgml",
|
|
||||||
".sh": "text/x-scriptsh",
|
|
||||||
".shar": "application/x-bsh",
|
|
||||||
".shtml": "text/x-server-parsed-html",
|
|
||||||
".sid": "audio/x-psid",
|
|
||||||
".skd": "application/x-koan",
|
|
||||||
".skm": "application/x-koan",
|
|
||||||
".skp": "application/x-koan",
|
|
||||||
".skt": "application/x-koan",
|
|
||||||
".sit": "application/x-stuffit",
|
|
||||||
".sitx": "application/x-stuffitx",
|
|
||||||
".sl": "application/x-seelogo",
|
|
||||||
".smi": "application/smil",
|
|
||||||
".smil": "application/smil",
|
|
||||||
".snd": "audio/basic",
|
|
||||||
".sol": "application/solids",
|
|
||||||
".spc": "text/x-speech",
|
|
||||||
".spl": "application/futuresplash",
|
|
||||||
".spr": "application/x-sprite",
|
|
||||||
".sprite": "application/x-sprite",
|
|
||||||
".spx": "audio/ogg",
|
|
||||||
".src": "application/x-wais-source",
|
|
||||||
".ssi": "text/x-server-parsed-html",
|
|
||||||
".ssm": "application/streamingmedia",
|
|
||||||
".sst": "application/vndms-pkicertstore",
|
|
||||||
".step": "application/step",
|
|
||||||
".stl": "application/sla",
|
|
||||||
".stp": "application/step",
|
|
||||||
".sv4cpio": "application/x-sv4cpio",
|
|
||||||
".sv4crc": "application/x-sv4crc",
|
|
||||||
".svf": "image/vnddwg",
|
|
||||||
".svg": "image/svg+xml",
|
|
||||||
".svr": "application/x-world",
|
|
||||||
".swf": "application/x-shockwave-flash",
|
|
||||||
".t": "application/x-troff",
|
|
||||||
".talk": "text/x-speech",
|
|
||||||
".tar": "application/x-tar",
|
|
||||||
".tbk": "application/toolbook",
|
|
||||||
".tcl": "text/x-scripttcl",
|
|
||||||
".tcsh": "text/x-scripttcsh",
|
|
||||||
".tex": "application/x-tex",
|
|
||||||
".texi": "application/x-texinfo",
|
|
||||||
".texinfo": "application/x-texinfo",
|
|
||||||
".text": ContentTextHeaderValue,
|
|
||||||
".tgz": "application/gnutar",
|
|
||||||
".tif": "image/tiff",
|
|
||||||
".tiff": "image/tiff",
|
|
||||||
".tr": "application/x-troff",
|
|
||||||
".tsi": "audio/tsp-audio",
|
|
||||||
".tsp": "application/dsptype",
|
|
||||||
".tsv": "text/tab-separated-values",
|
|
||||||
".turbot": "image/florian",
|
|
||||||
".txt": ContentTextHeaderValue,
|
|
||||||
".uil": "text/x-uil",
|
|
||||||
".uni": "text/uri-list",
|
|
||||||
".unis": "text/uri-list",
|
|
||||||
".unv": "application/i-deas",
|
|
||||||
".uri": "text/uri-list",
|
|
||||||
".uris": "text/uri-list",
|
|
||||||
".ustar": "application/x-ustar",
|
|
||||||
".uu": "text/x-uuencode",
|
|
||||||
".uue": "text/x-uuencode",
|
|
||||||
".vcd": "application/x-cdlink",
|
|
||||||
".vcf": "text/x-vcard",
|
|
||||||
".vcard": "text/x-vcard",
|
|
||||||
".vcs": "text/x-vcalendar",
|
|
||||||
".vda": "application/vda",
|
|
||||||
".vdo": "video/vdo",
|
|
||||||
".vew": "application/groupwise",
|
|
||||||
".viv": "video/vivo",
|
|
||||||
".vivo": "video/vivo",
|
|
||||||
".vmd": "application/vocaltec-media-desc",
|
|
||||||
".vmf": "application/vocaltec-media-file",
|
|
||||||
".voc": "audio/voc",
|
|
||||||
".vos": "video/vosaic",
|
|
||||||
".vox": "audio/voxware",
|
|
||||||
".vqe": "audio/x-twinvq-plugin",
|
|
||||||
".vqf": "audio/x-twinvq",
|
|
||||||
".vql": "audio/x-twinvq-plugin",
|
|
||||||
".vrml": "application/x-vrml",
|
|
||||||
".vrt": "x-world/x-vrt",
|
|
||||||
".vsd": "application/x-visio",
|
|
||||||
".vst": "application/x-visio",
|
|
||||||
".vsw": "application/x-visio",
|
|
||||||
".w60": "application/wordperfect60",
|
|
||||||
".w61": "application/wordperfect61",
|
|
||||||
".w6w": "application/msword",
|
|
||||||
".wav": "audio/wav",
|
|
||||||
".wb1": "application/x-qpro",
|
|
||||||
".wbmp": "image/vnd.wap.wbmp",
|
|
||||||
".web": "application/vndxara",
|
|
||||||
".wiz": "application/msword",
|
|
||||||
".wk1": "application/x-123",
|
|
||||||
".wmf": "windows/metafile",
|
|
||||||
".wml": "text/vnd.wap.wml",
|
|
||||||
".wmlc": "application/vnd.wap.wmlc",
|
|
||||||
".wmls": "text/vnd.wap.wmlscript",
|
|
||||||
".wmlsc": "application/vnd.wap.wmlscriptc",
|
|
||||||
".word": "application/msword",
|
|
||||||
".wp5": "application/wordperfect",
|
|
||||||
".wp6": "application/wordperfect",
|
|
||||||
".wp": "application/wordperfect",
|
|
||||||
".wpd": "application/wordperfect",
|
|
||||||
".wq1": "application/x-lotus",
|
|
||||||
".wri": "application/mswrite",
|
|
||||||
".wrl": "application/x-world",
|
|
||||||
".wrz": "model/vrml",
|
|
||||||
".wsc": "text/scriplet",
|
|
||||||
".wsrc": "application/x-wais-source",
|
|
||||||
".wtk": "application/x-wintalk",
|
|
||||||
".x-png": "image/png",
|
|
||||||
".xbm": "image/x-xbitmap",
|
|
||||||
".xdr": "video/x-amt-demorun",
|
|
||||||
".xgz": "xgl/drawing",
|
|
||||||
".xif": "image/vndxiff",
|
|
||||||
".xl": "application/excel",
|
|
||||||
".xla": "application/excel",
|
|
||||||
".xlb": "application/excel",
|
|
||||||
".xlc": "application/excel",
|
|
||||||
".xld": "application/excel",
|
|
||||||
".xlk": "application/excel",
|
|
||||||
".xll": "application/excel",
|
|
||||||
".xlm": "application/excel",
|
|
||||||
".xls": "application/excel",
|
|
||||||
".xlt": "application/excel",
|
|
||||||
".xlv": "application/excel",
|
|
||||||
".xlw": "application/excel",
|
|
||||||
".xm": "audio/xm",
|
|
||||||
".xml": ContentXMLHeaderValue,
|
|
||||||
".xmz": "xgl/movie",
|
|
||||||
".xpix": "application/x-vndls-xpix",
|
|
||||||
".xpm": "image/x-xpixmap",
|
|
||||||
".xsr": "video/x-amt-showrun",
|
|
||||||
".xwd": "image/x-xwd",
|
|
||||||
".xyz": "chemical/x-pdb",
|
|
||||||
".z": "application/x-compress",
|
|
||||||
".zip": "application/zip",
|
|
||||||
".zoo": "application/octet-stream",
|
|
||||||
".zsh": "text/x-scriptzsh",
|
|
||||||
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
||||||
".docm": "application/vnd.ms-word.document.macroEnabled.12",
|
|
||||||
".dotx": "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
|
|
||||||
".dotm": "application/vnd.ms-word.template.macroEnabled.12",
|
|
||||||
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
||||||
".xlsm": "application/vnd.ms-excel.sheet.macroEnabled.12",
|
|
||||||
".xltx": "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
|
|
||||||
".xltm": "application/vnd.ms-excel.template.macroEnabled.12",
|
|
||||||
".xlsb": "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
|
|
||||||
".xlam": "application/vnd.ms-excel.addin.macroEnabled.12",
|
|
||||||
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
||||||
".pptm": "application/vnd.ms-powerpoint.presentation.macroEnabled.12",
|
|
||||||
".ppsx": "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
|
|
||||||
".ppsm": "application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
|
|
||||||
".potx": "application/vnd.openxmlformats-officedocument.presentationml.template",
|
|
||||||
".potm": "application/vnd.ms-powerpoint.template.macroEnabled.12",
|
|
||||||
".ppam": "application/vnd.ms-powerpoint.addin.macroEnabled.12",
|
|
||||||
".sldx": "application/vnd.openxmlformats-officedocument.presentationml.slide",
|
|
||||||
".sldm": "application/vnd.ms-powerpoint.slide.macroEnabled.12",
|
|
||||||
".thmx": "application/vnd.ms-officetheme",
|
|
||||||
".onetoc": "application/onenote",
|
|
||||||
".onetoc2": "application/onenote",
|
|
||||||
".onetmp": "application/onenote",
|
|
||||||
".onepkg": "application/onenote",
|
|
||||||
".xpi": "application/x-xpinstall",
|
|
||||||
".wasm": "application/wasm",
|
|
||||||
".m4a": "audio/mp4",
|
|
||||||
".flac": "audio/x-flac",
|
|
||||||
".amr": "audio/amr",
|
|
||||||
".aac": "audio/aac",
|
|
||||||
".opus": "video/ogg",
|
|
||||||
".m4v": "video/mp4",
|
|
||||||
".mkv": "video/x-matroska",
|
|
||||||
".caf": "audio/x-caf",
|
|
||||||
".m3u8": "application/x-mpegURL",
|
|
||||||
".mpd": "application/dash+xml",
|
|
||||||
".webp": "image/webp",
|
|
||||||
".epub": "application/epub+zip",
|
|
||||||
}
|
|
||||||
|
|
||||||
//nolint:gochecknoinits
|
|
||||||
func init() {
|
|
||||||
for ext, typ := range types {
|
|
||||||
// skip errors
|
|
||||||
_ = mime.AddExtensionType(ext, typ)
|
|
||||||
}
|
|
||||||
}
|
|
@ -98,7 +98,7 @@ func CommonPrefix(sep byte, paths ...string) string {
|
|||||||
// (e.g. /home/user1, /home/user1/foo, /home/user1/bar).
|
// (e.g. /home/user1, /home/user1/foo, /home/user1/bar).
|
||||||
// path.Clean will have cleaned off trailing / separators with
|
// path.Clean will have cleaned off trailing / separators with
|
||||||
// the exception of the root directory, "/" (in which case we
|
// the exception of the root directory, "/" (in which case we
|
||||||
// make it "//", but this will get fixed up to "/" below).
|
// make it "//", but this will get fixed up to "/" bellow).
|
||||||
c = append(c, sep)
|
c = append(c, sep)
|
||||||
|
|
||||||
// Ignore the first path since it's already in c
|
// Ignore the first path since it's already in c
|
||||||
|
20
frontend/.eslintrc.json
Normal file
20
frontend/.eslintrc.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,2 @@
|
|||||||
# Ignore artifacts:
|
# Ignore artifacts:
|
||||||
dist
|
dist
|
||||||
pnpm-lock.yaml
|
|
1
frontend/env.d.ts
vendored
1
frontend/env.d.ts
vendored
@ -1 +0,0 @@
|
|||||||
/// <reference types="vite/client" />
|
|
@ -1,38 +0,0 @@
|
|||||||
import pluginVue from "eslint-plugin-vue";
|
|
||||||
import vueTsEslintConfig from "@vue/eslint-config-typescript";
|
|
||||||
import prettierConfig from "@vue/eslint-config-prettier";
|
|
||||||
|
|
||||||
export default [
|
|
||||||
{
|
|
||||||
name: "app/files-to-lint",
|
|
||||||
files: ["**/*.{ts,mts,tsx,vue}"],
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "app/files-to-ignore",
|
|
||||||
ignores: ["**/dist/**", "**/dist-ssr/**", "**/coverage/**"],
|
|
||||||
},
|
|
||||||
|
|
||||||
...pluginVue.configs["flat/essential"],
|
|
||||||
...vueTsEslintConfig(),
|
|
||||||
prettierConfig,
|
|
||||||
|
|
||||||
{
|
|
||||||
rules: {
|
|
||||||
// Note: you must disable the base rule as it can report incorrect errors
|
|
||||||
"no-unused-expressions": "off",
|
|
||||||
"@typescript-eslint/no-unused-expressions": "off",
|
|
||||||
// TODO: theres too many of these from before ts
|
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
|
||||||
// TODO: finish the ts conversion
|
|
||||||
"vue/block-lang": "off",
|
|
||||||
"vue/multi-word-component-names": "off",
|
|
||||||
"vue/no-mutating-props": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
shallowOnly: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
@ -187,6 +187,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
10
frontend/jsconfig.json
Normal file
10
frontend/jsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
6023
frontend/package-lock.json
generated
Normal file
6023
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,77 +1,64 @@
|
|||||||
{
|
{
|
||||||
"name": "filebrowser-frontend",
|
"name": "filebrowser-frontend",
|
||||||
"version": "3.0.0",
|
"version": "2.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
|
||||||
"node": ">=22.0.0",
|
|
||||||
"pnpm": ">=9.0.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "pnpm run typecheck && vite build",
|
"serve": "vite serve",
|
||||||
|
"build": "vite build",
|
||||||
|
"watch": "vite build --watch",
|
||||||
"clean": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitkeep' -exec rm -r {} +",
|
"clean": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitkeep' -exec rm -r {} +",
|
||||||
"typecheck": "vue-tsc -p ./tsconfig.tsc.json --noEmit",
|
"lint": "eslint --ext .vue,.js src/",
|
||||||
"lint": "eslint src/",
|
"lint:fix": "eslint --ext .vue,.js --fix src/",
|
||||||
"lint:fix": "eslint --fix src/",
|
"format": "prettier --write ."
|
||||||
"format": "prettier --write .",
|
|
||||||
"test": "playwright test"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chenfengyuan/vue-number-input": "^2.0.1",
|
"ace-builds": "^1.23.4",
|
||||||
"@vueuse/core": "^12.5.0",
|
"clipboard": "^2.0.11",
|
||||||
"@vueuse/integrations": "^12.5.0",
|
"core-js": "^3.32.0",
|
||||||
"ace-builds": "^1.37.5",
|
"css-vars-ponyfill": "^2.4.8",
|
||||||
"core-js": "^3.40.0",
|
"filesize": "^10.0.8",
|
||||||
"dayjs": "^1.11.10",
|
"js-base64": "^3.7.5",
|
||||||
"epubjs": "^0.3.93",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"filesize": "^10.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"js-base64": "^3.7.7",
|
"material-icons": "^1.13.9",
|
||||||
"jwt-decode": "^4.0.0",
|
"moment": "^2.29.4",
|
||||||
"lodash-es": "^4.17.21",
|
|
||||||
"marked": "^15.0.6",
|
|
||||||
"material-icons": "^1.13.13",
|
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"pinia": "^2.3.1",
|
"noty": "^3.2.0-beta",
|
||||||
"pretty-bytes": "^6.1.1",
|
"pretty-bytes": "^6.1.1",
|
||||||
"qrcode.vue": "^3.4.1",
|
"qrcode.vue": "^1.7.0",
|
||||||
"tus-js-client": "^4.3.1",
|
"tus-js-client": "^3.1.1",
|
||||||
"utif": "^3.1.0",
|
"utif": "^3.1.0",
|
||||||
"video.js": "^8.21.0",
|
"vue": "^2.7.14",
|
||||||
"videojs-hotkeys": "^0.2.28",
|
"vue-async-computed": "^3.9.0",
|
||||||
"videojs-mobile-ui": "^1.1.1",
|
"vue-i18n": "^8.28.2",
|
||||||
"vue": "^3.4.21",
|
"vue-lazyload": "^1.3.5",
|
||||||
"vue-final-modal": "^4.5.4",
|
"vue-router": "^3.6.5",
|
||||||
"vue-i18n": "^11.1.2",
|
"vue-simple-progress": "^1.1.1",
|
||||||
"vue-lazyload": "^3.0.0",
|
"vuex": "^3.6.2",
|
||||||
"vue-reader": "^1.2.17",
|
"vuex-router-sync": "^5.0.0",
|
||||||
"vue-router": "^4.3.0",
|
"whatwg-fetch": "^3.6.17"
|
||||||
"vue-toastification": "^2.0.0-rc.5"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@intlify/unplugin-vue-i18n": "^6.0.3",
|
"@vitejs/plugin-legacy": "^4.1.1",
|
||||||
"@playwright/test": "^1.50.0",
|
"@vitejs/plugin-vue2": "^2.2.0",
|
||||||
"@tsconfig/node22": "^22.0.0",
|
"@vue/eslint-config-prettier": "^8.0.0",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"autoprefixer": "^10.4.14",
|
||||||
"@types/node": "^22.10.10",
|
"eslint": "^8.46.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.21.0",
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
"@vitejs/plugin-legacy": "^6.0.0",
|
"eslint-plugin-vue": "^9.16.1",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"jsdom": "^22.1.0",
|
||||||
"@vue/eslint-config-prettier": "^10.2.0",
|
"postcss": "^8.4.31",
|
||||||
"@vue/eslint-config-typescript": "^14.3.0",
|
"prettier": "^3.0.1",
|
||||||
"@vue/tsconfig": "^0.7.0",
|
"terser": "^5.19.2",
|
||||||
"autoprefixer": "^10.4.19",
|
"vite": "^4.5.2",
|
||||||
"concurrently": "^9.1.2",
|
"vite-plugin-compression2": "^0.10.3",
|
||||||
"eslint": "^9.19.0",
|
"vite-plugin-rewrite-all": "^1.0.1"
|
||||||
"eslint-plugin-prettier": "^5.2.3",
|
|
||||||
"eslint-plugin-vue": "^9.24.0",
|
|
||||||
"jsdom": "^26.0.0",
|
|
||||||
"postcss": "^8.5.1",
|
|
||||||
"prettier": "^3.4.2",
|
|
||||||
"terser": "^5.37.0",
|
|
||||||
"vite": "^6.0.11",
|
|
||||||
"vite-plugin-compression2": "^1.0.0",
|
|
||||||
"vue-tsc": "^2.2.0"
|
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0"
|
"browserslist": [
|
||||||
|
"> 1%",
|
||||||
|
"last 2 versions",
|
||||||
|
"not ie < 11"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@ -1,80 +0,0 @@
|
|||||||
import { defineConfig, devices } from "@playwright/test";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read environment variables from file.
|
|
||||||
* https://github.com/motdotla/dotenv
|
|
||||||
*/
|
|
||||||
// require('dotenv').config();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* See https://playwright.dev/docs/test-configuration.
|
|
||||||
*/
|
|
||||||
export default defineConfig({
|
|
||||||
testDir: "./tests",
|
|
||||||
/* Run tests in files in parallel */
|
|
||||||
fullyParallel: true,
|
|
||||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
|
||||||
forbidOnly: !!process.env.CI,
|
|
||||||
/* Retry on CI only */
|
|
||||||
retries: process.env.CI ? 2 : 0,
|
|
||||||
/* Opt out of parallel tests on CI. */
|
|
||||||
workers: process.env.CI ? 1 : undefined,
|
|
||||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
|
||||||
reporter: "html",
|
|
||||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
|
||||||
use: {
|
|
||||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
|
||||||
baseURL: "http://127.0.0.1:5173",
|
|
||||||
|
|
||||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
|
||||||
trace: "on-first-retry",
|
|
||||||
|
|
||||||
/* Set default locale to English (US) */
|
|
||||||
locale: "en-US",
|
|
||||||
},
|
|
||||||
|
|
||||||
/* Configure projects for major browsers */
|
|
||||||
projects: [
|
|
||||||
{
|
|
||||||
name: "chromium",
|
|
||||||
use: { ...devices["Desktop Chrome"] },
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "firefox",
|
|
||||||
use: { ...devices["Desktop Firefox"] },
|
|
||||||
},
|
|
||||||
|
|
||||||
// {
|
|
||||||
// name: "webkit",
|
|
||||||
// use: { ...devices["Desktop Safari"] },
|
|
||||||
// },
|
|
||||||
|
|
||||||
/* Test against mobile viewports. */
|
|
||||||
// {
|
|
||||||
// name: 'Mobile Chrome',
|
|
||||||
// use: { ...devices['Pixel 5'] },
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: 'Mobile Safari',
|
|
||||||
// use: { ...devices['iPhone 12'] },
|
|
||||||
// },
|
|
||||||
|
|
||||||
/* Test against branded browsers. */
|
|
||||||
// {
|
|
||||||
// name: 'Microsoft Edge',
|
|
||||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: 'Google Chrome',
|
|
||||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
|
||||||
// },
|
|
||||||
],
|
|
||||||
|
|
||||||
/* Run your local dev server before starting the tests */
|
|
||||||
webServer: {
|
|
||||||
command: "npm run dev",
|
|
||||||
url: "http://127.0.0.1:5173",
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
},
|
|
||||||
});
|
|
5389
frontend/pnpm-lock.yaml
generated
5389
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -16,7 +16,7 @@
|
|||||||
[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]
|
[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]
|
||||||
</title>
|
</title>
|
||||||
|
|
||||||
<meta name="robots" content="noindex,nofollow" />
|
<meta name="robots" content="noindex,nofollow">
|
||||||
|
|
||||||
<link
|
<link
|
||||||
rel="icon"
|
rel="icon"
|
||||||
@ -181,9 +181,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
|
||||||
[{[ if .CSS -]}]
|
[{[ if .Theme -]}]
|
||||||
|
<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>
|
||||||
|
217
frontend/public/themes/dark.css
Normal file
217
frontend/public/themes/dark.css
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
:root {
|
||||||
|
--background: #141D24;
|
||||||
|
--surfacePrimary: #20292F;
|
||||||
|
--surfaceSecondary: #3A4147;
|
||||||
|
--divider: rgba(255, 255, 255, 0.12);
|
||||||
|
--icon: #ffffff;
|
||||||
|
--textPrimary: rgba(255, 255, 255, 0.87);
|
||||||
|
--textSecondary: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading {
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
#loading .spinner div, main .spinner div {
|
||||||
|
background: var(--icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
#login {
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
background: var(--surfacePrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
#search #input {
|
||||||
|
background: var(--surfaceSecondary);
|
||||||
|
border-color: var(--surfacePrimary);
|
||||||
|
}
|
||||||
|
#search #input input::placeholder {
|
||||||
|
color: var(--textSecondary);
|
||||||
|
}
|
||||||
|
#search.active #input {
|
||||||
|
background: var(--surfacePrimary);
|
||||||
|
}
|
||||||
|
#search.active input {
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
#search #result {
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
#search .boxes {
|
||||||
|
background: var(--surfaceSecondary);
|
||||||
|
}
|
||||||
|
#search .boxes h3 {
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
color: var(--textPrimary) !important;
|
||||||
|
}
|
||||||
|
.action:hover {
|
||||||
|
background-color: rgba(255, 255, 255, .1);
|
||||||
|
}
|
||||||
|
.action i {
|
||||||
|
color: var(--icon) !important;
|
||||||
|
}
|
||||||
|
.action .counter {
|
||||||
|
border-color: var(--surfacePrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
nav > div {
|
||||||
|
border-color: var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs {
|
||||||
|
border-color: var(--divider);
|
||||||
|
color: var(--textPrimary) !important;
|
||||||
|
}
|
||||||
|
.breadcrumbs span {
|
||||||
|
color: var(--textPrimary) !important;
|
||||||
|
}
|
||||||
|
.breadcrumbs a:hover {
|
||||||
|
background-color: rgba(255, 255, 255, .1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#listing .item {
|
||||||
|
background: var(--surfacePrimary);
|
||||||
|
color: var(--textPrimary);
|
||||||
|
border-color: var(--divider) !important;
|
||||||
|
}
|
||||||
|
#listing .item i {
|
||||||
|
color: var(--icon);
|
||||||
|
}
|
||||||
|
#listing .item .modified {
|
||||||
|
color: var(--textSecondary);
|
||||||
|
}
|
||||||
|
#listing h2,
|
||||||
|
#listing.list .header span {
|
||||||
|
color: var(--textPrimary) !important;
|
||||||
|
}
|
||||||
|
#listing.list .header span {
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
#listing.list .header i {
|
||||||
|
color: var(--icon);
|
||||||
|
}
|
||||||
|
#listing.list .item.header {
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--surfacePrimary);
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
.button--flat:hover {
|
||||||
|
background: var(--surfaceSecondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard #nav ul li {
|
||||||
|
color: var(--textSecondary);
|
||||||
|
}
|
||||||
|
.dashboard #nav ul li:hover {
|
||||||
|
background: var(--surfaceSecondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h3,
|
||||||
|
.dashboard #nav,
|
||||||
|
.dashboard p label {
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
.card#share input,
|
||||||
|
.card#share select,
|
||||||
|
.input {
|
||||||
|
background: var(--surfaceSecondary);
|
||||||
|
color: var(--textPrimary);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
.input:hover,
|
||||||
|
.input:focus {
|
||||||
|
border-color: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
.input--red {
|
||||||
|
background: #73302D;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input--green {
|
||||||
|
background: #147A41;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard #nav .wrapper,
|
||||||
|
.collapsible {
|
||||||
|
border-color: var(--divider);
|
||||||
|
}
|
||||||
|
.collapsible > label * {
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
table th {
|
||||||
|
color: var(--textSecondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list li:hover {
|
||||||
|
background: var(--surfaceSecondary);
|
||||||
|
}
|
||||||
|
.file-list li:before {
|
||||||
|
color: var(--textSecondary);
|
||||||
|
}
|
||||||
|
.file-list li[aria-selected=true]:before {
|
||||||
|
color: var(--icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell {
|
||||||
|
background: var(--surfacePrimary);
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
.shell__divider {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
.shell__divider:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
.shell__result {
|
||||||
|
border-top: 1px solid var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor-container {
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor-container .bar {
|
||||||
|
background: var(--surfacePrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 736px) {
|
||||||
|
#file-selection {
|
||||||
|
background: var(--surfaceSecondary) !important;
|
||||||
|
}
|
||||||
|
#file-selection span {
|
||||||
|
color: var(--textPrimary) !important;
|
||||||
|
}
|
||||||
|
nav {
|
||||||
|
background: var(--surfaceSecondary) !important;
|
||||||
|
}
|
||||||
|
#dropdown {
|
||||||
|
background: var(--surfaceSecondary) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.share__box {
|
||||||
|
background: var(--surfacePrimary) !important;
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.share__box__element {
|
||||||
|
border-top-color: var(--divider);
|
||||||
|
}
|
@ -4,30 +4,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script>
|
||||||
import { ref, onMounted, watch } from "vue";
|
// eslint-disable-next-line no-undef
|
||||||
import { useI18n } from "vue-i18n";
|
// __webpack_public_path__ = window.FileBrowser.StaticURL + "/";
|
||||||
import { setHtmlLocale } from "./i18n";
|
|
||||||
import { getMediaPreference, getTheme, setTheme } from "./utils/theme";
|
|
||||||
|
|
||||||
const { locale } = useI18n();
|
export default {
|
||||||
|
name: "app",
|
||||||
|
mounted() {
|
||||||
|
const loading = document.getElementById("loading");
|
||||||
|
loading.classList.add("done");
|
||||||
|
|
||||||
const userTheme = ref<UserTheme>(getTheme() || getMediaPreference());
|
setTimeout(function () {
|
||||||
|
loading.parentNode.removeChild(loading);
|
||||||
onMounted(() => {
|
}, 200);
|
||||||
setTheme(userTheme.value);
|
},
|
||||||
setHtmlLocale(locale.value);
|
};
|
||||||
// this might be null during HMR
|
|
||||||
const loading = document.getElementById("loading");
|
|
||||||
loading?.classList.add("done");
|
|
||||||
|
|
||||||
setTimeout(function () {
|
|
||||||
loading?.parentNode?.removeChild(loading);
|
|
||||||
}, 200);
|
|
||||||
});
|
|
||||||
|
|
||||||
// handles ltr/rtl changes
|
|
||||||
watch(locale, (newValue) => {
|
|
||||||
newValue && setHtmlLocale(newValue);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@import "./css/styles.css";
|
||||||
|
</style>
|
||||||
|
@ -1,22 +1,15 @@
|
|||||||
import { removePrefix } from "./utils";
|
import { removePrefix } from "./utils";
|
||||||
import { baseURL } from "@/utils/constants";
|
import { baseURL } from "@/utils/constants";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import store from "@/store";
|
||||||
|
|
||||||
const ssl = window.location.protocol === "https:";
|
const ssl = window.location.protocol === "https:";
|
||||||
const protocol = ssl ? "wss:" : "ws:";
|
const protocol = ssl ? "wss:" : "ws:";
|
||||||
|
|
||||||
export default function command(
|
export default function command(url, command, onmessage, onclose) {
|
||||||
url: string,
|
|
||||||
command: string,
|
|
||||||
onmessage: WebSocket["onmessage"],
|
|
||||||
onclose: WebSocket["onclose"]
|
|
||||||
) {
|
|
||||||
const authStore = useAuthStore();
|
|
||||||
|
|
||||||
url = removePrefix(url);
|
url = removePrefix(url);
|
||||||
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${authStore.jwt}`;
|
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${store.state.jwt}`;
|
||||||
|
|
||||||
const conn = new window.WebSocket(url);
|
let conn = new window.WebSocket(url);
|
||||||
conn.onopen = () => conn.send(command);
|
conn.onopen = () => conn.send(command);
|
||||||
conn.onmessage = onmessage;
|
conn.onmessage = onmessage;
|
||||||
conn.onclose = onclose;
|
conn.onclose = onclose;
|
@ -1,21 +1,19 @@
|
|||||||
import { useAuthStore } from "@/stores/auth";
|
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
|
||||||
import { baseURL } from "@/utils/constants";
|
|
||||||
import { upload as postTus, useTus } from "./tus";
|
|
||||||
import { createURL, fetchURL, removePrefix } from "./utils";
|
import { createURL, fetchURL, removePrefix } from "./utils";
|
||||||
|
import { baseURL } from "@/utils/constants";
|
||||||
|
import store from "@/store";
|
||||||
|
import { upload as postTus, useTus } from "./tus";
|
||||||
|
|
||||||
export async function fetch(url: string) {
|
export async function fetch(url) {
|
||||||
url = removePrefix(url);
|
url = removePrefix(url);
|
||||||
|
|
||||||
const res = await fetchURL(`/api/resources${url}`, {});
|
const res = await fetchURL(`/api/resources${url}`, {});
|
||||||
|
|
||||||
const data = (await res.json()) as Resource;
|
let data = await res.json();
|
||||||
data.url = `/files${url}`;
|
data.url = `/files${url}`;
|
||||||
|
|
||||||
if (data.isDir) {
|
if (data.isDir) {
|
||||||
if (!data.url.endsWith("/")) data.url += "/";
|
if (!data.url.endsWith("/")) data.url += "/";
|
||||||
// Perhaps change the any
|
data.items = data.items.map((item, index) => {
|
||||||
data.items = data.items.map((item: any, index: any) => {
|
|
||||||
item.index = index;
|
item.index = index;
|
||||||
item.url = `${data.url}${encodeURIComponent(item.name)}`;
|
item.url = `${data.url}${encodeURIComponent(item.name)}`;
|
||||||
|
|
||||||
@ -30,12 +28,10 @@ export async function fetch(url: string) {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resourceAction(url: string, method: ApiMethod, content?: any) {
|
async function resourceAction(url, method, content) {
|
||||||
url = removePrefix(url);
|
url = removePrefix(url);
|
||||||
|
|
||||||
const opts: ApiOpts = {
|
let opts = { method };
|
||||||
method,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (content) {
|
if (content) {
|
||||||
opts.body = content;
|
opts.body = content;
|
||||||
@ -46,15 +42,15 @@ async function resourceAction(url: string, method: ApiMethod, content?: any) {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function remove(url: string) {
|
export async function remove(url) {
|
||||||
return resourceAction(url, "DELETE");
|
return resourceAction(url, "DELETE");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function put(url: string, content = "") {
|
export async function put(url, content = "") {
|
||||||
return resourceAction(url, "PUT", content);
|
return resourceAction(url, "PUT", content);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function download(format: any, ...files: string[]) {
|
export function download(format, ...files) {
|
||||||
let url = `${baseURL}/api/raw`;
|
let url = `${baseURL}/api/raw`;
|
||||||
|
|
||||||
if (files.length === 1) {
|
if (files.length === 1) {
|
||||||
@ -62,7 +58,7 @@ export function download(format: any, ...files: string[]) {
|
|||||||
} else {
|
} else {
|
||||||
let arg = "";
|
let arg = "";
|
||||||
|
|
||||||
for (const file of files) {
|
for (let file of files) {
|
||||||
arg += removePrefix(file) + ",";
|
arg += removePrefix(file) + ",";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,20 +71,14 @@ export function download(format: any, ...files: string[]) {
|
|||||||
url += `algo=${format}&`;
|
url += `algo=${format}&`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
if (store.state.jwt) {
|
||||||
if (authStore.jwt) {
|
url += `auth=${store.state.jwt}&`;
|
||||||
url += `auth=${authStore.jwt}&`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.open(url);
|
window.open(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function post(
|
export async function post(url, content = "", overwrite = false, onupload) {
|
||||||
url: string,
|
|
||||||
content: ApiContent = "",
|
|
||||||
overwrite = false,
|
|
||||||
onupload: any = () => {}
|
|
||||||
) {
|
|
||||||
// Use the pre-existing API if:
|
// Use the pre-existing API if:
|
||||||
const useResourcesApi =
|
const useResourcesApi =
|
||||||
// a folder is being created
|
// a folder is being created
|
||||||
@ -103,15 +93,10 @@ export async function post(
|
|||||||
: postTus(url, content, overwrite, onupload);
|
: postTus(url, content, overwrite, onupload);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function postResources(
|
async function postResources(url, content = "", overwrite = false, onupload) {
|
||||||
url: string,
|
|
||||||
content: ApiContent = "",
|
|
||||||
overwrite = false,
|
|
||||||
onupload: any
|
|
||||||
) {
|
|
||||||
url = removePrefix(url);
|
url = removePrefix(url);
|
||||||
|
|
||||||
let bufferContent: ArrayBuffer;
|
let bufferContent;
|
||||||
if (
|
if (
|
||||||
content instanceof Blob &&
|
content instanceof Blob &&
|
||||||
!["http:", "https:"].includes(window.location.protocol)
|
!["http:", "https:"].includes(window.location.protocol)
|
||||||
@ -119,15 +104,14 @@ async function postResources(
|
|||||||
bufferContent = await new Response(content).arrayBuffer();
|
bufferContent = await new Response(content).arrayBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = new XMLHttpRequest();
|
let request = new XMLHttpRequest();
|
||||||
request.open(
|
request.open(
|
||||||
"POST",
|
"POST",
|
||||||
`${baseURL}/api/resources${url}?override=${overwrite}`,
|
`${baseURL}/api/resources${url}?override=${overwrite}`,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
request.setRequestHeader("X-Auth", authStore.jwt);
|
request.setRequestHeader("X-Auth", store.state.jwt);
|
||||||
|
|
||||||
if (typeof onupload === "function") {
|
if (typeof onupload === "function") {
|
||||||
request.upload.onprogress = onupload;
|
request.upload.onprogress = onupload;
|
||||||
@ -151,41 +135,35 @@ async function postResources(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveCopy(
|
function moveCopy(items, copy = false, overwrite = false, rename = false) {
|
||||||
items: any[],
|
let promises = [];
|
||||||
copy = false,
|
|
||||||
overwrite = false,
|
|
||||||
rename = false
|
|
||||||
) {
|
|
||||||
const layoutStore = useLayoutStore();
|
|
||||||
const promises = [];
|
|
||||||
|
|
||||||
for (const item of items) {
|
for (let item of items) {
|
||||||
const from = item.from;
|
const from = item.from;
|
||||||
const to = encodeURIComponent(removePrefix(item.to ?? ""));
|
const to = encodeURIComponent(removePrefix(item.to));
|
||||||
const url = `${from}?action=${
|
const url = `${from}?action=${
|
||||||
copy ? "copy" : "rename"
|
copy ? "copy" : "rename"
|
||||||
}&destination=${to}&override=${overwrite}&rename=${rename}`;
|
}&destination=${to}&override=${overwrite}&rename=${rename}`;
|
||||||
promises.push(resourceAction(url, "PATCH"));
|
promises.push(resourceAction(url, "PATCH"));
|
||||||
}
|
}
|
||||||
layoutStore.closeHovers();
|
|
||||||
return Promise.all(promises);
|
return Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function move(items: any[], overwrite = false, rename = false) {
|
export function move(items, overwrite = false, rename = false) {
|
||||||
return moveCopy(items, false, overwrite, rename);
|
return moveCopy(items, false, overwrite, rename);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function copy(items: any[], overwrite = false, rename = false) {
|
export function copy(items, overwrite = false, rename = false) {
|
||||||
return moveCopy(items, true, overwrite, rename);
|
return moveCopy(items, true, overwrite, rename);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checksum(url: string, algo: ChecksumAlg) {
|
export async function checksum(url, algo) {
|
||||||
const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
|
const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
|
||||||
return (await data.json()).checksums[algo];
|
return (await data.json()).checksums[algo];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDownloadURL(file: ResourceItem, inline: any) {
|
export function getDownloadURL(file, inline) {
|
||||||
const params = {
|
const params = {
|
||||||
...(inline && { inline: "true" }),
|
...(inline && { inline: "true" }),
|
||||||
};
|
};
|
||||||
@ -193,7 +171,7 @@ export function getDownloadURL(file: ResourceItem, inline: any) {
|
|||||||
return createURL("api/raw" + file.path, params);
|
return createURL("api/raw" + file.path, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPreviewURL(file: ResourceItem, size: string) {
|
export function getPreviewURL(file, size) {
|
||||||
const params = {
|
const params = {
|
||||||
inline: "true",
|
inline: "true",
|
||||||
key: Date.parse(file.modified),
|
key: Date.parse(file.modified),
|
||||||
@ -202,15 +180,20 @@ export function getPreviewURL(file: ResourceItem, size: string) {
|
|||||||
return createURL("api/preview/" + size + file.path, params);
|
return createURL("api/preview/" + size + file.path, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSubtitlesURL(file: ResourceItem) {
|
export function getSubtitlesURL(file) {
|
||||||
const params = {
|
const params = {
|
||||||
inline: "true",
|
inline: "true",
|
||||||
};
|
};
|
||||||
|
|
||||||
return file.subtitles?.map((d) => createURL("api/subtitle" + d, params));
|
const subtitles = [];
|
||||||
|
for (const sub of file.subtitles) {
|
||||||
|
subtitles.push(createURL("api/raw" + sub, params));
|
||||||
|
}
|
||||||
|
|
||||||
|
return subtitles;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function usage(url: string) {
|
export async function usage(url) {
|
||||||
url = removePrefix(url);
|
url = removePrefix(url);
|
||||||
|
|
||||||
const res = await fetchURL(`/api/usage${url}`, {});
|
const res = await fetchURL(`/api/usage${url}`, {});
|
@ -1,7 +1,7 @@
|
|||||||
import { fetchURL, removePrefix, createURL } from "./utils";
|
import { fetchURL, removePrefix, createURL } from "./utils";
|
||||||
import { baseURL } from "@/utils/constants";
|
import { baseURL } from "@/utils/constants";
|
||||||
|
|
||||||
export async function fetch(url: string, password: string = "") {
|
export async function fetch(url, password = "") {
|
||||||
url = removePrefix(url);
|
url = removePrefix(url);
|
||||||
|
|
||||||
const res = await fetchURL(
|
const res = await fetchURL(
|
||||||
@ -12,12 +12,12 @@ export async function fetch(url: string, password: string = "") {
|
|||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
const data = (await res.json()) as Resource;
|
let data = await res.json();
|
||||||
data.url = `/share${url}`;
|
data.url = `/share${url}`;
|
||||||
|
|
||||||
if (data.isDir) {
|
if (data.isDir) {
|
||||||
if (!data.url.endsWith("/")) data.url += "/";
|
if (!data.url.endsWith("/")) data.url += "/";
|
||||||
data.items = data.items.map((item: any, index: any) => {
|
data.items = data.items.map((item, index) => {
|
||||||
item.index = index;
|
item.index = index;
|
||||||
item.url = `${data.url}${encodeURIComponent(item.name)}`;
|
item.url = `${data.url}${encodeURIComponent(item.name)}`;
|
||||||
|
|
||||||
@ -32,12 +32,7 @@ export async function fetch(url: string, password: string = "") {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function download(
|
export function download(format, hash, token, ...files) {
|
||||||
format: DownloadFormat,
|
|
||||||
hash: string,
|
|
||||||
token: string,
|
|
||||||
...files: string[]
|
|
||||||
) {
|
|
||||||
let url = `${baseURL}/api/public/dl/${hash}`;
|
let url = `${baseURL}/api/public/dl/${hash}`;
|
||||||
|
|
||||||
if (files.length === 1) {
|
if (files.length === 1) {
|
||||||
@ -45,7 +40,7 @@ export function download(
|
|||||||
} else {
|
} else {
|
||||||
let arg = "";
|
let arg = "";
|
||||||
|
|
||||||
for (const file of files) {
|
for (let file of files) {
|
||||||
arg += encodeURIComponent(file) + ",";
|
arg += encodeURIComponent(file) + ",";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,11 +60,11 @@ export function download(
|
|||||||
window.open(url);
|
window.open(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDownloadURL(res: Resource, inline = false) {
|
export function getDownloadURL(share, inline = false) {
|
||||||
const params = {
|
const params = {
|
||||||
...(inline && { inline: "true" }),
|
...(inline && { inline: "true" }),
|
||||||
...(res.token && { token: res.token }),
|
...(share.token && { token: share.token }),
|
||||||
};
|
};
|
||||||
|
|
||||||
return createURL("api/public/dl/" + res.hash + res.path, params, false);
|
return createURL("api/public/dl/" + share.hash + share.path, params, false);
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import { fetchURL, removePrefix } from "./utils";
|
import { fetchURL, removePrefix } from "./utils";
|
||||||
import url from "../utils/url";
|
import url from "../utils/url";
|
||||||
|
|
||||||
export default async function search(base: string, query: string) {
|
export default async function search(base, query) {
|
||||||
base = removePrefix(base);
|
base = removePrefix(base);
|
||||||
query = encodeURIComponent(query);
|
query = encodeURIComponent(query);
|
||||||
|
|
||||||
@ -9,11 +9,11 @@ export default async function search(base: string, query: string) {
|
|||||||
base += "/";
|
base += "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetchURL(`/api/search${base}?query=${query}`, {});
|
let res = await fetchURL(`/api/search${base}?query=${query}`, {});
|
||||||
|
|
||||||
let data = await res.json();
|
let data = await res.json();
|
||||||
|
|
||||||
data = data.map((item: UploadItem) => {
|
data = data.map((item) => {
|
||||||
item.url = `/files${base}` + url.encodePath(item.path);
|
item.url = `/files${base}` + url.encodePath(item.path);
|
||||||
|
|
||||||
if (item.dir) {
|
if (item.dir) {
|
@ -1,10 +1,10 @@
|
|||||||
import { fetchURL, fetchJSON } from "./utils";
|
import { fetchURL, fetchJSON } from "./utils";
|
||||||
|
|
||||||
export function get() {
|
export function get() {
|
||||||
return fetchJSON<ISettings>(`/api/settings`, {});
|
return fetchJSON(`/api/settings`, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function update(settings: ISettings) {
|
export async function update(settings) {
|
||||||
await fetchURL(`/api/settings`, {
|
await fetchURL(`/api/settings`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify(settings),
|
body: JSON.stringify(settings),
|
@ -1,26 +1,21 @@
|
|||||||
import { fetchURL, fetchJSON, removePrefix, createURL } from "./utils";
|
import { fetchURL, fetchJSON, removePrefix, createURL } from "./utils";
|
||||||
|
|
||||||
export async function list() {
|
export async function list() {
|
||||||
return fetchJSON<Share[]>("/api/shares");
|
return fetchJSON("/api/shares");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get(url: string) {
|
export async function get(url) {
|
||||||
url = removePrefix(url);
|
url = removePrefix(url);
|
||||||
return fetchJSON<Share>(`/api/share${url}`);
|
return fetchJSON(`/api/share${url}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function remove(hash: string) {
|
export async function remove(hash) {
|
||||||
await fetchURL(`/api/share/${hash}`, {
|
await fetchURL(`/api/share/${hash}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function create(
|
export async function create(url, password = "", expires = "", unit = "hours") {
|
||||||
url: string,
|
|
||||||
password = "",
|
|
||||||
expires = "",
|
|
||||||
unit = "hours"
|
|
||||||
) {
|
|
||||||
url = removePrefix(url);
|
url = removePrefix(url);
|
||||||
url = `/api/share${url}`;
|
url = `/api/share${url}`;
|
||||||
if (expires !== "") {
|
if (expires !== "") {
|
||||||
@ -28,11 +23,7 @@ export async function create(
|
|||||||
}
|
}
|
||||||
let body = "{}";
|
let body = "{}";
|
||||||
if (password != "" || expires !== "" || unit !== "hours") {
|
if (password != "" || expires !== "" || unit !== "hours") {
|
||||||
body = JSON.stringify({
|
body = JSON.stringify({ password: password, expires: expires, unit: unit });
|
||||||
password: password,
|
|
||||||
expires: expires.toString(), // backend expects string not number
|
|
||||||
unit: unit,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return fetchJSON(url, {
|
return fetchJSON(url, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -40,6 +31,6 @@ export async function create(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getShareURL(share: Share) {
|
export function getShareURL(share) {
|
||||||
return createURL("share/" + share.hash, {}, false);
|
return createURL("share/" + share.hash, {}, false);
|
||||||
}
|
}
|
@ -1,7 +1,6 @@
|
|||||||
import * as tus from "tus-js-client";
|
import * as tus from "tus-js-client";
|
||||||
import { baseURL, tusEndpoint, tusSettings } from "@/utils/constants";
|
import { baseURL, tusEndpoint, tusSettings } from "@/utils/constants";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import store from "@/store";
|
||||||
import { useUploadStore } from "@/stores/upload";
|
|
||||||
import { removePrefix } from "@/api/utils";
|
import { removePrefix } from "@/api/utils";
|
||||||
import { fetchURL } from "./utils";
|
import { fetchURL } from "./utils";
|
||||||
|
|
||||||
@ -12,13 +11,13 @@ const ALPHA = 0.2;
|
|||||||
const ONE_MINUS_ALPHA = 1 - ALPHA;
|
const ONE_MINUS_ALPHA = 1 - ALPHA;
|
||||||
const RECENT_SPEEDS_LIMIT = 5;
|
const RECENT_SPEEDS_LIMIT = 5;
|
||||||
const MB_DIVISOR = 1024 * 1024;
|
const MB_DIVISOR = 1024 * 1024;
|
||||||
const CURRENT_UPLOAD_LIST: CurrentUploadList = {};
|
const CURRENT_UPLOAD_LIST = {};
|
||||||
|
|
||||||
export async function upload(
|
export async function upload(
|
||||||
filePath: string,
|
filePath,
|
||||||
content: ApiContent = "",
|
content = "",
|
||||||
overwrite = false,
|
overwrite = false,
|
||||||
onupload: any
|
onupload
|
||||||
) {
|
) {
|
||||||
if (!tusSettings) {
|
if (!tusSettings) {
|
||||||
// Shouldn't happen as we check for tus support before calling this function
|
// Shouldn't happen as we check for tus support before calling this function
|
||||||
@ -26,42 +25,36 @@ export async function upload(
|
|||||||
}
|
}
|
||||||
|
|
||||||
filePath = removePrefix(filePath);
|
filePath = removePrefix(filePath);
|
||||||
const resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`;
|
let resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`;
|
||||||
|
|
||||||
await createUpload(resourcePath);
|
await createUpload(resourcePath);
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
return new Promise((resolve, reject) => {
|
||||||
|
let upload = new tus.Upload(content, {
|
||||||
// Exit early because of typescript, tus content can't be a string
|
|
||||||
if (content === "") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return new Promise<void | string>((resolve, reject) => {
|
|
||||||
const upload = new tus.Upload(content, {
|
|
||||||
uploadUrl: `${baseURL}${resourcePath}`,
|
uploadUrl: `${baseURL}${resourcePath}`,
|
||||||
chunkSize: tusSettings.chunkSize,
|
chunkSize: tusSettings.chunkSize,
|
||||||
retryDelays: computeRetryDelays(tusSettings),
|
retryDelays: computeRetryDelays(tusSettings),
|
||||||
parallelUploads: 1,
|
parallelUploads: 1,
|
||||||
storeFingerprintForResuming: false,
|
storeFingerprintForResuming: false,
|
||||||
headers: {
|
headers: {
|
||||||
"X-Auth": authStore.jwt,
|
"X-Auth": store.state.jwt,
|
||||||
},
|
},
|
||||||
onError: function (error) {
|
onError: function (error) {
|
||||||
if (CURRENT_UPLOAD_LIST[filePath].interval) {
|
if (CURRENT_UPLOAD_LIST[filePath].interval) {
|
||||||
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
|
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
|
||||||
}
|
}
|
||||||
delete CURRENT_UPLOAD_LIST[filePath];
|
delete CURRENT_UPLOAD_LIST[filePath];
|
||||||
reject(new Error(`Upload failed: ${error.message}`));
|
reject("Upload failed: " + error);
|
||||||
},
|
},
|
||||||
onProgress: function (bytesUploaded) {
|
onProgress: function (bytesUploaded) {
|
||||||
const fileData = CURRENT_UPLOAD_LIST[filePath];
|
let fileData = CURRENT_UPLOAD_LIST[filePath];
|
||||||
fileData.currentBytesUploaded = bytesUploaded;
|
fileData.currentBytesUploaded = bytesUploaded;
|
||||||
|
|
||||||
if (!fileData.hasStarted) {
|
if (!fileData.hasStarted) {
|
||||||
fileData.hasStarted = true;
|
fileData.hasStarted = true;
|
||||||
fileData.lastProgressTimestamp = Date.now();
|
fileData.lastProgressTimestamp = Date.now();
|
||||||
|
|
||||||
fileData.interval = window.setInterval(() => {
|
fileData.interval = setInterval(() => {
|
||||||
calcProgress(filePath);
|
calcProgress(filePath);
|
||||||
}, SPEED_UPDATE_INTERVAL);
|
}, SPEED_UPDATE_INTERVAL);
|
||||||
}
|
}
|
||||||
@ -86,14 +79,14 @@ export async function upload(
|
|||||||
lastProgressTimestamp: null,
|
lastProgressTimestamp: null,
|
||||||
sumOfRecentSpeeds: 0,
|
sumOfRecentSpeeds: 0,
|
||||||
hasStarted: false,
|
hasStarted: false,
|
||||||
interval: undefined,
|
interval: null,
|
||||||
};
|
};
|
||||||
upload.start();
|
upload.start();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createUpload(resourcePath: string) {
|
async function createUpload(resourcePath) {
|
||||||
const headResp = await fetchURL(resourcePath, {
|
let headResp = await fetchURL(resourcePath, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
});
|
||||||
if (headResp.status !== 201) {
|
if (headResp.status !== 201) {
|
||||||
@ -103,10 +96,10 @@ async function createUpload(resourcePath: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeRetryDelays(tusSettings: TusSettings): number[] | undefined {
|
function computeRetryDelays(tusSettings) {
|
||||||
if (!tusSettings.retryCount || tusSettings.retryCount < 1) {
|
if (!tusSettings.retryCount || tusSettings.retryCount < 1) {
|
||||||
// Disable retries altogether
|
// Disable retries altogether
|
||||||
return undefined;
|
return null;
|
||||||
}
|
}
|
||||||
// The tus client expects our retries as an array with computed backoffs
|
// The tus client expects our retries as an array with computed backoffs
|
||||||
// E.g.: [0, 3000, 5000, 10000, 20000]
|
// E.g.: [0, 3000, 5000, 10000, 20000]
|
||||||
@ -122,7 +115,7 @@ function computeRetryDelays(tusSettings: TusSettings): number[] | undefined {
|
|||||||
return retryDelays;
|
return retryDelays;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function useTus(content: ApiContent) {
|
export async function useTus(content) {
|
||||||
return isTusSupported() && content instanceof Blob;
|
return isTusSupported() && content instanceof Blob;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,34 +123,25 @@ function isTusSupported() {
|
|||||||
return tus.isSupported === true;
|
return tus.isSupported === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeETA(state: ETAState, speed?: number) {
|
function computeETA(state) {
|
||||||
if (state.speedMbyte === 0) {
|
if (state.speedMbyte === 0) {
|
||||||
return Infinity;
|
return Infinity;
|
||||||
}
|
}
|
||||||
const totalSize = state.sizes.reduce(
|
const totalSize = state.sizes.reduce((acc, size) => acc + size, 0);
|
||||||
(acc: number, size: number) => acc + size,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
const uploadedSize = state.progress.reduce(
|
const uploadedSize = state.progress.reduce(
|
||||||
(acc: number, progress: Progress) => {
|
(acc, progress) => acc + progress,
|
||||||
if (typeof progress === "number") {
|
|
||||||
return acc + progress;
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
const remainingSize = totalSize - uploadedSize;
|
const remainingSize = totalSize - uploadedSize;
|
||||||
const speedBytesPerSecond = (speed ?? state.speedMbyte) * 1024 * 1024;
|
const speedBytesPerSecond = state.speedMbyte * 1024 * 1024;
|
||||||
return remainingSize / speedBytesPerSecond;
|
return remainingSize / speedBytesPerSecond;
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeGlobalSpeedAndETA() {
|
function computeGlobalSpeedAndETA() {
|
||||||
const uploadStore = useUploadStore();
|
|
||||||
let totalSpeed = 0;
|
let totalSpeed = 0;
|
||||||
let totalCount = 0;
|
let totalCount = 0;
|
||||||
|
|
||||||
for (const filePath in CURRENT_UPLOAD_LIST) {
|
for (let filePath in CURRENT_UPLOAD_LIST) {
|
||||||
totalSpeed += CURRENT_UPLOAD_LIST[filePath].currentAverageSpeed;
|
totalSpeed += CURRENT_UPLOAD_LIST[filePath].currentAverageSpeed;
|
||||||
totalCount++;
|
totalCount++;
|
||||||
}
|
}
|
||||||
@ -165,43 +149,41 @@ function computeGlobalSpeedAndETA() {
|
|||||||
if (totalCount === 0) return { speed: 0, eta: Infinity };
|
if (totalCount === 0) return { speed: 0, eta: Infinity };
|
||||||
|
|
||||||
const averageSpeed = totalSpeed / totalCount;
|
const averageSpeed = totalSpeed / totalCount;
|
||||||
const averageETA = computeETA(uploadStore, averageSpeed);
|
const averageETA = computeETA(store.state.upload, averageSpeed);
|
||||||
|
|
||||||
return { speed: averageSpeed, eta: averageETA };
|
return { speed: averageSpeed, eta: averageETA };
|
||||||
}
|
}
|
||||||
|
|
||||||
function calcProgress(filePath: string) {
|
function calcProgress(filePath) {
|
||||||
const uploadStore = useUploadStore();
|
let fileData = CURRENT_UPLOAD_LIST[filePath];
|
||||||
const fileData = CURRENT_UPLOAD_LIST[filePath];
|
|
||||||
|
|
||||||
const elapsedTime =
|
let elapsedTime = (Date.now() - fileData.lastProgressTimestamp) / 1000;
|
||||||
(Date.now() - (fileData.lastProgressTimestamp ?? 0)) / 1000;
|
let bytesSinceLastUpdate =
|
||||||
const bytesSinceLastUpdate =
|
|
||||||
fileData.currentBytesUploaded - fileData.initialBytesUploaded;
|
fileData.currentBytesUploaded - fileData.initialBytesUploaded;
|
||||||
const currentSpeed = bytesSinceLastUpdate / MB_DIVISOR / elapsedTime;
|
let currentSpeed = bytesSinceLastUpdate / MB_DIVISOR / elapsedTime;
|
||||||
|
|
||||||
if (fileData.recentSpeeds.length >= RECENT_SPEEDS_LIMIT) {
|
if (fileData.recentSpeeds.length >= RECENT_SPEEDS_LIMIT) {
|
||||||
fileData.sumOfRecentSpeeds -= fileData.recentSpeeds.shift() ?? 0;
|
fileData.sumOfRecentSpeeds -= fileData.recentSpeeds.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
fileData.recentSpeeds.push(currentSpeed);
|
fileData.recentSpeeds.push(currentSpeed);
|
||||||
fileData.sumOfRecentSpeeds += currentSpeed;
|
fileData.sumOfRecentSpeeds += currentSpeed;
|
||||||
|
|
||||||
const avgRecentSpeed =
|
let avgRecentSpeed =
|
||||||
fileData.sumOfRecentSpeeds / fileData.recentSpeeds.length;
|
fileData.sumOfRecentSpeeds / fileData.recentSpeeds.length;
|
||||||
fileData.currentAverageSpeed =
|
fileData.currentAverageSpeed =
|
||||||
ALPHA * avgRecentSpeed + ONE_MINUS_ALPHA * fileData.currentAverageSpeed;
|
ALPHA * avgRecentSpeed + ONE_MINUS_ALPHA * fileData.currentAverageSpeed;
|
||||||
|
|
||||||
const { speed, eta } = computeGlobalSpeedAndETA();
|
const { speed, eta } = computeGlobalSpeedAndETA();
|
||||||
uploadStore.setUploadSpeed(speed);
|
store.commit("setUploadSpeed", speed);
|
||||||
uploadStore.setETA(eta);
|
store.commit("setETA", eta);
|
||||||
|
|
||||||
fileData.initialBytesUploaded = fileData.currentBytesUploaded;
|
fileData.initialBytesUploaded = fileData.currentBytesUploaded;
|
||||||
fileData.lastProgressTimestamp = Date.now();
|
fileData.lastProgressTimestamp = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function abortAllUploads() {
|
export function abortAllUploads() {
|
||||||
for (const filePath in CURRENT_UPLOAD_LIST) {
|
for (let filePath in CURRENT_UPLOAD_LIST) {
|
||||||
if (CURRENT_UPLOAD_LIST[filePath].interval) {
|
if (CURRENT_UPLOAD_LIST[filePath].interval) {
|
||||||
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
|
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
|
||||||
}
|
}
|
@ -1,14 +1,14 @@
|
|||||||
import { fetchURL, fetchJSON, StatusError } from "./utils";
|
import { fetchURL, fetchJSON } from "./utils";
|
||||||
|
|
||||||
export async function getAll() {
|
export async function getAll() {
|
||||||
return fetchJSON<IUser[]>(`/api/users`, {});
|
return fetchJSON(`/api/users`, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get(id: number) {
|
export async function get(id) {
|
||||||
return fetchJSON<IUser>(`/api/users/${id}`, {});
|
return fetchJSON(`/api/users/${id}`, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function create(user: IUser) {
|
export async function create(user) {
|
||||||
const res = await fetchURL(`/api/users`, {
|
const res = await fetchURL(`/api/users`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@ -21,11 +21,9 @@ export async function create(user: IUser) {
|
|||||||
if (res.status === 201) {
|
if (res.status === 201) {
|
||||||
return res.headers.get("Location");
|
return res.headers.get("Location");
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new StatusError(await res.text(), res.status);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function update(user: Partial<IUser>, which = ["all"]) {
|
export async function update(user, which = ["all"]) {
|
||||||
await fetchURL(`/api/users/${user.id}`, {
|
await fetchURL(`/api/users/${user.id}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@ -36,7 +34,7 @@ export async function update(user: Partial<IUser>, which = ["all"]) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function remove(id: number) {
|
export async function remove(id) {
|
||||||
await fetchURL(`/api/users/${id}`, {
|
await fetchURL(`/api/users/${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
});
|
80
frontend/src/api/utils.js
Normal file
80
frontend/src/api/utils.js
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import store from "@/store";
|
||||||
|
import { renew, logout } from "@/utils/auth";
|
||||||
|
import { baseURL } from "@/utils/constants";
|
||||||
|
import { encodePath } from "@/utils/url";
|
||||||
|
|
||||||
|
export async function fetchURL(url, opts, auth = true) {
|
||||||
|
opts = opts || {};
|
||||||
|
opts.headers = opts.headers || {};
|
||||||
|
|
||||||
|
let { headers, ...rest } = opts;
|
||||||
|
let res;
|
||||||
|
try {
|
||||||
|
res = await fetch(`${baseURL}${url}`, {
|
||||||
|
headers: {
|
||||||
|
"X-Auth": store.state.jwt,
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
...rest,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
const error = new Error("000 No connection");
|
||||||
|
error.status = 0;
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth && res.headers.get("X-Renew-Token") === "true") {
|
||||||
|
await renew(store.state.jwt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.status < 200 || res.status > 299) {
|
||||||
|
const error = new Error(await res.text());
|
||||||
|
error.status = res.status;
|
||||||
|
|
||||||
|
if (auth && res.status == 401) {
|
||||||
|
logout();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchJSON(url, opts) {
|
||||||
|
const res = await fetchURL(url, opts);
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
return res.json();
|
||||||
|
} else {
|
||||||
|
throw new Error(res.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removePrefix(url) {
|
||||||
|
url = url.split("/").splice(2).join("/");
|
||||||
|
|
||||||
|
if (url === "") url = "/";
|
||||||
|
if (url[0] !== "/") url = "/" + url;
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createURL(endpoint, params = {}, auth = true) {
|
||||||
|
let prefix = baseURL;
|
||||||
|
if (!prefix.endsWith("/")) {
|
||||||
|
prefix = prefix + "/";
|
||||||
|
}
|
||||||
|
const url = new URL(prefix + encodePath(endpoint), origin);
|
||||||
|
|
||||||
|
const searchParams = {
|
||||||
|
...(auth && { auth: store.state.jwt }),
|
||||||
|
...params,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const key in searchParams) {
|
||||||
|
url.searchParams.set(key, searchParams[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return url.toString();
|
||||||
|
}
|
@ -1,98 +0,0 @@
|
|||||||
import { useAuthStore } from "@/stores/auth";
|
|
||||||
import { renew, logout } from "@/utils/auth";
|
|
||||||
import { baseURL } from "@/utils/constants";
|
|
||||||
import { encodePath } from "@/utils/url";
|
|
||||||
|
|
||||||
export class StatusError extends Error {
|
|
||||||
constructor(
|
|
||||||
message: any,
|
|
||||||
public status?: number
|
|
||||||
) {
|
|
||||||
super(message);
|
|
||||||
this.name = "StatusError";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchURL(
|
|
||||||
url: string,
|
|
||||||
opts: ApiOpts,
|
|
||||||
auth = true
|
|
||||||
): Promise<Response> {
|
|
||||||
const authStore = useAuthStore();
|
|
||||||
|
|
||||||
opts = opts || {};
|
|
||||||
opts.headers = opts.headers || {};
|
|
||||||
|
|
||||||
const { headers, ...rest } = opts;
|
|
||||||
let res;
|
|
||||||
try {
|
|
||||||
res = await fetch(`${baseURL}${url}`, {
|
|
||||||
headers: {
|
|
||||||
"X-Auth": authStore.jwt,
|
|
||||||
...headers,
|
|
||||||
},
|
|
||||||
...rest,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
throw new StatusError("000 No connection", 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auth && res.headers.get("X-Renew-Token") === "true") {
|
|
||||||
await renew(authStore.jwt);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.status < 200 || res.status > 299) {
|
|
||||||
const body = await res.text();
|
|
||||||
const error = new StatusError(
|
|
||||||
body || `${res.status} ${res.statusText}`,
|
|
||||||
res.status
|
|
||||||
);
|
|
||||||
|
|
||||||
if (auth && res.status == 401) {
|
|
||||||
logout();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchJSON<T>(url: string, opts?: any): Promise<T> {
|
|
||||||
const res = await fetchURL(url, opts);
|
|
||||||
|
|
||||||
if (res.status === 200) {
|
|
||||||
return res.json() as Promise<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new StatusError(`${res.status} ${res.statusText}`, res.status);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removePrefix(url: string): string {
|
|
||||||
url = url.split("/").splice(2).join("/");
|
|
||||||
|
|
||||||
if (url === "") url = "/";
|
|
||||||
if (url[0] !== "/") url = "/" + url;
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createURL(endpoint: string, params = {}, auth = true): string {
|
|
||||||
const authStore = useAuthStore();
|
|
||||||
|
|
||||||
let prefix = baseURL;
|
|
||||||
if (!prefix.endsWith("/")) {
|
|
||||||
prefix = prefix + "/";
|
|
||||||
}
|
|
||||||
const url = new URL(prefix + encodePath(endpoint), origin);
|
|
||||||
|
|
||||||
const searchParams: SearchParams = {
|
|
||||||
...(auth && { auth: authStore.jwt }),
|
|
||||||
...params,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const key in searchParams) {
|
|
||||||
url.searchParams.set(key, searchParams[key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return url.toString();
|
|
||||||
}
|
|
@ -3,8 +3,8 @@
|
|||||||
<component
|
<component
|
||||||
:is="element"
|
:is="element"
|
||||||
:to="base || ''"
|
:to="base || ''"
|
||||||
:aria-label="t('files.home')"
|
:aria-label="$t('files.home')"
|
||||||
:title="t('files.home')"
|
:title="$t('files.home')"
|
||||||
>
|
>
|
||||||
<i class="material-icons">home</i>
|
<i class="material-icons">home</i>
|
||||||
</component>
|
</component>
|
||||||
@ -18,66 +18,58 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script>
|
||||||
import { computed } from "vue";
|
export default {
|
||||||
import { useI18n } from "vue-i18n";
|
name: "breadcrumbs",
|
||||||
import { useRoute } from "vue-router";
|
props: ["base", "noLink"],
|
||||||
|
computed: {
|
||||||
|
items() {
|
||||||
|
const relativePath = this.$route.path.replace(this.base, "");
|
||||||
|
let parts = relativePath.split("/");
|
||||||
|
|
||||||
const { t } = useI18n();
|
if (parts[0] === "") {
|
||||||
|
parts.shift();
|
||||||
|
}
|
||||||
|
|
||||||
const route = useRoute();
|
if (parts[parts.length - 1] === "") {
|
||||||
|
parts.pop();
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
let breadcrumbs = [];
|
||||||
base: string;
|
|
||||||
noLink?: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const items = computed(() => {
|
for (let i = 0; i < parts.length; i++) {
|
||||||
const relativePath = route.path.replace(props.base, "");
|
if (i === 0) {
|
||||||
const parts = relativePath.split("/");
|
breadcrumbs.push({
|
||||||
|
name: decodeURIComponent(parts[i]),
|
||||||
|
url: this.base + "/" + parts[i] + "/",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
breadcrumbs.push({
|
||||||
|
name: decodeURIComponent(parts[i]),
|
||||||
|
url: breadcrumbs[i - 1].url + parts[i] + "/",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (parts[0] === "") {
|
if (breadcrumbs.length > 3) {
|
||||||
parts.shift();
|
while (breadcrumbs.length !== 4) {
|
||||||
}
|
breadcrumbs.shift();
|
||||||
|
}
|
||||||
|
|
||||||
if (parts[parts.length - 1] === "") {
|
breadcrumbs[0].name = "...";
|
||||||
parts.pop();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const breadcrumbs: BreadCrumb[] = [];
|
return breadcrumbs;
|
||||||
|
},
|
||||||
|
element() {
|
||||||
|
if (this.noLink !== undefined) {
|
||||||
|
return "span";
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < parts.length; i++) {
|
return "router-link";
|
||||||
if (i === 0) {
|
},
|
||||||
breadcrumbs.push({
|
},
|
||||||
name: decodeURIComponent(parts[i]),
|
};
|
||||||
url: props.base + "/" + parts[i] + "/",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
breadcrumbs.push({
|
|
||||||
name: decodeURIComponent(parts[i]),
|
|
||||||
url: breadcrumbs[i - 1].url + parts[i] + "/",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (breadcrumbs.length > 3) {
|
|
||||||
while (breadcrumbs.length !== 4) {
|
|
||||||
breadcrumbs.shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
breadcrumbs[0].name = "...";
|
|
||||||
}
|
|
||||||
|
|
||||||
return breadcrumbs;
|
|
||||||
});
|
|
||||||
|
|
||||||
const element = computed(() => {
|
|
||||||
if (props.noLink) {
|
|
||||||
return "span";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "router-link";
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="t-container">
|
|
||||||
<span>{{ message }}</span>
|
|
||||||
<button v-if="isReport" class="action" @click.stop="clicked">
|
|
||||||
{{ reportText }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
defineProps<{
|
|
||||||
message: string;
|
|
||||||
reportText?: string;
|
|
||||||
isReport?: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const clicked = () => {
|
|
||||||
window.open("https://github.com/filebrowser/filebrowser/issues/new/choose");
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.t-container {
|
|
||||||
width: 100%;
|
|
||||||
padding: 5px 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.action {
|
|
||||||
text-align: center;
|
|
||||||
height: 40px;
|
|
||||||
padding: 0 10px;
|
|
||||||
margin-left: 20px;
|
|
||||||
border-radius: 5px;
|
|
||||||
color: white;
|
|
||||||
cursor: pointer;
|
|
||||||
border: thin solid currentColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
html[dir="rtl"] .action {
|
|
||||||
margin-left: initial;
|
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,224 +0,0 @@
|
|||||||
<!-- This component taken directly from vue-simple-progress
|
|
||||||
since it didnt support Vue 3 but the component itself does
|
|
||||||
https://raw.githubusercontent.com/dzwillia/vue-simple-progress/master/src/components/Progress.vue -->
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="vue-simple-progress-text"
|
|
||||||
:style="text_style"
|
|
||||||
v-if="text.length > 0 && textPosition == 'top'"
|
|
||||||
>
|
|
||||||
{{ text }}
|
|
||||||
</div>
|
|
||||||
<div class="vue-simple-progress" :style="progress_style">
|
|
||||||
<div
|
|
||||||
class="vue-simple-progress-text"
|
|
||||||
:style="text_style"
|
|
||||||
v-if="text.length > 0 && textPosition == 'middle'"
|
|
||||||
>
|
|
||||||
{{ text }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style="position: relative; left: -9999px"
|
|
||||||
:style="text_style"
|
|
||||||
v-if="text.length > 0 && textPosition == 'inside'"
|
|
||||||
>
|
|
||||||
{{ text }}
|
|
||||||
</div>
|
|
||||||
<div class="vue-simple-progress-bar" :style="bar_style">
|
|
||||||
<div
|
|
||||||
:style="text_style"
|
|
||||||
v-if="text.length > 0 && textPosition == 'inside'"
|
|
||||||
>
|
|
||||||
{{ text }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="vue-simple-progress-text"
|
|
||||||
:style="text_style"
|
|
||||||
v-if="text.length > 0 && textPosition == 'bottom'"
|
|
||||||
>
|
|
||||||
{{ text }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// We're leaving this untouched as you can read in the beginning
|
|
||||||
const isNumber = function (n) {
|
|
||||||
return !isNaN(parseFloat(n)) && isFinite(n);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "progress-bar",
|
|
||||||
props: {
|
|
||||||
val: {
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
max: {
|
|
||||||
default: 100,
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
// either a number (pixel width/height) or 'tiny', 'small',
|
|
||||||
// 'medium', 'large', 'huge', 'massive' for common sizes
|
|
||||||
default: 3,
|
|
||||||
},
|
|
||||||
"bg-color": {
|
|
||||||
type: String,
|
|
||||||
default: "#eee",
|
|
||||||
},
|
|
||||||
"bar-color": {
|
|
||||||
type: String,
|
|
||||||
default: "#2196f3", // match .blue color to Material Design's 'Blue 500' color
|
|
||||||
},
|
|
||||||
"bar-transition": {
|
|
||||||
type: String,
|
|
||||||
default: "all 0.5s ease",
|
|
||||||
},
|
|
||||||
"bar-border-radius": {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
spacing: {
|
|
||||||
type: Number,
|
|
||||||
default: 4,
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
"text-align": {
|
|
||||||
type: String,
|
|
||||||
default: "center", // 'left', 'right'
|
|
||||||
},
|
|
||||||
"text-position": {
|
|
||||||
type: String,
|
|
||||||
default: "bottom", // 'bottom', 'top', 'middle', 'inside'
|
|
||||||
},
|
|
||||||
"font-size": {
|
|
||||||
type: Number,
|
|
||||||
default: 13,
|
|
||||||
},
|
|
||||||
"text-fg-color": {
|
|
||||||
type: String,
|
|
||||||
default: "#222",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
pct() {
|
|
||||||
let pct = (this.val / this.max) * 100;
|
|
||||||
pct = pct.toFixed(2);
|
|
||||||
return Math.min(pct, this.max);
|
|
||||||
},
|
|
||||||
size_px() {
|
|
||||||
switch (this.size) {
|
|
||||||
case "tiny":
|
|
||||||
return 2;
|
|
||||||
case "small":
|
|
||||||
return 4;
|
|
||||||
case "medium":
|
|
||||||
return 8;
|
|
||||||
case "large":
|
|
||||||
return 12;
|
|
||||||
case "big":
|
|
||||||
return 16;
|
|
||||||
case "huge":
|
|
||||||
return 32;
|
|
||||||
case "massive":
|
|
||||||
return 64;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isNumber(this.size) ? this.size : 32;
|
|
||||||
},
|
|
||||||
text_padding() {
|
|
||||||
switch (this.size) {
|
|
||||||
case "tiny":
|
|
||||||
case "small":
|
|
||||||
case "medium":
|
|
||||||
case "large":
|
|
||||||
case "big":
|
|
||||||
case "huge":
|
|
||||||
case "massive":
|
|
||||||
return Math.min(Math.max(Math.ceil(this.size_px / 8), 3), 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
return isNumber(this.spacing) ? this.spacing : 4;
|
|
||||||
},
|
|
||||||
text_font_size() {
|
|
||||||
switch (this.size) {
|
|
||||||
case "tiny":
|
|
||||||
case "small":
|
|
||||||
case "medium":
|
|
||||||
case "large":
|
|
||||||
case "big":
|
|
||||||
case "huge":
|
|
||||||
case "massive":
|
|
||||||
return Math.min(Math.max(Math.ceil(this.size_px * 1.4), 11), 32);
|
|
||||||
}
|
|
||||||
|
|
||||||
return isNumber(this.fontSize) ? this.fontSize : 13;
|
|
||||||
},
|
|
||||||
progress_style() {
|
|
||||||
const style = {
|
|
||||||
background: this.bgColor,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.textPosition == "middle" || this.textPosition == "inside") {
|
|
||||||
style["position"] = "relative";
|
|
||||||
style["min-height"] = this.size_px + "px";
|
|
||||||
style["z-index"] = "-2";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.barBorderRadius > 0) {
|
|
||||||
style["border-radius"] = this.barBorderRadius + "px";
|
|
||||||
}
|
|
||||||
|
|
||||||
return style;
|
|
||||||
},
|
|
||||||
bar_style() {
|
|
||||||
const style = {
|
|
||||||
background: this.barColor,
|
|
||||||
width: this.pct + "%",
|
|
||||||
height: this.size_px + "px",
|
|
||||||
transition: this.barTransition,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.barBorderRadius > 0) {
|
|
||||||
style["border-radius"] = this.barBorderRadius + "px";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.textPosition == "middle" || this.textPosition == "inside") {
|
|
||||||
style["position"] = "absolute";
|
|
||||||
style["top"] = "0";
|
|
||||||
style["height"] = "100%";
|
|
||||||
(style["min-height"] = this.size_px + "px"), (style["z-index"] = "-1");
|
|
||||||
}
|
|
||||||
|
|
||||||
return style;
|
|
||||||
},
|
|
||||||
text_style() {
|
|
||||||
const style = {
|
|
||||||
color: this.textFgColor,
|
|
||||||
"font-size": this.text_font_size + "px",
|
|
||||||
"text-align": this.textAlign,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.textPosition == "top" ||
|
|
||||||
this.textPosition == "middle" ||
|
|
||||||
this.textPosition == "inside"
|
|
||||||
)
|
|
||||||
style["padding-bottom"] = this.text_padding + "px";
|
|
||||||
if (
|
|
||||||
this.textPosition == "bottom" ||
|
|
||||||
this.textPosition == "middle" ||
|
|
||||||
this.textPosition == "inside"
|
|
||||||
)
|
|
||||||
style["padding-top"] = this.text_padding + "px";
|
|
||||||
|
|
||||||
return style;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
@ -17,7 +17,7 @@
|
|||||||
@keyup.enter="submit"
|
@keyup.enter="submit"
|
||||||
ref="input"
|
ref="input"
|
||||||
:autofocus="active"
|
:autofocus="active"
|
||||||
v-model.trim="prompt"
|
v-model.trim="value"
|
||||||
:aria-label="$t('search.search')"
|
:aria-label="$t('search.search')"
|
||||||
:placeholder="$t('search.search')"
|
:placeholder="$t('search.search')"
|
||||||
/>
|
/>
|
||||||
@ -28,7 +28,7 @@
|
|||||||
<template v-if="isEmpty">
|
<template v-if="isEmpty">
|
||||||
<p>{{ text }}</p>
|
<p>{{ text }}</p>
|
||||||
|
|
||||||
<template v-if="prompt.length === 0">
|
<template v-if="value.length === 0">
|
||||||
<div class="boxes">
|
<div class="boxes">
|
||||||
<h3>{{ $t("search.types") }}</h3>
|
<h3>{{ $t("search.types") }}</h3>
|
||||||
<div>
|
<div>
|
||||||
@ -49,7 +49,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<ul v-show="results.length > 0">
|
<ul v-show="results.length > 0">
|
||||||
<li v-for="(s, k) in filteredResults" :key="k">
|
<li v-for="(s, k) in filteredResults" :key="k">
|
||||||
<router-link v-on:click="close" :to="s.url">
|
<router-link @click.native="close" :to="s.url">
|
||||||
<i v-if="s.dir" class="material-icons">folder</i>
|
<i v-if="s.dir" class="material-icons">folder</i>
|
||||||
<i v-else class="material-icons">insert_drive_file</i>
|
<i v-else class="material-icons">insert_drive_file</i>
|
||||||
<span>./{{ s.path }}</span>
|
<span>./{{ s.path }}</span>
|
||||||
@ -64,155 +64,138 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script>
|
||||||
import { useFileStore } from "@/stores/file";
|
import { mapState, mapGetters, mapMutations } from "vuex";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
|
||||||
|
|
||||||
import url from "@/utils/url";
|
import url from "@/utils/url";
|
||||||
import { search } from "@/api";
|
import { search } from "@/api";
|
||||||
import { computed, inject, onMounted, ref, watch } from "vue";
|
|
||||||
import { useI18n } from "vue-i18n";
|
|
||||||
import { useRoute } from "vue-router";
|
|
||||||
import { storeToRefs } from "pinia";
|
|
||||||
|
|
||||||
const boxes = {
|
var boxes = {
|
||||||
image: { label: "images", icon: "insert_photo" },
|
image: { label: "images", icon: "insert_photo" },
|
||||||
audio: { label: "music", icon: "volume_up" },
|
audio: { label: "music", icon: "volume_up" },
|
||||||
video: { label: "video", icon: "movie" },
|
video: { label: "video", icon: "movie" },
|
||||||
pdf: { label: "pdf", icon: "picture_as_pdf" },
|
pdf: { label: "pdf", icon: "picture_as_pdf" },
|
||||||
};
|
};
|
||||||
|
|
||||||
const layoutStore = useLayoutStore();
|
export default {
|
||||||
const fileStore = useFileStore();
|
name: "search",
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
value: "",
|
||||||
|
active: false,
|
||||||
|
ongoing: false,
|
||||||
|
results: [],
|
||||||
|
reload: false,
|
||||||
|
resultsCount: 50,
|
||||||
|
scrollable: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
currentPrompt(val, old) {
|
||||||
|
this.active = val?.prompt === "search";
|
||||||
|
|
||||||
const { currentPromptName } = storeToRefs(layoutStore);
|
if (old?.prompt === "search" && !this.active) {
|
||||||
|
if (this.reload) {
|
||||||
|
this.setReload(true);
|
||||||
|
}
|
||||||
|
|
||||||
const prompt = ref<string>("");
|
document.body.style.overflow = "auto";
|
||||||
const active = ref<boolean>(false);
|
this.reset();
|
||||||
const ongoing = ref<boolean>(false);
|
this.value = "";
|
||||||
const results = ref<any[]>([]);
|
this.active = false;
|
||||||
const reload = ref<boolean>(false);
|
this.$refs.input.blur();
|
||||||
const resultsCount = ref<number>(50);
|
} else if (this.active) {
|
||||||
|
this.reload = false;
|
||||||
|
this.$refs.input.focus();
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
value() {
|
||||||
|
if (this.results.length) {
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(["user"]),
|
||||||
|
...mapGetters(["isListing", "currentPrompt"]),
|
||||||
|
boxes() {
|
||||||
|
return boxes;
|
||||||
|
},
|
||||||
|
isEmpty() {
|
||||||
|
return this.results.length === 0;
|
||||||
|
},
|
||||||
|
text() {
|
||||||
|
if (this.ongoing) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
const $showError = inject<IToastError>("$showError")!;
|
return this.value === ""
|
||||||
|
? this.$t("search.typeToSearch")
|
||||||
|
: this.$t("search.pressToSearch");
|
||||||
|
},
|
||||||
|
filteredResults() {
|
||||||
|
return this.results.slice(0, this.resultsCount);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$refs.result.addEventListener("scroll", (event) => {
|
||||||
|
if (
|
||||||
|
event.target.offsetHeight + event.target.scrollTop >=
|
||||||
|
event.target.scrollHeight - 100
|
||||||
|
) {
|
||||||
|
this.resultsCount += 50;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapMutations(["showHover", "closeHovers", "setReload"]),
|
||||||
|
open() {
|
||||||
|
this.showHover("search");
|
||||||
|
},
|
||||||
|
close(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
this.closeHovers();
|
||||||
|
},
|
||||||
|
keyup(event) {
|
||||||
|
if (event.keyCode === 27) {
|
||||||
|
this.close(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const input = ref<HTMLInputElement | null>(null);
|
this.results.length = 0;
|
||||||
const result = ref<HTMLElement | null>(null);
|
},
|
||||||
|
init(string) {
|
||||||
|
this.value = `${string} `;
|
||||||
|
this.$refs.input.focus();
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
this.ongoing = false;
|
||||||
|
this.resultsCount = 50;
|
||||||
|
this.results = [];
|
||||||
|
},
|
||||||
|
async submit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
const { t } = useI18n();
|
if (this.value === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const route = useRoute();
|
let path = this.$route.path;
|
||||||
|
if (!this.isListing) {
|
||||||
|
path = url.removeLastDir(path) + "/";
|
||||||
|
}
|
||||||
|
|
||||||
watch(currentPromptName, (newVal, oldVal) => {
|
this.ongoing = true;
|
||||||
active.value = newVal === "search";
|
|
||||||
|
|
||||||
if (oldVal === "search" && !active.value) {
|
try {
|
||||||
if (reload.value) {
|
this.results = await search(path, this.value);
|
||||||
fileStore.reload = true;
|
} catch (error) {
|
||||||
}
|
this.$showError(error);
|
||||||
|
}
|
||||||
|
|
||||||
document.body.style.overflow = "auto";
|
this.ongoing = false;
|
||||||
reset();
|
},
|
||||||
prompt.value = "";
|
},
|
||||||
active.value = false;
|
|
||||||
input.value?.blur();
|
|
||||||
} else if (active.value) {
|
|
||||||
reload.value = false;
|
|
||||||
input.value?.focus();
|
|
||||||
document.body.style.overflow = "hidden";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(prompt, () => {
|
|
||||||
if (results.value.length) {
|
|
||||||
reset();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ...mapState(useFileStore, ["isListing"]),
|
|
||||||
// ...mapState(useLayoutStore, ["show"]),
|
|
||||||
// ...mapWritableState(useFileStore, { sReload: "reload" }),
|
|
||||||
|
|
||||||
const isEmpty = computed(() => {
|
|
||||||
return results.value.length === 0;
|
|
||||||
});
|
|
||||||
const text = computed(() => {
|
|
||||||
if (ongoing.value) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return prompt.value === ""
|
|
||||||
? t("search.typeToSearch")
|
|
||||||
: t("search.pressToSearch");
|
|
||||||
});
|
|
||||||
const filteredResults = computed(() => {
|
|
||||||
return results.value.slice(0, resultsCount.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (result.value === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
result.value.addEventListener("scroll", (event: Event) => {
|
|
||||||
if (
|
|
||||||
(event.target as HTMLElement).offsetHeight +
|
|
||||||
(event.target as HTMLElement).scrollTop >=
|
|
||||||
(event.target as HTMLElement).scrollHeight - 100
|
|
||||||
) {
|
|
||||||
resultsCount.value += 50;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const open = () => {
|
|
||||||
!active.value && layoutStore.showHover("search");
|
|
||||||
};
|
|
||||||
|
|
||||||
const close = (event: Event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
layoutStore.closeHovers();
|
|
||||||
};
|
|
||||||
|
|
||||||
const keyup = (event: KeyboardEvent) => {
|
|
||||||
if (event.key === "Escape") {
|
|
||||||
close(event);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
results.value.length = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const init = (string: string) => {
|
|
||||||
prompt.value = `${string} `;
|
|
||||||
input.value !== null ? input.value.focus() : "";
|
|
||||||
};
|
|
||||||
|
|
||||||
const reset = () => {
|
|
||||||
ongoing.value = false;
|
|
||||||
resultsCount.value = 50;
|
|
||||||
results.value = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
const submit = async (event: Event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
if (prompt.value === "") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let path = route.path;
|
|
||||||
if (!fileStore.isListing) {
|
|
||||||
path = url.removeLastDir(path) + "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
ongoing.value = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
results.value = await search(path, prompt.value);
|
|
||||||
} catch (error: any) {
|
|
||||||
$showError(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
ongoing.value = false;
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -29,9 +29,9 @@
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
ref="input"
|
ref="input"
|
||||||
class="shell__text"
|
class="shell__text"
|
||||||
:contenteditable="true"
|
contenteditable="true"
|
||||||
@keydown.prevent.arrow-up="historyUp"
|
@keydown.prevent.38="historyUp"
|
||||||
@keydown.prevent.arrow-down="historyDown"
|
@keydown.prevent.40="historyDown"
|
||||||
@keypress.prevent.enter="submit"
|
@keypress.prevent.enter="submit"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -45,19 +45,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState, mapActions } from "pinia";
|
import { mapMutations, mapState, mapGetters } from "vuex";
|
||||||
import { useFileStore } from "@/stores/file";
|
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
|
||||||
|
|
||||||
import { commands } from "@/api";
|
import { commands } from "@/api";
|
||||||
import { throttle } from "lodash-es";
|
import { throttle } from "lodash";
|
||||||
import { theme } from "@/utils/constants";
|
import { theme } from "@/utils/constants";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "shell",
|
name: "shell",
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useLayoutStore, ["showShell"]),
|
...mapState(["user", "showShell"]),
|
||||||
...mapState(useFileStore, ["isFiles"]),
|
...mapGetters(["isFiles", "isLogged"]),
|
||||||
path: function () {
|
path: function () {
|
||||||
if (this.isFiles) {
|
if (this.isFiles) {
|
||||||
return this.$route.path;
|
return this.$route.path;
|
||||||
@ -78,11 +75,11 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
window.addEventListener("resize", this.resize);
|
window.addEventListener("resize", this.resize);
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeDestroy() {
|
||||||
window.removeEventListener("resize", this.resize);
|
window.removeEventListener("resize", this.resize);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useLayoutStore, ["toggleShell"]),
|
...mapMutations(["toggleShell"]),
|
||||||
checkTheme() {
|
checkTheme() {
|
||||||
if (theme == "dark") {
|
if (theme == "dark") {
|
||||||
return "rgba(255, 255, 255, 0.4)";
|
return "rgba(255, 255, 255, 0.4)";
|
||||||
@ -163,7 +160,7 @@ export default {
|
|||||||
this.canInput = false;
|
this.canInput = false;
|
||||||
event.target.innerHTML = "";
|
event.target.innerHTML = "";
|
||||||
|
|
||||||
const results = {
|
let results = {
|
||||||
text: `${cmd}\n\n`,
|
text: `${cmd}\n\n`,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -180,7 +177,7 @@ export default {
|
|||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
results.text = results.text
|
results.text = results.text
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
.replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now
|
.replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now
|
||||||
.trimEnd();
|
.trimEnd();
|
||||||
this.canInput = true;
|
this.canInput = true;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-show="active" @click="closeHovers" class="overlay"></div>
|
|
||||||
<nav :class="{ active }">
|
<nav :class="{ active }">
|
||||||
<template v-if="isLoggedIn">
|
<template v-if="isLogged">
|
||||||
<button
|
<button
|
||||||
class="action"
|
class="action"
|
||||||
@click="toRoot"
|
@click="toRoot"
|
||||||
@ -14,7 +13,7 @@
|
|||||||
|
|
||||||
<div v-if="user.perm.create">
|
<div v-if="user.perm.create">
|
||||||
<button
|
<button
|
||||||
@click="showHover('newDir')"
|
@click="$store.commit('showHover', 'newDir')"
|
||||||
class="action"
|
class="action"
|
||||||
:aria-label="$t('sidebar.newFolder')"
|
:aria-label="$t('sidebar.newFolder')"
|
||||||
:title="$t('sidebar.newFolder')"
|
:title="$t('sidebar.newFolder')"
|
||||||
@ -24,7 +23,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="showHover('newFile')"
|
@click="$store.commit('showHover', 'newFile')"
|
||||||
class="action"
|
class="action"
|
||||||
:aria-label="$t('sidebar.newFile')"
|
:aria-label="$t('sidebar.newFile')"
|
||||||
:title="$t('sidebar.newFile')"
|
:title="$t('sidebar.newFile')"
|
||||||
@ -83,7 +82,9 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="credits"
|
class="credits"
|
||||||
v-if="isFiles && !disableUsedPercentage"
|
v-if="
|
||||||
|
$router.currentRoute.path.includes('/files/') && !disableUsedPercentage
|
||||||
|
"
|
||||||
style="width: 90%; margin: 2em 2.5em 3em 2.5em"
|
style="width: 90%; margin: 2em 2.5em 3em 2.5em"
|
||||||
>
|
>
|
||||||
<progress-bar :val="usage.usedPercentage" size="small"></progress-bar>
|
<progress-bar :val="usage.usedPercentage" size="small"></progress-bar>
|
||||||
@ -101,7 +102,7 @@
|
|||||||
href="https://github.com/filebrowser/filebrowser"
|
href="https://github.com/filebrowser/filebrowser"
|
||||||
>File Browser</a
|
>File Browser</a
|
||||||
>
|
>
|
||||||
<span> {{ " " }} {{ version }}</span>
|
<span> {{ version }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<a @click="help">{{ $t("sidebar.help") }}</a>
|
<a @click="help">{{ $t("sidebar.help") }}</a>
|
||||||
@ -111,12 +112,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { reactive } from "vue";
|
import { mapState, mapGetters } from "vuex";
|
||||||
import { mapActions, mapState } from "pinia";
|
|
||||||
import { useAuthStore } from "@/stores/auth";
|
|
||||||
import { useFileStore } from "@/stores/file";
|
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
|
||||||
|
|
||||||
import * as auth from "@/utils/auth";
|
import * as auth from "@/utils/auth";
|
||||||
import {
|
import {
|
||||||
version,
|
version,
|
||||||
@ -127,27 +123,19 @@ import {
|
|||||||
loginPage,
|
loginPage,
|
||||||
} from "@/utils/constants";
|
} from "@/utils/constants";
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import ProgressBar from "@/components/ProgressBar.vue";
|
import ProgressBar from "vue-simple-progress";
|
||||||
import prettyBytes from "pretty-bytes";
|
import prettyBytes from "pretty-bytes";
|
||||||
|
|
||||||
const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 };
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "sidebar",
|
name: "sidebar",
|
||||||
setup() {
|
|
||||||
const usage = reactive(USAGE_DEFAULT);
|
|
||||||
return { usage };
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
ProgressBar,
|
ProgressBar,
|
||||||
},
|
},
|
||||||
inject: ["$showError"],
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useAuthStore, ["user", "isLoggedIn"]),
|
...mapState(["user"]),
|
||||||
...mapState(useFileStore, ["isFiles", "reload"]),
|
...mapGetters(["isLogged", "currentPrompt"]),
|
||||||
...mapState(useLayoutStore, ["currentPromptName"]),
|
|
||||||
active() {
|
active() {
|
||||||
return this.currentPromptName === "sidebar";
|
return this.currentPrompt?.prompt === "sidebar";
|
||||||
},
|
},
|
||||||
signup: () => signup,
|
signup: () => signup,
|
||||||
version: () => version,
|
version: () => version,
|
||||||
@ -155,50 +143,47 @@ export default {
|
|||||||
disableUsedPercentage: () => disableUsedPercentage,
|
disableUsedPercentage: () => disableUsedPercentage,
|
||||||
canLogout: () => !noAuth && loginPage,
|
canLogout: () => !noAuth && loginPage,
|
||||||
},
|
},
|
||||||
methods: {
|
asyncComputed: {
|
||||||
...mapActions(useLayoutStore, ["closeHovers", "showHover"]),
|
usage: {
|
||||||
async fetchUsage() {
|
async get() {
|
||||||
const path = this.$route.path.endsWith("/")
|
let path = this.$route.path.endsWith("/")
|
||||||
? this.$route.path
|
? this.$route.path
|
||||||
: this.$route.path + "/";
|
: this.$route.path + "/";
|
||||||
let usageStats = USAGE_DEFAULT;
|
let usageStats = { used: 0, total: 0, usedPercentage: 0 };
|
||||||
if (this.disableUsedPercentage) {
|
if (this.disableUsedPercentage) {
|
||||||
return Object.assign(this.usage, usageStats);
|
return usageStats;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const usage = await api.usage(path);
|
let usage = await api.usage(path);
|
||||||
usageStats = {
|
usageStats = {
|
||||||
used: prettyBytes(usage.used, { binary: true }),
|
used: prettyBytes(usage.used, { binary: true }),
|
||||||
total: prettyBytes(usage.total, { binary: true }),
|
total: prettyBytes(usage.total, { binary: true }),
|
||||||
usedPercentage: Math.round((usage.used / usage.total) * 100),
|
usedPercentage: Math.round((usage.used / usage.total) * 100),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.$showError(error);
|
this.$showError(error);
|
||||||
}
|
}
|
||||||
return Object.assign(this.usage, usageStats);
|
return usageStats;
|
||||||
|
},
|
||||||
|
default: { used: "0 B", total: "0 B", usedPercentage: 0 },
|
||||||
|
shouldUpdate() {
|
||||||
|
return this.$router.currentRoute.path.includes("/files/");
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
toRoot() {
|
toRoot() {
|
||||||
this.$router.push({ path: "/files" });
|
this.$router.push({ path: "/files/" }, () => {});
|
||||||
this.closeHovers();
|
this.$store.commit("closeHovers");
|
||||||
},
|
},
|
||||||
toSettings() {
|
toSettings() {
|
||||||
this.$router.push({ path: "/settings" });
|
this.$router.push({ path: "/settings" }, () => {});
|
||||||
this.closeHovers();
|
this.$store.commit("closeHovers");
|
||||||
},
|
},
|
||||||
help() {
|
help() {
|
||||||
this.showHover("help");
|
this.$store.commit("showHover", "help");
|
||||||
},
|
},
|
||||||
logout: auth.logout,
|
logout: auth.logout,
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
$route: {
|
|
||||||
handler(to) {
|
|
||||||
if (to.path.includes("/files")) {
|
|
||||||
this.fetchUsage();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
immediate: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -13,291 +13,261 @@
|
|||||||
<img class="image-ex-img image-ex-img-center" ref="imgex" @load="onLoad" />
|
<img class="image-ex-img image-ex-img-center" ref="imgex" @load="onLoad" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script>
|
||||||
import { throttle } from "lodash-es";
|
import throttle from "lodash.throttle";
|
||||||
import UTIF from "utif";
|
import UTIF from "utif";
|
||||||
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
|
||||||
|
|
||||||
interface IProps {
|
export default {
|
||||||
src: string;
|
props: {
|
||||||
moveDisabledTime?: number;
|
src: String,
|
||||||
classList?: any[];
|
moveDisabledTime: {
|
||||||
zoomStep?: number;
|
type: Number,
|
||||||
}
|
default: () => 200,
|
||||||
|
},
|
||||||
const props = withDefaults(defineProps<IProps>(), {
|
classList: {
|
||||||
moveDisabledTime: () => 200,
|
type: Array,
|
||||||
classList: () => [],
|
default: () => [],
|
||||||
zoomStep: () => 0.25,
|
},
|
||||||
});
|
zoomStep: {
|
||||||
|
type: Number,
|
||||||
const scale = ref<number>(1);
|
default: () => 0.25,
|
||||||
const lastX = ref<number | null>(null);
|
},
|
||||||
const lastY = ref<number | null>(null);
|
},
|
||||||
const inDrag = ref<boolean>(false);
|
data() {
|
||||||
const touches = ref<number>(0);
|
return {
|
||||||
const lastTouchDistance = ref<number | null>(0);
|
scale: 1,
|
||||||
const moveDisabled = ref<boolean>(false);
|
lastX: null,
|
||||||
const disabledTimer = ref<number | null>(null);
|
lastY: null,
|
||||||
const imageLoaded = ref<boolean>(false);
|
inDrag: false,
|
||||||
const position = ref<{
|
touches: 0,
|
||||||
center: { x: number; y: number };
|
lastTouchDistance: 0,
|
||||||
relative: { x: number; y: number };
|
moveDisabled: false,
|
||||||
}>({
|
disabledTimer: null,
|
||||||
center: { x: 0, y: 0 },
|
imageLoaded: false,
|
||||||
relative: { x: 0, y: 0 },
|
position: {
|
||||||
});
|
center: { x: 0, y: 0 },
|
||||||
const maxScale = ref<number>(4);
|
relative: { x: 0, y: 0 },
|
||||||
const minScale = ref<number>(0.25);
|
},
|
||||||
|
maxScale: 4,
|
||||||
// Refs
|
minScale: 0.25,
|
||||||
const imgex = ref<HTMLImageElement | null>(null);
|
};
|
||||||
const container = ref<HTMLDivElement | null>(null);
|
},
|
||||||
|
mounted() {
|
||||||
onMounted(() => {
|
if (!this.decodeUTIF()) {
|
||||||
if (!decodeUTIF() && imgex.value !== null) {
|
this.$refs.imgex.src = this.src;
|
||||||
imgex.value.src = props.src;
|
}
|
||||||
}
|
let container = this.$refs.container;
|
||||||
|
this.classList.forEach((className) => container.classList.add(className));
|
||||||
props.classList.forEach((className) =>
|
// set width and height if they are zero
|
||||||
container.value !== null ? container.value.classList.add(className) : ""
|
if (getComputedStyle(container).width === "0px") {
|
||||||
);
|
container.style.width = "100%";
|
||||||
|
}
|
||||||
if (container.value === null) {
|
if (getComputedStyle(container).height === "0px") {
|
||||||
return;
|
container.style.height = "100%";
|
||||||
}
|
|
||||||
|
|
||||||
// set width and height if they are zero
|
|
||||||
if (getComputedStyle(container.value).width === "0px") {
|
|
||||||
container.value.style.width = "100%";
|
|
||||||
}
|
|
||||||
if (getComputedStyle(container.value).height === "0px") {
|
|
||||||
container.value.style.height = "100%";
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("resize", onResize);
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
window.removeEventListener("resize", onResize);
|
|
||||||
document.removeEventListener("mouseup", onMouseUp);
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.src,
|
|
||||||
() => {
|
|
||||||
if (!decodeUTIF() && imgex.value !== null) {
|
|
||||||
imgex.value.src = props.src;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scale.value = 1;
|
window.addEventListener("resize", this.onResize);
|
||||||
setZoom();
|
},
|
||||||
setCenter();
|
beforeDestroy() {
|
||||||
}
|
window.removeEventListener("resize", this.onResize);
|
||||||
);
|
document.removeEventListener("mouseup", this.onMouseUp);
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
src: function () {
|
||||||
|
if (!this.decodeUTIF()) {
|
||||||
|
this.$refs.imgex.src = this.src;
|
||||||
|
}
|
||||||
|
|
||||||
// Modified from UTIF.replaceIMG
|
this.scale = 1;
|
||||||
const decodeUTIF = () => {
|
this.setZoom();
|
||||||
const sufs = ["tif", "tiff", "dng", "cr2", "nef"];
|
this.setCenter();
|
||||||
if (document?.location?.pathname === undefined) {
|
},
|
||||||
return;
|
},
|
||||||
}
|
methods: {
|
||||||
const suff =
|
// Modified from UTIF.replaceIMG
|
||||||
document.location.pathname.split(".")?.pop()?.toLowerCase() ?? "";
|
decodeUTIF() {
|
||||||
|
const sufs = ["tif", "tiff", "dng", "cr2", "nef"];
|
||||||
|
let suff = document.location.pathname.split(".").pop().toLowerCase();
|
||||||
|
if (sufs.indexOf(suff) == -1) return false;
|
||||||
|
let xhr = new XMLHttpRequest();
|
||||||
|
UTIF._xhrs.push(xhr);
|
||||||
|
UTIF._imgs.push(this.$refs.imgex);
|
||||||
|
xhr.open("GET", this.src);
|
||||||
|
xhr.responseType = "arraybuffer";
|
||||||
|
xhr.onload = UTIF._imgLoaded;
|
||||||
|
xhr.send();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
let img = this.$refs.imgex;
|
||||||
|
|
||||||
if (sufs.indexOf(suff) == -1) return false;
|
this.imageLoaded = true;
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
UTIF._xhrs.push(xhr);
|
|
||||||
UTIF._imgs.push(imgex.value);
|
|
||||||
xhr.open("GET", props.src);
|
|
||||||
xhr.responseType = "arraybuffer";
|
|
||||||
xhr.onload = UTIF._imgLoaded;
|
|
||||||
xhr.send();
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onLoad = () => {
|
if (img === undefined) {
|
||||||
imageLoaded.value = true;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (imgex.value === null) {
|
img.classList.remove("image-ex-img-center");
|
||||||
return;
|
this.setCenter();
|
||||||
}
|
img.classList.add("image-ex-img-ready");
|
||||||
|
|
||||||
imgex.value.classList.remove("image-ex-img-center");
|
document.addEventListener("mouseup", this.onMouseUp);
|
||||||
setCenter();
|
|
||||||
imgex.value.classList.add("image-ex-img-ready");
|
|
||||||
|
|
||||||
document.addEventListener("mouseup", onMouseUp);
|
let realSize = img.naturalWidth;
|
||||||
|
let displaySize = img.offsetWidth;
|
||||||
|
|
||||||
let realSize = imgex.value.naturalWidth;
|
// Image is in portrait orientation
|
||||||
let displaySize = imgex.value.offsetWidth;
|
if (img.naturalHeight > img.naturalWidth) {
|
||||||
|
realSize = img.naturalHeight;
|
||||||
|
displaySize = img.offsetHeight;
|
||||||
|
}
|
||||||
|
|
||||||
// Image is in portrait orientation
|
// Scale needed to display the image on full size
|
||||||
if (imgex.value.naturalHeight > imgex.value.naturalWidth) {
|
const fullScale = realSize / displaySize;
|
||||||
realSize = imgex.value.naturalHeight;
|
|
||||||
displaySize = imgex.value.offsetHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scale needed to display the image on full size
|
// Full size plus additional zoom
|
||||||
const fullScale = realSize / displaySize;
|
this.maxScale = fullScale + 4;
|
||||||
|
},
|
||||||
|
onMouseUp() {
|
||||||
|
this.inDrag = false;
|
||||||
|
},
|
||||||
|
onResize: throttle(function () {
|
||||||
|
if (this.imageLoaded) {
|
||||||
|
this.setCenter();
|
||||||
|
this.doMove(this.position.relative.x, this.position.relative.y);
|
||||||
|
}
|
||||||
|
}, 100),
|
||||||
|
setCenter() {
|
||||||
|
let container = this.$refs.container;
|
||||||
|
let img = this.$refs.imgex;
|
||||||
|
|
||||||
// Full size plus additional zoom
|
this.position.center.x = Math.floor(
|
||||||
maxScale.value = fullScale + 4;
|
(container.clientWidth - img.clientWidth) / 2
|
||||||
};
|
);
|
||||||
|
this.position.center.y = Math.floor(
|
||||||
|
(container.clientHeight - img.clientHeight) / 2
|
||||||
|
);
|
||||||
|
|
||||||
const onMouseUp = () => {
|
img.style.left = this.position.center.x + "px";
|
||||||
inDrag.value = false;
|
img.style.top = this.position.center.y + "px";
|
||||||
};
|
},
|
||||||
|
mousedownStart(event) {
|
||||||
|
this.lastX = null;
|
||||||
|
this.lastY = null;
|
||||||
|
this.inDrag = true;
|
||||||
|
event.preventDefault();
|
||||||
|
},
|
||||||
|
mouseMove(event) {
|
||||||
|
if (!this.inDrag) return;
|
||||||
|
this.doMove(event.movementX, event.movementY);
|
||||||
|
event.preventDefault();
|
||||||
|
},
|
||||||
|
mouseUp(event) {
|
||||||
|
this.inDrag = false;
|
||||||
|
event.preventDefault();
|
||||||
|
},
|
||||||
|
touchStart(event) {
|
||||||
|
this.lastX = null;
|
||||||
|
this.lastY = null;
|
||||||
|
this.lastTouchDistance = null;
|
||||||
|
if (event.targetTouches.length < 2) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.touches = 0;
|
||||||
|
}, 300);
|
||||||
|
this.touches++;
|
||||||
|
if (this.touches > 1) {
|
||||||
|
this.zoomAuto(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
},
|
||||||
|
zoomAuto(event) {
|
||||||
|
switch (this.scale) {
|
||||||
|
case 1:
|
||||||
|
this.scale = 2;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
this.scale = 4;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
case 4:
|
||||||
|
this.scale = 1;
|
||||||
|
this.setCenter();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.setZoom();
|
||||||
|
event.preventDefault();
|
||||||
|
},
|
||||||
|
touchMove(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (this.lastX === null) {
|
||||||
|
this.lastX = event.targetTouches[0].pageX;
|
||||||
|
this.lastY = event.targetTouches[0].pageY;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let step = this.$refs.imgex.width / 5;
|
||||||
|
if (event.targetTouches.length === 2) {
|
||||||
|
this.moveDisabled = true;
|
||||||
|
clearTimeout(this.disabledTimer);
|
||||||
|
this.disabledTimer = setTimeout(
|
||||||
|
() => (this.moveDisabled = false),
|
||||||
|
this.moveDisabledTime
|
||||||
|
);
|
||||||
|
|
||||||
const onResize = throttle(function () {
|
let p1 = event.targetTouches[0];
|
||||||
if (imageLoaded.value) {
|
let p2 = event.targetTouches[1];
|
||||||
setCenter();
|
let touchDistance = Math.sqrt(
|
||||||
doMove(position.value.relative.x, position.value.relative.y);
|
Math.pow(p2.pageX - p1.pageX, 2) + Math.pow(p2.pageY - p1.pageY, 2)
|
||||||
}
|
);
|
||||||
}, 100);
|
if (!this.lastTouchDistance) {
|
||||||
|
this.lastTouchDistance = touchDistance;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.scale += (touchDistance - this.lastTouchDistance) / step;
|
||||||
|
this.lastTouchDistance = touchDistance;
|
||||||
|
this.setZoom();
|
||||||
|
} else if (event.targetTouches.length === 1) {
|
||||||
|
if (this.moveDisabled) return;
|
||||||
|
let x = event.targetTouches[0].pageX - this.lastX;
|
||||||
|
let y = event.targetTouches[0].pageY - this.lastY;
|
||||||
|
if (Math.abs(x) >= step && Math.abs(y) >= step) return;
|
||||||
|
this.lastX = event.targetTouches[0].pageX;
|
||||||
|
this.lastY = event.targetTouches[0].pageY;
|
||||||
|
this.doMove(x, y);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
doMove(x, y) {
|
||||||
|
let style = this.$refs.imgex.style;
|
||||||
|
let posX = this.pxStringToNumber(style.left) + x;
|
||||||
|
let posY = this.pxStringToNumber(style.top) + y;
|
||||||
|
|
||||||
const setCenter = () => {
|
style.left = posX + "px";
|
||||||
if (container.value === null || imgex.value === null) {
|
style.top = posY + "px";
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
position.value.center.x = Math.floor(
|
this.position.relative.x = Math.abs(this.position.center.x - posX);
|
||||||
(container.value.clientWidth - imgex.value.clientWidth) / 2
|
this.position.relative.y = Math.abs(this.position.center.y - posY);
|
||||||
);
|
|
||||||
position.value.center.y = Math.floor(
|
|
||||||
(container.value.clientHeight - imgex.value.clientHeight) / 2
|
|
||||||
);
|
|
||||||
|
|
||||||
imgex.value.style.left = position.value.center.x + "px";
|
if (posX < this.position.center.x) {
|
||||||
imgex.value.style.top = position.value.center.y + "px";
|
this.position.relative.x = this.position.relative.x * -1;
|
||||||
};
|
}
|
||||||
|
|
||||||
const mousedownStart = (event: Event) => {
|
if (posY < this.position.center.y) {
|
||||||
lastX.value = null;
|
this.position.relative.y = this.position.relative.y * -1;
|
||||||
lastY.value = null;
|
}
|
||||||
inDrag.value = true;
|
},
|
||||||
event.preventDefault();
|
wheelMove(event) {
|
||||||
};
|
this.scale += -Math.sign(event.deltaY) * this.zoomStep;
|
||||||
const mouseMove = (event: MouseEvent) => {
|
this.setZoom();
|
||||||
if (!inDrag.value) return;
|
},
|
||||||
doMove(event.movementX, event.movementY);
|
setZoom() {
|
||||||
event.preventDefault();
|
this.scale = this.scale < this.minScale ? this.minScale : this.scale;
|
||||||
};
|
this.scale = this.scale > this.maxScale ? this.maxScale : this.scale;
|
||||||
const mouseUp = (event: Event) => {
|
this.$refs.imgex.style.transform = `scale(${this.scale})`;
|
||||||
inDrag.value = false;
|
},
|
||||||
event.preventDefault();
|
pxStringToNumber(style) {
|
||||||
};
|
return +style.replace("px", "");
|
||||||
const touchStart = (event: TouchEvent) => {
|
},
|
||||||
lastX.value = null;
|
},
|
||||||
lastY.value = null;
|
|
||||||
lastTouchDistance.value = null;
|
|
||||||
if (event.targetTouches.length < 2) {
|
|
||||||
setTimeout(() => {
|
|
||||||
touches.value = 0;
|
|
||||||
}, 300);
|
|
||||||
touches.value++;
|
|
||||||
if (touches.value > 1) {
|
|
||||||
zoomAuto(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
};
|
|
||||||
|
|
||||||
const zoomAuto = (event: Event) => {
|
|
||||||
switch (scale.value) {
|
|
||||||
case 1:
|
|
||||||
scale.value = 2;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
scale.value = 4;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
case 4:
|
|
||||||
scale.value = 1;
|
|
||||||
setCenter();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
setZoom();
|
|
||||||
event.preventDefault();
|
|
||||||
};
|
|
||||||
|
|
||||||
const touchMove = (event: TouchEvent) => {
|
|
||||||
event.preventDefault();
|
|
||||||
if (lastX.value === null) {
|
|
||||||
lastX.value = event.targetTouches[0].pageX;
|
|
||||||
lastY.value = event.targetTouches[0].pageY;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (imgex.value === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const step = imgex.value.width / 5;
|
|
||||||
if (event.targetTouches.length === 2) {
|
|
||||||
moveDisabled.value = true;
|
|
||||||
if (disabledTimer.value) clearTimeout(disabledTimer.value);
|
|
||||||
disabledTimer.value = window.setTimeout(
|
|
||||||
() => (moveDisabled.value = false),
|
|
||||||
props.moveDisabledTime
|
|
||||||
);
|
|
||||||
|
|
||||||
const p1 = event.targetTouches[0];
|
|
||||||
const p2 = event.targetTouches[1];
|
|
||||||
const touchDistance = Math.sqrt(
|
|
||||||
Math.pow(p2.pageX - p1.pageX, 2) + Math.pow(p2.pageY - p1.pageY, 2)
|
|
||||||
);
|
|
||||||
if (!lastTouchDistance.value) {
|
|
||||||
lastTouchDistance.value = touchDistance;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
scale.value += (touchDistance - lastTouchDistance.value) / step;
|
|
||||||
lastTouchDistance.value = touchDistance;
|
|
||||||
setZoom();
|
|
||||||
} else if (event.targetTouches.length === 1) {
|
|
||||||
if (moveDisabled.value) return;
|
|
||||||
const x = event.targetTouches[0].pageX - (lastX.value ?? 0);
|
|
||||||
const y = event.targetTouches[0].pageY - (lastY.value ?? 0);
|
|
||||||
if (Math.abs(x) >= step && Math.abs(y) >= step) return;
|
|
||||||
lastX.value = event.targetTouches[0].pageX;
|
|
||||||
lastY.value = event.targetTouches[0].pageY;
|
|
||||||
doMove(x, y);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const doMove = (x: number, y: number) => {
|
|
||||||
if (imgex.value === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const style = imgex.value.style;
|
|
||||||
|
|
||||||
const posX = pxStringToNumber(style.left) + x;
|
|
||||||
const posY = pxStringToNumber(style.top) + y;
|
|
||||||
|
|
||||||
style.left = posX + "px";
|
|
||||||
style.top = posY + "px";
|
|
||||||
|
|
||||||
position.value.relative.x = Math.abs(position.value.center.x - posX);
|
|
||||||
position.value.relative.y = Math.abs(position.value.center.y - posY);
|
|
||||||
|
|
||||||
if (posX < position.value.center.x) {
|
|
||||||
position.value.relative.x = position.value.relative.x * -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (posY < position.value.center.y) {
|
|
||||||
position.value.relative.y = position.value.relative.y * -1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const wheelMove = (event: WheelEvent) => {
|
|
||||||
scale.value += -Math.sign(event.deltaY) * props.zoomStep;
|
|
||||||
setZoom();
|
|
||||||
};
|
|
||||||
const setZoom = () => {
|
|
||||||
scale.value = scale.value < minScale.value ? minScale.value : scale.value;
|
|
||||||
scale.value = scale.value > maxScale.value ? maxScale.value : scale.value;
|
|
||||||
if (imgex.value !== null)
|
|
||||||
imgex.value.style.transform = `scale(${scale.value})`;
|
|
||||||
};
|
|
||||||
const pxStringToNumber = (style: string) => {
|
|
||||||
return +style.replace("px", "");
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
|
@ -12,11 +12,10 @@
|
|||||||
:data-type="type"
|
:data-type="type"
|
||||||
:aria-label="name"
|
:aria-label="name"
|
||||||
:aria-selected="isSelected"
|
:aria-selected="isSelected"
|
||||||
:data-ext="getExtension(name).toLowerCase()"
|
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<img
|
<img
|
||||||
v-if="!readOnly && type === 'image' && isThumbsEnabled"
|
v-if="readOnly == undefined && type === 'image' && isThumbsEnabled"
|
||||||
v-lazy="thumbnailUrl"
|
v-lazy="thumbnailUrl"
|
||||||
/>
|
/>
|
||||||
<i v-else class="material-icons"></i>
|
<i v-else class="material-icons"></i>
|
||||||
@ -35,250 +34,221 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script>
|
||||||
import { useAuthStore } from "@/stores/auth";
|
|
||||||
import { useFileStore } from "@/stores/file";
|
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
|
||||||
|
|
||||||
import { enableThumbs } from "@/utils/constants";
|
import { enableThumbs } from "@/utils/constants";
|
||||||
|
import { mapMutations, mapGetters, mapState } from "vuex";
|
||||||
import { filesize } from "@/utils";
|
import { filesize } from "@/utils";
|
||||||
import dayjs from "dayjs";
|
import moment from "moment/min/moment-with-locales";
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import * as upload from "@/utils/upload";
|
import * as upload from "@/utils/upload";
|
||||||
import { computed, inject, ref } from "vue";
|
|
||||||
import { useRouter } from "vue-router";
|
|
||||||
|
|
||||||
const touches = ref<number>(0);
|
export default {
|
||||||
|
name: "item",
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
touches: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
props: [
|
||||||
|
"name",
|
||||||
|
"isDir",
|
||||||
|
"url",
|
||||||
|
"type",
|
||||||
|
"size",
|
||||||
|
"modified",
|
||||||
|
"index",
|
||||||
|
"readOnly",
|
||||||
|
"path",
|
||||||
|
],
|
||||||
|
computed: {
|
||||||
|
...mapState(["user", "selected", "req", "jwt"]),
|
||||||
|
...mapGetters(["selectedCount"]),
|
||||||
|
singleClick() {
|
||||||
|
return this.readOnly == undefined && this.user.singleClick;
|
||||||
|
},
|
||||||
|
isSelected() {
|
||||||
|
return this.selected.indexOf(this.index) !== -1;
|
||||||
|
},
|
||||||
|
isDraggable() {
|
||||||
|
return this.readOnly == undefined && this.user.perm.rename;
|
||||||
|
},
|
||||||
|
canDrop() {
|
||||||
|
if (!this.isDir || this.readOnly !== undefined) return false;
|
||||||
|
|
||||||
const $showError = inject<IToastError>("$showError")!;
|
for (let i of this.selected) {
|
||||||
const router = useRouter();
|
if (this.req.items[i].url === this.url) {
|
||||||
|
return false;
|
||||||
const props = defineProps<{
|
}
|
||||||
name: string;
|
|
||||||
isDir: boolean;
|
|
||||||
url: string;
|
|
||||||
type: string;
|
|
||||||
size: number;
|
|
||||||
modified: string;
|
|
||||||
index: number;
|
|
||||||
readOnly?: boolean;
|
|
||||||
path?: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
|
||||||
const fileStore = useFileStore();
|
|
||||||
const layoutStore = useLayoutStore();
|
|
||||||
|
|
||||||
const singleClick = computed(
|
|
||||||
() => !props.readOnly && authStore.user?.singleClick
|
|
||||||
);
|
|
||||||
const isSelected = computed(
|
|
||||||
() => fileStore.selected.indexOf(props.index) !== -1
|
|
||||||
);
|
|
||||||
const isDraggable = computed(
|
|
||||||
() => !props.readOnly && authStore.user?.perm.rename
|
|
||||||
);
|
|
||||||
|
|
||||||
const canDrop = computed(() => {
|
|
||||||
if (!props.isDir || props.readOnly) return false;
|
|
||||||
|
|
||||||
for (const i of fileStore.selected) {
|
|
||||||
if (fileStore.req?.items[i].url === props.url) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
const thumbnailUrl = computed(() => {
|
|
||||||
const file = {
|
|
||||||
path: props.path,
|
|
||||||
modified: props.modified,
|
|
||||||
};
|
|
||||||
|
|
||||||
return api.getPreviewURL(file as Resource, "thumb");
|
|
||||||
});
|
|
||||||
|
|
||||||
const isThumbsEnabled = computed(() => {
|
|
||||||
return enableThumbs;
|
|
||||||
});
|
|
||||||
|
|
||||||
const humanSize = () => {
|
|
||||||
return props.type == "invalid_link" ? "invalid link" : filesize(props.size);
|
|
||||||
};
|
|
||||||
|
|
||||||
const humanTime = () => {
|
|
||||||
if (!props.readOnly && authStore.user?.dateFormat) {
|
|
||||||
return dayjs(props.modified).format("L LT");
|
|
||||||
}
|
|
||||||
return dayjs(props.modified).fromNow();
|
|
||||||
};
|
|
||||||
|
|
||||||
const dragStart = () => {
|
|
||||||
if (fileStore.selectedCount === 0) {
|
|
||||||
fileStore.selected.push(props.index);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isSelected.value) {
|
|
||||||
fileStore.selected = [];
|
|
||||||
fileStore.selected.push(props.index);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const dragOver = (event: Event) => {
|
|
||||||
if (!canDrop.value) return;
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
let el = event.target as HTMLElement | null;
|
|
||||||
if (el !== null) {
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
if (!el?.classList.contains("item")) {
|
|
||||||
el = el?.parentElement ?? null;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (el !== null) el.style.opacity = "1";
|
return true;
|
||||||
}
|
},
|
||||||
};
|
thumbnailUrl() {
|
||||||
|
const file = {
|
||||||
|
path: this.path,
|
||||||
|
modified: this.modified,
|
||||||
|
};
|
||||||
|
|
||||||
const drop = async (event: Event) => {
|
return api.getPreviewURL(file, "thumb");
|
||||||
if (!canDrop.value) return;
|
},
|
||||||
event.preventDefault();
|
isThumbsEnabled() {
|
||||||
|
return enableThumbs;
|
||||||
if (fileStore.selectedCount === 0) return;
|
},
|
||||||
|
},
|
||||||
let el = event.target as HTMLElement | null;
|
methods: {
|
||||||
for (let i = 0; i < 5; i++) {
|
...mapMutations(["addSelected", "removeSelected", "resetSelected"]),
|
||||||
if (el !== null && !el.classList.contains("item")) {
|
humanSize: function () {
|
||||||
el = el.parentElement;
|
return this.type == "invalid_link" ? "invalid link" : filesize(this.size);
|
||||||
}
|
},
|
||||||
}
|
humanTime: function () {
|
||||||
|
if (this.readOnly == undefined && this.user.dateFormat) {
|
||||||
const items: any[] = [];
|
return moment(this.modified).format("L LT");
|
||||||
|
}
|
||||||
for (const i of fileStore.selected) {
|
return moment(this.modified).fromNow();
|
||||||
if (fileStore.req) {
|
},
|
||||||
items.push({
|
dragStart: function () {
|
||||||
from: fileStore.req?.items[i].url,
|
if (this.selectedCount === 0) {
|
||||||
to: props.url + encodeURIComponent(fileStore.req?.items[i].name),
|
this.addSelected(this.index);
|
||||||
name: fileStore.req?.items[i].name,
|
return;
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get url from ListingItem instance
|
|
||||||
if (el === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const path = el.__vue__.url;
|
|
||||||
const baseItems = (await api.fetch(path)).items;
|
|
||||||
|
|
||||||
const action = (overwrite: boolean, rename: boolean) => {
|
|
||||||
api
|
|
||||||
.move(items, overwrite, rename)
|
|
||||||
.then(() => {
|
|
||||||
fileStore.reload = true;
|
|
||||||
})
|
|
||||||
.catch($showError);
|
|
||||||
};
|
|
||||||
|
|
||||||
const conflict = upload.checkConflict(items, baseItems);
|
|
||||||
|
|
||||||
let overwrite = false;
|
|
||||||
let rename = false;
|
|
||||||
|
|
||||||
if (conflict) {
|
|
||||||
layoutStore.showHover({
|
|
||||||
prompt: "replace-rename",
|
|
||||||
confirm: (event: Event, option: any) => {
|
|
||||||
overwrite = option == "overwrite";
|
|
||||||
rename = option == "rename";
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
layoutStore.closeHovers();
|
|
||||||
action(overwrite, rename);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
action(overwrite, rename);
|
|
||||||
};
|
|
||||||
|
|
||||||
const itemClick = (event: Event | KeyboardEvent) => {
|
|
||||||
if (
|
|
||||||
singleClick.value &&
|
|
||||||
!(event as KeyboardEvent).ctrlKey &&
|
|
||||||
!(event as KeyboardEvent).metaKey &&
|
|
||||||
!(event as KeyboardEvent).shiftKey &&
|
|
||||||
!fileStore.multiple
|
|
||||||
)
|
|
||||||
open();
|
|
||||||
else click(event);
|
|
||||||
};
|
|
||||||
|
|
||||||
const click = (event: Event | KeyboardEvent) => {
|
|
||||||
if (!singleClick.value && fileStore.selectedCount !== 0)
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
touches.value = 0;
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
touches.value++;
|
|
||||||
if (touches.value > 1) {
|
|
||||||
open();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileStore.selected.indexOf(props.index) !== -1) {
|
|
||||||
fileStore.removeSelected(props.index);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((event as KeyboardEvent).shiftKey && fileStore.selected.length > 0) {
|
|
||||||
let fi = 0;
|
|
||||||
let la = 0;
|
|
||||||
|
|
||||||
if (props.index > fileStore.selected[0]) {
|
|
||||||
fi = fileStore.selected[0] + 1;
|
|
||||||
la = props.index;
|
|
||||||
} else {
|
|
||||||
fi = props.index;
|
|
||||||
la = fileStore.selected[0] - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (; fi <= la; fi++) {
|
|
||||||
if (fileStore.selected.indexOf(fi) == -1) {
|
|
||||||
fileStore.selected.push(fi);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
if (!this.isSelected) {
|
||||||
}
|
this.resetSelected();
|
||||||
|
this.addSelected(this.index);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dragOver: function (event) {
|
||||||
|
if (!this.canDrop) return;
|
||||||
|
|
||||||
if (
|
event.preventDefault();
|
||||||
!singleClick.value &&
|
let el = event.target;
|
||||||
!(event as KeyboardEvent).ctrlKey &&
|
|
||||||
!(event as KeyboardEvent).metaKey &&
|
|
||||||
!fileStore.multiple
|
|
||||||
) {
|
|
||||||
fileStore.selected = [];
|
|
||||||
}
|
|
||||||
fileStore.selected.push(props.index);
|
|
||||||
};
|
|
||||||
|
|
||||||
const open = () => {
|
for (let i = 0; i < 5; i++) {
|
||||||
router.push({ path: props.url });
|
if (!el.classList.contains("item")) {
|
||||||
};
|
el = el.parentElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getExtension = (fileName: string): string => {
|
el.style.opacity = 1;
|
||||||
const lastDotIndex = fileName.lastIndexOf(".");
|
},
|
||||||
if (lastDotIndex === -1) {
|
drop: async function (event) {
|
||||||
return fileName;
|
if (!this.canDrop) return;
|
||||||
}
|
event.preventDefault();
|
||||||
return fileName.substring(lastDotIndex);
|
|
||||||
|
if (this.selectedCount === 0) return;
|
||||||
|
|
||||||
|
let el = event.target;
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
if (el !== null && !el.classList.contains("item")) {
|
||||||
|
el = el.parentElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let items = [];
|
||||||
|
|
||||||
|
for (let i of this.selected) {
|
||||||
|
items.push({
|
||||||
|
from: this.req.items[i].url,
|
||||||
|
to: this.url + encodeURIComponent(this.req.items[i].name),
|
||||||
|
name: this.req.items[i].name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get url from ListingItem instance
|
||||||
|
let path = el.__vue__.url;
|
||||||
|
let baseItems = (await api.fetch(path)).items;
|
||||||
|
|
||||||
|
let action = (overwrite, rename) => {
|
||||||
|
api
|
||||||
|
.move(items, overwrite, rename)
|
||||||
|
.then(() => {
|
||||||
|
this.$store.commit("setReload", true);
|
||||||
|
})
|
||||||
|
.catch(this.$showError);
|
||||||
|
};
|
||||||
|
|
||||||
|
let conflict = upload.checkConflict(items, baseItems);
|
||||||
|
|
||||||
|
let overwrite = false;
|
||||||
|
let rename = false;
|
||||||
|
|
||||||
|
if (conflict) {
|
||||||
|
this.$store.commit("showHover", {
|
||||||
|
prompt: "replace-rename",
|
||||||
|
confirm: (event, option) => {
|
||||||
|
overwrite = option == "overwrite";
|
||||||
|
rename = option == "rename";
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
this.$store.commit("closeHovers");
|
||||||
|
action(overwrite, rename);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
action(overwrite, rename);
|
||||||
|
},
|
||||||
|
itemClick: function (event) {
|
||||||
|
if (
|
||||||
|
!(event.ctrlKey || event.metaKey) &&
|
||||||
|
this.singleClick &&
|
||||||
|
!this.$store.state.multiple
|
||||||
|
)
|
||||||
|
this.open();
|
||||||
|
else this.click(event);
|
||||||
|
},
|
||||||
|
click: function (event) {
|
||||||
|
if (!this.singleClick && this.selectedCount !== 0) event.preventDefault();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.touches = 0;
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
this.touches++;
|
||||||
|
if (this.touches > 1) {
|
||||||
|
this.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$store.state.selected.indexOf(this.index) !== -1) {
|
||||||
|
this.removeSelected(this.index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.shiftKey && this.selected.length > 0) {
|
||||||
|
let fi = 0;
|
||||||
|
let la = 0;
|
||||||
|
|
||||||
|
if (this.index > this.selected[0]) {
|
||||||
|
fi = this.selected[0] + 1;
|
||||||
|
la = this.index;
|
||||||
|
} else {
|
||||||
|
fi = this.index;
|
||||||
|
la = this.selected[0] - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (; fi <= la; fi++) {
|
||||||
|
if (this.$store.state.selected.indexOf(fi) == -1) {
|
||||||
|
this.addSelected(fi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!this.singleClick &&
|
||||||
|
!event.ctrlKey &&
|
||||||
|
!event.metaKey &&
|
||||||
|
!this.$store.state.multiple
|
||||||
|
)
|
||||||
|
this.resetSelected();
|
||||||
|
this.addSelected(this.index);
|
||||||
|
},
|
||||||
|
open: function () {
|
||||||
|
this.$router.push({ path: this.url });
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,178 +0,0 @@
|
|||||||
<template>
|
|
||||||
<video ref="videoPlayer" class="video-max video-js" controls preload="auto">
|
|
||||||
<source />
|
|
||||||
<track
|
|
||||||
kind="subtitles"
|
|
||||||
v-for="(sub, index) in subtitles"
|
|
||||||
:key="index"
|
|
||||||
:src="sub"
|
|
||||||
:label="subLabel(sub)"
|
|
||||||
:default="index === 0"
|
|
||||||
/>
|
|
||||||
<p class="vjs-no-js">
|
|
||||||
Sorry, your browser doesn't support embedded videos, but don't worry, you
|
|
||||||
can <a :href="source">download it</a>
|
|
||||||
and watch it with your favorite video player!
|
|
||||||
</p>
|
|
||||||
</video>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, onMounted, onBeforeUnmount, nextTick } from "vue";
|
|
||||||
import videojs from "video.js";
|
|
||||||
import type Player from "video.js/dist/types/player";
|
|
||||||
import "videojs-mobile-ui";
|
|
||||||
import "videojs-hotkeys";
|
|
||||||
import "video.js/dist/video-js.min.css";
|
|
||||||
import "videojs-mobile-ui/dist/videojs-mobile-ui.css";
|
|
||||||
|
|
||||||
const videoPlayer = ref<HTMLElement | null>(null);
|
|
||||||
const player = ref<Player | null>(null);
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
source: string;
|
|
||||||
subtitles?: string[];
|
|
||||||
options?: any;
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
options: {},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const source = ref(props.source);
|
|
||||||
const sourceType = ref("");
|
|
||||||
|
|
||||||
nextTick(() => {
|
|
||||||
initVideoPlayer();
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (player.value) {
|
|
||||||
player.value.dispose();
|
|
||||||
player.value = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const initVideoPlayer = async () => {
|
|
||||||
try {
|
|
||||||
const lang = document.documentElement.lang;
|
|
||||||
const languagePack = await (
|
|
||||||
languageImports[lang] || languageImports.en
|
|
||||||
)?.();
|
|
||||||
videojs.addLanguage("videoPlayerLocal", languagePack.default);
|
|
||||||
sourceType.value = "";
|
|
||||||
|
|
||||||
//
|
|
||||||
sourceType.value = getSourceType(source.value);
|
|
||||||
|
|
||||||
const srcOpt = { sources: { src: props.source, type: sourceType.value } };
|
|
||||||
//Supporting localized language display.
|
|
||||||
const langOpt = { language: "videoPlayerLocal" };
|
|
||||||
// support for playback at different speeds.
|
|
||||||
const playbackRatesOpt = { playbackRates: [0.5, 1, 1.5, 2, 2.5, 3] };
|
|
||||||
const options = getOptions(
|
|
||||||
props.options,
|
|
||||||
langOpt,
|
|
||||||
srcOpt,
|
|
||||||
playbackRatesOpt
|
|
||||||
);
|
|
||||||
player.value = videojs(videoPlayer.value!, options, () => {});
|
|
||||||
|
|
||||||
// TODO: need to test on mobile
|
|
||||||
// @ts-expect-error no ts definition for mobileUi
|
|
||||||
player.value!.mobileUi();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error initializing video player:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getOptions = (...srcOpt: any[]) => {
|
|
||||||
const options = {
|
|
||||||
controlBar: {
|
|
||||||
skipButtons: {
|
|
||||||
forward: 5,
|
|
||||||
backward: 5,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
html5: {
|
|
||||||
nativeTextTracks: false,
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
hotkeys: {
|
|
||||||
volumeStep: 0.1,
|
|
||||||
seekStep: 10,
|
|
||||||
enableModifiersForNumbers: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return videojs.obj.merge(options, ...srcOpt);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Attempting to fix the issue of being unable to play .MKV format video files
|
|
||||||
const getSourceType = (source: string) => {
|
|
||||||
const fileExtension = source ? source.split("?")[0].split(".").pop() : "";
|
|
||||||
if (fileExtension?.toLowerCase() === "mkv") {
|
|
||||||
return "video/mp4";
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
};
|
|
||||||
|
|
||||||
const subLabel = (subUrl: string) => {
|
|
||||||
let url: URL;
|
|
||||||
try {
|
|
||||||
url = new URL(subUrl);
|
|
||||||
} catch {
|
|
||||||
// treat it as a relative url
|
|
||||||
// we only need this for filename
|
|
||||||
url = new URL(subUrl, window.location.origin);
|
|
||||||
}
|
|
||||||
|
|
||||||
const label = decodeURIComponent(
|
|
||||||
url.pathname
|
|
||||||
.split("/")
|
|
||||||
.pop()!
|
|
||||||
.replace(/\.[^/.]+$/, "")
|
|
||||||
);
|
|
||||||
|
|
||||||
return label;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface LanguageImports {
|
|
||||||
[key: string]: () => Promise<any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const languageImports: LanguageImports = {
|
|
||||||
he: () => import("video.js/dist/lang/he.json"),
|
|
||||||
hu: () => import("video.js/dist/lang/hu.json"),
|
|
||||||
ar: () => import("video.js/dist/lang/ar.json"),
|
|
||||||
de: () => import("video.js/dist/lang/de.json"),
|
|
||||||
el: () => import("video.js/dist/lang/el.json"),
|
|
||||||
en: () => import("video.js/dist/lang/en.json"),
|
|
||||||
es: () => import("video.js/dist/lang/es.json"),
|
|
||||||
fr: () => import("video.js/dist/lang/fr.json"),
|
|
||||||
it: () => import("video.js/dist/lang/it.json"),
|
|
||||||
ja: () => import("video.js/dist/lang/ja.json"),
|
|
||||||
ko: () => import("video.js/dist/lang/ko.json"),
|
|
||||||
"nl-be": () => import("video.js/dist/lang/nl.json"),
|
|
||||||
pl: () => import("video.js/dist/lang/pl.json"),
|
|
||||||
"pt-br": () => import("video.js/dist/lang/pt-BR.json"),
|
|
||||||
pt: () => import("video.js/dist/lang/pt-PT.json"),
|
|
||||||
ro: () => import("video.js/dist/lang/ro.json"),
|
|
||||||
ru: () => import("video.js/dist/lang/ru.json"),
|
|
||||||
sk: () => import("video.js/dist/lang/sk.json"),
|
|
||||||
tr: () => import("video.js/dist/lang/tr.json"),
|
|
||||||
uk: () => import("video.js/dist/lang/uk.json"),
|
|
||||||
"zh-cn": () => import("video.js/dist/lang/zh-CN.json"),
|
|
||||||
"zh-tw": () => import("video.js/dist/lang/zh-TW.json"),
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<style scoped>
|
|
||||||
.video-max {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -2,31 +2,24 @@
|
|||||||
<button @click="action" :aria-label="label" :title="label" class="action">
|
<button @click="action" :aria-label="label" :title="label" class="action">
|
||||||
<i class="material-icons">{{ icon }}</i>
|
<i class="material-icons">{{ icon }}</i>
|
||||||
<span>{{ label }}</span>
|
<span>{{ label }}</span>
|
||||||
<span v-if="counter && counter > 0" class="counter">{{ counter }}</span>
|
<span v-if="counter > 0" class="counter">{{ counter }}</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script>
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
export default {
|
||||||
|
name: "action",
|
||||||
|
props: ["icon", "label", "counter", "show"],
|
||||||
|
methods: {
|
||||||
|
action: function () {
|
||||||
|
if (this.show) {
|
||||||
|
this.$store.commit("showHover", this.show);
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
this.$emit("action");
|
||||||
icon?: string;
|
},
|
||||||
label?: string;
|
},
|
||||||
counter?: number;
|
|
||||||
show?: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "action"): any;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const layoutStore = useLayoutStore();
|
|
||||||
|
|
||||||
const action = () => {
|
|
||||||
if (props.show) {
|
|
||||||
layoutStore.showHover(props.show);
|
|
||||||
}
|
|
||||||
|
|
||||||
emit("action");
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
|
@ -1,59 +1,62 @@
|
|||||||
<template>
|
<template>
|
||||||
<header>
|
<header>
|
||||||
<img v-if="showLogo" :src="logoURL" />
|
<img v-if="showLogo !== undefined" :src="logoURL" />
|
||||||
<Action
|
<action
|
||||||
v-if="showMenu"
|
v-if="showMenu !== undefined"
|
||||||
class="menu-button"
|
class="menu-button"
|
||||||
icon="menu"
|
icon="menu"
|
||||||
:label="t('buttons.toggleSidebar')"
|
:label="$t('buttons.toggleSidebar')"
|
||||||
@action="layoutStore.showHover('sidebar')"
|
@action="openSidebar()"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
|
|
||||||
<div
|
<div id="dropdown" :class="{ active: this.currentPromptName === 'more' }">
|
||||||
id="dropdown"
|
|
||||||
:class="{ active: layoutStore.currentPromptName === 'more' }"
|
|
||||||
>
|
|
||||||
<slot name="actions" />
|
<slot name="actions" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Action
|
<action
|
||||||
v-if="ifActionsSlot"
|
v-if="this.$slots.actions"
|
||||||
id="more"
|
id="more"
|
||||||
icon="more_vert"
|
icon="more_vert"
|
||||||
:label="t('buttons.more')"
|
:label="$t('buttons.more')"
|
||||||
@action="layoutStore.showHover('more')"
|
@action="$store.commit('showHover', 'more')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="overlay"
|
class="overlay"
|
||||||
v-show="layoutStore.currentPromptName == 'more'"
|
v-show="this.currentPromptName == 'more'"
|
||||||
@click="layoutStore.closeHovers"
|
@click="$store.commit('closeHovers')"
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script>
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
|
||||||
|
|
||||||
import { logoURL } from "@/utils/constants";
|
import { logoURL } from "@/utils/constants";
|
||||||
|
|
||||||
import Action from "@/components/header/Action.vue";
|
import Action from "@/components/header/Action.vue";
|
||||||
import { computed, useSlots } from "vue";
|
import { mapGetters } from "vuex";
|
||||||
import { useI18n } from "vue-i18n";
|
|
||||||
|
|
||||||
defineProps<{
|
export default {
|
||||||
showLogo?: boolean;
|
name: "header-bar",
|
||||||
showMenu?: boolean;
|
props: ["showLogo", "showMenu"],
|
||||||
}>();
|
components: {
|
||||||
|
Action,
|
||||||
const layoutStore = useLayoutStore();
|
},
|
||||||
const slots = useSlots();
|
data: function () {
|
||||||
|
return {
|
||||||
const { t } = useI18n();
|
logoURL,
|
||||||
|
};
|
||||||
const ifActionsSlot = computed(() => (slots.actions ? true : false));
|
},
|
||||||
|
methods: {
|
||||||
|
openSidebar() {
|
||||||
|
this.$store.commit("showHover", "sidebar");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(["currentPromptName"]),
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
<template>
|
|
||||||
<VueFinalModal
|
|
||||||
class="vfm-modal"
|
|
||||||
overlay-transition="vfm-fade"
|
|
||||||
content-transition="vfm-fade"
|
|
||||||
@closed="layoutStore.closeHovers"
|
|
||||||
:focus-trap="{
|
|
||||||
initialFocus: '#focus-prompt',
|
|
||||||
fallbackFocus: 'div.vfm__content',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</VueFinalModal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { VueFinalModal } from "vue-final-modal";
|
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
|
||||||
|
|
||||||
const layoutStore = useLayoutStore();
|
|
||||||
</script>
|
|
@ -6,11 +6,8 @@
|
|||||||
|
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<p>{{ $t("prompts.copyMessage") }}</p>
|
<p>{{ $t("prompts.copyMessage") }}</p>
|
||||||
<file-list
|
<file-list ref="fileList" @update:selected="(val) => (dest = val)">
|
||||||
ref="fileList"
|
</file-list>
|
||||||
@update:selected="(val) => (dest = val)"
|
|
||||||
tabindex="1"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -31,20 +28,17 @@
|
|||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
class="button button--flat button--grey"
|
class="button button--flat button--grey"
|
||||||
@click="closeHovers"
|
@click="$store.commit('closeHovers')"
|
||||||
:aria-label="$t('buttons.cancel')"
|
:aria-label="$t('buttons.cancel')"
|
||||||
:title="$t('buttons.cancel')"
|
:title="$t('buttons.cancel')"
|
||||||
tabindex="3"
|
|
||||||
>
|
>
|
||||||
{{ $t("buttons.cancel") }}
|
{{ $t("buttons.cancel") }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
id="focus-prompt"
|
|
||||||
class="button button--flat"
|
class="button button--flat"
|
||||||
@click="copy"
|
@click="copy"
|
||||||
:aria-label="$t('buttons.copy')"
|
:aria-label="$t('buttons.copy')"
|
||||||
:title="$t('buttons.copy')"
|
:title="$t('buttons.copy')"
|
||||||
tabindex="2"
|
|
||||||
>
|
>
|
||||||
{{ $t("buttons.copy") }}
|
{{ $t("buttons.copy") }}
|
||||||
</button>
|
</button>
|
||||||
@ -54,10 +48,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
import { mapState } from "vuex";
|
||||||
import { useFileStore } from "@/stores/file";
|
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
|
||||||
import { useAuthStore } from "@/stores/auth";
|
|
||||||
import FileList from "./FileList.vue";
|
import FileList from "./FileList.vue";
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import buttons from "@/utils/buttons";
|
import buttons from "@/utils/buttons";
|
||||||
@ -72,20 +63,14 @@ export default {
|
|||||||
dest: null,
|
dest: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
inject: ["$showError"],
|
computed: mapState(["req", "selected", "user"]),
|
||||||
computed: {
|
|
||||||
...mapState(useFileStore, ["req", "selected"]),
|
|
||||||
...mapState(useAuthStore, ["user"]),
|
|
||||||
...mapWritableState(useFileStore, ["reload"]),
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
|
|
||||||
copy: async function (event) {
|
copy: async function (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const items = [];
|
let items = [];
|
||||||
|
|
||||||
// Create a new promise for each file.
|
// Create a new promise for each file.
|
||||||
for (const item of this.selected) {
|
for (let item of this.selected) {
|
||||||
items.push({
|
items.push({
|
||||||
from: this.req.items[item].url,
|
from: this.req.items[item].url,
|
||||||
to: this.dest + encodeURIComponent(this.req.items[item].name),
|
to: this.dest + encodeURIComponent(this.req.items[item].name),
|
||||||
@ -93,7 +78,7 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const action = async (overwrite, rename) => {
|
let action = async (overwrite, rename) => {
|
||||||
buttons.loading("copy");
|
buttons.loading("copy");
|
||||||
|
|
||||||
await api
|
await api
|
||||||
@ -102,7 +87,7 @@ export default {
|
|||||||
buttons.success("copy");
|
buttons.success("copy");
|
||||||
|
|
||||||
if (this.$route.path === this.dest) {
|
if (this.$route.path === this.dest) {
|
||||||
this.reload = true;
|
this.$store.commit("setReload", true);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -116,27 +101,27 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (this.$route.path === this.dest) {
|
if (this.$route.path === this.dest) {
|
||||||
this.closeHovers();
|
this.$store.commit("closeHovers");
|
||||||
action(false, true);
|
action(false, true);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dstItems = (await api.fetch(this.dest)).items;
|
let dstItems = (await api.fetch(this.dest)).items;
|
||||||
const conflict = upload.checkConflict(items, dstItems);
|
let conflict = upload.checkConflict(items, dstItems);
|
||||||
|
|
||||||
let overwrite = false;
|
let overwrite = false;
|
||||||
let rename = false;
|
let rename = false;
|
||||||
|
|
||||||
if (conflict) {
|
if (conflict) {
|
||||||
this.showHover({
|
this.$store.commit("showHover", {
|
||||||
prompt: "replace-rename",
|
prompt: "replace-rename",
|
||||||
confirm: (event, option) => {
|
confirm: (event, option) => {
|
||||||
overwrite = option == "overwrite";
|
overwrite = option == "overwrite";
|
||||||
rename = option == "rename";
|
rename = option == "rename";
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.closeHovers();
|
this.$store.commit("closeHovers");
|
||||||
action(overwrite, rename);
|
action(overwrite, rename);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -10,21 +10,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-action">
|
<div class="card-action">
|
||||||
<button
|
<button
|
||||||
@click="closeHovers"
|
@click="$store.commit('closeHovers')"
|
||||||
class="button button--flat button--grey"
|
class="button button--flat button--grey"
|
||||||
:aria-label="$t('buttons.cancel')"
|
:aria-label="$t('buttons.cancel')"
|
||||||
:title="$t('buttons.cancel')"
|
:title="$t('buttons.cancel')"
|
||||||
tabindex="2"
|
|
||||||
>
|
>
|
||||||
{{ $t("buttons.cancel") }}
|
{{ $t("buttons.cancel") }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
id="focus-prompt"
|
|
||||||
@click="submit"
|
@click="submit"
|
||||||
class="button button--flat button--red"
|
class="button button--flat button--red"
|
||||||
:aria-label="$t('buttons.delete')"
|
:aria-label="$t('buttons.delete')"
|
||||||
:title="$t('buttons.delete')"
|
:title="$t('buttons.delete')"
|
||||||
tabindex="1"
|
|
||||||
>
|
>
|
||||||
{{ $t("buttons.delete") }}
|
{{ $t("buttons.delete") }}
|
||||||
</button>
|
</button>
|
||||||
@ -33,27 +30,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
import { mapGetters, mapMutations, mapState } from "vuex";
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import buttons from "@/utils/buttons";
|
import buttons from "@/utils/buttons";
|
||||||
import { useFileStore } from "@/stores/file";
|
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "delete",
|
name: "delete",
|
||||||
inject: ["$showError"],
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useFileStore, [
|
...mapGetters(["isListing", "selectedCount", "currentPrompt"]),
|
||||||
"isListing",
|
...mapState(["req", "selected"]),
|
||||||
"selectedCount",
|
|
||||||
"req",
|
|
||||||
"selected",
|
|
||||||
"currentPrompt",
|
|
||||||
]),
|
|
||||||
...mapWritableState(useFileStore, ["reload"]),
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
...mapMutations(["closeHovers"]),
|
||||||
submit: async function () {
|
submit: async function () {
|
||||||
buttons.loading("delete");
|
buttons.loading("delete");
|
||||||
|
|
||||||
@ -74,18 +62,18 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const promises = [];
|
let promises = [];
|
||||||
for (const index of this.selected) {
|
for (let index of this.selected) {
|
||||||
promises.push(api.remove(this.req.items[index].url));
|
promises.push(api.remove(this.req.items[index].url));
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
buttons.success("delete");
|
buttons.success("delete");
|
||||||
this.reload = true;
|
this.$store.commit("setReload", true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
buttons.done("delete");
|
buttons.done("delete");
|
||||||
this.$showError(e);
|
this.$showError(e);
|
||||||
if (this.isListing) this.reload = true;
|
if (this.isListing) this.$store.commit("setReload", true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,40 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="card floating">
|
|
||||||
<div class="card-content">
|
|
||||||
<p>{{ t("prompts.deleteUser") }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-action">
|
|
||||||
<button
|
|
||||||
id="focus-prompt"
|
|
||||||
class="button button--flat button--grey"
|
|
||||||
@click="layoutStore.closeHovers"
|
|
||||||
:aria-label="t('buttons.cancel')"
|
|
||||||
:title="t('buttons.cancel')"
|
|
||||||
tabindex="1"
|
|
||||||
>
|
|
||||||
{{ t("buttons.cancel") }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="button button--flat"
|
|
||||||
@click="layoutStore.currentPrompt?.confirm"
|
|
||||||
tabindex="2"
|
|
||||||
>
|
|
||||||
{{ t("buttons.delete") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
|
||||||
import { useI18n } from "vue-i18n";
|
|
||||||
|
|
||||||
const layoutStore = useLayoutStore();
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
// const emit = defineEmits<{
|
|
||||||
// (e: "confirm"): void;
|
|
||||||
// }>();
|
|
||||||
</script>
|
|
@ -7,21 +7,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-action">
|
<div class="card-action">
|
||||||
<button
|
<button
|
||||||
|
@click="$store.commit('closeHovers')"
|
||||||
class="button button--flat button--grey"
|
class="button button--flat button--grey"
|
||||||
@click="closeHovers"
|
|
||||||
:aria-label="$t('buttons.cancel')"
|
:aria-label="$t('buttons.cancel')"
|
||||||
:title="$t('buttons.cancel')"
|
:title="$t('buttons.cancel')"
|
||||||
tabindex="2"
|
|
||||||
>
|
>
|
||||||
{{ $t("buttons.cancel") }}
|
{{ $t("buttons.cancel") }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
id="focus-prompt"
|
|
||||||
@click="submit"
|
@click="submit"
|
||||||
class="button button--flat button--red"
|
class="button button--flat button--red"
|
||||||
:aria-label="$t('buttons.discardChanges')"
|
:aria-label="$t('buttons.discardChanges')"
|
||||||
:title="$t('buttons.discardChanges')"
|
:title="$t('buttons.discardChanges')"
|
||||||
tabindex="1"
|
|
||||||
>
|
>
|
||||||
{{ $t("buttons.discardChanges") }}
|
{{ $t("buttons.discardChanges") }}
|
||||||
</button>
|
</button>
|
||||||
@ -30,20 +27,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapActions } from "pinia";
|
import { mapMutations } from "vuex";
|
||||||
import url from "@/utils/url";
|
import url from "@/utils/url";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
|
||||||
import { useFileStore } from "@/stores/file";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "discardEditorChanges",
|
name: "discardEditorChanges",
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
...mapMutations(["closeHovers"]),
|
||||||
...mapActions(useFileStore, ["updateRequest"]),
|
|
||||||
submit: async function () {
|
submit: async function () {
|
||||||
this.updateRequest(null);
|
this.$store.commit("updateRequest", {});
|
||||||
|
|
||||||
const uri = url.removeLastDir(this.$route.path) + "/";
|
let uri = url.removeLastDir(this.$route.path) + "/";
|
||||||
this.$router.push({ path: uri });
|
this.$router.push({ path: uri });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card floating" id="download">
|
<div class="card floating" id="download">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<h2>{{ t("prompts.download") }}</h2>
|
<h2>{{ $t("prompts.download") }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<p>{{ t("prompts.downloadMessage") }}</p>
|
<p>{{ $t("prompts.downloadMessage") }}</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
id="focus-prompt"
|
|
||||||
v-for="(ext, format) in formats"
|
v-for="(ext, format) in formats"
|
||||||
:key="format"
|
:key="format"
|
||||||
class="button button--block"
|
class="button button--block"
|
||||||
@click="layoutStore.currentPrompt?.confirm(format)"
|
@click="currentPrompt.confirm(format)"
|
||||||
|
v-focus
|
||||||
>
|
>
|
||||||
{{ ext }}
|
{{ ext }}
|
||||||
</button>
|
</button>
|
||||||
@ -20,21 +20,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script>
|
||||||
import { useI18n } from "vue-i18n";
|
import { mapGetters } from "vuex";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
|
||||||
|
|
||||||
const layoutStore = useLayoutStore();
|
export default {
|
||||||
|
name: "download",
|
||||||
const { t } = useI18n();
|
data: function () {
|
||||||
|
return {
|
||||||
const formats = {
|
formats: {
|
||||||
zip: "zip",
|
zip: "zip",
|
||||||
tar: "tar",
|
tar: "tar",
|
||||||
targz: "tar.gz",
|
targz: "tar.gz",
|
||||||
tarbz2: "tar.bz2",
|
tarbz2: "tar.bz2",
|
||||||
tarxz: "tar.xz",
|
tarxz: "tar.xz",
|
||||||
tarlz4: "tar.lz4",
|
tarlz4: "tar.lz4",
|
||||||
tarsz: "tar.sz",
|
tarsz: "tar.sz",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(["currentPrompt"]),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -25,10 +25,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from "pinia";
|
import { mapState } from "vuex";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
|
||||||
import { useFileStore } from "@/stores/file";
|
|
||||||
|
|
||||||
import url from "@/utils/url";
|
import url from "@/utils/url";
|
||||||
import { files } from "@/api";
|
import { files } from "@/api";
|
||||||
|
|
||||||
@ -45,10 +42,8 @@ export default {
|
|||||||
current: window.location.pathname,
|
current: window.location.pathname,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
inject: ["$showError"],
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useAuthStore, ["user"]),
|
...mapState(["req", "user"]),
|
||||||
...mapState(useFileStore, ["req"]),
|
|
||||||
nav() {
|
nav() {
|
||||||
return decodeURIComponent(this.current);
|
return decodeURIComponent(this.current);
|
||||||
},
|
},
|
||||||
@ -80,7 +75,7 @@ export default {
|
|||||||
|
|
||||||
// Otherwise we add every directory to the
|
// Otherwise we add every directory to the
|
||||||
// move options.
|
// move options.
|
||||||
for (const item of req.items) {
|
for (let item of req.items) {
|
||||||
if (!item.isDir) continue;
|
if (!item.isDir) continue;
|
||||||
|
|
||||||
this.items.push({
|
this.items.push({
|
||||||
@ -93,12 +88,12 @@ export default {
|
|||||||
// Retrieves the URL of the directory the user
|
// Retrieves the URL of the directory the user
|
||||||
// just clicked in and fill the options with its
|
// just clicked in and fill the options with its
|
||||||
// content.
|
// content.
|
||||||
const uri = event.currentTarget.dataset.url;
|
let uri = event.currentTarget.dataset.url;
|
||||||
|
|
||||||
files.fetch(uri).then(this.fillOptions).catch(this.$showError);
|
files.fetch(uri).then(this.fillOptions).catch(this.$showError);
|
||||||
},
|
},
|
||||||
touchstart(event) {
|
touchstart(event) {
|
||||||
const url = event.currentTarget.dataset.url;
|
let url = event.currentTarget.dataset.url;
|
||||||
|
|
||||||
// In 300 milliseconds, we shall reset the count.
|
// In 300 milliseconds, we shall reset the count.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -20,13 +20,11 @@
|
|||||||
|
|
||||||
<div class="card-action">
|
<div class="card-action">
|
||||||
<button
|
<button
|
||||||
id="focus-prompt"
|
|
||||||
type="submit"
|
type="submit"
|
||||||
@click="closeHovers"
|
@click="$store.commit('closeHovers')"
|
||||||
class="button button--flat"
|
class="button button--flat"
|
||||||
:aria-label="$t('buttons.ok')"
|
:aria-label="$t('buttons.ok')"
|
||||||
:title="$t('buttons.ok')"
|
:title="$t('buttons.ok')"
|
||||||
tabindex="1"
|
|
||||||
>
|
>
|
||||||
{{ $t("buttons.ok") }}
|
{{ $t("buttons.ok") }}
|
||||||
</button>
|
</button>
|
||||||
@ -35,13 +33,5 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapActions } from "pinia";
|
export default { name: "help" };
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "help",
|
|
||||||
methods: {
|
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -40,45 +40,33 @@
|
|||||||
<p>
|
<p>
|
||||||
<strong>MD5: </strong
|
<strong>MD5: </strong
|
||||||
><code
|
><code
|
||||||
><a
|
><a @click="checksum($event, 'md5')">{{
|
||||||
@click="checksum($event, 'md5')"
|
$t("prompts.show")
|
||||||
@keypress.enter="checksum($event, 'md5')"
|
}}</a></code
|
||||||
tabindex="2"
|
|
||||||
>{{ $t("prompts.show") }}</a
|
|
||||||
></code
|
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>SHA1: </strong
|
<strong>SHA1: </strong
|
||||||
><code
|
><code
|
||||||
><a
|
><a @click="checksum($event, 'sha1')">{{
|
||||||
@click="checksum($event, 'sha1')"
|
$t("prompts.show")
|
||||||
@keypress.enter="checksum($event, 'sha1')"
|
}}</a></code
|
||||||
tabindex="3"
|
|
||||||
>{{ $t("prompts.show") }}</a
|
|
||||||
></code
|
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>SHA256: </strong
|
<strong>SHA256: </strong
|
||||||
><code
|
><code
|
||||||
><a
|
><a @click="checksum($event, 'sha256')">{{
|
||||||
@click="checksum($event, 'sha256')"
|
$t("prompts.show")
|
||||||
@keypress.enter="checksum($event, 'sha256')"
|
}}</a></code
|
||||||
tabindex="4"
|
|
||||||
>{{ $t("prompts.show") }}</a
|
|
||||||
></code
|
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>SHA512: </strong
|
<strong>SHA512: </strong
|
||||||
><code
|
><code
|
||||||
><a
|
><a @click="checksum($event, 'sha512')">{{
|
||||||
@click="checksum($event, 'sha512')"
|
$t("prompts.show")
|
||||||
@keypress.enter="checksum($event, 'sha512')"
|
}}</a></code
|
||||||
tabindex="5"
|
|
||||||
>{{ $t("prompts.show") }}</a
|
|
||||||
></code
|
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
@ -86,9 +74,8 @@
|
|||||||
|
|
||||||
<div class="card-action">
|
<div class="card-action">
|
||||||
<button
|
<button
|
||||||
id="focus-prompt"
|
|
||||||
type="submit"
|
type="submit"
|
||||||
@click="closeHovers"
|
@click="$store.commit('closeHovers')"
|
||||||
class="button button--flat"
|
class="button button--flat"
|
||||||
:aria-label="$t('buttons.ok')"
|
:aria-label="$t('buttons.ok')"
|
||||||
:title="$t('buttons.ok')"
|
:title="$t('buttons.ok')"
|
||||||
@ -100,23 +87,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapActions, mapState } from "pinia";
|
import { mapState, mapGetters } from "vuex";
|
||||||
import { useFileStore } from "@/stores/file";
|
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
|
||||||
import { filesize } from "@/utils";
|
import { filesize } from "@/utils";
|
||||||
import dayjs from "dayjs";
|
import moment from "moment/min/moment-with-locales";
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "info",
|
name: "info",
|
||||||
inject: ["$showError"],
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useFileStore, [
|
...mapState(["req", "selected"]),
|
||||||
"req",
|
...mapGetters(["selectedCount", "isListing"]),
|
||||||
"selected",
|
|
||||||
"selectedCount",
|
|
||||||
"isListing",
|
|
||||||
]),
|
|
||||||
humanSize: function () {
|
humanSize: function () {
|
||||||
if (this.selectedCount === 0 || !this.isListing) {
|
if (this.selectedCount === 0 || !this.isListing) {
|
||||||
return filesize(this.req.size);
|
return filesize(this.req.size);
|
||||||
@ -124,7 +104,7 @@ export default {
|
|||||||
|
|
||||||
let sum = 0;
|
let sum = 0;
|
||||||
|
|
||||||
for (const selected of this.selected) {
|
for (let selected of this.selected) {
|
||||||
sum += this.req.items[selected].size;
|
sum += this.req.items[selected].size;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,19 +112,13 @@ export default {
|
|||||||
},
|
},
|
||||||
humanTime: function () {
|
humanTime: function () {
|
||||||
if (this.selectedCount === 0) {
|
if (this.selectedCount === 0) {
|
||||||
return dayjs(this.req.modified).fromNow();
|
return moment(this.req.modified).fromNow();
|
||||||
}
|
}
|
||||||
|
|
||||||
return dayjs(this.req.items[this.selected[0]].modified).fromNow();
|
return moment(this.req.items[this.selected[0]].modified).fromNow();
|
||||||
},
|
},
|
||||||
modTime: function () {
|
modTime: function () {
|
||||||
if (this.selectedCount === 0) {
|
return new Date(Date.parse(this.req.modified)).toLocaleString();
|
||||||
return new Date(Date.parse(this.req.modified)).toLocaleString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Date(
|
|
||||||
Date.parse(this.req.items[this.selected[0]].modified)
|
|
||||||
).toLocaleString();
|
|
||||||
},
|
},
|
||||||
name: function () {
|
name: function () {
|
||||||
return this.selectedCount === 0
|
return this.selectedCount === 0
|
||||||
@ -172,7 +146,6 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
|
||||||
checksum: async function (event, algo) {
|
checksum: async function (event, algo) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
@ -186,7 +159,8 @@ export default {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const hash = await api.checksum(link, algo);
|
const hash = await api.checksum(link, algo);
|
||||||
event.target.textContent = hash;
|
// eslint-disable-next-line
|
||||||
|
event.target.innerHTML = hash;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.$showError(e);
|
this.$showError(e);
|
||||||
}
|
}
|
||||||
|
@ -5,11 +5,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<file-list
|
<file-list ref="fileList" @update:selected="(val) => (dest = val)">
|
||||||
ref="fileList"
|
</file-list>
|
||||||
@update:selected="(val) => (dest = val)"
|
|
||||||
tabindex="1"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -30,21 +27,18 @@
|
|||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
class="button button--flat button--grey"
|
class="button button--flat button--grey"
|
||||||
@click="closeHovers"
|
@click="$store.commit('closeHovers')"
|
||||||
:aria-label="$t('buttons.cancel')"
|
:aria-label="$t('buttons.cancel')"
|
||||||
:title="$t('buttons.cancel')"
|
:title="$t('buttons.cancel')"
|
||||||
tabindex="3"
|
|
||||||
>
|
>
|
||||||
{{ $t("buttons.cancel") }}
|
{{ $t("buttons.cancel") }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
id="focus-prompt"
|
|
||||||
class="button button--flat"
|
class="button button--flat"
|
||||||
@click="move"
|
@click="move"
|
||||||
:disabled="$route.path === dest"
|
:disabled="$route.path === dest"
|
||||||
:aria-label="$t('buttons.move')"
|
:aria-label="$t('buttons.move')"
|
||||||
:title="$t('buttons.move')"
|
:title="$t('buttons.move')"
|
||||||
tabindex="2"
|
|
||||||
>
|
>
|
||||||
{{ $t("buttons.move") }}
|
{{ $t("buttons.move") }}
|
||||||
</button>
|
</button>
|
||||||
@ -54,10 +48,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapActions, mapState } from "pinia";
|
import { mapState } from "vuex";
|
||||||
import { useFileStore } from "@/stores/file";
|
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
|
||||||
import { useAuthStore } from "@/stores/auth";
|
|
||||||
import FileList from "./FileList.vue";
|
import FileList from "./FileList.vue";
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import buttons from "@/utils/buttons";
|
import buttons from "@/utils/buttons";
|
||||||
@ -72,18 +63,13 @@ export default {
|
|||||||
dest: null,
|
dest: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
inject: ["$showError"],
|
computed: mapState(["req", "selected", "user"]),
|
||||||
computed: {
|
|
||||||
...mapState(useFileStore, ["req", "selected"]),
|
|
||||||
...mapState(useAuthStore, ["user"]),
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
|
|
||||||
move: async function (event) {
|
move: async function (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const items = [];
|
let items = [];
|
||||||
|
|
||||||
for (const item of this.selected) {
|
for (let item of this.selected) {
|
||||||
items.push({
|
items.push({
|
||||||
from: this.req.items[item].url,
|
from: this.req.items[item].url,
|
||||||
to: this.dest + encodeURIComponent(this.req.items[item].name),
|
to: this.dest + encodeURIComponent(this.req.items[item].name),
|
||||||
@ -91,7 +77,7 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const action = async (overwrite, rename) => {
|
let action = async (overwrite, rename) => {
|
||||||
buttons.loading("move");
|
buttons.loading("move");
|
||||||
|
|
||||||
await api
|
await api
|
||||||
@ -106,21 +92,21 @@ export default {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const dstItems = (await api.fetch(this.dest)).items;
|
let dstItems = (await api.fetch(this.dest)).items;
|
||||||
const conflict = upload.checkConflict(items, dstItems);
|
let conflict = upload.checkConflict(items, dstItems);
|
||||||
|
|
||||||
let overwrite = false;
|
let overwrite = false;
|
||||||
let rename = false;
|
let rename = false;
|
||||||
|
|
||||||
if (conflict) {
|
if (conflict) {
|
||||||
this.showHover({
|
this.$store.commit("showHover", {
|
||||||
prompt: "replace-rename",
|
prompt: "replace-rename",
|
||||||
confirm: (event, option) => {
|
confirm: (event, option) => {
|
||||||
overwrite = option == "overwrite";
|
overwrite = option == "overwrite";
|
||||||
rename = option == "rename";
|
rename = option == "rename";
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.closeHovers();
|
this.$store.commit("closeHovers");
|
||||||
action(overwrite, rename);
|
action(overwrite, rename);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,104 +1,98 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card floating">
|
<div class="card floating">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<h2>{{ t("prompts.newDir") }}</h2>
|
<h2>{{ $t("prompts.newDir") }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<p>{{ t("prompts.newDirMessage") }}</p>
|
<p>{{ $t("prompts.newDirMessage") }}</p>
|
||||||
<input
|
<input
|
||||||
id="focus-prompt"
|
|
||||||
class="input input--block"
|
class="input input--block"
|
||||||
type="text"
|
type="text"
|
||||||
@keyup.enter="submit"
|
@keyup.enter="submit"
|
||||||
v-model.trim="name"
|
v-model.trim="name"
|
||||||
tabindex="1"
|
v-focus
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-action">
|
<div class="card-action">
|
||||||
<button
|
<button
|
||||||
class="button button--flat button--grey"
|
class="button button--flat button--grey"
|
||||||
@click="layoutStore.closeHovers"
|
@click="$store.commit('closeHovers')"
|
||||||
:aria-label="t('buttons.cancel')"
|
:aria-label="$t('buttons.cancel')"
|
||||||
:title="t('buttons.cancel')"
|
:title="$t('buttons.cancel')"
|
||||||
tabindex="3"
|
|
||||||
>
|
>
|
||||||
{{ t("buttons.cancel") }}
|
{{ $t("buttons.cancel") }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="button button--flat"
|
class="button button--flat"
|
||||||
:aria-label="$t('buttons.create')"
|
:aria-label="$t('buttons.create')"
|
||||||
:title="t('buttons.create')"
|
:title="$t('buttons.create')"
|
||||||
@click="submit"
|
@click="submit"
|
||||||
tabindex="2"
|
|
||||||
>
|
>
|
||||||
{{ t("buttons.create") }}
|
{{ $t("buttons.create") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script>
|
||||||
import { inject, ref } from "vue";
|
import { mapGetters } from "vuex";
|
||||||
import { useFileStore } from "@/stores/file";
|
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
|
||||||
|
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import url from "@/utils/url";
|
import url from "@/utils/url";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
|
||||||
import { useI18n } from "vue-i18n";
|
|
||||||
|
|
||||||
const $showError = inject<IToastError>("$showError")!;
|
export default {
|
||||||
|
name: "new-dir",
|
||||||
const props = defineProps({
|
props: {
|
||||||
base: String,
|
redirect: {
|
||||||
redirect: {
|
type: Boolean,
|
||||||
type: Boolean,
|
default: true,
|
||||||
default: true,
|
},
|
||||||
|
base: {
|
||||||
|
type: [String, null],
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
data: function () {
|
||||||
|
return {
|
||||||
|
name: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(["isFiles", "isListing"]),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
submit: async function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (this.new === "") return;
|
||||||
|
|
||||||
const fileStore = useFileStore();
|
// Build the path of the new directory.
|
||||||
const layoutStore = useLayoutStore();
|
let uri;
|
||||||
|
|
||||||
const route = useRoute();
|
if (this.base) uri = this.base;
|
||||||
const router = useRouter();
|
else if (this.isFiles) uri = this.$route.path + "/";
|
||||||
const { t } = useI18n();
|
else uri = "/";
|
||||||
|
|
||||||
const name = ref<string>("");
|
if (!this.isListing) {
|
||||||
|
uri = url.removeLastDir(uri) + "/";
|
||||||
|
}
|
||||||
|
|
||||||
const submit = async (event: Event) => {
|
uri += encodeURIComponent(this.name) + "/";
|
||||||
event.preventDefault();
|
uri = uri.replace("//", "/");
|
||||||
if (name.value === "") return;
|
try {
|
||||||
|
await api.post(uri);
|
||||||
|
if (this.redirect) {
|
||||||
|
this.$router.push({ path: uri });
|
||||||
|
} else if (!this.base) {
|
||||||
|
const res = await api.fetch(url.removeLastDir(uri) + "/");
|
||||||
|
this.$store.commit("updateRequest", res);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$showError(e);
|
||||||
|
}
|
||||||
|
|
||||||
// Build the path of the new directory.
|
this.$store.commit("closeHovers");
|
||||||
let uri: string;
|
},
|
||||||
if (props.base) uri = props.base;
|
},
|
||||||
else if (fileStore.isFiles) uri = route.path + "/";
|
|
||||||
else uri = "/";
|
|
||||||
|
|
||||||
if (!fileStore.isListing) {
|
|
||||||
uri = url.removeLastDir(uri) + "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
uri += encodeURIComponent(name.value) + "/";
|
|
||||||
uri = uri.replace("//", "/");
|
|
||||||
|
|
||||||
try {
|
|
||||||
await api.post(uri);
|
|
||||||
if (props.redirect) {
|
|
||||||
router.push({ path: uri });
|
|
||||||
} else if (!props.base) {
|
|
||||||
const res = await api.fetch(url.removeLastDir(uri) + "/");
|
|
||||||
fileStore.updateRequest(res);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Error) {
|
|
||||||
$showError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layoutStore.closeHovers();
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card floating">
|
<div class="card floating">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<h2>{{ t("prompts.newFile") }}</h2>
|
<h2>{{ $t("prompts.newFile") }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<p>{{ t("prompts.newFileMessage") }}</p>
|
<p>{{ $t("prompts.newFileMessage") }}</p>
|
||||||
<input
|
<input
|
||||||
id="focus-prompt"
|
|
||||||
class="input input--block"
|
class="input input--block"
|
||||||
|
v-focus
|
||||||
type="text"
|
type="text"
|
||||||
@keyup.enter="submit"
|
@keyup.enter="submit"
|
||||||
v-model.trim="name"
|
v-model.trim="name"
|
||||||
@ -18,68 +18,63 @@
|
|||||||
<div class="card-action">
|
<div class="card-action">
|
||||||
<button
|
<button
|
||||||
class="button button--flat button--grey"
|
class="button button--flat button--grey"
|
||||||
@click="layoutStore.closeHovers"
|
@click="$store.commit('closeHovers')"
|
||||||
:aria-label="t('buttons.cancel')"
|
:aria-label="$t('buttons.cancel')"
|
||||||
:title="t('buttons.cancel')"
|
:title="$t('buttons.cancel')"
|
||||||
>
|
>
|
||||||
{{ t("buttons.cancel") }}
|
{{ $t("buttons.cancel") }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="button button--flat"
|
class="button button--flat"
|
||||||
@click="submit"
|
@click="submit"
|
||||||
:aria-label="t('buttons.create')"
|
:aria-label="$t('buttons.create')"
|
||||||
:title="t('buttons.create')"
|
:title="$t('buttons.create')"
|
||||||
>
|
>
|
||||||
{{ t("buttons.create") }}
|
{{ $t("buttons.create") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script>
|
||||||
import { inject, ref } from "vue";
|
import { mapGetters } from "vuex";
|
||||||
import { useI18n } from "vue-i18n";
|
|
||||||
import { useRoute, useRouter } from "vue-router";
|
|
||||||
import { useFileStore } from "@/stores/file";
|
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
|
||||||
|
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import url from "@/utils/url";
|
import url from "@/utils/url";
|
||||||
|
|
||||||
const $showError = inject<IToastError>("$showError")!;
|
export default {
|
||||||
|
name: "new-file",
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
name: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(["isFiles", "isListing"]),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
submit: async function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (this.new === "") return;
|
||||||
|
|
||||||
const fileStore = useFileStore();
|
// Build the path of the new directory.
|
||||||
const layoutStore = useLayoutStore();
|
let uri = this.isFiles ? this.$route.path + "/" : "/";
|
||||||
|
|
||||||
const route = useRoute();
|
if (!this.isListing) {
|
||||||
const router = useRouter();
|
uri = url.removeLastDir(uri) + "/";
|
||||||
const { t } = useI18n();
|
}
|
||||||
|
|
||||||
const name = ref<string>("");
|
uri += encodeURIComponent(this.name);
|
||||||
|
uri = uri.replace("//", "/");
|
||||||
|
|
||||||
const submit = async (event: Event) => {
|
try {
|
||||||
event.preventDefault();
|
await api.post(uri);
|
||||||
if (name.value === "") return;
|
this.$router.push({ path: uri });
|
||||||
|
} catch (e) {
|
||||||
|
this.$showError(e);
|
||||||
|
}
|
||||||
|
|
||||||
// Build the path of the new directory.
|
this.$store.commit("closeHovers");
|
||||||
let uri = fileStore.isFiles ? route.path + "/" : "/";
|
},
|
||||||
|
},
|
||||||
if (!fileStore.isListing) {
|
|
||||||
uri = url.removeLastDir(uri) + "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
uri += encodeURIComponent(name.value);
|
|
||||||
uri = uri.replace("//", "/");
|
|
||||||
|
|
||||||
try {
|
|
||||||
await api.post(uri);
|
|
||||||
router.push({ path: uri });
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Error) {
|
|
||||||
$showError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layoutStore.closeHovers();
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,20 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<ModalsContainer />
|
<div>
|
||||||
|
<component
|
||||||
|
v-if="showOverlay"
|
||||||
|
:ref="currentPromptName"
|
||||||
|
:is="currentPromptName"
|
||||||
|
v-bind="currentPrompt.props"
|
||||||
|
>
|
||||||
|
</component>
|
||||||
|
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script>
|
||||||
import { watch } from "vue";
|
|
||||||
import { ModalsContainer, useModal } from "vue-final-modal";
|
|
||||||
import { storeToRefs } from "pinia";
|
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
|
||||||
|
|
||||||
import BaseModal from "./BaseModal.vue";
|
|
||||||
import Help from "./Help.vue";
|
import Help from "./Help.vue";
|
||||||
import Info from "./Info.vue";
|
import Info from "./Info.vue";
|
||||||
import Delete from "./Delete.vue";
|
import Delete from "./Delete.vue";
|
||||||
import DeleteUser from "./DeleteUser.vue";
|
|
||||||
import Download from "./Download.vue";
|
|
||||||
import Rename from "./Rename.vue";
|
import Rename from "./Rename.vue";
|
||||||
|
import Download from "./Download.vue";
|
||||||
import Move from "./Move.vue";
|
import Move from "./Move.vue";
|
||||||
import Copy from "./Copy.vue";
|
import Copy from "./Copy.vue";
|
||||||
import NewFile from "./NewFile.vue";
|
import NewFile from "./NewFile.vue";
|
||||||
@ -22,54 +24,87 @@ import NewDir from "./NewDir.vue";
|
|||||||
import Replace from "./Replace.vue";
|
import Replace from "./Replace.vue";
|
||||||
import ReplaceRename from "./ReplaceRename.vue";
|
import ReplaceRename from "./ReplaceRename.vue";
|
||||||
import Share from "./Share.vue";
|
import Share from "./Share.vue";
|
||||||
import ShareDelete from "./ShareDelete.vue";
|
|
||||||
import Upload from "./Upload.vue";
|
import Upload from "./Upload.vue";
|
||||||
|
import ShareDelete from "./ShareDelete.vue";
|
||||||
|
import Sidebar from "../Sidebar.vue";
|
||||||
import DiscardEditorChanges from "./DiscardEditorChanges.vue";
|
import DiscardEditorChanges from "./DiscardEditorChanges.vue";
|
||||||
|
import { mapGetters, mapState } from "vuex";
|
||||||
|
import buttons from "@/utils/buttons";
|
||||||
|
|
||||||
const layoutStore = useLayoutStore();
|
export default {
|
||||||
|
name: "prompts",
|
||||||
|
components: {
|
||||||
|
Info,
|
||||||
|
Delete,
|
||||||
|
Rename,
|
||||||
|
Download,
|
||||||
|
Move,
|
||||||
|
Copy,
|
||||||
|
Share,
|
||||||
|
NewFile,
|
||||||
|
NewDir,
|
||||||
|
Help,
|
||||||
|
Replace,
|
||||||
|
ReplaceRename,
|
||||||
|
Upload,
|
||||||
|
ShareDelete,
|
||||||
|
Sidebar,
|
||||||
|
DiscardEditorChanges,
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
pluginData: {
|
||||||
|
buttons,
|
||||||
|
store: this.$store,
|
||||||
|
router: this.$router,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
window.addEventListener("keydown", (event) => {
|
||||||
|
if (this.currentPrompt == null) return;
|
||||||
|
|
||||||
const { currentPromptName } = storeToRefs(layoutStore);
|
const promptName = this.currentPrompt.prompt;
|
||||||
|
const prompt = this.$refs[promptName];
|
||||||
|
|
||||||
const components = new Map<string, any>([
|
if (event.code === "Escape") {
|
||||||
["info", Info],
|
event.stopImmediatePropagation();
|
||||||
["help", Help],
|
this.$store.commit("closeHovers");
|
||||||
["delete", Delete],
|
}
|
||||||
["rename", Rename],
|
|
||||||
["move", Move],
|
|
||||||
["copy", Copy],
|
|
||||||
["newFile", NewFile],
|
|
||||||
["newDir", NewDir],
|
|
||||||
["download", Download],
|
|
||||||
["replace", Replace],
|
|
||||||
["replace-rename", ReplaceRename],
|
|
||||||
["share", Share],
|
|
||||||
["upload", Upload],
|
|
||||||
["share-delete", ShareDelete],
|
|
||||||
["deleteUser", DeleteUser],
|
|
||||||
["discardEditorChanges", DiscardEditorChanges],
|
|
||||||
]);
|
|
||||||
|
|
||||||
watch(currentPromptName, (newValue) => {
|
if (event.code === "Enter") {
|
||||||
const modal = components.get(newValue!);
|
switch (promptName) {
|
||||||
if (!modal) return;
|
case "delete":
|
||||||
|
prompt.submit();
|
||||||
const { open, close } = useModal({
|
break;
|
||||||
component: BaseModal,
|
case "copy":
|
||||||
slots: {
|
prompt.copy(event);
|
||||||
default: modal,
|
break;
|
||||||
|
case "move":
|
||||||
|
prompt.move(event);
|
||||||
|
break;
|
||||||
|
case "replace":
|
||||||
|
prompt.showConfirm(event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(["plugins"]),
|
||||||
|
...mapGetters(["currentPrompt", "currentPromptName"]),
|
||||||
|
showOverlay: function () {
|
||||||
|
return (
|
||||||
|
this.currentPrompt !== null &&
|
||||||
|
this.currentPrompt.prompt !== "search" &&
|
||||||
|
this.currentPrompt.prompt !== "more"
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
methods: {
|
||||||
layoutStore.setCloseOnPrompt(close, newValue!);
|
resetPrompts() {
|
||||||
open();
|
this.$store.commit("closeHovers");
|
||||||
});
|
},
|
||||||
|
},
|
||||||
window.addEventListener("keydown", (event) => {
|
};
|
||||||
if (!layoutStore.currentPrompt) return;
|
|
||||||
|
|
||||||
if (event.key === "Escape") {
|
|
||||||
event.stopImmediatePropagation();
|
|
||||||
layoutStore.closeHovers();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -10,8 +10,8 @@
|
|||||||
>:
|
>:
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
id="focus-prompt"
|
|
||||||
class="input input--block"
|
class="input input--block"
|
||||||
|
v-focus
|
||||||
type="text"
|
type="text"
|
||||||
@keyup.enter="submit"
|
@keyup.enter="submit"
|
||||||
v-model.trim="name"
|
v-model.trim="name"
|
||||||
@ -21,7 +21,7 @@
|
|||||||
<div class="card-action">
|
<div class="card-action">
|
||||||
<button
|
<button
|
||||||
class="button button--flat button--grey"
|
class="button button--flat button--grey"
|
||||||
@click="closeHovers"
|
@click="$store.commit('closeHovers')"
|
||||||
:aria-label="$t('buttons.cancel')"
|
:aria-label="$t('buttons.cancel')"
|
||||||
:title="$t('buttons.cancel')"
|
:title="$t('buttons.cancel')"
|
||||||
>
|
>
|
||||||
@ -41,9 +41,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
import { mapState, mapGetters } from "vuex";
|
||||||
import { useFileStore } from "@/stores/file";
|
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
|
||||||
import url from "@/utils/url";
|
import url from "@/utils/url";
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
|
|
||||||
@ -57,20 +55,13 @@ export default {
|
|||||||
created() {
|
created() {
|
||||||
this.name = this.oldName();
|
this.name = this.oldName();
|
||||||
},
|
},
|
||||||
inject: ["$showError"],
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useFileStore, [
|
...mapState(["req", "selected", "selectedCount"]),
|
||||||
"req",
|
...mapGetters(["isListing"]),
|
||||||
"selected",
|
|
||||||
"selectedCount",
|
|
||||||
"isListing",
|
|
||||||
]),
|
|
||||||
...mapWritableState(useFileStore, ["reload"]),
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
|
||||||
cancel: function () {
|
cancel: function () {
|
||||||
this.closeHovers();
|
this.$store.commit("closeHovers");
|
||||||
},
|
},
|
||||||
oldName: function () {
|
oldName: function () {
|
||||||
if (!this.isListing) {
|
if (!this.isListing) {
|
||||||
@ -105,12 +96,12 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.reload = true;
|
this.$store.commit("setReload", true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.$showError(e);
|
this.$showError(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.closeHovers();
|
this.$store.commit("closeHovers");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -11,10 +11,9 @@
|
|||||||
<div class="card-action">
|
<div class="card-action">
|
||||||
<button
|
<button
|
||||||
class="button button--flat button--grey"
|
class="button button--flat button--grey"
|
||||||
@click="closeHovers"
|
@click="$store.commit('closeHovers')"
|
||||||
:aria-label="$t('buttons.cancel')"
|
:aria-label="$t('buttons.cancel')"
|
||||||
:title="$t('buttons.cancel')"
|
:title="$t('buttons.cancel')"
|
||||||
tabindex="3"
|
|
||||||
>
|
>
|
||||||
{{ $t("buttons.cancel") }}
|
{{ $t("buttons.cancel") }}
|
||||||
</button>
|
</button>
|
||||||
@ -23,17 +22,14 @@
|
|||||||
@click="currentPrompt.action"
|
@click="currentPrompt.action"
|
||||||
:aria-label="$t('buttons.continue')"
|
:aria-label="$t('buttons.continue')"
|
||||||
:title="$t('buttons.continue')"
|
:title="$t('buttons.continue')"
|
||||||
tabindex="2"
|
|
||||||
>
|
>
|
||||||
{{ $t("buttons.continue") }}
|
{{ $t("buttons.continue") }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
id="focus-prompt"
|
|
||||||
class="button button--flat button--red"
|
class="button button--flat button--red"
|
||||||
@click="currentPrompt.confirm"
|
@click="currentPrompt.confirm"
|
||||||
:aria-label="$t('buttons.replace')"
|
:aria-label="$t('buttons.replace')"
|
||||||
:title="$t('buttons.replace')"
|
:title="$t('buttons.replace')"
|
||||||
tabindex="1"
|
|
||||||
>
|
>
|
||||||
{{ $t("buttons.replace") }}
|
{{ $t("buttons.replace") }}
|
||||||
</button>
|
</button>
|
||||||
@ -42,16 +38,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapActions, mapState } from "pinia";
|
import { mapGetters } from "vuex";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "replace",
|
name: "replace",
|
||||||
computed: {
|
computed: mapGetters(["currentPrompt"]),
|
||||||
...mapState(useLayoutStore, ["currentPrompt"]),
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -11,10 +11,9 @@
|
|||||||
<div class="card-action">
|
<div class="card-action">
|
||||||
<button
|
<button
|
||||||
class="button button--flat button--grey"
|
class="button button--flat button--grey"
|
||||||
@click="closeHovers"
|
@click="$store.commit('closeHovers')"
|
||||||
:aria-label="$t('buttons.cancel')"
|
:aria-label="$t('buttons.cancel')"
|
||||||
:title="$t('buttons.cancel')"
|
:title="$t('buttons.cancel')"
|
||||||
tabindex="3"
|
|
||||||
>
|
>
|
||||||
{{ $t("buttons.cancel") }}
|
{{ $t("buttons.cancel") }}
|
||||||
</button>
|
</button>
|
||||||
@ -23,17 +22,14 @@
|
|||||||
@click="(event) => currentPrompt.confirm(event, 'rename')"
|
@click="(event) => currentPrompt.confirm(event, 'rename')"
|
||||||
:aria-label="$t('buttons.rename')"
|
:aria-label="$t('buttons.rename')"
|
||||||
:title="$t('buttons.rename')"
|
:title="$t('buttons.rename')"
|
||||||
tabindex="2"
|
|
||||||
>
|
>
|
||||||
{{ $t("buttons.rename") }}
|
{{ $t("buttons.rename") }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
id="focus-prompt"
|
|
||||||
class="button button--flat button--red"
|
class="button button--flat button--red"
|
||||||
@click="(event) => currentPrompt.confirm(event, 'overwrite')"
|
@click="(event) => currentPrompt.confirm(event, 'overwrite')"
|
||||||
:aria-label="$t('buttons.replace')"
|
:aria-label="$t('buttons.replace')"
|
||||||
:title="$t('buttons.replace')"
|
:title="$t('buttons.replace')"
|
||||||
tabindex="1"
|
|
||||||
>
|
>
|
||||||
{{ $t("buttons.replace") }}
|
{{ $t("buttons.replace") }}
|
||||||
</button>
|
</button>
|
||||||
@ -42,16 +38,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapActions, mapState } from "pinia";
|
import { mapGetters } from "vuex";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "replace-rename",
|
name: "replace-rename",
|
||||||
computed: {
|
computed: mapGetters(["currentPrompt"]),
|
||||||
...mapState(useLayoutStore, ["currentPrompt"]),
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card floating" id="share">
|
<div class="card floating share__promt__card" id="share">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<h2>{{ $t("buttons.share") }}</h2>
|
<h2>{{ $t("buttons.share") }}</h2>
|
||||||
</div>
|
</div>
|
||||||
@ -25,9 +25,9 @@
|
|||||||
<td class="small">
|
<td class="small">
|
||||||
<button
|
<button
|
||||||
class="action copy-clipboard"
|
class="action copy-clipboard"
|
||||||
|
:data-clipboard-text="buildLink(link)"
|
||||||
:aria-label="$t('buttons.copyToClipboard')"
|
:aria-label="$t('buttons.copyToClipboard')"
|
||||||
:title="$t('buttons.copyToClipboard')"
|
:title="$t('buttons.copyToClipboard')"
|
||||||
@click="copyToClipboard(buildLink(link))"
|
|
||||||
>
|
>
|
||||||
<i class="material-icons">content_paste</i>
|
<i class="material-icons">content_paste</i>
|
||||||
</button>
|
</button>
|
||||||
@ -35,9 +35,9 @@
|
|||||||
<td class="small" v-if="hasDownloadLink()">
|
<td class="small" v-if="hasDownloadLink()">
|
||||||
<button
|
<button
|
||||||
class="action copy-clipboard"
|
class="action copy-clipboard"
|
||||||
|
:data-clipboard-text="buildDownloadLink(link)"
|
||||||
:aria-label="$t('buttons.copyDownloadLinkToClipboard')"
|
:aria-label="$t('buttons.copyDownloadLinkToClipboard')"
|
||||||
:title="$t('buttons.copyDownloadLinkToClipboard')"
|
:title="$t('buttons.copyDownloadLinkToClipboard')"
|
||||||
@click="copyToClipboard(buildDownloadLink(link))"
|
|
||||||
>
|
>
|
||||||
<i class="material-icons">content_paste_go</i>
|
<i class="material-icons">content_paste_go</i>
|
||||||
</button>
|
</button>
|
||||||
@ -59,20 +59,17 @@
|
|||||||
<div class="card-action">
|
<div class="card-action">
|
||||||
<button
|
<button
|
||||||
class="button button--flat button--grey"
|
class="button button--flat button--grey"
|
||||||
@click="closeHovers"
|
@click="$store.commit('closeHovers')"
|
||||||
:aria-label="$t('buttons.close')"
|
:aria-label="$t('buttons.close')"
|
||||||
:title="$t('buttons.close')"
|
:title="$t('buttons.close')"
|
||||||
tabindex="2"
|
|
||||||
>
|
>
|
||||||
{{ $t("buttons.close") }}
|
{{ $t("buttons.close") }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
id="focus-prompt"
|
|
||||||
class="button button--flat button--blue"
|
class="button button--flat button--blue"
|
||||||
@click="() => switchListing()"
|
@click="() => switchListing()"
|
||||||
:aria-label="$t('buttons.new')"
|
:aria-label="$t('buttons.new')"
|
||||||
:title="$t('buttons.new')"
|
:title="$t('buttons.new')"
|
||||||
tabindex="1"
|
|
||||||
>
|
>
|
||||||
{{ $t("buttons.new") }}
|
{{ $t("buttons.new") }}
|
||||||
</button>
|
</button>
|
||||||
@ -83,22 +80,15 @@
|
|||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<p>{{ $t("settings.shareDuration") }}</p>
|
<p>{{ $t("settings.shareDuration") }}</p>
|
||||||
<div class="input-group input">
|
<div class="input-group input">
|
||||||
<vue-number-input
|
<input
|
||||||
center
|
v-focus
|
||||||
controls
|
type="number"
|
||||||
size="small"
|
max="2147483647"
|
||||||
:max="2147483647"
|
min="1"
|
||||||
:min="0"
|
|
||||||
@keyup.enter="submit"
|
@keyup.enter="submit"
|
||||||
v-model="time"
|
v-model.trim="time"
|
||||||
tabindex="1"
|
|
||||||
/>
|
/>
|
||||||
<select
|
<select class="right" v-model="unit" :aria-label="$t('time.unit')">
|
||||||
class="right"
|
|
||||||
v-model="unit"
|
|
||||||
:aria-label="$t('time.unit')"
|
|
||||||
tabindex="2"
|
|
||||||
>
|
|
||||||
<option value="seconds">{{ $t("time.seconds") }}</option>
|
<option value="seconds">{{ $t("time.seconds") }}</option>
|
||||||
<option value="minutes">{{ $t("time.minutes") }}</option>
|
<option value="minutes">{{ $t("time.minutes") }}</option>
|
||||||
<option value="hours">{{ $t("time.hours") }}</option>
|
<option value="hours">{{ $t("time.hours") }}</option>
|
||||||
@ -110,7 +100,6 @@
|
|||||||
class="input input--block"
|
class="input input--block"
|
||||||
type="password"
|
type="password"
|
||||||
v-model.trim="password"
|
v-model.trim="password"
|
||||||
tabindex="3"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -120,17 +109,14 @@
|
|||||||
@click="() => switchListing()"
|
@click="() => switchListing()"
|
||||||
:aria-label="$t('buttons.cancel')"
|
:aria-label="$t('buttons.cancel')"
|
||||||
:title="$t('buttons.cancel')"
|
:title="$t('buttons.cancel')"
|
||||||
tabindex="5"
|
|
||||||
>
|
>
|
||||||
{{ $t("buttons.cancel") }}
|
{{ $t("buttons.cancel") }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
id="focus-prompt"
|
|
||||||
class="button button--flat button--blue"
|
class="button button--flat button--blue"
|
||||||
@click="submit"
|
@click="submit"
|
||||||
:aria-label="$t('buttons.share')"
|
:aria-label="$t('buttons.share')"
|
||||||
:title="$t('buttons.share')"
|
:title="$t('buttons.share')"
|
||||||
tabindex="4"
|
|
||||||
>
|
>
|
||||||
{{ $t("buttons.share") }}
|
{{ $t("buttons.share") }}
|
||||||
</button>
|
</button>
|
||||||
@ -140,18 +126,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapActions, mapState } from "pinia";
|
import { mapState, mapGetters } from "vuex";
|
||||||
import { useFileStore } from "@/stores/file";
|
|
||||||
import { share as api, pub as pub_api } from "@/api";
|
import { share as api, pub as pub_api } from "@/api";
|
||||||
import dayjs from "dayjs";
|
import moment from "moment/min/moment-with-locales";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import Clipboard from "clipboard";
|
||||||
import { copy } from "@/utils/clipboard";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "share",
|
name: "share",
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
time: 0,
|
time: "",
|
||||||
unit: "hours",
|
unit: "hours",
|
||||||
links: [],
|
links: [],
|
||||||
clip: null,
|
clip: null,
|
||||||
@ -159,14 +143,9 @@ export default {
|
|||||||
listing: true,
|
listing: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
inject: ["$showError", "$showSuccess"],
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useFileStore, [
|
...mapState(["req", "selected", "selectedCount"]),
|
||||||
"req",
|
...mapGetters(["isListing"]),
|
||||||
"selected",
|
|
||||||
"selectedCount",
|
|
||||||
"isListing",
|
|
||||||
]),
|
|
||||||
url() {
|
url() {
|
||||||
if (!this.isListing) {
|
if (!this.isListing) {
|
||||||
return this.$route.path;
|
return this.$route.path;
|
||||||
@ -193,34 +172,23 @@ export default {
|
|||||||
this.$showError(e);
|
this.$showError(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
this.clip = new Clipboard(".copy-clipboard");
|
||||||
|
this.clip.on("success", () => {
|
||||||
|
this.$showSuccess(this.$t("success.linkCopied"));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.clip.destroy();
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
|
||||||
copyToClipboard: function (text) {
|
|
||||||
copy({ text }).then(
|
|
||||||
() => {
|
|
||||||
// clipboard successfully set
|
|
||||||
this.$showSuccess(this.$t("success.linkCopied"));
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
// clipboard write failed
|
|
||||||
copy({ text }, { permission: true }).then(
|
|
||||||
() => {
|
|
||||||
// clipboard successfully set
|
|
||||||
this.$showSuccess(this.$t("success.linkCopied"));
|
|
||||||
},
|
|
||||||
(e) => {
|
|
||||||
// clipboard write failed
|
|
||||||
this.$showError(e);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
submit: async function () {
|
submit: async function () {
|
||||||
|
let isPermanent = !this.time || this.time == 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let res = null;
|
let res = null;
|
||||||
|
|
||||||
if (!this.time) {
|
if (isPermanent) {
|
||||||
res = await api.create(this.url, this.password);
|
res = await api.create(this.url, this.password);
|
||||||
} else {
|
} else {
|
||||||
res = await api.create(this.url, this.password, this.time, this.unit);
|
res = await api.create(this.url, this.password, this.time, this.unit);
|
||||||
@ -229,7 +197,7 @@ export default {
|
|||||||
this.links.push(res);
|
this.links.push(res);
|
||||||
this.sort();
|
this.sort();
|
||||||
|
|
||||||
this.time = 0;
|
this.time = "";
|
||||||
this.unit = "hours";
|
this.unit = "hours";
|
||||||
this.password = "";
|
this.password = "";
|
||||||
|
|
||||||
@ -252,7 +220,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
humanTime(time) {
|
humanTime(time) {
|
||||||
return dayjs(time * 1000).fromNow();
|
return moment(time * 1000).fromNow();
|
||||||
},
|
},
|
||||||
buildLink(share) {
|
buildLink(share) {
|
||||||
return api.getShareURL(share);
|
return api.getShareURL(share);
|
||||||
@ -274,7 +242,7 @@ export default {
|
|||||||
},
|
},
|
||||||
switchListing() {
|
switchListing() {
|
||||||
if (this.links.length == 0 && !this.listing) {
|
if (this.links.length == 0 && !this.listing) {
|
||||||
this.closeHovers();
|
this.$store.commit("closeHovers");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.listing = !this.listing;
|
this.listing = !this.listing;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user