Compare commits
267 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec28375208 | ||
|
|
01068a9217 | ||
|
|
7f01753bc5 | ||
|
|
0e223a056e | ||
|
|
9d08f9bed1 | ||
|
|
2cabeb8f68 | ||
|
|
7aaebab348 | ||
|
|
928cdfe2ae | ||
|
|
edb7b4dc17 | ||
|
|
85ee63af43 | ||
|
|
74b23a0bc5 | ||
|
|
be6c0bb850 | ||
|
|
ddb670ae1e | ||
|
|
4752758cf7 | ||
|
|
bd8aab4cba | ||
|
|
c61ede4153 | ||
|
|
efd46d6bd3 | ||
|
|
f2a8abb264 | ||
|
|
978aadc9b1 | ||
|
|
0626f07270 | ||
|
|
0afc8c9e5c | ||
|
|
28480c6acd | ||
|
|
826cdddca7 | ||
|
|
ec92ad9f47 | ||
|
|
ce97b9b9fd | ||
|
|
bbd93e111d | ||
|
|
f7c7d50e54 | ||
|
|
854d8bb705 | ||
|
|
7c0f261a97 | ||
|
|
7b9861b2c6 | ||
|
|
3a8fcbf4bc | ||
|
|
83d9247df6 | ||
|
|
d32286a13d | ||
|
|
621936f461 | ||
|
|
802e715fae | ||
|
|
adcafff384 | ||
|
|
a5ce1cf1e1 | ||
|
|
87d18a3089 | ||
|
|
e7fc0e97d6 | ||
|
|
34bdb8fcfc | ||
|
|
13b04f7672 | ||
|
|
76b9b2fa37 | ||
|
|
896d7cfbed | ||
|
|
0fb1b0840f | ||
|
|
1c0250075b | ||
|
|
cf2af817b9 | ||
|
|
ec24f79204 | ||
|
|
be902be453 | ||
|
|
888e08792e | ||
|
|
adc6ef22d9 | ||
|
|
0318d39112 | ||
|
|
abcfa0a05b | ||
|
|
a4b5c99ebb | ||
|
|
546e61a403 | ||
|
|
dcb68bd7a8 | ||
|
|
d411720234 | ||
|
|
1ae887d77c | ||
|
|
30465a771e | ||
|
|
f004b48b99 | ||
|
|
fc5e2247f6 | ||
|
|
5956647bd0 | ||
|
|
87eaf3ce5c | ||
|
|
73eba60210 | ||
|
|
4597ceb3a6 | ||
|
|
c0c25344c8 | ||
|
|
5efb36103f | ||
|
|
c0575a68ee | ||
|
|
ffd8a3a637 | ||
|
|
425ec275e9 | ||
|
|
3b9f336634 | ||
|
|
f792c31046 | ||
|
|
55a54ff89e | ||
|
|
78a40c9b14 | ||
|
|
17f32d16cc | ||
|
|
d57a0f2ae1 | ||
|
|
d6fdfef243 | ||
|
|
62d28dc89e | ||
|
|
57c65156f7 | ||
|
|
6e54dff40d | ||
|
|
0e722c8df1 | ||
|
|
f05479865c | ||
|
|
4e4055e7a8 | ||
|
|
7414ca10b3 | ||
|
|
1e17dfa6cb | ||
|
|
64d6d9e93b | ||
|
|
68902312cc | ||
|
|
a52b50b706 | ||
|
|
2527bdbfe1 | ||
|
|
473aaf13be | ||
|
|
0844b597f8 | ||
|
|
d45d7f92fb | ||
|
|
b3a822b4e8 | ||
|
|
788fadbd5e | ||
|
|
40f29e1e9b | ||
|
|
a036a25e1d | ||
|
|
abed362dc5 | ||
|
|
ce78299464 | ||
|
|
030f6607f0 | ||
|
|
c3a4e33245 | ||
|
|
aabf0843ab | ||
|
|
748e4acfb6 | ||
|
|
6e48a6b512 | ||
|
|
88500ab219 | ||
|
|
d0f8c141e1 | ||
|
|
34a1bf1380 | ||
|
|
b87ba12a7d | ||
|
|
bb0d048235 | ||
|
|
b991c65d8b | ||
|
|
b3b5db351f | ||
|
|
9562e06b92 | ||
|
|
7fc4899507 | ||
|
|
d649ae6ff7 | ||
|
|
633579e738 | ||
|
|
a65cb32d70 | ||
|
|
f1a7bc54ea | ||
|
|
8e1815944b | ||
|
|
db924c475a | ||
|
|
d970bb7de7 | ||
|
|
1fa91adae4 | ||
|
|
604487920d | ||
|
|
72e74d421c | ||
|
|
df5fc427ef | ||
|
|
12088154fe | ||
|
|
8ec27734bb | ||
|
|
1e6a0939a2 | ||
|
|
e58daaac83 | ||
|
|
7d0f25e530 | ||
|
|
cba41a1a32 | ||
|
|
997f21fc55 | ||
|
|
ead7fb4233 | ||
|
|
4590884a34 | ||
|
|
cc6689ac3a | ||
|
|
3ab225a101 | ||
|
|
31b70a7736 | ||
|
|
bbeadee98e | ||
|
|
b8169b6ebe | ||
|
|
df42e352f7 | ||
|
|
1f985fe72f | ||
|
|
575296d7fc | ||
|
|
b93dc9f200 | ||
|
|
6255f737ba | ||
|
|
3ed2144a0e | ||
|
|
a437761d03 | ||
|
|
b432e1bf46 | ||
|
|
8dd59e3e67 | ||
|
|
fd5543407a | ||
|
|
48d012ff92 | ||
|
|
2c4eae5ca2 | ||
|
|
629646122f | ||
|
|
d4f284f1a3 | ||
|
|
ff3b5b39a5 | ||
|
|
9667980f2d | ||
|
|
188a34f835 | ||
|
|
f9cd5f11d9 | ||
|
|
adedf0178b | ||
|
|
4e15b82896 | ||
|
|
ed0ea34161 | ||
|
|
e2ffd36073 | ||
|
|
6bd2a1852f | ||
|
|
371236e364 | ||
|
|
6cbdc9d7c5 | ||
|
|
a94125f3f2 | ||
|
|
2f5f5d75a7 | ||
|
|
02f2284f3b | ||
|
|
2b1305a315 | ||
|
|
ec78f67abd | ||
|
|
25c04af500 | ||
|
|
c0391d866e | ||
|
|
dcb97be587 | ||
|
|
c6eb98aef2 | ||
|
|
1c6e15c064 | ||
|
|
711a3a30b0 | ||
|
|
e203ca14f4 | ||
|
|
0f1b69b625 | ||
|
|
176eaad70b | ||
|
|
486dfe4e63 | ||
|
|
81cf4bab99 | ||
|
|
9c3f563f83 | ||
|
|
6b42781c21 | ||
|
|
891a0d1bd0 | ||
|
|
3b9063dc63 | ||
|
|
2bfdffb9c4 | ||
|
|
9f8685bf10 | ||
|
|
b58bc414bf | ||
|
|
0b81723118 | ||
|
|
66418ec064 | ||
|
|
d87640a4f1 | ||
|
|
e5580ac0c4 | ||
|
|
b92c800e00 | ||
|
|
e370fbe500 | ||
|
|
89d4d828b9 | ||
|
|
d004015f03 | ||
|
|
ba5b5fbfe3 | ||
|
|
dd29a87107 | ||
|
|
b394540f53 | ||
|
|
3ed5f8c0bd | ||
|
|
5d5cef2a87 | ||
|
|
9264e344d7 | ||
|
|
0a46ac3e1b | ||
|
|
a438fc746f | ||
|
|
0c8ffaf73e | ||
|
|
6c1bbb3248 | ||
|
|
e663c60a89 | ||
|
|
e1e8979e0b | ||
|
|
de53b24536 | ||
|
|
bc518a0e82 | ||
|
|
2ed87febcb | ||
|
|
ee169b3a46 | ||
|
|
90d690c187 | ||
|
|
0e7d4ef110 | ||
|
|
7a6397af22 | ||
|
|
ac512612e7 | ||
|
|
95fc3dfdfb | ||
|
|
39be89780e | ||
|
|
2642333928 | ||
|
|
7e1d745435 | ||
|
|
218e638f88 | ||
|
|
cad2a989c1 | ||
|
|
f844aeb2b4 | ||
|
|
7847763a31 | ||
|
|
3f49bc382e | ||
|
|
3ae9e518a3 | ||
|
|
1e5ced6737 | ||
|
|
fa67652ba4 | ||
|
|
62106cc0a4 | ||
|
|
99740e3eab | ||
|
|
c12adbb594 | ||
|
|
157b2da133 | ||
|
|
208f21728f | ||
|
|
0377080da6 | ||
|
|
f5c48c9679 | ||
|
|
99ef1308ea | ||
|
|
9f075c16c5 | ||
|
|
2f17f19425 | ||
|
|
06f00e9664 | ||
|
|
c681174adf | ||
|
|
4a12ce1888 | ||
|
|
4e39f2387a | ||
|
|
e354098b96 | ||
|
|
9086720c3c | ||
|
|
2bb9171e32 | ||
|
|
294efef38c | ||
|
|
4f1d25fba7 | ||
|
|
d3e363a4d5 | ||
|
|
a10f286f0f | ||
|
|
390c53097f | ||
|
|
4c30b2c665 | ||
|
|
325e6e0904 | ||
|
|
56ec440272 | ||
|
|
0af5e07eed | ||
|
|
8a764ceb67 | ||
|
|
fe829aa850 | ||
|
|
2ca22656d6 | ||
|
|
cb7fa99fd3 | ||
|
|
441639a8d5 | ||
|
|
a78e1d504b | ||
|
|
02f6b0ec61 | ||
|
|
8c60cc7084 | ||
|
|
a7e4596e97 | ||
|
|
97d53ceb2e | ||
|
|
a49fb20885 | ||
|
|
2d5e97e140 | ||
|
|
ebdf5a0601 | ||
|
|
9ca02c90ed | ||
|
|
0595638228 | ||
|
|
1f4d0cc3cd | ||
|
|
e6c0d1c28a |
79
.circleci/config.yml
Normal file
@@ -0,0 +1,79 @@
|
||||
version: 2
|
||||
jobs:
|
||||
lint:
|
||||
docker:
|
||||
- image: golangci/golangci-lint:v1.16
|
||||
steps:
|
||||
- checkout
|
||||
- run: golangci-lint run -v -D errcheck
|
||||
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:
|
||||
- '*'
|
||||
build-go:
|
||||
docker:
|
||||
- image: circleci/golang:1.12
|
||||
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.12
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: '~/project'
|
||||
- setup_remote_docker
|
||||
- run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
|
||||
- run: curl -sL https://git.io/goreleaser | bash
|
||||
- run: docker logout
|
||||
workflows:
|
||||
version: 2
|
||||
build-workflow:
|
||||
jobs:
|
||||
- lint:
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
- build-node:
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
- build-go:
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
requires:
|
||||
- build-node
|
||||
- lint
|
||||
- release:
|
||||
context: deploy
|
||||
requires:
|
||||
- build-go
|
||||
filters:
|
||||
tags:
|
||||
only: /^v.*/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
6
.github/ISSUE_TEMPLATE/caddy_bug_report.md
vendored
@@ -1,6 +0,0 @@
|
||||
---
|
||||
name: Caddy related bug report
|
||||
about: Create a report to help us improve
|
||||
---
|
||||
|
||||
### Please open the issue on https://github.com/filebrowser/caddy/issues/new
|
||||
25
.gitignore
vendored
@@ -3,4 +3,27 @@
|
||||
*.bak
|
||||
_old
|
||||
rice-box.go
|
||||
.idea/
|
||||
.idea/
|
||||
filebrowser
|
||||
|
||||
.DS_Store
|
||||
node_modules
|
||||
/frontend/dist
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw*
|
||||
|
||||
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
||||
[submodule "frontend"]
|
||||
path = frontend
|
||||
url = https://github.com/filebrowser/frontend
|
||||
@@ -1,20 +0,0 @@
|
||||
run:
|
||||
deadline: 5m
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- deadcode
|
||||
- govet
|
||||
- ineffassign
|
||||
- interfacer
|
||||
- maligned
|
||||
- megacheck
|
||||
- structcheck
|
||||
- unconvert
|
||||
- varcheck
|
||||
enable-all: false
|
||||
disable-all: true
|
||||
# presets:
|
||||
# - bugs
|
||||
# - unused
|
||||
fast: false
|
||||
@@ -1,8 +1,17 @@
|
||||
project_name: filebrowser
|
||||
|
||||
env:
|
||||
- GO111MODULE=on
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod download
|
||||
|
||||
build:
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
ldflags:
|
||||
- -s -w -X github.com/filebrowser/filebrowser/v2/version.Version={{ .Version }} -X github.com/filebrowser/filebrowser/v2/version.CommitSHA={{ .ShortCommit }}
|
||||
main: main.go
|
||||
binary: filebrowser
|
||||
goos:
|
||||
@@ -35,15 +44,13 @@ build:
|
||||
- goos: solaris
|
||||
goarch: arm
|
||||
|
||||
archive:
|
||||
name_template: "{{.Os}}-{{.Arch}}{{if .Arm}}v{{.Arm}}{{end}}-{{ .ProjectName }}"
|
||||
format: tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
|
||||
release:
|
||||
disable: true
|
||||
archives:
|
||||
-
|
||||
name_template: "{{.Os}}-{{.Arch}}{{if .Arm}}v{{.Arm}}{{end}}-{{ .ProjectName }}"
|
||||
format: tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
|
||||
dockers:
|
||||
-
|
||||
@@ -53,6 +60,6 @@ dockers:
|
||||
image_templates:
|
||||
- "filebrowser/filebrowser:latest"
|
||||
- "filebrowser/filebrowser:{{ .Tag }}"
|
||||
skip_push: true
|
||||
- "filebrowser/filebrowser:v{{ .Major }}"
|
||||
extra_files:
|
||||
- .docker.json
|
||||
|
||||
49
.travis.yml
@@ -1,49 +0,0 @@
|
||||
os: linux
|
||||
services: docker
|
||||
language: go
|
||||
go:
|
||||
- 1.12.x
|
||||
install: skip
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- pass
|
||||
env:
|
||||
global:
|
||||
- USE_DOCKER="true"
|
||||
- GO111MODULE=on
|
||||
stages:
|
||||
- lint
|
||||
- build
|
||||
- release
|
||||
cache:
|
||||
directories:
|
||||
- http/rice-box.go
|
||||
jobs:
|
||||
include:
|
||||
- stage: lint
|
||||
script: ./wizard.sh -l
|
||||
- stage: build
|
||||
script: ./wizard.sh -b
|
||||
deploy:
|
||||
provider: script
|
||||
skip_cleanup: true
|
||||
script: ./wizard.sh -p
|
||||
on:
|
||||
tags: false
|
||||
repo: filebrowser/filebrowser
|
||||
branch: master
|
||||
- stage: release
|
||||
script: ./wizard.sh -r "$TRAVIS_TAG"
|
||||
if: tag IS present
|
||||
deploy:
|
||||
provider: releases
|
||||
skip_cleanup: true
|
||||
api_key:
|
||||
secure: GCURbl9xmjOmeNc7cYSvfSwbEp46cacWmJRczcsU6rQa0aWqzjELYdyIsl6HWW+o0dzuZvbWRD6muxYqIud92oPLYDuXSnra9tM3mCjswrjiPCJ57bksWkSPBfFQcxIyB6c3o+A/FMnX3nnSE/2r5HYZnPNFbEcBbC7WSgwx9ejXUuyWn1PUFK9YQWANdl6J7b7EKsk+9MxS9Pmw6M2ycBwX8ScUQdofkUPvR/nqlXISm+3hs30VubqQi9Ha6DM9Bw3aFK3/Ts/ujCOxP1ZoMCBZ6tfnaQOElIG96WTwnt77eDYlZezBOLym3Z18iif+Qny+XndFKDbexaiUT06VlWFXCKtt3iLs6HJwRcjmiHmB0Z3v+W4cKPl3cEyxxrU2aal54k1PBhU+5L0Xc8ileKbDMYg5tps88zWHNefeZVfaxYSVrmUHkuygMe481oaBLacDXTxs4t6XEpStREuLmvx9NLTwTFAbWjMNM0PqlueDMxO4bdwNvzXg/TcKLWV9FezqAlre8lFNZK5wX6lKFVSZ3hFjxCfwrJL2cPwg5A8Yd5EOC4Nh81WdgYuFGOxZzMAoSJlaVRvQS1trCUP/++ONnDep3ExSxvw4B7vijGZWeXUhrOMiPQHXu+t6BnrlnDjQ4gi44QTW0y/iM2WC2DBKfgYjAKwyHx13hFrmOCg=
|
||||
file: "dist/*.*"
|
||||
file_glob: true
|
||||
on:
|
||||
repo: filebrowser/filebrowser
|
||||
all_branches: true
|
||||
tags: true
|
||||
10
.tx/config
Normal file
@@ -0,0 +1,10 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
lang_map = pt_BR: pt-br, zh_CN: zh-cn, zh_HK: zh-hk, zh_TW: zh-tw, nl_BE: nl-be, sv_SE: sv-se
|
||||
|
||||
[file-browser.file-browser]
|
||||
file_filter = frontend/src/i18n/<lang>.json
|
||||
minimum_perc = 50
|
||||
source_file = frontend/src/i18n/en.json
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
||||
@@ -1,6 +1,8 @@
|
||||
FROM scratch
|
||||
FROM alpine:latest as certs
|
||||
RUN apk --update add ca-certificates
|
||||
|
||||
COPY --from=filebrowser/dev /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
FROM scratch
|
||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
VOLUME /srv
|
||||
EXPOSE 80
|
||||
|
||||
12
README.md
@@ -2,6 +2,8 @@
|
||||
<img src="https://raw.githubusercontent.com/filebrowser/logo/master/banner.png" width="550"/>
|
||||
</p>
|
||||
|
||||
> ⚠️ WARN: **This project is not under active development ATM. A small group of developers keeps the project alive, but due to lack of time, we can't continue adding new features or doing deep changes. Please read [#532](https://github.com/filebrowser/filebrowser/issues/532) for more info!**
|
||||
|
||||

|
||||
|
||||
[](https://travis-ci.com/filebrowser/filebrowser)
|
||||
@@ -10,22 +12,20 @@
|
||||
[](https://github.com/filebrowser/filebrowser/releases/latest)
|
||||
[](http://webchat.freenode.net/?channels=%23filebrowser)
|
||||
|
||||
> ℹ INFO: **This project is not under active development ATM. A small group of developers keeps the project alive, but due to lack of time, we can't continue adding new features or doing deep changes. Please read [#532](https://github.com/filebrowser/filebrowser/issues/532) for more info!**
|
||||
|
||||
filebrowser provides a file managing interface within a specified directory and it can be used to upload, delete, preview, rename and edit your files. It allows the creation of multiple users and each user can have its own directory. It can be used as a standalone app or as a middleware.
|
||||
|
||||
## Features
|
||||
|
||||
Please refer to our docs at [docs.filebrowser.xyz/features](https://docs.filebrowser.xyz/features)
|
||||
Please refer to our docs at [filebrowser.xyz/features](https://filebrowser.xyz/features)
|
||||
|
||||
## Install
|
||||
|
||||
Please refer to our docs at [docs.filebrowser.xyz](https://docs.filebrowser.xyz/).
|
||||
Please refer to our docs at [filebrowser.xyz](https://filebrowser.xyz/).
|
||||
|
||||
## Usage
|
||||
|
||||
Please refer to our docs at [docs.filebrowser.xyz/usage](https://docs.filebrowser.xyz/usage).
|
||||
Please refer to our docs at [filebrowser.xyz/usage](https://filebrowser.xyz/usage).
|
||||
|
||||
## Contributing
|
||||
|
||||
Please refer to our docs at [docs.filebrowser.xyz/contributing](https://docs.filebrowser.xyz/contributing).
|
||||
Please refer to our docs at [filebrowser.xyz/contributing](https://filebrowser.xyz/contributing).
|
||||
|
||||
@@ -76,7 +76,7 @@ type ReCaptcha struct {
|
||||
// Ok checks if a reCaptcha responde is correct.
|
||||
func (r *ReCaptcha) Ok(response string) (bool, error) {
|
||||
body := url.Values{}
|
||||
body.Set("secret", r.Key)
|
||||
body.Set("secret", r.Secret)
|
||||
body.Add("response", response)
|
||||
|
||||
client := &http.Client{}
|
||||
|
||||
@@ -44,15 +44,37 @@ func addConfigFlags(flags *pflag.FlagSet) {
|
||||
flags.Bool("branding.disableExternal", false, "disable external links such as GitHub links")
|
||||
}
|
||||
|
||||
func getAuthentication(flags *pflag.FlagSet) (settings.AuthMethod, auth.Auther) {
|
||||
func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, auth.Auther) {
|
||||
method := settings.AuthMethod(mustGetString(flags, "auth.method"))
|
||||
|
||||
var defaultAuther map[string]interface{}
|
||||
if len(defaults) > 0 {
|
||||
if hasAuth := defaults[0]; hasAuth != true {
|
||||
for _, arg := range defaults {
|
||||
switch def := arg.(type) {
|
||||
case *settings.Settings:
|
||||
method = settings.AuthMethod(def.AuthMethod)
|
||||
case auth.Auther:
|
||||
ms, err := json.Marshal(def)
|
||||
checkErr(err)
|
||||
json.Unmarshal(ms, &defaultAuther)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var auther auth.Auther
|
||||
if method == auth.MethodProxyAuth {
|
||||
header := mustGetString(flags, "auth.header")
|
||||
|
||||
if header == "" {
|
||||
panic(nerrors.New("you must set the flag 'auth.header' for method 'proxy'"))
|
||||
header = defaultAuther["header"].(string)
|
||||
}
|
||||
|
||||
if header == "" {
|
||||
checkErr(nerrors.New("you must set the flag 'auth.header' for method 'proxy'"))
|
||||
}
|
||||
|
||||
auther = &auth.ProxyAuth{Header: header}
|
||||
}
|
||||
|
||||
@@ -62,11 +84,22 @@ func getAuthentication(flags *pflag.FlagSet) (settings.AuthMethod, auth.Auther)
|
||||
|
||||
if method == auth.MethodJSONAuth {
|
||||
jsonAuth := &auth.JSONAuth{}
|
||||
|
||||
host := mustGetString(flags, "recaptcha.host")
|
||||
key := mustGetString(flags, "recaptcha.key")
|
||||
secret := mustGetString(flags, "recaptcha.secret")
|
||||
|
||||
if key == "" {
|
||||
if kmap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok {
|
||||
key = kmap["key"].(string)
|
||||
}
|
||||
}
|
||||
|
||||
if secret == "" {
|
||||
if smap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok {
|
||||
secret = smap["secret"].(string)
|
||||
}
|
||||
}
|
||||
|
||||
if key != "" && secret != "" {
|
||||
jsonAuth.ReCaptcha = &auth.ReCaptcha{
|
||||
Host: host,
|
||||
@@ -74,7 +107,6 @@ func getAuthentication(flags *pflag.FlagSet) (settings.AuthMethod, auth.Auther)
|
||||
Secret: secret,
|
||||
}
|
||||
}
|
||||
|
||||
auther = jsonAuth
|
||||
}
|
||||
|
||||
@@ -101,6 +133,7 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
|
||||
fmt.Fprintf(w, "\tPort:\t%s\n", ser.Port)
|
||||
fmt.Fprintf(w, "\tBase URL:\t%s\n", ser.BaseURL)
|
||||
fmt.Fprintf(w, "\tRoot:\t%s\n", ser.Root)
|
||||
fmt.Fprintf(w, "\tSocket:\t%s\n", ser.Socket)
|
||||
fmt.Fprintf(w, "\tAddress:\t%s\n", ser.Address)
|
||||
fmt.Fprintf(w, "\tTLS Cert:\t%s\n", ser.TLSCert)
|
||||
fmt.Fprintf(w, "\tTLS Key:\t%s\n", ser.TLSKey)
|
||||
|
||||
@@ -43,6 +43,7 @@ override the options.`,
|
||||
|
||||
ser := &settings.Server{
|
||||
Address: mustGetString(flags, "address"),
|
||||
Socket: mustGetString(flags, "socket"),
|
||||
Root: mustGetString(flags, "root"),
|
||||
BaseURL: mustGetString(flags, "baseurl"),
|
||||
TLSKey: mustGetString(flags, "key"),
|
||||
|
||||
@@ -3,7 +3,6 @@ package cmd
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/auth"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
@@ -34,6 +33,8 @@ you want to change. Other options will remain unchanged.`,
|
||||
ser.BaseURL = mustGetString(flags, flag.Name)
|
||||
case "root":
|
||||
ser.Root = mustGetString(flags, flag.Name)
|
||||
case "socket":
|
||||
ser.Socket = mustGetString(flags, flag.Name)
|
||||
case "cert":
|
||||
ser.TLSCert = mustGetString(flags, flag.Name)
|
||||
case "key":
|
||||
@@ -61,16 +62,15 @@ you want to change. Other options will remain unchanged.`,
|
||||
|
||||
getUserDefaults(flags, &set.Defaults, false)
|
||||
|
||||
var auther auth.Auther
|
||||
if hasAuth {
|
||||
set.AuthMethod, auther = getAuthentication(flags)
|
||||
err = d.store.Auth.Save(auther)
|
||||
checkErr(err)
|
||||
} else {
|
||||
auther, err = d.store.Auth.Get(set.AuthMethod)
|
||||
checkErr(err)
|
||||
}
|
||||
// read the defaults
|
||||
auther, err := d.store.Auth.Get(set.AuthMethod)
|
||||
checkErr(err)
|
||||
|
||||
// check if there are new flags for existing auth method
|
||||
set.AuthMethod, auther = getAuthentication(flags, hasAuth, set, auther)
|
||||
|
||||
err = d.store.Auth.Save(auther)
|
||||
checkErr(err)
|
||||
err = d.store.Settings.Save(set)
|
||||
checkErr(err)
|
||||
err = d.store.Settings.SaveServer(ser)
|
||||
|
||||
59
cmd/root.go
@@ -2,13 +2,16 @@ package cmd
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/auth"
|
||||
fbhttp "github.com/filebrowser/filebrowser/v2/http"
|
||||
@@ -50,6 +53,7 @@ func addServerFlags(flags *pflag.FlagSet) {
|
||||
flags.StringP("cert", "t", "", "tls certificate")
|
||||
flags.StringP("key", "k", "", "tls key")
|
||||
flags.StringP("root", "r", ".", "root to prepend to relative paths")
|
||||
flags.String("socket", "", "socket to listen to (cannot be used with address, port, cert nor key flags)")
|
||||
flags.StringP("baseurl", "b", "", "base url")
|
||||
}
|
||||
|
||||
@@ -109,7 +113,10 @@ user created with the credentials from options "username" and "password".`,
|
||||
|
||||
var listener net.Listener
|
||||
|
||||
if server.TLSKey != "" && server.TLSCert != "" {
|
||||
if server.Socket != "" {
|
||||
listener, err = net.Listen("unix", server.Socket)
|
||||
checkErr(err)
|
||||
} else if server.TLSKey != "" && server.TLSCert != "" {
|
||||
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey)
|
||||
checkErr(err)
|
||||
listener, err = tls.Listen("tcp", adr, &tls.Config{Certificates: []tls.Certificate{cer}})
|
||||
@@ -119,9 +126,15 @@ user created with the credentials from options "username" and "password".`,
|
||||
checkErr(err)
|
||||
}
|
||||
|
||||
sigc := make(chan os.Signal, 1)
|
||||
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
|
||||
go cleanupHandler(listener, sigc)
|
||||
|
||||
handler, err := fbhttp.NewHandler(d.store, server)
|
||||
checkErr(err)
|
||||
|
||||
defer listener.Close()
|
||||
|
||||
log.Println("Listening on", listener.Addr().String())
|
||||
if err := http.Serve(listener, handler); err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -129,6 +142,13 @@ user created with the credentials from options "username" and "password".`,
|
||||
}, pythonConfig{allowNoDB: true}),
|
||||
}
|
||||
|
||||
func cleanupHandler(listener net.Listener, c chan os.Signal) {
|
||||
sig := <-c
|
||||
log.Printf("Caught signal %s: shutting down.", sig)
|
||||
listener.Close()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
|
||||
server, err := st.Settings.GetServer()
|
||||
checkErr(err)
|
||||
@@ -141,24 +161,45 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
|
||||
server.BaseURL = val
|
||||
}
|
||||
|
||||
if val, set := getParamB(flags, "address"); set {
|
||||
server.Address = val
|
||||
}
|
||||
|
||||
if val, set := getParamB(flags, "port"); set {
|
||||
server.Port = val
|
||||
}
|
||||
|
||||
if val, set := getParamB(flags, "log"); set {
|
||||
server.Log = val
|
||||
}
|
||||
|
||||
isSocketSet := false
|
||||
isAddrSet := false
|
||||
|
||||
if val, set := getParamB(flags, "address"); set {
|
||||
server.Address = val
|
||||
isAddrSet = isAddrSet || set
|
||||
}
|
||||
|
||||
if val, set := getParamB(flags, "port"); set {
|
||||
server.Port = val
|
||||
isAddrSet = isAddrSet || set
|
||||
}
|
||||
|
||||
if val, set := getParamB(flags, "key"); set {
|
||||
server.TLSKey = val
|
||||
isAddrSet = isAddrSet || set
|
||||
}
|
||||
|
||||
if val, set := getParamB(flags, "cert"); set {
|
||||
server.TLSCert = val
|
||||
isAddrSet = isAddrSet || set
|
||||
}
|
||||
|
||||
if val, set := getParamB(flags, "socket"); set {
|
||||
server.Socket = val
|
||||
isSocketSet = isSocketSet || set
|
||||
}
|
||||
|
||||
if isAddrSet && isSocketSet {
|
||||
checkErr(errors.New("--socket flag cannot be used with --address, --port, --key nor --cert"))
|
||||
}
|
||||
|
||||
// Do not use saved Socket if address was manually set.
|
||||
if isAddrSet && server.Socket != "" {
|
||||
server.Socket = ""
|
||||
}
|
||||
|
||||
return server
|
||||
|
||||
@@ -15,6 +15,6 @@ var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the version number",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("File Browser Version " + version.Version)
|
||||
fmt.Println("File Browser v" + version.Version + "/" + version.CommitSHA)
|
||||
},
|
||||
}
|
||||
|
||||
1
frontend
5
frontend/babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/app'
|
||||
]
|
||||
}
|
||||
12488
frontend/package-lock.json
generated
Normal file
60
frontend/package.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "filebrowser-frontend",
|
||||
"version": "2.0.0",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"ace-builds": "^1.4.6",
|
||||
"clipboard": "^2.0.4",
|
||||
"js-base64": "^2.5.1",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"material-design-icons": "^3.0.1",
|
||||
"moment": "^2.24.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
"noty": "^3.2.0-beta",
|
||||
"qrcode.vue": "^1.6.3",
|
||||
"vue": "^2.6.10",
|
||||
"vue-i18n": "^8.15.0",
|
||||
"vue-router": "^3.1.3",
|
||||
"vuex": "^3.1.1",
|
||||
"vuex-router-sync": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^4.0.4",
|
||||
"@vue/cli-plugin-eslint": "^4.0.3",
|
||||
"@vue/cli-service": "^4.0.4",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-plugin-vue": "^5.2.3",
|
||||
"vue-template-compiler": "^2.6.10"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"rules": {},
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
},
|
||||
"postcss": {
|
||||
"plugins": {
|
||||
"autoprefixer": {}
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not ie <= 8"
|
||||
]
|
||||
}
|
||||
BIN
frontend/public/img/icons/android-chrome-192x192.png
Normal file
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
BIN
frontend/public/img/icons/android-chrome-512x512.png
Normal file
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
BIN
frontend/public/img/icons/apple-touch-icon.png
Normal file
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
9
frontend/public/img/icons/browserconfig.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/mstile-150x150.png"/>
|
||||
<TileColor>#455a64</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
||||
BIN
frontend/public/img/icons/favicon-16x16.png
Normal file
|
Before Width: | Height: | Size: 843 B After Width: | Height: | Size: 843 B |
BIN
frontend/public/img/icons/favicon-32x32.png
Normal file
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
BIN
frontend/public/img/icons/favicon.ico
Normal file
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
BIN
frontend/public/img/icons/mstile-144x144.png
Normal file
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
BIN
frontend/public/img/icons/mstile-150x150.png
Normal file
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
BIN
frontend/public/img/icons/mstile-310x150.png
Normal file
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
BIN
frontend/public/img/icons/mstile-310x310.png
Normal file
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
BIN
frontend/public/img/icons/mstile-70x70.png
Normal file
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
42
frontend/public/img/icons/safari-pinned-tab.svg
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M3245 6989 c-522 -39 -1042 -197 -1480 -449 -849 -488 -1459 -1308
|
||||
-1673 -2250 -177 -776 -89 -1582 250 -2301 368 -778 1052 -1418 1857 -1739
|
||||
903 -359 1927 -325 2812 92 778 368 1418 1052 1739 1857 359 903 325 1927 -92
|
||||
2812 -296 627 -806 1175 -1423 1529 -587 338 -1308 500 -1990 449z m555 -580
|
||||
c519 -51 1018 -245 1446 -565 788 -588 1229 -1526 1174 -2496 -16 -277 -58
|
||||
-500 -145 -763 -144 -440 -378 -819 -710 -1150 -452 -452 -1005 -730 -1655
|
||||
-832 -91 -14 -175 -18 -405 -18 -304 0 -369 6 -595 51 -1105 223 -1999 1092
|
||||
-2259 2197 -52 221 -73 412 -73 667 0 397 64 732 204 1080 304 752 886 1334
|
||||
1638 1638 431 174 895 238 1380 191z"/>
|
||||
<path d="M2670 5215 c0 -13 -44 -15 -335 -15 -352 0 -383 -3 -399 -45 -3 -9
|
||||
-6 -758 -6 -1663 0 -1168 -3 -1643 -11 -1632 -8 11 -9 8 -4 -15 3 -16 17 -41
|
||||
31 -55 l24 -25 1530 0 1530 0 24 25 c14 14 26 36 27 50 1 14 1 711 1 1550 l-2
|
||||
1526 -228 142 -229 142 -136 0 -137 0 0 -600 0 -600 -705 0 -705 0 0 615 0
|
||||
615 -135 0 c-113 0 -135 -2 -135 -15z m-264 -190 c57 -29 89 -71 103 -137 35
|
||||
-154 -98 -282 -258 -247 -55 12 -122 62 -148 113 -36 69 -12 186 49 243 62 58
|
||||
170 70 254 28z m2316 -1702 c17 -15 18 -49 18 -670 l0 -653 -1245 0 -1245 0 0
|
||||
654 c0 582 2 656 16 670 14 14 139 16 1226 16 1113 0 1213 -1 1230 -17z
|
||||
m-2602 -1363 c40 -40 13 -100 -43 -100 -60 0 -88 59 -47 100 11 11 31 20 45
|
||||
20 14 0 34 -9 45 -20z m2840 0 c41 -41 11 -100 -52 -100 -35 0 -58 24 -58 60
|
||||
0 54 71 79 110 40z"/>
|
||||
<path d="M2431 3091 c-7 -13 -7 -23 2 -35 11 -15 97 -16 1067 -14 l1055 3 0
|
||||
30 0 30 -1057 3 c-1023 2 -1058 1 -1067 -17z"/>
|
||||
<path d="M2436 2675 c-19 -19 -11 -41 17 -49 41 -11 2067 -7 2088 4 23 13 25
|
||||
46 3 54 -9 3 -483 6 -1054 6 -919 0 -1040 -2 -1054 -15z"/>
|
||||
<path d="M2447 2273 c-14 -4 -17 -13 -15 -36 l3 -32 1049 -3 c767 -1 1052 1
|
||||
1062 9 20 16 17 47 -5 59 -20 10 -2055 13 -2094 3z"/>
|
||||
<path d="M3822 5027 c-21 -23 -22 -30 -22 -293 0 -258 1 -271 20 -292 27 -29
|
||||
56 -35 140 -30 56 3 75 8 93 26 22 22 22 26 22 298 l0 276 -24 19 c-19 16 -40
|
||||
19 -115 19 -84 0 -95 -2 -114 -23z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
147
frontend/public/img/logo.svg
Normal file
@@ -0,0 +1,147 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xml:space="preserve"
|
||||
width="560"
|
||||
height="560"
|
||||
version="1.1"
|
||||
style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
|
||||
viewBox="0 0 560 560"
|
||||
id="svg44"
|
||||
sodipodi:docname="icon_raw.svg"
|
||||
inkscape:version="0.92.3 (2405546, 2018-03-11)"
|
||||
inkscape:export-filename="/home/umarcor/filebrowser/logo/icon_raw.svg.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"><metadata
|
||||
id="metadata48"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1366"
|
||||
inkscape:window-height="711"
|
||||
id="namedview46"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.33714286"
|
||||
inkscape:cx="-172.33051"
|
||||
inkscape:cy="280"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="20"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg44" />
|
||||
<defs
|
||||
id="defs4">
|
||||
<style
|
||||
type="text/css"
|
||||
id="style2">
|
||||
<![CDATA[
|
||||
.fil1 {fill:#FEFEFE}
|
||||
.fil6 {fill:#006498}
|
||||
.fil7 {fill:#0EA5EB}
|
||||
.fil8 {fill:#2979FF}
|
||||
.fil3 {fill:#2BBCFF}
|
||||
.fil0 {fill:#455A64}
|
||||
.fil4 {fill:#53C6FC}
|
||||
.fil5 {fill:#BDEAFF}
|
||||
.fil2 {fill:#332C2B;fill-opacity:0.149020}
|
||||
]]>
|
||||
</style>
|
||||
</defs>
|
||||
<g
|
||||
id="g85"
|
||||
transform="translate(-70,-70)"><path
|
||||
class="fil1"
|
||||
d="M 350,71 C 504,71 629,196 629,350 629,504 504,629 350,629 196,629 71,504 71,350 71,196 196,71 350,71 Z"
|
||||
id="path9"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#fefefe" /><path
|
||||
class="fil2"
|
||||
d="M 475,236 593,387 C 596,503 444,639 301,585 L 225,486 339,330 c 0,0 138,-95 136,-94 z"
|
||||
id="path11"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#332c2b;fill-opacity:0.14902003" /><path
|
||||
class="fil3"
|
||||
d="m 231,211 h 208 l 38,24 v 246 c 0,5 -3,8 -8,8 H 231 c -5,0 -8,-3 -8,-8 V 219 c 0,-5 3,-8 8,-8 z"
|
||||
id="path13"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#2bbcff" /><path
|
||||
class="fil4"
|
||||
d="m 231,211 h 208 l 38,24 v 2 L 440,214 H 231 c -4,0 -7,3 -7,7 v 263 c -1,-1 -1,-2 -1,-3 V 219 c 0,-5 3,-8 8,-8 z"
|
||||
id="path15"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#53c6fc" /><polygon
|
||||
class="fil5"
|
||||
points="305,212 418,212 418,310 305,310 "
|
||||
id="polygon17"
|
||||
style="fill:#bdeaff" /><path
|
||||
class="fil5"
|
||||
d="m 255,363 h 189 c 3,0 5,2 5,4 V 483 H 250 V 367 c 0,-2 2,-4 5,-4 z"
|
||||
id="path19"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#bdeaff" /><polygon
|
||||
class="fil6"
|
||||
points="250,470 449,470 449,483 250,483 "
|
||||
id="polygon21"
|
||||
style="fill:#006498" /><path
|
||||
class="fil6"
|
||||
d="m 380,226 h 10 c 3,0 6,2 6,5 v 40 c 0,3 -3,6 -6,6 h -10 c -3,0 -6,-3 -6,-6 v -40 c 0,-3 3,-5 6,-5 z"
|
||||
id="path23"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#006498" /><path
|
||||
class="fil1"
|
||||
d="m 254,226 c 10,0 17,7 17,17 0,9 -7,16 -17,16 -9,0 -17,-7 -17,-16 0,-10 8,-17 17,-17 z"
|
||||
id="path25"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#fefefe" /><path
|
||||
class="fil6"
|
||||
d="m 267,448 h 165 c 2,0 3,1 3,3 v 0 c 0,1 -1,3 -3,3 H 267 c -2,0 -3,-2 -3,-3 v 0 c 0,-2 1,-3 3,-3 z"
|
||||
id="path27"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#006498" /><path
|
||||
class="fil6"
|
||||
d="m 267,415 h 165 c 2,0 3,1 3,3 v 0 c 0,1 -1,2 -3,2 H 267 c -2,0 -3,-1 -3,-2 v 0 c 0,-2 1,-3 3,-3 z"
|
||||
id="path29"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#006498" /><path
|
||||
class="fil6"
|
||||
d="m 267,381 h 165 c 2,0 3,2 3,3 v 0 c 0,2 -1,3 -3,3 H 267 c -2,0 -3,-1 -3,-3 v 0 c 0,-1 1,-3 3,-3 z"
|
||||
id="path31"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#006498" /><path
|
||||
class="fil1"
|
||||
d="m 236,472 c 3,0 5,2 5,5 0,2 -2,4 -5,4 -3,0 -5,-2 -5,-4 0,-3 2,-5 5,-5 z"
|
||||
id="path33"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#fefefe" /><path
|
||||
class="fil1"
|
||||
d="m 463,472 c 3,0 5,2 5,5 0,2 -2,4 -5,4 -3,0 -5,-2 -5,-4 0,-3 2,-5 5,-5 z"
|
||||
id="path35"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#fefefe" /><polygon
|
||||
class="fil6"
|
||||
points="305,212 284,212 284,310 305,310 "
|
||||
id="polygon37"
|
||||
style="fill:#006498" /><path
|
||||
class="fil7"
|
||||
d="m 477,479 v 2 c 0,5 -3,8 -8,8 H 231 c -5,0 -8,-3 -8,-8 v -2 c 0,4 3,8 8,8 h 238 c 5,0 8,-4 8,-8 z"
|
||||
id="path39"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#0ea5eb" /><path
|
||||
class="fil8"
|
||||
d="M 350,70 C 505,70 630,195 630,350 630,505 505,630 350,630 195,630 70,505 70,350 70,195 195,70 350,70 Z m 0,46 C 479,116 584,221 584,350 584,479 479,584 350,584 221,584 116,479 116,350 116,221 221,116 350,116 Z"
|
||||
id="path41"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#2979ff" /></g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
140
frontend/public/index.html
Normal file
@@ -0,0 +1,140 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
|
||||
[{[ if .ReCaptcha -]}]
|
||||
<script src="[{[ .ReCaptchaHost ]}]/recaptcha/api.js?render=explicit"></script>
|
||||
[{[ end ]}]
|
||||
|
||||
<title>[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]</title>
|
||||
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/[{[ .StaticURL ]}]/img/icons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/[{[ .StaticURL ]}]/img/icons/favicon-16x16.png">
|
||||
<!-- Add to home screen for Android and modern mobile browsers -->
|
||||
<link rel="manifest" id="manifestPlaceholder">
|
||||
<meta name="theme-color" content="#2979ff">
|
||||
|
||||
<!-- Add to home screen for Safari on iOS -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="assets">
|
||||
<link rel="apple-touch-icon" href="/[{[ .StaticURL ]}]/img/icons/apple-touch-icon-152x152.png">
|
||||
|
||||
<!-- Add to home screen for Windows -->
|
||||
<meta name="msapplication-TileImage" content="/[{[ .StaticURL ]}]/img/icons/msapplication-icon-144x144.png">
|
||||
<meta name="msapplication-TileColor" content="#2979ff">
|
||||
|
||||
<!-- Inject Some Variables and generate the manifest json -->
|
||||
<script>
|
||||
window.FileBrowser = JSON.parse(`[{[ .Json ]}]`);
|
||||
|
||||
var fullStaticURL = window.location.origin + "/" + window.FileBrowser.StaticURL;
|
||||
var dynamicManifest = {
|
||||
"name": window.FileBrowser.Name || 'File Browser',
|
||||
"short_name": window.FileBrowser.Name || 'File Browser',
|
||||
"icons": [
|
||||
{
|
||||
"src": fullStaticURL + "/img/icons/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": fullStaticURL + "/img/icons/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"start_url": window.location.origin + window.FileBrowser.BaseURL,
|
||||
"display": "standalone",
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "#455a64"
|
||||
}
|
||||
|
||||
const stringManifest = JSON.stringify(dynamicManifest);
|
||||
const blob = new Blob([stringManifest], {type: 'application/json'});
|
||||
const manifestURL = URL.createObjectURL(blob);
|
||||
document.querySelector('#manifestPlaceholder').setAttribute('href', manifestURL);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#loading {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
z-index: 9999;
|
||||
transition: .1s ease opacity;
|
||||
-webkit-transition: .1s ease opacity;
|
||||
}
|
||||
|
||||
#loading.done {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 70px;
|
||||
text-align: center;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
-webkit-transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.spinner > div {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-color: #333;
|
||||
border-radius: 100%;
|
||||
display: inline-block;
|
||||
-webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
|
||||
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
|
||||
}
|
||||
|
||||
.spinner .bounce1 {
|
||||
-webkit-animation-delay: -0.32s;
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
.spinner .bounce2 {
|
||||
-webkit-animation-delay: -0.16s;
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
@-webkit-keyframes sk-bouncedelay {
|
||||
0%, 80%, 100% { -webkit-transform: scale(0) }
|
||||
40% { -webkit-transform: scale(1.0) }
|
||||
}
|
||||
|
||||
@keyframes sk-bouncedelay {
|
||||
0%, 80%, 100% {
|
||||
-webkit-transform: scale(0);
|
||||
transform: scale(0);
|
||||
} 40% {
|
||||
-webkit-transform: scale(1.0);
|
||||
transform: scale(1.0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<div id="loading">
|
||||
<div class="spinner">
|
||||
<div class="bounce1"></div>
|
||||
<div class="bounce2"></div>
|
||||
<div class="bounce3"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
[{[ if .CSS -]}]
|
||||
<link rel="stylesheet" href="/[{[ .StaticURL ]}]/custom.css" />
|
||||
[{[ end ]}]
|
||||
</body>
|
||||
</html>
|
||||
20
frontend/public/manifest.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "File Browser",
|
||||
"short_name": "File Browser",
|
||||
"icons": [
|
||||
{
|
||||
"src": "./img/icons/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./static/img/icons/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "#455a64"
|
||||
}
|
||||
23
frontend/src/App.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div>
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'app',
|
||||
mounted () {
|
||||
const loading = document.getElementById('loading')
|
||||
loading.classList.add('done')
|
||||
|
||||
setTimeout(function () {
|
||||
loading.parentNode.removeChild(loading)
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import './css/styles.css';
|
||||
</style>
|
||||
16
frontend/src/api/commands.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { removePrefix } from './utils'
|
||||
import { baseURL } from '@/utils/constants'
|
||||
import store from '@/store'
|
||||
|
||||
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}`
|
||||
|
||||
let conn = new window.WebSocket(url)
|
||||
conn.onopen = () => conn.send(command)
|
||||
conn.onmessage = onmessage
|
||||
conn.onclose = onclose
|
||||
}
|
||||
143
frontend/src/api/files.js
Normal file
@@ -0,0 +1,143 @@
|
||||
import { fetchURL, removePrefix } from './utils'
|
||||
import { baseURL } from '@/utils/constants'
|
||||
import store from '@/store'
|
||||
|
||||
export async function fetch (url) {
|
||||
url = removePrefix(url)
|
||||
|
||||
const res = await fetchURL(`/api/resources${url}`, {})
|
||||
|
||||
if (res.status === 200) {
|
||||
let data = await res.json()
|
||||
data.url = `/files${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)
|
||||
}
|
||||
}
|
||||
|
||||
async function resourceAction (url, method, content) {
|
||||
url = removePrefix(url)
|
||||
|
||||
let opts = { method }
|
||||
|
||||
if (content) {
|
||||
opts.body = content
|
||||
}
|
||||
|
||||
const res = await fetchURL(`/api/resources${url}`, opts)
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error(res.responseText)
|
||||
} else {
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove (url) {
|
||||
return resourceAction(url, 'DELETE')
|
||||
}
|
||||
|
||||
export async function put (url, content = '') {
|
||||
return resourceAction(url, 'PUT', content)
|
||||
}
|
||||
|
||||
export function download (format, ...files) {
|
||||
let url = `${baseURL}/api/raw`
|
||||
|
||||
if (files.length === 1) {
|
||||
url += removePrefix(files[0]) + '?'
|
||||
} else {
|
||||
let arg = ''
|
||||
|
||||
for (let file of files) {
|
||||
arg += removePrefix(file) + ','
|
||||
}
|
||||
|
||||
arg = arg.substring(0, arg.length - 1)
|
||||
arg = encodeURIComponent(arg)
|
||||
url += `/?files=${arg}&`
|
||||
}
|
||||
|
||||
if (format !== null) {
|
||||
url += `algo=${format}&`
|
||||
}
|
||||
|
||||
url += `auth=${store.state.jwt}`
|
||||
window.open(url)
|
||||
}
|
||||
|
||||
export async function post (url, content = '', overwrite = false, onupload) {
|
||||
url = removePrefix(url)
|
||||
|
||||
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)
|
||||
|
||||
if (typeof onupload === 'function') {
|
||||
request.upload.onprogress = onupload
|
||||
}
|
||||
|
||||
// Send a message to user before closing the tab during file upload
|
||||
window.onbeforeunload = () => "Files are being uploaded."
|
||||
|
||||
request.onload = () => {
|
||||
if (request.status === 200) {
|
||||
resolve(request.responseText)
|
||||
} else if (request.status === 409) {
|
||||
reject(request.status)
|
||||
} else {
|
||||
reject(request.responseText)
|
||||
}
|
||||
}
|
||||
|
||||
request.onerror = (error) => {
|
||||
reject(error)
|
||||
}
|
||||
|
||||
request.send(content)
|
||||
// Upload is done no more message before closing the tab
|
||||
}).finally(() => { window.onbeforeunload = null })
|
||||
}
|
||||
|
||||
function moveCopy (items, copy = 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}`
|
||||
promises.push(resourceAction(url, 'PATCH'))
|
||||
}
|
||||
|
||||
return Promise.all(promises)
|
||||
}
|
||||
|
||||
export function move (items) {
|
||||
return moveCopy(items)
|
||||
}
|
||||
|
||||
export function copy (items) {
|
||||
return moveCopy(items, true)
|
||||
}
|
||||
|
||||
export async function checksum (url, algo) {
|
||||
const data = await resourceAction(`${url}?checksum=${algo}`, 'GET')
|
||||
return (await data.json()).checksums[algo]
|
||||
}
|
||||
15
frontend/src/api/index.js
Normal file
@@ -0,0 +1,15 @@
|
||||
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'
|
||||
|
||||
export {
|
||||
files,
|
||||
share,
|
||||
users,
|
||||
settings,
|
||||
commands,
|
||||
search
|
||||
}
|
||||
8
frontend/src/api/search.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { fetchJSON, removePrefix } from './utils'
|
||||
|
||||
export default async function search (url, query) {
|
||||
url = removePrefix(url)
|
||||
query = encodeURIComponent(query)
|
||||
|
||||
return fetchJSON(`/api/search${url}?query=${query}`, {})
|
||||
}
|
||||
16
frontend/src/api/settings.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { fetchURL, fetchJSON } from './utils'
|
||||
|
||||
export function get () {
|
||||
return fetchJSON(`/api/settings`, {})
|
||||
}
|
||||
|
||||
export async function update (settings) {
|
||||
const res = await fetchURL(`/api/settings`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(settings)
|
||||
})
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error(res.status)
|
||||
}
|
||||
}
|
||||
32
frontend/src/api/share.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { fetchURL, fetchJSON, removePrefix } from './utils'
|
||||
|
||||
export async function getHash(hash) {
|
||||
return fetchJSON(`/api/public/share/${hash}`)
|
||||
}
|
||||
|
||||
export async function get(url) {
|
||||
url = removePrefix(url)
|
||||
return fetchJSON(`/api/share${url}`)
|
||||
}
|
||||
|
||||
export async function remove(hash) {
|
||||
const res = await fetchURL(`/api/share/${hash}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
|
||||
if (res.status !== 200) {
|
||||
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}`
|
||||
}
|
||||
|
||||
return fetchJSON(url, {
|
||||
method: 'POST'
|
||||
})
|
||||
}
|
||||
52
frontend/src/api/users.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import { fetchURL, fetchJSON } from './utils'
|
||||
|
||||
export async function getAll () {
|
||||
return fetchJSON(`/api/users`, {})
|
||||
}
|
||||
|
||||
export async function get (id) {
|
||||
return fetchJSON(`/api/users/${id}`, {})
|
||||
}
|
||||
|
||||
export async function create (user) {
|
||||
const res = await fetchURL(`/api/users`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
what: 'user',
|
||||
which: [],
|
||||
data: user
|
||||
})
|
||||
})
|
||||
|
||||
if (res.status === 201) {
|
||||
return res.headers.get('Location')
|
||||
} else {
|
||||
throw new Error(res.status)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export async function update (user, which = ['all']) {
|
||||
const res = await fetchURL(`/api/users/${user.id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
what: 'user',
|
||||
which: which,
|
||||
data: user
|
||||
})
|
||||
})
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error(res.status)
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove (id) {
|
||||
const res = await fetchURL(`/api/users/${id}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error(res.status)
|
||||
}
|
||||
}
|
||||
45
frontend/src/api/utils.js
Normal file
@@ -0,0 +1,45 @@
|
||||
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 || {}
|
||||
|
||||
let { headers, ...rest } = opts
|
||||
|
||||
const res = await fetch(`${baseURL}${url}`, {
|
||||
headers: {
|
||||
'X-Auth': store.state.jwt,
|
||||
...headers
|
||||
},
|
||||
...rest
|
||||
})
|
||||
|
||||
if (res.headers.get('X-Renew-Token') === 'true') {
|
||||
await renew(store.state.jwt)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
export async function fetchJSON (url, opts) {
|
||||
const res = await fetchURL(url, opts)
|
||||
|
||||
if (res.status === 200) {
|
||||
return res.json()
|
||||
} else {
|
||||
throw new Error(res.status)
|
||||
}
|
||||
}
|
||||
|
||||
export function removePrefix (url) {
|
||||
if (url.startsWith('/files')) {
|
||||
url = url.slice(6)
|
||||
}
|
||||
|
||||
if (url === '') url = '/'
|
||||
if (url[0] !== '/') url = '/' + url
|
||||
return url
|
||||
}
|
||||
|
||||
BIN
frontend/src/assets/fonts/roboto/medium-cyrillic-ext.woff2
Normal file
BIN
frontend/src/assets/fonts/roboto/medium-cyrillic.woff2
Normal file
BIN
frontend/src/assets/fonts/roboto/medium-greek-ext.woff2
Normal file
BIN
frontend/src/assets/fonts/roboto/medium-greek.woff2
Normal file
BIN
frontend/src/assets/fonts/roboto/medium-latin-ext.woff2
Normal file
BIN
frontend/src/assets/fonts/roboto/medium-latin.woff2
Normal file
BIN
frontend/src/assets/fonts/roboto/medium-vietnamese.woff2
Normal file
BIN
frontend/src/assets/fonts/roboto/normal-cyrillic-ext.woff2
Normal file
BIN
frontend/src/assets/fonts/roboto/normal-cyrillic.woff2
Normal file
BIN
frontend/src/assets/fonts/roboto/normal-greek-ext.woff2
Normal file
BIN
frontend/src/assets/fonts/roboto/normal-greek.woff2
Normal file
BIN
frontend/src/assets/fonts/roboto/normal-latin-ext.woff2
Normal file
BIN
frontend/src/assets/fonts/roboto/normal-latin.woff2
Normal file
BIN
frontend/src/assets/fonts/roboto/normal-vietnamese.woff2
Normal file
189
frontend/src/components/Header.vue
Normal file
@@ -0,0 +1,189 @@
|
||||
<template>
|
||||
<header>
|
||||
<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 v-show="showSaveButton" :aria-label="$t('buttons.save')" :title="$t('buttons.save')" class="action" id="save-button">
|
||||
<i class="material-icons">save</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-show="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="openSelect" :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 } 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',
|
||||
'isListing',
|
||||
'isLogged'
|
||||
]),
|
||||
...mapState([
|
||||
'req',
|
||||
'user',
|
||||
'loading',
|
||||
'reload',
|
||||
'multiple'
|
||||
]),
|
||||
logoURL: () => logoURL,
|
||||
isMobile () {
|
||||
return this.width <= 736
|
||||
},
|
||||
showUpload () {
|
||||
return this.isListing && this.user.perm.create
|
||||
},
|
||||
showSaveButton () {
|
||||
return this.isEditor && this.user.perm.modify
|
||||
},
|
||||
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')
|
||||
},
|
||||
openSelect () {
|
||||
this.$store.commit('multiple', true)
|
||||
this.resetPrompts()
|
||||
},
|
||||
resetPrompts () {
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
198
frontend/src/components/Search.vue
Normal file
@@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<div id="search" @click="open" v-bind:class="{ active , ongoing }">
|
||||
<div id="input">
|
||||
<button
|
||||
v-if="active"
|
||||
class="action"
|
||||
@click="close"
|
||||
:aria-label="$t('buttons.close')"
|
||||
:title="$t('buttons.close')"
|
||||
>
|
||||
<i class="material-icons">arrow_back</i>
|
||||
</button>
|
||||
<i v-else class="material-icons">search</i>
|
||||
<input
|
||||
type="text"
|
||||
@keyup.exact="keyup"
|
||||
@keyup.enter="submit"
|
||||
ref="input"
|
||||
:autofocus="active"
|
||||
v-model.trim="value"
|
||||
:aria-label="$t('search.search')"
|
||||
:placeholder="$t('search.search')"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div id="result" ref="result">
|
||||
<div>
|
||||
<template v-if="isEmpty">
|
||||
<p>{{ text }}</p>
|
||||
|
||||
<template v-if="value.length === 0">
|
||||
<div class="boxes">
|
||||
<h3>{{ $t('search.types') }}</h3>
|
||||
<div>
|
||||
<div
|
||||
tabindex="0"
|
||||
v-for="(v,k) in boxes"
|
||||
:key="k"
|
||||
role="button"
|
||||
@click="init('type:'+k)"
|
||||
:aria-label="$t('search.'+v.label)"
|
||||
>
|
||||
<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">
|
||||
<router-link @click.native="close" :to="'./' + s.path">
|
||||
<i v-if="s.dir" class="material-icons">folder</i>
|
||||
<i v-else class="material-icons">insert_drive_file</i>
|
||||
<span>./{{ s.path }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p id="renew">
|
||||
<i class="material-icons spin">autorenew</i>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
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" }
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "search",
|
||||
data: function() {
|
||||
return {
|
||||
value: "",
|
||||
active: false,
|
||||
ongoing: false,
|
||||
results: [],
|
||||
reload: false,
|
||||
resultsCount: 50,
|
||||
scrollable: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show (val, old) {
|
||||
this.active = val === "search"
|
||||
|
||||
if (old === "search" && !this.active) {
|
||||
if (this.reload) {
|
||||
this.setReload(true)
|
||||
}
|
||||
|
||||
document.body.style.overflow = "auto"
|
||||
this.reset()
|
||||
this.value = ''
|
||||
this.active = false
|
||||
this.$refs.input.blur()
|
||||
} else if (this.active) {
|
||||
this.reload = false
|
||||
this.$refs.input.focus()
|
||||
document.body.style.overflow = "hidden"
|
||||
}
|
||||
},
|
||||
value () {
|
||||
if (this.results.length) {
|
||||
this.reset()
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(["user", "show"]),
|
||||
...mapGetters(["isListing"]),
|
||||
boxes() {
|
||||
return boxes
|
||||
},
|
||||
isEmpty() {
|
||||
return this.results.length === 0
|
||||
},
|
||||
text() {
|
||||
if (this.ongoing) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return this.value === '' ? this.$t("search.typeToSearch") : this.$t("search.pressToSearch")
|
||||
},
|
||||
filteredResults () {
|
||||
return this.results.slice(0, this.resultsCount)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener("keydown", event => {
|
||||
if (event.keyCode === 27) {
|
||||
this.closeHovers()
|
||||
}
|
||||
})
|
||||
|
||||
this.$refs.result.addEventListener('scroll', event => {
|
||||
if (event.target.offsetHeight + event.target.scrollTop >= event.target.scrollHeight - 100) {
|
||||
this.resultsCount += 50
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(["showHover", "closeHovers", "setReload"]),
|
||||
open() {
|
||||
this.showHover("search")
|
||||
},
|
||||
close(event) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
this.closeHovers()
|
||||
},
|
||||
keyup(event) {
|
||||
if (event.keyCode === 27) {
|
||||
this.close(event)
|
||||
return
|
||||
}
|
||||
|
||||
this.results.length = 0
|
||||
},
|
||||
init (string) {
|
||||
this.value = `${string} `
|
||||
this.$refs.input.focus()
|
||||
},
|
||||
reset () {
|
||||
this.ongoing = false
|
||||
this.resultsCount = 50
|
||||
this.results = []
|
||||
},
|
||||
async submit(event) {
|
||||
event.preventDefault()
|
||||
|
||||
if (this.value === '') {
|
||||
return
|
||||
}
|
||||
|
||||
let path = this.$route.path
|
||||
if (!this.isListing) {
|
||||
path = url.removeLastDir(path) + "/"
|
||||
}
|
||||
|
||||
this.ongoing = true
|
||||
|
||||
|
||||
this.results = await search(path, this.value)
|
||||
this.ongoing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
115
frontend/src/components/Shell.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<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>
|
||||
<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>
|
||||
<pre
|
||||
tabindex="0"
|
||||
ref="input"
|
||||
class="shell__text"
|
||||
contenteditable="true"
|
||||
@keydown.prevent.38="historyUp"
|
||||
@keydown.prevent.40="historyDown"
|
||||
@keypress.prevent.enter="submit" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapMutations, mapState, mapGetters } from 'vuex'
|
||||
import { commands } from '@/api'
|
||||
|
||||
export default {
|
||||
name: 'shell',
|
||||
computed: {
|
||||
...mapState([ 'user', 'showShell' ]),
|
||||
...mapGetters([ 'isFiles', 'isLogged' ]),
|
||||
path: function () {
|
||||
if (this.isFiles) {
|
||||
return this.$route.path
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
content: [],
|
||||
history: [],
|
||||
historyPos: 0,
|
||||
canInput: true
|
||||
}),
|
||||
methods: {
|
||||
...mapMutations([ 'toggleShell' ]),
|
||||
scroll: function () {
|
||||
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight
|
||||
},
|
||||
focus: function () {
|
||||
this.$refs.input.focus()
|
||||
},
|
||||
historyUp () {
|
||||
if (this.historyPos > 0) {
|
||||
this.$refs.input.innerText = this.history[--this.historyPos]
|
||||
this.focus()
|
||||
}
|
||||
},
|
||||
historyDown () {
|
||||
if (this.historyPos >= 0 && this.historyPos < this.history.length - 1) {
|
||||
this.$refs.input.innerText = this.history[++this.historyPos]
|
||||
this.focus()
|
||||
} else {
|
||||
this.historyPos = this.history.length
|
||||
this.$refs.input.innerText = ''
|
||||
}
|
||||
},
|
||||
submit: function (event) {
|
||||
const cmd = event.target.innerText.trim()
|
||||
|
||||
if (cmd === '') {
|
||||
return
|
||||
}
|
||||
|
||||
if (cmd === 'clear') {
|
||||
this.content = []
|
||||
event.target.innerHTML = ''
|
||||
return
|
||||
}
|
||||
|
||||
if (cmd === 'exit') {
|
||||
event.target.innerHTML = ''
|
||||
this.toggleShell()
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
commands(
|
||||
this.path,
|
||||
cmd,
|
||||
event => {
|
||||
results.text += `${event.data}\n`
|
||||
this.scroll()
|
||||
},
|
||||
() => {
|
||||
results.text = results.text.trimEnd()
|
||||
this.canInput = true
|
||||
this.$refs.input.focus()
|
||||
this.scroll()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
81
frontend/src/components/Sidebar.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<nav :class="{active}">
|
||||
<template v-if="isLogged">
|
||||
<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>
|
||||
</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')">
|
||||
<i class="material-icons">create_new_folder</i>
|
||||
<span>{{ $t('sidebar.newFolder') }}</span>
|
||||
</button>
|
||||
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<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>
|
||||
</router-link>
|
||||
|
||||
<button v-if="!noAuth" @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>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<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>
|
||||
</router-link>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
<span> {{ version }}</span>
|
||||
</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 } from '@/utils/constants'
|
||||
|
||||
export default {
|
||||
name: 'sidebar',
|
||||
computed: {
|
||||
...mapState([ 'user' ]),
|
||||
...mapGetters([ 'isLogged' ]),
|
||||
active () {
|
||||
return this.$store.state.show === 'sidebar'
|
||||
},
|
||||
signup: () => signup,
|
||||
version: () => version,
|
||||
disableExternal: () => disableExternal,
|
||||
noAuth: () => noAuth
|
||||
},
|
||||
methods: {
|
||||
help () {
|
||||
this.$store.commit('showHover', 'help')
|
||||
},
|
||||
logout: auth.logout
|
||||
}
|
||||
}
|
||||
</script>
|
||||
17
frontend/src/components/buttons/Copy.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<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>
|
||||
17
frontend/src/components/buttons/Delete.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<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>
|
||||
35
frontend/src/components/buttons/Download.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<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>
|
||||
17
frontend/src/components/buttons/Info.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<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>
|
||||
17
frontend/src/components/buttons/Move.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<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>
|
||||
17
frontend/src/components/buttons/Rename.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<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>
|
||||
17
frontend/src/components/buttons/Share.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<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>
|
||||
17
frontend/src/components/buttons/Shell.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<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>
|
||||
40
frontend/src/components/buttons/SwitchView.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<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>
|
||||
17
frontend/src/components/buttons/Upload.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<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 () {
|
||||
document.getElementById('upload-input').click()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
75
frontend/src/components/files/Editor.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<form id="editor"></form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import { files as api } from '@/api'
|
||||
import buttons from '@/utils/buttons'
|
||||
|
||||
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'
|
||||
|
||||
export default {
|
||||
name: 'editor',
|
||||
computed: {
|
||||
...mapState(['req'])
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
content: null,
|
||||
editor: null
|
||||
}
|
||||
},
|
||||
created () {
|
||||
window.addEventListener('keydown', this.keyEvent)
|
||||
document.getElementById('save-button').addEventListener('click', this.save)
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.keyEvent)
|
||||
document.getElementById('save-button').removeEventListener('click', this.save)
|
||||
},
|
||||
mounted: function () {
|
||||
if (this.req.content === undefined || this.req.content === null) {
|
||||
this.req.content = ''
|
||||
}
|
||||
|
||||
this.editor = ace.edit('editor', {
|
||||
maxLines: Infinity,
|
||||
minLines: 20,
|
||||
value: this.req.content,
|
||||
showPrintMargin: false,
|
||||
readOnly: this.req.type === 'textImmutable',
|
||||
theme: 'ace/theme/chrome',
|
||||
mode: modelist.getModeForPath(this.req.name).mode
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
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>
|
||||
193
frontend/src/components/files/ExtendedImage.vue
Normal file
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<div
|
||||
class="image-ex-container"
|
||||
ref="container"
|
||||
@touchstart="touchStart"
|
||||
@touchmove="touchMove"
|
||||
@dblclick="zoomAuto"
|
||||
@mousedown="mousedownStart"
|
||||
@mousemove="mouseMove"
|
||||
@mouseup="mouseUp"
|
||||
@wheel="wheelMove"
|
||||
>
|
||||
<img :src="src" class="image-ex-img" ref="imgex" @load="setCenter">
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
src: String,
|
||||
moveDisabledTime: {
|
||||
type: Number,
|
||||
default: () => 200
|
||||
},
|
||||
maxScale: {
|
||||
type: Number,
|
||||
default: () => 4
|
||||
},
|
||||
minScale: {
|
||||
type: Number,
|
||||
default: () => 0.25
|
||||
},
|
||||
classList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
zoomStep: {
|
||||
type: Number,
|
||||
default: () => 0.25
|
||||
},
|
||||
autofill: {
|
||||
type: Boolean,
|
||||
default: () => false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
scale: 1,
|
||||
lastX: null,
|
||||
lastY: null,
|
||||
inDrag: false,
|
||||
lastTouchDistance: 0,
|
||||
moveDisabled: false,
|
||||
disabledTimer: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
let container = this.$refs.container
|
||||
this.classList.forEach(className => container.classList.add(className))
|
||||
// set width and height if they are zero
|
||||
if (getComputedStyle(container).width === "0px") {
|
||||
container.style.width = "100%"
|
||||
}
|
||||
if (getComputedStyle(container).height === "0px") {
|
||||
container.style.height = "100%"
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setCenter() {
|
||||
let container = this.$refs.container
|
||||
let img = this.$refs.imgex
|
||||
|
||||
let rate = Math.min(
|
||||
container.clientWidth / img.clientWidth,
|
||||
container.clientHeight / img.clientHeight
|
||||
)
|
||||
if (!this.autofill && rate > 1) {
|
||||
rate = 1
|
||||
}
|
||||
// height will be auto set
|
||||
img.width = Math.floor(img.clientWidth * rate)
|
||||
img.style.top = `${Math.floor((container.clientHeight - img.clientHeight) / 2)}px`
|
||||
img.style.left = `${Math.floor((container.clientWidth - img.clientWidth) / 2)}px`
|
||||
document.addEventListener('mouseup', () => this.inDrag = false )
|
||||
},
|
||||
mousedownStart(event) {
|
||||
this.lastX = null
|
||||
this.lastY = null
|
||||
this.inDrag = true
|
||||
event.preventDefault()
|
||||
},
|
||||
mouseMove(event) {
|
||||
if (!this.inDrag) return
|
||||
this.doMove(event.movementX, event.movementY)
|
||||
event.preventDefault()
|
||||
},
|
||||
mouseUp(event) {
|
||||
this.inDrag = false
|
||||
event.preventDefault()
|
||||
},
|
||||
touchStart(event) {
|
||||
this.lastX = null
|
||||
this.lastY = null
|
||||
this.lastTouchDistance = null
|
||||
event.preventDefault()
|
||||
},
|
||||
zoomAuto(event) {
|
||||
switch (this.scale) {
|
||||
case 1:
|
||||
this.scale = 2
|
||||
break
|
||||
case 2:
|
||||
this.scale = 4
|
||||
break
|
||||
default:
|
||||
case 4:
|
||||
this.scale = 1
|
||||
break
|
||||
}
|
||||
this.setZoom()
|
||||
event.preventDefault()
|
||||
},
|
||||
touchMove(event) {
|
||||
event.preventDefault()
|
||||
if (this.lastX === null) {
|
||||
this.lastX = event.targetTouches[0].pageX
|
||||
this.lastY = event.targetTouches[0].pageY
|
||||
return
|
||||
}
|
||||
let step = this.$refs.imgex.width / 5
|
||||
if (event.targetTouches.length === 2) {
|
||||
this.moveDisabled = true
|
||||
clearTimeout(this.disabledTimer)
|
||||
this.disabledTimer = setTimeout(
|
||||
() => (this.moveDisabled = false),
|
||||
this.moveDisabledTime
|
||||
)
|
||||
|
||||
let p1 = event.targetTouches[0]
|
||||
let p2 = event.targetTouches[1]
|
||||
let touchDistance = Math.sqrt(
|
||||
Math.pow(p2.pageX - p1.pageX, 2) + Math.pow(p2.pageY - p1.pageY, 2)
|
||||
)
|
||||
if (!this.lastTouchDistance) {
|
||||
this.lastTouchDistance = touchDistance
|
||||
return
|
||||
}
|
||||
this.scale += (touchDistance - this.lastTouchDistance) / step
|
||||
this.lastTouchDistance = touchDistance
|
||||
this.setZoom()
|
||||
} else if (event.targetTouches.length === 1) {
|
||||
if (this.moveDisabled) return
|
||||
let x = event.targetTouches[0].pageX - this.lastX
|
||||
let y = event.targetTouches[0].pageY - this.lastY
|
||||
if (Math.abs(x) >= step && Math.abs(y) >= step) return
|
||||
this.lastX = event.targetTouches[0].pageX
|
||||
this.lastY = event.targetTouches[0].pageY
|
||||
this.doMove(x, y)
|
||||
}
|
||||
},
|
||||
doMove(x, y) {
|
||||
let style = this.$refs.imgex.style
|
||||
style.left = `${this.pxStringToNumber(style.left) + x}px`
|
||||
style.top = `${this.pxStringToNumber(style.top) + y}px`
|
||||
},
|
||||
wheelMove(event) {
|
||||
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})`
|
||||
},
|
||||
pxStringToNumber(style) {
|
||||
return +style.replace("px", "")
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.image-ex-container {
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.image-ex-img {
|
||||
left: 0;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
</style>
|
||||
428
frontend/src/components/files/Listing.vue
Normal file
@@ -0,0 +1,428 @@
|
||||
<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>
|
||||
</div>
|
||||
<div v-else id="listing"
|
||||
:class="user.viewMode"
|
||||
@dragenter="dragEnter"
|
||||
@dragend="dragEnd">
|
||||
<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>
|
||||
|
||||
<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 buttons from '@/utils/buttons'
|
||||
import url from '@/utils/url'
|
||||
|
||||
export default {
|
||||
name: 'listing',
|
||||
components: { Item },
|
||||
data: function () {
|
||||
return {
|
||||
show: 50
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['req', 'selected', 'user']),
|
||||
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.show)
|
||||
},
|
||||
files () {
|
||||
let show = this.show - this.items.dirs.length
|
||||
|
||||
if (show < 0) show = 0
|
||||
|
||||
return this.items.files.slice(0, show)
|
||||
},
|
||||
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('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('drop', this.drop)
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([ 'updateUser' ]),
|
||||
base64: function (name) {
|
||||
return window.btoa(unescape(encodeURIComponent(name)))
|
||||
},
|
||||
keyEvent (event) {
|
||||
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
|
||||
}
|
||||
},
|
||||
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
|
||||
})
|
||||
},
|
||||
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 })
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.$store.state.clipboard.key === 'x') {
|
||||
api.move(items).then(() => {
|
||||
this.$store.commit('setReload', true)
|
||||
}).catch(this.$showError)
|
||||
return
|
||||
}
|
||||
|
||||
api.copy(items).then(() => {
|
||||
this.$store.commit('setReload', true)
|
||||
}).catch(this.$showError)
|
||||
},
|
||||
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.show += 50
|
||||
}
|
||||
},
|
||||
dragEnter () {
|
||||
// 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
|
||||
})
|
||||
},
|
||||
dragEnd () {
|
||||
this.resetOpacity()
|
||||
},
|
||||
drop: function (event) {
|
||||
event.preventDefault()
|
||||
this.resetOpacity()
|
||||
|
||||
let dt = event.dataTransfer
|
||||
let files = dt.files
|
||||
let el = event.target
|
||||
|
||||
if (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 + '/'
|
||||
}
|
||||
|
||||
if (base !== '') {
|
||||
api.fetch(this.$route.path + base)
|
||||
.then(req => {
|
||||
this.checkConflict(files, req.items, base)
|
||||
})
|
||||
.catch(this.$showError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this.checkConflict(files, this.req.items, base)
|
||||
},
|
||||
checkConflict (files, items, base) {
|
||||
if (typeof items === 'undefined' || items === null) {
|
||||
items = []
|
||||
}
|
||||
|
||||
let conflict = false
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
let res = items.findIndex(function hasConflict (element) {
|
||||
return (element.name === this)
|
||||
}, files[i].name)
|
||||
|
||||
if (res >= 0) {
|
||||
conflict = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!conflict) {
|
||||
this.handleFiles(files, base)
|
||||
return
|
||||
}
|
||||
|
||||
this.$store.commit('showHover', {
|
||||
prompt: 'replace',
|
||||
confirm: (event) => {
|
||||
event.preventDefault()
|
||||
this.$store.commit('closeHovers')
|
||||
this.handleFiles(files, base, true)
|
||||
}
|
||||
})
|
||||
},
|
||||
uploadInput (event) {
|
||||
this.checkConflict(event.currentTarget.files, this.req.items, '')
|
||||
},
|
||||
resetOpacity () {
|
||||
let items = document.getElementsByClassName('item')
|
||||
|
||||
Array.from(items).forEach(file => {
|
||||
file.style.opacity = 1
|
||||
})
|
||||
},
|
||||
handleFiles (files, base, overwrite = false) {
|
||||
buttons.loading('upload')
|
||||
let promises = []
|
||||
let progress = new Array(files.length).fill(0)
|
||||
|
||||
let onupload = (id) => (event) => {
|
||||
progress[id] = (event.loaded / event.total) * 100
|
||||
|
||||
let sum = 0
|
||||
for (let i = 0; i < progress.length; i++) {
|
||||
sum += progress[i]
|
||||
}
|
||||
|
||||
this.$store.commit('setProgress', Math.ceil(sum / progress.length))
|
||||
}
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
let file = files[i]
|
||||
let filenameEncoded = url.encodeRFC5987ValueChars(file.name)
|
||||
promises.push(api.post(this.$route.path + base + filenameEncoded, file, overwrite, onupload(i)))
|
||||
}
|
||||
|
||||
let finish = () => {
|
||||
buttons.success('upload')
|
||||
this.$store.commit('setProgress', 0)
|
||||
}
|
||||
|
||||
Promise.all(promises)
|
||||
.then(() => {
|
||||
finish()
|
||||
this.$store.commit('setReload', true)
|
||||
})
|
||||
.catch(error => {
|
||||
finish()
|
||||
this.$showError(error)
|
||||
})
|
||||
|
||||
return false
|
||||
},
|
||||
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>
|
||||
169
frontend/src/components/files/ListingItem.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<div class="item"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
draggable="true"
|
||||
@dragstart="dragStart"
|
||||
@dragover="dragOver"
|
||||
@drop="drop"
|
||||
@click="click"
|
||||
@dblclick="open"
|
||||
@touchstart="touchstart"
|
||||
:data-dir="isDir"
|
||||
:aria-label="name"
|
||||
:aria-selected="isSelected">
|
||||
<div>
|
||||
<i class="material-icons">{{ icon }}</i>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="name">{{ name }}</p>
|
||||
|
||||
<p v-if="isDir" class="size" data-order="-1">—</p>
|
||||
<p v-else class="size" :data-order="humanSize()">{{ humanSize() }}</p>
|
||||
|
||||
<p class="modified">
|
||||
<time :datetime="modified">{{ humanTime() }}</time>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapMutations, mapGetters, mapState } from 'vuex'
|
||||
import filesize from 'filesize'
|
||||
import moment from 'moment'
|
||||
import { files as api } from '@/api'
|
||||
|
||||
export default {
|
||||
name: 'item',
|
||||
data: function () {
|
||||
return {
|
||||
touches: 0
|
||||
}
|
||||
},
|
||||
props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index'],
|
||||
computed: {
|
||||
...mapState(['selected', 'req']),
|
||||
...mapGetters(['selectedCount']),
|
||||
isSelected () {
|
||||
return (this.selected.indexOf(this.index) !== -1)
|
||||
},
|
||||
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
|
||||
|
||||
for (let i of this.selected) {
|
||||
if (this.req.items[i].url === this.url) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(['addSelected', 'removeSelected', 'resetSelected']),
|
||||
humanSize: function () {
|
||||
return filesize(this.size)
|
||||
},
|
||||
humanTime: function () {
|
||||
return moment(this.modified).fromNow()
|
||||
},
|
||||
dragStart: function () {
|
||||
if (this.selectedCount === 0) {
|
||||
this.addSelected(this.index)
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.isSelected) {
|
||||
this.resetSelected()
|
||||
this.addSelected(this.index)
|
||||
}
|
||||
},
|
||||
dragOver: function (event) {
|
||||
if (!this.canDrop) return
|
||||
|
||||
event.preventDefault()
|
||||
let el = event.target
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (!el.classList.contains('item')) {
|
||||
el = el.parentElement
|
||||
}
|
||||
}
|
||||
|
||||
el.style.opacity = 1
|
||||
},
|
||||
drop: function (event) {
|
||||
if (!this.canDrop) return
|
||||
event.preventDefault()
|
||||
|
||||
if (this.selectedCount === 0) return
|
||||
|
||||
let items = []
|
||||
|
||||
for (let i of this.selected) {
|
||||
items.push({
|
||||
from: this.req.items[i].url,
|
||||
to: this.url + this.req.items[i].name
|
||||
})
|
||||
}
|
||||
|
||||
api.move(items)
|
||||
.then(() => {
|
||||
this.$store.commit('setReload', true)
|
||||
})
|
||||
.catch(this.$showError)
|
||||
},
|
||||
click: function (event) {
|
||||
if (this.selectedCount !== 0) event.preventDefault()
|
||||
if (this.$store.state.selected.indexOf(this.index) !== -1) {
|
||||
this.removeSelected(this.index)
|
||||
return
|
||||
}
|
||||
|
||||
if (event.shiftKey && this.selected.length === 1) {
|
||||
let fi = 0
|
||||
let la = 0
|
||||
|
||||
if (this.index > this.selected[0]) {
|
||||
fi = this.selected[0] + 1
|
||||
la = this.index
|
||||
} else {
|
||||
fi = this.index
|
||||
la = this.selected[0] - 1
|
||||
}
|
||||
|
||||
for (; fi <= la; fi++) {
|
||||
this.addSelected(fi)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (!event.ctrlKey && !this.$store.state.multiple) this.resetSelected()
|
||||
this.addSelected(this.index)
|
||||
},
|
||||
touchstart () {
|
||||
setTimeout(() => {
|
||||
this.touches = 0
|
||||
}, 300)
|
||||
|
||||
this.touches++
|
||||
if (this.touches > 1) {
|
||||
this.open()
|
||||
}
|
||||
},
|
||||
open: function () {
|
||||
this.$router.push({path: this.url})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
160
frontend/src/components/files/Preview.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<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>
|
||||
|
||||
<rename-button v-if="user.perm.rename"></rename-button>
|
||||
<delete-button v-if="user.perm.delete"></delete-button>
|
||||
<download-button v-if="user.perm.download"></download-button>
|
||||
<info-button></info-button>
|
||||
</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>
|
||||
|
||||
<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 == '.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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import url from '@/utils/url'
|
||||
import { baseURL } from '@/utils/constants'
|
||||
import { files as api } from '@/api'
|
||||
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: {
|
||||
InfoButton,
|
||||
DeleteButton,
|
||||
RenameButton,
|
||||
DownloadButton,
|
||||
ExtendedImage
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
previousLink: '',
|
||||
nextLink: '',
|
||||
listing: null,
|
||||
subtitles: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['req', 'user', 'oldReq', 'jwt']),
|
||||
hasPrevious () {
|
||||
return (this.previousLink !== '')
|
||||
},
|
||||
hasNext () {
|
||||
return (this.nextLink !== '')
|
||||
},
|
||||
download () {
|
||||
return `${baseURL}/api/raw${this.req.path}?auth=${this.jwt}`
|
||||
},
|
||||
raw () {
|
||||
return `${this.download}&inline=true`
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
window.addEventListener('keyup', this.key)
|
||||
|
||||
if (this.req.subtitles) {
|
||||
this.subtitles = this.req.subtitles.map(sub => `${baseURL}/api/raw${sub}?auth=${this.jwt}&inline=true`)
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.oldReq.items) {
|
||||
this.updateLinks(this.oldReq.items)
|
||||
} else {
|
||||
const path = url.removeLastDir(this.$route.path)
|
||||
const res = await api.fetch(path)
|
||||
this.updateLinks(res.items)
|
||||
}
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
}
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keyup', this.key)
|
||||
},
|
||||
methods: {
|
||||
back () {
|
||||
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) {
|
||||
event.preventDefault()
|
||||
|
||||
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()
|
||||
}
|
||||
},
|
||||
updateLinks (items) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].name !== this.req.name) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (let j = i - 1; j >= 0; j--) {
|
||||
if (mediaTypes.includes(items[j].type)) {
|
||||
this.previousLink = items[j].url
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for (let j = i + 1; j < items.length; j++) {
|
||||
if (mediaTypes.includes(items[j].type)) {
|
||||
this.nextLink = items[j].url
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
67
frontend/src/components/prompts/Copy.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.copy') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<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"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="button button--flat"
|
||||
@click="copy"
|
||||
:disabled="$route.path === dest"
|
||||
:aria-label="$t('buttons.copy')"
|
||||
: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'
|
||||
|
||||
export default {
|
||||
name: 'copy',
|
||||
components: { FileList },
|
||||
data: function () {
|
||||
return {
|
||||
current: window.location.pathname,
|
||||
dest: null
|
||||
}
|
||||
},
|
||||
computed: mapState(['req', 'selected']),
|
||||
methods: {
|
||||
copy: async function (event) {
|
||||
event.preventDefault()
|
||||
buttons.loading('copy')
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
await api.copy(items)
|
||||
buttons.success('copy')
|
||||
this.$router.push({ path: this.dest })
|
||||
} catch (e) {
|
||||
buttons.done('copy')
|
||||
this.$showError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
66
frontend/src/components/prompts/Delete.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<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>
|
||||
</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 {mapGetters, mapMutations, mapState} from 'vuex'
|
||||
import { files as api } from '@/api'
|
||||
import url from '@/utils/url'
|
||||
import buttons from '@/utils/buttons'
|
||||
|
||||
export default {
|
||||
name: 'delete',
|
||||
computed: {
|
||||
...mapGetters(['isListing', 'selectedCount']),
|
||||
...mapState(['req', 'selected'])
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(['closeHovers']),
|
||||
submit: async function () {
|
||||
this.closeHovers()
|
||||
buttons.loading('delete')
|
||||
|
||||
try {
|
||||
if (!this.isListing) {
|
||||
await api.remove(this.$route.path)
|
||||
buttons.success('delete')
|
||||
this.$router.push({ path: url.removeLastDir(this.$route.path) + '/' })
|
||||
return
|
||||
}
|
||||
|
||||
if (this.selectedCount === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
let promises = []
|
||||
for (let index of this.selected) {
|
||||
promises.push(api.remove(this.req.items[index].url))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
49
frontend/src/components/prompts/Download.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div class="card floating" id="download">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.download') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapGetters, mapState} from 'vuex'
|
||||
import { files as api } from '@/api'
|
||||
|
||||
export default {
|
||||
name: 'download',
|
||||
computed: {
|
||||
...mapState(['selected', 'req']),
|
||||
...mapGetters(['selectedCount'])
|
||||
},
|
||||
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')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
140
frontend/src/components/prompts/FileList.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<div>
|
||||
<ul class="file-list">
|
||||
<li @click="select"
|
||||
@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>
|
||||
</ul>
|
||||
|
||||
<p>{{ $t('prompts.currentlyNavigating') }} <code>{{ nav }}</code>.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import url from '@/utils/url'
|
||||
import { files } from '@/api'
|
||||
|
||||
export default {
|
||||
name: 'file-list',
|
||||
data: function () {
|
||||
return {
|
||||
items: [],
|
||||
touches: {
|
||||
id: '',
|
||||
count: 0
|
||||
},
|
||||
selected: null,
|
||||
current: window.location.pathname
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState([ 'req' ]),
|
||||
nav () {
|
||||
return decodeURIComponent(this.current)
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
// If we're showing this on a listing,
|
||||
// we can use the current request object
|
||||
// to fill the move options.
|
||||
if (this.req.kind === 'listing') {
|
||||
this.fillOptions(this.req)
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, we must be on a preview or editor
|
||||
// so we fetch the data from the previous directory.
|
||||
files.fetch(url.removeLastDir(this.$route.path))
|
||||
.then(this.fillOptions)
|
||||
.catch(this.$showError)
|
||||
},
|
||||
methods: {
|
||||
fillOptions (req) {
|
||||
// Sets the current path and resets
|
||||
// the current items.
|
||||
this.current = req.url
|
||||
this.items = []
|
||||
|
||||
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/') {
|
||||
this.items.push({
|
||||
name: '..',
|
||||
url: url.removeLastDir(req.url) + '/'
|
||||
})
|
||||
}
|
||||
|
||||
// If this folder is empty, finish here.
|
||||
if (req.items === null) return
|
||||
|
||||
// Otherwise we add every directory to the
|
||||
// move options.
|
||||
for (let item of req.items) {
|
||||
if (!item.isDir) continue
|
||||
|
||||
this.items.push({
|
||||
name: item.name,
|
||||
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
|
||||
|
||||
files.fetch(uri)
|
||||
.then(this.fillOptions)
|
||||
.catch(this.$showError)
|
||||
},
|
||||
touchstart (event) {
|
||||
let url = event.currentTarget.dataset.url
|
||||
|
||||
// In 300 milliseconds, we shall reset the count.
|
||||
setTimeout(() => {
|
||||
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.count++
|
||||
|
||||
// If there is more than one touch already,
|
||||
// open the next screen.
|
||||
if (this.touches.count > 1) {
|
||||
this.next(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
|
||||
}
|
||||
|
||||
// Otherwise select the element.
|
||||
this.selected = event.currentTarget.dataset.url
|
||||
this.$emit('update:selected', this.selected)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
34
frontend/src/components/prompts/Help.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="card floating help">
|
||||
<div class="card-title">
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button type="submit"
|
||||
@click="$store.commit('closeHovers')"
|
||||
class="button button--flat"
|
||||
:aria-label="$t('buttons.ok')"
|
||||
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default { name: 'help' }
|
||||
</script>
|
||||
|
||||
98
frontend/src/components/prompts/Info.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<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 < 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>
|
||||
</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>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button type="submit"
|
||||
@click="$store.commit('closeHovers')"
|
||||
class="button button--flat"
|
||||
:aria-label="$t('buttons.ok')"
|
||||
: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'
|
||||
|
||||
export default {
|
||||
name: 'info',
|
||||
computed: {
|
||||
...mapState(['req', 'selected']),
|
||||
...mapGetters(['selectedCount', 'isListing']),
|
||||
humanSize: function () {
|
||||
if (this.selectedCount === 0 || !this.isListing) {
|
||||
return filesize(this.req.size)
|
||||
}
|
||||
|
||||
let sum = 0
|
||||
|
||||
for (let selected of this.selected) {
|
||||
sum += this.req.items[selected].size
|
||||
}
|
||||
|
||||
return filesize(sum)
|
||||
},
|
||||
humanTime: function () {
|
||||
if (this.selectedCount === 0) {
|
||||
return moment(this.req.modified).fromNow()
|
||||
}
|
||||
|
||||
return moment(this.req.items[this.selected[0]]).fromNow()
|
||||
},
|
||||
name: function () {
|
||||
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)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
checksum: async function (event, algo) {
|
||||
event.preventDefault()
|
||||
|
||||
let link
|
||||
|
||||
if (this.selectedCount) {
|
||||
link = this.req.items[this.selected[0]].url
|
||||
} else {
|
||||
link = this.$route.path
|
||||
}
|
||||
|
||||
try {
|
||||
const hash = await api.checksum(link, algo)
|
||||
event.target.innerHTML = hash
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
67
frontend/src/components/prompts/Move.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.move') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<file-list @update:selected="val => dest = val"></file-list>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="button button--flat button--grey"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="button button--flat"
|
||||
@click="move"
|
||||
:disabled="$route.path === dest"
|
||||
:aria-label="$t('buttons.move')"
|
||||
:title="$t('buttons.move')">{{ $t('buttons.move') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import FileList from './FileList'
|
||||
import { files as api } from '@/api'
|
||||
import buttons from '@/utils/buttons'
|
||||
|
||||
export default {
|
||||
name: 'move',
|
||||
components: { FileList },
|
||||
data: function () {
|
||||
return {
|
||||
current: window.location.pathname,
|
||||
dest: null
|
||||
}
|
||||
},
|
||||
computed: mapState(['req', 'selected']),
|
||||
methods: {
|
||||
move: async function (event) {
|
||||
event.preventDefault()
|
||||
buttons.loading('move')
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
api.move(items)
|
||||
buttons.success('move')
|
||||
this.$router.push({ path: this.dest })
|
||||
} catch (e) {
|
||||
buttons.done('move')
|
||||
this.$showError(e)
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
71
frontend/src/components/prompts/NewDir.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button
|
||||
class="button button--flat button--grey"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')"
|
||||
>{{ $t('buttons.cancel') }}</button>
|
||||
<button
|
||||
class="button button--flat"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')"
|
||||
@click="submit"
|
||||
>{{ $t('buttons.create') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import { files as api } from '@/api'
|
||||
import url from '@/utils/url'
|
||||
|
||||
export default {
|
||||
name: 'new-dir',
|
||||
data: function() {
|
||||
return {
|
||||
name: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([ 'isFiles', 'isListing' ])
|
||||
},
|
||||
methods: {
|
||||
submit: async function(event) {
|
||||
event.preventDefault()
|
||||
if (this.new === '') return
|
||||
|
||||
// Build the path of the new directory.
|
||||
let uri = this.isFiles ? this.$route.path + '/' : '/'
|
||||
|
||||
if (!this.isListing) {
|
||||
uri = url.removeLastDir(uri) + '/'
|
||||
}
|
||||
|
||||
uri += encodeURIComponent(this.name) + '/'
|
||||
uri = uri.replace('//', '/')
|
||||
|
||||
try {
|
||||
await api.post(uri)
|
||||
this.$router.push({ path: uri })
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
}
|
||||
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
71
frontend/src/components/prompts/NewFile.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<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">
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button
|
||||
class="button button--flat button--grey"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')"
|
||||
>{{ $t('buttons.cancel') }}</button>
|
||||
<button
|
||||
class="button button--flat"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')"
|
||||
>{{ $t('buttons.create') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import { files as api } from '@/api'
|
||||
import url from '@/utils/url'
|
||||
|
||||
export default {
|
||||
name: 'new-file',
|
||||
data: function() {
|
||||
return {
|
||||
name: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([ 'isFiles', 'isListing' ])
|
||||
},
|
||||
methods: {
|
||||
submit: async function(event) {
|
||||
event.preventDefault()
|
||||
if (this.new === '') return
|
||||
|
||||
// Build the path of the new directory.
|
||||
let uri = this.isFiles ? this.$route.path + '/' : '/'
|
||||
|
||||
if (!this.isListing) {
|
||||
uri = url.removeLastDir(uri) + '/'
|
||||
}
|
||||
|
||||
uri += encodeURIComponent(this.name)
|
||||
uri = uri.replace('//', '/')
|
||||
|
||||
try {
|
||||
await api.post(uri)
|
||||
this.$router.push({ path: uri })
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
}
|
||||
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
79
frontend/src/components/prompts/Prompts.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div>
|
||||
<help v-if="showHelp" ></help>
|
||||
<download v-else-if="showDownload"></download>
|
||||
<new-file v-else-if="showNewFile"></new-file>
|
||||
<new-dir v-else-if="showNewDir"></new-dir>
|
||||
<rename v-else-if="showRename"></rename>
|
||||
<delete v-else-if="showDelete"></delete>
|
||||
<info v-else-if="showInfo"></info>
|
||||
<move v-else-if="showMove"></move>
|
||||
<copy v-else-if="showCopy"></copy>
|
||||
<replace v-else-if="showReplace"></replace>
|
||||
<share v-else-if="show === 'share'"></share>
|
||||
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Help from './Help'
|
||||
import Info from './Info'
|
||||
import Delete from './Delete'
|
||||
import Rename from './Rename'
|
||||
import Download from './Download'
|
||||
import Move from './Move'
|
||||
import Copy from './Copy'
|
||||
import NewFile from './NewFile'
|
||||
import NewDir from './NewDir'
|
||||
import Replace from './Replace'
|
||||
import Share from './Share'
|
||||
import { mapState } from 'vuex'
|
||||
import buttons from '@/utils/buttons'
|
||||
|
||||
export default {
|
||||
name: 'prompts',
|
||||
components: {
|
||||
Info,
|
||||
Delete,
|
||||
Rename,
|
||||
Download,
|
||||
Move,
|
||||
Copy,
|
||||
Share,
|
||||
NewFile,
|
||||
NewDir,
|
||||
Help,
|
||||
Replace
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
pluginData: {
|
||||
buttons,
|
||||
'store': this.$store,
|
||||
'router': this.$router
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['show', 'plugins']),
|
||||
showInfo: function () { return this.show === 'info' },
|
||||
showHelp: function () { return this.show === 'help' },
|
||||
showDelete: function () { return this.show === 'delete' },
|
||||
showRename: function () { return this.show === 'rename' },
|
||||
showMove: function () { return this.show === 'move' },
|
||||
showCopy: function () { return this.show === 'copy' },
|
||||
showNewFile: function () { return this.show === 'newFile' },
|
||||
showNewDir: function () { return this.show === 'newDir' },
|
||||
showDownload: function () { return this.show === 'download' },
|
||||
showReplace: function () { return this.show === 'replace' },
|
||||
showOverlay: function () {
|
||||
return (this.show !== null && this.show !== 'search' && this.show !== 'more')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetPrompts () {
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
89
frontend/src/components/prompts/Rename.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<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">
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="button button--flat button--grey"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button @click="submit"
|
||||
class="button button--flat"
|
||||
type="submit"
|
||||
:aria-label="$t('buttons.rename')"
|
||||
: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'
|
||||
|
||||
export default {
|
||||
name: 'rename',
|
||||
data: function () {
|
||||
return {
|
||||
name: ''
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.name = this.oldName()
|
||||
},
|
||||
computed: {
|
||||
...mapState(['req', 'selected', 'selectedCount']),
|
||||
...mapGetters(['isListing'])
|
||||
},
|
||||
methods: {
|
||||
cancel: function () {
|
||||
this.$store.commit('closeHovers')
|
||||
},
|
||||
oldName: function () {
|
||||
if (!this.isListing) {
|
||||
return this.req.name
|
||||
}
|
||||
|
||||
if (this.selectedCount === 0 || this.selectedCount > 1) {
|
||||
// This shouldn't happen.
|
||||
return
|
||||
}
|
||||
|
||||
return this.req.items[this.selected[0]].name
|
||||
},
|
||||
submit: async function () {
|
||||
let oldLink = ''
|
||||
let newLink = ''
|
||||
|
||||
if (!this.isListing) {
|
||||
oldLink = this.req.url
|
||||
} else {
|
||||
oldLink = this.req.items[this.selected[0]].url
|
||||
}
|
||||
|
||||
newLink = url.removeLastDir(oldLink) + '/' + encodeURIComponent(this.name)
|
||||
|
||||
try {
|
||||
await api.move([{ from: oldLink, to: newLink }])
|
||||
if (!this.isListing) {
|
||||
this.$router.push({ path: newLink })
|
||||
return
|
||||
}
|
||||
|
||||
this.$store.commit('setReload', true)
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
}
|
||||
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
31
frontend/src/components/prompts/Replace.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.replace') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.replaceMessage') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="button button--flat button--grey"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="button button--flat button--red"
|
||||
@click="showConfirm"
|
||||
:aria-label="$t('buttons.replace')"
|
||||
:title="$t('buttons.replace')">{{ $t('buttons.replace') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'replace',
|
||||
computed: mapState(['showConfirm'])
|
||||
}
|
||||
</script>
|
||||
167
frontend/src/components/prompts/Share.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<div class="card floating" id="share">
|
||||
<div class="card-title">
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<button class="action"
|
||||
@click="deleteLink($event, link)"
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')"><i class="material-icons">delete</i></button>
|
||||
|
||||
<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
|
||||
type="number"
|
||||
max="2147483647"
|
||||
min="0"
|
||||
@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>
|
||||
</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 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>
|
||||
</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'
|
||||
|
||||
export default {
|
||||
name: 'share',
|
||||
data: function () {
|
||||
return {
|
||||
time: '',
|
||||
unit: 'hours',
|
||||
hasPermanent: false,
|
||||
links: [],
|
||||
clip: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState([ 'req', 'selected', 'selectedCount' ]),
|
||||
...mapGetters([ 'isListing' ]),
|
||||
url () {
|
||||
if (!this.isListing) {
|
||||
return this.$route.path
|
||||
}
|
||||
|
||||
if (this.selectedCount === 0 || this.selectedCount > 1) {
|
||||
// This shouldn't happen.
|
||||
return
|
||||
}
|
||||
|
||||
return this.req.items[this.selected[0]].url
|
||||
}
|
||||
},
|
||||
async beforeMount () {
|
||||
try {
|
||||
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
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.clip = new Clipboard('.copy-clipboard')
|
||||
this.clip.on('success', () => {
|
||||
this.$showSuccess(this.$t('success.linkCopied'))
|
||||
})
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.clip.destroy()
|
||||
},
|
||||
methods: {
|
||||
submit: async function () {
|
||||
if (!this.time) return
|
||||
|
||||
try {
|
||||
const res = await api.create(this.url, this.time, this.unit)
|
||||
this.links.push(res)
|
||||
this.sort()
|
||||
} 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)
|
||||
}
|
||||
},
|
||||
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)
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
}
|
||||
},
|
||||
humanTime (time) {
|
||||
return moment(time * 1000).fromNow()
|
||||
},
|
||||
buildLink (hash) {
|
||||
return `${window.location.origin}${baseURL}/share/${hash}`
|
||||
},
|
||||
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>
|
||||
|
||||
24
frontend/src/components/settings/Commands.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<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">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'permissions',
|
||||
props: ['commands'],
|
||||
computed: {
|
||||
raw: {
|
||||
get () {
|
||||
return this.commands.join(' ')
|
||||
},
|
||||
set (value) {
|
||||
this.$emit('update:commands', value.split(' '))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
34
frontend/src/components/settings/Languages.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<select v-on:change="change" :value="locale">
|
||||
<option value="ar">{{ $t('languages.ar') }}</option>
|
||||
<option value="de">{{ $t('languages.de') }}</option>
|
||||
<option value="en">{{ $t('languages.en') }}</option>
|
||||
<option value="es">{{ $t('languages.es') }}</option>
|
||||
<option value="fr">{{ $t('languages.fr') }}</option>
|
||||
<option value="is">{{ $t('languages.is') }}</option>
|
||||
<option value="it">{{ $t('languages.it') }}</option>
|
||||
<option value="ja">{{ $t('languages.ja') }}</option>
|
||||
<option value="ko">{{ $t('languages.ko') }}</option>
|
||||
<option value="nl-be">{{ $t('languages.nlBE') }}</option>
|
||||
<option value="pl">{{ $t('languages.pl') }}</option>
|
||||
<option value="pt-br">{{ $t('languages.ptBR') }}</option>
|
||||
<option value="pt">{{ $t('languages.pt') }}</option>
|
||||
<option value="ro">{{ $t('languages.ro') }}</option>
|
||||
<option value="ru">{{ $t('languages.ru') }}</option>
|
||||
<option value="sv-se">{{ $t('languages.svSE') }}</option>
|
||||
<option value="zh-cn">{{ $t('languages.zhCN') }}</option>
|
||||
<option value="zh-tw">{{ $t('languages.zhTW') }}</option>
|
||||
</select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'languages',
|
||||
props: [ 'locale' ],
|
||||
methods: {
|
||||
change (event) {
|
||||
this.$emit('update:locale', event.target.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
39
frontend/src/components/settings/Permissions.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div>
|
||||
<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" :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><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>
|
||||
export default {
|
||||
name: 'permissions',
|
||||
props: ['perm'],
|
||||
computed: {
|
||||
admin: {
|
||||
get () {
|
||||
return this.perm.admin
|
||||
},
|
||||
set (value) {
|
||||
if (value) {
|
||||
for (const key in this.perm) {
|
||||
this.perm[key] = true
|
||||
}
|
||||
}
|
||||
|
||||
this.perm.admin = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
57
frontend/src/components/settings/Rules.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<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
|
||||
@keypress.enter.prevent
|
||||
type="text"
|
||||
v-if="rule.regex"
|
||||
v-model="rule.regexp.raw"
|
||||
:placeholder="$t('settings.insertRegex')" />
|
||||
<input
|
||||
@keypress.enter.prevent
|
||||
type="text"
|
||||
v-else
|
||||
v-model="rule.path"
|
||||
:placeholder="$t('settings.insertPath')" />
|
||||
|
||||
<button class="button button--red" @click="remove($event, index)">-</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="button" @click="create" default="false">{{ $t('buttons.new') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'rules-textarea',
|
||||
props: ['rules'],
|
||||
methods: {
|
||||
remove (event, index) {
|
||||
event.preventDefault()
|
||||
let rules = [ ...this.rules ]
|
||||
rules.splice(index, 1)
|
||||
this.$emit('update:rules', [ ...rules ])
|
||||
},
|
||||
create (event) {
|
||||
event.preventDefault()
|
||||
|
||||
this.$emit('update:rules', [
|
||||
...this.rules,
|
||||
{
|
||||
allow: true,
|
||||
path: '',
|
||||
regex: false,
|
||||
regexp: {
|
||||
raw: ''
|
||||
}
|
||||
}
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
65
frontend/src/components/settings/UserForm.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<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">
|
||||
</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">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<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>
|
||||
</p>
|
||||
|
||||
<p v-if="!isDefault">
|
||||
<input type="checkbox" :disabled="user.perm.admin" v-model="user.lockPassword"> {{ $t('settings.lockPassword') }}
|
||||
</p>
|
||||
|
||||
<permissions :perm.sync="user.perm" />
|
||||
<commands :commands.sync="user.commands" />
|
||||
|
||||
<div v-if="!isDefault">
|
||||
<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'
|
||||
|
||||
export default {
|
||||
name: 'user',
|
||||
components: {
|
||||
Permissions,
|
||||
Languages,
|
||||
Rules,
|
||||
Commands
|
||||
},
|
||||
props: [ 'user', 'isNew', 'isDefault' ],
|
||||
computed: {
|
||||
passwordPlaceholder () {
|
||||
return this.isNew ? '' : this.$t('settings.avoidChanges')
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'user.perm.admin': function () {
|
||||
if (!this.user.perm.admin) return
|
||||
this.user.lockPassword = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
55
frontend/src/css/_buttons.css
Normal file
@@ -0,0 +1,55 @@
|
||||
.button {
|
||||
outline: 0;
|
||||
border: 0;
|
||||
padding: .5em 1em;
|
||||
border-radius: .1em;
|
||||
cursor: pointer;
|
||||
background: var(--blue);
|
||||
color: white;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.05);
|
||||
transition: .1s ease all;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: var(--dark-blue);
|
||||
}
|
||||
|
||||
.button--block {
|
||||
margin: 0 0 0.5em;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button--red {
|
||||
background: var(--red);
|
||||
}
|
||||
|
||||
.button--red:hover {
|
||||
background: var(--dark-red);
|
||||
}
|
||||
|
||||
.button--flat {
|
||||
color: var(--dark-blue);
|
||||
background: transparent;
|
||||
box-shadow: 0 0 0;
|
||||
border: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.button--flat:hover {
|
||||
background: var(--moon-grey);
|
||||
}
|
||||
|
||||
.button--flat.button--red {
|
||||
color: var(--dark-red);
|
||||
}
|
||||
|
||||
.button--flat.button--grey {
|
||||
color: #6f6f6f;
|
||||
}
|
||||
|
||||
.button[disabled] {
|
||||
opacity: .5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
35
frontend/src/css/_inputs.css
Normal file
@@ -0,0 +1,35 @@
|
||||
.input {
|
||||
border-radius: .1em;
|
||||
padding: .5em 1em;
|
||||
background: white;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
transition: .2s ease all;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.input:hover,
|
||||
.input:focus {
|
||||
border-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.input--block {
|
||||
margin-bottom: .5em;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input--textarea {
|
||||
line-height: 1.15;
|
||||
font-family: monospace;
|
||||
min-height: 10em;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.input--red {
|
||||
background: #fcd0cd;
|
||||
}
|
||||
|
||||
.input--green {
|
||||
background: #c9f2da;
|
||||
}
|
||||
29
frontend/src/css/_share.css
Normal file
@@ -0,0 +1,29 @@
|
||||
.share__box {
|
||||
text-align: center;
|
||||
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
|
||||
background: #fff;
|
||||
display: block;
|
||||
border-radius: 0.2em;
|
||||
width: 90%;
|
||||
max-width: 25em;
|
||||
margin: 6em auto;
|
||||
}
|
||||
|
||||
.share__box__download {
|
||||
width: 100%;
|
||||
padding: 1em;
|
||||
cursor: pointer;
|
||||
background: #ffffff;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.share__box__info {
|
||||
padding: 2em 3em;
|
||||
}
|
||||
|
||||
.share__box__title {
|
||||
margin-top: .2em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||