Compare commits
82 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b068b3058 | ||
|
|
da54bd6c21 | ||
|
|
0d179eca4d | ||
|
|
dacd511d24 | ||
|
|
c44b37c50c | ||
|
|
a721dc1f31 | ||
|
|
d2e6d23741 | ||
|
|
5f4a0317ab | ||
|
|
22f4be8f54 | ||
|
|
eeadc532fe | ||
|
|
93a35ad251 | ||
|
|
99787287bb | ||
|
|
bdd523190e | ||
|
|
4c1dd5c097 | ||
|
|
e1f658633d | ||
|
|
9c79105c02 | ||
|
|
6d5ceae8b4 | ||
|
|
381f09087a | ||
|
|
426b38bb33 | ||
|
|
488d98045e | ||
|
|
7955e0720b | ||
|
|
e017a19985 | ||
|
|
f8df76f526 | ||
|
|
11ebaec5f0 | ||
|
|
326b35a7ac | ||
|
|
5bf15548d0 | ||
|
|
6a734c0139 | ||
|
|
81b6f4d6f6 | ||
|
|
0b92d94570 | ||
|
|
fc5506179a | ||
|
|
0fe34ad224 | ||
|
|
54f35701a2 | ||
|
|
a809404ce1 | ||
|
|
fb32e44b47 | ||
|
|
e9c0369062 | ||
|
|
7358b3fe31 | ||
|
|
2a1f759e9e | ||
|
|
2fccb8c367 | ||
|
|
e039d95192 | ||
|
|
0f96031d6f | ||
|
|
6214fc84fa | ||
|
|
47578e02e3 | ||
|
|
35a4379b67 | ||
|
|
23f84642e6 | ||
|
|
edb9e85efd | ||
|
|
2d2c598fa6 | ||
|
|
cf4836dc75 | ||
|
|
d8306559fd | ||
|
|
e8c9d1c539 | ||
|
|
d8f415f8ab | ||
|
|
7b6579ac8a | ||
|
|
057307181e | ||
|
|
4fb832c042 | ||
|
|
e503cb69f2 | ||
|
|
95811e99bc | ||
|
|
62fff5ca60 | ||
|
|
5b28aa0848 | ||
|
|
db5aad8eb6 | ||
|
|
977ec33918 | ||
|
|
1819377897 | ||
|
|
f1b7bd59f6 | ||
|
|
f3afd5cb79 | ||
|
|
e6a5bf116e | ||
|
|
21b5a76fa7 | ||
|
|
b6263eb607 | ||
|
|
c8257e848e | ||
|
|
05bb7c8553 | ||
|
|
b600b11415 | ||
|
|
019ce80fc5 | ||
|
|
8cea2f75b3 | ||
|
|
6914063853 | ||
|
|
43e0d4a856 | ||
|
|
066d8e8d6c | ||
|
|
948e05c083 | ||
|
|
fb5b28d9cb | ||
|
|
677bce376b | ||
|
|
8faa96f5e6 | ||
|
|
f62806f6c9 | ||
|
|
58835b7e53 | ||
|
|
7a5298a755 | ||
|
|
bc4a6462ce | ||
|
|
ac3673e111 |
@@ -1,92 +0,0 @@
|
||||
version: 2
|
||||
jobs:
|
||||
lint:
|
||||
docker:
|
||||
- image: golangci/golangci-lint:v1.27.0
|
||||
steps:
|
||||
- checkout
|
||||
- run: golangci-lint run -v
|
||||
build-node:
|
||||
docker:
|
||||
- image: circleci/node
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: "Build"
|
||||
command: ./wizard.sh -a
|
||||
- run:
|
||||
name: "Cleanup"
|
||||
command: rm -rf frontend/node_modules
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- '*'
|
||||
test:
|
||||
docker:
|
||||
- image: circleci/golang:1.15.2
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: "Test"
|
||||
command: go test ./...
|
||||
build-go:
|
||||
docker:
|
||||
- image: circleci/golang:1.15.2
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: '~/project'
|
||||
- run:
|
||||
name: "Compile"
|
||||
command: GOOS=linux GOARCH=amd64 ./wizard.sh -c
|
||||
- run:
|
||||
name: "Cleanup"
|
||||
command: |
|
||||
rm -rf frontend/build
|
||||
git checkout -- go.sum # TODO: why is it being changed?
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- '*'
|
||||
release:
|
||||
docker:
|
||||
- image: circleci/golang:1.15.2
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: '~/project'
|
||||
- setup_remote_docker
|
||||
- run: echo $DOCKER_PASSWORD | docker login --username $DOCKER_USERNAME --password-stdin
|
||||
- run: curl -sL https://git.io/goreleaser | bash
|
||||
- run: docker logout
|
||||
workflows:
|
||||
version: 2
|
||||
build-workflow:
|
||||
jobs:
|
||||
- lint:
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
- test:
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
- build-node:
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
- build-go:
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
requires:
|
||||
- build-node
|
||||
- lint
|
||||
- test
|
||||
- release:
|
||||
context: deploy
|
||||
requires:
|
||||
- build-go
|
||||
filters:
|
||||
tags:
|
||||
only: /^v.*/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
@@ -5,4 +5,4 @@
|
||||
"log": "stdout",
|
||||
"database": "/database.db",
|
||||
"root": "/srv"
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
testdata/
|
||||
.github/
|
||||
**.git
|
||||
*
|
||||
!.docker.json
|
||||
!filebrowser
|
||||
5
.github/CODEOWNERS
vendored
Normal file
5
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# These owners will be the default owners for everything in the repo.
|
||||
# Unless a later match takes precedence, @o1egl will be requested for
|
||||
# review when someone opens a pull request.
|
||||
|
||||
* @o1egl
|
||||
66
.github/workflows/main.yaml
vendored
Normal file
66
.github/workflows/main.yaml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
name: main
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.16
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
- run: npm i -g commitlint
|
||||
- run: make lint
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.16
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
- run: make test
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint, test]
|
||||
if: startsWith(github.event.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.16
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Build fronetend
|
||||
run: make build-frontend
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
with:
|
||||
version: latest
|
||||
args: release --rm-dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -5,11 +5,10 @@ _old
|
||||
rice-box.go
|
||||
.idea/
|
||||
filebrowser
|
||||
dist/
|
||||
filebrowser.exe
|
||||
|
||||
.DS_Store
|
||||
node_modules
|
||||
/frontend/dist
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
@@ -28,3 +27,5 @@ yarn-error.log*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw*
|
||||
bin/
|
||||
build/
|
||||
|
||||
@@ -63,7 +63,6 @@ linters:
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- gofmt
|
||||
- goimports
|
||||
- golint
|
||||
- gomnd
|
||||
|
||||
131
.goreleaser.yml
131
.goreleaser.yml
@@ -3,10 +3,6 @@ project_name: filebrowser
|
||||
env:
|
||||
- GO111MODULE=on
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod download
|
||||
|
||||
build:
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
@@ -19,10 +15,6 @@ build:
|
||||
- linux
|
||||
- windows
|
||||
- freebsd
|
||||
- netbsd
|
||||
- openbsd
|
||||
- dragonfly
|
||||
- solaris
|
||||
goarch:
|
||||
- amd64
|
||||
- 386
|
||||
@@ -35,14 +27,8 @@ build:
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: 386
|
||||
- goos: openbsd
|
||||
goarch: arm
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
- goos: netbsd
|
||||
goarch: arm
|
||||
- goos: solaris
|
||||
goarch: arm
|
||||
|
||||
archives:
|
||||
-
|
||||
@@ -55,53 +41,106 @@ archives:
|
||||
dockers:
|
||||
-
|
||||
dockerfile: Dockerfile
|
||||
binaries:
|
||||
- filebrowser
|
||||
use_buildx: true
|
||||
build_flag_templates:
|
||||
- "--pull"
|
||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||
- "--label=org.opencontainers.image.name={{.ProjectName}}"
|
||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||
- "--label=org.opencontainers.image.source={{.GitURL}}"
|
||||
- "--platform=linux/amd64"
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
goarm: ''
|
||||
image_templates:
|
||||
- "filebrowser/filebrowser:latest"
|
||||
- "filebrowser/filebrowser:{{ .Tag }}"
|
||||
- "filebrowser/filebrowser:v{{ .Major }}"
|
||||
- "filebrowser/filebrowser:{{ .Tag }}-amd64"
|
||||
- "filebrowser/filebrowser:v{{ .Major }}-amd64"
|
||||
extra_files:
|
||||
- .docker.json
|
||||
-
|
||||
dockerfile: Dockerfile
|
||||
binaries:
|
||||
- filebrowser
|
||||
use_buildx: true
|
||||
build_flag_templates:
|
||||
- "--pull"
|
||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||
- "--label=org.opencontainers.image.name={{.ProjectName}}"
|
||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||
- "--label=org.opencontainers.image.source={{.GitURL}}"
|
||||
- "--platform=linux/arm64"
|
||||
goos: linux
|
||||
goarch: arm64
|
||||
image_templates:
|
||||
- "filebrowser/filebrowser:{{ .Tag }}-arm64"
|
||||
- "filebrowser/filebrowser:v{{ .Major }}-arm64"
|
||||
extra_files:
|
||||
- .docker.json
|
||||
-
|
||||
dockerfile: Dockerfile
|
||||
use_buildx: true
|
||||
build_flag_templates:
|
||||
- "--pull"
|
||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||
- "--label=org.opencontainers.image.name={{.ProjectName}}"
|
||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||
- "--label=org.opencontainers.image.source={{.GitURL}}"
|
||||
- "--platform=linux/arm/v6"
|
||||
goos: linux
|
||||
goarch: arm
|
||||
goarm: '5'
|
||||
goarm: '6'
|
||||
image_templates:
|
||||
- "filebrowser/filebrowser:pi"
|
||||
- "filebrowser/filebrowser:{{ .Tag }}-pi"
|
||||
- "filebrowser/filebrowser:v{{ .Major }}-pi"
|
||||
- "filebrowser/filebrowser:{{ .Tag }}-armv6"
|
||||
- "filebrowser/filebrowser:v{{ .Major }}-armv6"
|
||||
extra_files:
|
||||
- .docker.json
|
||||
-
|
||||
dockerfile: Dockerfile.alpine
|
||||
binaries:
|
||||
- filebrowser
|
||||
dockerfile: Dockerfile
|
||||
use_buildx: true
|
||||
build_flag_templates:
|
||||
- "--pull"
|
||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||
- "--label=org.opencontainers.image.name={{.ProjectName}}"
|
||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||
- "--label=org.opencontainers.image.source={{.GitURL}}"
|
||||
- "--platform=linux/arm/v7"
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
goarm: ''
|
||||
goarch: arm
|
||||
goarm: '7'
|
||||
image_templates:
|
||||
- "filebrowser/filebrowser:alpine"
|
||||
- "filebrowser/filebrowser:{{ .Tag }}-alpine"
|
||||
- "filebrowser/filebrowser:v{{ .Major }}-alpine"
|
||||
- "filebrowser/filebrowser:{{ .Tag }}-armv7"
|
||||
- "filebrowser/filebrowser:v{{ .Major }}-armv7"
|
||||
extra_files:
|
||||
- .docker.json
|
||||
-
|
||||
dockerfile: Dockerfile.debian
|
||||
binaries:
|
||||
- filebrowser
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
goarm: ''
|
||||
docker_manifests:
|
||||
- name_template: "filebrowser/filebrowser:latest"
|
||||
image_templates:
|
||||
- "filebrowser/filebrowser:debian"
|
||||
- "filebrowser/filebrowser:{{ .Tag }}-debian"
|
||||
- "filebrowser/filebrowser:v{{ .Major }}-debian"
|
||||
extra_files:
|
||||
- .docker.json
|
||||
- "filebrowser/filebrowser:{{ .Tag }}-amd64"
|
||||
- "filebrowser/filebrowser:{{ .Tag }}-arm64"
|
||||
- "filebrowser/filebrowser:{{ .Tag }}-armv6"
|
||||
- "filebrowser/filebrowser:{{ .Tag }}-armv7"
|
||||
- name_template: "filebrowser/filebrowser:{{ .Tag }}"
|
||||
image_templates:
|
||||
- "filebrowser/filebrowser:{{ .Tag }}-amd64"
|
||||
- "filebrowser/filebrowser:{{ .Tag }}-arm64"
|
||||
- "filebrowser/filebrowser:{{ .Tag }}-armv6"
|
||||
- "filebrowser/filebrowser:{{ .Tag }}-armv7"
|
||||
- name_template: "filebrowser/filebrowser:v{{ .Major }}"
|
||||
image_templates:
|
||||
- "filebrowser/filebrowser:v{{ .Major }}-amd64"
|
||||
- "filebrowser/filebrowser:v{{ .Major }}-arm64"
|
||||
- "filebrowser/filebrowser:v{{ .Major }}-armv6"
|
||||
- "filebrowser/filebrowser:v{{ .Major }}-armv7"
|
||||
brews:
|
||||
- name: filebrowser
|
||||
tap:
|
||||
owner: filebrowser
|
||||
name: homebrew-tap
|
||||
folder: Formula
|
||||
homepage: https://filebrowser.org
|
||||
commit_author:
|
||||
name: FileBrowser Robot
|
||||
email: robot@filebrowser.org
|
||||
description: File Browser is a create-your-own-cloud-kind of software where you can install it on a server, direct it to a path and then access your files through a nice web interface
|
||||
license: "MIT"
|
||||
14
.versionrc
Normal file
14
.versionrc
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"types": [
|
||||
{ "type": "feat", "section": "Features" },
|
||||
{ "type": "fix", "section": "Bug Fixes" },
|
||||
{ "type": "perf", "section": "Performance improvements" },
|
||||
{ "type": "revert", "section": "Reverts" },
|
||||
{ "type": "refactor", "section": "Refactorings" },
|
||||
{ "type": "build", "section": "Build" },
|
||||
{ "type": "ci", "hidden": true },
|
||||
{ "type": "test", "hidden": true },
|
||||
{ "type": "chore", "hidden": true },
|
||||
{ "type": "docs", "hidden": true }
|
||||
]
|
||||
}
|
||||
102
CHANGELOG.md
102
CHANGELOG.md
@@ -2,6 +2,108 @@
|
||||
|
||||
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.14.1](https://github.com/filebrowser/filebrowser/compare/v2.14.0...v2.14.1) (2021-03-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* display public routes with header proxy auth ([da54bd6](https://github.com/filebrowser/filebrowser/commit/da54bd6c214d7ee39b71d710ddfe6dd25fc4e5d6))
|
||||
|
||||
## [2.14.0](https://github.com/filebrowser/filebrowser/compare/v2.13.0...v2.14.0) (2021-03-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add health check handler ([a721dc1](https://github.com/filebrowser/filebrowser/commit/a721dc1f314732e60d331a1a7da97d06e0e8b613))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* hide dotfile error on share ([5f4a031](https://github.com/filebrowser/filebrowser/commit/5f4a0317ab5685fe4a558df74e604c12e04a1c10))
|
||||
* prefix handling on http router ([93a35ad](https://github.com/filebrowser/filebrowser/commit/93a35ad2516accdcb9735db509550979d01de2c3))
|
||||
* qr code url on share ([22f4be8](https://github.com/filebrowser/filebrowser/commit/22f4be8f54162b7cf494177705ffb8b09117bd01))
|
||||
* text file detection on editor ([eeadc53](https://github.com/filebrowser/filebrowser/commit/eeadc532fe6057969b3c1a4726f236851b154cfa))
|
||||
|
||||
## [2.13.0](https://github.com/filebrowser/filebrowser/compare/v2.12.1...v2.13.0) (2021-03-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* dual pane settings view ([db5aad8](https://github.com/filebrowser/filebrowser/commit/db5aad8eb679cfe1b1ace5142cf342951217f0f7))
|
||||
* improved settings navbar ([5b28aa0](https://github.com/filebrowser/filebrowser/commit/5b28aa0848710b9d3ee02a2aa912856395f48bd2))
|
||||
* improved sharing prompt ([1819377](https://github.com/filebrowser/filebrowser/commit/18193778971e27d18b5a35df8c2d0e2953b48111))
|
||||
* increased header button counter size ([4fb832c](https://github.com/filebrowser/filebrowser/commit/4fb832c0422107e16f22b7aa928224f36de4978f))
|
||||
* larger previewer content ([62fff5c](https://github.com/filebrowser/filebrowser/commit/62fff5ca60da1f887c1f95fa4808d3753596dab2))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* archive contains parent path on Windows ([54f3570](https://github.com/filebrowser/filebrowser/commit/54f35701a2bd5cb7ec0628ca9789047072c073db))
|
||||
* check rules on http resource handlers ([5bf1554](https://github.com/filebrowser/filebrowser/commit/5bf15548d0ad147acfad5000277531be2671f7ce))
|
||||
* download current dir on file listing ([488d980](https://github.com/filebrowser/filebrowser/commit/488d98045e7476ed11e53c13d9498a9db3165bbc))
|
||||
* encoded file path on share ([7955e07](https://github.com/filebrowser/filebrowser/commit/7955e0720baef3710106c7e69bbbf078d5489220))
|
||||
* full file path on share ([e017a19](https://github.com/filebrowser/filebrowser/commit/e017a199850e19dd51b960ba59402c215fd8f1af))
|
||||
* header dropdown icon color on previewer ([f8df76f](https://github.com/filebrowser/filebrowser/commit/f8df76f52684f10722ce123fec2c90e321ddf103))
|
||||
* item dragging on file listing ([326b35a](https://github.com/filebrowser/filebrowser/commit/326b35a7ac7871afcdf892ca150349665b7f6379))
|
||||
* modified time on info prompt ([11ebaec](https://github.com/filebrowser/filebrowser/commit/11ebaec5f0671ec02ebe55d4a73a514bce3a6713))
|
||||
* root path name on archive ([426b38b](https://github.com/filebrowser/filebrowser/commit/426b38bb3362d2d477d0d8aa27d880664d537431))
|
||||
* stuck icon on header button ([6a734c0](https://github.com/filebrowser/filebrowser/commit/6a734c01391b437c2842f5d97fb63f29a0017510))
|
||||
* update image cache when replacing ([81b6f4d](https://github.com/filebrowser/filebrowser/commit/81b6f4d6f6a01886583016f61f4f1951a59f244d))
|
||||
* wait for async command exit ([#1326](https://github.com/filebrowser/filebrowser/issues/1326)) ([6d5ceae](https://github.com/filebrowser/filebrowser/commit/6d5ceae8b454edd749b3b65c88aacc0a31ce9215))
|
||||
|
||||
|
||||
### Refactorings
|
||||
|
||||
* migrate from rice to embed.FS ([fc55061](https://github.com/filebrowser/filebrowser/commit/fc5506179a64e9e2f57f7b6d6cce4b95f5ebc235))
|
||||
|
||||
### [2.12.1](https://github.com/filebrowser/filebrowser/compare/v2.12.0...v2.12.1) (2021-03-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add missing default config into the docker image ([7358b3f](https://github.com/filebrowser/filebrowser/commit/7358b3fe3178c20007b4b5ef5c03705badd538c4))
|
||||
|
||||
## [2.12.0](https://github.com/filebrowser/filebrowser/compare/v2.11.0...v2.12.0) (2021-03-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add homebrew tap ([2d2c598](https://github.com/filebrowser/filebrowser/commit/2d2c598fa6bd1ecaf39c542182890c8dd9b1cad0))
|
||||
* added tiff files preview support ([#1222](https://github.com/filebrowser/filebrowser/issues/1222)) ([e8c9d1c](https://github.com/filebrowser/filebrowser/commit/e8c9d1c53989b4b52f6fba2a8ac41ae612c03a7c))
|
||||
* allow disabling file detections by reading header ([#1175](https://github.com/filebrowser/filebrowser/issues/1175)) ([6914063](https://github.com/filebrowser/filebrowser/commit/6914063853a8a3f3cecfa4b21f223820c2a0b7df))
|
||||
* allow to password protect shares ([#1252](https://github.com/filebrowser/filebrowser/issues/1252)) ([d8f415f](https://github.com/filebrowser/filebrowser/commit/d8f415f8abd0c4301803bd968c54429dd3fe4b59))
|
||||
* build multi-arch docker images ([cf4836d](https://github.com/filebrowser/filebrowser/commit/cf4836dc757ef79ad615179bb7a6c7bbd3b09c2c))
|
||||
* share management delete confirm ([#1212](https://github.com/filebrowser/filebrowser/issues/1212)) ([b600b11](https://github.com/filebrowser/filebrowser/commit/b600b11415fd1fb90ff2f5136be95a9c737ae1cb))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* don't allow to remove root user ([019ce80](https://github.com/filebrowser/filebrowser/commit/019ce80fc529a0437984fdc3d1ab6916f34dd594))
|
||||
* double click to zoom pics in phone's browser ([#1274](https://github.com/filebrowser/filebrowser/issues/1274)) ([f1b7bd5](https://github.com/filebrowser/filebrowser/commit/f1b7bd59f67e719b7bfd203b0d7ec016fd21ab49))
|
||||
* environmental variables not expanded in command ([#1241](https://github.com/filebrowser/filebrowser/issues/1241)) ([f3afd5c](https://github.com/filebrowser/filebrowser/commit/f3afd5cb79d6ad8b9cc8d54cb8fc2344b7c07d3d))
|
||||
* fetch resource api once when sorting (closes [#1172](https://github.com/filebrowser/filebrowser/issues/1172)) ([#1202](https://github.com/filebrowser/filebrowser/issues/1202)) ([05bb7c8](https://github.com/filebrowser/filebrowser/commit/05bb7c85531349f3e9d1d8a523bb1243587b2ebc))
|
||||
|
||||
|
||||
### Build
|
||||
|
||||
* use make for building the project ([#1304](https://github.com/filebrowser/filebrowser/issues/1304)) ([23f8464](https://github.com/filebrowser/filebrowser/commit/23f84642e6c1e07f89f98d2c1bb6fc9da36cc71c))
|
||||
|
||||
## [2.11.0](https://github.com/filebrowser/filebrowser/compare/v2.10.0...v2.11.0) (2020-12-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add sharing management ([#1178](https://github.com/filebrowser/filebrowser/issues/1178)) (closes [#1000](https://github.com/filebrowser/filebrowser/issues/1000)) ([677bce3](https://github.com/filebrowser/filebrowser/commit/677bce376b024d9ff38f34e74243034fe5a1ec3c))
|
||||
* download shared subdirectory ([#1184](https://github.com/filebrowser/filebrowser/issues/1184)) ([fb5b28d](https://github.com/filebrowser/filebrowser/commit/fb5b28d9cbdee10d38fcd719b9fd832121be58ef))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* check user input to prevent permission elevation ([#1196](https://github.com/filebrowser/filebrowser/issues/1196)) (closes [#1195](https://github.com/filebrowser/filebrowser/issues/1195)) ([f62806f](https://github.com/filebrowser/filebrowser/commit/f62806f6c9e9c7f392d1b747d65b8fe40b313e89))
|
||||
* delete extra remove prefix ([#1186](https://github.com/filebrowser/filebrowser/issues/1186)) ([7a5298a](https://github.com/filebrowser/filebrowser/commit/7a5298a7556f7dcc52f59b8ea76d040d3ddc3d12))
|
||||
* move files between different volumes (closes [#1177](https://github.com/filebrowser/filebrowser/issues/1177)) ([58835b7](https://github.com/filebrowser/filebrowser/commit/58835b7e535cc96e1c8a5d85821c1545743ca757))
|
||||
* recaptcha race condition ([#1176](https://github.com/filebrowser/filebrowser/issues/1176)) ([ac3673e](https://github.com/filebrowser/filebrowser/commit/ac3673e111afac6616af9650ca07028b6c27e6cd))
|
||||
|
||||
## [2.10.0](https://github.com/filebrowser/filebrowser/compare/v2.9.0...v2.10.0) (2020-11-24)
|
||||
|
||||
|
||||
|
||||
14
Dockerfile
14
Dockerfile
@@ -1,10 +1,10 @@
|
||||
FROM alpine:latest as alpine
|
||||
RUN apk --update add ca-certificates
|
||||
RUN apk --update add mailcap
|
||||
FROM alpine:latest
|
||||
RUN apk --update add ca-certificates \
|
||||
mailcap \
|
||||
curl
|
||||
|
||||
FROM scratch
|
||||
COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY --from=alpine /etc/mime.types /etc/mime.types
|
||||
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
|
||||
CMD curl -f http://localhost/health || exit 1
|
||||
|
||||
VOLUME /srv
|
||||
EXPOSE 80
|
||||
@@ -12,4 +12,4 @@ EXPOSE 80
|
||||
COPY .docker.json /.filebrowser.json
|
||||
COPY filebrowser /filebrowser
|
||||
|
||||
ENTRYPOINT [ "/filebrowser" ]
|
||||
ENTRYPOINT [ "/filebrowser" ]
|
||||
@@ -1,11 +0,0 @@
|
||||
FROM alpine:latest as alpine
|
||||
RUN apk --update add ca-certificates
|
||||
RUN apk --update add mailcap
|
||||
|
||||
VOLUME /srv
|
||||
EXPOSE 80
|
||||
|
||||
COPY .docker.json /.filebrowser.json
|
||||
COPY filebrowser /filebrowser
|
||||
|
||||
ENTRYPOINT [ "/filebrowser" ]
|
||||
@@ -1,9 +0,0 @@
|
||||
FROM debian:buster
|
||||
|
||||
VOLUME /srv
|
||||
EXPOSE 80
|
||||
|
||||
COPY .docker.json /.filebrowser.json
|
||||
COPY filebrowser /filebrowser
|
||||
|
||||
ENTRYPOINT [ "/filebrowser" ]
|
||||
94
Makefile
Normal file
94
Makefile
Normal file
@@ -0,0 +1,94 @@
|
||||
SHELL := /bin/bash
|
||||
BASE_PATH := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
||||
VERSION ?= $(shell git describe --tags --always --match=v* 2> /dev/null || \
|
||||
cat $(CURDIR)/.version 2> /dev/null || echo v0)
|
||||
VERSION_HASH = $(shell git rev-parse HEAD)
|
||||
|
||||
BIN = $(BASE_PATH)/bin
|
||||
PATH := $(BIN):$(PATH)
|
||||
export PATH
|
||||
|
||||
# printing
|
||||
V = 0
|
||||
Q = $(if $(filter 1,$V),,@)
|
||||
M = $(shell printf "\033[34;1m▶\033[0m")
|
||||
|
||||
GO = GOGC=off go
|
||||
# go module
|
||||
MODULE = $(shell env GO111MODULE=on $(GO) list -m)
|
||||
|
||||
DATE ?= $(shell date +%FT%T%z)
|
||||
VERSION ?= $(shell git describe --tags --always --match=v* 2> /dev/null || \
|
||||
cat $(CURDIR)/.version 2> /dev/null || echo v0)
|
||||
VERSION_HASH = $(shell git rev-parse HEAD)
|
||||
BRANCH = $(shell git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
LDFLAGS += -X "$(MODULE)/varsion.Version=$(VERSION)" -X "$(MODULE)/varsion.CommitSHA=$(VERSION_HASH)"
|
||||
|
||||
# tools
|
||||
$(BIN):
|
||||
@mkdir -p $@
|
||||
$(BIN)/%: | $(BIN) ; $(info $(M) installing $(PACKAGE)…)
|
||||
$Q env GOBIN=$(BIN) $(GO) install $(PACKAGE)
|
||||
|
||||
GOLANGCI_LINT = $(BIN)/golangci-lint
|
||||
$(BIN)/golangci-lint: PACKAGE=github.com/golangci/golangci-lint/cmd/golangci-lint@v1.37.1
|
||||
|
||||
GOIMPORTS = $(BIN)/goimports
|
||||
$(BIN)/goimports: PACKAGE=golang.org/x/tools/cmd/goimports@v0.1.0
|
||||
|
||||
## build: Build
|
||||
.PHONY: build
|
||||
build: | build-frontend build-backend ; $(info $(M) building…)
|
||||
|
||||
## build-frontend: Build frontend
|
||||
.PHONY: build-frontend
|
||||
build-frontend: | ; $(info $(M) building frontend…)
|
||||
$Q cd frontend && npm ci && npm run build
|
||||
|
||||
## build-backend: Build backend
|
||||
.PHONY: build-backend
|
||||
build-backend: | ; $(info $(M) building backend…)
|
||||
$Q $(GO) build -ldflags '$(LDFLAGS)' -o .
|
||||
|
||||
## test: Run all tests
|
||||
.PHONY: test
|
||||
test: | test-frontend test-backend ; $(info $(M) running tests…)
|
||||
|
||||
## test-frontend: Run frontend tests
|
||||
.PHONY: test-frontend
|
||||
test-frontend: | ; $(info $(M) running frontend tests…)
|
||||
|
||||
## test-backend: Run backend tests
|
||||
.PHONY: test-backend
|
||||
test-backend: | ; $(info $(M) running backend tests…)
|
||||
$Q $(GO) test -v ./...
|
||||
|
||||
## lint: Lint
|
||||
.PHONY: lint
|
||||
lint: lint-frontend lint-backend lint-commits | ; $(info $(M) running all linters…)
|
||||
|
||||
## lint-frontend: Lint frontend
|
||||
.PHONY: lint-frontend
|
||||
lint-frontend: | ; $(info $(M) running frontend linters…)
|
||||
$Q cd frontend && npm ci && npm run lint
|
||||
|
||||
## lint-backend: Lint backend
|
||||
.PHONY: lint-backend
|
||||
lint-backend: | $(GOLANGCI_LINT) ; $(info $(M) running backend linters…)
|
||||
$Q $(GOLANGCI_LINT) run
|
||||
|
||||
## lint-commits: Lint commits
|
||||
.PHONY: lint-commits
|
||||
lint-commits: | ; $(info $(M) running commitlint…)
|
||||
$Q ./scripts/commitlint.sh
|
||||
|
||||
## bump-version: Bump app version
|
||||
.PHONY: bump-version
|
||||
bump-version: | ; $(info $(M) creating a new release…)
|
||||
$Q ./scripts/bump_version.sh
|
||||
|
||||
## help: Show this help
|
||||
.PHONY: help
|
||||
help:
|
||||
@sed -n 's/^## //p' $(MAKEFILE_LIST) | column -t -s ':' | sed -e 's/^/ /' | sort
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||

|
||||
|
||||
[](https://travis-ci.com/filebrowser/filebrowser)
|
||||
[](https://github.com/filebrowser/filebrowser/actions/workflows/main.yaml)
|
||||
[](https://goreportcard.com/report/github.com/filebrowser/filebrowser)
|
||||
[](http://godoc.org/github.com/filebrowser/filebrowser)
|
||||
[](https://github.com/filebrowser/filebrowser/releases/latest)
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
// Auther is the authentication interface.
|
||||
type Auther interface {
|
||||
// Auth is called to authenticate a request.
|
||||
Auth(r *http.Request, s *users.Storage, root string) (*users.User, error)
|
||||
Auth(r *http.Request, s users.Store, root string) (*users.User, error)
|
||||
// LoginPage indicates if this auther needs a login page.
|
||||
LoginPage() bool
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ type JSONAuth struct {
|
||||
}
|
||||
|
||||
// Auth authenticates the user via a json in content body.
|
||||
func (a JSONAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users.User, error) {
|
||||
func (a JSONAuth) Auth(r *http.Request, sto users.Store, root string) (*users.User, error) {
|
||||
var cred jsonCred
|
||||
|
||||
if r.Body == nil {
|
||||
@@ -40,7 +40,7 @@ func (a JSONAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users
|
||||
|
||||
// If ReCaptcha is enabled, check the code.
|
||||
if a.ReCaptcha != nil && len(a.ReCaptcha.Secret) > 0 {
|
||||
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:shadow
|
||||
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:govet
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -14,7 +14,7 @@ const MethodNoAuth settings.AuthMethod = "noauth"
|
||||
type NoAuth struct{}
|
||||
|
||||
// Auth uses authenticates user 1.
|
||||
func (a NoAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users.User, error) {
|
||||
func (a NoAuth) Auth(r *http.Request, sto users.Store, root string) (*users.User, error) {
|
||||
return sto.Get(root, uint(1))
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ type ProxyAuth struct {
|
||||
}
|
||||
|
||||
// Auth authenticates the user via an HTTP header.
|
||||
func (a ProxyAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users.User, error) {
|
||||
func (a ProxyAuth) Auth(r *http.Request, sto users.Store, root string) (*users.User, error) {
|
||||
username := r.Header.Get(a.Header)
|
||||
user, err := sto.Get(root, username)
|
||||
if err == errors.ErrNotExist {
|
||||
|
||||
@@ -14,7 +14,7 @@ var cmdsAddCmd = &cobra.Command{
|
||||
Use: "add <event> <command>",
|
||||
Short: "Add a command to run on a specific event",
|
||||
Long: `Add a command to run on a specific event.`,
|
||||
Args: cobra.MinimumNArgs(2), //nolint:mnd
|
||||
Args: cobra.MinimumNArgs(2), //nolint:gomnd
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
s, err := d.store.Settings.Get()
|
||||
checkErr(err)
|
||||
|
||||
@@ -23,7 +23,7 @@ You can also specify an optional parameter (index_end) so
|
||||
you can remove all commands from 'index' to 'index_end',
|
||||
including 'index_end'.`,
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if err := cobra.RangeArgs(2, 3)(cmd, args); err != nil { //nolint:mnd
|
||||
if err := cobra.RangeArgs(2, 3)(cmd, args); err != nil { //nolint:gomnd
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ including 'index_end'.`,
|
||||
i, err := strconv.Atoi(args[1])
|
||||
checkErr(err)
|
||||
f := i
|
||||
if len(args) == 3 { //nolint:mnd
|
||||
if len(args) == 3 { //nolint:gomnd
|
||||
f, err = strconv.Atoi(args[2])
|
||||
checkErr(err)
|
||||
}
|
||||
|
||||
22
cmd/root.go
22
cmd/root.go
@@ -3,6 +3,7 @@ package cmd
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/auth"
|
||||
"github.com/filebrowser/filebrowser/v2/diskcache"
|
||||
"github.com/filebrowser/filebrowser/v2/frontend"
|
||||
fbhttp "github.com/filebrowser/filebrowser/v2/http"
|
||||
"github.com/filebrowser/filebrowser/v2/img"
|
||||
"github.com/filebrowser/filebrowser/v2/settings"
|
||||
@@ -66,6 +68,7 @@ func addServerFlags(flags *pflag.FlagSet) {
|
||||
flags.Bool("disable-thumbnails", false, "disable image thumbnails")
|
||||
flags.Bool("disable-preview-resize", false, "disable resize of image previews")
|
||||
flags.Bool("disable-exec", false, "disables Command Runner feature")
|
||||
flags.Bool("disable-type-detection-by-header", false, "disables type detection by reading file headers")
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
@@ -151,12 +154,15 @@ user created with the credentials from options "username" and "password".`,
|
||||
err = os.Chmod(server.Socket, os.FileMode(socketPerm))
|
||||
checkErr(err)
|
||||
case server.TLSKey != "" && server.TLSCert != "":
|
||||
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:shadow
|
||||
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:govet
|
||||
checkErr(err)
|
||||
listener, err = tls.Listen("tcp", adr, &tls.Config{Certificates: []tls.Certificate{cer}}) //nolint:shadow
|
||||
listener, err = tls.Listen("tcp", adr, &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
Certificates: []tls.Certificate{cer}},
|
||||
)
|
||||
checkErr(err)
|
||||
default:
|
||||
listener, err = net.Listen("tcp", adr) //nolint:shadow
|
||||
listener, err = net.Listen("tcp", adr)
|
||||
checkErr(err)
|
||||
}
|
||||
|
||||
@@ -164,7 +170,12 @@ user created with the credentials from options "username" and "password".`,
|
||||
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
|
||||
go cleanupHandler(listener, sigc)
|
||||
|
||||
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server)
|
||||
assetsFs, err := fs.Sub(frontend.Assets(), "dist")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server, assetsFs)
|
||||
checkErr(err)
|
||||
|
||||
defer listener.Close()
|
||||
@@ -243,6 +254,9 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
|
||||
_, disablePreviewResize := getParamB(flags, "disable-preview-resize")
|
||||
server.ResizePreview = !disablePreviewResize
|
||||
|
||||
_, disableTypeDetectionByHeader := getParamB(flags, "disable-type-detection-by-header")
|
||||
server.TypeDetectionByHeader = !disableTypeDetectionByHeader
|
||||
|
||||
_, disableExec := getParamB(flags, "disable-exec")
|
||||
server.EnableExec = !disableExec
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ including 'index_end'.`,
|
||||
i, err := strconv.Atoi(args[0])
|
||||
checkErr(err)
|
||||
f := i
|
||||
if len(args) == 2 { //nolint:mnd
|
||||
if len(args) == 2 { //nolint:gomnd
|
||||
f, err = strconv.Atoi(args[1])
|
||||
checkErr(err)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ var usersAddCmd = &cobra.Command{
|
||||
Use: "add <username> <password>",
|
||||
Short: "Create a new user",
|
||||
Long: `Create a new user and add it to the database.`,
|
||||
Args: cobra.ExactArgs(2), //nolint:mnd
|
||||
Args: cobra.ExactArgs(2), //nolint:gomnd
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
s, err := d.store.Settings.Get()
|
||||
checkErr(err)
|
||||
|
||||
@@ -67,7 +67,7 @@ list or set it to 0.`,
|
||||
// with the new username. If there is, print an error and cancel the
|
||||
// operation
|
||||
if user.Username != onDB.Username {
|
||||
if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:shadow
|
||||
if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:govet
|
||||
checkErr(usernameConflictError(user.Username, conflictuous.ID, user.ID))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ func dbExists(path string) (bool, error) {
|
||||
d := filepath.Dir(path)
|
||||
_, err = os.Stat(d)
|
||||
if os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(d, 0700); err != nil { //nolint:shadow
|
||||
if err := os.MkdirAll(d, 0700); err != nil { //nolint:govet
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
|
||||
34
commitlint.config.js
Normal file
34
commitlint.config.js
Normal file
@@ -0,0 +1,34 @@
|
||||
module.exports = {
|
||||
rules: {
|
||||
'body-leading-blank': [1, 'always'],
|
||||
'body-max-line-length': [2, 'always', 100],
|
||||
'footer-leading-blank': [1, 'always'],
|
||||
'footer-max-line-length': [2, 'always', 100],
|
||||
'header-max-length': [2, 'always', 100],
|
||||
'scope-case': [2, 'always', 'lower-case'],
|
||||
'subject-case': [
|
||||
2,
|
||||
'never',
|
||||
['sentence-case', 'start-case', 'pascal-case', 'upper-case'],
|
||||
],
|
||||
'subject-full-stop': [2, 'never', '.'],
|
||||
'type-case': [2, 'always', 'lower-case'],
|
||||
'type-empty': [2, 'never'],
|
||||
'type-enum': [
|
||||
2,
|
||||
'always',
|
||||
[
|
||||
'feat',
|
||||
'fix',
|
||||
'perf',
|
||||
'revert',
|
||||
'refactor',
|
||||
'build',
|
||||
'ci',
|
||||
'test',
|
||||
'chore',
|
||||
'docs',
|
||||
],
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -17,4 +17,5 @@ var (
|
||||
ErrPermissionDenied = errors.New("permission denied")
|
||||
ErrInvalidRequestParams = errors.New("invalid request params")
|
||||
ErrSourceIsParent = errors.New("source is parent")
|
||||
ErrRootUserDeletion = errors.New("user with id 1 can't be deleted")
|
||||
)
|
||||
|
||||
111
files/file.go
111
files/file.go
@@ -38,15 +38,18 @@ type FileInfo struct {
|
||||
Subtitles []string `json:"subtitles,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Checksums map[string]string `json:"checksums,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
}
|
||||
|
||||
// FileOptions are the options when getting a file info.
|
||||
type FileOptions struct {
|
||||
Fs afero.Fs
|
||||
Path string
|
||||
Modify bool
|
||||
Expand bool
|
||||
Checker rules.Checker
|
||||
Fs afero.Fs
|
||||
Path string
|
||||
Modify bool
|
||||
Expand bool
|
||||
ReadHeader bool
|
||||
Token string
|
||||
Checker rules.Checker
|
||||
}
|
||||
|
||||
// NewFileInfo creates a File object from a path and a given user. This File
|
||||
@@ -71,17 +74,18 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
|
||||
IsDir: info.IsDir(),
|
||||
Size: info.Size(),
|
||||
Extension: filepath.Ext(info.Name()),
|
||||
Token: opts.Token,
|
||||
}
|
||||
|
||||
if opts.Expand {
|
||||
if file.IsDir {
|
||||
if err := file.readListing(opts.Checker); err != nil { //nolint:shadow
|
||||
if err := file.readListing(opts.Checker, opts.ReadHeader); err != nil { //nolint:govet
|
||||
return nil, err
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
err = file.detectType(opts.Modify, true)
|
||||
err = file.detectType(opts.Modify, true, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -134,7 +138,7 @@ func (i *FileInfo) Checksum(algo string) error {
|
||||
|
||||
//nolint:goconst
|
||||
//TODO: use constants
|
||||
func (i *FileInfo) detectType(modify, saveContent bool) error {
|
||||
func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
|
||||
if IsNamedPipe(i.Mode) {
|
||||
i.Type = "blob"
|
||||
return nil
|
||||
@@ -143,6 +147,54 @@ func (i *FileInfo) detectType(modify, saveContent bool) error {
|
||||
// imagine the situation where a file in a dir with thousands
|
||||
// of files couldn't be opened: we'd have immediately
|
||||
// a 500 even though it doesn't matter. So we just log it.
|
||||
|
||||
mimetype := mime.TypeByExtension(i.Extension)
|
||||
|
||||
var buffer []byte
|
||||
if readHeader {
|
||||
buffer = i.readFirstBytes()
|
||||
|
||||
if mimetype == "" {
|
||||
mimetype = http.DetectContentType(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(mimetype, "video"):
|
||||
i.Type = "video"
|
||||
i.detectSubtitles()
|
||||
return nil
|
||||
case strings.HasPrefix(mimetype, "audio"):
|
||||
i.Type = "audio"
|
||||
return nil
|
||||
case strings.HasPrefix(mimetype, "image"):
|
||||
i.Type = "image"
|
||||
return nil
|
||||
case (strings.HasPrefix(mimetype, "text") || (len(buffer) > 0 && !isBinary(buffer))) && i.Size <= 10*1024*1024: // 10 MB
|
||||
i.Type = "text"
|
||||
|
||||
if !modify {
|
||||
i.Type = "textImmutable"
|
||||
}
|
||||
|
||||
if saveContent {
|
||||
afs := &afero.Afero{Fs: i.Fs}
|
||||
content, err := afs.ReadFile(i.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i.Content = string(content)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
i.Type = "blob"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *FileInfo) readFirstBytes() []byte {
|
||||
reader, err := i.Fs.Open(i.Path)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
@@ -159,44 +211,7 @@ func (i *FileInfo) detectType(modify, saveContent bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
mimetype := mime.TypeByExtension(i.Extension)
|
||||
if mimetype == "" {
|
||||
mimetype = http.DetectContentType(buffer[:n])
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(mimetype, "video"):
|
||||
i.Type = "video"
|
||||
i.detectSubtitles()
|
||||
return nil
|
||||
case strings.HasPrefix(mimetype, "audio"):
|
||||
i.Type = "audio"
|
||||
return nil
|
||||
case strings.HasPrefix(mimetype, "image"):
|
||||
i.Type = "image"
|
||||
return nil
|
||||
case isBinary(buffer[:n], n) || i.Size > 10*1024*1024: // 10 MB
|
||||
i.Type = "blob"
|
||||
return nil
|
||||
default:
|
||||
i.Type = "text"
|
||||
|
||||
if !modify {
|
||||
i.Type = "textImmutable"
|
||||
}
|
||||
|
||||
if saveContent {
|
||||
afs := &afero.Afero{Fs: i.Fs}
|
||||
content, err := afs.ReadFile(i.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i.Content = string(content)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return buffer[:n]
|
||||
}
|
||||
|
||||
func (i *FileInfo) detectSubtitles() {
|
||||
@@ -215,7 +230,7 @@ func (i *FileInfo) detectSubtitles() {
|
||||
}
|
||||
}
|
||||
|
||||
func (i *FileInfo) readListing(checker rules.Checker) error {
|
||||
func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
|
||||
afs := &afero.Afero{Fs: i.Fs}
|
||||
dir, err := afs.ReadDir(i.Path)
|
||||
if err != nil {
|
||||
@@ -261,7 +276,7 @@ func (i *FileInfo) readListing(checker rules.Checker) error {
|
||||
} else {
|
||||
listing.NumFiles++
|
||||
|
||||
err := file.detectType(true, false)
|
||||
err := file.detectType(true, false, readHeader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func isBinary(content []byte, _ int) bool {
|
||||
func isBinary(content []byte) bool {
|
||||
maybeStr := string(content)
|
||||
runeCnt := utf8.RuneCount(content)
|
||||
runeIndex := 0
|
||||
|
||||
@@ -9,6 +9,25 @@ import (
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// MoveFile moves file from src to dst.
|
||||
// By default the rename filesystem system call is used. If src and dst point to different volumes
|
||||
// the file copy is used as a fallback
|
||||
func MoveFile(fs afero.Fs, src, dst string) error {
|
||||
if fs.Rename(src, dst) == nil {
|
||||
return nil
|
||||
}
|
||||
// fallback
|
||||
err := CopyFile(fs, src, dst)
|
||||
if err != nil {
|
||||
_ = fs.Remove(dst)
|
||||
return err
|
||||
}
|
||||
if err := fs.Remove(src); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CopyFile copies a file from source to dest and returns
|
||||
// an error if any.
|
||||
func CopyFile(fs afero.Fs, source, dest string) error {
|
||||
@@ -39,14 +58,14 @@ func CopyFile(fs afero.Fs, source, dest string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy the mode if the user can't
|
||||
// open the file.
|
||||
// Copy the mode
|
||||
info, err := fs.Stat(source)
|
||||
if err != nil {
|
||||
err = fs.Chmod(dest, info.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
err = fs.Chmod(dest, info.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
12
frontend/assets.go
Normal file
12
frontend/assets.go
Normal file
@@ -0,0 +1,12 @@
|
||||
// +build !dev
|
||||
|
||||
package frontend
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed dist/*
|
||||
var assets embed.FS
|
||||
|
||||
func Assets() embed.FS {
|
||||
return assets
|
||||
}
|
||||
14
frontend/assets_dev.go
Normal file
14
frontend/assets_dev.go
Normal file
@@ -0,0 +1,14 @@
|
||||
// +build dev
|
||||
|
||||
package frontend
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
)
|
||||
|
||||
var assets fs.FS = os.DirFS("frontend")
|
||||
|
||||
func Assets() fs.FS {
|
||||
return assets
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/app'
|
||||
]
|
||||
}
|
||||
presets: ["@vue/app"],
|
||||
};
|
||||
|
||||
4
frontend/dist/.gitignore
vendored
Normal file
4
frontend/dist/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Ignore everything in this directory
|
||||
*
|
||||
# Except this file
|
||||
!.gitignore
|
||||
25292
frontend/package-lock.json
generated
25292
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,13 +4,15 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"watch": "vue-cli-service build --watch",
|
||||
"lint": "vue-cli-service lint --fix"
|
||||
"build": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitignore' -exec rm -r {} + && vue-cli-service build --no-clean",
|
||||
"lint": "npx vue-cli-service lint --no-fix",
|
||||
"fix": "npx vue-cli-service lint",
|
||||
"watch": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitignore' -exec rm -r {} + && vue-cli-service build --watch --no-clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"ace-builds": "^1.4.7",
|
||||
"clipboard": "^2.0.4",
|
||||
"core-js": "^3.9.1",
|
||||
"js-base64": "^2.5.1",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
@@ -19,6 +21,7 @@
|
||||
"normalize.css": "^8.0.1",
|
||||
"noty": "^3.2.0-beta",
|
||||
"qrcode.vue": "^1.7.0",
|
||||
"utif": "^3.1.0",
|
||||
"vue": "^2.6.10",
|
||||
"vue-i18n": "^8.15.3",
|
||||
"vue-lazyload": "^1.3.3",
|
||||
@@ -28,11 +31,14 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^4.1.2",
|
||||
"@vue/cli-plugin-eslint": "^4.1.1",
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"@vue/cli-service": "^4.1.2",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"@vue/eslint-config-prettier": "^6.0.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^6.1.2",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"prettier": "^2.2.1",
|
||||
"vue-template-compiler": "^2.6.10"
|
||||
},
|
||||
"eslintConfig": {
|
||||
@@ -42,7 +48,8 @@
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"eslint:recommended"
|
||||
"eslint:recommended",
|
||||
"@vue/prettier"
|
||||
],
|
||||
"rules": {},
|
||||
"parserOptions": {
|
||||
|
||||
@@ -69,13 +69,16 @@ nav > div {
|
||||
border-color: var(--divider);
|
||||
}
|
||||
|
||||
#breadcrumbs {
|
||||
.breadcrumbs {
|
||||
border-color: var(--divider);
|
||||
color: var(--textPrimary) !important;
|
||||
}
|
||||
#breadcrumbs span {
|
||||
.breadcrumbs span {
|
||||
color: var(--textPrimary) !important;
|
||||
}
|
||||
.breadcrumbs a:hover {
|
||||
background-color: rgba(255, 255, 255, .1);
|
||||
}
|
||||
|
||||
#listing .item {
|
||||
background: var(--surfacePrimary);
|
||||
@@ -114,13 +117,20 @@ nav > div {
|
||||
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 ul li input,
|
||||
.card#share ul li select,
|
||||
.card#share input,
|
||||
.card#share select,
|
||||
.input {
|
||||
background: var(--surfaceSecondary);
|
||||
color: var(--textPrimary);
|
||||
@@ -138,7 +148,7 @@ nav > div {
|
||||
background: #147A41;
|
||||
}
|
||||
|
||||
.dashboard #nav li,
|
||||
.dashboard #nav .wrapper,
|
||||
.collapsible {
|
||||
border-color: var(--divider);
|
||||
}
|
||||
|
||||
@@ -6,18 +6,18 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'app',
|
||||
mounted () {
|
||||
const loading = document.getElementById('loading')
|
||||
loading.classList.add('done')
|
||||
name: "app",
|
||||
mounted() {
|
||||
const loading = document.getElementById("loading");
|
||||
loading.classList.add("done");
|
||||
|
||||
setTimeout(function () {
|
||||
loading.parentNode.removeChild(loading)
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
loading.parentNode.removeChild(loading);
|
||||
}, 200);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import './css/styles.css';
|
||||
@import "./css/styles.css";
|
||||
</style>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { removePrefix } from './utils'
|
||||
import { baseURL } from '@/utils/constants'
|
||||
import store from '@/store'
|
||||
import { removePrefix } from "./utils";
|
||||
import { baseURL } from "@/utils/constants";
|
||||
import store from "@/store";
|
||||
|
||||
const ssl = (window.location.protocol === 'https:')
|
||||
const protocol = (ssl ? 'wss:' : 'ws:')
|
||||
const ssl = window.location.protocol === "https:";
|
||||
const protocol = ssl ? "wss:" : "ws:";
|
||||
|
||||
export default function command(url, command, onmessage, onclose) {
|
||||
url = removePrefix(url)
|
||||
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${store.state.jwt}`
|
||||
url = removePrefix(url);
|
||||
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${store.state.jwt}`;
|
||||
|
||||
let conn = new window.WebSocket(url)
|
||||
conn.onopen = () => conn.send(command)
|
||||
conn.onmessage = onmessage
|
||||
conn.onclose = onclose
|
||||
let conn = new window.WebSocket(url);
|
||||
conn.onopen = () => conn.send(command);
|
||||
conn.onmessage = onmessage;
|
||||
conn.onclose = onclose;
|
||||
}
|
||||
|
||||
@@ -1,144 +1,156 @@
|
||||
import { fetchURL, removePrefix } from './utils'
|
||||
import { baseURL } from '@/utils/constants'
|
||||
import store from '@/store'
|
||||
import { fetchURL, removePrefix } from "./utils";
|
||||
import { baseURL } from "@/utils/constants";
|
||||
import store from "@/store";
|
||||
|
||||
export async function fetch (url) {
|
||||
url = removePrefix(url)
|
||||
export async function fetch(url) {
|
||||
url = removePrefix(url);
|
||||
|
||||
const res = await fetchURL(`/api/resources${url}`, {})
|
||||
const res = await fetchURL(`/api/resources${url}`, {});
|
||||
|
||||
if (res.status === 200) {
|
||||
let data = await res.json()
|
||||
data.url = `/files${url}`
|
||||
let data = await res.json();
|
||||
data.url = `/files${url}`;
|
||||
|
||||
if (data.isDir) {
|
||||
if (!data.url.endsWith('/')) data.url += '/'
|
||||
if (!data.url.endsWith("/")) data.url += "/";
|
||||
data.items = data.items.map((item, index) => {
|
||||
item.index = index
|
||||
item.url = `${data.url}${encodeURIComponent(item.name)}`
|
||||
item.index = index;
|
||||
item.url = `${data.url}${encodeURIComponent(item.name)}`;
|
||||
|
||||
if (item.isDir) {
|
||||
item.url += '/'
|
||||
item.url += "/";
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
return data
|
||||
return data;
|
||||
} else {
|
||||
throw new Error(res.status)
|
||||
throw new Error(res.status);
|
||||
}
|
||||
}
|
||||
|
||||
async function resourceAction (url, method, content) {
|
||||
url = removePrefix(url)
|
||||
async function resourceAction(url, method, content) {
|
||||
url = removePrefix(url);
|
||||
|
||||
let opts = { method }
|
||||
let opts = { method };
|
||||
|
||||
if (content) {
|
||||
opts.body = content
|
||||
opts.body = content;
|
||||
}
|
||||
|
||||
const res = await fetchURL(`/api/resources${url}`, opts)
|
||||
const res = await fetchURL(`/api/resources${url}`, opts);
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error(await res.text())
|
||||
throw new Error(await res.text());
|
||||
} else {
|
||||
return res
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove (url) {
|
||||
return resourceAction(url, 'DELETE')
|
||||
export async function remove(url) {
|
||||
return resourceAction(url, "DELETE");
|
||||
}
|
||||
|
||||
export async function put (url, content = '') {
|
||||
return resourceAction(url, 'PUT', content)
|
||||
export async function put(url, content = "") {
|
||||
return resourceAction(url, "PUT", content);
|
||||
}
|
||||
|
||||
export function download (format, ...files) {
|
||||
let url = `${baseURL}/api/raw`
|
||||
export function download(format, ...files) {
|
||||
let url = `${baseURL}/api/raw`;
|
||||
|
||||
if (files.length === 1) {
|
||||
url += removePrefix(files[0]) + '?'
|
||||
url += removePrefix(files[0]) + "?";
|
||||
} else {
|
||||
let arg = ''
|
||||
let arg = "";
|
||||
|
||||
for (let file of files) {
|
||||
arg += removePrefix(file) + ','
|
||||
arg += removePrefix(file) + ",";
|
||||
}
|
||||
|
||||
arg = arg.substring(0, arg.length - 1)
|
||||
arg = encodeURIComponent(arg)
|
||||
url += `/?files=${arg}&`
|
||||
arg = arg.substring(0, arg.length - 1);
|
||||
arg = encodeURIComponent(arg);
|
||||
url += `/?files=${arg}&`;
|
||||
}
|
||||
|
||||
if (format !== null) {
|
||||
url += `algo=${format}&`
|
||||
if (format) {
|
||||
url += `algo=${format}&`;
|
||||
}
|
||||
|
||||
url += `auth=${store.state.jwt}`
|
||||
window.open(url)
|
||||
if (store.state.jwt) {
|
||||
url += `auth=${store.state.jwt}&`;
|
||||
}
|
||||
|
||||
window.open(url);
|
||||
}
|
||||
|
||||
export async function post (url, content = '', overwrite = false, onupload) {
|
||||
url = removePrefix(url)
|
||||
export async function post(url, content = "", overwrite = false, onupload) {
|
||||
url = removePrefix(url);
|
||||
|
||||
let bufferContent
|
||||
if (content instanceof Blob && !['http:', 'https:'].includes(window.location.protocol)) {
|
||||
bufferContent = await new Response(content).arrayBuffer()
|
||||
let bufferContent;
|
||||
if (
|
||||
content instanceof Blob &&
|
||||
!["http:", "https:"].includes(window.location.protocol)
|
||||
) {
|
||||
bufferContent = await new Response(content).arrayBuffer();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new XMLHttpRequest()
|
||||
request.open('POST', `${baseURL}/api/resources${url}?override=${overwrite}`, true)
|
||||
request.setRequestHeader('X-Auth', store.state.jwt)
|
||||
let request = new XMLHttpRequest();
|
||||
request.open(
|
||||
"POST",
|
||||
`${baseURL}/api/resources${url}?override=${overwrite}`,
|
||||
true
|
||||
);
|
||||
request.setRequestHeader("X-Auth", store.state.jwt);
|
||||
|
||||
if (typeof onupload === 'function') {
|
||||
request.upload.onprogress = onupload
|
||||
if (typeof onupload === "function") {
|
||||
request.upload.onprogress = onupload;
|
||||
}
|
||||
|
||||
request.onload = () => {
|
||||
if (request.status === 200) {
|
||||
resolve(request.responseText)
|
||||
resolve(request.responseText);
|
||||
} else if (request.status === 409) {
|
||||
reject(request.status)
|
||||
reject(request.status);
|
||||
} else {
|
||||
reject(request.responseText)
|
||||
reject(request.responseText);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = (error) => {
|
||||
reject(error)
|
||||
}
|
||||
reject(error);
|
||||
};
|
||||
|
||||
request.send(bufferContent || content)
|
||||
})
|
||||
request.send(bufferContent || content);
|
||||
});
|
||||
}
|
||||
|
||||
function moveCopy (items, copy = false, overwrite = false, rename = false) {
|
||||
let promises = []
|
||||
function moveCopy(items, copy = false, overwrite = false, rename = false) {
|
||||
let promises = [];
|
||||
|
||||
for (let item of items) {
|
||||
const from = removePrefix(item.from)
|
||||
const to = encodeURIComponent(removePrefix(item.to))
|
||||
const url = `${from}?action=${copy ? 'copy' : 'rename'}&destination=${to}&override=${overwrite}&rename=${rename}`
|
||||
promises.push(resourceAction(url, 'PATCH'))
|
||||
const from = item.from;
|
||||
const to = encodeURIComponent(removePrefix(item.to));
|
||||
const url = `${from}?action=${
|
||||
copy ? "copy" : "rename"
|
||||
}&destination=${to}&override=${overwrite}&rename=${rename}`;
|
||||
promises.push(resourceAction(url, "PATCH"));
|
||||
}
|
||||
|
||||
return Promise.all(promises)
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
export function move (items, overwrite = false, rename = false) {
|
||||
return moveCopy(items, false, overwrite, rename)
|
||||
export function move(items, overwrite = false, rename = false) {
|
||||
return moveCopy(items, false, overwrite, rename);
|
||||
}
|
||||
|
||||
export function copy (items, overwrite = false, rename = false) {
|
||||
return moveCopy(items, true, overwrite, rename)
|
||||
export function copy(items, overwrite = false, rename = false) {
|
||||
return moveCopy(items, true, overwrite, rename);
|
||||
}
|
||||
|
||||
export async function checksum (url, algo) {
|
||||
const data = await resourceAction(`${url}?checksum=${algo}`, 'GET')
|
||||
return (await data.json()).checksums[algo]
|
||||
export async function checksum(url, algo) {
|
||||
const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
|
||||
return (await data.json()).checksums[algo];
|
||||
}
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import * as files from './files'
|
||||
import * as share from './share'
|
||||
import * as users from './users'
|
||||
import * as settings from './settings'
|
||||
import search from './search'
|
||||
import commands from './commands'
|
||||
import * as files from "./files";
|
||||
import * as share from "./share";
|
||||
import * as users from "./users";
|
||||
import * as settings from "./settings";
|
||||
import * as pub from "./pub";
|
||||
import search from "./search";
|
||||
import commands from "./commands";
|
||||
|
||||
export {
|
||||
files,
|
||||
share,
|
||||
users,
|
||||
settings,
|
||||
commands,
|
||||
search
|
||||
}
|
||||
export { files, share, users, settings, pub, commands, search };
|
||||
|
||||
61
frontend/src/api/pub.js
Normal file
61
frontend/src/api/pub.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import { fetchURL, removePrefix } from "./utils";
|
||||
import { baseURL } from "@/utils/constants";
|
||||
|
||||
export async function fetch(url, password = "") {
|
||||
url = removePrefix(url);
|
||||
|
||||
const res = await fetchURL(`/api/public/share${url}`, {
|
||||
headers: { "X-SHARE-PASSWORD": password },
|
||||
});
|
||||
|
||||
if (res.status === 200) {
|
||||
let data = await res.json();
|
||||
data.url = `/share${url}`;
|
||||
|
||||
if (data.isDir) {
|
||||
if (!data.url.endsWith("/")) data.url += "/";
|
||||
data.items = data.items.map((item, index) => {
|
||||
item.index = index;
|
||||
item.url = `${data.url}${encodeURIComponent(item.name)}`;
|
||||
|
||||
if (item.isDir) {
|
||||
item.url += "/";
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
} else {
|
||||
throw new Error(res.status);
|
||||
}
|
||||
}
|
||||
|
||||
export function download(format, hash, token, ...files) {
|
||||
let url = `${baseURL}/api/public/dl/${hash}`;
|
||||
|
||||
if (files.length === 1) {
|
||||
url += encodeURIComponent(files[0]) + "?";
|
||||
} else {
|
||||
let arg = "";
|
||||
|
||||
for (let file of files) {
|
||||
arg += encodeURIComponent(file) + ",";
|
||||
}
|
||||
|
||||
arg = arg.substring(0, arg.length - 1);
|
||||
arg = encodeURIComponent(arg);
|
||||
url += `/?files=${arg}&`;
|
||||
}
|
||||
|
||||
if (format) {
|
||||
url += `algo=${format}&`;
|
||||
}
|
||||
|
||||
if (token) {
|
||||
url += `token=${token}&`;
|
||||
}
|
||||
|
||||
window.open(url);
|
||||
}
|
||||
@@ -1,31 +1,31 @@
|
||||
import { fetchURL, removePrefix } from './utils'
|
||||
import url from '../utils/url'
|
||||
import { fetchURL, removePrefix } from "./utils";
|
||||
import url from "../utils/url";
|
||||
|
||||
export default async function search (base, query) {
|
||||
base = removePrefix(base)
|
||||
query = encodeURIComponent(query)
|
||||
export default async function search(base, query) {
|
||||
base = removePrefix(base);
|
||||
query = encodeURIComponent(query);
|
||||
|
||||
if (!base.endsWith('/')) {
|
||||
base += '/'
|
||||
if (!base.endsWith("/")) {
|
||||
base += "/";
|
||||
}
|
||||
|
||||
let res = await fetchURL(`/api/search${base}?query=${query}`, {})
|
||||
let res = await fetchURL(`/api/search${base}?query=${query}`, {});
|
||||
|
||||
if (res.status === 200) {
|
||||
let data = await res.json()
|
||||
let data = await res.json();
|
||||
|
||||
data = data.map((item) => {
|
||||
item.url = `/files${base}` + url.encodePath(item.path)
|
||||
item.url = `/files${base}` + url.encodePath(item.path);
|
||||
|
||||
if (item.dir) {
|
||||
item.url += '/'
|
||||
item.url += "/";
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
return item;
|
||||
});
|
||||
|
||||
return data
|
||||
return data;
|
||||
} else {
|
||||
throw Error(res.status)
|
||||
throw Error(res.status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { fetchURL, fetchJSON } from './utils'
|
||||
import { fetchURL, fetchJSON } from "./utils";
|
||||
|
||||
export function get () {
|
||||
return fetchJSON(`/api/settings`, {})
|
||||
export function get() {
|
||||
return fetchJSON(`/api/settings`, {});
|
||||
}
|
||||
|
||||
export async function update (settings) {
|
||||
export async function update(settings) {
|
||||
const res = await fetchURL(`/api/settings`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(settings)
|
||||
})
|
||||
method: "PUT",
|
||||
body: JSON.stringify(settings),
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error(res.status)
|
||||
throw new Error(res.status);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,36 @@
|
||||
import { fetchURL, fetchJSON, removePrefix } from './utils'
|
||||
import { fetchURL, fetchJSON, removePrefix } from "./utils";
|
||||
|
||||
export async function getHash(hash) {
|
||||
return fetchJSON(`/api/public/share/${hash}`)
|
||||
export async function list() {
|
||||
return fetchJSON("/api/shares");
|
||||
}
|
||||
|
||||
export async function get(url) {
|
||||
url = removePrefix(url)
|
||||
return fetchJSON(`/api/share${url}`)
|
||||
url = removePrefix(url);
|
||||
return fetchJSON(`/api/share${url}`);
|
||||
}
|
||||
|
||||
export async function remove(hash) {
|
||||
const res = await fetchURL(`/api/share/${hash}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error(res.status)
|
||||
throw new Error(res.status);
|
||||
}
|
||||
}
|
||||
|
||||
export async function create(url, expires = '', unit = 'hours') {
|
||||
url = removePrefix(url)
|
||||
url = `/api/share${url}`
|
||||
if (expires !== '') {
|
||||
url += `?expires=${expires}&unit=${unit}`
|
||||
export async function create(url, password = "", expires = "", unit = "hours") {
|
||||
url = removePrefix(url);
|
||||
url = `/api/share${url}`;
|
||||
if (expires !== "") {
|
||||
url += `?expires=${expires}&unit=${unit}`;
|
||||
}
|
||||
let body = "{}";
|
||||
if (password != "" || expires !== "" || unit !== "hours") {
|
||||
body = JSON.stringify({ password: password, expires: expires, unit: unit });
|
||||
}
|
||||
|
||||
return fetchJSON(url, {
|
||||
method: 'POST'
|
||||
})
|
||||
method: "POST",
|
||||
body: body,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,52 +1,51 @@
|
||||
import { fetchURL, fetchJSON } from './utils'
|
||||
import { fetchURL, fetchJSON } from "./utils";
|
||||
|
||||
export async function getAll () {
|
||||
return fetchJSON(`/api/users`, {})
|
||||
export async function getAll() {
|
||||
return fetchJSON(`/api/users`, {});
|
||||
}
|
||||
|
||||
export async function get (id) {
|
||||
return fetchJSON(`/api/users/${id}`, {})
|
||||
export async function get(id) {
|
||||
return fetchJSON(`/api/users/${id}`, {});
|
||||
}
|
||||
|
||||
export async function create (user) {
|
||||
export async function create(user) {
|
||||
const res = await fetchURL(`/api/users`, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
what: 'user',
|
||||
what: "user",
|
||||
which: [],
|
||||
data: user
|
||||
})
|
||||
})
|
||||
data: user,
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.status === 201) {
|
||||
return res.headers.get('Location')
|
||||
return res.headers.get("Location");
|
||||
} else {
|
||||
throw new Error(res.status)
|
||||
throw new Error(res.status);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export async function update (user, which = ['all']) {
|
||||
export async function update(user, which = ["all"]) {
|
||||
const res = await fetchURL(`/api/users/${user.id}`, {
|
||||
method: 'PUT',
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
what: 'user',
|
||||
what: "user",
|
||||
which: which,
|
||||
data: user
|
||||
})
|
||||
})
|
||||
data: user,
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error(res.status)
|
||||
throw new Error(res.status);
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove (id) {
|
||||
export async function remove(id) {
|
||||
const res = await fetchURL(`/api/users/${id}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error(res.status)
|
||||
throw new Error(res.status);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,42 @@
|
||||
import store from '@/store'
|
||||
import { renew } from '@/utils/auth'
|
||||
import { baseURL } from '@/utils/constants'
|
||||
import store from "@/store";
|
||||
import { renew } from "@/utils/auth";
|
||||
import { baseURL } from "@/utils/constants";
|
||||
|
||||
export async function fetchURL (url, opts) {
|
||||
opts = opts || {}
|
||||
opts.headers = opts.headers || {}
|
||||
export async function fetchURL(url, opts) {
|
||||
opts = opts || {};
|
||||
opts.headers = opts.headers || {};
|
||||
|
||||
let { headers, ...rest } = opts
|
||||
let { headers, ...rest } = opts;
|
||||
|
||||
const res = await fetch(`${baseURL}${url}`, {
|
||||
headers: {
|
||||
'X-Auth': store.state.jwt,
|
||||
...headers
|
||||
"X-Auth": store.state.jwt,
|
||||
...headers,
|
||||
},
|
||||
...rest
|
||||
})
|
||||
...rest,
|
||||
});
|
||||
|
||||
if (res.headers.get('X-Renew-Token') === 'true') {
|
||||
await renew(store.state.jwt)
|
||||
if (res.headers.get("X-Renew-Token") === "true") {
|
||||
await renew(store.state.jwt);
|
||||
}
|
||||
|
||||
return res
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function fetchJSON (url, opts) {
|
||||
const res = await fetchURL(url, opts)
|
||||
export async function fetchJSON(url, opts) {
|
||||
const res = await fetchURL(url, opts);
|
||||
|
||||
if (res.status === 200) {
|
||||
return res.json()
|
||||
return res.json();
|
||||
} else {
|
||||
throw new Error(res.status)
|
||||
throw new Error(res.status);
|
||||
}
|
||||
}
|
||||
|
||||
export function removePrefix (url) {
|
||||
if (url.startsWith('/files')) {
|
||||
url = url.slice(6)
|
||||
}
|
||||
export function removePrefix(url) {
|
||||
url = url.split("/").splice(2).join("/");
|
||||
|
||||
if (url === '') url = '/'
|
||||
if (url[0] !== '/') url = '/' + url
|
||||
return url
|
||||
if (url === "") url = "/";
|
||||
if (url[0] !== "/") url = "/" + url;
|
||||
return url;
|
||||
}
|
||||
|
||||
|
||||
75
frontend/src/components/Breadcrumbs.vue
Normal file
75
frontend/src/components/Breadcrumbs.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div class="breadcrumbs">
|
||||
<component
|
||||
:is="element"
|
||||
:to="base || ''"
|
||||
:aria-label="$t('files.home')"
|
||||
:title="$t('files.home')"
|
||||
>
|
||||
<i class="material-icons">home</i>
|
||||
</component>
|
||||
|
||||
<span v-for="(link, index) in items" :key="index">
|
||||
<span class="chevron"
|
||||
><i class="material-icons">keyboard_arrow_right</i></span
|
||||
>
|
||||
<component :is="element" :to="link.url">{{ link.name }}</component>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "breadcrumbs",
|
||||
props: ["base", "noLink"],
|
||||
computed: {
|
||||
items() {
|
||||
const relativePath = this.$route.path.replace(this.base, "");
|
||||
let parts = relativePath.split("/");
|
||||
|
||||
if (parts[0] === "") {
|
||||
parts.shift();
|
||||
}
|
||||
|
||||
if (parts[parts.length - 1] === "") {
|
||||
parts.pop();
|
||||
}
|
||||
|
||||
let breadcrumbs = [];
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (i === 0) {
|
||||
breadcrumbs.push({
|
||||
name: decodeURIComponent(parts[i]),
|
||||
url: this.base + "/" + parts[i] + "/",
|
||||
});
|
||||
} else {
|
||||
breadcrumbs.push({
|
||||
name: decodeURIComponent(parts[i]),
|
||||
url: breadcrumbs[i - 1].url + parts[i] + "/",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (breadcrumbs.length > 3) {
|
||||
while (breadcrumbs.length !== 4) {
|
||||
breadcrumbs.shift();
|
||||
}
|
||||
|
||||
breadcrumbs[0].name = "...";
|
||||
}
|
||||
|
||||
return breadcrumbs;
|
||||
},
|
||||
element() {
|
||||
if (this.noLink !== undefined) {
|
||||
return "span";
|
||||
}
|
||||
|
||||
return "router-link";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -1,184 +0,0 @@
|
||||
<template>
|
||||
<header v-if="!isEditor && !isPreview">
|
||||
<div>
|
||||
<button @click="openSidebar" :aria-label="$t('buttons.toggleSidebar')" :title="$t('buttons.toggleSidebar')" class="action">
|
||||
<i class="material-icons">menu</i>
|
||||
</button>
|
||||
<img :src="logoURL" alt="File Browser">
|
||||
<search v-if="isLogged"></search>
|
||||
</div>
|
||||
<div>
|
||||
<template v-if="isLogged">
|
||||
<button @click="openSearch" :aria-label="$t('buttons.search')" :title="$t('buttons.search')" class="search-button action">
|
||||
<i class="material-icons">search</i>
|
||||
</button>
|
||||
|
||||
<button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
|
||||
<i class="material-icons">more_vert</i>
|
||||
</button>
|
||||
|
||||
<!-- Menu that shows on listing AND mobile when there are files selected -->
|
||||
<div id="file-selection" v-if="isMobile && isListing">
|
||||
<span v-if="selectedCount > 0">{{ selectedCount }} selected</span>
|
||||
<share-button v-show="showShareButton"></share-button>
|
||||
<rename-button v-show="showRenameButton"></rename-button>
|
||||
<copy-button v-show="showCopyButton"></copy-button>
|
||||
<move-button v-show="showMoveButton"></move-button>
|
||||
<delete-button v-show="showDeleteButton"></delete-button>
|
||||
</div>
|
||||
|
||||
<!-- This buttons are shown on a dropdown on mobile phones -->
|
||||
<div id="dropdown" :class="{ active: showMore }">
|
||||
<div v-if="!isListing || !isMobile">
|
||||
<share-button v-show="showShareButton"></share-button>
|
||||
<rename-button v-show="showRenameButton"></rename-button>
|
||||
<copy-button v-show="showCopyButton"></copy-button>
|
||||
<move-button v-show="showMoveButton"></move-button>
|
||||
<delete-button v-show="showDeleteButton"></delete-button>
|
||||
</div>
|
||||
|
||||
<shell-button v-if="isExecEnabled && user.perm.execute" />
|
||||
<switch-button v-show="isListing"></switch-button>
|
||||
<download-button v-show="showDownloadButton"></download-button>
|
||||
<upload-button v-show="showUpload"></upload-button>
|
||||
<info-button v-show="isFiles"></info-button>
|
||||
|
||||
<button v-show="isListing" @click="toggleMultipleSelection" :aria-label="$t('buttons.selectMultiple')" :title="$t('buttons.selectMultiple')" class="action" >
|
||||
<i class="material-icons">check_circle</i>
|
||||
<span>{{ $t('buttons.select') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Search from './Search'
|
||||
import InfoButton from './buttons/Info'
|
||||
import DeleteButton from './buttons/Delete'
|
||||
import RenameButton from './buttons/Rename'
|
||||
import UploadButton from './buttons/Upload'
|
||||
import DownloadButton from './buttons/Download'
|
||||
import SwitchButton from './buttons/SwitchView'
|
||||
import MoveButton from './buttons/Move'
|
||||
import CopyButton from './buttons/Copy'
|
||||
import ShareButton from './buttons/Share'
|
||||
import ShellButton from './buttons/Shell'
|
||||
import {mapGetters, mapState} from 'vuex'
|
||||
import { logoURL, enableExec } from '@/utils/constants'
|
||||
import * as api from '@/api'
|
||||
import buttons from '@/utils/buttons'
|
||||
|
||||
export default {
|
||||
name: 'header-layout',
|
||||
components: {
|
||||
Search,
|
||||
InfoButton,
|
||||
DeleteButton,
|
||||
ShareButton,
|
||||
RenameButton,
|
||||
DownloadButton,
|
||||
CopyButton,
|
||||
UploadButton,
|
||||
SwitchButton,
|
||||
MoveButton,
|
||||
ShellButton
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
width: window.innerWidth,
|
||||
pluginData: {
|
||||
api,
|
||||
buttons,
|
||||
'store': this.$store,
|
||||
'router': this.$router
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
window.addEventListener('resize', () => {
|
||||
this.width = window.innerWidth
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'selectedCount',
|
||||
'isFiles',
|
||||
'isEditor',
|
||||
'isPreview',
|
||||
'isListing',
|
||||
'isLogged'
|
||||
]),
|
||||
...mapState([
|
||||
'req',
|
||||
'user',
|
||||
'loading',
|
||||
'reload',
|
||||
'multiple'
|
||||
]),
|
||||
logoURL: () => logoURL,
|
||||
isExecEnabled: () => enableExec,
|
||||
isMobile () {
|
||||
return this.width <= 736
|
||||
},
|
||||
showUpload () {
|
||||
return this.isListing && this.user.perm.create
|
||||
},
|
||||
showDownloadButton () {
|
||||
return this.isFiles && this.user.perm.download
|
||||
},
|
||||
showDeleteButton () {
|
||||
return this.isFiles && (this.isListing
|
||||
? (this.selectedCount !== 0 && this.user.perm.delete)
|
||||
: this.user.perm.delete)
|
||||
},
|
||||
showRenameButton () {
|
||||
return this.isFiles && (this.isListing
|
||||
? (this.selectedCount === 1 && this.user.perm.rename)
|
||||
: this.user.perm.rename)
|
||||
},
|
||||
showShareButton () {
|
||||
return this.isFiles && (this.isListing
|
||||
? (this.selectedCount === 1 && this.user.perm.share)
|
||||
: this.user.perm.share)
|
||||
},
|
||||
showMoveButton () {
|
||||
return this.isFiles && (this.isListing
|
||||
? (this.selectedCount > 0 && this.user.perm.rename)
|
||||
: this.user.perm.rename)
|
||||
},
|
||||
showCopyButton () {
|
||||
return this.isFiles && (this.isListing
|
||||
? (this.selectedCount > 0 && this.user.perm.create)
|
||||
: this.user.perm.create)
|
||||
},
|
||||
showMore () {
|
||||
return this.isFiles && this.$store.state.show === 'more'
|
||||
},
|
||||
showOverlay () {
|
||||
return this.showMore
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openSidebar () {
|
||||
this.$store.commit('showHover', 'sidebar')
|
||||
},
|
||||
openMore () {
|
||||
this.$store.commit('showHover', 'more')
|
||||
},
|
||||
openSearch () {
|
||||
this.$store.commit('showHover', 'search')
|
||||
},
|
||||
toggleMultipleSelection () {
|
||||
this.$store.commit('multiple', !this.multiple)
|
||||
this.resetPrompts()
|
||||
},
|
||||
resetPrompts () {
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div id="search" @click="open" v-bind:class="{ active , ongoing }">
|
||||
<div id="search" @click="open" v-bind:class="{ active, ongoing }">
|
||||
<div id="input">
|
||||
<button
|
||||
v-if="active"
|
||||
@@ -20,7 +20,7 @@
|
||||
v-model.trim="value"
|
||||
:aria-label="$t('search.search')"
|
||||
:placeholder="$t('search.search')"
|
||||
>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="result" ref="result">
|
||||
@@ -30,25 +30,25 @@
|
||||
|
||||
<template v-if="value.length === 0">
|
||||
<div class="boxes">
|
||||
<h3>{{ $t('search.types') }}</h3>
|
||||
<h3>{{ $t("search.types") }}</h3>
|
||||
<div>
|
||||
<div
|
||||
tabindex="0"
|
||||
v-for="(v,k) in boxes"
|
||||
v-for="(v, k) in boxes"
|
||||
:key="k"
|
||||
role="button"
|
||||
@click="init('type:'+k)"
|
||||
:aria-label="$t('search.'+v.label)"
|
||||
@click="init('type:' + k)"
|
||||
:aria-label="$t('search.' + v.label)"
|
||||
>
|
||||
<i class="material-icons">{{v.icon}}</i>
|
||||
<p>{{ $t('search.'+v.label) }}</p>
|
||||
<i class="material-icons">{{ v.icon }}</i>
|
||||
<p>{{ $t("search." + v.label) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<ul v-show="results.length > 0">
|
||||
<li v-for="(s,k) in filteredResults" :key="k">
|
||||
<li v-for="(s, k) in filteredResults" :key="k">
|
||||
<router-link @click.native="close" :to="s.url">
|
||||
<i v-if="s.dir" class="material-icons">folder</i>
|
||||
<i v-else class="material-icons">insert_drive_file</i>
|
||||
@@ -65,20 +65,20 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters, mapMutations } from "vuex"
|
||||
import url from "@/utils/url"
|
||||
import { search } from "@/api"
|
||||
import { mapState, mapGetters, mapMutations } from "vuex";
|
||||
import url from "@/utils/url";
|
||||
import { search } from "@/api";
|
||||
|
||||
var boxes = {
|
||||
image: { label: "images", icon: "insert_photo" },
|
||||
audio: { label: "music", icon: "volume_up" },
|
||||
video: { label: "video", icon: "movie" },
|
||||
pdf: { label: "pdf", icon: "picture_as_pdf" }
|
||||
}
|
||||
pdf: { label: "pdf", icon: "picture_as_pdf" },
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "search",
|
||||
data: function() {
|
||||
data: function () {
|
||||
return {
|
||||
value: "",
|
||||
active: false,
|
||||
@@ -86,111 +86,116 @@ export default {
|
||||
results: [],
|
||||
reload: false,
|
||||
resultsCount: 50,
|
||||
scrollable: null
|
||||
}
|
||||
scrollable: null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
show (val, old) {
|
||||
this.active = val === "search"
|
||||
show(val, old) {
|
||||
this.active = val === "search";
|
||||
|
||||
if (old === "search" && !this.active) {
|
||||
if (this.reload) {
|
||||
this.setReload(true)
|
||||
this.setReload(true);
|
||||
}
|
||||
|
||||
document.body.style.overflow = "auto"
|
||||
this.reset()
|
||||
this.value = ''
|
||||
this.active = false
|
||||
this.$refs.input.blur()
|
||||
document.body.style.overflow = "auto";
|
||||
this.reset();
|
||||
this.value = "";
|
||||
this.active = false;
|
||||
this.$refs.input.blur();
|
||||
} else if (this.active) {
|
||||
this.reload = false
|
||||
this.$refs.input.focus()
|
||||
document.body.style.overflow = "hidden"
|
||||
this.reload = false;
|
||||
this.$refs.input.focus();
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
},
|
||||
value () {
|
||||
value() {
|
||||
if (this.results.length) {
|
||||
this.reset()
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(["user", "show"]),
|
||||
...mapGetters(["isListing"]),
|
||||
boxes() {
|
||||
return boxes
|
||||
return boxes;
|
||||
},
|
||||
isEmpty() {
|
||||
return this.results.length === 0
|
||||
return this.results.length === 0;
|
||||
},
|
||||
text() {
|
||||
if (this.ongoing) {
|
||||
return ""
|
||||
return "";
|
||||
}
|
||||
|
||||
return this.value === '' ? this.$t("search.typeToSearch") : this.$t("search.pressToSearch")
|
||||
return this.value === ""
|
||||
? this.$t("search.typeToSearch")
|
||||
: this.$t("search.pressToSearch");
|
||||
},
|
||||
filteredResults() {
|
||||
return this.results.slice(0, this.resultsCount);
|
||||
},
|
||||
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
|
||||
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")
|
||||
this.showHover("search");
|
||||
},
|
||||
close(event) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
this.closeHovers()
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.closeHovers();
|
||||
},
|
||||
keyup(event) {
|
||||
if (event.keyCode === 27) {
|
||||
this.close(event)
|
||||
return
|
||||
this.close(event);
|
||||
return;
|
||||
}
|
||||
|
||||
this.results.length = 0
|
||||
this.results.length = 0;
|
||||
},
|
||||
init (string) {
|
||||
this.value = `${string} `
|
||||
this.$refs.input.focus()
|
||||
init(string) {
|
||||
this.value = `${string} `;
|
||||
this.$refs.input.focus();
|
||||
},
|
||||
reset () {
|
||||
this.ongoing = false
|
||||
this.resultsCount = 50
|
||||
this.results = []
|
||||
reset() {
|
||||
this.ongoing = false;
|
||||
this.resultsCount = 50;
|
||||
this.results = [];
|
||||
},
|
||||
async submit(event) {
|
||||
event.preventDefault()
|
||||
event.preventDefault();
|
||||
|
||||
if (this.value === '') {
|
||||
return
|
||||
if (this.value === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
let path = this.$route.path
|
||||
let path = this.$route.path;
|
||||
if (!this.isListing) {
|
||||
path = url.removeLastDir(path) + "/"
|
||||
path = url.removeLastDir(path) + "/";
|
||||
}
|
||||
|
||||
this.ongoing = true
|
||||
this.ongoing = true;
|
||||
|
||||
try {
|
||||
this.results = await search(path, this.value)
|
||||
this.results = await search(path, this.value);
|
||||
} catch (error) {
|
||||
this.$showError(error)
|
||||
this.$showError(error);
|
||||
}
|
||||
|
||||
this.ongoing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
this.ongoing = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
<template>
|
||||
<div @click="focus" class="shell" ref="scrollable" :class="{ ['shell--hidden']: !showShell}">
|
||||
<div v-for="(c, index) in content" :key="index" class="shell__result" >
|
||||
<div class="shell__prompt"><i class="material-icons">chevron_right</i></div>
|
||||
<div
|
||||
@click="focus"
|
||||
class="shell"
|
||||
ref="scrollable"
|
||||
:class="{ ['shell--hidden']: !showShell }"
|
||||
>
|
||||
<div v-for="(c, index) in content" :key="index" class="shell__result">
|
||||
<div class="shell__prompt">
|
||||
<i class="material-icons">chevron_right</i>
|
||||
</div>
|
||||
<pre class="shell__text">{{ c.text }}</pre>
|
||||
</div>
|
||||
|
||||
<div class="shell__result" :class="{ 'shell__result--hidden': !canInput }" >
|
||||
<div class="shell__prompt"><i class="material-icons">chevron_right</i></div>
|
||||
<div class="shell__result" :class="{ 'shell__result--hidden': !canInput }">
|
||||
<div class="shell__prompt">
|
||||
<i class="material-icons">chevron_right</i>
|
||||
</div>
|
||||
<pre
|
||||
tabindex="0"
|
||||
ref="input"
|
||||
@@ -14,102 +23,103 @@
|
||||
contenteditable="true"
|
||||
@keydown.prevent.38="historyUp"
|
||||
@keydown.prevent.40="historyDown"
|
||||
@keypress.prevent.enter="submit" />
|
||||
@keypress.prevent.enter="submit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapMutations, mapState, mapGetters } from 'vuex'
|
||||
import { commands } from '@/api'
|
||||
import { mapMutations, mapState, mapGetters } from "vuex";
|
||||
import { commands } from "@/api";
|
||||
|
||||
export default {
|
||||
name: 'shell',
|
||||
name: "shell",
|
||||
computed: {
|
||||
...mapState([ 'user', 'showShell' ]),
|
||||
...mapGetters([ 'isFiles', 'isLogged' ]),
|
||||
...mapState(["user", "showShell"]),
|
||||
...mapGetters(["isFiles", "isLogged"]),
|
||||
path: function () {
|
||||
if (this.isFiles) {
|
||||
return this.$route.path
|
||||
return this.$route.path;
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
return "";
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
content: [],
|
||||
history: [],
|
||||
historyPos: 0,
|
||||
canInput: true
|
||||
canInput: true,
|
||||
}),
|
||||
methods: {
|
||||
...mapMutations([ 'toggleShell' ]),
|
||||
...mapMutations(["toggleShell"]),
|
||||
scroll: function () {
|
||||
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight
|
||||
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight;
|
||||
},
|
||||
focus: function () {
|
||||
this.$refs.input.focus()
|
||||
this.$refs.input.focus();
|
||||
},
|
||||
historyUp () {
|
||||
historyUp() {
|
||||
if (this.historyPos > 0) {
|
||||
this.$refs.input.innerText = this.history[--this.historyPos]
|
||||
this.focus()
|
||||
this.$refs.input.innerText = this.history[--this.historyPos];
|
||||
this.focus();
|
||||
}
|
||||
},
|
||||
historyDown () {
|
||||
historyDown() {
|
||||
if (this.historyPos >= 0 && this.historyPos < this.history.length - 1) {
|
||||
this.$refs.input.innerText = this.history[++this.historyPos]
|
||||
this.focus()
|
||||
this.$refs.input.innerText = this.history[++this.historyPos];
|
||||
this.focus();
|
||||
} else {
|
||||
this.historyPos = this.history.length
|
||||
this.$refs.input.innerText = ''
|
||||
this.historyPos = this.history.length;
|
||||
this.$refs.input.innerText = "";
|
||||
}
|
||||
},
|
||||
submit: function (event) {
|
||||
const cmd = event.target.innerText.trim()
|
||||
const cmd = event.target.innerText.trim();
|
||||
|
||||
if (cmd === '') {
|
||||
return
|
||||
if (cmd === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd === 'clear') {
|
||||
this.content = []
|
||||
event.target.innerHTML = ''
|
||||
return
|
||||
if (cmd === "clear") {
|
||||
this.content = [];
|
||||
event.target.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd === 'exit') {
|
||||
event.target.innerHTML = ''
|
||||
this.toggleShell()
|
||||
return
|
||||
if (cmd === "exit") {
|
||||
event.target.innerHTML = "";
|
||||
this.toggleShell();
|
||||
return;
|
||||
}
|
||||
|
||||
this.canInput = false
|
||||
event.target.innerHTML = ''
|
||||
this.canInput = false;
|
||||
event.target.innerHTML = "";
|
||||
|
||||
let results = {
|
||||
text: `${cmd}\n\n`
|
||||
}
|
||||
|
||||
this.history.push(cmd)
|
||||
this.historyPos = this.history.length
|
||||
this.content.push(results)
|
||||
text: `${cmd}\n\n`,
|
||||
};
|
||||
|
||||
this.history.push(cmd);
|
||||
this.historyPos = this.history.length;
|
||||
this.content.push(results);
|
||||
|
||||
commands(
|
||||
this.path,
|
||||
cmd,
|
||||
event => {
|
||||
results.text += `${event.data}\n`
|
||||
this.scroll()
|
||||
(event) => {
|
||||
results.text += `${event.data}\n`;
|
||||
this.scroll();
|
||||
},
|
||||
() => {
|
||||
results.text = results.text.trimEnd()
|
||||
this.canInput = true
|
||||
this.$refs.input.focus()
|
||||
this.scroll()
|
||||
results.text = results.text.trimEnd();
|
||||
this.canInput = true;
|
||||
this.$refs.input.focus();
|
||||
this.scroll();
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,82 +1,134 @@
|
||||
<template>
|
||||
<nav :class="{active}">
|
||||
<nav :class="{ active }">
|
||||
<template v-if="isLogged">
|
||||
<router-link class="action" to="/files/" :aria-label="$t('sidebar.myFiles')" :title="$t('sidebar.myFiles')">
|
||||
<router-link
|
||||
class="action"
|
||||
to="/files/"
|
||||
:aria-label="$t('sidebar.myFiles')"
|
||||
:title="$t('sidebar.myFiles')"
|
||||
>
|
||||
<i class="material-icons">folder</i>
|
||||
<span>{{ $t('sidebar.myFiles') }}</span>
|
||||
<span>{{ $t("sidebar.myFiles") }}</span>
|
||||
</router-link>
|
||||
|
||||
<div v-if="user.perm.create">
|
||||
<button @click="$store.commit('showHover', 'newDir')" class="action" :aria-label="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')">
|
||||
<button
|
||||
@click="$store.commit('showHover', 'newDir')"
|
||||
class="action"
|
||||
:aria-label="$t('sidebar.newFolder')"
|
||||
:title="$t('sidebar.newFolder')"
|
||||
>
|
||||
<i class="material-icons">create_new_folder</i>
|
||||
<span>{{ $t('sidebar.newFolder') }}</span>
|
||||
<span>{{ $t("sidebar.newFolder") }}</span>
|
||||
</button>
|
||||
|
||||
<button @click="$store.commit('showHover', 'newFile')" class="action" :aria-label="$t('sidebar.newFile')" :title="$t('sidebar.newFile')">
|
||||
<button
|
||||
@click="$store.commit('showHover', 'newFile')"
|
||||
class="action"
|
||||
:aria-label="$t('sidebar.newFile')"
|
||||
:title="$t('sidebar.newFile')"
|
||||
>
|
||||
<i class="material-icons">note_add</i>
|
||||
<span>{{ $t('sidebar.newFile') }}</span>
|
||||
<span>{{ $t("sidebar.newFile") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<router-link class="action" to="/settings" :aria-label="$t('sidebar.settings')" :title="$t('sidebar.settings')">
|
||||
<router-link
|
||||
class="action"
|
||||
to="/settings"
|
||||
:aria-label="$t('sidebar.settings')"
|
||||
:title="$t('sidebar.settings')"
|
||||
>
|
||||
<i class="material-icons">settings_applications</i>
|
||||
<span>{{ $t('sidebar.settings') }}</span>
|
||||
<span>{{ $t("sidebar.settings") }}</span>
|
||||
</router-link>
|
||||
|
||||
<button v-if="authMethod == 'json'" @click="logout" class="action" id="logout" :aria-label="$t('sidebar.logout')" :title="$t('sidebar.logout')">
|
||||
<button
|
||||
v-if="authMethod == 'json'"
|
||||
@click="logout"
|
||||
class="action"
|
||||
id="logout"
|
||||
:aria-label="$t('sidebar.logout')"
|
||||
:title="$t('sidebar.logout')"
|
||||
>
|
||||
<i class="material-icons">exit_to_app</i>
|
||||
<span>{{ $t('sidebar.logout') }}</span>
|
||||
<span>{{ $t("sidebar.logout") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<router-link class="action" to="/login" :aria-label="$t('sidebar.login')" :title="$t('sidebar.login')">
|
||||
<router-link
|
||||
class="action"
|
||||
to="/login"
|
||||
:aria-label="$t('sidebar.login')"
|
||||
:title="$t('sidebar.login')"
|
||||
>
|
||||
<i class="material-icons">exit_to_app</i>
|
||||
<span>{{ $t('sidebar.login') }}</span>
|
||||
<span>{{ $t("sidebar.login") }}</span>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="signup" class="action" to="/login" :aria-label="$t('sidebar.signup')" :title="$t('sidebar.signup')">
|
||||
<router-link
|
||||
v-if="signup"
|
||||
class="action"
|
||||
to="/login"
|
||||
:aria-label="$t('sidebar.signup')"
|
||||
:title="$t('sidebar.signup')"
|
||||
>
|
||||
<i class="material-icons">person_add</i>
|
||||
<span>{{ $t('sidebar.signup') }}</span>
|
||||
<span>{{ $t("sidebar.signup") }}</span>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<p class="credits">
|
||||
<span>
|
||||
<span v-if="disableExternal">File Browser</span>
|
||||
<a v-else rel="noopener noreferrer" target="_blank" href="https://github.com/filebrowser/filebrowser">File Browser</a>
|
||||
<a
|
||||
v-else
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
href="https://github.com/filebrowser/filebrowser"
|
||||
>File Browser</a
|
||||
>
|
||||
<span> {{ version }}</span>
|
||||
</span>
|
||||
<span><a @click="help">{{ $t('sidebar.help') }}</a></span>
|
||||
<span
|
||||
><a @click="help">{{ $t("sidebar.help") }}</a></span
|
||||
>
|
||||
</p>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from 'vuex'
|
||||
import * as auth from '@/utils/auth'
|
||||
import { version, signup, disableExternal, noAuth, authMethod } from '@/utils/constants'
|
||||
import { mapState, mapGetters } from "vuex";
|
||||
import * as auth from "@/utils/auth";
|
||||
import {
|
||||
version,
|
||||
signup,
|
||||
disableExternal,
|
||||
noAuth,
|
||||
authMethod,
|
||||
} from "@/utils/constants";
|
||||
|
||||
export default {
|
||||
name: 'sidebar',
|
||||
name: "sidebar",
|
||||
computed: {
|
||||
...mapState([ 'user' ]),
|
||||
...mapGetters([ 'isLogged' ]),
|
||||
active () {
|
||||
return this.$store.state.show === 'sidebar'
|
||||
...mapState(["user"]),
|
||||
...mapGetters(["isLogged"]),
|
||||
active() {
|
||||
return this.$store.state.show === "sidebar";
|
||||
},
|
||||
signup: () => signup,
|
||||
version: () => version,
|
||||
disableExternal: () => disableExternal,
|
||||
noAuth: () => noAuth,
|
||||
authMethod: () => authMethod
|
||||
authMethod: () => authMethod,
|
||||
},
|
||||
methods: {
|
||||
help () {
|
||||
this.$store.commit('showHover', 'help')
|
||||
help() {
|
||||
this.$store.commit("showHover", "help");
|
||||
},
|
||||
logout: auth.logout
|
||||
}
|
||||
}
|
||||
logout: auth.logout,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<template>
|
||||
<button @click="show" :aria-label="$t('buttons.copy')" :title="$t('buttons.copy')" class="action" id="copy-button">
|
||||
<i class="material-icons">content_copy</i>
|
||||
<span>{{ $t('buttons.copyFile') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'copy-button',
|
||||
methods: {
|
||||
show: function () {
|
||||
this.$store.commit('showHover', 'copy')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,17 +0,0 @@
|
||||
<template>
|
||||
<button @click="show" :aria-label="$t('buttons.delete')" :title="$t('buttons.delete')" class="action" id="delete-button">
|
||||
<i class="material-icons">delete</i>
|
||||
<span>{{ $t('buttons.delete') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'delete-button',
|
||||
methods: {
|
||||
show: function () {
|
||||
this.$store.commit('showHover', 'delete')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,35 +0,0 @@
|
||||
<template>
|
||||
<button @click="download" :aria-label="$t('buttons.download')" :title="$t('buttons.download')" id="download-button" class="action">
|
||||
<i class="material-icons">file_download</i>
|
||||
<span>{{ $t('buttons.download') }}</span>
|
||||
<span v-if="selectedCount > 0" class="counter">{{ selectedCount }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapGetters, mapState} from 'vuex'
|
||||
import { files as api } from '@/api'
|
||||
|
||||
export default {
|
||||
name: 'download-button',
|
||||
computed: {
|
||||
...mapState(['req', 'selected']),
|
||||
...mapGetters(['isListing', 'selectedCount'])
|
||||
},
|
||||
methods: {
|
||||
download: function () {
|
||||
if (!this.isListing) {
|
||||
api.download(null, this.$route.path)
|
||||
return
|
||||
}
|
||||
|
||||
if (this.selectedCount === 1 && !this.req.items[this.selected[0]].isDir) {
|
||||
api.download(null, this.req.items[this.selected[0]].url)
|
||||
return
|
||||
}
|
||||
|
||||
this.$store.commit('showHover', 'download')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,17 +0,0 @@
|
||||
<template>
|
||||
<button :title="$t('buttons.info')" :aria-label="$t('buttons.info')" class="action" @click="show">
|
||||
<i class="material-icons">info</i>
|
||||
<span>{{ $t('buttons.info') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'info-button',
|
||||
methods: {
|
||||
show: function () {
|
||||
this.$store.commit('showHover', 'info')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,17 +0,0 @@
|
||||
<template>
|
||||
<button @click="show" :aria-label="$t('buttons.move')" :title="$t('buttons.move')" class="action" id="move-button">
|
||||
<i class="material-icons">forward</i>
|
||||
<span>{{ $t('buttons.moveFile') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'move-button',
|
||||
methods: {
|
||||
show: function () {
|
||||
this.$store.commit('showHover', 'move')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,22 +0,0 @@
|
||||
<template>
|
||||
<button :title="$t('buttons.info')" :aria-label="$t('buttons.info')" class="action" @click="$emit('change-size')">
|
||||
<i class="material-icons">{{ this.icon }}</i>
|
||||
<span>{{ $t('buttons.info') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'preview-size-button',
|
||||
props: [ 'size' ],
|
||||
computed: {
|
||||
icon () {
|
||||
if (this.size) {
|
||||
return 'photo_size_select_large'
|
||||
}
|
||||
|
||||
return 'hd'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,17 +0,0 @@
|
||||
<template>
|
||||
<button @click="show" :aria-label="$t('buttons.rename')" :title="$t('buttons.rename')" class="action" id="rename-button">
|
||||
<i class="material-icons">mode_edit</i>
|
||||
<span>{{ $t('buttons.rename') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'rename-button',
|
||||
methods: {
|
||||
show: function () {
|
||||
this.$store.commit('showHover', 'rename')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,17 +0,0 @@
|
||||
<template>
|
||||
<button @click="show" :aria-label="$t('buttons.share')" :title="$t('buttons.share')" class="action">
|
||||
<i class="material-icons">share</i>
|
||||
<span>{{ $t('buttons.share') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'share-button',
|
||||
methods: {
|
||||
show () {
|
||||
this.$store.commit('showHover', 'share')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,17 +0,0 @@
|
||||
<template>
|
||||
<button @click="show" :aria-label="$t('buttons.shell')" :title="$t('buttons.shell')" class="action">
|
||||
<i class="material-icons">code</i>
|
||||
<span>{{ $t('buttons.shell') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'shell-button',
|
||||
methods: {
|
||||
show: function () {
|
||||
this.$store.commit('toggleShell')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,40 +0,0 @@
|
||||
<template>
|
||||
<button @click="change" :aria-label="$t('buttons.switchView')" :title="$t('buttons.switchView')" class="action" id="switch-view-button">
|
||||
<i class="material-icons">{{ icon }}</i>
|
||||
<span>{{ $t('buttons.switchView') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapMutations } from 'vuex'
|
||||
import { users as api } from '@/api'
|
||||
|
||||
export default {
|
||||
name: 'switch-button',
|
||||
computed: {
|
||||
...mapState(['user']),
|
||||
icon: function () {
|
||||
if (this.user.viewMode === 'mosaic') return 'view_list'
|
||||
return 'view_module'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([ 'updateUser', 'closeHovers' ]),
|
||||
change: async function () {
|
||||
this.closeHovers()
|
||||
|
||||
const data = {
|
||||
id: this.user.id,
|
||||
viewMode: (this.icon === 'view_list') ? 'list' : 'mosaic'
|
||||
}
|
||||
|
||||
try {
|
||||
await api.update(data, ['viewMode'])
|
||||
this.updateUser(data)
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,21 +0,0 @@
|
||||
<template>
|
||||
<button @click="upload" :aria-label="$t('buttons.upload')" :title="$t('buttons.upload')" class="action" id="upload-button">
|
||||
<i class="material-icons">file_upload</i>
|
||||
<span>{{ $t('buttons.upload') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'upload-button',
|
||||
methods: {
|
||||
upload: function () {
|
||||
if (typeof(DataTransferItem.prototype.webkitGetAsEntry) !== 'undefined') {
|
||||
this.$store.commit('showHover', 'upload')
|
||||
} else {
|
||||
document.getElementById('upload-input').click();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,132 +0,0 @@
|
||||
<template>
|
||||
<div id="editor-container">
|
||||
<div class="bar">
|
||||
<button @click="back" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close" class="action">
|
||||
<i class="material-icons">close</i>
|
||||
</button>
|
||||
|
||||
<div class="title">
|
||||
<span>{{ req.name }}</span>
|
||||
</div>
|
||||
|
||||
<button @click="save" v-show="user.perm.modify" :aria-label="$t('buttons.save')" :title="$t('buttons.save')" id="save-button" class="action">
|
||||
<i class="material-icons">save</i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="breadcrumbs">
|
||||
<span><i class="material-icons">home</i></span>
|
||||
|
||||
<span v-for="(link, index) in breadcrumbs" :key="index">
|
||||
<span class="chevron"><i class="material-icons">keyboard_arrow_right</i></span>
|
||||
<span>{{ link.name }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<form id="editor"></form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import { files as api } from '@/api'
|
||||
import buttons from '@/utils/buttons'
|
||||
import url from '@/utils/url'
|
||||
|
||||
import ace from 'ace-builds/src-min-noconflict/ace.js'
|
||||
import modelist from 'ace-builds/src-min-noconflict/ext-modelist.js'
|
||||
import 'ace-builds/webpack-resolver'
|
||||
import { theme } from '@/utils/constants'
|
||||
|
||||
export default {
|
||||
name: 'editor',
|
||||
data: function () {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['req', 'user']),
|
||||
breadcrumbs () {
|
||||
let parts = this.$route.path.split('/')
|
||||
|
||||
if (parts[0] === '') {
|
||||
parts.shift()
|
||||
}
|
||||
|
||||
if (parts[parts.length - 1] === '') {
|
||||
parts.pop()
|
||||
}
|
||||
|
||||
let breadcrumbs = []
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
breadcrumbs.push({ name: decodeURIComponent(parts[i]) })
|
||||
}
|
||||
|
||||
breadcrumbs.shift()
|
||||
|
||||
if (breadcrumbs.length > 3) {
|
||||
while (breadcrumbs.length !== 4) {
|
||||
breadcrumbs.shift()
|
||||
}
|
||||
|
||||
breadcrumbs[0].name = '...'
|
||||
}
|
||||
|
||||
return breadcrumbs
|
||||
}
|
||||
},
|
||||
created () {
|
||||
window.addEventListener('keydown', this.keyEvent)
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.keyEvent)
|
||||
this.editor.destroy();
|
||||
},
|
||||
mounted: function () {
|
||||
const fileContent = this.req.content || '';
|
||||
|
||||
this.editor = ace.edit('editor', {
|
||||
value: fileContent,
|
||||
showPrintMargin: false,
|
||||
readOnly: this.req.type === 'textImmutable',
|
||||
theme: 'ace/theme/chrome',
|
||||
mode: modelist.getModeForPath(this.req.name).mode,
|
||||
wrap: true
|
||||
})
|
||||
|
||||
if (theme == 'dark') {
|
||||
this.editor.setTheme("ace/theme/twilight");
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
back () {
|
||||
let uri = url.removeLastDir(this.$route.path) + '/'
|
||||
this.$router.push({ path: uri })
|
||||
},
|
||||
keyEvent (event) {
|
||||
if (!event.ctrlKey && !event.metaKey) {
|
||||
return
|
||||
}
|
||||
|
||||
if (String.fromCharCode(event.which).toLowerCase() !== 's') {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
this.save()
|
||||
},
|
||||
async save () {
|
||||
const button = 'save'
|
||||
buttons.loading('save')
|
||||
|
||||
try {
|
||||
await api.put(this.$route.path, this.editor.getValue())
|
||||
buttons.success(button)
|
||||
} catch (e) {
|
||||
buttons.done(button)
|
||||
this.$showError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -10,39 +10,45 @@
|
||||
@mouseup="mouseUp"
|
||||
@wheel="wheelMove"
|
||||
>
|
||||
<img :src="src" class="image-ex-img image-ex-img-center" ref="imgex" @load="onLoad">
|
||||
<img
|
||||
src=""
|
||||
class="image-ex-img image-ex-img-center"
|
||||
ref="imgex"
|
||||
@load="onLoad"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import throttle from 'lodash.throttle'
|
||||
import throttle from "lodash.throttle";
|
||||
import UTIF from "utif";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
src: String,
|
||||
moveDisabledTime: {
|
||||
type: Number,
|
||||
default: () => 200
|
||||
default: () => 200,
|
||||
},
|
||||
maxScale: {
|
||||
type: Number,
|
||||
default: () => 4
|
||||
default: () => 4,
|
||||
},
|
||||
minScale: {
|
||||
type: Number,
|
||||
default: () => 0.25
|
||||
default: () => 0.25,
|
||||
},
|
||||
classList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
default: () => [],
|
||||
},
|
||||
zoomStep: {
|
||||
type: Number,
|
||||
default: () => 0.25
|
||||
default: () => 0.25,
|
||||
},
|
||||
autofill: {
|
||||
type: Boolean,
|
||||
default: () => false
|
||||
}
|
||||
default: () => false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -56,177 +62,208 @@ export default {
|
||||
imageLoaded: false,
|
||||
position: {
|
||||
center: { x: 0, y: 0 },
|
||||
relative: { x: 0, y: 0 }
|
||||
}
|
||||
}
|
||||
relative: { x: 0, y: 0 },
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
let container = this.$refs.container
|
||||
this.classList.forEach(className => container.classList.add(className))
|
||||
if (!this.decodeUTIF()) {
|
||||
this.$refs.imgex.src = this.src;
|
||||
}
|
||||
let container = this.$refs.container;
|
||||
this.classList.forEach((className) => container.classList.add(className));
|
||||
// set width and height if they are zero
|
||||
if (getComputedStyle(container).width === "0px") {
|
||||
container.style.width = "100%"
|
||||
container.style.width = "100%";
|
||||
}
|
||||
if (getComputedStyle(container).height === "0px") {
|
||||
container.style.height = "100%"
|
||||
container.style.height = "100%";
|
||||
}
|
||||
|
||||
window.addEventListener('resize', this.onResize)
|
||||
window.addEventListener("resize", this.onResize);
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('resize', this.onResize)
|
||||
document.removeEventListener('mouseup', this.onMouseUp)
|
||||
beforeDestroy() {
|
||||
window.removeEventListener("resize", this.onResize);
|
||||
document.removeEventListener("mouseup", this.onMouseUp);
|
||||
},
|
||||
watch: {
|
||||
src: function () {
|
||||
this.scale = 1
|
||||
this.setZoom()
|
||||
this.setCenter()
|
||||
}
|
||||
this.scale = 1;
|
||||
this.setZoom();
|
||||
this.setCenter();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// Modified from UTIF.replaceIMG
|
||||
decodeUTIF() {
|
||||
const sufs = ["tif", "tiff", "dng", "cr2", "nef"];
|
||||
let suff = document.location.pathname.split(".").pop().toLowerCase();
|
||||
if (sufs.indexOf(suff) == -1) return false;
|
||||
let xhr = new XMLHttpRequest();
|
||||
UTIF._xhrs.push(xhr);
|
||||
UTIF._imgs.push(this.$refs.imgex);
|
||||
xhr.open("GET", this.src);
|
||||
xhr.responseType = "arraybuffer";
|
||||
xhr.onload = UTIF._imgLoaded;
|
||||
xhr.send();
|
||||
return true;
|
||||
},
|
||||
onLoad() {
|
||||
let img = this.$refs.imgex
|
||||
let img = this.$refs.imgex;
|
||||
|
||||
this.imageLoaded = true
|
||||
this.imageLoaded = true;
|
||||
|
||||
if (img === undefined) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
img.classList.remove('image-ex-img-center')
|
||||
this.setCenter()
|
||||
img.classList.add('image-ex-img-ready')
|
||||
img.classList.remove("image-ex-img-center");
|
||||
this.setCenter();
|
||||
img.classList.add("image-ex-img-ready");
|
||||
|
||||
document.addEventListener('mouseup', this.onMouseUp)
|
||||
document.addEventListener("mouseup", this.onMouseUp);
|
||||
},
|
||||
onMouseUp() {
|
||||
this.inDrag = false
|
||||
this.inDrag = false;
|
||||
},
|
||||
onResize: throttle(function() {
|
||||
onResize: throttle(function () {
|
||||
if (this.imageLoaded) {
|
||||
this.setCenter()
|
||||
this.doMove(this.position.relative.x, this.position.relative.y)
|
||||
this.setCenter();
|
||||
this.doMove(this.position.relative.x, this.position.relative.y);
|
||||
}
|
||||
}, 100),
|
||||
setCenter() {
|
||||
let container = this.$refs.container
|
||||
let img = this.$refs.imgex
|
||||
let container = this.$refs.container;
|
||||
let img = this.$refs.imgex;
|
||||
|
||||
this.position.center.x = Math.floor((container.clientWidth - img.clientWidth) / 2)
|
||||
this.position.center.y = Math.floor((container.clientHeight - img.clientHeight) / 2)
|
||||
this.position.center.x = Math.floor(
|
||||
(container.clientWidth - img.clientWidth) / 2
|
||||
);
|
||||
this.position.center.y = Math.floor(
|
||||
(container.clientHeight - img.clientHeight) / 2
|
||||
);
|
||||
|
||||
img.style.left = this.position.center.x + 'px'
|
||||
img.style.top = this.position.center.y + 'px'
|
||||
img.style.left = this.position.center.x + "px";
|
||||
img.style.top = this.position.center.y + "px";
|
||||
},
|
||||
mousedownStart(event) {
|
||||
this.lastX = null
|
||||
this.lastY = null
|
||||
this.inDrag = true
|
||||
event.preventDefault()
|
||||
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()
|
||||
if (!this.inDrag) return;
|
||||
this.doMove(event.movementX, event.movementY);
|
||||
event.preventDefault();
|
||||
},
|
||||
mouseUp(event) {
|
||||
this.inDrag = false
|
||||
event.preventDefault()
|
||||
this.inDrag = false;
|
||||
event.preventDefault();
|
||||
},
|
||||
touchStart(event) {
|
||||
this.lastX = null
|
||||
this.lastY = null
|
||||
this.lastTouchDistance = null
|
||||
event.preventDefault()
|
||||
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
|
||||
this.scale = 2;
|
||||
break;
|
||||
case 2:
|
||||
this.scale = 4
|
||||
break
|
||||
this.scale = 4;
|
||||
break;
|
||||
default:
|
||||
case 4:
|
||||
this.scale = 1
|
||||
break
|
||||
this.scale = 1;
|
||||
this.setCenter();
|
||||
break;
|
||||
}
|
||||
this.setZoom()
|
||||
event.preventDefault()
|
||||
this.setZoom();
|
||||
event.preventDefault();
|
||||
},
|
||||
touchMove(event) {
|
||||
event.preventDefault()
|
||||
event.preventDefault();
|
||||
if (this.lastX === null) {
|
||||
this.lastX = event.targetTouches[0].pageX
|
||||
this.lastY = event.targetTouches[0].pageY
|
||||
return
|
||||
this.lastX = event.targetTouches[0].pageX;
|
||||
this.lastY = event.targetTouches[0].pageY;
|
||||
return;
|
||||
}
|
||||
let step = this.$refs.imgex.width / 5
|
||||
let step = this.$refs.imgex.width / 5;
|
||||
if (event.targetTouches.length === 2) {
|
||||
this.moveDisabled = true
|
||||
clearTimeout(this.disabledTimer)
|
||||
this.moveDisabled = true;
|
||||
clearTimeout(this.disabledTimer);
|
||||
this.disabledTimer = setTimeout(
|
||||
() => (this.moveDisabled = false),
|
||||
this.moveDisabledTime
|
||||
)
|
||||
);
|
||||
|
||||
let p1 = event.targetTouches[0]
|
||||
let p2 = event.targetTouches[1]
|
||||
let p1 = event.targetTouches[0];
|
||||
let p2 = event.targetTouches[1];
|
||||
let touchDistance = Math.sqrt(
|
||||
Math.pow(p2.pageX - p1.pageX, 2) + Math.pow(p2.pageY - p1.pageY, 2)
|
||||
)
|
||||
);
|
||||
if (!this.lastTouchDistance) {
|
||||
this.lastTouchDistance = touchDistance
|
||||
return
|
||||
this.lastTouchDistance = touchDistance;
|
||||
return;
|
||||
}
|
||||
this.scale += (touchDistance - this.lastTouchDistance) / step
|
||||
this.lastTouchDistance = touchDistance
|
||||
this.setZoom()
|
||||
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)
|
||||
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
|
||||
let style = this.$refs.imgex.style;
|
||||
let posX = this.pxStringToNumber(style.left) + x;
|
||||
let posY = this.pxStringToNumber(style.top) + y;
|
||||
|
||||
style.left = posX + 'px'
|
||||
style.top = posY + 'px'
|
||||
style.left = posX + "px";
|
||||
style.top = posY + "px";
|
||||
|
||||
this.position.relative.x = Math.abs(this.position.center.x - posX)
|
||||
this.position.relative.y = Math.abs(this.position.center.y - posY)
|
||||
this.position.relative.x = Math.abs(this.position.center.x - posX);
|
||||
this.position.relative.y = Math.abs(this.position.center.y - posY);
|
||||
|
||||
if (posX < this.position.center.x) {
|
||||
this.position.relative.x = this.position.relative.x * -1
|
||||
this.position.relative.x = this.position.relative.x * -1;
|
||||
}
|
||||
|
||||
if (posY < this.position.center.y) {
|
||||
this.position.relative.y = this.position.relative.y * -1
|
||||
this.position.relative.y = this.position.relative.y * -1;
|
||||
}
|
||||
},
|
||||
wheelMove(event) {
|
||||
this.scale += (event.wheelDeltaY / 100) * this.zoomStep
|
||||
this.setZoom()
|
||||
this.scale += (event.wheelDeltaY / 100) * this.zoomStep;
|
||||
this.setZoom();
|
||||
},
|
||||
setZoom() {
|
||||
this.scale = this.scale < this.minScale ? this.minScale : this.scale
|
||||
this.scale = this.scale > this.maxScale ? this.maxScale : this.scale
|
||||
this.$refs.imgex.style.transform = `scale(${this.scale})`
|
||||
this.scale = this.scale < this.minScale ? this.minScale : this.scale;
|
||||
this.scale = this.scale > this.maxScale ? this.maxScale : this.scale;
|
||||
this.$refs.imgex.style.transform = `scale(${this.scale})`;
|
||||
},
|
||||
pxStringToNumber(style) {
|
||||
return +style.replace("px", "")
|
||||
}
|
||||
}
|
||||
}
|
||||
return +style.replace("px", "");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.image-ex-container {
|
||||
|
||||
@@ -1,464 +0,0 @@
|
||||
<template>
|
||||
<div v-if="(req.numDirs + req.numFiles) == 0">
|
||||
<h2 class="message">
|
||||
<i class="material-icons">sentiment_dissatisfied</i>
|
||||
<span>{{ $t('files.lonely') }}</span>
|
||||
</h2>
|
||||
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
|
||||
<input style="display:none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory multiple>
|
||||
</div>
|
||||
<div v-else id="listing"
|
||||
:class="user.viewMode">
|
||||
<div>
|
||||
<div class="item header">
|
||||
<div></div>
|
||||
<div>
|
||||
<p :class="{ active: nameSorted }" class="name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="sort('name')"
|
||||
:title="$t('files.sortByName')"
|
||||
:aria-label="$t('files.sortByName')">
|
||||
<span>{{ $t('files.name') }}</span>
|
||||
<i class="material-icons">{{ nameIcon }}</i>
|
||||
</p>
|
||||
|
||||
<p :class="{ active: sizeSorted }" class="size"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="sort('size')"
|
||||
:title="$t('files.sortBySize')"
|
||||
:aria-label="$t('files.sortBySize')">
|
||||
<span>{{ $t('files.size') }}</span>
|
||||
<i class="material-icons">{{ sizeIcon }}</i>
|
||||
</p>
|
||||
<p :class="{ active: modifiedSorted }" class="modified"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="sort('modified')"
|
||||
:title="$t('files.sortByLastModified')"
|
||||
:aria-label="$t('files.sortByLastModified')">
|
||||
<span>{{ $t('files.lastModified') }}</span>
|
||||
<i class="material-icons">{{ modifiedIcon }}</i>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 v-if="req.numDirs > 0">{{ $t('files.folders') }}</h2>
|
||||
<div v-if="req.numDirs > 0">
|
||||
<item v-for="(item) in dirs"
|
||||
:key="base64(item.name)"
|
||||
v-bind:index="item.index"
|
||||
v-bind:name="item.name"
|
||||
v-bind:isDir="item.isDir"
|
||||
v-bind:url="item.url"
|
||||
v-bind:modified="item.modified"
|
||||
v-bind:type="item.type"
|
||||
v-bind:size="item.size">
|
||||
</item>
|
||||
</div>
|
||||
|
||||
<h2 v-if="req.numFiles > 0">{{ $t('files.files') }}</h2>
|
||||
<div v-if="req.numFiles > 0">
|
||||
<item v-for="(item) in files"
|
||||
:key="base64(item.name)"
|
||||
v-bind:index="item.index"
|
||||
v-bind:name="item.name"
|
||||
v-bind:isDir="item.isDir"
|
||||
v-bind:url="item.url"
|
||||
v-bind:modified="item.modified"
|
||||
v-bind:type="item.type"
|
||||
v-bind:size="item.size">
|
||||
</item>
|
||||
</div>
|
||||
|
||||
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
|
||||
<input style="display:none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory multiple>
|
||||
|
||||
<div :class="{ active: $store.state.multiple }" id="multiple-selection">
|
||||
<p>{{ $t('files.multipleSelectionEnabled') }}</p>
|
||||
<div @click="$store.commit('multiple', false)" tabindex="0" role="button" :title="$t('files.clear')" :aria-label="$t('files.clear')" class="action">
|
||||
<i class="material-icons">clear</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapMutations } from 'vuex'
|
||||
import Item from './ListingItem'
|
||||
import css from '@/utils/css'
|
||||
import { users, files as api } from '@/api'
|
||||
import * as upload from '@/utils/upload'
|
||||
|
||||
export default {
|
||||
name: 'listing',
|
||||
components: { Item },
|
||||
data: function () {
|
||||
return {
|
||||
showLimit: 50,
|
||||
dragCounter: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['req', 'selected', 'user', 'show']),
|
||||
nameSorted () {
|
||||
return (this.req.sorting.by === 'name')
|
||||
},
|
||||
sizeSorted () {
|
||||
return (this.req.sorting.by === 'size')
|
||||
},
|
||||
modifiedSorted () {
|
||||
return (this.req.sorting.by === 'modified')
|
||||
},
|
||||
ascOrdered () {
|
||||
return this.req.sorting.asc
|
||||
},
|
||||
items () {
|
||||
const dirs = []
|
||||
const files = []
|
||||
|
||||
this.req.items.forEach((item) => {
|
||||
if (item.isDir) {
|
||||
dirs.push(item)
|
||||
} else {
|
||||
files.push(item)
|
||||
}
|
||||
})
|
||||
|
||||
return { dirs, files }
|
||||
},
|
||||
dirs () {
|
||||
return this.items.dirs.slice(0, this.showLimit)
|
||||
},
|
||||
files () {
|
||||
let showLimit = this.showLimit - this.items.dirs.length
|
||||
|
||||
if (showLimit < 0) showLimit = 0
|
||||
|
||||
return this.items.files.slice(0, showLimit)
|
||||
},
|
||||
nameIcon () {
|
||||
if (this.nameSorted && !this.ascOrdered) {
|
||||
return 'arrow_upward'
|
||||
}
|
||||
|
||||
return 'arrow_downward'
|
||||
},
|
||||
sizeIcon () {
|
||||
if (this.sizeSorted && this.ascOrdered) {
|
||||
return 'arrow_downward'
|
||||
}
|
||||
|
||||
return 'arrow_upward'
|
||||
},
|
||||
modifiedIcon () {
|
||||
if (this.modifiedSorted && this.ascOrdered) {
|
||||
return 'arrow_downward'
|
||||
}
|
||||
|
||||
return 'arrow_upward'
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
// Check the columns size for the first time.
|
||||
this.resizeEvent()
|
||||
|
||||
// Add the needed event listeners to the window and document.
|
||||
window.addEventListener('keydown', this.keyEvent)
|
||||
window.addEventListener('resize', this.resizeEvent)
|
||||
window.addEventListener('scroll', this.scrollEvent)
|
||||
document.addEventListener('dragover', this.preventDefault)
|
||||
document.addEventListener('dragenter', this.dragEnter)
|
||||
document.addEventListener('dragleave', this.dragLeave)
|
||||
document.addEventListener('drop', this.drop)
|
||||
},
|
||||
beforeDestroy () {
|
||||
// Remove event listeners before destroying this page.
|
||||
window.removeEventListener('keydown', this.keyEvent)
|
||||
window.removeEventListener('resize', this.resizeEvent)
|
||||
window.removeEventListener('scroll', this.scrollEvent)
|
||||
document.removeEventListener('dragover', this.preventDefault)
|
||||
document.removeEventListener('dragenter', this.dragEnter)
|
||||
document.removeEventListener('dragleave', this.dragLeave)
|
||||
document.removeEventListener('drop', this.drop)
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([ 'updateUser', 'addSelected' ]),
|
||||
base64: function (name) {
|
||||
return window.btoa(unescape(encodeURIComponent(name)))
|
||||
},
|
||||
keyEvent (event) {
|
||||
if (this.show !== null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!event.ctrlKey && !event.metaKey) {
|
||||
return
|
||||
}
|
||||
|
||||
let key = String.fromCharCode(event.which).toLowerCase()
|
||||
|
||||
switch (key) {
|
||||
case 'f':
|
||||
event.preventDefault()
|
||||
this.$store.commit('showHover', 'search')
|
||||
break
|
||||
case 'c':
|
||||
case 'x':
|
||||
this.copyCut(event, key)
|
||||
break
|
||||
case 'v':
|
||||
this.paste(event)
|
||||
break
|
||||
case 'a':
|
||||
event.preventDefault()
|
||||
for (let file of this.items.files) {
|
||||
if (this.$store.state.selected.indexOf(file.index) === -1) {
|
||||
this.addSelected(file.index)
|
||||
}
|
||||
}
|
||||
for (let dir of this.items.dirs) {
|
||||
if (this.$store.state.selected.indexOf(dir.index) === -1) {
|
||||
this.addSelected(dir.index)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
},
|
||||
preventDefault (event) {
|
||||
// Wrapper around prevent default.
|
||||
event.preventDefault()
|
||||
},
|
||||
copyCut (event, key) {
|
||||
if (event.target.tagName.toLowerCase() === 'input') {
|
||||
return
|
||||
}
|
||||
|
||||
let items = []
|
||||
|
||||
for (let i of this.selected) {
|
||||
items.push({
|
||||
from: this.req.items[i].url,
|
||||
name: encodeURIComponent(this.req.items[i].name)
|
||||
})
|
||||
}
|
||||
|
||||
if (items.length == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$store.commit('updateClipboard', {
|
||||
key: key,
|
||||
items: items,
|
||||
path: this.$route.path
|
||||
})
|
||||
},
|
||||
paste (event) {
|
||||
if (event.target.tagName.toLowerCase() === 'input') {
|
||||
return
|
||||
}
|
||||
|
||||
let items = []
|
||||
|
||||
for (let item of this.$store.state.clipboard.items) {
|
||||
const from = item.from.endsWith('/') ? item.from.slice(0, -1) : item.from
|
||||
const to = this.$route.path + item.name
|
||||
items.push({ from, to, name: item.name })
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
let action = (overwrite, rename) => {
|
||||
api.copy(items, overwrite, rename).then(() => {
|
||||
this.$store.commit('setReload', true)
|
||||
}).catch(this.$showError)
|
||||
}
|
||||
|
||||
if (this.$store.state.clipboard.key === 'x') {
|
||||
action = (overwrite, rename) => {
|
||||
api.move(items, overwrite, rename).then(() => {
|
||||
this.$store.commit('resetClipboard')
|
||||
this.$store.commit('setReload', true)
|
||||
}).catch(this.$showError)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.$store.state.clipboard.path == this.$route.path) {
|
||||
action(false, true)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let conflict = upload.checkConflict(items, this.req.items)
|
||||
|
||||
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)
|
||||
},
|
||||
resizeEvent () {
|
||||
// Update the columns size based on the window width.
|
||||
let columns = Math.floor(document.querySelector('main').offsetWidth / 300)
|
||||
let items = css(['#listing.mosaic .item', '.mosaic#listing .item'])
|
||||
if (columns === 0) columns = 1
|
||||
items.style.width = `calc(${100 / columns}% - 1em)`
|
||||
},
|
||||
scrollEvent () {
|
||||
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
|
||||
this.showLimit += 50
|
||||
}
|
||||
},
|
||||
dragEnter () {
|
||||
this.dragCounter++
|
||||
|
||||
// When the user starts dragging an item, put every
|
||||
// file on the listing with 50% opacity.
|
||||
let items = document.getElementsByClassName('item')
|
||||
|
||||
Array.from(items).forEach(file => {
|
||||
file.style.opacity = 0.5
|
||||
})
|
||||
},
|
||||
dragLeave () {
|
||||
this.dragCounter--
|
||||
|
||||
if (this.dragCounter == 0) {
|
||||
this.resetOpacity()
|
||||
}
|
||||
},
|
||||
drop: async function (event) {
|
||||
event.preventDefault()
|
||||
this.dragCounter = 0
|
||||
this.resetOpacity()
|
||||
|
||||
let dt = event.dataTransfer
|
||||
let el = event.target
|
||||
|
||||
if (dt.files.length <= 0) return
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (el !== null && !el.classList.contains('item')) {
|
||||
el = el.parentElement
|
||||
}
|
||||
}
|
||||
|
||||
let base = ''
|
||||
if (el !== null && el.classList.contains('item') && el.dataset.dir === 'true') {
|
||||
base = el.querySelector('.name').innerHTML + '/'
|
||||
}
|
||||
|
||||
let files = await upload.scanFiles(dt)
|
||||
let path = this.$route.path.endsWith('/') ? this.$route.path + base : this.$route.path + '/' + base
|
||||
let items = this.req.items
|
||||
|
||||
if (base !== '') {
|
||||
try {
|
||||
items = (await api.fetch(path)).items
|
||||
} catch (error) {
|
||||
this.$showError(error)
|
||||
}
|
||||
}
|
||||
|
||||
let conflict = upload.checkConflict(files, items)
|
||||
|
||||
if (conflict) {
|
||||
this.$store.commit('showHover', {
|
||||
prompt: 'replace',
|
||||
confirm: (event) => {
|
||||
event.preventDefault()
|
||||
this.$store.commit('closeHovers')
|
||||
upload.handleFiles(files, path, true)
|
||||
}
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
upload.handleFiles(files, path)
|
||||
},
|
||||
uploadInput (event) {
|
||||
this.$store.commit('closeHovers')
|
||||
|
||||
let files = event.currentTarget.files
|
||||
let folder_upload = files[0].webkitRelativePath !== undefined && files[0].webkitRelativePath !== ''
|
||||
|
||||
if (folder_upload) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
let file = files[i]
|
||||
files[i].fullPath = file.webkitRelativePath
|
||||
}
|
||||
}
|
||||
|
||||
let path = this.$route.path.endsWith('/') ? this.$route.path : this.$route.path + '/'
|
||||
let conflict = upload.checkConflict(files, this.req.items)
|
||||
|
||||
if (conflict) {
|
||||
this.$store.commit('showHover', {
|
||||
prompt: 'replace',
|
||||
confirm: (event) => {
|
||||
event.preventDefault()
|
||||
this.$store.commit('closeHovers')
|
||||
upload.handleFiles(files, path, true)
|
||||
}
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
upload.handleFiles(files, path)
|
||||
},
|
||||
resetOpacity () {
|
||||
let items = document.getElementsByClassName('item')
|
||||
|
||||
Array.from(items).forEach(file => {
|
||||
file.style.opacity = 1
|
||||
})
|
||||
},
|
||||
async sort (by) {
|
||||
let asc = false
|
||||
|
||||
if (by === 'name') {
|
||||
if (this.nameIcon === 'arrow_upward') {
|
||||
asc = true
|
||||
}
|
||||
} else if (by === 'size') {
|
||||
if (this.sizeIcon === 'arrow_upward') {
|
||||
asc = true
|
||||
}
|
||||
} else if (by === 'modified') {
|
||||
if (this.modifiedIcon === 'arrow_upward') {
|
||||
asc = true
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await users.update({ id: this.user.id, sorting: { by, asc } }, ['sorting'])
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
}
|
||||
|
||||
this.$store.commit('setReload', true)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,19 +1,24 @@
|
||||
<template>
|
||||
<div class="item"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:draggable="isDraggable"
|
||||
@dragstart="dragStart"
|
||||
@dragover="dragOver"
|
||||
@drop="drop"
|
||||
@click="itemClick"
|
||||
@dblclick="dblclick"
|
||||
@touchstart="touchstart"
|
||||
:data-dir="isDir"
|
||||
:aria-label="name"
|
||||
:aria-selected="isSelected">
|
||||
<div
|
||||
class="item"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:draggable="isDraggable"
|
||||
@dragstart="dragStart"
|
||||
@dragover="dragOver"
|
||||
@drop="drop"
|
||||
@click="itemClick"
|
||||
@dblclick="dblclick"
|
||||
@touchstart="touchstart"
|
||||
:data-dir="isDir"
|
||||
:aria-label="name"
|
||||
:aria-selected="isSelected"
|
||||
>
|
||||
<div>
|
||||
<img v-if="type==='image' && isThumbsEnabled" v-lazy="thumbnailUrl">
|
||||
<img
|
||||
v-if="readOnly == undefined && type === 'image' && isThumbsEnabled"
|
||||
v-lazy="thumbnailUrl"
|
||||
/>
|
||||
<i v-else class="material-icons">{{ icon }}</i>
|
||||
</div>
|
||||
|
||||
@@ -31,196 +36,221 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { baseURL, enableThumbs } from '@/utils/constants'
|
||||
import { mapMutations, mapGetters, mapState } from 'vuex'
|
||||
import filesize from 'filesize'
|
||||
import moment from 'moment'
|
||||
import { files as api } from '@/api'
|
||||
import * as upload from '@/utils/upload'
|
||||
import { baseURL, enableThumbs } from "@/utils/constants";
|
||||
import { mapMutations, mapGetters, mapState } from "vuex";
|
||||
import filesize from "filesize";
|
||||
import moment from "moment";
|
||||
import { files as api } from "@/api";
|
||||
import * as upload from "@/utils/upload";
|
||||
|
||||
export default {
|
||||
name: 'item',
|
||||
name: "item",
|
||||
data: function () {
|
||||
return {
|
||||
touches: 0
|
||||
}
|
||||
touches: 0,
|
||||
};
|
||||
},
|
||||
props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index'],
|
||||
props: [
|
||||
"name",
|
||||
"isDir",
|
||||
"url",
|
||||
"type",
|
||||
"size",
|
||||
"modified",
|
||||
"index",
|
||||
"readOnly",
|
||||
],
|
||||
computed: {
|
||||
...mapState(['user', 'selected', 'req', 'user', 'jwt']),
|
||||
...mapGetters(['selectedCount']),
|
||||
isSelected () {
|
||||
return (this.selected.indexOf(this.index) !== -1)
|
||||
...mapState(["user", "selected", "req", "jwt"]),
|
||||
...mapGetters(["selectedCount"]),
|
||||
singleClick() {
|
||||
return this.readOnly == undefined && this.user.singleClick;
|
||||
},
|
||||
icon () {
|
||||
if (this.isDir) return 'folder'
|
||||
if (this.type === 'image') return 'insert_photo'
|
||||
if (this.type === 'audio') return 'volume_up'
|
||||
if (this.type === 'video') return 'movie'
|
||||
return 'insert_drive_file'
|
||||
isSelected() {
|
||||
return this.selected.indexOf(this.index) !== -1;
|
||||
},
|
||||
isDraggable () {
|
||||
return this.user.perm.rename
|
||||
icon() {
|
||||
if (this.isDir) return "folder";
|
||||
if (this.type === "image") return "insert_photo";
|
||||
if (this.type === "audio") return "volume_up";
|
||||
if (this.type === "video") return "movie";
|
||||
return "insert_drive_file";
|
||||
},
|
||||
canDrop () {
|
||||
if (!this.isDir) return false
|
||||
isDraggable() {
|
||||
return this.readOnly == undefined && this.user.perm.rename;
|
||||
},
|
||||
canDrop() {
|
||||
if (!this.isDir || this.readOnly !== undefined) return false;
|
||||
|
||||
for (let i of this.selected) {
|
||||
if (this.req.items[i].url === this.url) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
return true;
|
||||
},
|
||||
thumbnailUrl () {
|
||||
const path = this.url.replace(/^\/files\//, '')
|
||||
return `${baseURL}/api/preview/thumb/${path}?auth=${this.jwt}&inline=true`
|
||||
thumbnailUrl() {
|
||||
const path = this.url.replace(/^\/files\//, "");
|
||||
|
||||
// reload the image when the file is replaced
|
||||
const key = Date.parse(this.modified);
|
||||
|
||||
return `${baseURL}/api/preview/thumb/${path}?auth=${this.jwt}&inline=true&k=${key}`;
|
||||
},
|
||||
isThumbsEnabled() {
|
||||
return enableThumbs;
|
||||
},
|
||||
isThumbsEnabled () {
|
||||
return enableThumbs
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(['addSelected', 'removeSelected', 'resetSelected']),
|
||||
...mapMutations(["addSelected", "removeSelected", "resetSelected"]),
|
||||
humanSize: function () {
|
||||
return filesize(this.size)
|
||||
return filesize(this.size);
|
||||
},
|
||||
humanTime: function () {
|
||||
return moment(this.modified).fromNow()
|
||||
return moment(this.modified).fromNow();
|
||||
},
|
||||
dragStart: function () {
|
||||
if (this.selectedCount === 0) {
|
||||
this.addSelected(this.index)
|
||||
return
|
||||
this.addSelected(this.index);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isSelected) {
|
||||
this.resetSelected()
|
||||
this.addSelected(this.index)
|
||||
this.resetSelected();
|
||||
this.addSelected(this.index);
|
||||
}
|
||||
},
|
||||
dragOver: function (event) {
|
||||
if (!this.canDrop) return
|
||||
if (!this.canDrop) return;
|
||||
|
||||
event.preventDefault()
|
||||
let el = event.target
|
||||
event.preventDefault();
|
||||
let el = event.target;
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (!el.classList.contains('item')) {
|
||||
el = el.parentElement
|
||||
if (!el.classList.contains("item")) {
|
||||
el = el.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
el.style.opacity = 1
|
||||
el.style.opacity = 1;
|
||||
},
|
||||
drop: async function (event) {
|
||||
if (!this.canDrop) return
|
||||
event.preventDefault()
|
||||
if (!this.canDrop) return;
|
||||
event.preventDefault();
|
||||
|
||||
if (this.selectedCount === 0) return
|
||||
if (this.selectedCount === 0) return;
|
||||
|
||||
let el = event.target
|
||||
let el = event.target;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (el !== null && !el.classList.contains('item')) {
|
||||
el = el.parentElement
|
||||
if (el !== null && !el.classList.contains("item")) {
|
||||
el = el.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
let items = []
|
||||
let items = [];
|
||||
|
||||
for (let i of this.selected) {
|
||||
items.push({
|
||||
from: this.req.items[i].url,
|
||||
to: this.url + this.req.items[i].name,
|
||||
name: this.req.items[i].name
|
||||
})
|
||||
}
|
||||
name: this.req.items[i].name,
|
||||
});
|
||||
}
|
||||
|
||||
let base = el.querySelector('.name').innerHTML + '/'
|
||||
let path = this.$route.path + base
|
||||
let baseItems = (await api.fetch(path)).items
|
||||
let base = el.querySelector(".name").innerHTML + "/";
|
||||
let path = this.$route.path + base;
|
||||
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)
|
||||
}
|
||||
api
|
||||
.move(items, overwrite, rename)
|
||||
.then(() => {
|
||||
this.$store.commit("setReload", true);
|
||||
})
|
||||
.catch(this.$showError);
|
||||
};
|
||||
|
||||
let conflict = upload.checkConflict(items, baseItems)
|
||||
let conflict = upload.checkConflict(items, baseItems);
|
||||
|
||||
let overwrite = false
|
||||
let rename = false
|
||||
let overwrite = false;
|
||||
let rename = false;
|
||||
|
||||
if (conflict) {
|
||||
this.$store.commit('showHover', {
|
||||
prompt: 'replace-rename',
|
||||
this.$store.commit("showHover", {
|
||||
prompt: "replace-rename",
|
||||
confirm: (event, option) => {
|
||||
overwrite = option == 'overwrite'
|
||||
rename = option == 'rename'
|
||||
overwrite = option == "overwrite";
|
||||
rename = option == "rename";
|
||||
|
||||
event.preventDefault()
|
||||
this.$store.commit('closeHovers')
|
||||
action(overwrite, rename)
|
||||
}
|
||||
})
|
||||
event.preventDefault();
|
||||
this.$store.commit("closeHovers");
|
||||
action(overwrite, rename);
|
||||
},
|
||||
});
|
||||
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
action(overwrite, rename)
|
||||
action(overwrite, rename);
|
||||
},
|
||||
itemClick: function(event) {
|
||||
if (this.user.singleClick && !this.$store.state.multiple) this.open()
|
||||
else this.click(event)
|
||||
itemClick: function (event) {
|
||||
if (this.singleClick && !this.$store.state.multiple) this.open();
|
||||
else this.click(event);
|
||||
},
|
||||
click: function (event) {
|
||||
if (!this.user.singleClick && this.selectedCount !== 0) event.preventDefault()
|
||||
if (!this.singleClick && this.selectedCount !== 0) event.preventDefault();
|
||||
if (this.$store.state.selected.indexOf(this.index) !== -1) {
|
||||
this.removeSelected(this.index)
|
||||
return
|
||||
this.removeSelected(this.index);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.shiftKey && this.selected.length > 0) {
|
||||
let fi = 0
|
||||
let la = 0
|
||||
let fi = 0;
|
||||
let la = 0;
|
||||
|
||||
if (this.index > this.selected[0]) {
|
||||
fi = this.selected[0] + 1
|
||||
la = this.index
|
||||
fi = this.selected[0] + 1;
|
||||
la = this.index;
|
||||
} else {
|
||||
fi = this.index
|
||||
la = this.selected[0] - 1
|
||||
fi = this.index;
|
||||
la = this.selected[0] - 1;
|
||||
}
|
||||
|
||||
for (; fi <= la; fi++) {
|
||||
if (this.$store.state.selected.indexOf(fi) == -1) {
|
||||
this.addSelected(fi)
|
||||
this.addSelected(fi);
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.user.singleClick && !event.ctrlKey && !this.$store.state.multiple) this.resetSelected()
|
||||
this.addSelected(this.index)
|
||||
if (
|
||||
!this.singleClick &&
|
||||
!event.ctrlKey &&
|
||||
!event.metaKey &&
|
||||
!this.$store.state.multiple
|
||||
)
|
||||
this.resetSelected();
|
||||
this.addSelected(this.index);
|
||||
},
|
||||
dblclick: function () {
|
||||
if (!this.user.singleClick) this.open()
|
||||
if (!this.singleClick) this.open();
|
||||
},
|
||||
touchstart () {
|
||||
touchstart() {
|
||||
setTimeout(() => {
|
||||
this.touches = 0
|
||||
}, 300)
|
||||
this.touches = 0;
|
||||
}, 300);
|
||||
|
||||
this.touches++
|
||||
this.touches++;
|
||||
if (this.touches > 1) {
|
||||
this.open()
|
||||
this.open();
|
||||
}
|
||||
},
|
||||
open: function () {
|
||||
this.$router.push({path: this.url})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
this.$router.push({ path: this.url });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
<template>
|
||||
<div id="previewer">
|
||||
<div class="bar">
|
||||
<button @click="back" class="action" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close">
|
||||
<i class="material-icons">close</i>
|
||||
</button>
|
||||
|
||||
<div class="title">{{ this.name }}</div>
|
||||
|
||||
<preview-size-button v-if="isResizeEnabled && this.req.type === 'image'" @change-size="toggleSize" v-bind:size="fullSize" :disabled="loading"></preview-size-button>
|
||||
<button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
|
||||
<i class="material-icons">more_vert</i>
|
||||
</button>
|
||||
|
||||
<div id="dropdown" :class="{ active : showMore }">
|
||||
<rename-button :disabled="loading" v-if="user.perm.rename"></rename-button>
|
||||
<delete-button :disabled="loading" v-if="user.perm.delete"></delete-button>
|
||||
<download-button :disabled="loading" v-if="user.perm.download"></download-button>
|
||||
<info-button :disabled="loading"></info-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="loading" v-if="loading">
|
||||
<div class="spinner">
|
||||
<div class="bounce1"></div>
|
||||
<div class="bounce2"></div>
|
||||
<div class="bounce3"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="action" @click="prev" v-show="hasPrevious" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')">
|
||||
<i class="material-icons">chevron_left</i>
|
||||
</button>
|
||||
<button class="action" @click="next" v-show="hasNext" :aria-label="$t('buttons.next')" :title="$t('buttons.next')">
|
||||
<i class="material-icons">chevron_right</i>
|
||||
</button>
|
||||
|
||||
<template v-if="!loading">
|
||||
<div class="preview">
|
||||
<ExtendedImage v-if="req.type == 'image'" :src="raw"></ExtendedImage>
|
||||
<audio v-else-if="req.type == 'audio'" :src="raw" autoplay controls></audio>
|
||||
<video v-else-if="req.type == 'video'" :src="raw" autoplay controls>
|
||||
<track
|
||||
kind="captions"
|
||||
v-for="(sub, index) in subtitles"
|
||||
:key="index"
|
||||
:src="sub"
|
||||
:label="'Subtitle ' + index" :default="index === 0">
|
||||
Sorry, your browser doesn't support embedded videos,
|
||||
but don't worry, you can <a :href="download">download it</a>
|
||||
and watch it with your favorite video player!
|
||||
</video>
|
||||
<object v-else-if="req.extension.toLowerCase() == '.pdf'" class="pdf" :data="raw"></object>
|
||||
<a v-else-if="req.type == 'blob'" :href="download">
|
||||
<h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-show="showMore" @click="resetPrompts" class="overlay"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import url from '@/utils/url'
|
||||
import { baseURL, resizePreview } from '@/utils/constants'
|
||||
import { files as api } from '@/api'
|
||||
import PreviewSizeButton from '@/components/buttons/PreviewSize'
|
||||
import InfoButton from '@/components/buttons/Info'
|
||||
import DeleteButton from '@/components/buttons/Delete'
|
||||
import RenameButton from '@/components/buttons/Rename'
|
||||
import DownloadButton from '@/components/buttons/Download'
|
||||
import ExtendedImage from './ExtendedImage'
|
||||
|
||||
const mediaTypes = [
|
||||
"image",
|
||||
"video",
|
||||
"audio",
|
||||
"blob"
|
||||
]
|
||||
|
||||
export default {
|
||||
name: 'preview',
|
||||
components: {
|
||||
PreviewSizeButton,
|
||||
InfoButton,
|
||||
DeleteButton,
|
||||
RenameButton,
|
||||
DownloadButton,
|
||||
ExtendedImage
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
previousLink: '',
|
||||
nextLink: '',
|
||||
listing: null,
|
||||
name: '',
|
||||
subtitles: [],
|
||||
fullSize: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['req', 'user', 'oldReq', 'jwt', 'loading', 'show']),
|
||||
hasPrevious () {
|
||||
return (this.previousLink !== '')
|
||||
},
|
||||
hasNext () {
|
||||
return (this.nextLink !== '')
|
||||
},
|
||||
download () {
|
||||
return `${baseURL}/api/raw${url.encodePath(this.req.path)}?auth=${this.jwt}`
|
||||
},
|
||||
previewUrl () {
|
||||
if (this.req.type === 'image' && !this.fullSize) {
|
||||
return `${baseURL}/api/preview/big${url.encodePath(this.req.path)}?auth=${this.jwt}`
|
||||
}
|
||||
return `${baseURL}/api/raw${url.encodePath(this.req.path)}?auth=${this.jwt}`
|
||||
},
|
||||
raw () {
|
||||
return `${this.previewUrl}&inline=true`
|
||||
},
|
||||
showMore () {
|
||||
return this.$store.state.show === 'more'
|
||||
},
|
||||
isResizeEnabled () {
|
||||
return resizePreview
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route: function () {
|
||||
this.updatePreview()
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
window.addEventListener('keydown', this.key)
|
||||
this.$store.commit('setPreviewMode', true)
|
||||
this.listing = this.oldReq.items
|
||||
this.$root.$on('preview-deleted', this.deleted)
|
||||
this.updatePreview()
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.key)
|
||||
this.$store.commit('setPreviewMode', false)
|
||||
this.$root.$off('preview-deleted', this.deleted)
|
||||
},
|
||||
methods: {
|
||||
deleted () {
|
||||
this.listing = this.listing.filter(item => item.name !== this.name)
|
||||
|
||||
if (this.hasNext) {
|
||||
this.next()
|
||||
} else if (!this.hasPrevious && !this.hasNext) {
|
||||
this.back()
|
||||
} else {
|
||||
this.prev()
|
||||
}
|
||||
},
|
||||
back () {
|
||||
this.$store.commit('setPreviewMode', false)
|
||||
let uri = url.removeLastDir(this.$route.path) + '/'
|
||||
this.$router.push({ path: uri })
|
||||
},
|
||||
prev () {
|
||||
this.$router.push({ path: this.previousLink })
|
||||
},
|
||||
next () {
|
||||
this.$router.push({ path: this.nextLink })
|
||||
},
|
||||
key (event) {
|
||||
|
||||
if (this.show !== null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (event.which === 13 || event.which === 39) { // right arrow
|
||||
if (this.hasNext) this.next()
|
||||
} else if (event.which === 37) { // left arrow
|
||||
if (this.hasPrevious) this.prev()
|
||||
}
|
||||
},
|
||||
async updatePreview () {
|
||||
if (this.req.subtitles) {
|
||||
this.subtitles = this.req.subtitles.map(sub => `${baseURL}/api/raw${sub}?auth=${this.jwt}&inline=true`)
|
||||
}
|
||||
|
||||
let dirs = this.$route.fullPath.split("/")
|
||||
this.name = decodeURIComponent(dirs[dirs.length - 1])
|
||||
|
||||
if (!this.listing) {
|
||||
try {
|
||||
const path = url.removeLastDir(this.$route.path)
|
||||
const res = await api.fetch(path)
|
||||
this.listing = res.items
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
}
|
||||
}
|
||||
|
||||
this.previousLink = ''
|
||||
this.nextLink = ''
|
||||
|
||||
for (let i = 0; i < this.listing.length; i++) {
|
||||
if (this.listing[i].name !== this.name) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (let j = i - 1; j >= 0; j--) {
|
||||
if (mediaTypes.includes(this.listing[j].type)) {
|
||||
this.previousLink = this.listing[j].url
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for (let j = i + 1; j < this.listing.length; j++) {
|
||||
if (mediaTypes.includes(this.listing[j].type)) {
|
||||
this.nextLink = this.listing[j].url
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
},
|
||||
openMore () {
|
||||
this.$store.commit('showHover', 'more')
|
||||
},
|
||||
resetPrompts () {
|
||||
this.$store.commit('closeHovers')
|
||||
},
|
||||
toggleSize () {
|
||||
this.fullSize = !this.fullSize
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
25
frontend/src/components/header/Action.vue
Normal file
25
frontend/src/components/header/Action.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<button @click="action" :aria-label="label" :title="label" class="action">
|
||||
<i class="material-icons">{{ icon }}</i>
|
||||
<span>{{ label }}</span>
|
||||
<span v-if="counter > 0" class="counter">{{ counter }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "action",
|
||||
props: ["icon", "label", "counter", "show"],
|
||||
methods: {
|
||||
action: function () {
|
||||
if (this.show) {
|
||||
this.$store.commit("showHover", this.show);
|
||||
}
|
||||
|
||||
this.$emit("action");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
58
frontend/src/components/header/HeaderBar.vue
Normal file
58
frontend/src/components/header/HeaderBar.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<header>
|
||||
<img v-if="showLogo !== undefined" :src="logoURL" />
|
||||
<action
|
||||
v-if="showMenu !== undefined"
|
||||
class="menu-button"
|
||||
icon="menu"
|
||||
:label="$t('buttons.toggleSidebar')"
|
||||
@action="openSidebar()"
|
||||
/>
|
||||
|
||||
<slot />
|
||||
|
||||
<div id="dropdown" :class="{ active: this.$store.state.show === 'more' }">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
|
||||
<action
|
||||
v-if="this.$slots.actions"
|
||||
id="more"
|
||||
icon="more_vert"
|
||||
:label="$t('buttons.more')"
|
||||
@action="$store.commit('showHover', 'more')"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="overlay"
|
||||
v-show="this.$store.state.show == 'more'"
|
||||
@click="$store.commit('closeHovers')"
|
||||
/>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { logoURL } from "@/utils/constants";
|
||||
|
||||
import Action from "@/components/header/Action";
|
||||
|
||||
export default {
|
||||
name: "header-bar",
|
||||
props: ["showLogo", "showMenu"],
|
||||
components: {
|
||||
Action,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
logoURL,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
openSidebar() {
|
||||
this.$store.commit("showHover", "sidebar");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -1,108 +1,119 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.copy') }}</h2>
|
||||
<h2>{{ $t("prompts.copy") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.copyMessage') }}</p>
|
||||
<file-list @update:selected="val => dest = val"></file-list>
|
||||
<p>{{ $t("prompts.copyMessage") }}</p>
|
||||
<file-list @update:selected="(val) => (dest = val)"></file-list>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="button button--flat button--grey"
|
||||
<button
|
||||
class="button button--flat button--grey"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="button button--flat"
|
||||
:title="$t('buttons.cancel')"
|
||||
>
|
||||
{{ $t("buttons.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
class="button button--flat"
|
||||
@click="copy"
|
||||
:aria-label="$t('buttons.copy')"
|
||||
:title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button>
|
||||
:title="$t('buttons.copy')"
|
||||
>
|
||||
{{ $t("buttons.copy") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import FileList from './FileList'
|
||||
import { files as api } from '@/api'
|
||||
import buttons from '@/utils/buttons'
|
||||
import * as upload from '@/utils/upload'
|
||||
import { mapState } from "vuex";
|
||||
import FileList from "./FileList";
|
||||
import { files as api } from "@/api";
|
||||
import buttons from "@/utils/buttons";
|
||||
import * as upload from "@/utils/upload";
|
||||
|
||||
export default {
|
||||
name: 'copy',
|
||||
name: "copy",
|
||||
components: { FileList },
|
||||
data: function () {
|
||||
return {
|
||||
current: window.location.pathname,
|
||||
dest: null
|
||||
}
|
||||
dest: null,
|
||||
};
|
||||
},
|
||||
computed: mapState(['req', 'selected']),
|
||||
computed: mapState(["req", "selected"]),
|
||||
methods: {
|
||||
copy: async function (event) {
|
||||
event.preventDefault()
|
||||
let items = []
|
||||
event.preventDefault();
|
||||
let items = [];
|
||||
|
||||
// Create a new promise for each file.
|
||||
for (let item of this.selected) {
|
||||
items.push({
|
||||
from: this.req.items[item].url,
|
||||
to: this.dest + encodeURIComponent(this.req.items[item].name),
|
||||
name: this.req.items[item].name
|
||||
})
|
||||
name: this.req.items[item].name,
|
||||
});
|
||||
}
|
||||
|
||||
let action = async (overwrite, rename) => {
|
||||
buttons.loading('copy')
|
||||
buttons.loading("copy");
|
||||
|
||||
await api.copy(items, overwrite, rename).then(() => {
|
||||
buttons.success('copy')
|
||||
await api
|
||||
.copy(items, overwrite, rename)
|
||||
.then(() => {
|
||||
buttons.success("copy");
|
||||
|
||||
if (this.$route.path === this.dest) {
|
||||
this.$store.commit('setReload', true)
|
||||
if (this.$route.path === this.dest) {
|
||||
this.$store.commit("setReload", true);
|
||||
|
||||
return
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.$router.push({ path: this.dest })
|
||||
}).catch((e) => {
|
||||
buttons.done('copy')
|
||||
this.$showError(e)
|
||||
})
|
||||
}
|
||||
this.$router.push({ path: this.dest });
|
||||
})
|
||||
.catch((e) => {
|
||||
buttons.done("copy");
|
||||
this.$showError(e);
|
||||
});
|
||||
};
|
||||
|
||||
if (this.$route.path === this.dest) {
|
||||
this.$store.commit('closeHovers')
|
||||
action(false, true)
|
||||
this.$store.commit("closeHovers");
|
||||
action(false, true);
|
||||
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
let dstItems = (await api.fetch(this.dest)).items
|
||||
let conflict = upload.checkConflict(items, dstItems)
|
||||
let dstItems = (await api.fetch(this.dest)).items;
|
||||
let conflict = upload.checkConflict(items, dstItems);
|
||||
|
||||
let overwrite = false
|
||||
let rename = false
|
||||
let overwrite = false;
|
||||
let rename = false;
|
||||
|
||||
if (conflict) {
|
||||
this.$store.commit('showHover', {
|
||||
prompt: 'replace-rename',
|
||||
this.$store.commit("showHover", {
|
||||
prompt: "replace-rename",
|
||||
confirm: (event, option) => {
|
||||
overwrite = option == 'overwrite'
|
||||
rename = option == 'rename'
|
||||
overwrite = option == "overwrite";
|
||||
rename = option == "rename";
|
||||
|
||||
event.preventDefault()
|
||||
this.$store.commit('closeHovers')
|
||||
action(overwrite, rename)
|
||||
}
|
||||
})
|
||||
event.preventDefault();
|
||||
this.$store.commit("closeHovers");
|
||||
action(overwrite, rename);
|
||||
},
|
||||
});
|
||||
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
action(overwrite, rename)
|
||||
}
|
||||
}
|
||||
}
|
||||
action(overwrite, rename);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,68 +1,80 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-content">
|
||||
<p v-if="req.kind !== 'listing'">{{ $t('prompts.deleteMessageSingle') }}</p>
|
||||
<p v-else>{{ $t('prompts.deleteMessageMultiple', { count: selectedCount}) }}</p>
|
||||
<p v-if="req.kind !== 'listing'">
|
||||
{{ $t("prompts.deleteMessageSingle") }}
|
||||
</p>
|
||||
<p v-else>
|
||||
{{ $t("prompts.deleteMessageMultiple", { count: selectedCount }) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<button @click="$store.commit('closeHovers')"
|
||||
<button
|
||||
@click="$store.commit('closeHovers')"
|
||||
class="button button--flat button--grey"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button @click="submit"
|
||||
:title="$t('buttons.cancel')"
|
||||
>
|
||||
{{ $t("buttons.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
@click="submit"
|
||||
class="button button--flat button--red"
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
|
||||
:title="$t('buttons.delete')"
|
||||
>
|
||||
{{ $t("buttons.delete") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapGetters, mapMutations, mapState} from 'vuex'
|
||||
import { files as api } from '@/api'
|
||||
import buttons from '@/utils/buttons'
|
||||
import { mapGetters, mapMutations, mapState } from "vuex";
|
||||
import { files as api } from "@/api";
|
||||
import buttons from "@/utils/buttons";
|
||||
|
||||
export default {
|
||||
name: 'delete',
|
||||
name: "delete",
|
||||
computed: {
|
||||
...mapGetters(['isListing', 'selectedCount']),
|
||||
...mapState(['req', 'selected'])
|
||||
...mapGetters(["isListing", "selectedCount"]),
|
||||
...mapState(["req", "selected", "showConfirm"]),
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(['closeHovers']),
|
||||
...mapMutations(["closeHovers"]),
|
||||
submit: async function () {
|
||||
buttons.loading('delete')
|
||||
buttons.loading("delete");
|
||||
|
||||
try {
|
||||
if (!this.isListing) {
|
||||
await api.remove(this.$route.path)
|
||||
buttons.success('delete')
|
||||
await api.remove(this.$route.path);
|
||||
buttons.success("delete");
|
||||
|
||||
this.$root.$emit('preview-deleted')
|
||||
this.closeHovers()
|
||||
return
|
||||
this.showConfirm();
|
||||
this.closeHovers();
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeHovers()
|
||||
this.closeHovers();
|
||||
|
||||
if (this.selectedCount === 0) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
let promises = []
|
||||
let promises = [];
|
||||
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)
|
||||
buttons.success('delete')
|
||||
this.$store.commit('setReload', true)
|
||||
await Promise.all(promises);
|
||||
buttons.success("delete");
|
||||
this.$store.commit("setReload", true);
|
||||
} catch (e) {
|
||||
buttons.done('delete')
|
||||
this.$showError(e)
|
||||
if (this.isListing) this.$store.commit('setReload', true)
|
||||
buttons.done("delete");
|
||||
this.$showError(e);
|
||||
if (this.isListing) this.$store.commit("setReload", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,49 +1,43 @@
|
||||
<template>
|
||||
<div class="card floating" id="download">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.download') }}</h2>
|
||||
<h2>{{ $t("prompts.download") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.downloadMessage') }}</p>
|
||||
<p>{{ $t("prompts.downloadMessage") }}</p>
|
||||
|
||||
<button class="button button--block" @click="download('zip')" v-focus>zip</button>
|
||||
<button class="button button--block" @click="download('tar')" v-focus>tar</button>
|
||||
<button class="button button--block" @click="download('targz')" v-focus>tar.gz</button>
|
||||
<button class="button button--block" @click="download('tarbz2')" v-focus>tar.bz2</button>
|
||||
<button class="button button--block" @click="download('tarxz')" v-focus>tar.xz</button>
|
||||
<button class="button button--block" @click="download('tarlz4')" v-focus>tar.lz4</button>
|
||||
<button class="button button--block" @click="download('tarsz')" v-focus>tar.sz</button>
|
||||
<button
|
||||
v-for="(ext, format) in formats"
|
||||
:key="format"
|
||||
class="button button--block"
|
||||
@click="showConfirm(format)"
|
||||
v-focus
|
||||
>
|
||||
{{ ext }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapGetters, mapState} from 'vuex'
|
||||
import { files as api } from '@/api'
|
||||
import { mapState } from "vuex";
|
||||
|
||||
export default {
|
||||
name: 'download',
|
||||
computed: {
|
||||
...mapState(['selected', 'req']),
|
||||
...mapGetters(['selectedCount'])
|
||||
name: "download",
|
||||
data: function () {
|
||||
return {
|
||||
formats: {
|
||||
zip: "zip",
|
||||
tar: "tar",
|
||||
targz: "tar.gz",
|
||||
tarbz2: "tar.bz2",
|
||||
tarxz: "tar.xz",
|
||||
tarlz4: "tar.lz4",
|
||||
tarsz: "tar.sz",
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
download: function (format) {
|
||||
if (this.selectedCount === 0) {
|
||||
api.download(format, this.$route.path)
|
||||
} else {
|
||||
let files = []
|
||||
|
||||
for (let i of this.selected) {
|
||||
files.push(this.req.items[i].url)
|
||||
}
|
||||
|
||||
api.download(format, ...files)
|
||||
}
|
||||
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
}
|
||||
}
|
||||
computed: mapState(["showConfirm"]),
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,128 +1,138 @@
|
||||
<template>
|
||||
<div>
|
||||
<ul class="file-list">
|
||||
<li @click="select"
|
||||
<li
|
||||
@click="itemClick"
|
||||
@touchstart="touchstart"
|
||||
@dblclick="next"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:aria-label="item.name"
|
||||
:aria-selected="selected == item.url"
|
||||
:key="item.name" v-for="item in items"
|
||||
:data-url="item.url">{{ item.name }}</li>
|
||||
:key="item.name"
|
||||
v-for="item in items"
|
||||
:data-url="item.url"
|
||||
>
|
||||
{{ item.name }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>{{ $t('prompts.currentlyNavigating') }} <code>{{ nav }}</code>.</p>
|
||||
<p>
|
||||
{{ $t("prompts.currentlyNavigating") }} <code>{{ nav }}</code
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import url from '@/utils/url'
|
||||
import { files } from '@/api'
|
||||
import { mapState } from "vuex";
|
||||
import url from "@/utils/url";
|
||||
import { files } from "@/api";
|
||||
|
||||
export default {
|
||||
name: 'file-list',
|
||||
name: "file-list",
|
||||
data: function () {
|
||||
return {
|
||||
items: [],
|
||||
touches: {
|
||||
id: '',
|
||||
count: 0
|
||||
id: "",
|
||||
count: 0,
|
||||
},
|
||||
selected: null,
|
||||
current: window.location.pathname
|
||||
}
|
||||
current: window.location.pathname,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState([ 'req' ]),
|
||||
nav () {
|
||||
return decodeURIComponent(this.current)
|
||||
}
|
||||
...mapState(["req", "user"]),
|
||||
nav() {
|
||||
return decodeURIComponent(this.current);
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.fillOptions(this.req)
|
||||
mounted() {
|
||||
this.fillOptions(this.req);
|
||||
},
|
||||
methods: {
|
||||
fillOptions (req) {
|
||||
fillOptions(req) {
|
||||
// Sets the current path and resets
|
||||
// the current items.
|
||||
this.current = req.url
|
||||
this.items = []
|
||||
this.current = req.url;
|
||||
this.items = [];
|
||||
|
||||
this.$emit('update:selected', this.current)
|
||||
this.$emit("update:selected", this.current);
|
||||
|
||||
// If the path isn't the root path,
|
||||
// show a button to navigate to the previous
|
||||
// directory.
|
||||
if (req.url !== '/files/') {
|
||||
if (req.url !== "/files/") {
|
||||
this.items.push({
|
||||
name: '..',
|
||||
url: url.removeLastDir(req.url) + '/'
|
||||
})
|
||||
name: "..",
|
||||
url: url.removeLastDir(req.url) + "/",
|
||||
});
|
||||
}
|
||||
|
||||
// If this folder is empty, finish here.
|
||||
if (req.items === null) return
|
||||
if (req.items === null) return;
|
||||
|
||||
// Otherwise we add every directory to the
|
||||
// move options.
|
||||
for (let item of req.items) {
|
||||
if (!item.isDir) continue
|
||||
if (!item.isDir) continue;
|
||||
|
||||
this.items.push({
|
||||
name: item.name,
|
||||
url: item.url
|
||||
})
|
||||
url: item.url,
|
||||
});
|
||||
}
|
||||
},
|
||||
next: function (event) {
|
||||
// Retrieves the URL of the directory the user
|
||||
// just clicked in and fill the options with its
|
||||
// content.
|
||||
let 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) {
|
||||
let url = event.currentTarget.dataset.url
|
||||
touchstart(event) {
|
||||
let url = event.currentTarget.dataset.url;
|
||||
|
||||
// In 300 milliseconds, we shall reset the count.
|
||||
setTimeout(() => {
|
||||
this.touches.count = 0
|
||||
}, 300)
|
||||
this.touches.count = 0;
|
||||
}, 300);
|
||||
|
||||
// If the element the user is touching
|
||||
// is different from the last one he touched,
|
||||
// reset the count.
|
||||
if (this.touches.id !== url) {
|
||||
this.touches.id = url
|
||||
this.touches.count = 1
|
||||
return
|
||||
this.touches.id = url;
|
||||
this.touches.count = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
this.touches.count++
|
||||
this.touches.count++;
|
||||
|
||||
// If there is more than one touch already,
|
||||
// open the next screen.
|
||||
if (this.touches.count > 1) {
|
||||
this.next(event)
|
||||
this.next(event);
|
||||
}
|
||||
},
|
||||
itemClick: function (event) {
|
||||
if (this.user.singleClick) this.next(event);
|
||||
else this.select(event);
|
||||
},
|
||||
select: function (event) {
|
||||
// If the element is already selected, unselect it.
|
||||
if (this.selected === event.currentTarget.dataset.url) {
|
||||
this.selected = null
|
||||
this.$emit('update:selected', this.current)
|
||||
return
|
||||
this.selected = null;
|
||||
this.$emit("update:selected", this.current);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise select the element.
|
||||
this.selected = event.currentTarget.dataset.url
|
||||
this.$emit('update:selected', this.selected)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.selected = event.currentTarget.dataset.url;
|
||||
this.$emit("update:selected", this.selected);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,34 +1,37 @@
|
||||
<template>
|
||||
<div class="card floating help">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('help.help') }}</h2>
|
||||
<h2>{{ $t("help.help") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<ul>
|
||||
<li><strong>F1</strong> - {{ $t('help.f1') }}</li>
|
||||
<li><strong>F2</strong> - {{ $t('help.f2') }}</li>
|
||||
<li><strong>DEL</strong> - {{ $t('help.del') }}</li>
|
||||
<li><strong>ESC</strong> - {{ $t('help.esc') }}</li>
|
||||
<li><strong>CTRL + S</strong> - {{ $t('help.ctrl.s') }}</li>
|
||||
<li><strong>CTRL + F</strong> - {{ $t('help.ctrl.f') }}</li>
|
||||
<li><strong>CTRL + Click</strong> - {{ $t('help.ctrl.click') }}</li>
|
||||
<li><strong>Click</strong> - {{ $t('help.click') }}</li>
|
||||
<li><strong>Double click</strong> - {{ $t('help.doubleClick') }}</li>
|
||||
<li><strong>F1</strong> - {{ $t("help.f1") }}</li>
|
||||
<li><strong>F2</strong> - {{ $t("help.f2") }}</li>
|
||||
<li><strong>DEL</strong> - {{ $t("help.del") }}</li>
|
||||
<li><strong>ESC</strong> - {{ $t("help.esc") }}</li>
|
||||
<li><strong>CTRL + S</strong> - {{ $t("help.ctrl.s") }}</li>
|
||||
<li><strong>CTRL + F</strong> - {{ $t("help.ctrl.f") }}</li>
|
||||
<li><strong>CTRL + Click</strong> - {{ $t("help.ctrl.click") }}</li>
|
||||
<li><strong>Click</strong> - {{ $t("help.click") }}</li>
|
||||
<li><strong>Double click</strong> - {{ $t("help.doubleClick") }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button type="submit"
|
||||
<button
|
||||
type="submit"
|
||||
@click="$store.commit('closeHovers')"
|
||||
class="button button--flat"
|
||||
:aria-label="$t('buttons.ok')"
|
||||
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
|
||||
:title="$t('buttons.ok')"
|
||||
>
|
||||
{{ $t("buttons.ok") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default { name: 'help' }
|
||||
export default { name: "help" };
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,99 +1,149 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.fileInfo') }}</h2>
|
||||
<h2>{{ $t("prompts.fileInfo") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p v-if="selected.length > 1">{{ $t('prompts.filesSelected', { count: selected.length }) }}</p>
|
||||
<p v-if="selected.length > 1">
|
||||
{{ $t("prompts.filesSelected", { count: selected.length }) }}
|
||||
</p>
|
||||
|
||||
<p class="break-word" v-if="selected.length < 2"><strong>{{ $t('prompts.displayName') }}</strong> {{ name }}</p>
|
||||
<p v-if="!dir || selected.length > 1"><strong>{{ $t('prompts.size') }}:</strong> <span id="content_length"></span> {{ humanSize }}</p>
|
||||
<p v-if="selected.length < 2"><strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime }}</p>
|
||||
<p class="break-word" v-if="selected.length < 2">
|
||||
<strong>{{ $t("prompts.displayName") }}</strong> {{ name }}
|
||||
</p>
|
||||
<p v-if="!dir || selected.length > 1">
|
||||
<strong>{{ $t("prompts.size") }}:</strong>
|
||||
<span id="content_length"></span> {{ humanSize }}
|
||||
</p>
|
||||
<p v-if="selected.length < 2">
|
||||
<strong>{{ $t("prompts.lastModified") }}:</strong> {{ humanTime }}
|
||||
</p>
|
||||
|
||||
<template v-if="dir && selected.length === 0">
|
||||
<p><strong>{{ $t('prompts.numberFiles') }}:</strong> {{ req.numFiles }}</p>
|
||||
<p><strong>{{ $t('prompts.numberDirs') }}:</strong> {{ req.numDirs }}</p>
|
||||
<p>
|
||||
<strong>{{ $t("prompts.numberFiles") }}:</strong> {{ req.numFiles }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{{ $t("prompts.numberDirs") }}:</strong> {{ req.numDirs }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template v-if="!dir">
|
||||
<p><strong>MD5: </strong><code><a @click="checksum($event, 'md5')">{{ $t('prompts.show') }}</a></code></p>
|
||||
<p><strong>SHA1: </strong><code><a @click="checksum($event, 'sha1')">{{ $t('prompts.show') }}</a></code></p>
|
||||
<p><strong>SHA256: </strong><code><a @click="checksum($event, 'sha256')">{{ $t('prompts.show') }}</a></code></p>
|
||||
<p><strong>SHA512: </strong><code><a @click="checksum($event, 'sha512')">{{ $t('prompts.show') }}</a></code></p>
|
||||
<p>
|
||||
<strong>MD5: </strong
|
||||
><code
|
||||
><a @click="checksum($event, 'md5')">{{
|
||||
$t("prompts.show")
|
||||
}}</a></code
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<strong>SHA1: </strong
|
||||
><code
|
||||
><a @click="checksum($event, 'sha1')">{{
|
||||
$t("prompts.show")
|
||||
}}</a></code
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<strong>SHA256: </strong
|
||||
><code
|
||||
><a @click="checksum($event, 'sha256')">{{
|
||||
$t("prompts.show")
|
||||
}}</a></code
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<strong>SHA512: </strong
|
||||
><code
|
||||
><a @click="checksum($event, 'sha512')">{{
|
||||
$t("prompts.show")
|
||||
}}</a></code
|
||||
>
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button type="submit"
|
||||
<button
|
||||
type="submit"
|
||||
@click="$store.commit('closeHovers')"
|
||||
class="button button--flat"
|
||||
:aria-label="$t('buttons.ok')"
|
||||
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
|
||||
:title="$t('buttons.ok')"
|
||||
>
|
||||
{{ $t("buttons.ok") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState, mapGetters} from 'vuex'
|
||||
import filesize from 'filesize'
|
||||
import moment from 'moment'
|
||||
import { files as api } from '@/api'
|
||||
import { mapState, mapGetters } from "vuex";
|
||||
import filesize from "filesize";
|
||||
import moment from "moment";
|
||||
import { files as api } from "@/api";
|
||||
|
||||
export default {
|
||||
name: 'info',
|
||||
name: "info",
|
||||
computed: {
|
||||
...mapState(['req', 'selected']),
|
||||
...mapGetters(['selectedCount', 'isListing']),
|
||||
...mapState(["req", "selected"]),
|
||||
...mapGetters(["selectedCount", "isListing"]),
|
||||
humanSize: function () {
|
||||
if (this.selectedCount === 0 || !this.isListing) {
|
||||
return filesize(this.req.size)
|
||||
return filesize(this.req.size);
|
||||
}
|
||||
|
||||
let sum = 0
|
||||
let sum = 0;
|
||||
|
||||
for (let selected of this.selected) {
|
||||
sum += this.req.items[selected].size
|
||||
sum += this.req.items[selected].size;
|
||||
}
|
||||
|
||||
return filesize(sum)
|
||||
return filesize(sum);
|
||||
},
|
||||
humanTime: function () {
|
||||
if (this.selectedCount === 0) {
|
||||
return moment(this.req.modified).fromNow()
|
||||
return moment(this.req.modified).fromNow();
|
||||
}
|
||||
|
||||
return moment(this.req.items[this.selected[0]]).fromNow()
|
||||
return moment(this.req.items[this.selected[0]].modified).fromNow();
|
||||
},
|
||||
name: function () {
|
||||
return this.selectedCount === 0 ? this.req.name : this.req.items[this.selected[0]].name
|
||||
return this.selectedCount === 0
|
||||
? this.req.name
|
||||
: this.req.items[this.selected[0]].name;
|
||||
},
|
||||
dir: function () {
|
||||
return this.selectedCount > 1 || (this.selectedCount === 0
|
||||
? this.req.isDir
|
||||
: this.req.items[this.selected[0]].isDir)
|
||||
}
|
||||
return (
|
||||
this.selectedCount > 1 ||
|
||||
(this.selectedCount === 0
|
||||
? this.req.isDir
|
||||
: this.req.items[this.selected[0]].isDir)
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
checksum: async function (event, algo) {
|
||||
event.preventDefault()
|
||||
event.preventDefault();
|
||||
|
||||
let link
|
||||
let link;
|
||||
|
||||
if (this.selectedCount) {
|
||||
link = this.req.items[this.selected[0]].url
|
||||
link = this.req.items[this.selected[0]].url;
|
||||
} else {
|
||||
link = this.$route.path
|
||||
link = this.$route.path;
|
||||
}
|
||||
|
||||
try {
|
||||
const hash = await api.checksum(link, algo)
|
||||
const hash = await api.checksum(link, algo);
|
||||
// eslint-disable-next-line
|
||||
event.target.innerHTML = hash
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
this.$showError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,93 +1,104 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.move') }}</h2>
|
||||
<h2>{{ $t("prompts.move") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<file-list @update:selected="val => dest = val"></file-list>
|
||||
<file-list @update:selected="(val) => (dest = val)"></file-list>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="button button--flat button--grey"
|
||||
<button
|
||||
class="button button--flat button--grey"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="button button--flat"
|
||||
:title="$t('buttons.cancel')"
|
||||
>
|
||||
{{ $t("buttons.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
class="button button--flat"
|
||||
@click="move"
|
||||
:disabled="$route.path === dest"
|
||||
:aria-label="$t('buttons.move')"
|
||||
:title="$t('buttons.move')">{{ $t('buttons.move') }}</button>
|
||||
:title="$t('buttons.move')"
|
||||
>
|
||||
{{ $t("buttons.move") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import FileList from './FileList'
|
||||
import { files as api } from '@/api'
|
||||
import buttons from '@/utils/buttons'
|
||||
import * as upload from '@/utils/upload'
|
||||
import { mapState } from "vuex";
|
||||
import FileList from "./FileList";
|
||||
import { files as api } from "@/api";
|
||||
import buttons from "@/utils/buttons";
|
||||
import * as upload from "@/utils/upload";
|
||||
|
||||
export default {
|
||||
name: 'move',
|
||||
name: "move",
|
||||
components: { FileList },
|
||||
data: function () {
|
||||
return {
|
||||
current: window.location.pathname,
|
||||
dest: null
|
||||
}
|
||||
dest: null,
|
||||
};
|
||||
},
|
||||
computed: mapState(['req', 'selected']),
|
||||
computed: mapState(["req", "selected"]),
|
||||
methods: {
|
||||
move: async function (event) {
|
||||
event.preventDefault()
|
||||
let items = []
|
||||
event.preventDefault();
|
||||
let items = [];
|
||||
|
||||
for (let item of this.selected) {
|
||||
items.push({
|
||||
from: this.req.items[item].url,
|
||||
to: this.dest + encodeURIComponent(this.req.items[item].name),
|
||||
name: this.req.items[item].name
|
||||
})
|
||||
name: this.req.items[item].name,
|
||||
});
|
||||
}
|
||||
|
||||
let action = async (overwrite, rename) => {
|
||||
buttons.loading('move')
|
||||
buttons.loading("move");
|
||||
|
||||
await api.move(items, overwrite, rename).then(() => {
|
||||
buttons.success('move')
|
||||
this.$router.push({ path: this.dest })
|
||||
}).catch((e) => {
|
||||
buttons.done('move')
|
||||
this.$showError(e)
|
||||
})
|
||||
}
|
||||
await api
|
||||
.move(items, overwrite, rename)
|
||||
.then(() => {
|
||||
buttons.success("move");
|
||||
this.$router.push({ path: this.dest });
|
||||
})
|
||||
.catch((e) => {
|
||||
buttons.done("move");
|
||||
this.$showError(e);
|
||||
});
|
||||
};
|
||||
|
||||
let dstItems = (await api.fetch(this.dest)).items
|
||||
let conflict = upload.checkConflict(items, dstItems)
|
||||
let dstItems = (await api.fetch(this.dest)).items;
|
||||
let conflict = upload.checkConflict(items, dstItems);
|
||||
|
||||
let overwrite = false
|
||||
let rename = false
|
||||
let overwrite = false;
|
||||
let rename = false;
|
||||
|
||||
if (conflict) {
|
||||
this.$store.commit('showHover', {
|
||||
prompt: 'replace-rename',
|
||||
this.$store.commit("showHover", {
|
||||
prompt: "replace-rename",
|
||||
confirm: (event, option) => {
|
||||
overwrite = option == 'overwrite'
|
||||
rename = option == 'rename'
|
||||
overwrite = option == "overwrite";
|
||||
rename = option == "rename";
|
||||
|
||||
event.preventDefault()
|
||||
this.$store.commit('closeHovers')
|
||||
action(overwrite, rename)
|
||||
}
|
||||
})
|
||||
event.preventDefault();
|
||||
this.$store.commit("closeHovers");
|
||||
action(overwrite, rename);
|
||||
},
|
||||
});
|
||||
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
action(overwrite, rename)
|
||||
}
|
||||
}
|
||||
}
|
||||
action(overwrite, rename);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.newDir') }}</h2>
|
||||
<h2>{{ $t("prompts.newDir") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.newDirMessage') }}</p>
|
||||
<input class="input input--block" type="text" @keyup.enter="submit" v-model.trim="name" v-focus>
|
||||
<p>{{ $t("prompts.newDirMessage") }}</p>
|
||||
<input
|
||||
class="input input--block"
|
||||
type="text"
|
||||
@keyup.enter="submit"
|
||||
v-model.trim="name"
|
||||
v-focus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
@@ -15,57 +21,60 @@
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')"
|
||||
>{{ $t('buttons.cancel') }}</button>
|
||||
>
|
||||
{{ $t("buttons.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
class="button button--flat"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')"
|
||||
@click="submit"
|
||||
>{{ $t('buttons.create') }}</button>
|
||||
>
|
||||
{{ $t("buttons.create") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import { files as api } from '@/api'
|
||||
import url from '@/utils/url'
|
||||
import { mapGetters } from "vuex";
|
||||
import { files as api } from "@/api";
|
||||
import url from "@/utils/url";
|
||||
|
||||
export default {
|
||||
name: 'new-dir',
|
||||
data: function() {
|
||||
name: "new-dir",
|
||||
data: function () {
|
||||
return {
|
||||
name: ''
|
||||
name: "",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([ 'isFiles', 'isListing' ])
|
||||
...mapGetters(["isFiles", "isListing"]),
|
||||
},
|
||||
methods: {
|
||||
submit: async function(event) {
|
||||
event.preventDefault()
|
||||
if (this.new === '') return
|
||||
submit: async function (event) {
|
||||
event.preventDefault();
|
||||
if (this.new === "") return;
|
||||
|
||||
// Build the path of the new directory.
|
||||
let uri = this.isFiles ? this.$route.path + '/' : '/'
|
||||
let uri = this.isFiles ? this.$route.path + "/" : "/";
|
||||
|
||||
if (!this.isListing) {
|
||||
uri = url.removeLastDir(uri) + '/'
|
||||
uri = url.removeLastDir(uri) + "/";
|
||||
}
|
||||
|
||||
uri += encodeURIComponent(this.name) + '/'
|
||||
uri = uri.replace('//', '/')
|
||||
uri += encodeURIComponent(this.name) + "/";
|
||||
uri = uri.replace("//", "/");
|
||||
|
||||
try {
|
||||
await api.post(uri)
|
||||
this.$router.push({ path: uri })
|
||||
await api.post(uri);
|
||||
this.$router.push({ path: uri });
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
this.$showError(e);
|
||||
}
|
||||
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
}
|
||||
this.$store.commit("closeHovers");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.newFile') }}</h2>
|
||||
<h2>{{ $t("prompts.newFile") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.newFileMessage') }}</p>
|
||||
<input class="input input--block" v-focus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||
<p>{{ $t("prompts.newFileMessage") }}</p>
|
||||
<input
|
||||
class="input input--block"
|
||||
v-focus
|
||||
type="text"
|
||||
@keyup.enter="submit"
|
||||
v-model.trim="name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
@@ -15,57 +21,60 @@
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')"
|
||||
>{{ $t('buttons.cancel') }}</button>
|
||||
>
|
||||
{{ $t("buttons.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
class="button button--flat"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')"
|
||||
>{{ $t('buttons.create') }}</button>
|
||||
>
|
||||
{{ $t("buttons.create") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import { files as api } from '@/api'
|
||||
import url from '@/utils/url'
|
||||
import { mapGetters } from "vuex";
|
||||
import { files as api } from "@/api";
|
||||
import url from "@/utils/url";
|
||||
|
||||
export default {
|
||||
name: 'new-file',
|
||||
data: function() {
|
||||
name: "new-file",
|
||||
data: function () {
|
||||
return {
|
||||
name: ''
|
||||
name: "",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([ 'isFiles', 'isListing' ])
|
||||
...mapGetters(["isFiles", "isListing"]),
|
||||
},
|
||||
methods: {
|
||||
submit: async function(event) {
|
||||
event.preventDefault()
|
||||
if (this.new === '') return
|
||||
submit: async function (event) {
|
||||
event.preventDefault();
|
||||
if (this.new === "") return;
|
||||
|
||||
// Build the path of the new directory.
|
||||
let uri = this.isFiles ? this.$route.path + '/' : '/'
|
||||
let uri = this.isFiles ? this.$route.path + "/" : "/";
|
||||
|
||||
if (!this.isListing) {
|
||||
uri = url.removeLastDir(uri) + '/'
|
||||
uri = url.removeLastDir(uri) + "/";
|
||||
}
|
||||
|
||||
uri += encodeURIComponent(this.name)
|
||||
uri = uri.replace('//', '/')
|
||||
uri += encodeURIComponent(this.name);
|
||||
uri = uri.replace("//", "/");
|
||||
|
||||
try {
|
||||
await api.post(uri)
|
||||
this.$router.push({ path: uri })
|
||||
await api.post(uri);
|
||||
this.$router.push({ path: uri });
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
this.$showError(e);
|
||||
}
|
||||
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
}
|
||||
this.$store.commit("closeHovers");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -6,24 +6,25 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Help from './Help'
|
||||
import Info from './Info'
|
||||
import Delete from './Delete'
|
||||
import Rename from './Rename'
|
||||
import Download from './Download'
|
||||
import Move from './Move'
|
||||
import Copy from './Copy'
|
||||
import NewFile from './NewFile'
|
||||
import NewDir from './NewDir'
|
||||
import Replace from './Replace'
|
||||
import ReplaceRename from './ReplaceRename'
|
||||
import Share from './Share'
|
||||
import Upload from './Upload'
|
||||
import { mapState } from 'vuex'
|
||||
import buttons from '@/utils/buttons'
|
||||
import Help from "./Help";
|
||||
import Info from "./Info";
|
||||
import Delete from "./Delete";
|
||||
import Rename from "./Rename";
|
||||
import Download from "./Download";
|
||||
import Move from "./Move";
|
||||
import Copy from "./Copy";
|
||||
import NewFile from "./NewFile";
|
||||
import NewDir from "./NewDir";
|
||||
import Replace from "./Replace";
|
||||
import ReplaceRename from "./ReplaceRename";
|
||||
import Share from "./Share";
|
||||
import Upload from "./Upload";
|
||||
import ShareDelete from "./ShareDelete";
|
||||
import { mapState } from "vuex";
|
||||
import buttons from "@/utils/buttons";
|
||||
|
||||
export default {
|
||||
name: 'prompts',
|
||||
name: "prompts",
|
||||
components: {
|
||||
Info,
|
||||
Delete,
|
||||
@@ -37,73 +38,76 @@ export default {
|
||||
Help,
|
||||
Replace,
|
||||
ReplaceRename,
|
||||
Upload
|
||||
Upload,
|
||||
ShareDelete,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
pluginData: {
|
||||
buttons,
|
||||
'store': this.$store,
|
||||
'router': this.$router
|
||||
}
|
||||
}
|
||||
store: this.$store,
|
||||
router: this.$router,
|
||||
},
|
||||
};
|
||||
},
|
||||
created () {
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if (this.show == null)
|
||||
return
|
||||
created() {
|
||||
window.addEventListener("keydown", (event) => {
|
||||
if (this.show == null) return;
|
||||
|
||||
let prompt = this.$refs.currentComponent;
|
||||
|
||||
// Enter
|
||||
if (event.keyCode == 13) {
|
||||
switch (this.show) {
|
||||
case 'delete':
|
||||
prompt.submit()
|
||||
case "delete":
|
||||
prompt.submit();
|
||||
break;
|
||||
case 'copy':
|
||||
prompt.copy(event)
|
||||
case "copy":
|
||||
prompt.copy(event);
|
||||
break;
|
||||
case 'move':
|
||||
prompt.move(event)
|
||||
case "move":
|
||||
prompt.move(event);
|
||||
break;
|
||||
case 'replace':
|
||||
prompt.showConfirm(event)
|
||||
case "replace":
|
||||
prompt.showConfirm(event);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
...mapState(['show', 'plugins']),
|
||||
...mapState(["show", "plugins"]),
|
||||
currentComponent: function () {
|
||||
const matched = [
|
||||
'info',
|
||||
'help',
|
||||
'delete',
|
||||
'rename',
|
||||
'move',
|
||||
'copy',
|
||||
'newFile',
|
||||
'newDir',
|
||||
'download',
|
||||
'replace',
|
||||
'replace-rename',
|
||||
'share',
|
||||
'upload'
|
||||
].indexOf(this.show) >= 0;
|
||||
const matched =
|
||||
[
|
||||
"info",
|
||||
"help",
|
||||
"delete",
|
||||
"rename",
|
||||
"move",
|
||||
"copy",
|
||||
"newFile",
|
||||
"newDir",
|
||||
"download",
|
||||
"replace",
|
||||
"replace-rename",
|
||||
"share",
|
||||
"upload",
|
||||
"share-delete",
|
||||
].indexOf(this.show) >= 0;
|
||||
|
||||
return matched && this.show || null;
|
||||
return (matched && this.show) || null;
|
||||
},
|
||||
showOverlay: function () {
|
||||
return (this.show !== null && this.show !== 'search' && this.show !== 'more')
|
||||
}
|
||||
return (
|
||||
this.show !== null && this.show !== "search" && this.show !== "more"
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
resetPrompts () {
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
}
|
||||
}
|
||||
resetPrompts() {
|
||||
this.$store.commit("closeHovers");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,89 +1,107 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.rename') }}</h2>
|
||||
<h2>{{ $t("prompts.rename") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.renameMessage') }} <code>{{ oldName() }}</code>:</p>
|
||||
<input class="input input--block" v-focus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||
<p>
|
||||
{{ $t("prompts.renameMessage") }} <code>{{ oldName() }}</code
|
||||
>:
|
||||
</p>
|
||||
<input
|
||||
class="input input--block"
|
||||
v-focus
|
||||
type="text"
|
||||
@keyup.enter="submit"
|
||||
v-model.trim="name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="button button--flat button--grey"
|
||||
<button
|
||||
class="button button--flat button--grey"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button @click="submit"
|
||||
:title="$t('buttons.cancel')"
|
||||
>
|
||||
{{ $t("buttons.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
@click="submit"
|
||||
class="button button--flat"
|
||||
type="submit"
|
||||
:aria-label="$t('buttons.rename')"
|
||||
:title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button>
|
||||
:title="$t('buttons.rename')"
|
||||
>
|
||||
{{ $t("buttons.rename") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from 'vuex'
|
||||
import url from '@/utils/url'
|
||||
import { files as api } from '@/api'
|
||||
import { mapState, mapGetters } from "vuex";
|
||||
import url from "@/utils/url";
|
||||
import { files as api } from "@/api";
|
||||
|
||||
export default {
|
||||
name: 'rename',
|
||||
name: "rename",
|
||||
data: function () {
|
||||
return {
|
||||
name: ''
|
||||
}
|
||||
name: "",
|
||||
};
|
||||
},
|
||||
created () {
|
||||
this.name = this.oldName()
|
||||
created() {
|
||||
this.name = this.oldName();
|
||||
},
|
||||
computed: {
|
||||
...mapState(['req', 'selected', 'selectedCount']),
|
||||
...mapGetters(['isListing'])
|
||||
...mapState(["req", "selected", "selectedCount"]),
|
||||
...mapGetters(["isListing"]),
|
||||
},
|
||||
methods: {
|
||||
cancel: function () {
|
||||
this.$store.commit('closeHovers')
|
||||
this.$store.commit("closeHovers");
|
||||
},
|
||||
oldName: function () {
|
||||
if (!this.isListing) {
|
||||
return this.req.name
|
||||
return this.req.name;
|
||||
}
|
||||
|
||||
if (this.selectedCount === 0 || this.selectedCount > 1) {
|
||||
// This shouldn't happen.
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
return this.req.items[this.selected[0]].name
|
||||
return this.req.items[this.selected[0]].name;
|
||||
},
|
||||
submit: async function () {
|
||||
let oldLink = ''
|
||||
let newLink = ''
|
||||
let oldLink = "";
|
||||
let newLink = "";
|
||||
|
||||
if (!this.isListing) {
|
||||
oldLink = this.req.url
|
||||
oldLink = this.req.url;
|
||||
} else {
|
||||
oldLink = this.req.items[this.selected[0]].url
|
||||
oldLink = this.req.items[this.selected[0]].url;
|
||||
}
|
||||
|
||||
newLink = url.removeLastDir(oldLink) + '/' + encodeURIComponent(this.name)
|
||||
newLink =
|
||||
url.removeLastDir(oldLink) + "/" + encodeURIComponent(this.name);
|
||||
|
||||
try {
|
||||
await api.move([{ from: oldLink, to: newLink }])
|
||||
await api.move([{ from: oldLink, to: newLink }]);
|
||||
if (!this.isListing) {
|
||||
this.$router.push({ path: newLink })
|
||||
return
|
||||
this.$router.push({ path: newLink });
|
||||
return;
|
||||
}
|
||||
|
||||
this.$store.commit('setReload', true)
|
||||
this.$store.commit("setReload", true);
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
this.$showError(e);
|
||||
}
|
||||
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
}
|
||||
}
|
||||
this.$store.commit("closeHovers");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,31 +1,39 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.replace') }}</h2>
|
||||
<h2>{{ $t("prompts.replace") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.replaceMessage') }}</p>
|
||||
<p>{{ $t("prompts.replaceMessage") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="button button--flat button--grey"
|
||||
<button
|
||||
class="button button--flat button--grey"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="button button--flat button--red"
|
||||
:title="$t('buttons.cancel')"
|
||||
>
|
||||
{{ $t("buttons.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
class="button button--flat button--red"
|
||||
@click="showConfirm"
|
||||
:aria-label="$t('buttons.replace')"
|
||||
:title="$t('buttons.replace')">{{ $t('buttons.replace') }}</button>
|
||||
:title="$t('buttons.replace')"
|
||||
>
|
||||
{{ $t("buttons.replace") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import { mapState } from "vuex";
|
||||
|
||||
export default {
|
||||
name: 'replace',
|
||||
computed: mapState(['showConfirm'])
|
||||
}
|
||||
name: "replace",
|
||||
computed: mapState(["showConfirm"]),
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,35 +1,47 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.replace') }}</h2>
|
||||
<h2>{{ $t("prompts.replace") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.replaceMessage') }}</p>
|
||||
<p>{{ $t("prompts.replaceMessage") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="button button--flat button--grey"
|
||||
<button
|
||||
class="button button--flat button--grey"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="button button--flat button--blue"
|
||||
:title="$t('buttons.cancel')"
|
||||
>
|
||||
{{ $t("buttons.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
class="button button--flat button--blue"
|
||||
@click="(event) => showConfirm(event, 'rename')"
|
||||
:aria-label="$t('buttons.rename')"
|
||||
:title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button>
|
||||
<button class="button button--flat button--red"
|
||||
:title="$t('buttons.rename')"
|
||||
>
|
||||
{{ $t("buttons.rename") }}
|
||||
</button>
|
||||
<button
|
||||
class="button button--flat button--red"
|
||||
@click="(event) => showConfirm(event, 'overwrite')"
|
||||
:aria-label="$t('buttons.replace')"
|
||||
:title="$t('buttons.replace')">{{ $t('buttons.replace') }}</button>
|
||||
:title="$t('buttons.replace')"
|
||||
>
|
||||
{{ $t("buttons.replace") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import { mapState } from "vuex";
|
||||
|
||||
export default {
|
||||
name: 'replace-rename',
|
||||
computed: mapState(['showConfirm'])
|
||||
}
|
||||
name: "replace-rename",
|
||||
computed: mapState(["showConfirm"]),
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,167 +1,235 @@
|
||||
<template>
|
||||
<div class="card floating" id="share">
|
||||
<div class="card floating share__promt__card" id="share">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('buttons.share') }}</h2>
|
||||
<h2>{{ $t("buttons.share") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<ul>
|
||||
<li v-if="!hasPermanent">
|
||||
<a @click="getPermalink" :aria-label="$t('buttons.permalink')">{{ $t('buttons.permalink') }}</a>
|
||||
</li>
|
||||
<template v-if="listing">
|
||||
<div class="card-content">
|
||||
<table>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>{{ $t("settings.shareDuration") }}</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
||||
<li v-for="link in links" :key="link.hash">
|
||||
<a :href="buildLink(link.hash)" target="_blank">
|
||||
<template v-if="link.expire !== 0">{{ humanTime(link.expire) }}</template>
|
||||
<template v-else>{{ $t('permanent') }}</template>
|
||||
</a>
|
||||
<tr v-for="link in links" :key="link.hash">
|
||||
<td>{{ link.hash }}</td>
|
||||
<td>
|
||||
<template v-if="link.expire !== 0">{{
|
||||
humanTime(link.expire)
|
||||
}}</template>
|
||||
<template v-else>{{ $t("permanent") }}</template>
|
||||
</td>
|
||||
<td class="small">
|
||||
<button
|
||||
class="action copy-clipboard"
|
||||
:data-clipboard-text="buildLink(link.hash)"
|
||||
:aria-label="$t('buttons.copyToClipboard')"
|
||||
:title="$t('buttons.copyToClipboard')"
|
||||
>
|
||||
<i class="material-icons">content_paste</i>
|
||||
</button>
|
||||
</td>
|
||||
<td class="small">
|
||||
<button
|
||||
class="action"
|
||||
@click="deleteLink($event, link)"
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')"
|
||||
>
|
||||
<i class="material-icons">delete</i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<button class="action"
|
||||
@click="deleteLink($event, link)"
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')"><i class="material-icons">delete</i></button>
|
||||
<div class="card-action">
|
||||
<button
|
||||
class="button button--flat button--grey"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.close')"
|
||||
:title="$t('buttons.close')"
|
||||
>
|
||||
{{ $t("buttons.close") }}
|
||||
</button>
|
||||
<button
|
||||
class="button button--flat button--blue"
|
||||
@click="() => switchListing()"
|
||||
:aria-label="$t('buttons.new')"
|
||||
:title="$t('buttons.new')"
|
||||
>
|
||||
{{ $t("buttons.new") }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<button class="action copy-clipboard"
|
||||
:data-clipboard-text="buildLink(link.hash)"
|
||||
:aria-label="$t('buttons.copyToClipboard')"
|
||||
:title="$t('buttons.copyToClipboard')"><i class="material-icons">content_paste</i></button>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<input v-focus
|
||||
<template v-else>
|
||||
<div class="card-content">
|
||||
<p>{{ $t("settings.shareDuration") }}</p>
|
||||
<div class="input-group input">
|
||||
<input
|
||||
v-focus
|
||||
type="number"
|
||||
max="2147483647"
|
||||
min="0"
|
||||
min="1"
|
||||
@keyup.enter="submit"
|
||||
v-model.trim="time">
|
||||
<select v-model="unit" :aria-label="$t('time.unit')">
|
||||
<option value="seconds">{{ $t('time.seconds') }}</option>
|
||||
<option value="minutes">{{ $t('time.minutes') }}</option>
|
||||
<option value="hours">{{ $t('time.hours') }}</option>
|
||||
<option value="days">{{ $t('time.days') }}</option>
|
||||
v-model.trim="time"
|
||||
/>
|
||||
<select class="right" v-model="unit" :aria-label="$t('time.unit')">
|
||||
<option value="seconds">{{ $t("time.seconds") }}</option>
|
||||
<option value="minutes">{{ $t("time.minutes") }}</option>
|
||||
<option value="hours">{{ $t("time.hours") }}</option>
|
||||
<option value="days">{{ $t("time.days") }}</option>
|
||||
</select>
|
||||
<button class="action"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')"><i class="material-icons">add</i></button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<p>{{ $t("prompts.optionalPassword") }}</p>
|
||||
<input
|
||||
class="input input--block"
|
||||
type="password"
|
||||
v-model.trim="password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="button button--flat"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.close')"
|
||||
:title="$t('buttons.close')">{{ $t('buttons.close') }}</button>
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<button
|
||||
class="button button--flat button--grey"
|
||||
@click="() => switchListing()"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')"
|
||||
>
|
||||
{{ $t("buttons.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
class="button button--flat button--blue"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.share')"
|
||||
:title="$t('buttons.share')"
|
||||
>
|
||||
{{ $t("buttons.share") }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from 'vuex'
|
||||
import { share as api } from '@/api'
|
||||
import { baseURL } from '@/utils/constants'
|
||||
import moment from 'moment'
|
||||
import Clipboard from 'clipboard'
|
||||
import { mapState, mapGetters } from "vuex";
|
||||
import { share as api } from "@/api";
|
||||
import { baseURL } from "@/utils/constants";
|
||||
import moment from "moment";
|
||||
import Clipboard from "clipboard";
|
||||
|
||||
export default {
|
||||
name: 'share',
|
||||
name: "share",
|
||||
data: function () {
|
||||
return {
|
||||
time: '',
|
||||
unit: 'hours',
|
||||
hasPermanent: false,
|
||||
time: "",
|
||||
unit: "hours",
|
||||
links: [],
|
||||
clip: null
|
||||
}
|
||||
clip: null,
|
||||
password: "",
|
||||
listing: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState([ 'req', 'selected', 'selectedCount' ]),
|
||||
...mapGetters([ 'isListing' ]),
|
||||
url () {
|
||||
...mapState(["req", "selected", "selectedCount"]),
|
||||
...mapGetters(["isListing"]),
|
||||
url() {
|
||||
if (!this.isListing) {
|
||||
return this.$route.path
|
||||
return this.$route.path;
|
||||
}
|
||||
|
||||
if (this.selectedCount === 0 || this.selectedCount > 1) {
|
||||
// This shouldn't happen.
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
return this.req.items[this.selected[0]].url
|
||||
}
|
||||
return this.req.items[this.selected[0]].url;
|
||||
},
|
||||
},
|
||||
async beforeMount () {
|
||||
async beforeMount() {
|
||||
try {
|
||||
const links = await api.get(this.url)
|
||||
this.links = links
|
||||
this.sort()
|
||||
const links = await api.get(this.url);
|
||||
this.links = links;
|
||||
this.sort();
|
||||
|
||||
for (let link of this.links) {
|
||||
if (link.expire === 0) {
|
||||
this.hasPermanent = true
|
||||
break
|
||||
}
|
||||
if (this.links.length == 0) {
|
||||
this.listing = false;
|
||||
}
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
this.$showError(e);
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.clip = new Clipboard('.copy-clipboard')
|
||||
this.clip.on('success', () => {
|
||||
this.$showSuccess(this.$t('success.linkCopied'))
|
||||
})
|
||||
mounted() {
|
||||
this.clip = new Clipboard(".copy-clipboard");
|
||||
this.clip.on("success", () => {
|
||||
this.$showSuccess(this.$t("success.linkCopied"));
|
||||
});
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.clip.destroy()
|
||||
beforeDestroy() {
|
||||
this.clip.destroy();
|
||||
},
|
||||
methods: {
|
||||
submit: async function () {
|
||||
if (!this.time) return
|
||||
let isPermanent = !this.time || this.time == 0;
|
||||
|
||||
try {
|
||||
const res = await api.create(this.url, this.time, this.unit)
|
||||
this.links.push(res)
|
||||
this.sort()
|
||||
let res = null;
|
||||
|
||||
if (isPermanent) {
|
||||
res = await api.create(this.url, this.password);
|
||||
} else {
|
||||
res = await api.create(this.url, this.password, this.time, this.unit);
|
||||
}
|
||||
|
||||
this.links.push(res);
|
||||
this.sort();
|
||||
|
||||
this.time = "";
|
||||
this.unit = "hours";
|
||||
this.password = "";
|
||||
|
||||
this.listing = true;
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
}
|
||||
},
|
||||
getPermalink: async function () {
|
||||
try {
|
||||
const res = await api.create(this.url)
|
||||
this.links.push(res)
|
||||
this.sort()
|
||||
this.hasPermanent = true
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
this.$showError(e);
|
||||
}
|
||||
},
|
||||
deleteLink: async function (event, link) {
|
||||
event.preventDefault()
|
||||
try {
|
||||
await api.remove(link.hash)
|
||||
if (link.expire === 0) this.hasPermanent = false
|
||||
this.links = this.links.filter(item => item.hash !== link.hash)
|
||||
event.preventDefault();
|
||||
try {
|
||||
await api.remove(link.hash);
|
||||
this.links = this.links.filter((item) => item.hash !== link.hash);
|
||||
|
||||
if (this.links.length == 0) {
|
||||
this.listing = false;
|
||||
}
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
this.$showError(e);
|
||||
}
|
||||
},
|
||||
humanTime (time) {
|
||||
return moment(time * 1000).fromNow()
|
||||
humanTime(time) {
|
||||
return moment(time * 1000).fromNow();
|
||||
},
|
||||
buildLink (hash) {
|
||||
return `${window.location.origin}${baseURL}/share/${hash}`
|
||||
buildLink(hash) {
|
||||
return `${window.location.origin}${baseURL}/share/${hash}`;
|
||||
},
|
||||
sort () {
|
||||
sort() {
|
||||
this.links = this.links.sort((a, b) => {
|
||||
if (a.expire === 0) return -1
|
||||
if (b.expire === 0) return 1
|
||||
return new Date(a.expire) - new Date(b.expire)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
if (a.expire === 0) return -1;
|
||||
if (b.expire === 0) return 1;
|
||||
return new Date(a.expire) - new Date(b.expire);
|
||||
});
|
||||
},
|
||||
switchListing() {
|
||||
if (this.links.length == 0 && !this.listing) {
|
||||
this.$store.commit("closeHovers");
|
||||
}
|
||||
|
||||
this.listing = !this.listing;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
41
frontend/src/components/prompts/ShareDelete.vue
Normal file
41
frontend/src/components/prompts/ShareDelete.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-content">
|
||||
<p>{{ $t("prompts.deleteMessageShare", { path: "" }) }}</p>
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<button
|
||||
@click="$store.commit('closeHovers')"
|
||||
class="button button--flat button--grey"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')"
|
||||
>
|
||||
{{ $t("buttons.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
@click="submit"
|
||||
class="button button--flat button--red"
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')"
|
||||
>
|
||||
{{ $t("buttons.delete") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from "vuex";
|
||||
|
||||
export default {
|
||||
name: "share-delete",
|
||||
computed: {
|
||||
...mapState(["showConfirm"]),
|
||||
},
|
||||
methods: {
|
||||
submit: function () {
|
||||
this.showConfirm();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.upload') }}</h2>
|
||||
<h2>{{ $t("prompts.upload") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.uploadMessage') }}</p>
|
||||
<p>{{ $t("prompts.uploadMessage") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="card-action full">
|
||||
@@ -22,18 +22,17 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'upload',
|
||||
name: "upload",
|
||||
methods: {
|
||||
uploadFile: function () {
|
||||
document.getElementById('upload-input').value = ''
|
||||
document.getElementById('upload-input').click()
|
||||
document.getElementById("upload-input").value = "";
|
||||
document.getElementById("upload-input").click();
|
||||
},
|
||||
uploadFolder: function () {
|
||||
document.getElementById('upload-folder-input').value = ''
|
||||
document.getElementById('upload-folder-input').click()
|
||||
}
|
||||
}
|
||||
}
|
||||
document.getElementById("upload-folder-input").value = "";
|
||||
document.getElementById("upload-folder-input").click();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,28 +1,30 @@
|
||||
<template>
|
||||
<div>
|
||||
<h3>{{ $t('settings.userCommands') }}</h3>
|
||||
<p class="small">{{ $t('settings.userCommandsHelp') }} <i>git svn hg</i>.</p>
|
||||
<input class="input input--block" type="text" v-model.trim="raw">
|
||||
<h3>{{ $t("settings.userCommands") }}</h3>
|
||||
<p class="small">
|
||||
{{ $t("settings.userCommandsHelp") }} <i>git svn hg</i>.
|
||||
</p>
|
||||
<input class="input input--block" type="text" v-model.trim="raw" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'permissions',
|
||||
props: ['commands'],
|
||||
name: "permissions",
|
||||
props: ["commands"],
|
||||
computed: {
|
||||
raw: {
|
||||
get () {
|
||||
return this.commands.join(' ')
|
||||
get() {
|
||||
return this.commands.join(" ");
|
||||
},
|
||||
set (value) {
|
||||
if (value !== '') {
|
||||
this.$emit('update:commands', value.split(' '))
|
||||
set(value) {
|
||||
if (value !== "") {
|
||||
this.$emit("update:commands", value.split(" "));
|
||||
} else {
|
||||
this.$emit('update:commands', [])
|
||||
this.$emit("update:commands", []);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,46 +1,50 @@
|
||||
<template>
|
||||
<select v-on:change="change" :value="locale">
|
||||
<option v-for="(language, value) in locales" :key="value" :value="value">{{ $t('languages.' + language) }}</option>
|
||||
<option v-for="(language, value) in locales" :key="value" :value="value">
|
||||
{{ $t("languages." + language) }}
|
||||
</option>
|
||||
</select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'languages',
|
||||
props: [ 'locale' ],
|
||||
name: "languages",
|
||||
props: ["locale"],
|
||||
data() {
|
||||
let dataObj = {
|
||||
locales: {
|
||||
ar: 'ar',
|
||||
de: 'de',
|
||||
en: 'en',
|
||||
es: 'es',
|
||||
fr: 'fr',
|
||||
is: 'is',
|
||||
it: 'it',
|
||||
ja: 'ja',
|
||||
ko: 'ko',
|
||||
'nl-be': 'nlBE',
|
||||
pl: 'pl',
|
||||
'pt-br': 'ptBR',
|
||||
pt: 'pt',
|
||||
ro: 'ro',
|
||||
ru: 'ru',
|
||||
'sv-se': 'svSE',
|
||||
'zh-cn': 'zhCN',
|
||||
'zh-tw': 'zhTW'
|
||||
}
|
||||
ar: "ar",
|
||||
de: "de",
|
||||
en: "en",
|
||||
es: "es",
|
||||
fr: "fr",
|
||||
is: "is",
|
||||
it: "it",
|
||||
ja: "ja",
|
||||
ko: "ko",
|
||||
"nl-be": "nlBE",
|
||||
pl: "pl",
|
||||
"pt-br": "ptBR",
|
||||
pt: "pt",
|
||||
ro: "ro",
|
||||
ru: "ru",
|
||||
"sv-se": "svSE",
|
||||
"zh-cn": "zhCN",
|
||||
"zh-tw": "zhTW",
|
||||
},
|
||||
};
|
||||
|
||||
Object.defineProperty(dataObj, "locales", { configurable: false, writable: false });
|
||||
Object.defineProperty(dataObj, "locales", {
|
||||
configurable: false,
|
||||
writable: false,
|
||||
});
|
||||
|
||||
return dataObj;
|
||||
},
|
||||
methods: {
|
||||
change (event) {
|
||||
this.$emit('update:locale', event.target.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
change(event) {
|
||||
this.$emit("update:locale", event.target.value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,41 +1,65 @@
|
||||
<template>
|
||||
<div>
|
||||
<h3>{{ $t('settings.permissions') }}</h3>
|
||||
<p class="small">{{ $t('settings.permissionsHelp') }}</p>
|
||||
<h3>{{ $t("settings.permissions") }}</h3>
|
||||
<p class="small">{{ $t("settings.permissionsHelp") }}</p>
|
||||
|
||||
<p><input type="checkbox" v-model="admin"> {{ $t('settings.administrator') }}</p>
|
||||
<p>
|
||||
<input type="checkbox" v-model="admin" />
|
||||
{{ $t("settings.administrator") }}
|
||||
</p>
|
||||
|
||||
<p><input type="checkbox" :disabled="admin" v-model="perm.create"> {{ $t('settings.perm.create') }}</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="perm.delete"> {{ $t('settings.perm.delete') }}</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="perm.download"> {{ $t('settings.perm.download') }}</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="perm.modify"> {{ $t('settings.perm.modify') }}</p>
|
||||
<p v-if="isExecEnabled"><input type="checkbox" :disabled="admin" v-model="perm.execute"> {{ $t('settings.perm.execute') }}</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="perm.rename"> {{ $t('settings.perm.rename') }}</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="perm.share"> {{ $t('settings.perm.share') }}</p>
|
||||
<p>
|
||||
<input type="checkbox" :disabled="admin" v-model="perm.create" />
|
||||
{{ $t("settings.perm.create") }}
|
||||
</p>
|
||||
<p>
|
||||
<input type="checkbox" :disabled="admin" v-model="perm.delete" />
|
||||
{{ $t("settings.perm.delete") }}
|
||||
</p>
|
||||
<p>
|
||||
<input type="checkbox" :disabled="admin" v-model="perm.download" />
|
||||
{{ $t("settings.perm.download") }}
|
||||
</p>
|
||||
<p>
|
||||
<input type="checkbox" :disabled="admin" v-model="perm.modify" />
|
||||
{{ $t("settings.perm.modify") }}
|
||||
</p>
|
||||
<p v-if="isExecEnabled">
|
||||
<input type="checkbox" :disabled="admin" v-model="perm.execute" />
|
||||
{{ $t("settings.perm.execute") }}
|
||||
</p>
|
||||
<p>
|
||||
<input type="checkbox" :disabled="admin" v-model="perm.rename" />
|
||||
{{ $t("settings.perm.rename") }}
|
||||
</p>
|
||||
<p>
|
||||
<input type="checkbox" :disabled="admin" v-model="perm.share" />
|
||||
{{ $t("settings.perm.share") }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { enableExec } from '@/utils/constants'
|
||||
import { enableExec } from "@/utils/constants";
|
||||
export default {
|
||||
name: 'permissions',
|
||||
props: ['perm'],
|
||||
name: "permissions",
|
||||
props: ["perm"],
|
||||
computed: {
|
||||
admin: {
|
||||
get () {
|
||||
return this.perm.admin
|
||||
get() {
|
||||
return this.perm.admin;
|
||||
},
|
||||
set (value) {
|
||||
set(value) {
|
||||
if (value) {
|
||||
for (const key in this.perm) {
|
||||
this.perm[key] = true
|
||||
this.perm[key] = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.perm.admin = value
|
||||
}
|
||||
this.perm.admin = value;
|
||||
},
|
||||
},
|
||||
isExecEnabled: () => enableExec
|
||||
}
|
||||
}
|
||||
isExecEnabled: () => enableExec,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,57 +1,63 @@
|
||||
<template>
|
||||
<form class="rules small">
|
||||
<div v-for="(rule, index) in rules" :key="index">
|
||||
<input type="checkbox" v-model="rule.regex"><label>Regex</label>
|
||||
<input type="checkbox" v-model="rule.allow"><label>Allow</label>
|
||||
<input type="checkbox" v-model="rule.regex" /><label>Regex</label>
|
||||
<input type="checkbox" v-model="rule.allow" /><label>Allow</label>
|
||||
|
||||
<input
|
||||
@keypress.enter.prevent
|
||||
type="text"
|
||||
v-if="rule.regex"
|
||||
v-model="rule.regexp.raw"
|
||||
:placeholder="$t('settings.insertRegex')" />
|
||||
:placeholder="$t('settings.insertRegex')"
|
||||
/>
|
||||
<input
|
||||
@keypress.enter.prevent
|
||||
type="text"
|
||||
v-else
|
||||
v-model="rule.path"
|
||||
:placeholder="$t('settings.insertPath')" />
|
||||
:placeholder="$t('settings.insertPath')"
|
||||
/>
|
||||
|
||||
<button class="button button--red" @click="remove($event, index)">-</button>
|
||||
<button class="button button--red" @click="remove($event, index)">
|
||||
-
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="button" @click="create" default="false">{{ $t('buttons.new') }}</button>
|
||||
<button class="button" @click="create" default="false">
|
||||
{{ $t("buttons.new") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'rules-textarea',
|
||||
props: ['rules'],
|
||||
name: "rules-textarea",
|
||||
props: ["rules"],
|
||||
methods: {
|
||||
remove (event, index) {
|
||||
event.preventDefault()
|
||||
let rules = [ ...this.rules ]
|
||||
rules.splice(index, 1)
|
||||
this.$emit('update:rules', [ ...rules ])
|
||||
remove(event, index) {
|
||||
event.preventDefault();
|
||||
let rules = [...this.rules];
|
||||
rules.splice(index, 1);
|
||||
this.$emit("update:rules", [...rules]);
|
||||
},
|
||||
create (event) {
|
||||
event.preventDefault()
|
||||
create(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.$emit('update:rules', [
|
||||
this.$emit("update:rules", [
|
||||
...this.rules,
|
||||
{
|
||||
allow: true,
|
||||
path: '',
|
||||
path: "",
|
||||
regex: false,
|
||||
regexp: {
|
||||
raw: ''
|
||||
}
|
||||
}
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
raw: "",
|
||||
},
|
||||
},
|
||||
]);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<template>
|
||||
<select v-on:change="change" :value="theme">
|
||||
<option value="">{{ $t('settings.themes.light') }}</option>
|
||||
<option value="dark">{{ $t('settings.themes.dark') }}</option>
|
||||
<option value="">{{ $t("settings.themes.light") }}</option>
|
||||
<option value="dark">{{ $t("settings.themes.dark") }}</option>
|
||||
</select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'themes',
|
||||
props: [ 'theme' ],
|
||||
name: "themes",
|
||||
props: ["theme"],
|
||||
methods: {
|
||||
change (event) {
|
||||
this.$emit('update:theme', event.target.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
change(event) {
|
||||
this.$emit("update:theme", event.target.value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,71 +1,92 @@
|
||||
<template>
|
||||
<div>
|
||||
<p v-if="!isDefault">
|
||||
<label for="username">{{ $t('settings.username') }}</label>
|
||||
<input class="input input--block" type="text" v-model="user.username" id="username">
|
||||
<label for="username">{{ $t("settings.username") }}</label>
|
||||
<input
|
||||
class="input input--block"
|
||||
type="text"
|
||||
v-model="user.username"
|
||||
id="username"
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p v-if="!isDefault">
|
||||
<label for="password">{{ $t('settings.password') }}</label>
|
||||
<input class="input input--block" type="password" :placeholder="passwordPlaceholder" v-model="user.password" id="password">
|
||||
<label for="password">{{ $t("settings.password") }}</label>
|
||||
<input
|
||||
class="input input--block"
|
||||
type="password"
|
||||
:placeholder="passwordPlaceholder"
|
||||
v-model="user.password"
|
||||
id="password"
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="scope">{{ $t('settings.scope') }}</label>
|
||||
<input class="input input--block" type="text" v-model="user.scope" id="scope">
|
||||
<label for="scope">{{ $t("settings.scope") }}</label>
|
||||
<input
|
||||
class="input input--block"
|
||||
type="text"
|
||||
v-model="user.scope"
|
||||
id="scope"
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="locale">{{ $t('settings.language') }}</label>
|
||||
<languages class="input input--block" id="locale" :locale.sync="user.locale"></languages>
|
||||
<label for="locale">{{ $t("settings.language") }}</label>
|
||||
<languages
|
||||
class="input input--block"
|
||||
id="locale"
|
||||
:locale.sync="user.locale"
|
||||
></languages>
|
||||
</p>
|
||||
|
||||
<p v-if="!isDefault">
|
||||
<input type="checkbox" :disabled="user.perm.admin" v-model="user.lockPassword"> {{ $t('settings.lockPassword') }}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<input type="checkbox" v-model="user.singleClick"> {{ $t('settings.singleClick') }}
|
||||
<input
|
||||
type="checkbox"
|
||||
:disabled="user.perm.admin"
|
||||
v-model="user.lockPassword"
|
||||
/>
|
||||
{{ $t("settings.lockPassword") }}
|
||||
</p>
|
||||
|
||||
<permissions :perm.sync="user.perm" />
|
||||
<commands v-if="isExecEnabled" :commands.sync="user.commands" />
|
||||
|
||||
<div v-if="!isDefault">
|
||||
<h3>{{ $t('settings.rules') }}</h3>
|
||||
<p class="small">{{ $t('settings.rulesHelp') }}</p>
|
||||
<h3>{{ $t("settings.rules") }}</h3>
|
||||
<p class="small">{{ $t("settings.rulesHelp") }}</p>
|
||||
<rules :rules.sync="user.rules" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Languages from './Languages'
|
||||
import Rules from './Rules'
|
||||
import Permissions from './Permissions'
|
||||
import Commands from './Commands'
|
||||
import { enableExec } from '@/utils/constants'
|
||||
import Languages from "./Languages";
|
||||
import Rules from "./Rules";
|
||||
import Permissions from "./Permissions";
|
||||
import Commands from "./Commands";
|
||||
import { enableExec } from "@/utils/constants";
|
||||
|
||||
export default {
|
||||
name: 'user',
|
||||
name: "user",
|
||||
components: {
|
||||
Permissions,
|
||||
Languages,
|
||||
Rules,
|
||||
Commands
|
||||
Commands,
|
||||
},
|
||||
props: [ 'user', 'isNew', 'isDefault' ],
|
||||
props: ["user", "isNew", "isDefault"],
|
||||
computed: {
|
||||
passwordPlaceholder () {
|
||||
return this.isNew ? '' : this.$t('settings.avoidChanges')
|
||||
passwordPlaceholder() {
|
||||
return this.isNew ? "" : this.$t("settings.avoidChanges");
|
||||
},
|
||||
isExecEnabled: () => enableExec
|
||||
isExecEnabled: () => enableExec,
|
||||
},
|
||||
watch: {
|
||||
'user.perm.admin': function () {
|
||||
if (!this.user.perm.admin) return
|
||||
this.user.lockPassword = false
|
||||
}
|
||||
}
|
||||
}
|
||||
"user.perm.admin": function () {
|
||||
if (!this.user.perm.admin) return;
|
||||
this.user.lockPassword = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
}
|
||||
|
||||
.share__box__items #listing.list .item {
|
||||
cursor: auto;
|
||||
cursor: pointer;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
border-bottom: 0;
|
||||
@@ -57,5 +57,17 @@
|
||||
}
|
||||
|
||||
.share__box__items #listing.list .item .name {
|
||||
width: auto;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.share__box__items #listing.list .item .modified {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.share__wrong__password {
|
||||
background: var(--red);
|
||||
color: #fff;
|
||||
padding: .5em;
|
||||
text-align: center;
|
||||
animation: .2s opac forwards;
|
||||
}
|
||||
@@ -83,29 +83,29 @@ main {
|
||||
width: calc(100% - 19em);
|
||||
}
|
||||
|
||||
#breadcrumbs {
|
||||
.breadcrumbs {
|
||||
height: 3em;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
#breadcrumbs span,
|
||||
#breadcrumbs {
|
||||
.breadcrumbs span,
|
||||
.breadcrumbs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #6f6f6f;
|
||||
}
|
||||
|
||||
#breadcrumbs a {
|
||||
.breadcrumbs a {
|
||||
color: inherit;
|
||||
transition: .1s ease-in;
|
||||
border-radius: .125em;
|
||||
}
|
||||
|
||||
#breadcrumbs a:hover {
|
||||
.breadcrumbs a:hover {
|
||||
background-color: rgba(0,0,0, 0.05);
|
||||
}
|
||||
|
||||
#breadcrumbs span a {
|
||||
.breadcrumbs span a {
|
||||
padding: .2em;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,29 @@
|
||||
.dashboard {
|
||||
max-width: 600px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.dashboard .row {
|
||||
display: flex;
|
||||
margin: 0 -.5em;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dashboard .row .column {
|
||||
display: flex;
|
||||
padding: 0 .5em;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.dashboard .row .column .card {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@media(max-width: 1200px) {
|
||||
.dashboard .row .column {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit
|
||||
}
|
||||
@@ -28,25 +49,56 @@ p code {
|
||||
}
|
||||
|
||||
.dashboard #nav {
|
||||
display: flex;
|
||||
padding-bottom: 1em;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.dashboard #nav .wrapper {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
border-bottom: 2px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.dashboard #nav ul {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
color: rgb(84, 110, 122);
|
||||
font-weight: 500;
|
||||
margin: 0 0 1em;
|
||||
padding: 0;
|
||||
margin: 0 0 -2px 0;
|
||||
font-size: .8em;
|
||||
text-align: center;
|
||||
justify-content: space-between;
|
||||
padding: 0;
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.dashboard #nav li {
|
||||
.dashboard #nav ul li {
|
||||
position: relative;
|
||||
padding: 1.5em 2em;
|
||||
white-space: nowrap;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: .1s ease-in-out all;
|
||||
|
||||
}
|
||||
|
||||
.dashboard #nav ul li:hover {
|
||||
background: var(--moon-grey);
|
||||
}
|
||||
|
||||
.dashboard #nav ul li.active {
|
||||
border-color: var(--blue);
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.dashboard #nav ul li.active::before {
|
||||
width: 100%;
|
||||
padding: 0 0 1em;
|
||||
border-bottom: 2px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.dashboard #nav li.active {
|
||||
border-color: var(--blue)
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
content: "";
|
||||
background: var(--blue);
|
||||
opacity: 0.08;
|
||||
}
|
||||
|
||||
.dashboard #nav i {
|
||||
@@ -92,7 +144,7 @@ table tr>*:last-child {
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
margin: .5rem 0 1rem 0;
|
||||
margin: 0 0 1rem 0;
|
||||
background-color: #fff;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
|
||||
@@ -151,6 +203,7 @@ table tr>*:last-child {
|
||||
|
||||
.card .card-content.full {
|
||||
padding-bottom: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
@@ -226,6 +279,18 @@ table tr>*:last-child {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.card#share .input-group {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.card#share .input-group * {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.card#share .input-group input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
position: fixed;
|
||||
|
||||
@@ -6,9 +6,25 @@ header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 4em;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
padding: 0.5em 0.5em 0.5em 1em;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header > * {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
header title {
|
||||
display: block;
|
||||
flex: 1 1 auto;
|
||||
padding: 0 1em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
header .overlay {
|
||||
@@ -30,17 +46,6 @@ header img {
|
||||
height: 2.5em;
|
||||
}
|
||||
|
||||
header>div:first-child>.action {
|
||||
display: none;
|
||||
}
|
||||
|
||||
header>div {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 0.5em 0.5em 0.5em 1em;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header .action span {
|
||||
display: none;
|
||||
}
|
||||
@@ -50,19 +55,8 @@ header>div div {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
header>div:last-child div {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
header>div:first-child {
|
||||
height: 4em;
|
||||
}
|
||||
|
||||
header>div:last-child {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
header .search-button {
|
||||
header .search-button,
|
||||
header .menu-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
|
||||
width: 95%;
|
||||
max-width: 20em;
|
||||
z-index: 1;
|
||||
}
|
||||
#file-selection .action {
|
||||
border-radius: 50%;
|
||||
@@ -81,6 +82,9 @@
|
||||
color: #6f6f6f;
|
||||
margin-right: auto;
|
||||
}
|
||||
#file-selection .action span {
|
||||
display: none;
|
||||
}
|
||||
nav {
|
||||
top: 0;
|
||||
z-index: 99999;
|
||||
@@ -95,7 +99,7 @@
|
||||
left: 0;
|
||||
}
|
||||
header .search-button,
|
||||
header>div:first-child>.action {
|
||||
header .menu-button {
|
||||
display: inherit;
|
||||
}
|
||||
header img {
|
||||
|
||||
@@ -96,10 +96,11 @@
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
font-size: .75em;
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
width: 1.8em;
|
||||
height: 1.8em;
|
||||
text-align: center;
|
||||
line-height: 1.25em;
|
||||
line-height: 1.55em;
|
||||
font-weight: bold;
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
@@ -117,45 +118,33 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#previewer .bar {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
padding: 0.5em;
|
||||
height: 3.7em;
|
||||
}
|
||||
|
||||
#previewer .bar > * {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
#previewer .bar .title {
|
||||
display: block;
|
||||
flex: 1 1 auto;
|
||||
padding: 0 1em;
|
||||
line-height: 2.3em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 1.2em;
|
||||
#previewer header {
|
||||
background: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#previewer .action i {
|
||||
#previewer header > .action i {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#previewer .action:hover {
|
||||
@media (min-width: 738px) {
|
||||
#previewer header #dropdown .action i {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
#previewer header .action:hover {
|
||||
background-color: rgba(255, 255, 255, 0.3)
|
||||
}
|
||||
|
||||
#previewer .action span {
|
||||
#previewer header .action span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#previewer .preview {
|
||||
margin: 2em auto 4em;
|
||||
max-width: 80%;
|
||||
margin-top: 4em;
|
||||
text-align: center;
|
||||
height: calc(100vh - 9.7em);
|
||||
height: calc(100vh - 4em);
|
||||
}
|
||||
|
||||
#previewer .preview pre {
|
||||
@@ -170,6 +159,10 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#previewer .preview video {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#previewer .pdf {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -182,8 +175,25 @@
|
||||
#previewer>button {
|
||||
margin: 0;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
top: calc(50% + 1.85em);
|
||||
transform: translateY(-50%);
|
||||
background-color: rgba(80, 80, 80, .5);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
transition: 0.2s ease all;
|
||||
}
|
||||
|
||||
#previewer>button.hidden {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#previewer>button>i {
|
||||
padding: 0.4em;
|
||||
}
|
||||
|
||||
#previewer>button:first-of-type {
|
||||
@@ -199,6 +209,7 @@
|
||||
#editor-container {
|
||||
background-color: #fafafa;
|
||||
position: fixed;
|
||||
margin-top: 4em;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
@@ -206,43 +217,28 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#editor-container .bar {
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
padding: 0.5em;
|
||||
height: 3.7em;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.075);
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
#editor-container .title {
|
||||
margin-right: auto;
|
||||
padding: 0 1em;
|
||||
line-height: 2.7em;
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
#previewer .loading {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#editor-container #editor {
|
||||
height: calc(100vh - 8.2em);
|
||||
height: calc(100vh - 8.4em);
|
||||
}
|
||||
|
||||
#editor-container #breadcrumbs {
|
||||
#editor-container .breadcrumbs {
|
||||
height: 2.3em;
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
#editor-container #breadcrumbs span {
|
||||
#editor-container .breadcrumbs span {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#editor-container .breadcrumbs i {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* * * * * * * * * * * * * * * *
|
||||
* PROMPT *
|
||||
* * * * * * * * * * * * * * * */
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{
|
||||
"permanent": "دائم",
|
||||
"buttons": {
|
||||
"shell": "Toggle shell",
|
||||
"cancel": "إلغاء",
|
||||
"close": "إغلاق",
|
||||
"copy": "نسخ",
|
||||
@@ -10,6 +8,7 @@
|
||||
"create": "إنشاء",
|
||||
"delete": "حذف",
|
||||
"download": "تحميل",
|
||||
"hideDotfiles": "",
|
||||
"info": "معلومات",
|
||||
"more": "المزيد",
|
||||
"move": "نقل",
|
||||
@@ -17,25 +16,28 @@
|
||||
"new": "جديد",
|
||||
"next": "التالي",
|
||||
"ok": "موافق",
|
||||
"replace": "استبدال",
|
||||
"permalink": "الحصول على لنك دائم",
|
||||
"previous": "السابق",
|
||||
"publish": "نشر",
|
||||
"rename": "إعادة تسمية",
|
||||
"replace": "استبدال",
|
||||
"reportIssue": "إبلاغ عن مشكلة",
|
||||
"save": "حفظ",
|
||||
"schedule": "جدولة",
|
||||
"search": "بحث",
|
||||
"select": "تحديد",
|
||||
"share": "مشاركة",
|
||||
"publish": "نشر",
|
||||
"selectMultiple": "تحديد متعدد",
|
||||
"schedule": "جدولة",
|
||||
"share": "مشاركة",
|
||||
"shell": "Toggle shell",
|
||||
"switchView": "تغيير العرض",
|
||||
"toggleSidebar": "تبديل الشريط الجانبي",
|
||||
"update": "تحديث",
|
||||
"upload": "رفع",
|
||||
"permalink": "الحصول على لنك دائم"
|
||||
"upload": "رفع"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "تم نسخ الملف"
|
||||
"download": {
|
||||
"downloadFile": "Download File",
|
||||
"downloadFolder": "Download Folder",
|
||||
"downloadSelected": ""
|
||||
},
|
||||
"errors": {
|
||||
"forbidden": "You don't have permissions to access this.",
|
||||
@@ -43,11 +45,11 @@
|
||||
"notFound": "لا يمكن الوصول لهذا المحتوى."
|
||||
},
|
||||
"files": {
|
||||
"folders": "المجلدات",
|
||||
"files": "الملفات",
|
||||
"body": "الصفحة",
|
||||
"clear": "مسح",
|
||||
"closePreview": "إغلاق العرض",
|
||||
"files": "الملفات",
|
||||
"folders": "المجلدات",
|
||||
"home": "الصفحة الاولى",
|
||||
"lastModified": "آخر تعديل",
|
||||
"loading": "جاري التحميل...",
|
||||
@@ -56,9 +58,9 @@
|
||||
"multipleSelectionEnabled": "التحديد المتعدد مفعل",
|
||||
"name": "الإسم",
|
||||
"size": "الحجم",
|
||||
"sortByLastModified": "الترتيب بآخر تعديل",
|
||||
"sortByName": "الترتيب بالإسم",
|
||||
"sortBySize": "الترتيب بالحجم",
|
||||
"sortByLastModified": "الترتيب بآخر تعديل"
|
||||
"sortBySize": "الترتيب بالحجم"
|
||||
},
|
||||
"help": {
|
||||
"click": "حدد الملف أو المجلد",
|
||||
@@ -74,18 +76,39 @@
|
||||
"f2": "إعادة تسمية الملف",
|
||||
"help": "مساعدة"
|
||||
},
|
||||
"languages": {
|
||||
"ar": "العربية",
|
||||
"de": "Deutsch",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"fr": "Français",
|
||||
"is": "",
|
||||
"it": "Italiano",
|
||||
"ja": "日本語",
|
||||
"ko": "한국어",
|
||||
"nlBE": "",
|
||||
"pl": "Polski",
|
||||
"pt": "Português",
|
||||
"ptBR": "Português (Brasil)",
|
||||
"ro": "",
|
||||
"ru": "Русский",
|
||||
"svSE": "",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)"
|
||||
},
|
||||
"login": {
|
||||
"password": "كلمة المرور",
|
||||
"passwordConfirm": "Password Confirmation",
|
||||
"submit": "تسجيل دخول",
|
||||
"createAnAccount": "Create an account",
|
||||
"loginInstead": "Already have an account",
|
||||
"password": "كلمة المرور",
|
||||
"passwordConfirm": "Password Confirmation",
|
||||
"passwordsDontMatch": "Passwords don't match",
|
||||
"usernameTaken": "Username already taken",
|
||||
"signup": "Signup",
|
||||
"submit": "تسجيل دخول",
|
||||
"username": "إسم المستخدم",
|
||||
"usernameTaken": "Username already taken",
|
||||
"wrongCredentials": "بيانات دخول خاطئة"
|
||||
},
|
||||
"permanent": "دائم",
|
||||
"prompts": {
|
||||
"copy": "نسخ",
|
||||
"copyMessage": "رجاء حدد المكان لنسخ ملفاتك فيه:",
|
||||
@@ -102,43 +125,64 @@
|
||||
"lastModified": "آخر تعديل",
|
||||
"move": "نقل",
|
||||
"moveMessage": "إختر مكان جديد للملفات أو المجلدات المراد نقلها:",
|
||||
"newArchetype": "إنشاء منشور من المنشور الأصلي. الملف سيتم انشاءه في مجلد المحتويات.",
|
||||
"newDir": "مجلد جديد",
|
||||
"newDirMessage": "رجاء أدخل اسم المجلد الجديد.",
|
||||
"newFile": "ملف جديد",
|
||||
"newFileMessage": "رجاء ادخل اسم الملف الجديد.",
|
||||
"numberDirs": "عدد المجلدات",
|
||||
"numberFiles": "عدد الملفات",
|
||||
"replace": "إستبدال",
|
||||
"replaceMessage": "أحد الملفات التي تحاول رفعها يتعارض مع ملف موجود بنفس الإسم. هل تريد إستبدال الملف الموجود؟\n",
|
||||
"rename": "إعادة تسمية",
|
||||
"renameMessage": "إدراج اسم جديد لـ",
|
||||
"show": "عرض",
|
||||
"size": "الحجم",
|
||||
"replace": "إستبدال",
|
||||
"replaceMessage": "أحد الملفات التي تحاول رفعها يتعارض مع ملف موجود بنفس الإسم. هل تريد إستبدال الملف الموجود؟\n",
|
||||
"schedule": "جدولة",
|
||||
"scheduleMessage": "أختر الوقت والتاريخ لجدولة نشر هذا المقال.",
|
||||
"newArchetype": "إنشاء منشور من المنشور الأصلي. الملف سيتم انشاءه في مجلد المحتويات."
|
||||
"show": "عرض",
|
||||
"size": "الحجم",
|
||||
"upload": "",
|
||||
"uploadMessage": ""
|
||||
},
|
||||
"search": {
|
||||
"images": "الصور",
|
||||
"music": "الموسيقى",
|
||||
"pdf": "PDF",
|
||||
"pressToSearch": "Press enter to search...",
|
||||
"search": "البحث...",
|
||||
"typeToSearch": "Type to search...",
|
||||
"types": "الأنواع",
|
||||
"video": "فيديوهات"
|
||||
},
|
||||
"settings": {
|
||||
"instanceName": "Instance name",
|
||||
"brandingDirectoryPath": "Branding directory path",
|
||||
"documentation": "documentation",
|
||||
"branding": "Branding",
|
||||
"disableExternalLinks": "Disable external links (except documentation)",
|
||||
"brandingHelp": "You can costumize how your File Browser instance looks and feels by changing its name, replacing the logo, adding custom styles and even disable external links to GitHub.\nFor more information about custom branding, please check out the {0}.",
|
||||
"admin": "Admin",
|
||||
"administrator": "Administrator",
|
||||
"allowCommands": "تنفيذ الأوامر",
|
||||
"allowEdit": "تعديل، إعادة تسمية وحذف الملفات والمجلدات",
|
||||
"allowNew": "إنشاء ملفات ومجلدات جديدة",
|
||||
"allowPublish": "نشر مقالات وصفحات جديدة",
|
||||
"allowSignup": "Allow users to signup",
|
||||
"avoidChanges": "(أتركه فارغاً إن لم ترد تغييره)",
|
||||
"branding": "Branding",
|
||||
"brandingDirectoryPath": "Branding directory path",
|
||||
"brandingHelp": "You can customize how your File Browser instance looks and feels by changing its name, replacing the logo, adding custom styles and even disable external links to GitHub.\nFor more information about custom branding, please check out the {0}.",
|
||||
"changePassword": "تغيير كلمة المرور",
|
||||
"commandRunner": "Command runner",
|
||||
"commandRunnerHelp": "Here you can set commands that are executed in the named events. You must write one per line. The environment variables {0} and {1} will be available, being {0} relative to {1}. For more information about this feature and the available environment variables, please read the {2}.",
|
||||
"commandsUpdated": "تم تحديث الأوامر",
|
||||
"createUserDir": "Auto create user home dir while adding new user",
|
||||
"customStylesheet": "ستايل مخصص",
|
||||
"defaultUserDescription": "This are the default settings for new users.",
|
||||
"disableExternalLinks": "Disable external links (except documentation)",
|
||||
"documentation": "documentation",
|
||||
"examples": "أمثلة",
|
||||
"executeOnShell": "Execute on shell",
|
||||
"executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you want to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This apply to both user commands and event hooks.",
|
||||
"globalRules": "This is a global set of allow and disallow rules. They apply to every user. You can define specific rules on each user's settings to override this ones.",
|
||||
"globalSettings": "إعدادات عامة",
|
||||
"hideDotfiles": "",
|
||||
"insertPath": "Insert the path",
|
||||
"insertRegex": "Insert regex expression",
|
||||
"instanceName": "Instance name",
|
||||
"language": "اللغة",
|
||||
"lockPassword": "منع المستخدم من تغيير كلمة المرور",
|
||||
"newPassword": "كلمة المرور الجديدة",
|
||||
@@ -146,6 +190,16 @@
|
||||
"newUser": "مستخدم جديد",
|
||||
"password": "كلمة المرور",
|
||||
"passwordUpdated": "تم تغيير كلمة المرور",
|
||||
"path": "",
|
||||
"perm": {
|
||||
"create": "Create files and directories",
|
||||
"delete": "Delete files and directories",
|
||||
"download": "Download",
|
||||
"execute": "Execute commands",
|
||||
"modify": "Edit files",
|
||||
"rename": "Rename or move files and directories",
|
||||
"share": "Share files"
|
||||
},
|
||||
"permissions": "الصلاحيات",
|
||||
"permissionsHelp": "يمكنك تعيين المستخدم كـ \"مدير\" أو تحديد الصلاحيات بشكل منفرد.\n إذا قمت بتحديد المستخدم كـ \"مدير\"، باقي الخيارات سيتم تحديدها تلقائياً.\n إدارة المستخدمين تبقى صلاحية فريدة للـ \"مدير\" فقط.\n",
|
||||
"profileSettings": "إعدادات الحساب",
|
||||
@@ -155,82 +209,46 @@
|
||||
"rulesHelp": "يمكنك هنا تحديد مجموعة من شروط السماح والمنع لهذا المستخدم. الملفات الممنوعة لن تظهر ضمن القائمة لهذا المستخدم ولن يستطيع الوصول لها. هنا ندعم الـ regex والـ relative path لنطاق المستخدمين.\n",
|
||||
"scope": "نطاق",
|
||||
"settingsUpdated": "تم تعديل الإعدادات",
|
||||
"shareDuration": "",
|
||||
"shareManagement": "",
|
||||
"singleClick": "",
|
||||
"themes": {
|
||||
"dark": "",
|
||||
"light": "",
|
||||
"title": ""
|
||||
},
|
||||
"user": "المستخدم",
|
||||
"userCommands": "الأوامر",
|
||||
"userCommandsHelp": "الأوامر المتاحة لهذا المستخدم مفصولة فيما بينها بمسافة. مثال:\n",
|
||||
"userCreated": "تم إنشاء المستخدم",
|
||||
"userDefaults": "User default settings",
|
||||
"userDeleted": "تم حذف المستخدم",
|
||||
"userManagement": "إدارة المستخدمين",
|
||||
"username": "إسم المستخدم",
|
||||
"users": "المستخدمين",
|
||||
"globalRules": "This is a global set of allow and disallow rules. They apply to every user. You can define specific rules on each user's settings to override this ones.",
|
||||
"allowSignup": "Allow users to signup",
|
||||
"createUserDir": "Auto create user home dir while adding new user",
|
||||
"insertRegex": "Insert regex expression",
|
||||
"insertPath": "Insert the path",
|
||||
"userUpdated": "تم تعديل المستخدم",
|
||||
"userDefaults": "User default settings",
|
||||
"defaultUserDescription": "This are the default settings for new users.",
|
||||
"executeOnShell": "Execute on shell",
|
||||
"executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you want to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This apply to both user commands and event hooks.",
|
||||
"perm": {
|
||||
"create": "Create files and directories",
|
||||
"delete": "Delete files and directories",
|
||||
"download": "Download",
|
||||
"modify": "Edit files",
|
||||
"execute": "Execute commands",
|
||||
"rename": "Rename or move files and directories",
|
||||
"share": "Share files"
|
||||
}
|
||||
"username": "إسم المستخدم",
|
||||
"users": "المستخدمين"
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "مساعدة",
|
||||
"hugoNew": "هيوجو جديد",
|
||||
"login": "Login",
|
||||
"signup": "Signup",
|
||||
"logout": "تسجيل خروج",
|
||||
"myFiles": "ملفاتي",
|
||||
"newFile": "ملف جديد",
|
||||
"newFolder": "مجلد جديد",
|
||||
"preview": "معاينة",
|
||||
"settings": "الإعدادات",
|
||||
"siteSettings": "إعدادات الموقع",
|
||||
"hugoNew": "هيوجو جديد",
|
||||
"preview": "معاينة"
|
||||
"signup": "Signup",
|
||||
"siteSettings": "إعدادات الموقع"
|
||||
},
|
||||
"search": {
|
||||
"images": "الصور",
|
||||
"music": "الموسيقى",
|
||||
"pdf": "PDF",
|
||||
"types": "الأنواع",
|
||||
"video": "فيديوهات",
|
||||
"search": "البحث...",
|
||||
"typeToSearch": "Type to search...",
|
||||
"pressToSearch": "Press enter to search..."
|
||||
},
|
||||
"languages": {
|
||||
"ar": "العربية",
|
||||
"en": "English",
|
||||
"it": "Italiano",
|
||||
"fr": "Français",
|
||||
"pt": "Português",
|
||||
"ptBR": "Português (Brasil)",
|
||||
"ja": "日本語",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"es": "Español",
|
||||
"de": "Deutsch",
|
||||
"ru": "Русский",
|
||||
"pl": "Polski",
|
||||
"ko": "한국어"
|
||||
"success": {
|
||||
"linkCopied": "تم نسخ الملف"
|
||||
},
|
||||
"time": {
|
||||
"unit": "وحدة الوقت",
|
||||
"seconds": "ثواني",
|
||||
"minutes": "دقائق",
|
||||
"days": "أيام",
|
||||
"hours": "ساعات",
|
||||
"days": "أيام"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Download File",
|
||||
"downloadFolder": "Download Folder"
|
||||
"minutes": "دقائق",
|
||||
"seconds": "ثواني",
|
||||
"unit": "وحدة الوقت"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user