Compare commits

..

67 Commits

Author SHA1 Message Date
Oleg Lobanov
f0bc9167b1 chore(release): 2.17.0 2021-08-21 16:51:34 +02:00
Ramires Viana
23d646c456 fix: escape quote on index template
fixes #1501
2021-08-20 14:43:06 +02:00
Ramires Viana
76add9e527 feat: open file option on preview 2021-08-20 14:43:06 +02:00
Ramires Viana
c63cc5a2d2 fix: file caching directive 2021-08-20 14:43:06 +02:00
Andrew Kennedy
25c8788390 fix: 401 error in share view open file button (#1495) 2021-08-19 14:35:24 +02:00
Oleg Lobanov
aa52b07bb1 chore(release): 2.16.1 2021-08-04 11:44:29 +02:00
Oleg Lobanov
76b466f649 fix: check symlink target type (closes #1488) 2021-08-04 11:44:02 +02:00
Oleg Lobanov
8ecc2da947 chore(release): 2.16.0 2021-07-26 13:03:53 +02:00
Oleg Lobanov
8650d2ffe7 fix: failure on broken symlink deletion 2021-07-26 13:02:11 +02:00
Oleg Lobanov
34d7d2c8c4 chore: upgrade golangci-lint 2021-07-26 12:00:05 +02:00
Oleg Lobanov
201329abce chore: add Content-Security-Policy header 2021-07-26 11:08:39 +02:00
Oleg Lobanov
f2b5dd3787 chore: don't break folder download if any file processing causes an error 2021-07-26 10:41:56 +02:00
Oleg Lobanov
5072bbb2cb fix: break resource create/update handlers on error (closes #1464) 2021-07-24 15:33:54 +02:00
Oleg Lobanov
6b19ab6613 fix: don't remove files on unsuccessful updates (closes #1456) 2021-07-24 15:32:24 +02:00
Oleg Lobanov
730be5ef6b Create SECURITY.md 2021-07-03 16:56:27 +02:00
laggardkernel
46ee595389 fix: short commit sha and typo fix in Makefile (#1411) 2021-05-25 11:29:38 +02:00
Oleg Lobanov
dee465ab86 Merge pull request #1373 from ramiresviana/fixes-9
Some fixes
2021-05-03 23:29:12 +02:00
Ramires Viana
209f9fa77f fix: omit file content 2021-04-23 12:04:02 +00:00
Ramires Viana
ba8c09f454 feat: show more button on share 2021-04-23 11:59:34 +00:00
Ramires Viana
16a34defc0 feat: file name on page title 2021-04-23 11:59:04 +00:00
Ramires Viana
7d1e03075d feat: mod time title on file info 2021-04-23 11:57:56 +00:00
Ramires Viana
1c25f6ee69 feat: open file option on share 2021-04-23 11:55:56 +00:00
Ramires Viana
aa172b8bb5 feat: gzip encoding for static js files 2021-04-22 12:48:45 +00:00
Ramires Viana
4711e7bcd5 chore: set public path on the fly 2021-04-20 19:51:10 +00:00
Ramires Viana
8a47aee137 chore: split preview creation logic 2021-04-19 13:16:48 +00:00
Ramires Viana
190cb99a79 feat: browser cache directives 2021-04-19 12:49:40 +00:00
Ramires Viana
603203848a feat: display error messages on settings 2021-04-16 15:07:05 +00:00
Ramires Viana
5e6f14b5dc feat: message for connection error 2021-04-16 14:01:10 +00:00
Ramires Viana
976eb5583d feat: loading spinner on views navigation 2021-04-16 12:47:50 +00:00
Ramires Viana
b92152693f chore: split action on resource patch handler 2021-04-16 12:04:06 +00:00
Ramires Viana
7ec24d9d77 feat: support for IE11 browser 2021-04-15 12:28:19 +00:00
Ramires Viana
20ebbf6611 fix: copying files with special characters 2021-04-15 11:50:30 +00:00
Ramires Viana
ba7e71a7c3 fix: inconsistent double click on listing item 2021-04-14 15:29:06 +00:00
Ramires Viana
8973c4598f fix: delete image cache when moving 2021-04-14 15:20:38 +00:00
Ramires Viana
18889ad725 fix: no items displayed on file listing 2021-04-14 11:51:33 +00:00
Oleg Lobanov
73ccbe912f chore(release): 2.15.0 2021-04-06 13:57:29 +02:00
Oleg Lobanov
84e3a98303 Merge pull request #1353 from ramiresviana/fixes-8
Some fixes
2021-03-30 09:43:27 +02:00
adrium
7dd5b34d42 feat: add EXIF thumbnail support for JPEG files (#1234) 2021-03-29 11:40:00 +02:00
Alexis Lefebvre
4470d0a704 chore: update issue templates (#1355) 2021-03-28 12:52:03 +02:00
Ramires Viana
a76e01d2b7 feat: dynamic autoplay on previewer 2021-03-26 17:31:27 +00:00
Ramires Viana
2697093ac1 fix: empty archive name on directory download 2021-03-26 14:45:18 +00:00
Ramires Viana
59f9964e80 fix: check modify permission on file overwrite 2021-03-26 13:30:14 +00:00
Ramires Viana
1516d9932b fix: buttons without permission on header 2021-03-26 12:45:17 +00:00
Ramires Viana
fcb115f42d fix: mouse wheel zoom on previewer 2021-03-25 19:37:54 +00:00
Ramires Viana
e410272e6b feat: dynamic zoom limit on previewer 2021-03-25 19:36:53 +00:00
Ramires Viana
87f1881b42 fix: list item interactions on share 2021-03-25 15:47:49 +00:00
Ramires Viana
c0d85f3d85 fix: image quality switch on previewer 2021-03-25 14:24:46 +00:00
Ramires Viana
98d79b8ed9 fix: missing bold variation for Roboto font 2021-03-24 19:06:56 +00:00
Ramires Viana
fe80730bb1 fix: no header button animations on file listing 2021-03-24 19:05:15 +00:00
Ramires Viana
6c8ee96e6a feat: dynamic item count on file listing 2021-03-24 17:50:16 +00:00
Ramires Viana
b521dec8f9 fix: hidden editor header on Safari 2021-03-24 12:23:05 +00:00
Ramires Viana
e9baf0c4b6 fix: empty text file on editor 2021-03-23 18:18:02 +00:00
Ramires Viana
e1a6f593e1 fix: error causes panic on upload 2021-03-23 13:13:46 +00:00
Oleg Lobanov
4b068b3058 chore(release): 2.14.1 2021-03-21 14:24:33 +01:00
Oleg Lobanov
da54bd6c21 fix: display public routes with header proxy auth 2021-03-21 14:24:23 +01:00
Oleg Lobanov
0d179eca4d chore(release): 2.14.0 2021-03-21 13:19:53 +01:00
Oleg Lobanov
dacd511d24 chore: run npm update 2021-03-21 13:05:22 +01:00
Oleg Lobanov
c44b37c50c chore: add prettier frontent linter 2021-03-21 12:51:58 +01:00
Oleg Lobanov
a721dc1f31 feat: add health check handler 2021-03-21 12:30:48 +01:00
Oleg Lobanov
d2e6d23741 Merge pull request #1339 from ramiresviana/fixes-7
Some fixes
2021-03-19 17:32:41 +01:00
Ramires Viana
5f4a0317ab fix: hide dotfile error on share 2021-03-18 18:24:24 +00:00
Ramires Viana
22f4be8f54 fix: qr code url on share 2021-03-18 13:10:10 +00:00
Ramires Viana
eeadc532fe fix: text file detection on editor 2021-03-17 18:06:56 +00:00
Ramires Viana
93a35ad251 fix: prefix handling on http router 2021-03-17 17:54:25 +00:00
Oleg Lobanov
99787287bb Merge pull request #1331 from ramiresviana/tweaks-2
Some development tweaks
2021-03-15 16:56:20 +01:00
Ramires Viana
bdd523190e chore: frontend DirFS for development 2021-03-15 14:06:21 +00:00
Ramires Viana
4c1dd5c097 chore: automatic output name on build 2021-03-15 14:00:23 +00:00
128 changed files with 24817 additions and 9083 deletions

View File

@@ -1,2 +1,3 @@
*
!.docker.json
!filebrowser

View File

@@ -4,19 +4,19 @@ about: Create a report to help us improve
---
**Description**
A clear and concise description of what the issue is about. What are you trying to do?
<!-- A clear and concise description of what the issue is about. What are you trying to do? -->
**Expected behaviour**
What did you expect to happen?
<!-- What did you expect to happen? -->
**What is happening instead?**
Please, give full error messages and/or log.
<!-- Please, give full error messages and/or log. -->
**Additional context**
Add any other context about the problem here. If applicable, add screenshots to help explain your problem.
<!-- Add any other context about the problem here. If applicable, add screenshots to help explain your problem. -->
**How to reproduce?**
Tell us how to reproduce this issue. How can someone who is starting from scratch reproduce this behaviour as minimally as possible?
<!-- Tell us how to reproduce this issue. How can someone who is starting from scratch reproduce this behaviour as minimally as possible? -->
**Files**
A list of relevant files for this issue. Large files can be uploaded one-by-one or in a tarball/zipfile.
<!-- A list of relevant files for this issue. Large files can be uploaded one-by-one or in a tarball/zipfile. -->

View File

@@ -4,13 +4,13 @@ about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
Add a clear and concise description of what the problem is. E.g. *I'm always frustrated when [...]*
<!-- Add a clear and concise description of what the problem is. E.g. *I'm always frustrated when [...]* -->
**Describe the solution you'd like**
Add a clear and concise description of what you want to happen.
<!-- Add a clear and concise description of what you want to happen. -->
**Describe alternatives you've considered**
Add a clear and concise description of any alternative solutions or features you've considered.
<!-- Add a clear and concise description of any alternative solutions or features you've considered. -->
**Additional context**
Add any other context or screenshots about the feature request here.
<!-- Add any other context or screenshots about the feature request here. -->

View File

@@ -1,6 +1,8 @@
**Description**
<!--
Please explain the changes you made here.
If the feature changes current behaviour, explain why your solution is better.
-->
:rotating_light: Before submitting your PR, please read [community](https://github.com/filebrowser/community), and indicate which issues (in any of the repos) are either fixed or closed by this PR. See [GitHub Help: Closing issues using keywords](https://help.github.com/articles/closing-issues-via-commit-messages/).
@@ -11,6 +13,8 @@ If the feature changes current behaviour, explain why your solution is better.
- [ ] AVOID breaking the continuous integration build.
**Further comments**
<!--
If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did, what alternatives you considered, etc.
:heart: Thank you!
-->

1
.gitignore vendored
View File

@@ -5,6 +5,7 @@ _old
rice-box.go
.idea/
filebrowser
filebrowser.exe
.DS_Store
node_modules

View File

@@ -6,6 +6,8 @@ linters-settings:
funlen:
lines: 100
statements: 50
gci:
local-prefixes: github.com/filebrowser/filebrowser
goconst:
min-len: 2
min-occurrences: 2
@@ -26,8 +28,6 @@ linters-settings:
min-complexity: 15
goimports:
local-prefixes: github.com/filebrowser/filebrowser
golint:
min-confidence: 0
gomnd:
settings:
mnd:
@@ -58,26 +58,25 @@ linters:
- dogsled
- dupl
- errcheck
- exportloopref
- exhaustive
- funlen
- gochecknoinits
- goconst
- gocritic
- gocyclo
- goimports
- golint
- gomnd
- goprintffuncname
- gosec
- gosimple
- govet
- ineffassign
- interfacer
- lll
- misspell
- nakedret
- nolintlint
- rowserrcheck
- scopelint
- staticcheck
- structcheck
- stylecheck
@@ -89,19 +88,6 @@ linters:
- whitespace
- prealloc
# don't enable:
# - asciicheck
# - exhaustive (TODO: enable after next release; current release at time of writing is v1.27)
# - gochecknoglobals
# - gocognit
# - godot
# - godox
# - goerr113
# - maligned
# - nestif
# - testpackage
# - wsl
issues:
exclude-rules:
- path: cmd/.*.go
@@ -118,6 +104,9 @@ issues:
- text: "Auther"
linters:
- misspell
- text: "strconv.Parse"
linters:
- gomnd
run:
skip-dirs:

View File

@@ -2,6 +2,103 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [2.17.0](https://github.com/filebrowser/filebrowser/compare/v2.16.1...v2.17.0) (2021-08-21)
### Features
* open file option on preview ([76add9e](https://github.com/filebrowser/filebrowser/commit/76add9e5274b0373c6b983e3b20e387a14ea6c9e))
### Bug Fixes
* 401 error in share view open file button ([#1495](https://github.com/filebrowser/filebrowser/issues/1495)) ([25c8788](https://github.com/filebrowser/filebrowser/commit/25c87883908babde073390a2e2320a8e5880a87c))
* escape quote on index template ([23d646c](https://github.com/filebrowser/filebrowser/commit/23d646c456876d06cf48e71c1e57b69de99511f0)), closes [#1501](https://github.com/filebrowser/filebrowser/issues/1501)
* file caching directive ([c63cc5a](https://github.com/filebrowser/filebrowser/commit/c63cc5a2d25909cc4e2f2e7235f276ec66c32bf2))
### [2.16.1](https://github.com/filebrowser/filebrowser/compare/v2.16.0...v2.16.1) (2021-08-04)
### Bug Fixes
* check symlink target type (closes [#1488](https://github.com/filebrowser/filebrowser/issues/1488)) ([76b466f](https://github.com/filebrowser/filebrowser/commit/76b466f6492e74cf13e66a33e7e5f597ac92b240))
## [2.16.0](https://github.com/filebrowser/filebrowser/compare/v2.15.0...v2.16.0) (2021-07-26)
### Features
* browser cache directives ([190cb99](https://github.com/filebrowser/filebrowser/commit/190cb99a79a0d438eca2da13539f8c6449ad73ac))
* display error messages on settings ([6032038](https://github.com/filebrowser/filebrowser/commit/603203848a8b2221158088b6d849609db4c0c46c))
* file name on page title ([16a34de](https://github.com/filebrowser/filebrowser/commit/16a34defc02554a77c6ac47b9e17e69d098a09fe))
* gzip encoding for static js files ([aa172b8](https://github.com/filebrowser/filebrowser/commit/aa172b8bb5f17d5f5cb9666bfb5ee650d8091fb5))
* loading spinner on views navigation ([976eb55](https://github.com/filebrowser/filebrowser/commit/976eb5583dae474125fd7ddec5dc19b6c291f98f))
* message for connection error ([5e6f14b](https://github.com/filebrowser/filebrowser/commit/5e6f14b5dcb9c5efdf526f1346e09c2d0b2f6974))
* mod time title on file info ([7d1e030](https://github.com/filebrowser/filebrowser/commit/7d1e03075d2c27148f60813defa0f68403d1d3c2))
* open file option on share ([1c25f6e](https://github.com/filebrowser/filebrowser/commit/1c25f6ee69bd71eed82af7020006d0e27537a967))
* show more button on share ([ba8c09f](https://github.com/filebrowser/filebrowser/commit/ba8c09f454feeadf4a1e97547a34151a81b389d5))
* support for IE11 browser ([7ec24d9](https://github.com/filebrowser/filebrowser/commit/7ec24d9d7794fa37825f64ca2d1575f568fb1362))
### Bug Fixes
* break resource create/update handlers on error (closes [#1464](https://github.com/filebrowser/filebrowser/issues/1464)) ([5072bbb](https://github.com/filebrowser/filebrowser/commit/5072bbb2cbf5b29d041629faa8367f15e4d145a2))
* copying files with special characters ([20ebbf6](https://github.com/filebrowser/filebrowser/commit/20ebbf6611b734371426fb1b9cb5e388be90bf7e))
* delete image cache when moving ([8973c45](https://github.com/filebrowser/filebrowser/commit/8973c4598ff817647f1f1ad6ee36480054cd2776))
* don't remove files on unsuccessful updates (closes [#1456](https://github.com/filebrowser/filebrowser/issues/1456)) ([6b19ab6](https://github.com/filebrowser/filebrowser/commit/6b19ab6613b12be7f075299cd98f4b41d43827c7))
* failure on broken symlink deletion ([8650d2f](https://github.com/filebrowser/filebrowser/commit/8650d2ffe7a29cbafa800efcecbf6a61598a9f0c))
* inconsistent double click on listing item ([ba7e71a](https://github.com/filebrowser/filebrowser/commit/ba7e71a7c3b0cc71012e5adf94b1c642e554972e))
* no items displayed on file listing ([18889ad](https://github.com/filebrowser/filebrowser/commit/18889ad725f7f7e5a7e3f7abcf156487556dbeaf))
* omit file content ([209f9fa](https://github.com/filebrowser/filebrowser/commit/209f9fa77f751054512355f2b74b9b7258465d0b))
* short commit sha and typo fix in Makefile ([#1411](https://github.com/filebrowser/filebrowser/issues/1411)) ([46ee595](https://github.com/filebrowser/filebrowser/commit/46ee59538914dc2859f0da6b32e2d062d0a01b10))
## [2.15.0](https://github.com/filebrowser/filebrowser/compare/v2.14.1...v2.15.0) (2021-04-06)
### Features
* add EXIF thumbnail support for JPEG files ([#1234](https://github.com/filebrowser/filebrowser/issues/1234)) ([7dd5b34](https://github.com/filebrowser/filebrowser/commit/7dd5b34d425dfbc2782152310483cbecf85c800a))
* dynamic autoplay on previewer ([a76e01d](https://github.com/filebrowser/filebrowser/commit/a76e01d2b78a785f3665a8b3532c7cc566bfabce))
* dynamic item count on file listing ([6c8ee96](https://github.com/filebrowser/filebrowser/commit/6c8ee96e6a21fae5d4608bdc7a5c5a161d7dafd3))
* dynamic zoom limit on previewer ([e410272](https://github.com/filebrowser/filebrowser/commit/e410272e6be6a0b660efe8d4eee6c6e9dd834cc5))
### Bug Fixes
* buttons without permission on header ([1516d99](https://github.com/filebrowser/filebrowser/commit/1516d9932bf9926ac8b4cb3e738a5f51e80d5b1d))
* check modify permission on file overwrite ([59f9964](https://github.com/filebrowser/filebrowser/commit/59f9964e80c8233775f27be33a4c16a31bfe848a))
* empty archive name on directory download ([2697093](https://github.com/filebrowser/filebrowser/commit/2697093ac151f74eea3022951d128acfe04d1dcf))
* empty text file on editor ([e9baf0c](https://github.com/filebrowser/filebrowser/commit/e9baf0c4b688fab291cdc842ec464c7a7a816499))
* error causes panic on upload ([e1a6f59](https://github.com/filebrowser/filebrowser/commit/e1a6f593e1824e7fa4345a61dff5b1bb8cd22d05))
* hidden editor header on Safari ([b521dec](https://github.com/filebrowser/filebrowser/commit/b521dec8f9b14dd92248c429e902ebc639046389))
* image quality switch on previewer ([c0d85f3](https://github.com/filebrowser/filebrowser/commit/c0d85f3d85926c8790757bf142140d19455ae8ca))
* list item interactions on share ([87f1881](https://github.com/filebrowser/filebrowser/commit/87f1881b429877a740ea84a8e783ad4103248289))
* missing bold variation for Roboto font ([98d79b8](https://github.com/filebrowser/filebrowser/commit/98d79b8ed955df5691a306d709b4ab60d91da408))
* mouse wheel zoom on previewer ([fcb115f](https://github.com/filebrowser/filebrowser/commit/fcb115f42d33db2be7a4d428ec53d65d6050320b))
* no header button animations on file listing ([fe80730](https://github.com/filebrowser/filebrowser/commit/fe80730bb135b38e4d9de470c75cbe10b1aec201))
### [2.14.1](https://github.com/filebrowser/filebrowser/compare/v2.14.0...v2.14.1) (2021-03-21)
### Bug Fixes
* display public routes with header proxy auth ([da54bd6](https://github.com/filebrowser/filebrowser/commit/da54bd6c214d7ee39b71d710ddfe6dd25fc4e5d6))
## [2.14.0](https://github.com/filebrowser/filebrowser/compare/v2.13.0...v2.14.0) (2021-03-21)
### Features
* add health check handler ([a721dc1](https://github.com/filebrowser/filebrowser/commit/a721dc1f314732e60d331a1a7da97d06e0e8b613))
### Bug Fixes
* hide dotfile error on share ([5f4a031](https://github.com/filebrowser/filebrowser/commit/5f4a0317ab5685fe4a558df74e604c12e04a1c10))
* prefix handling on http router ([93a35ad](https://github.com/filebrowser/filebrowser/commit/93a35ad2516accdcb9735db509550979d01de2c3))
* qr code url on share ([22f4be8](https://github.com/filebrowser/filebrowser/commit/22f4be8f54162b7cf494177705ffb8b09117bd01))
* text file detection on editor ([eeadc53](https://github.com/filebrowser/filebrowser/commit/eeadc532fe6057969b3c1a4726f236851b154cfa))
## [2.13.0](https://github.com/filebrowser/filebrowser/compare/v2.12.1...v2.13.0) (2021-03-14)

View File

@@ -1,6 +1,10 @@
FROM alpine:latest
RUN apk --update add ca-certificates
RUN apk --update add mailcap
RUN apk --update add ca-certificates \
mailcap \
curl
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
CMD curl -f http://localhost/health || exit 1
VOLUME /srv
EXPOSE 80

View File

@@ -20,10 +20,10 @@ MODULE = $(shell env GO111MODULE=on $(GO) list -m)
DATE ?= $(shell date +%FT%T%z)
VERSION ?= $(shell git describe --tags --always --match=v* 2> /dev/null || \
cat $(CURDIR)/.version 2> /dev/null || echo v0)
VERSION_HASH = $(shell git rev-parse HEAD)
VERSION_HASH = $(shell git rev-parse --short HEAD)
BRANCH = $(shell git rev-parse --abbrev-ref HEAD)
LDFLAGS += -X "$(MODULE)/varsion.Version=$(VERSION)" -X "$(MODULE)/varsion.CommitSHA=$(VERSION_HASH)"
LDFLAGS += -X "$(MODULE)/version.Version=$(VERSION)" -X "$(MODULE)/version.CommitSHA=$(VERSION_HASH)"
# tools
$(BIN):
@@ -32,10 +32,10 @@ $(BIN)/%: | $(BIN) ; $(info $(M) installing $(PACKAGE)…)
$Q env GOBIN=$(BIN) $(GO) install $(PACKAGE)
GOLANGCI_LINT = $(BIN)/golangci-lint
$(BIN)/golangci-lint: PACKAGE=github.com/golangci/golangci-lint/cmd/golangci-lint@v1.37.1
$(BIN)/golangci-lint: PACKAGE=github.com/golangci/golangci-lint/cmd/golangci-lint@v1.41.1
GOIMPORTS = $(BIN)/goimports
$(BIN)/goimports: PACKAGE=golang.org/x/tools/cmd/goimports@v0.1.0
$(BIN)/goimports: PACKAGE=golang.org/x/tools/cmd/goimports@v0.1.5
## build: Build
.PHONY: build
@@ -49,7 +49,7 @@ build-frontend: | ; $(info $(M) building frontend…)
## build-backend: Build backend
.PHONY: build-backend
build-backend: | ; $(info $(M) building backend)
$Q $(GO) build -ldflags '$(LDFLAGS)' -o filebrowser
$Q $(GO) build -ldflags '$(LDFLAGS)' -o .
## test: Run all tests
.PHONY: test
@@ -61,7 +61,7 @@ test-frontend: | ; $(info $(M) running frontend tests…)
## test-backend: Run backend tests
.PHONY: test-backend
test-backend: | $(RICE) ; $(info $(M) running backend tests)
test-backend: | ; $(info $(M) running backend tests)
$Q $(GO) test -v ./...
## lint: Lint

26
SECURITY.md Normal file
View File

@@ -0,0 +1,26 @@
# Security Policy
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ------- | ------------------ |
| 2.x | :white_check_mark: |
| < 2.0 | :x: |
## Reporting a Vulnerability
Vulnerabilities should be reported to filebrowser@googlegroups.com - which is a private, maintainer-only group. Maintainers will attempt to respond to/confirm reports within 2-3 days, but if you believe your report to be "critical" to user safety and security, please note as such in the subject. We have tens of thousands of users using our software, and take security vulnerabilities seriously.
When reporting an issue, where possible, please provide at least:
* The commit version the issue was identified at
* A proof of concept (plaintext; no binaries)
* Steps to reproduce
* Your recommended remediation(s), if any.
The FileBrowser team is a volunteer-only effort, and may reach back out for clarification.
> Note: Please do not open public issues for security issues, as GitHub does not provide facility for private issues, and deleting the issue makes it hard to triage/respond back to the reporter.

View File

@@ -121,7 +121,7 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.
}
func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Auther) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) //nolint:gomnd
fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup)
fmt.Fprintf(w, "Create User Dir:\t%t\n", set.CreateUserDir)

View File

@@ -61,10 +61,10 @@ func addServerFlags(flags *pflag.FlagSet) {
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.Uint32("socket-perm", 0666, "unix socket file permissions")
flags.Uint32("socket-perm", 0666, "unix socket file permissions") //nolint:gomnd
flags.StringP("baseurl", "b", "", "base url")
flags.String("cache-dir", "", "file cache directory (disabled if empty)")
flags.Int("img-processors", 4, "image processors count")
flags.Int("img-processors", 4, "image processors count") //nolint:gomnd
flags.Bool("disable-thumbnails", false, "disable image thumbnails")
flags.Bool("disable-preview-resize", false, "disable resize of image previews")
flags.Bool("disable-exec", false, "disables Command Runner feature")
@@ -128,7 +128,7 @@ user created with the credentials from options "username" and "password".`,
cacheDir, err := cmd.Flags().GetString("cache-dir")
checkErr(err)
if cacheDir != "" {
if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet
if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet,gomnd
log.Fatalf("can't make directory %s: %s", cacheDir, err)
}
fileCache = diskcache.New(afero.NewOsFs(), cacheDir)

View File

@@ -28,7 +28,7 @@ You can also specify an optional parameter (index_end) so
you can remove all commands from 'index' to 'index_end',
including 'index_end'.`,
Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.RangeArgs(1, 2)(cmd, args); err != nil {
if err := cobra.RangeArgs(1, 2)(cmd, args); err != nil { //nolint:gomnd
return err
}

View File

@@ -26,7 +26,7 @@ var usersCmd = &cobra.Command{
}
func printUsers(usrs []*users.User) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) //nolint:gomnd
fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tS.Click\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock")
for _, u := range usrs {
@@ -53,7 +53,7 @@ func printUsers(usrs []*users.User) {
}
func parseUsernameOrID(arg string) (username string, id uint) {
id64, err := strconv.ParseUint(arg, 10, 0)
id64, err := strconv.ParseUint(arg, 10, 64) //nolint:gomnd
if err != nil {
return arg, 0
}

View File

@@ -72,7 +72,7 @@ func dbExists(path string) (bool, error) {
d := filepath.Dir(path)
_, err = os.Stat(d)
if os.IsNotExist(err) {
if err := os.MkdirAll(d, 0700); err != nil { //nolint:govet
if err := os.MkdirAll(d, 0700); err != nil { //nolint:govet,gomnd
return false, err
}
return false, nil

View File

@@ -37,11 +37,11 @@ func (f *FileCache) Store(ctx context.Context, key string, value []byte) error {
defer mu.Unlock()
fileName := f.getFileName(key)
if err := f.fs.MkdirAll(filepath.Dir(fileName), 0700); err != nil {
if err := f.fs.MkdirAll(filepath.Dir(fileName), 0700); err != nil { //nolint:gomnd
return err
}
if err := afero.WriteFile(f.fs, fileName, value, 0700); err != nil {
if err := afero.WriteFile(f.fs, fileName, value, 0700); err != nil { //nolint:gomnd
return err
}

View File

@@ -34,6 +34,7 @@ type FileInfo struct {
ModTime time.Time `json:"modified"`
Mode os.FileMode `json:"mode"`
IsDir bool `json:"isDir"`
IsSymlink bool `json:"isSymlink"`
Type string `json:"type"`
Subtitles []string `json:"subtitles,omitempty"`
Content string `json:"content,omitempty"`
@@ -50,6 +51,7 @@ type FileOptions struct {
ReadHeader bool
Token string
Checker rules.Checker
Content bool
}
// NewFileInfo creates a File object from a path and a given user. This File
@@ -60,12 +62,73 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
return nil, os.ErrPermission
}
info, err := opts.Fs.Stat(opts.Path)
file, err := stat(opts)
if err != nil {
return nil, err
}
file := &FileInfo{
if opts.Expand {
if file.IsDir {
if err := file.readListing(opts.Checker, opts.ReadHeader); err != nil { //nolint:govet
return nil, err
}
return file, nil
}
err = file.detectType(opts.Modify, opts.Content, true)
if err != nil {
return nil, err
}
}
return file, err
}
func stat(opts FileOptions) (*FileInfo, error) {
var file *FileInfo
if lstaterFs, ok := opts.Fs.(afero.Lstater); ok {
info, _, err := lstaterFs.LstatIfPossible(opts.Path)
if err != nil {
return nil, err
}
file = &FileInfo{
Fs: opts.Fs,
Path: opts.Path,
Name: info.Name(),
ModTime: info.ModTime(),
Mode: info.Mode(),
IsDir: info.IsDir(),
IsSymlink: IsSymlink(info.Mode()),
Size: info.Size(),
Extension: filepath.Ext(info.Name()),
Token: opts.Token,
}
}
// regular file
if file != nil && !file.IsSymlink {
return file, nil
}
// fs doesn't support afero.Lstater interface or the file is a symlink
info, err := opts.Fs.Stat(opts.Path)
if err != nil {
// can't follow symlink
if file != nil && file.IsSymlink {
return file, nil
}
return nil, err
}
// set correct file size in case of symlink
if file != nil && file.IsSymlink {
file.Size = info.Size()
file.IsDir = info.IsDir()
return file, nil
}
file = &FileInfo{
Fs: opts.Fs,
Path: opts.Path,
Name: info.Name(),
@@ -77,21 +140,7 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
Token: opts.Token,
}
if opts.Expand {
if file.IsDir {
if err := file.readListing(opts.Checker, opts.ReadHeader); err != nil { //nolint:govet
return nil, err
}
return file, nil
}
err = file.detectType(opts.Modify, true, true)
if err != nil {
return nil, err
}
}
return file, err
return file, nil
}
// Checksum checksums a given File for a given User, using a specific
@@ -148,12 +197,15 @@ func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
// of files couldn't be opened: we'd have immediately
// a 500 even though it doesn't matter. So we just log it.
var buffer []byte
mimetype := mime.TypeByExtension(i.Extension)
if mimetype == "" && readHeader {
var buffer []byte
if readHeader {
buffer = i.readFirstBytes()
mimetype = http.DetectContentType(buffer)
if mimetype == "" {
mimetype = http.DetectContentType(buffer)
}
}
switch {
@@ -167,7 +219,7 @@ func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
case strings.HasPrefix(mimetype, "image"):
i.Type = "image"
return nil
case (strings.HasPrefix(mimetype, "text") || (len(buffer) > 0 && !isBinary(buffer))) && i.Size <= 10*1024*1024: // 10 MB
case (strings.HasPrefix(mimetype, "text") || !isBinary(buffer)) && i.Size <= 10*1024*1024: // 10 MB
i.Type = "text"
if !modify {
@@ -200,7 +252,7 @@ func (i *FileInfo) readFirstBytes() []byte {
}
defer reader.Close()
buffer := make([]byte, 512)
buffer := make([]byte, 512) //nolint:gomnd
n, err := reader.Read(buffer)
if err != nil && err != io.EOF {
log.Print(err)
@@ -248,7 +300,9 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
continue
}
isSymlink := false
if IsSymlink(f.Mode()) {
isSymlink = true
// It's a symbolic link. We try to follow it. If it doesn't work,
// we stay with the link information instead of the target's.
info, err := i.Fs.Stat(fPath)
@@ -264,6 +318,7 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
ModTime: f.ModTime(),
Mode: f.Mode(),
IsDir: f.IsDir(),
IsSymlink: isSymlink,
Extension: filepath.Ext(name),
Path: fPath,
}

View File

@@ -40,13 +40,13 @@ func CopyFile(fs afero.Fs, source, dest string) error {
// Makes the directory needed to create the dst
// file.
err = fs.MkdirAll(filepath.Dir(dest), 0666)
err = fs.MkdirAll(filepath.Dir(dest), 0666) //nolint:gomnd
if err != nil {
return err
}
// Create the destination file.
dst, err := fs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
dst, err := fs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775) //nolint:gomnd
if err != nil {
return err
}

View File

@@ -1,3 +1,5 @@
// +build !dev
package frontend
import "embed"

14
frontend/assets_dev.go Normal file
View File

@@ -0,0 +1,14 @@
// +build dev
package frontend
import (
"io/fs"
"os"
)
var assets fs.FS = os.DirFS("frontend")
func Assets() fs.FS {
return assets
}

View File

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

25759
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,12 +5,15 @@
"scripts": {
"serve": "vue-cli-service serve",
"build": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitignore' -exec rm -r {} + && vue-cli-service build --no-clean",
"watch": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitignore' -exec rm -r {} + && vue-cli-service build --watch --no-clean",
"lint": "vue-cli-service lint --fix"
"lint": "npx vue-cli-service lint --no-fix",
"fix": "npx vue-cli-service lint",
"watch": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitignore' -exec rm -r {} + && vue-cli-service build --watch --no-clean"
},
"dependencies": {
"ace-builds": "^1.4.7",
"clipboard": "^2.0.4",
"core-js": "^3.9.1",
"css-vars-ponyfill": "^2.4.3",
"js-base64": "^2.5.1",
"lodash.clonedeep": "^4.5.0",
"lodash.throttle": "^4.1.1",
@@ -25,15 +28,20 @@
"vue-lazyload": "^1.3.3",
"vue-router": "^3.1.3",
"vuex": "^3.1.2",
"vuex-router-sync": "^5.0.0"
"vuex-router-sync": "^5.0.0",
"whatwg-fetch": "^3.6.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.1.2",
"@vue/cli-plugin-eslint": "^4.1.1",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-service": "^4.1.2",
"babel-eslint": "^10.0.3",
"@vue/eslint-config-prettier": "^6.0.0",
"babel-eslint": "^10.1.0",
"compression-webpack-plugin": "^6.0.3",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.1.2",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-vue": "^6.2.2",
"prettier": "^2.2.1",
"vue-template-compiler": "^2.6.10"
},
"eslintConfig": {
@@ -43,7 +51,8 @@
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
"eslint:recommended",
"@vue/prettier"
],
"rules": {},
"parserOptions": {
@@ -58,6 +67,6 @@
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
"not ie < 11"
]
}

View File

@@ -30,7 +30,7 @@
<!-- Inject Some Variables and generate the manifest json -->
<script>
window.FileBrowser = JSON.parse(`[{[ .Json ]}]`);
window.FileBrowser = JSON.parse('[{[ .Json ]}]');
var fullStaticURL = window.location.origin + window.FileBrowser.StaticURL;
var dynamicManifest = {
@@ -77,7 +77,7 @@
opacity: 0;
}
.spinner {
#loading .spinner {
width: 70px;
text-align: center;
position: fixed;
@@ -87,7 +87,7 @@
transform: translate(-50%, -50%);
}
.spinner > div {
#loading .spinner > div {
width: 18px;
height: 18px;
background-color: #333;
@@ -97,12 +97,12 @@
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
}
.spinner .bounce1 {
#loading .spinner .bounce1 {
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
.spinner .bounce2 {
#loading .spinner .bounce2 {
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}

View File

@@ -16,7 +16,7 @@ body {
#loading {
background: var(--background);
}
#loading .spinner div, #previewer .loading .spinner div {
#loading .spinner div, main .spinner div {
background: var(--icon);
}

View File

@@ -5,19 +5,22 @@
</template>
<script>
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.FileBrowser.StaticURL + "/";
export default {
name: 'app',
mounted () {
const loading = document.getElementById('loading')
loading.classList.add('done')
name: "app",
mounted() {
const loading = document.getElementById("loading");
loading.classList.add("done");
setTimeout(function () {
loading.parentNode.removeChild(loading)
}, 200)
}
}
loading.parentNode.removeChild(loading);
}, 200);
},
};
</script>
<style>
@import './css/styles.css';
@import "./css/styles.css";
</style>

View File

@@ -1,16 +1,16 @@
import { removePrefix } from './utils'
import { baseURL } from '@/utils/constants'
import store from '@/store'
import { removePrefix } from "./utils";
import { baseURL } from "@/utils/constants";
import store from "@/store";
const ssl = (window.location.protocol === 'https:')
const protocol = (ssl ? 'wss:' : 'ws:')
const ssl = window.location.protocol === "https:";
const protocol = ssl ? "wss:" : "ws:";
export default function command(url, command, onmessage, onclose) {
url = removePrefix(url)
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${store.state.jwt}`
url = removePrefix(url);
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${store.state.jwt}`;
let conn = new window.WebSocket(url)
conn.onopen = () => conn.send(command)
conn.onmessage = onmessage
conn.onclose = onclose
let conn = new window.WebSocket(url);
conn.onopen = () => conn.send(command);
conn.onmessage = onmessage;
conn.onclose = onclose;
}

View File

@@ -1,147 +1,156 @@
import { fetchURL, removePrefix } from './utils'
import { baseURL } from '@/utils/constants'
import store from '@/store'
import { fetchURL, removePrefix } from "./utils";
import { baseURL } from "@/utils/constants";
import store from "@/store";
export async function fetch (url) {
url = removePrefix(url)
export async function fetch(url) {
url = removePrefix(url);
const res = await fetchURL(`/api/resources${url}`, {})
const res = await fetchURL(`/api/resources${url}`, {});
if (res.status === 200) {
let data = await res.json()
data.url = `/files${url}`
let data = await res.json();
data.url = `/files${url}`;
if (data.isDir) {
if (!data.url.endsWith('/')) data.url += '/'
if (!data.url.endsWith("/")) data.url += "/";
data.items = data.items.map((item, index) => {
item.index = index
item.url = `${data.url}${encodeURIComponent(item.name)}`
item.index = index;
item.url = `${data.url}${encodeURIComponent(item.name)}`;
if (item.isDir) {
item.url += '/'
item.url += "/";
}
return item
})
return item;
});
}
return data
return data;
} else {
throw new Error(res.status)
throw new Error(res.status);
}
}
async function resourceAction (url, method, content) {
url = removePrefix(url)
async function resourceAction(url, method, content) {
url = removePrefix(url);
let opts = { method }
let opts = { method };
if (content) {
opts.body = content
opts.body = content;
}
const res = await fetchURL(`/api/resources${url}`, opts)
const res = await fetchURL(`/api/resources${url}`, opts);
if (res.status !== 200) {
throw new Error(await res.text())
throw new Error(await res.text());
} else {
return res
return res;
}
}
export async function remove (url) {
return resourceAction(url, 'DELETE')
export async function remove(url) {
return resourceAction(url, "DELETE");
}
export async function put (url, content = '') {
return resourceAction(url, 'PUT', content)
export async function put(url, content = "") {
return resourceAction(url, "PUT", content);
}
export function download (format, ...files) {
let url = `${baseURL}/api/raw`
export function download(format, ...files) {
let url = `${baseURL}/api/raw`;
if (files.length === 1) {
url += removePrefix(files[0]) + '?'
url += removePrefix(files[0]) + "?";
} else {
let arg = ''
let arg = "";
for (let file of files) {
arg += removePrefix(file) + ','
arg += removePrefix(file) + ",";
}
arg = arg.substring(0, arg.length - 1)
arg = encodeURIComponent(arg)
url += `/?files=${arg}&`
arg = arg.substring(0, arg.length - 1);
arg = encodeURIComponent(arg);
url += `/?files=${arg}&`;
}
if (format) {
url += `algo=${format}&`
url += `algo=${format}&`;
}
if (store.state.jwt){
url += `auth=${store.state.jwt}&`
if (store.state.jwt) {
url += `auth=${store.state.jwt}&`;
}
window.open(url)
window.open(url);
}
export async function post (url, content = '', overwrite = false, onupload) {
url = removePrefix(url)
export async function post(url, content = "", overwrite = false, onupload) {
url = removePrefix(url);
let bufferContent
if (content instanceof Blob && !['http:', 'https:'].includes(window.location.protocol)) {
bufferContent = await new Response(content).arrayBuffer()
let bufferContent;
if (
content instanceof Blob &&
!["http:", "https:"].includes(window.location.protocol)
) {
bufferContent = await new Response(content).arrayBuffer();
}
return new Promise((resolve, reject) => {
let request = new XMLHttpRequest()
request.open('POST', `${baseURL}/api/resources${url}?override=${overwrite}`, true)
request.setRequestHeader('X-Auth', store.state.jwt)
let request = new XMLHttpRequest();
request.open(
"POST",
`${baseURL}/api/resources${url}?override=${overwrite}`,
true
);
request.setRequestHeader("X-Auth", store.state.jwt);
if (typeof onupload === 'function') {
request.upload.onprogress = onupload
if (typeof onupload === "function") {
request.upload.onprogress = onupload;
}
request.onload = () => {
if (request.status === 200) {
resolve(request.responseText)
resolve(request.responseText);
} else if (request.status === 409) {
reject(request.status)
reject(request.status);
} else {
reject(request.responseText)
reject(request.responseText);
}
}
};
request.onerror = (error) => {
reject(error)
}
reject(error);
};
request.send(bufferContent || content)
})
request.send(bufferContent || content);
});
}
function moveCopy (items, copy = false, overwrite = false, rename = false) {
let promises = []
function moveCopy(items, copy = false, overwrite = false, rename = false) {
let promises = [];
for (let item of items) {
const from = item.from
const to = encodeURIComponent(removePrefix(item.to))
const url = `${from}?action=${copy ? 'copy' : 'rename'}&destination=${to}&override=${overwrite}&rename=${rename}`
promises.push(resourceAction(url, 'PATCH'))
const from = item.from;
const to = encodeURIComponent(removePrefix(item.to));
const url = `${from}?action=${
copy ? "copy" : "rename"
}&destination=${to}&override=${overwrite}&rename=${rename}`;
promises.push(resourceAction(url, "PATCH"));
}
return Promise.all(promises)
return Promise.all(promises);
}
export function move (items, overwrite = false, rename = false) {
return moveCopy(items, false, overwrite, rename)
export function move(items, overwrite = false, rename = false) {
return moveCopy(items, false, overwrite, rename);
}
export function copy (items, overwrite = false, rename = false) {
return moveCopy(items, true, overwrite, rename)
export function copy(items, overwrite = false, rename = false) {
return moveCopy(items, true, overwrite, rename);
}
export async function checksum (url, algo) {
const data = await resourceAction(`${url}?checksum=${algo}`, 'GET')
return (await data.json()).checksums[algo]
export async function checksum(url, algo) {
const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
return (await data.json()).checksums[algo];
}

View File

@@ -1,17 +1,9 @@
import * as files from './files'
import * as share from './share'
import * as users from './users'
import * as settings from './settings'
import * as pub from './pub'
import search from './search'
import commands from './commands'
import * as files from "./files";
import * as share from "./share";
import * as users from "./users";
import * as settings from "./settings";
import * as pub from "./pub";
import search from "./search";
import commands from "./commands";
export {
files,
share,
users,
settings,
pub,
commands,
search
}
export { files, share, users, settings, pub, commands, search };

View File

@@ -1,61 +1,61 @@
import { fetchURL, removePrefix } from './utils'
import { baseURL } from '@/utils/constants'
import { fetchURL, removePrefix } from "./utils";
import { baseURL } from "@/utils/constants";
export async function fetch (url, password = "") {
url = removePrefix(url)
export async function fetch(url, password = "") {
url = removePrefix(url);
const res = await fetchURL(`/api/public/share${url}`, {
headers: {'X-SHARE-PASSWORD': password},
})
headers: { "X-SHARE-PASSWORD": password },
});
if (res.status === 200) {
let data = await res.json()
data.url = `/share${url}`
let data = await res.json();
data.url = `/share${url}`;
if (data.isDir) {
if (!data.url.endsWith('/')) data.url += '/'
if (!data.url.endsWith("/")) data.url += "/";
data.items = data.items.map((item, index) => {
item.index = index
item.url = `${data.url}${encodeURIComponent(item.name)}`
item.index = index;
item.url = `${data.url}${encodeURIComponent(item.name)}`;
if (item.isDir) {
item.url += '/'
item.url += "/";
}
return item
})
return item;
});
}
return data
return data;
} else {
throw new Error(res.status)
throw new Error(res.status);
}
}
export function download(format, hash, token, ...files) {
let url = `${baseURL}/api/public/dl/${hash}`
let url = `${baseURL}/api/public/dl/${hash}`;
if (files.length === 1) {
url += encodeURIComponent(files[0]) + '?'
url += encodeURIComponent(files[0]) + "?";
} else {
let arg = ''
let arg = "";
for (let file of files) {
arg += encodeURIComponent(file) + ','
arg += encodeURIComponent(file) + ",";
}
arg = arg.substring(0, arg.length - 1)
arg = encodeURIComponent(arg)
url += `/?files=${arg}&`
arg = arg.substring(0, arg.length - 1);
arg = encodeURIComponent(arg);
url += `/?files=${arg}&`;
}
if (format) {
url += `algo=${format}&`
url += `algo=${format}&`;
}
if (token) {
url += `token=${token}&`
url += `token=${token}&`;
}
window.open(url)
}
window.open(url);
}

View File

@@ -1,31 +1,31 @@
import { fetchURL, removePrefix } from './utils'
import url from '../utils/url'
import { fetchURL, removePrefix } from "./utils";
import url from "../utils/url";
export default async function search (base, query) {
base = removePrefix(base)
query = encodeURIComponent(query)
export default async function search(base, query) {
base = removePrefix(base);
query = encodeURIComponent(query);
if (!base.endsWith('/')) {
base += '/'
if (!base.endsWith("/")) {
base += "/";
}
let res = await fetchURL(`/api/search${base}?query=${query}`, {})
let res = await fetchURL(`/api/search${base}?query=${query}`, {});
if (res.status === 200) {
let data = await res.json()
let data = await res.json();
data = data.map((item) => {
item.url = `/files${base}` + url.encodePath(item.path)
item.url = `/files${base}` + url.encodePath(item.path);
if (item.dir) {
item.url += '/'
item.url += "/";
}
return item
})
return item;
});
return data
return data;
} else {
throw Error(res.status)
throw Error(res.status);
}
}
}

View File

@@ -1,16 +1,16 @@
import { fetchURL, fetchJSON } from './utils'
import { fetchURL, fetchJSON } from "./utils";
export function get () {
return fetchJSON(`/api/settings`, {})
export function get() {
return fetchJSON(`/api/settings`, {});
}
export async function update (settings) {
export async function update(settings) {
const res = await fetchURL(`/api/settings`, {
method: 'PUT',
body: JSON.stringify(settings)
})
method: "PUT",
body: JSON.stringify(settings),
});
if (res.status !== 200) {
throw new Error(res.status)
throw new Error(res.status);
}
}

View File

@@ -1,36 +1,36 @@
import { fetchURL, fetchJSON, removePrefix } from './utils'
import { fetchURL, fetchJSON, removePrefix } from "./utils";
export async function list() {
return fetchJSON('/api/shares')
return fetchJSON("/api/shares");
}
export async function get(url) {
url = removePrefix(url)
return fetchJSON(`/api/share${url}`)
url = removePrefix(url);
return fetchJSON(`/api/share${url}`);
}
export async function remove(hash) {
const res = await fetchURL(`/api/share/${hash}`, {
method: 'DELETE'
})
method: "DELETE",
});
if (res.status !== 200) {
throw new Error(res.status)
throw new Error(res.status);
}
}
export async function create(url, password = '', expires = '', unit = 'hours') {
url = removePrefix(url)
url = `/api/share${url}`
if (expires !== '') {
url += `?expires=${expires}&unit=${unit}`
export async function create(url, password = "", expires = "", unit = "hours") {
url = removePrefix(url);
url = `/api/share${url}`;
if (expires !== "") {
url += `?expires=${expires}&unit=${unit}`;
}
let body = '{}';
if (password != '' || expires !== '' || unit !== 'hours') {
body = JSON.stringify({password: password, expires: expires, unit: unit})
let body = "{}";
if (password != "" || expires !== "" || unit !== "hours") {
body = JSON.stringify({ password: password, expires: expires, unit: unit });
}
return fetchJSON(url, {
method: 'POST',
method: "POST",
body: body,
})
});
}

View File

@@ -1,52 +1,51 @@
import { fetchURL, fetchJSON } from './utils'
import { fetchURL, fetchJSON } from "./utils";
export async function getAll () {
return fetchJSON(`/api/users`, {})
export async function getAll() {
return fetchJSON(`/api/users`, {});
}
export async function get (id) {
return fetchJSON(`/api/users/${id}`, {})
export async function get(id) {
return fetchJSON(`/api/users/${id}`, {});
}
export async function create (user) {
export async function create(user) {
const res = await fetchURL(`/api/users`, {
method: 'POST',
method: "POST",
body: JSON.stringify({
what: 'user',
what: "user",
which: [],
data: user
})
})
data: user,
}),
});
if (res.status === 201) {
return res.headers.get('Location')
return res.headers.get("Location");
} else {
throw new Error(res.status)
throw new Error(res.status);
}
}
export async function update (user, which = ['all']) {
export async function update(user, which = ["all"]) {
const res = await fetchURL(`/api/users/${user.id}`, {
method: 'PUT',
method: "PUT",
body: JSON.stringify({
what: 'user',
what: "user",
which: which,
data: user
})
})
data: user,
}),
});
if (res.status !== 200) {
throw new Error(res.status)
throw new Error(res.status);
}
}
export async function remove (id) {
export async function remove(id) {
const res = await fetchURL(`/api/users/${id}`, {
method: 'DELETE'
})
method: "DELETE",
});
if (res.status !== 200) {
throw new Error(res.status)
throw new Error(res.status);
}
}

View File

@@ -1,43 +1,47 @@
import store from '@/store'
import { renew } from '@/utils/auth'
import { baseURL } from '@/utils/constants'
import store from "@/store";
import { renew } from "@/utils/auth";
import { baseURL } from "@/utils/constants";
export async function fetchURL (url, opts) {
opts = opts || {}
opts.headers = opts.headers || {}
export async function fetchURL(url, opts) {
opts = opts || {};
opts.headers = opts.headers || {};
let { headers, ...rest } = opts
let { headers, ...rest } = opts;
const res = await fetch(`${baseURL}${url}`, {
headers: {
'X-Auth': store.state.jwt,
...headers
},
...rest
})
if (res.headers.get('X-Renew-Token') === 'true') {
await renew(store.state.jwt)
let res;
try {
res = await fetch(`${baseURL}${url}`, {
headers: {
"X-Auth": store.state.jwt,
...headers,
},
...rest,
});
} catch (error) {
return { status: 0 };
}
return res
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)
export async function fetchJSON(url, opts) {
const res = await fetchURL(url, opts);
if (res.status === 200) {
return res.json()
return res.json();
} else {
throw new Error(res.status)
throw new Error(res.status);
}
}
export function removePrefix (url) {
url = url.split('/').splice(2).join('/')
export function removePrefix(url) {
url = url.split("/").splice(2).join("/");
if (url === '') url = '/'
if (url[0] !== '/') url = '/' + url
return url
if (url === "") url = "/";
if (url[0] !== "/") url = "/" + url;
return url;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,11 +1,18 @@
<template>
<div class="breadcrumbs">
<component :is="element" :to="base || ''" :aria-label="$t('files.home')" :title="$t('files.home')">
<component
:is="element"
:to="base || ''"
:aria-label="$t('files.home')"
:title="$t('files.home')"
>
<i class="material-icons">home</i>
</component>
<span v-for="(link, index) in items" :key="index">
<span class="chevron"><i class="material-icons">keyboard_arrow_right</i></span>
<span class="chevron"
><i class="material-icons">keyboard_arrow_right</i></span
>
<component :is="element" :to="link.url">{{ link.name }}</component>
</span>
</div>
@@ -13,55 +20,56 @@
<script>
export default {
name: 'breadcrumbs',
props: [
'base',
'noLink'
],
name: "breadcrumbs",
props: ["base", "noLink"],
computed: {
items () {
const relativePath = this.$route.path.replace(this.base, '')
let parts = relativePath.split('/')
items() {
const relativePath = this.$route.path.replace(this.base, "");
let parts = relativePath.split("/");
if (parts[0] === '') {
parts.shift()
if (parts[0] === "") {
parts.shift();
}
if (parts[parts.length - 1] === '') {
parts.pop()
if (parts[parts.length - 1] === "") {
parts.pop();
}
let breadcrumbs = []
let breadcrumbs = [];
for (let i = 0; i < parts.length; i++) {
if (i === 0) {
breadcrumbs.push({ name: decodeURIComponent(parts[i]), url: this.base + '/' + parts[i] + '/' })
} else {
breadcrumbs.push({ name: decodeURIComponent(parts[i]), url: breadcrumbs[i - 1].url + parts[i] + '/' })
breadcrumbs.push({
name: decodeURIComponent(parts[i]),
url: this.base + "/" + parts[i] + "/",
});
} else {
breadcrumbs.push({
name: decodeURIComponent(parts[i]),
url: breadcrumbs[i - 1].url + parts[i] + "/",
});
}
}
if (breadcrumbs.length > 3) {
while (breadcrumbs.length !== 4) {
breadcrumbs.shift()
breadcrumbs.shift();
}
breadcrumbs[0].name = '...'
breadcrumbs[0].name = "...";
}
return breadcrumbs
return breadcrumbs;
},
element () {
element() {
if (this.noLink !== undefined) {
return 'span'
return "span";
}
return 'router-link'
}
}
}
return "router-link";
},
},
};
</script>
<style>
</style>
<style></style>

View File

@@ -1,5 +1,5 @@
<template>
<div id="search" @click="open" v-bind:class="{ active , ongoing }">
<div id="search" @click="open" v-bind:class="{ active, ongoing }">
<div id="input">
<button
v-if="active"
@@ -20,7 +20,7 @@
v-model.trim="value"
:aria-label="$t('search.search')"
:placeholder="$t('search.search')"
>
/>
</div>
<div id="result" ref="result">
@@ -30,25 +30,25 @@
<template v-if="value.length === 0">
<div class="boxes">
<h3>{{ $t('search.types') }}</h3>
<h3>{{ $t("search.types") }}</h3>
<div>
<div
tabindex="0"
v-for="(v,k) in boxes"
v-for="(v, k) in boxes"
:key="k"
role="button"
@click="init('type:'+k)"
:aria-label="$t('search.'+v.label)"
@click="init('type:' + k)"
:aria-label="$t('search.' + v.label)"
>
<i class="material-icons">{{v.icon}}</i>
<p>{{ $t('search.'+v.label) }}</p>
<i class="material-icons">{{ v.icon }}</i>
<p>{{ $t("search." + v.label) }}</p>
</div>
</div>
</div>
</template>
</template>
<ul v-show="results.length > 0">
<li v-for="(s,k) in filteredResults" :key="k">
<li v-for="(s, k) in filteredResults" :key="k">
<router-link @click.native="close" :to="s.url">
<i v-if="s.dir" class="material-icons">folder</i>
<i v-else class="material-icons">insert_drive_file</i>
@@ -65,20 +65,20 @@
</template>
<script>
import { mapState, mapGetters, mapMutations } from "vuex"
import url from "@/utils/url"
import { search } from "@/api"
import { mapState, mapGetters, mapMutations } from "vuex";
import url from "@/utils/url";
import { search } from "@/api";
var boxes = {
image: { label: "images", icon: "insert_photo" },
audio: { label: "music", icon: "volume_up" },
video: { label: "video", icon: "movie" },
pdf: { label: "pdf", icon: "picture_as_pdf" }
}
pdf: { label: "pdf", icon: "picture_as_pdf" },
};
export default {
name: "search",
data: function() {
data: function () {
return {
value: "",
active: false,
@@ -86,111 +86,116 @@ export default {
results: [],
reload: false,
resultsCount: 50,
scrollable: null
}
scrollable: null,
};
},
watch: {
show (val, old) {
this.active = val === "search"
show(val, old) {
this.active = val === "search";
if (old === "search" && !this.active) {
if (this.reload) {
this.setReload(true)
this.setReload(true);
}
document.body.style.overflow = "auto"
this.reset()
this.value = ''
this.active = false
this.$refs.input.blur()
document.body.style.overflow = "auto";
this.reset();
this.value = "";
this.active = false;
this.$refs.input.blur();
} else if (this.active) {
this.reload = false
this.$refs.input.focus()
document.body.style.overflow = "hidden"
this.reload = false;
this.$refs.input.focus();
document.body.style.overflow = "hidden";
}
},
value () {
value() {
if (this.results.length) {
this.reset()
this.reset();
}
}
},
},
computed: {
...mapState(["user", "show"]),
...mapGetters(["isListing"]),
boxes() {
return boxes
return boxes;
},
isEmpty() {
return this.results.length === 0
return this.results.length === 0;
},
text() {
if (this.ongoing) {
return ""
return "";
}
return this.value === '' ? this.$t("search.typeToSearch") : this.$t("search.pressToSearch")
return this.value === ""
? this.$t("search.typeToSearch")
: this.$t("search.pressToSearch");
},
filteredResults() {
return this.results.slice(0, this.resultsCount);
},
filteredResults () {
return this.results.slice(0, this.resultsCount)
}
},
mounted() {
this.$refs.result.addEventListener('scroll', event => {
if (event.target.offsetHeight + event.target.scrollTop >= event.target.scrollHeight - 100) {
this.resultsCount += 50
this.$refs.result.addEventListener("scroll", (event) => {
if (
event.target.offsetHeight + event.target.scrollTop >=
event.target.scrollHeight - 100
) {
this.resultsCount += 50;
}
})
});
},
methods: {
...mapMutations(["showHover", "closeHovers", "setReload"]),
open() {
this.showHover("search")
this.showHover("search");
},
close(event) {
event.stopPropagation()
event.preventDefault()
this.closeHovers()
event.stopPropagation();
event.preventDefault();
this.closeHovers();
},
keyup(event) {
if (event.keyCode === 27) {
this.close(event)
return
this.close(event);
return;
}
this.results.length = 0
this.results.length = 0;
},
init (string) {
this.value = `${string} `
this.$refs.input.focus()
init(string) {
this.value = `${string} `;
this.$refs.input.focus();
},
reset () {
this.ongoing = false
this.resultsCount = 50
this.results = []
reset() {
this.ongoing = false;
this.resultsCount = 50;
this.results = [];
},
async submit(event) {
event.preventDefault()
event.preventDefault();
if (this.value === '') {
return
if (this.value === "") {
return;
}
let path = this.$route.path
let path = this.$route.path;
if (!this.isListing) {
path = url.removeLastDir(path) + "/"
path = url.removeLastDir(path) + "/";
}
this.ongoing = true
this.ongoing = true;
try {
this.results = await search(path, this.value)
this.results = await search(path, this.value);
} catch (error) {
this.$showError(error)
this.$showError(error);
}
this.ongoing = false
}
}
}
this.ongoing = false;
},
},
};
</script>

View File

@@ -1,12 +1,21 @@
<template>
<div @click="focus" class="shell" ref="scrollable" :class="{ ['shell--hidden']: !showShell}">
<div v-for="(c, index) in content" :key="index" class="shell__result" >
<div class="shell__prompt"><i class="material-icons">chevron_right</i></div>
<div
@click="focus"
class="shell"
ref="scrollable"
:class="{ ['shell--hidden']: !showShell }"
>
<div v-for="(c, index) in content" :key="index" class="shell__result">
<div class="shell__prompt">
<i class="material-icons">chevron_right</i>
</div>
<pre class="shell__text">{{ c.text }}</pre>
</div>
<div class="shell__result" :class="{ 'shell__result--hidden': !canInput }" >
<div class="shell__prompt"><i class="material-icons">chevron_right</i></div>
<div class="shell__result" :class="{ 'shell__result--hidden': !canInput }">
<div class="shell__prompt">
<i class="material-icons">chevron_right</i>
</div>
<pre
tabindex="0"
ref="input"
@@ -14,102 +23,103 @@
contenteditable="true"
@keydown.prevent.38="historyUp"
@keydown.prevent.40="historyDown"
@keypress.prevent.enter="submit" />
@keypress.prevent.enter="submit"
/>
</div>
</div>
</template>
<script>
import { mapMutations, mapState, mapGetters } from 'vuex'
import { commands } from '@/api'
import { mapMutations, mapState, mapGetters } from "vuex";
import { commands } from "@/api";
export default {
name: 'shell',
name: "shell",
computed: {
...mapState([ 'user', 'showShell' ]),
...mapGetters([ 'isFiles', 'isLogged' ]),
...mapState(["user", "showShell"]),
...mapGetters(["isFiles", "isLogged"]),
path: function () {
if (this.isFiles) {
return this.$route.path
return this.$route.path;
}
return ''
}
return "";
},
},
data: () => ({
content: [],
history: [],
historyPos: 0,
canInput: true
canInput: true,
}),
methods: {
...mapMutations([ 'toggleShell' ]),
...mapMutations(["toggleShell"]),
scroll: function () {
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight;
},
focus: function () {
this.$refs.input.focus()
this.$refs.input.focus();
},
historyUp () {
historyUp() {
if (this.historyPos > 0) {
this.$refs.input.innerText = this.history[--this.historyPos]
this.focus()
this.$refs.input.innerText = this.history[--this.historyPos];
this.focus();
}
},
historyDown () {
historyDown() {
if (this.historyPos >= 0 && this.historyPos < this.history.length - 1) {
this.$refs.input.innerText = this.history[++this.historyPos]
this.focus()
this.$refs.input.innerText = this.history[++this.historyPos];
this.focus();
} else {
this.historyPos = this.history.length
this.$refs.input.innerText = ''
this.historyPos = this.history.length;
this.$refs.input.innerText = "";
}
},
submit: function (event) {
const cmd = event.target.innerText.trim()
const cmd = event.target.innerText.trim();
if (cmd === '') {
return
if (cmd === "") {
return;
}
if (cmd === 'clear') {
this.content = []
event.target.innerHTML = ''
return
if (cmd === "clear") {
this.content = [];
event.target.innerHTML = "";
return;
}
if (cmd === 'exit') {
event.target.innerHTML = ''
this.toggleShell()
return
if (cmd === "exit") {
event.target.innerHTML = "";
this.toggleShell();
return;
}
this.canInput = false
event.target.innerHTML = ''
this.canInput = false;
event.target.innerHTML = "";
let results = {
text: `${cmd}\n\n`
}
this.history.push(cmd)
this.historyPos = this.history.length
this.content.push(results)
text: `${cmd}\n\n`,
};
this.history.push(cmd);
this.historyPos = this.history.length;
this.content.push(results);
commands(
this.path,
cmd,
event => {
results.text += `${event.data}\n`
this.scroll()
(event) => {
results.text += `${event.data}\n`;
this.scroll();
},
() => {
results.text = results.text.trimEnd()
this.canInput = true
this.$refs.input.focus()
this.scroll()
results.text = results.text.trimEnd();
this.canInput = true;
this.$refs.input.focus();
this.scroll();
}
)
}
}
}
);
},
},
};
</script>

View File

@@ -1,82 +1,134 @@
<template>
<nav :class="{active}">
<nav :class="{ active }">
<template v-if="isLogged">
<router-link class="action" to="/files/" :aria-label="$t('sidebar.myFiles')" :title="$t('sidebar.myFiles')">
<router-link
class="action"
to="/files/"
:aria-label="$t('sidebar.myFiles')"
:title="$t('sidebar.myFiles')"
>
<i class="material-icons">folder</i>
<span>{{ $t('sidebar.myFiles') }}</span>
<span>{{ $t("sidebar.myFiles") }}</span>
</router-link>
<div v-if="user.perm.create">
<button @click="$store.commit('showHover', 'newDir')" class="action" :aria-label="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')">
<button
@click="$store.commit('showHover', 'newDir')"
class="action"
:aria-label="$t('sidebar.newFolder')"
:title="$t('sidebar.newFolder')"
>
<i class="material-icons">create_new_folder</i>
<span>{{ $t('sidebar.newFolder') }}</span>
<span>{{ $t("sidebar.newFolder") }}</span>
</button>
<button @click="$store.commit('showHover', 'newFile')" class="action" :aria-label="$t('sidebar.newFile')" :title="$t('sidebar.newFile')">
<button
@click="$store.commit('showHover', 'newFile')"
class="action"
:aria-label="$t('sidebar.newFile')"
:title="$t('sidebar.newFile')"
>
<i class="material-icons">note_add</i>
<span>{{ $t('sidebar.newFile') }}</span>
<span>{{ $t("sidebar.newFile") }}</span>
</button>
</div>
<div>
<router-link class="action" to="/settings" :aria-label="$t('sidebar.settings')" :title="$t('sidebar.settings')">
<router-link
class="action"
to="/settings"
:aria-label="$t('sidebar.settings')"
:title="$t('sidebar.settings')"
>
<i class="material-icons">settings_applications</i>
<span>{{ $t('sidebar.settings') }}</span>
<span>{{ $t("sidebar.settings") }}</span>
</router-link>
<button v-if="authMethod == 'json'" @click="logout" class="action" id="logout" :aria-label="$t('sidebar.logout')" :title="$t('sidebar.logout')">
<button
v-if="authMethod == 'json'"
@click="logout"
class="action"
id="logout"
:aria-label="$t('sidebar.logout')"
:title="$t('sidebar.logout')"
>
<i class="material-icons">exit_to_app</i>
<span>{{ $t('sidebar.logout') }}</span>
<span>{{ $t("sidebar.logout") }}</span>
</button>
</div>
</template>
<template v-else>
<router-link class="action" to="/login" :aria-label="$t('sidebar.login')" :title="$t('sidebar.login')">
<router-link
class="action"
to="/login"
:aria-label="$t('sidebar.login')"
:title="$t('sidebar.login')"
>
<i class="material-icons">exit_to_app</i>
<span>{{ $t('sidebar.login') }}</span>
<span>{{ $t("sidebar.login") }}</span>
</router-link>
<router-link v-if="signup" class="action" to="/login" :aria-label="$t('sidebar.signup')" :title="$t('sidebar.signup')">
<router-link
v-if="signup"
class="action"
to="/login"
:aria-label="$t('sidebar.signup')"
:title="$t('sidebar.signup')"
>
<i class="material-icons">person_add</i>
<span>{{ $t('sidebar.signup') }}</span>
<span>{{ $t("sidebar.signup") }}</span>
</router-link>
</template>
<p class="credits">
<span>
<span v-if="disableExternal">File Browser</span>
<a v-else rel="noopener noreferrer" target="_blank" href="https://github.com/filebrowser/filebrowser">File Browser</a>
<a
v-else
rel="noopener noreferrer"
target="_blank"
href="https://github.com/filebrowser/filebrowser"
>File Browser</a
>
<span> {{ version }}</span>
</span>
<span><a @click="help">{{ $t('sidebar.help') }}</a></span>
<span
><a @click="help">{{ $t("sidebar.help") }}</a></span
>
</p>
</nav>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
import * as auth from '@/utils/auth'
import { version, signup, disableExternal, noAuth, authMethod } from '@/utils/constants'
import { mapState, mapGetters } from "vuex";
import * as auth from "@/utils/auth";
import {
version,
signup,
disableExternal,
noAuth,
authMethod,
} from "@/utils/constants";
export default {
name: 'sidebar',
name: "sidebar",
computed: {
...mapState([ 'user' ]),
...mapGetters([ 'isLogged' ]),
active () {
return this.$store.state.show === 'sidebar'
...mapState(["user"]),
...mapGetters(["isLogged"]),
active() {
return this.$store.state.show === "sidebar";
},
signup: () => signup,
version: () => version,
disableExternal: () => disableExternal,
noAuth: () => noAuth,
authMethod: () => authMethod
authMethod: () => authMethod,
},
methods: {
help () {
this.$store.commit('showHover', 'help')
help() {
this.$store.commit("showHover", "help");
},
logout: auth.logout
}
}
logout: auth.logout,
},
};
</script>

View File

@@ -10,40 +10,33 @@
@mouseup="mouseUp"
@wheel="wheelMove"
>
<img src="" class="image-ex-img image-ex-img-center" ref="imgex" @load="onLoad">
<img
src=""
class="image-ex-img image-ex-img-center"
ref="imgex"
@load="onLoad"
/>
</div>
</template>
<script>
import throttle from 'lodash.throttle'
import UTIF from 'utif'
import throttle from "lodash.throttle";
import UTIF from "utif";
export default {
props: {
src: String,
moveDisabledTime: {
type: Number,
default: () => 200
},
maxScale: {
type: Number,
default: () => 4
},
minScale: {
type: Number,
default: () => 0.25
default: () => 200,
},
classList: {
type: Array,
default: () => []
default: () => [],
},
zoomStep: {
type: Number,
default: () => 0.25
default: () => 0.25,
},
autofill: {
type: Boolean,
default: () => false
}
},
data() {
return {
@@ -57,204 +50,229 @@ export default {
imageLoaded: false,
position: {
center: { x: 0, y: 0 },
relative: { x: 0, y: 0 }
}
}
relative: { x: 0, y: 0 },
},
maxScale: 4,
minScale: 0.25,
};
},
mounted() {
if (!this.decodeUTIF()) {
this.$refs.imgex.src = this.src
this.$refs.imgex.src = this.src;
}
let container = this.$refs.container
this.classList.forEach(className => container.classList.add(className))
let container = this.$refs.container;
this.classList.forEach((className) => container.classList.add(className));
// set width and height if they are zero
if (getComputedStyle(container).width === "0px") {
container.style.width = "100%"
container.style.width = "100%";
}
if (getComputedStyle(container).height === "0px") {
container.style.height = "100%"
container.style.height = "100%";
}
window.addEventListener('resize', this.onResize)
window.addEventListener("resize", this.onResize);
},
beforeDestroy () {
window.removeEventListener('resize', this.onResize)
document.removeEventListener('mouseup', this.onMouseUp)
beforeDestroy() {
window.removeEventListener("resize", this.onResize);
document.removeEventListener("mouseup", this.onMouseUp);
},
watch: {
src: function () {
this.scale = 1
this.setZoom()
this.setCenter()
}
if (!this.decodeUTIF()) {
this.$refs.imgex.src = this.src;
}
this.scale = 1;
this.setZoom();
this.setCenter();
},
},
methods: {
// Modified from UTIF.replaceIMG
decodeUTIF() {
const sufs = ["tif", "tiff", "dng", "cr2", "nef"]
let suff = document.location.pathname.split(".").pop().toLowerCase()
if (sufs.indexOf(suff) == -1) return false
let xhr = new XMLHttpRequest()
UTIF._xhrs.push(xhr)
UTIF._imgs.push(this.$refs.imgex)
xhr.open("GET", this.src)
xhr.responseType = "arraybuffer"
xhr.onload = UTIF._imgLoaded
xhr.send()
return true
const sufs = ["tif", "tiff", "dng", "cr2", "nef"];
let suff = document.location.pathname.split(".").pop().toLowerCase();
if (sufs.indexOf(suff) == -1) return false;
let xhr = new XMLHttpRequest();
UTIF._xhrs.push(xhr);
UTIF._imgs.push(this.$refs.imgex);
xhr.open("GET", this.src);
xhr.responseType = "arraybuffer";
xhr.onload = UTIF._imgLoaded;
xhr.send();
return true;
},
onLoad() {
let img = this.$refs.imgex
let img = this.$refs.imgex;
this.imageLoaded = true
this.imageLoaded = true;
if (img === undefined) {
return
return;
}
img.classList.remove('image-ex-img-center')
this.setCenter()
img.classList.add('image-ex-img-ready')
img.classList.remove("image-ex-img-center");
this.setCenter();
img.classList.add("image-ex-img-ready");
document.addEventListener('mouseup', this.onMouseUp)
document.addEventListener("mouseup", this.onMouseUp);
let realSize = img.naturalWidth;
let displaySize = img.offsetWidth;
// Image is in portrait orientation
if (img.naturalHeight > img.naturalWidth) {
realSize = img.naturalHeight;
displaySize = img.offsetHeight;
}
// Scale needed to display the image on full size
const fullScale = realSize / displaySize;
// Full size plus additional zoom
this.maxScale = fullScale + 4;
},
onMouseUp() {
this.inDrag = false
this.inDrag = false;
},
onResize: throttle(function() {
onResize: throttle(function () {
if (this.imageLoaded) {
this.setCenter()
this.doMove(this.position.relative.x, this.position.relative.y)
this.setCenter();
this.doMove(this.position.relative.x, this.position.relative.y);
}
}, 100),
setCenter() {
let container = this.$refs.container
let img = this.$refs.imgex
let container = this.$refs.container;
let img = this.$refs.imgex;
this.position.center.x = Math.floor((container.clientWidth - img.clientWidth) / 2)
this.position.center.y = Math.floor((container.clientHeight - img.clientHeight) / 2)
this.position.center.x = Math.floor(
(container.clientWidth - img.clientWidth) / 2
);
this.position.center.y = Math.floor(
(container.clientHeight - img.clientHeight) / 2
);
img.style.left = this.position.center.x + 'px'
img.style.top = this.position.center.y + 'px'
img.style.left = this.position.center.x + "px";
img.style.top = this.position.center.y + "px";
},
mousedownStart(event) {
this.lastX = null
this.lastY = null
this.inDrag = true
event.preventDefault()
this.lastX = null;
this.lastY = null;
this.inDrag = true;
event.preventDefault();
},
mouseMove(event) {
if (!this.inDrag) return
this.doMove(event.movementX, event.movementY)
event.preventDefault()
if (!this.inDrag) return;
this.doMove(event.movementX, event.movementY);
event.preventDefault();
},
mouseUp(event) {
this.inDrag = false
event.preventDefault()
this.inDrag = false;
event.preventDefault();
},
touchStart(event) {
this.lastX = null
this.lastY = null
this.lastTouchDistance = null
this.lastX = null;
this.lastY = null;
this.lastTouchDistance = null;
if (event.targetTouches.length < 2) {
setTimeout(() => {
this.touches = 0
}, 300)
this.touches++
this.touches = 0;
}, 300);
this.touches++;
if (this.touches > 1) {
this.zoomAuto(event)
this.zoomAuto(event);
}
}
event.preventDefault()
}
event.preventDefault();
},
zoomAuto(event) {
switch (this.scale) {
case 1:
this.scale = 2
break
this.scale = 2;
break;
case 2:
this.scale = 4
break
this.scale = 4;
break;
default:
case 4:
this.scale = 1
this.setCenter()
break
this.scale = 1;
this.setCenter();
break;
}
this.setZoom()
event.preventDefault()
this.setZoom();
event.preventDefault();
},
touchMove(event) {
event.preventDefault()
event.preventDefault();
if (this.lastX === null) {
this.lastX = event.targetTouches[0].pageX
this.lastY = event.targetTouches[0].pageY
return
this.lastX = event.targetTouches[0].pageX;
this.lastY = event.targetTouches[0].pageY;
return;
}
let step = this.$refs.imgex.width / 5
let step = this.$refs.imgex.width / 5;
if (event.targetTouches.length === 2) {
this.moveDisabled = true
clearTimeout(this.disabledTimer)
this.moveDisabled = true;
clearTimeout(this.disabledTimer);
this.disabledTimer = setTimeout(
() => (this.moveDisabled = false),
this.moveDisabledTime
)
);
let p1 = event.targetTouches[0]
let p2 = event.targetTouches[1]
let p1 = event.targetTouches[0];
let p2 = event.targetTouches[1];
let touchDistance = Math.sqrt(
Math.pow(p2.pageX - p1.pageX, 2) + Math.pow(p2.pageY - p1.pageY, 2)
)
);
if (!this.lastTouchDistance) {
this.lastTouchDistance = touchDistance
return
this.lastTouchDistance = touchDistance;
return;
}
this.scale += (touchDistance - this.lastTouchDistance) / step
this.lastTouchDistance = touchDistance
this.setZoom()
this.scale += (touchDistance - this.lastTouchDistance) / step;
this.lastTouchDistance = touchDistance;
this.setZoom();
} else if (event.targetTouches.length === 1) {
if (this.moveDisabled) return
let x = event.targetTouches[0].pageX - this.lastX
let y = event.targetTouches[0].pageY - this.lastY
if (Math.abs(x) >= step && Math.abs(y) >= step) return
this.lastX = event.targetTouches[0].pageX
this.lastY = event.targetTouches[0].pageY
this.doMove(x, y)
if (this.moveDisabled) return;
let x = event.targetTouches[0].pageX - this.lastX;
let y = event.targetTouches[0].pageY - this.lastY;
if (Math.abs(x) >= step && Math.abs(y) >= step) return;
this.lastX = event.targetTouches[0].pageX;
this.lastY = event.targetTouches[0].pageY;
this.doMove(x, y);
}
},
doMove(x, y) {
let style = this.$refs.imgex.style
let posX = this.pxStringToNumber(style.left) + x
let posY = this.pxStringToNumber(style.top) + y
let style = this.$refs.imgex.style;
let posX = this.pxStringToNumber(style.left) + x;
let posY = this.pxStringToNumber(style.top) + y;
style.left = posX + 'px'
style.top = posY + 'px'
style.left = posX + "px";
style.top = posY + "px";
this.position.relative.x = Math.abs(this.position.center.x - posX)
this.position.relative.y = Math.abs(this.position.center.y - posY)
this.position.relative.x = Math.abs(this.position.center.x - posX);
this.position.relative.y = Math.abs(this.position.center.y - posY);
if (posX < this.position.center.x) {
this.position.relative.x = this.position.relative.x * -1
this.position.relative.x = this.position.relative.x * -1;
}
if (posY < this.position.center.y) {
this.position.relative.y = this.position.relative.y * -1
this.position.relative.y = this.position.relative.y * -1;
}
},
wheelMove(event) {
this.scale += (event.wheelDeltaY / 100) * this.zoomStep
this.setZoom()
this.scale += -Math.sign(event.deltaY) * this.zoomStep;
this.setZoom();
},
setZoom() {
this.scale = this.scale < this.minScale ? this.minScale : this.scale
this.scale = this.scale > this.maxScale ? this.maxScale : this.scale
this.$refs.imgex.style.transform = `scale(${this.scale})`
this.scale = this.scale < this.minScale ? this.minScale : this.scale;
this.scale = this.scale > this.maxScale ? this.maxScale : this.scale;
this.$refs.imgex.style.transform = `scale(${this.scale})`;
},
pxStringToNumber(style) {
return +style.replace("px", "")
}
}
}
return +style.replace("px", "");
},
},
};
</script>
<style>
.image-ex-container {

View File

@@ -1,19 +1,22 @@
<template>
<div class="item"
role="button"
tabindex="0"
:draggable="isDraggable"
@dragstart="dragStart"
@dragover="dragOver"
@drop="drop"
@click="itemClick"
@dblclick="dblclick"
@touchstart="touchstart"
:data-dir="isDir"
:aria-label="name"
:aria-selected="isSelected">
<div
class="item"
role="button"
tabindex="0"
:draggable="isDraggable"
@dragstart="dragStart"
@dragover="dragOver"
@drop="drop"
@click="itemClick"
:data-dir="isDir"
:aria-label="name"
:aria-selected="isSelected"
>
<div>
<img v-if="readOnly == undefined && type==='image' && isThumbsEnabled" v-lazy="thumbnailUrl">
<img
v-if="readOnly == undefined && type === 'image' && isThumbsEnabled"
v-lazy="thumbnailUrl"
/>
<i v-else class="material-icons">{{ icon }}</i>
</div>
@@ -31,203 +34,218 @@
</template>
<script>
import { baseURL, enableThumbs } from '@/utils/constants'
import { mapMutations, mapGetters, mapState } from 'vuex'
import filesize from 'filesize'
import moment from 'moment'
import { files as api } from '@/api'
import * as upload from '@/utils/upload'
import { baseURL, enableThumbs } from "@/utils/constants";
import { mapMutations, mapGetters, mapState } from "vuex";
import filesize from "filesize";
import moment from "moment";
import { files as api } from "@/api";
import * as upload from "@/utils/upload";
export default {
name: 'item',
name: "item",
data: function () {
return {
touches: 0
}
touches: 0,
};
},
props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index', 'readOnly'],
props: [
"name",
"isDir",
"url",
"type",
"size",
"modified",
"index",
"readOnly",
],
computed: {
...mapState(['user', 'selected', 'req', 'jwt']),
...mapGetters(['selectedCount']),
singleClick () {
return this.readOnly == undefined && this.user.singleClick
...mapState(["user", "selected", "req", "jwt"]),
...mapGetters(["selectedCount"]),
singleClick() {
return this.readOnly == undefined && this.user.singleClick;
},
isSelected () {
return (this.selected.indexOf(this.index) !== -1)
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'
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";
},
isDraggable () {
return this.readOnly == undefined && this.user.perm.rename
isDraggable() {
return this.readOnly == undefined && this.user.perm.rename;
},
canDrop () {
if (!this.isDir || this.readOnly !== undefined) return false
canDrop() {
if (!this.isDir || this.readOnly !== undefined) return false;
for (let i of this.selected) {
if (this.req.items[i].url === this.url) {
return false
return false;
}
}
return true
return true;
},
thumbnailUrl () {
const path = this.url.replace(/^\/files\//, '')
thumbnailUrl() {
const path = this.url.replace(/^\/files\//, "");
// reload the image when the file is replaced
const key = Date.parse(this.modified)
const key = Date.parse(this.modified);
return `${baseURL}/api/preview/thumb/${path}?auth=${this.jwt}&inline=true&k=${key}`
return `${baseURL}/api/preview/thumb/${path}?k=${key}&inline=true`;
},
isThumbsEnabled() {
return enableThumbs;
},
isThumbsEnabled () {
return enableThumbs
}
},
methods: {
...mapMutations(['addSelected', 'removeSelected', 'resetSelected']),
...mapMutations(["addSelected", "removeSelected", "resetSelected"]),
humanSize: function () {
return filesize(this.size)
return filesize(this.size);
},
humanTime: function () {
return moment(this.modified).fromNow()
return moment(this.modified).fromNow();
},
dragStart: function () {
if (this.selectedCount === 0) {
this.addSelected(this.index)
return
this.addSelected(this.index);
return;
}
if (!this.isSelected) {
this.resetSelected()
this.addSelected(this.index)
this.resetSelected();
this.addSelected(this.index);
}
},
dragOver: function (event) {
if (!this.canDrop) return
if (!this.canDrop) return;
event.preventDefault()
let el = event.target
event.preventDefault();
let el = event.target;
for (let i = 0; i < 5; i++) {
if (!el.classList.contains('item')) {
el = el.parentElement
if (!el.classList.contains("item")) {
el = el.parentElement;
}
}
el.style.opacity = 1
el.style.opacity = 1;
},
drop: async function (event) {
if (!this.canDrop) return
event.preventDefault()
if (!this.canDrop) return;
event.preventDefault();
if (this.selectedCount === 0) return
if (this.selectedCount === 0) return;
let el = event.target
let el = event.target;
for (let i = 0; i < 5; i++) {
if (el !== null && !el.classList.contains('item')) {
el = el.parentElement
if (el !== null && !el.classList.contains("item")) {
el = el.parentElement;
}
}
let items = []
let items = [];
for (let i of this.selected) {
items.push({
from: this.req.items[i].url,
to: this.url + this.req.items[i].name,
name: this.req.items[i].name
})
to: this.url + encodeURIComponent(this.req.items[i].name),
name: this.req.items[i].name,
});
}
let base = el.querySelector('.name').innerHTML + '/'
let path = this.$route.path + base
let baseItems = (await api.fetch(path)).items
// Get url from ListingItem instance
let path = el.__vue__.url;
let baseItems = (await api.fetch(path)).items;
let action = (overwrite, rename) => {
api.move(items, overwrite, rename).then(() => {
this.$store.commit('setReload', true)
}).catch(this.$showError)
}
api
.move(items, overwrite, rename)
.then(() => {
this.$store.commit("setReload", true);
})
.catch(this.$showError);
};
let conflict = upload.checkConflict(items, baseItems)
let conflict = upload.checkConflict(items, baseItems);
let overwrite = false
let rename = false
let overwrite = false;
let rename = false;
if (conflict) {
this.$store.commit('showHover', {
prompt: 'replace-rename',
this.$store.commit("showHover", {
prompt: "replace-rename",
confirm: (event, option) => {
overwrite = option == 'overwrite'
rename = option == 'rename'
overwrite = option == "overwrite";
rename = option == "rename";
event.preventDefault()
this.$store.commit('closeHovers')
action(overwrite, rename)
}
})
event.preventDefault();
this.$store.commit("closeHovers");
action(overwrite, rename);
},
});
return
return;
}
action(overwrite, rename)
action(overwrite, rename);
},
itemClick: function(event) {
if (this.singleClick && !this.$store.state.multiple) this.open()
else this.click(event)
itemClick: function (event) {
if (this.singleClick && !this.$store.state.multiple) this.open();
else this.click(event);
},
click: function (event) {
if (!this.singleClick && this.selectedCount !== 0) event.preventDefault()
if (!this.singleClick && this.selectedCount !== 0) event.preventDefault();
setTimeout(() => {
this.touches = 0;
}, 300);
this.touches++;
if (this.touches > 1) {
this.open();
}
if (this.$store.state.selected.indexOf(this.index) !== -1) {
this.removeSelected(this.index)
return
this.removeSelected(this.index);
return;
}
if (event.shiftKey && this.selected.length > 0) {
let fi = 0
let la = 0
let fi = 0;
let la = 0;
if (this.index > this.selected[0]) {
fi = this.selected[0] + 1
la = this.index
fi = this.selected[0] + 1;
la = this.index;
} else {
fi = this.index
la = this.selected[0] - 1
fi = this.index;
la = this.selected[0] - 1;
}
for (; fi <= la; fi++) {
if (this.$store.state.selected.indexOf(fi) == -1) {
this.addSelected(fi)
this.addSelected(fi);
}
}
return
return;
}
if (!this.singleClick && !event.ctrlKey && !event.metaKey && !this.$store.state.multiple) this.resetSelected()
this.addSelected(this.index)
},
dblclick: function () {
if (!this.singleClick) this.open()
},
touchstart () {
setTimeout(() => {
this.touches = 0
}, 300)
this.touches++
if (this.touches > 1) {
this.open()
}
if (
!this.singleClick &&
!event.ctrlKey &&
!event.metaKey &&
!this.$store.state.multiple
)
this.resetSelected();
this.addSelected(this.index);
},
open: function () {
this.$router.push({path: this.url})
}
}
}
</script>
this.$router.push({ path: this.url });
},
},
};
</script>

View File

@@ -1,5 +1,5 @@
<template>
<button @click="action" :aria-label="label" :title="label" class="action">
<button @click="action" :aria-label="label" :title="label" class="action">
<i class="material-icons">{{ icon }}</i>
<span>{{ label }}</span>
<span v-if="counter > 0" class="counter">{{ counter }}</span>
@@ -8,25 +8,18 @@
<script>
export default {
name: 'action',
props: [
'icon',
'label',
'counter',
'show'
],
name: "action",
props: ["icon", "label", "counter", "show"],
methods: {
action: function () {
if (this.show) {
this.$store.commit('showHover', this.show)
this.$store.commit("showHover", this.show);
}
this.$emit('action')
}
}
}
this.$emit("action");
},
},
};
</script>
<style>
</style>
<style></style>

View File

@@ -1,7 +1,13 @@
<template>
<header>
<img v-if="showLogo !== undefined" :src="logoURL" />
<action v-if="showMenu !== undefined" class="menu-button" icon="menu" :label="$t('buttons.toggleSidebar')" @action="openSidebar()" />
<action
v-if="showMenu !== undefined"
class="menu-button"
icon="menu"
:label="$t('buttons.toggleSidebar')"
@action="openSidebar()"
/>
<slot />
@@ -9,39 +15,44 @@
<slot name="actions" />
</div>
<action v-if="this.$slots.actions" id="more" icon="more_vert" :label="$t('buttons.more')" @action="$store.commit('showHover', 'more')" />
<action
v-if="this.$slots.actions"
id="more"
icon="more_vert"
:label="$t('buttons.more')"
@action="$store.commit('showHover', 'more')"
/>
<div class="overlay" v-show="this.$store.state.show == 'more'" @click="$store.commit('closeHovers')" />
<div
class="overlay"
v-show="this.$store.state.show == 'more'"
@click="$store.commit('closeHovers')"
/>
</header>
</template>
<script>
import { logoURL } from '@/utils/constants'
import { logoURL } from "@/utils/constants";
import Action from '@/components/header/Action'
import Action from "@/components/header/Action";
export default {
name: 'header-bar',
props: [
'showLogo',
'showMenu',
],
name: "header-bar",
props: ["showLogo", "showMenu"],
components: {
Action
Action,
},
data: function () {
return {
logoURL
}
logoURL,
};
},
methods: {
openSidebar () {
this.$store.commit('showHover', 'sidebar')
}
}
}
openSidebar() {
this.$store.commit("showHover", "sidebar");
},
},
};
</script>
<style>
</style>
<style></style>

View File

@@ -1,108 +1,119 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.copy') }}</h2>
<h2>{{ $t("prompts.copy") }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.copyMessage') }}</p>
<file-list @update:selected="val => dest = val"></file-list>
<p>{{ $t("prompts.copyMessage") }}</p>
<file-list @update:selected="(val) => (dest = val)"></file-list>
</div>
<div class="card-action">
<button class="button button--flat button--grey"
<button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="button button--flat"
:title="$t('buttons.cancel')"
>
{{ $t("buttons.cancel") }}
</button>
<button
class="button button--flat"
@click="copy"
:aria-label="$t('buttons.copy')"
:title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button>
:title="$t('buttons.copy')"
>
{{ $t("buttons.copy") }}
</button>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
import FileList from './FileList'
import { files as api } from '@/api'
import buttons from '@/utils/buttons'
import * as upload from '@/utils/upload'
import { mapState } from "vuex";
import FileList from "./FileList";
import { files as api } from "@/api";
import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload";
export default {
name: 'copy',
name: "copy",
components: { FileList },
data: function () {
return {
current: window.location.pathname,
dest: null
}
dest: null,
};
},
computed: mapState(['req', 'selected']),
computed: mapState(["req", "selected"]),
methods: {
copy: async function (event) {
event.preventDefault()
let items = []
event.preventDefault();
let items = [];
// Create a new promise for each file.
for (let item of this.selected) {
items.push({
from: this.req.items[item].url,
to: this.dest + encodeURIComponent(this.req.items[item].name),
name: this.req.items[item].name
})
name: this.req.items[item].name,
});
}
let action = async (overwrite, rename) => {
buttons.loading('copy')
buttons.loading("copy");
await api.copy(items, overwrite, rename).then(() => {
buttons.success('copy')
await api
.copy(items, overwrite, rename)
.then(() => {
buttons.success("copy");
if (this.$route.path === this.dest) {
this.$store.commit('setReload', true)
if (this.$route.path === this.dest) {
this.$store.commit("setReload", true);
return
}
return;
}
this.$router.push({ path: this.dest })
}).catch((e) => {
buttons.done('copy')
this.$showError(e)
})
}
this.$router.push({ path: this.dest });
})
.catch((e) => {
buttons.done("copy");
this.$showError(e);
});
};
if (this.$route.path === this.dest) {
this.$store.commit('closeHovers')
action(false, true)
this.$store.commit("closeHovers");
action(false, true);
return
return;
}
let dstItems = (await api.fetch(this.dest)).items
let conflict = upload.checkConflict(items, dstItems)
let dstItems = (await api.fetch(this.dest)).items;
let conflict = upload.checkConflict(items, dstItems);
let overwrite = false
let rename = false
let overwrite = false;
let rename = false;
if (conflict) {
this.$store.commit('showHover', {
prompt: 'replace-rename',
this.$store.commit("showHover", {
prompt: "replace-rename",
confirm: (event, option) => {
overwrite = option == 'overwrite'
rename = option == 'rename'
overwrite = option == "overwrite";
rename = option == "rename";
event.preventDefault()
this.$store.commit('closeHovers')
action(overwrite, rename)
}
})
event.preventDefault();
this.$store.commit("closeHovers");
action(overwrite, rename);
},
});
return
return;
}
action(overwrite, rename)
}
}
}
action(overwrite, rename);
},
},
};
</script>

View File

@@ -1,68 +1,80 @@
<template>
<div class="card floating">
<div class="card-content">
<p v-if="req.kind !== 'listing'">{{ $t('prompts.deleteMessageSingle') }}</p>
<p v-else>{{ $t('prompts.deleteMessageMultiple', { count: selectedCount}) }}</p>
<p v-if="req.kind !== 'listing'">
{{ $t("prompts.deleteMessageSingle") }}
</p>
<p v-else>
{{ $t("prompts.deleteMessageMultiple", { count: selectedCount }) }}
</p>
</div>
<div class="card-action">
<button @click="$store.commit('closeHovers')"
<button
@click="$store.commit('closeHovers')"
class="button button--flat button--grey"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button @click="submit"
:title="$t('buttons.cancel')"
>
{{ $t("buttons.cancel") }}
</button>
<button
@click="submit"
class="button button--flat button--red"
:aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
:title="$t('buttons.delete')"
>
{{ $t("buttons.delete") }}
</button>
</div>
</div>
</template>
<script>
import {mapGetters, mapMutations, mapState} from 'vuex'
import { files as api } from '@/api'
import buttons from '@/utils/buttons'
import { mapGetters, mapMutations, mapState } from "vuex";
import { files as api } from "@/api";
import buttons from "@/utils/buttons";
export default {
name: 'delete',
name: "delete",
computed: {
...mapGetters(['isListing', 'selectedCount']),
...mapState(['req', 'selected', 'showConfirm'])
...mapGetters(["isListing", "selectedCount"]),
...mapState(["req", "selected", "showConfirm"]),
},
methods: {
...mapMutations(['closeHovers']),
...mapMutations(["closeHovers"]),
submit: async function () {
buttons.loading('delete')
buttons.loading("delete");
try {
if (!this.isListing) {
await api.remove(this.$route.path)
buttons.success('delete')
await api.remove(this.$route.path);
buttons.success("delete");
this.showConfirm()
this.closeHovers()
return
this.showConfirm();
this.closeHovers();
return;
}
this.closeHovers()
this.closeHovers();
if (this.selectedCount === 0) {
return
return;
}
let promises = []
let promises = [];
for (let index of this.selected) {
promises.push(api.remove(this.req.items[index].url))
promises.push(api.remove(this.req.items[index].url));
}
await Promise.all(promises)
buttons.success('delete')
this.$store.commit('setReload', true)
await Promise.all(promises);
buttons.success("delete");
this.$store.commit("setReload", true);
} catch (e) {
buttons.done('delete')
this.$showError(e)
if (this.isListing) this.$store.commit('setReload', true)
buttons.done("delete");
this.$showError(e);
if (this.isListing) this.$store.commit("setReload", true);
}
}
}
}
},
},
};
</script>

View File

@@ -1,35 +1,43 @@
<template>
<div class="card floating" id="download">
<div class="card-title">
<h2>{{ $t('prompts.download') }}</h2>
<h2>{{ $t("prompts.download") }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.downloadMessage') }}</p>
<p>{{ $t("prompts.downloadMessage") }}</p>
<button v-for="(ext, format) in formats" :key="format" class="button button--block" @click="showConfirm(format)" v-focus>{{ ext }}</button>
<button
v-for="(ext, format) in formats"
:key="format"
class="button button--block"
@click="showConfirm(format)"
v-focus
>
{{ ext }}
</button>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
import { mapState } from "vuex";
export default {
name: 'download',
name: "download",
data: function () {
return {
formats: {
zip: 'zip',
tar: 'tar',
targz: 'tar.gz',
tarbz2: 'tar.bz2',
tarxz: 'tar.xz',
tarlz4: 'tar.lz4',
tarsz: 'tar.sz'
}
}
zip: "zip",
tar: "tar",
targz: "tar.gz",
tarbz2: "tar.bz2",
tarxz: "tar.xz",
tarlz4: "tar.lz4",
tarsz: "tar.sz",
},
};
},
computed: mapState(['showConfirm'])
}
computed: mapState(["showConfirm"]),
};
</script>

View File

@@ -1,132 +1,138 @@
<template>
<div>
<ul class="file-list">
<li @click="itemClick"
<li
@click="itemClick"
@touchstart="touchstart"
@dblclick="next"
role="button"
tabindex="0"
:aria-label="item.name"
:aria-selected="selected == item.url"
:key="item.name" v-for="item in items"
:data-url="item.url">{{ item.name }}</li>
:key="item.name"
v-for="item in items"
:data-url="item.url"
>
{{ item.name }}
</li>
</ul>
<p>{{ $t('prompts.currentlyNavigating') }} <code>{{ nav }}</code>.</p>
<p>
{{ $t("prompts.currentlyNavigating") }} <code>{{ nav }}</code
>.
</p>
</div>
</template>
<script>
import { mapState } from 'vuex'
import url from '@/utils/url'
import { files } from '@/api'
import { mapState } from "vuex";
import url from "@/utils/url";
import { files } from "@/api";
export default {
name: 'file-list',
name: "file-list",
data: function () {
return {
items: [],
touches: {
id: '',
count: 0
id: "",
count: 0,
},
selected: null,
current: window.location.pathname
}
current: window.location.pathname,
};
},
computed: {
...mapState([ 'req', 'user' ]),
nav () {
return decodeURIComponent(this.current)
}
...mapState(["req", "user"]),
nav() {
return decodeURIComponent(this.current);
},
},
mounted () {
this.fillOptions(this.req)
mounted() {
this.fillOptions(this.req);
},
methods: {
fillOptions (req) {
fillOptions(req) {
// Sets the current path and resets
// the current items.
this.current = req.url
this.items = []
this.current = req.url;
this.items = [];
this.$emit('update:selected', this.current)
this.$emit("update:selected", this.current);
// If the path isn't the root path,
// show a button to navigate to the previous
// directory.
if (req.url !== '/files/') {
if (req.url !== "/files/") {
this.items.push({
name: '..',
url: url.removeLastDir(req.url) + '/'
})
name: "..",
url: url.removeLastDir(req.url) + "/",
});
}
// If this folder is empty, finish here.
if (req.items === null) return
if (req.items === null) return;
// Otherwise we add every directory to the
// move options.
for (let item of req.items) {
if (!item.isDir) continue
if (!item.isDir) continue;
this.items.push({
name: item.name,
url: item.url
})
url: item.url,
});
}
},
next: function (event) {
// Retrieves the URL of the directory the user
// just clicked in and fill the options with its
// content.
let uri = event.currentTarget.dataset.url
let uri = event.currentTarget.dataset.url;
files.fetch(uri)
.then(this.fillOptions)
.catch(this.$showError)
files.fetch(uri).then(this.fillOptions).catch(this.$showError);
},
touchstart (event) {
let url = event.currentTarget.dataset.url
touchstart(event) {
let url = event.currentTarget.dataset.url;
// In 300 milliseconds, we shall reset the count.
setTimeout(() => {
this.touches.count = 0
}, 300)
this.touches.count = 0;
}, 300);
// If the element the user is touching
// is different from the last one he touched,
// reset the count.
if (this.touches.id !== url) {
this.touches.id = url
this.touches.count = 1
return
this.touches.id = url;
this.touches.count = 1;
return;
}
this.touches.count++
this.touches.count++;
// If there is more than one touch already,
// open the next screen.
if (this.touches.count > 1) {
this.next(event)
this.next(event);
}
},
itemClick: function (event) {
if (this.user.singleClick) this.next(event)
else this.select(event)
if (this.user.singleClick) this.next(event);
else this.select(event);
},
select: function (event) {
// If the element is already selected, unselect it.
if (this.selected === event.currentTarget.dataset.url) {
this.selected = null
this.$emit('update:selected', this.current)
return
this.selected = null;
this.$emit("update:selected", this.current);
return;
}
// Otherwise select the element.
this.selected = event.currentTarget.dataset.url
this.$emit('update:selected', this.selected)
}
}
}
this.selected = event.currentTarget.dataset.url;
this.$emit("update:selected", this.selected);
},
},
};
</script>

View File

@@ -1,34 +1,37 @@
<template>
<div class="card floating help">
<div class="card-title">
<h2>{{ $t('help.help') }}</h2>
<h2>{{ $t("help.help") }}</h2>
</div>
<div class="card-content">
<ul>
<li><strong>F1</strong> - {{ $t('help.f1') }}</li>
<li><strong>F2</strong> - {{ $t('help.f2') }}</li>
<li><strong>DEL</strong> - {{ $t('help.del') }}</li>
<li><strong>ESC</strong> - {{ $t('help.esc') }}</li>
<li><strong>CTRL + S</strong> - {{ $t('help.ctrl.s') }}</li>
<li><strong>CTRL + F</strong> - {{ $t('help.ctrl.f') }}</li>
<li><strong>CTRL + Click</strong> - {{ $t('help.ctrl.click') }}</li>
<li><strong>Click</strong> - {{ $t('help.click') }}</li>
<li><strong>Double click</strong> - {{ $t('help.doubleClick') }}</li>
<li><strong>F1</strong> - {{ $t("help.f1") }}</li>
<li><strong>F2</strong> - {{ $t("help.f2") }}</li>
<li><strong>DEL</strong> - {{ $t("help.del") }}</li>
<li><strong>ESC</strong> - {{ $t("help.esc") }}</li>
<li><strong>CTRL + S</strong> - {{ $t("help.ctrl.s") }}</li>
<li><strong>CTRL + F</strong> - {{ $t("help.ctrl.f") }}</li>
<li><strong>CTRL + Click</strong> - {{ $t("help.ctrl.click") }}</li>
<li><strong>Click</strong> - {{ $t("help.click") }}</li>
<li><strong>Double click</strong> - {{ $t("help.doubleClick") }}</li>
</ul>
</div>
<div class="card-action">
<button type="submit"
<button
type="submit"
@click="$store.commit('closeHovers')"
class="button button--flat"
:aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
:title="$t('buttons.ok')"
>
{{ $t("buttons.ok") }}
</button>
</div>
</div>
</template>
<script>
export default { name: 'help' }
export default { name: "help" };
</script>

View File

@@ -1,99 +1,152 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.fileInfo') }}</h2>
<h2>{{ $t("prompts.fileInfo") }}</h2>
</div>
<div class="card-content">
<p v-if="selected.length > 1">{{ $t('prompts.filesSelected', { count: selected.length }) }}</p>
<p v-if="selected.length > 1">
{{ $t("prompts.filesSelected", { count: selected.length }) }}
</p>
<p class="break-word" v-if="selected.length < 2"><strong>{{ $t('prompts.displayName') }}</strong> {{ name }}</p>
<p v-if="!dir || selected.length > 1"><strong>{{ $t('prompts.size') }}:</strong> <span id="content_length"></span> {{ humanSize }}</p>
<p v-if="selected.length < 2"><strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime }}</p>
<p class="break-word" v-if="selected.length < 2">
<strong>{{ $t("prompts.displayName") }}</strong> {{ name }}
</p>
<p v-if="!dir || selected.length > 1">
<strong>{{ $t("prompts.size") }}:</strong>
<span id="content_length"></span> {{ humanSize }}
</p>
<p v-if="selected.length < 2" :title="modTime">
<strong>{{ $t("prompts.lastModified") }}:</strong> {{ humanTime }}
</p>
<template v-if="dir && selected.length === 0">
<p><strong>{{ $t('prompts.numberFiles') }}:</strong> {{ req.numFiles }}</p>
<p><strong>{{ $t('prompts.numberDirs') }}:</strong> {{ req.numDirs }}</p>
<p>
<strong>{{ $t("prompts.numberFiles") }}:</strong> {{ req.numFiles }}
</p>
<p>
<strong>{{ $t("prompts.numberDirs") }}:</strong> {{ req.numDirs }}
</p>
</template>
<template v-if="!dir">
<p><strong>MD5: </strong><code><a @click="checksum($event, 'md5')">{{ $t('prompts.show') }}</a></code></p>
<p><strong>SHA1: </strong><code><a @click="checksum($event, 'sha1')">{{ $t('prompts.show') }}</a></code></p>
<p><strong>SHA256: </strong><code><a @click="checksum($event, 'sha256')">{{ $t('prompts.show') }}</a></code></p>
<p><strong>SHA512: </strong><code><a @click="checksum($event, 'sha512')">{{ $t('prompts.show') }}</a></code></p>
<p>
<strong>MD5: </strong
><code
><a @click="checksum($event, 'md5')">{{
$t("prompts.show")
}}</a></code
>
</p>
<p>
<strong>SHA1: </strong
><code
><a @click="checksum($event, 'sha1')">{{
$t("prompts.show")
}}</a></code
>
</p>
<p>
<strong>SHA256: </strong
><code
><a @click="checksum($event, 'sha256')">{{
$t("prompts.show")
}}</a></code
>
</p>
<p>
<strong>SHA512: </strong
><code
><a @click="checksum($event, 'sha512')">{{
$t("prompts.show")
}}</a></code
>
</p>
</template>
</div>
<div class="card-action">
<button type="submit"
<button
type="submit"
@click="$store.commit('closeHovers')"
class="button button--flat"
:aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
:title="$t('buttons.ok')"
>
{{ $t("buttons.ok") }}
</button>
</div>
</div>
</template>
<script>
import {mapState, mapGetters} from 'vuex'
import filesize from 'filesize'
import moment from 'moment'
import { files as api } from '@/api'
import { mapState, mapGetters } from "vuex";
import filesize from "filesize";
import moment from "moment";
import { files as api } from "@/api";
export default {
name: 'info',
name: "info",
computed: {
...mapState(['req', 'selected']),
...mapGetters(['selectedCount', 'isListing']),
...mapState(["req", "selected"]),
...mapGetters(["selectedCount", "isListing"]),
humanSize: function () {
if (this.selectedCount === 0 || !this.isListing) {
return filesize(this.req.size)
return filesize(this.req.size);
}
let sum = 0
let sum = 0;
for (let selected of this.selected) {
sum += this.req.items[selected].size
sum += this.req.items[selected].size;
}
return filesize(sum)
return filesize(sum);
},
humanTime: function () {
if (this.selectedCount === 0) {
return moment(this.req.modified).fromNow()
return moment(this.req.modified).fromNow();
}
return moment(this.req.items[this.selected[0]].modified).fromNow()
return moment(this.req.items[this.selected[0]].modified).fromNow();
},
modTime: function () {
return new Date(Date.parse(this.req.modified)).toLocaleString();
},
name: function () {
return this.selectedCount === 0 ? this.req.name : this.req.items[this.selected[0]].name
return this.selectedCount === 0
? this.req.name
: this.req.items[this.selected[0]].name;
},
dir: function () {
return this.selectedCount > 1 || (this.selectedCount === 0
? this.req.isDir
: this.req.items[this.selected[0]].isDir)
}
return (
this.selectedCount > 1 ||
(this.selectedCount === 0
? this.req.isDir
: this.req.items[this.selected[0]].isDir)
);
},
},
methods: {
checksum: async function (event, algo) {
event.preventDefault()
event.preventDefault();
let link
let link;
if (this.selectedCount) {
link = this.req.items[this.selected[0]].url
link = this.req.items[this.selected[0]].url;
} else {
link = this.$route.path
link = this.$route.path;
}
try {
const hash = await api.checksum(link, algo)
const hash = await api.checksum(link, algo);
// eslint-disable-next-line
event.target.innerHTML = hash
} catch (e) {
this.$showError(e)
this.$showError(e);
}
}
}
}
},
},
};
</script>

View File

@@ -1,93 +1,104 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.move') }}</h2>
<h2>{{ $t("prompts.move") }}</h2>
</div>
<div class="card-content">
<file-list @update:selected="val => dest = val"></file-list>
<file-list @update:selected="(val) => (dest = val)"></file-list>
</div>
<div class="card-action">
<button class="button button--flat button--grey"
<button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="button button--flat"
:title="$t('buttons.cancel')"
>
{{ $t("buttons.cancel") }}
</button>
<button
class="button button--flat"
@click="move"
:disabled="$route.path === dest"
:aria-label="$t('buttons.move')"
:title="$t('buttons.move')">{{ $t('buttons.move') }}</button>
:title="$t('buttons.move')"
>
{{ $t("buttons.move") }}
</button>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
import FileList from './FileList'
import { files as api } from '@/api'
import buttons from '@/utils/buttons'
import * as upload from '@/utils/upload'
import { mapState } from "vuex";
import FileList from "./FileList";
import { files as api } from "@/api";
import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload";
export default {
name: 'move',
name: "move",
components: { FileList },
data: function () {
return {
current: window.location.pathname,
dest: null
}
dest: null,
};
},
computed: mapState(['req', 'selected']),
computed: mapState(["req", "selected"]),
methods: {
move: async function (event) {
event.preventDefault()
let items = []
event.preventDefault();
let items = [];
for (let item of this.selected) {
items.push({
from: this.req.items[item].url,
to: this.dest + encodeURIComponent(this.req.items[item].name),
name: this.req.items[item].name
})
name: this.req.items[item].name,
});
}
let action = async (overwrite, rename) => {
buttons.loading('move')
buttons.loading("move");
await api.move(items, overwrite, rename).then(() => {
buttons.success('move')
this.$router.push({ path: this.dest })
}).catch((e) => {
buttons.done('move')
this.$showError(e)
})
}
await api
.move(items, overwrite, rename)
.then(() => {
buttons.success("move");
this.$router.push({ path: this.dest });
})
.catch((e) => {
buttons.done("move");
this.$showError(e);
});
};
let dstItems = (await api.fetch(this.dest)).items
let conflict = upload.checkConflict(items, dstItems)
let dstItems = (await api.fetch(this.dest)).items;
let conflict = upload.checkConflict(items, dstItems);
let overwrite = false
let rename = false
let overwrite = false;
let rename = false;
if (conflict) {
this.$store.commit('showHover', {
prompt: 'replace-rename',
this.$store.commit("showHover", {
prompt: "replace-rename",
confirm: (event, option) => {
overwrite = option == 'overwrite'
rename = option == 'rename'
overwrite = option == "overwrite";
rename = option == "rename";
event.preventDefault()
this.$store.commit('closeHovers')
action(overwrite, rename)
}
})
event.preventDefault();
this.$store.commit("closeHovers");
action(overwrite, rename);
},
});
return
return;
}
action(overwrite, rename)
}
}
}
action(overwrite, rename);
},
},
};
</script>

View File

@@ -1,12 +1,18 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.newDir') }}</h2>
<h2>{{ $t("prompts.newDir") }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.newDirMessage') }}</p>
<input class="input input--block" type="text" @keyup.enter="submit" v-model.trim="name" v-focus>
<p>{{ $t("prompts.newDirMessage") }}</p>
<input
class="input input--block"
type="text"
@keyup.enter="submit"
v-model.trim="name"
v-focus
/>
</div>
<div class="card-action">
@@ -15,57 +21,60 @@
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
>{{ $t('buttons.cancel') }}</button>
>
{{ $t("buttons.cancel") }}
</button>
<button
class="button button--flat"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')"
@click="submit"
>{{ $t('buttons.create') }}</button>
>
{{ $t("buttons.create") }}
</button>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import { files as api } from '@/api'
import url from '@/utils/url'
import { mapGetters } from "vuex";
import { files as api } from "@/api";
import url from "@/utils/url";
export default {
name: 'new-dir',
data: function() {
name: "new-dir",
data: function () {
return {
name: ''
name: "",
};
},
computed: {
...mapGetters([ 'isFiles', 'isListing' ])
...mapGetters(["isFiles", "isListing"]),
},
methods: {
submit: async function(event) {
event.preventDefault()
if (this.new === '') return
submit: async function (event) {
event.preventDefault();
if (this.new === "") return;
// Build the path of the new directory.
let uri = this.isFiles ? this.$route.path + '/' : '/'
let uri = this.isFiles ? this.$route.path + "/" : "/";
if (!this.isListing) {
uri = url.removeLastDir(uri) + '/'
uri = url.removeLastDir(uri) + "/";
}
uri += encodeURIComponent(this.name) + '/'
uri = uri.replace('//', '/')
uri += encodeURIComponent(this.name) + "/";
uri = uri.replace("//", "/");
try {
await api.post(uri)
this.$router.push({ path: uri })
await api.post(uri);
this.$router.push({ path: uri });
} catch (e) {
this.$showError(e)
this.$showError(e);
}
this.$store.commit('closeHovers')
}
}
this.$store.commit("closeHovers");
},
},
};
</script>

View File

@@ -1,12 +1,18 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.newFile') }}</h2>
<h2>{{ $t("prompts.newFile") }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.newFileMessage') }}</p>
<input class="input input--block" v-focus type="text" @keyup.enter="submit" v-model.trim="name">
<p>{{ $t("prompts.newFileMessage") }}</p>
<input
class="input input--block"
v-focus
type="text"
@keyup.enter="submit"
v-model.trim="name"
/>
</div>
<div class="card-action">
@@ -15,57 +21,60 @@
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
>{{ $t('buttons.cancel') }}</button>
>
{{ $t("buttons.cancel") }}
</button>
<button
class="button button--flat"
@click="submit"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')"
>{{ $t('buttons.create') }}</button>
>
{{ $t("buttons.create") }}
</button>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import { files as api } from '@/api'
import url from '@/utils/url'
import { mapGetters } from "vuex";
import { files as api } from "@/api";
import url from "@/utils/url";
export default {
name: 'new-file',
data: function() {
name: "new-file",
data: function () {
return {
name: ''
name: "",
};
},
computed: {
...mapGetters([ 'isFiles', 'isListing' ])
...mapGetters(["isFiles", "isListing"]),
},
methods: {
submit: async function(event) {
event.preventDefault()
if (this.new === '') return
submit: async function (event) {
event.preventDefault();
if (this.new === "") return;
// Build the path of the new directory.
let uri = this.isFiles ? this.$route.path + '/' : '/'
let uri = this.isFiles ? this.$route.path + "/" : "/";
if (!this.isListing) {
uri = url.removeLastDir(uri) + '/'
uri = url.removeLastDir(uri) + "/";
}
uri += encodeURIComponent(this.name)
uri = uri.replace('//', '/')
uri += encodeURIComponent(this.name);
uri = uri.replace("//", "/");
try {
await api.post(uri)
this.$router.push({ path: uri })
await api.post(uri);
this.$router.push({ path: uri });
} catch (e) {
this.$showError(e)
this.$showError(e);
}
this.$store.commit('closeHovers')
}
}
this.$store.commit("closeHovers");
},
},
};
</script>

View File

@@ -6,25 +6,25 @@
</template>
<script>
import Help from './Help'
import Info from './Info'
import Delete from './Delete'
import Rename from './Rename'
import Download from './Download'
import Move from './Move'
import Copy from './Copy'
import NewFile from './NewFile'
import NewDir from './NewDir'
import Replace from './Replace'
import ReplaceRename from './ReplaceRename'
import Share from './Share'
import Upload from './Upload'
import ShareDelete from './ShareDelete'
import { mapState } from 'vuex'
import buttons from '@/utils/buttons'
import Help from "./Help";
import Info from "./Info";
import Delete from "./Delete";
import Rename from "./Rename";
import Download from "./Download";
import Move from "./Move";
import Copy from "./Copy";
import NewFile from "./NewFile";
import NewDir from "./NewDir";
import Replace from "./Replace";
import ReplaceRename from "./ReplaceRename";
import Share from "./Share";
import Upload from "./Upload";
import ShareDelete from "./ShareDelete";
import { mapState } from "vuex";
import buttons from "@/utils/buttons";
export default {
name: 'prompts',
name: "prompts",
components: {
Info,
Delete,
@@ -39,74 +39,81 @@ export default {
Replace,
ReplaceRename,
Upload,
ShareDelete
ShareDelete,
},
data: function () {
return {
pluginData: {
buttons,
'store': this.$store,
'router': this.$router
}
}
store: this.$store,
router: this.$router,
},
};
},
created () {
window.addEventListener('keydown', (event) => {
if (this.show == null)
return
created() {
window.addEventListener("keydown", (event) => {
if (this.show == null) return;
let prompt = this.$refs.currentComponent;
// Esc!
if (event.keyCode === 27) {
event.stopImmediatePropagation();
this.$store.commit("closeHovers");
}
// Enter
if (event.keyCode == 13) {
switch (this.show) {
case 'delete':
prompt.submit()
case "delete":
prompt.submit();
break;
case 'copy':
prompt.copy(event)
case "copy":
prompt.copy(event);
break;
case 'move':
prompt.move(event)
case "move":
prompt.move(event);
break;
case 'replace':
prompt.showConfirm(event)
case "replace":
prompt.showConfirm(event);
break;
}
}
})
});
},
computed: {
...mapState(['show', 'plugins']),
...mapState(["show", "plugins"]),
currentComponent: function () {
const matched = [
'info',
'help',
'delete',
'rename',
'move',
'copy',
'newFile',
'newDir',
'download',
'replace',
'replace-rename',
'share',
'upload',
'share-delete'
].indexOf(this.show) >= 0;
const matched =
[
"info",
"help",
"delete",
"rename",
"move",
"copy",
"newFile",
"newDir",
"download",
"replace",
"replace-rename",
"share",
"upload",
"share-delete",
].indexOf(this.show) >= 0;
return matched && this.show || null;
return (matched && this.show) || null;
},
showOverlay: function () {
return (this.show !== null && this.show !== 'search' && this.show !== 'more')
}
return (
this.show !== null && this.show !== "search" && this.show !== "more"
);
},
},
methods: {
resetPrompts () {
this.$store.commit('closeHovers')
}
}
}
resetPrompts() {
this.$store.commit("closeHovers");
},
},
};
</script>

View File

@@ -1,89 +1,107 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.rename') }}</h2>
<h2>{{ $t("prompts.rename") }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.renameMessage') }} <code>{{ oldName() }}</code>:</p>
<input class="input input--block" v-focus type="text" @keyup.enter="submit" v-model.trim="name">
<p>
{{ $t("prompts.renameMessage") }} <code>{{ oldName() }}</code
>:
</p>
<input
class="input input--block"
v-focus
type="text"
@keyup.enter="submit"
v-model.trim="name"
/>
</div>
<div class="card-action">
<button class="button button--flat button--grey"
<button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button @click="submit"
:title="$t('buttons.cancel')"
>
{{ $t("buttons.cancel") }}
</button>
<button
@click="submit"
class="button button--flat"
type="submit"
:aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button>
:title="$t('buttons.rename')"
>
{{ $t("buttons.rename") }}
</button>
</div>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
import url from '@/utils/url'
import { files as api } from '@/api'
import { mapState, mapGetters } from "vuex";
import url from "@/utils/url";
import { files as api } from "@/api";
export default {
name: 'rename',
name: "rename",
data: function () {
return {
name: ''
}
name: "",
};
},
created () {
this.name = this.oldName()
created() {
this.name = this.oldName();
},
computed: {
...mapState(['req', 'selected', 'selectedCount']),
...mapGetters(['isListing'])
...mapState(["req", "selected", "selectedCount"]),
...mapGetters(["isListing"]),
},
methods: {
cancel: function () {
this.$store.commit('closeHovers')
this.$store.commit("closeHovers");
},
oldName: function () {
if (!this.isListing) {
return this.req.name
return this.req.name;
}
if (this.selectedCount === 0 || this.selectedCount > 1) {
// This shouldn't happen.
return
return;
}
return this.req.items[this.selected[0]].name
return this.req.items[this.selected[0]].name;
},
submit: async function () {
let oldLink = ''
let newLink = ''
let oldLink = "";
let newLink = "";
if (!this.isListing) {
oldLink = this.req.url
oldLink = this.req.url;
} else {
oldLink = this.req.items[this.selected[0]].url
oldLink = this.req.items[this.selected[0]].url;
}
newLink = url.removeLastDir(oldLink) + '/' + encodeURIComponent(this.name)
newLink =
url.removeLastDir(oldLink) + "/" + encodeURIComponent(this.name);
try {
await api.move([{ from: oldLink, to: newLink }])
await api.move([{ from: oldLink, to: newLink }]);
if (!this.isListing) {
this.$router.push({ path: newLink })
return
this.$router.push({ path: newLink });
return;
}
this.$store.commit('setReload', true)
this.$store.commit("setReload", true);
} catch (e) {
this.$showError(e)
this.$showError(e);
}
this.$store.commit('closeHovers')
}
}
}
this.$store.commit("closeHovers");
},
},
};
</script>

View File

@@ -1,31 +1,39 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.replace') }}</h2>
<h2>{{ $t("prompts.replace") }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.replaceMessage') }}</p>
<p>{{ $t("prompts.replaceMessage") }}</p>
</div>
<div class="card-action">
<button class="button button--flat button--grey"
<button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="button button--flat button--red"
:title="$t('buttons.cancel')"
>
{{ $t("buttons.cancel") }}
</button>
<button
class="button button--flat button--red"
@click="showConfirm"
:aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')">{{ $t('buttons.replace') }}</button>
:title="$t('buttons.replace')"
>
{{ $t("buttons.replace") }}
</button>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
import { mapState } from "vuex";
export default {
name: 'replace',
computed: mapState(['showConfirm'])
}
name: "replace",
computed: mapState(["showConfirm"]),
};
</script>

View File

@@ -1,35 +1,47 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.replace') }}</h2>
<h2>{{ $t("prompts.replace") }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.replaceMessage') }}</p>
<p>{{ $t("prompts.replaceMessage") }}</p>
</div>
<div class="card-action">
<button class="button button--flat button--grey"
<button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="button button--flat button--blue"
:title="$t('buttons.cancel')"
>
{{ $t("buttons.cancel") }}
</button>
<button
class="button button--flat button--blue"
@click="(event) => showConfirm(event, 'rename')"
:aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button>
<button class="button button--flat button--red"
:title="$t('buttons.rename')"
>
{{ $t("buttons.rename") }}
</button>
<button
class="button button--flat button--red"
@click="(event) => showConfirm(event, 'overwrite')"
:aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')">{{ $t('buttons.replace') }}</button>
:title="$t('buttons.replace')"
>
{{ $t("buttons.replace") }}
</button>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
import { mapState } from "vuex";
export default {
name: 'replace-rename',
computed: mapState(['showConfirm'])
}
name: "replace-rename",
computed: mapState(["showConfirm"]),
};
</script>

View File

@@ -1,7 +1,7 @@
<template>
<div class="card floating share__promt__card" id="share">
<div class="card-title">
<h2>{{ $t('buttons.share') }}</h2>
<h2>{{ $t("buttons.share") }}</h2>
</div>
<template v-if="listing">
@@ -9,7 +9,7 @@
<table>
<tr>
<th>#</th>
<th>{{ $t('settings.shareDuration') }}</th>
<th>{{ $t("settings.shareDuration") }}</th>
<th></th>
<th></th>
</tr>
@@ -17,188 +17,219 @@
<tr v-for="link in links" :key="link.hash">
<td>{{ link.hash }}</td>
<td>
<template v-if="link.expire !== 0">{{ humanTime(link.expire) }}</template>
<template v-else>{{ $t('permanent') }}</template>
<template v-if="link.expire !== 0">{{
humanTime(link.expire)
}}</template>
<template v-else>{{ $t("permanent") }}</template>
</td>
<td class="small">
<button class="action copy-clipboard"
<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>
:title="$t('buttons.copyToClipboard')"
>
<i class="material-icons">content_paste</i>
</button>
</td>
<td class="small">
<button class="action"
<button
class="action"
@click="deleteLink($event, link)"
:aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')"><i class="material-icons">delete</i></button>
:title="$t('buttons.delete')"
>
<i class="material-icons">delete</i>
</button>
</td>
</tr>
</table>
</div>
<div class="card-action">
<button class="button button--flat button--grey"
<button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.close')"
:title="$t('buttons.close')">{{ $t('buttons.close') }}</button>
<button class="button button--flat button--blue"
:title="$t('buttons.close')"
>
{{ $t("buttons.close") }}
</button>
<button
class="button button--flat button--blue"
@click="() => switchListing()"
:aria-label="$t('buttons.new')"
:title="$t('buttons.new')">{{ $t('buttons.new') }}</button>
:title="$t('buttons.new')"
>
{{ $t("buttons.new") }}
</button>
</div>
</template>
<template v-else>
<div class="card-content">
<p>{{ $t('settings.shareDuration') }}</p>
<p>{{ $t("settings.shareDuration") }}</p>
<div class="input-group input">
<input v-focus
type="number"
max="2147483647"
min="1"
@keyup.enter="submit"
v-model.trim="time">
<select class="right" v-model="unit" :aria-label="$t('time.unit')">
<option value="seconds">{{ $t('time.seconds') }}</option>
<option value="minutes">{{ $t('time.minutes') }}</option>
<option value="hours">{{ $t('time.hours') }}</option>
<option value="days">{{ $t('time.days') }}</option>
</select>
<input
v-focus
type="number"
max="2147483647"
min="1"
@keyup.enter="submit"
v-model.trim="time"
/>
<select class="right" v-model="unit" :aria-label="$t('time.unit')">
<option value="seconds">{{ $t("time.seconds") }}</option>
<option value="minutes">{{ $t("time.minutes") }}</option>
<option value="hours">{{ $t("time.hours") }}</option>
<option value="days">{{ $t("time.days") }}</option>
</select>
</div>
<p>{{ $t('prompts.optionalPassword') }}</p>
<input class="input input--block" type="password" v-model.trim="password">
<p>{{ $t("prompts.optionalPassword") }}</p>
<input
class="input input--block"
type="password"
v-model.trim="password"
/>
</div>
<div class="card-action">
<button class="button button--flat button--grey"
<button
class="button button--flat button--grey"
@click="() => switchListing()"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="button button--flat button--blue"
:title="$t('buttons.cancel')"
>
{{ $t("buttons.cancel") }}
</button>
<button
class="button button--flat button--blue"
@click="submit"
:aria-label="$t('buttons.share')"
:title="$t('buttons.share')">{{ $t('buttons.share') }}</button>
:title="$t('buttons.share')"
>
{{ $t("buttons.share") }}
</button>
</div>
</template>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
import { share as api } from '@/api'
import { baseURL } from '@/utils/constants'
import moment from 'moment'
import Clipboard from 'clipboard'
import { mapState, mapGetters } from "vuex";
import { share as api } from "@/api";
import { baseURL } from "@/utils/constants";
import moment from "moment";
import Clipboard from "clipboard";
export default {
name: 'share',
name: "share",
data: function () {
return {
time: '',
unit: 'hours',
time: "",
unit: "hours",
links: [],
clip: null,
password: '',
listing: true
}
password: "",
listing: true,
};
},
computed: {
...mapState([ 'req', 'selected', 'selectedCount' ]),
...mapGetters([ 'isListing' ]),
url () {
...mapState(["req", "selected", "selectedCount"]),
...mapGetters(["isListing"]),
url() {
if (!this.isListing) {
return this.$route.path
return this.$route.path;
}
if (this.selectedCount === 0 || this.selectedCount > 1) {
// This shouldn't happen.
return
return;
}
return this.req.items[this.selected[0]].url
}
return this.req.items[this.selected[0]].url;
},
},
async beforeMount () {
async beforeMount() {
try {
const links = await api.get(this.url)
this.links = links
this.sort()
const links = await api.get(this.url);
this.links = links;
this.sort();
if (this.links.length == 0) {
this.listing = false
this.listing = false;
}
} catch (e) {
this.$showError(e)
this.$showError(e);
}
},
mounted () {
this.clip = new Clipboard('.copy-clipboard')
this.clip.on('success', () => {
this.$showSuccess(this.$t('success.linkCopied'))
})
mounted() {
this.clip = new Clipboard(".copy-clipboard");
this.clip.on("success", () => {
this.$showSuccess(this.$t("success.linkCopied"));
});
},
beforeDestroy () {
this.clip.destroy()
beforeDestroy() {
this.clip.destroy();
},
methods: {
submit: async function () {
let isPermanent = !this.time || this.time == 0
let isPermanent = !this.time || this.time == 0;
try {
let res = null
let res = null;
if (isPermanent) {
res = await api.create(this.url, this.password)
res = await api.create(this.url, this.password);
} else {
res = await api.create(this.url, this.password, this.time, this.unit)
res = await api.create(this.url, this.password, this.time, this.unit);
}
this.links.push(res)
this.sort()
this.links.push(res);
this.sort();
this.time = ''
this.unit = 'hours'
this.password = ''
this.time = "";
this.unit = "hours";
this.password = "";
this.listing = true
this.listing = true;
} catch (e) {
this.$showError(e)
this.$showError(e);
}
},
deleteLink: async function (event, link) {
event.preventDefault()
try {
await api.remove(link.hash)
this.links = this.links.filter(item => item.hash !== link.hash)
event.preventDefault();
try {
await api.remove(link.hash);
this.links = this.links.filter((item) => item.hash !== link.hash);
if (this.links.length == 0) {
this.listing = false
this.listing = false;
}
} catch (e) {
this.$showError(e)
this.$showError(e);
}
},
humanTime (time) {
return moment(time * 1000).fromNow()
humanTime(time) {
return moment(time * 1000).fromNow();
},
buildLink (hash) {
return `${window.location.origin}${baseURL}/share/${hash}`
buildLink(hash) {
return `${window.location.origin}${baseURL}/share/${hash}`;
},
sort () {
sort() {
this.links = this.links.sort((a, b) => {
if (a.expire === 0) return -1
if (b.expire === 0) return 1
return new Date(a.expire) - new Date(b.expire)
})
if (a.expire === 0) return -1;
if (b.expire === 0) return 1;
return new Date(a.expire) - new Date(b.expire);
});
},
switchListing () {
switchListing() {
if (this.links.length == 0 && !this.listing) {
this.$store.commit('closeHovers')
this.$store.commit("closeHovers");
}
this.listing = !this.listing
}
}
}
this.listing = !this.listing;
},
},
};
</script>

View File

@@ -1,33 +1,41 @@
<template>
<div class="card floating">
<div class="card-content">
<p>{{ $t('prompts.deleteMessageShare', {path: ''}) }}</p>
<p>{{ $t("prompts.deleteMessageShare", { path: "" }) }}</p>
</div>
<div class="card-action">
<button @click="$store.commit('closeHovers')"
<button
@click="$store.commit('closeHovers')"
class="button button--flat button--grey"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button @click="submit"
:title="$t('buttons.cancel')"
>
{{ $t("buttons.cancel") }}
</button>
<button
@click="submit"
class="button button--flat button--red"
:aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
:title="$t('buttons.delete')"
>
{{ $t("buttons.delete") }}
</button>
</div>
</div>
</template>
<script>
import {mapState} from 'vuex'
import { mapState } from "vuex";
export default {
name: 'share-delete',
name: "share-delete",
computed: {
...mapState(['showConfirm'])
...mapState(["showConfirm"]),
},
methods: {
submit: function () {
this.showConfirm()
}
}
}
this.showConfirm();
},
},
};
</script>

View File

@@ -1,11 +1,11 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.upload') }}</h2>
<h2>{{ $t("prompts.upload") }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.uploadMessage') }}</p>
<p>{{ $t("prompts.uploadMessage") }}</p>
</div>
<div class="card-action full">
@@ -22,18 +22,17 @@
</template>
<script>
export default {
name: 'upload',
name: "upload",
methods: {
uploadFile: function () {
document.getElementById('upload-input').value = ''
document.getElementById('upload-input').click()
document.getElementById("upload-input").value = "";
document.getElementById("upload-input").click();
},
uploadFolder: function () {
document.getElementById('upload-folder-input').value = ''
document.getElementById('upload-folder-input').click()
}
}
}
document.getElementById("upload-folder-input").value = "";
document.getElementById("upload-folder-input").click();
},
},
};
</script>

View File

@@ -1,28 +1,30 @@
<template>
<div>
<h3>{{ $t('settings.userCommands') }}</h3>
<p class="small">{{ $t('settings.userCommandsHelp') }} <i>git svn hg</i>.</p>
<input class="input input--block" type="text" v-model.trim="raw">
<h3>{{ $t("settings.userCommands") }}</h3>
<p class="small">
{{ $t("settings.userCommandsHelp") }} <i>git svn hg</i>.
</p>
<input class="input input--block" type="text" v-model.trim="raw" />
</div>
</template>
<script>
export default {
name: 'permissions',
props: ['commands'],
name: "permissions",
props: ["commands"],
computed: {
raw: {
get () {
return this.commands.join(' ')
get() {
return this.commands.join(" ");
},
set (value) {
if (value !== '') {
this.$emit('update:commands', value.split(' '))
set(value) {
if (value !== "") {
this.$emit("update:commands", value.split(" "));
} else {
this.$emit('update:commands', [])
this.$emit("update:commands", []);
}
}
}
}
}
},
},
},
};
</script>

View File

@@ -1,46 +1,50 @@
<template>
<select v-on:change="change" :value="locale">
<option v-for="(language, value) in locales" :key="value" :value="value">{{ $t('languages.' + language) }}</option>
<option v-for="(language, value) in locales" :key="value" :value="value">
{{ $t("languages." + language) }}
</option>
</select>
</template>
<script>
export default {
name: 'languages',
props: [ 'locale' ],
name: "languages",
props: ["locale"],
data() {
let dataObj = {
locales: {
ar: 'ar',
de: 'de',
en: 'en',
es: 'es',
fr: 'fr',
is: 'is',
it: 'it',
ja: 'ja',
ko: 'ko',
'nl-be': 'nlBE',
pl: 'pl',
'pt-br': 'ptBR',
pt: 'pt',
ro: 'ro',
ru: 'ru',
'sv-se': 'svSE',
'zh-cn': 'zhCN',
'zh-tw': 'zhTW'
}
ar: "ar",
de: "de",
en: "en",
es: "es",
fr: "fr",
is: "is",
it: "it",
ja: "ja",
ko: "ko",
"nl-be": "nlBE",
pl: "pl",
"pt-br": "ptBR",
pt: "pt",
ro: "ro",
ru: "ru",
"sv-se": "svSE",
"zh-cn": "zhCN",
"zh-tw": "zhTW",
},
};
Object.defineProperty(dataObj, "locales", { configurable: false, writable: false });
Object.defineProperty(dataObj, "locales", {
configurable: false,
writable: false,
});
return dataObj;
},
methods: {
change (event) {
this.$emit('update:locale', event.target.value)
}
}
}
change(event) {
this.$emit("update:locale", event.target.value);
},
},
};
</script>

View File

@@ -1,41 +1,65 @@
<template>
<div>
<h3>{{ $t('settings.permissions') }}</h3>
<p class="small">{{ $t('settings.permissionsHelp') }}</p>
<h3>{{ $t("settings.permissions") }}</h3>
<p class="small">{{ $t("settings.permissionsHelp") }}</p>
<p><input type="checkbox" v-model="admin"> {{ $t('settings.administrator') }}</p>
<p>
<input type="checkbox" v-model="admin" />
{{ $t("settings.administrator") }}
</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.create"> {{ $t('settings.perm.create') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.delete"> {{ $t('settings.perm.delete') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.download"> {{ $t('settings.perm.download') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.modify"> {{ $t('settings.perm.modify') }}</p>
<p v-if="isExecEnabled"><input type="checkbox" :disabled="admin" v-model="perm.execute"> {{ $t('settings.perm.execute') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.rename"> {{ $t('settings.perm.rename') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.share"> {{ $t('settings.perm.share') }}</p>
<p>
<input type="checkbox" :disabled="admin" v-model="perm.create" />
{{ $t("settings.perm.create") }}
</p>
<p>
<input type="checkbox" :disabled="admin" v-model="perm.delete" />
{{ $t("settings.perm.delete") }}
</p>
<p>
<input type="checkbox" :disabled="admin" v-model="perm.download" />
{{ $t("settings.perm.download") }}
</p>
<p>
<input type="checkbox" :disabled="admin" v-model="perm.modify" />
{{ $t("settings.perm.modify") }}
</p>
<p v-if="isExecEnabled">
<input type="checkbox" :disabled="admin" v-model="perm.execute" />
{{ $t("settings.perm.execute") }}
</p>
<p>
<input type="checkbox" :disabled="admin" v-model="perm.rename" />
{{ $t("settings.perm.rename") }}
</p>
<p>
<input type="checkbox" :disabled="admin" v-model="perm.share" />
{{ $t("settings.perm.share") }}
</p>
</div>
</template>
<script>
import { enableExec } from '@/utils/constants'
import { enableExec } from "@/utils/constants";
export default {
name: 'permissions',
props: ['perm'],
name: "permissions",
props: ["perm"],
computed: {
admin: {
get () {
return this.perm.admin
get() {
return this.perm.admin;
},
set (value) {
set(value) {
if (value) {
for (const key in this.perm) {
this.perm[key] = true
this.perm[key] = true;
}
}
this.perm.admin = value
}
this.perm.admin = value;
},
},
isExecEnabled: () => enableExec
}
}
isExecEnabled: () => enableExec,
},
};
</script>

View File

@@ -1,57 +1,63 @@
<template>
<form class="rules small">
<div v-for="(rule, index) in rules" :key="index">
<input type="checkbox" v-model="rule.regex"><label>Regex</label>
<input type="checkbox" v-model="rule.allow"><label>Allow</label>
<input type="checkbox" v-model="rule.regex" /><label>Regex</label>
<input type="checkbox" v-model="rule.allow" /><label>Allow</label>
<input
@keypress.enter.prevent
type="text"
v-if="rule.regex"
v-model="rule.regexp.raw"
:placeholder="$t('settings.insertRegex')" />
:placeholder="$t('settings.insertRegex')"
/>
<input
@keypress.enter.prevent
type="text"
v-else
v-model="rule.path"
:placeholder="$t('settings.insertPath')" />
:placeholder="$t('settings.insertPath')"
/>
<button class="button button--red" @click="remove($event, index)">-</button>
<button class="button button--red" @click="remove($event, index)">
-
</button>
</div>
<div>
<button class="button" @click="create" default="false">{{ $t('buttons.new') }}</button>
<button class="button" @click="create" default="false">
{{ $t("buttons.new") }}
</button>
</div>
</form>
</template>
<script>
export default {
name: 'rules-textarea',
props: ['rules'],
name: "rules-textarea",
props: ["rules"],
methods: {
remove (event, index) {
event.preventDefault()
let rules = [ ...this.rules ]
rules.splice(index, 1)
this.$emit('update:rules', [ ...rules ])
remove(event, index) {
event.preventDefault();
let rules = [...this.rules];
rules.splice(index, 1);
this.$emit("update:rules", [...rules]);
},
create (event) {
event.preventDefault()
create(event) {
event.preventDefault();
this.$emit('update:rules', [
this.$emit("update:rules", [
...this.rules,
{
allow: true,
path: '',
path: "",
regex: false,
regexp: {
raw: ''
}
}
])
}
}
}
raw: "",
},
},
]);
},
},
};
</script>

View File

@@ -1,18 +1,18 @@
<template>
<select v-on:change="change" :value="theme">
<option value="">{{ $t('settings.themes.light') }}</option>
<option value="dark">{{ $t('settings.themes.dark') }}</option>
<option value="">{{ $t("settings.themes.light") }}</option>
<option value="dark">{{ $t("settings.themes.dark") }}</option>
</select>
</template>
<script>
export default {
name: 'themes',
props: [ 'theme' ],
name: "themes",
props: ["theme"],
methods: {
change (event) {
this.$emit('update:theme', event.target.value)
}
}
}
</script>
change(event) {
this.$emit("update:theme", event.target.value);
},
},
};
</script>

View File

@@ -1,67 +1,92 @@
<template>
<div>
<p v-if="!isDefault">
<label for="username">{{ $t('settings.username') }}</label>
<input class="input input--block" type="text" v-model="user.username" id="username">
<label for="username">{{ $t("settings.username") }}</label>
<input
class="input input--block"
type="text"
v-model="user.username"
id="username"
/>
</p>
<p v-if="!isDefault">
<label for="password">{{ $t('settings.password') }}</label>
<input class="input input--block" type="password" :placeholder="passwordPlaceholder" v-model="user.password" id="password">
<label for="password">{{ $t("settings.password") }}</label>
<input
class="input input--block"
type="password"
:placeholder="passwordPlaceholder"
v-model="user.password"
id="password"
/>
</p>
<p>
<label for="scope">{{ $t('settings.scope') }}</label>
<input class="input input--block" type="text" v-model="user.scope" id="scope">
<label for="scope">{{ $t("settings.scope") }}</label>
<input
class="input input--block"
type="text"
v-model="user.scope"
id="scope"
/>
</p>
<p>
<label for="locale">{{ $t('settings.language') }}</label>
<languages class="input input--block" id="locale" :locale.sync="user.locale"></languages>
<label for="locale">{{ $t("settings.language") }}</label>
<languages
class="input input--block"
id="locale"
:locale.sync="user.locale"
></languages>
</p>
<p v-if="!isDefault">
<input type="checkbox" :disabled="user.perm.admin" v-model="user.lockPassword"> {{ $t('settings.lockPassword') }}
<input
type="checkbox"
:disabled="user.perm.admin"
v-model="user.lockPassword"
/>
{{ $t("settings.lockPassword") }}
</p>
<permissions :perm.sync="user.perm" />
<commands v-if="isExecEnabled" :commands.sync="user.commands" />
<div v-if="!isDefault">
<h3>{{ $t('settings.rules') }}</h3>
<p class="small">{{ $t('settings.rulesHelp') }}</p>
<h3>{{ $t("settings.rules") }}</h3>
<p class="small">{{ $t("settings.rulesHelp") }}</p>
<rules :rules.sync="user.rules" />
</div>
</div>
</template>
<script>
import Languages from './Languages'
import Rules from './Rules'
import Permissions from './Permissions'
import Commands from './Commands'
import { enableExec } from '@/utils/constants'
import Languages from "./Languages";
import Rules from "./Rules";
import Permissions from "./Permissions";
import Commands from "./Commands";
import { enableExec } from "@/utils/constants";
export default {
name: 'user',
name: "user",
components: {
Permissions,
Languages,
Rules,
Commands
Commands,
},
props: [ 'user', 'isNew', 'isDefault' ],
props: ["user", "isNew", "isDefault"],
computed: {
passwordPlaceholder () {
return this.isNew ? '' : this.$t('settings.avoidChanges')
passwordPlaceholder() {
return this.isNew ? "" : this.$t("settings.avoidChanges");
},
isExecEnabled: () => enableExec
isExecEnabled: () => enableExec,
},
watch: {
'user.perm.admin': function () {
if (!this.user.perm.admin) return
this.user.lockPassword = false
}
}
}
"user.perm.admin": function () {
if (!this.user.perm.admin) return;
this.user.lockPassword = false;
},
},
};
</script>

View File

@@ -34,7 +34,7 @@
}
.share__box__info {
flex: 1 1 auto;
flex: 1 1 18em;
}
.share__box__element {
@@ -43,6 +43,15 @@
word-break: break-all;
}
.share__box__element .button {
display: inline-block;
}
.share__box__element .button i {
display: block;
margin-bottom: 4px;
}
.share__box__items {
text-align: left;
flex: 10 0 25em;

View File

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

View File

@@ -25,6 +25,7 @@
transition: .1s ease background, .1s ease opacity;
align-items: center;
cursor: pointer;
user-select: none;
}
#listing .item div:last-of-type {
@@ -55,6 +56,7 @@
#listing .item img {
width: 4em;
height: 4em;
object-fit: cover;
margin-right: 0.1em;
vertical-align: bottom;
}

View File

@@ -17,6 +17,48 @@
color: var(--blue);
}
main .spinner {
display: block;
text-align: center;
line-height: 0;
padding: 1em 0;
}
main .spinner > div {
width: .8em;
height: .8em;
margin: 0 .1em;
font-size: 1em;
background-color: rgba(0, 0, 0, 0.3);
border-radius: 100%;
display: inline-block;
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
}
main .spinner .bounce1 {
animation-delay: -0.32s;
}
main .spinner .bounce2 {
animation-delay: -0.16s;
}
.delayed {
animation: delayed linear 100ms;
}
@keyframes delayed {
0% {
opacity: 0;
}
99% {
opacity: 0;
}
100% {
opacity: 1;
}
}
/* * * * * * * * * * * * * * * *
* ACTION *
* * * * * * * * * * * * * * * */
@@ -109,6 +151,7 @@
#previewer {
background-color: rgba(0, 0, 0, 0.9);
padding-top: 4em;
position: fixed;
top: 0;
left: 0;
@@ -142,7 +185,6 @@
}
#previewer .preview {
margin-top: 4em;
text-align: center;
height: calc(100vh - 4em);
}
@@ -163,6 +205,34 @@
height: 100%;
}
#previewer .preview .info {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 1.5em;
color: #fff;
}
#previewer .preview .info .title {
margin-bottom: 1em;
}
#previewer .preview .info .title i {
display: block;
margin-bottom: .1em;
font-size: 4em;
}
#previewer .preview .info .button {
display: inline-block;
}
#previewer .preview .info .button:hover {
background-color: rgba(255, 255, 255, 0.2)
}
#previewer .preview .info .button i {
display: block;
margin-bottom: 4px;
font-size: 1.3em;
}
#previewer .pdf {
width: 100%;
height: 100%;
@@ -204,12 +274,26 @@
right: 0.5em;
}
#previewer .spinner {
text-align: center;
position: fixed;
top: calc(50% + 1.85em);
left: 50%;
transform: translate(-50%, -50%);
}
#previewer .spinner > div {
width: 18px;
height: 18px;
background-color: white;
}
/* EDITOR */
#editor-container {
background-color: #fafafa;
position: fixed;
margin-top: 4em;
padding-top: 4em;
top: 0;
left: 0;
width: 100%;
@@ -217,11 +301,6 @@
overflow: hidden;
}
#previewer .loading {
height: 100%;
width: 100%;
}
#editor-container #editor {
height: calc(100vh - 8.4em);
}
@@ -283,7 +362,6 @@
@keyframes spin {
100% {
-webkit-transform: rotate(-360deg);
transform: rotate(-360deg);
}
}

View File

@@ -33,7 +33,8 @@
"switchView": "Switch view",
"toggleSidebar": "Toggle sidebar",
"update": "Update",
"upload": "Upload"
"upload": "Upload",
"openFile": "Open file"
},
"download": {
"downloadFile": "Download File",
@@ -43,7 +44,8 @@
"errors": {
"forbidden": "You don't have permissions to access this.",
"internal": "Something really went wrong.",
"notFound": "This location can't be reached."
"notFound": "This location can't be reached.",
"connection": "The server can't be reached."
},
"files": {
"body": "Body",
@@ -61,7 +63,8 @@
"size": "Size",
"sortByLastModified": "Sort by last modified",
"sortByName": "Sort by name",
"sortBySize": "Sort by size"
"sortBySize": "Sort by size",
"noPreview": "Preview is not available for this file."
},
"help": {
"click": "select file or directory",
@@ -214,6 +217,7 @@
"settingsUpdated": "Settings updated!",
"shareDuration": "Share Duration",
"shareManagement": "Share Management",
"shareDeleted": "Share deleted!",
"singleClick": "Use single clicks to open files and directories",
"themes": {
"dark": "Dark",

View File

@@ -1,116 +1,116 @@
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import Vue from "vue";
import VueI18n from "vue-i18n";
import ar from './ar.json'
import de from './de.json'
import en from './en.json'
import es from './es.json'
import fr from './fr.json'
import is from './is.json'
import it from './it.json'
import ja from './ja.json'
import ko from './ko.json'
import nlBE from './nl-be.json'
import pl from './pl.json'
import pt from './pt.json'
import ptBR from './pt-br.json'
import ro from './ro.json'
import ru from './ru.json'
import svSE from './sv-se.json'
import zhCN from './zh-cn.json'
import zhTW from './zh-tw.json'
import ar from "./ar.json";
import de from "./de.json";
import en from "./en.json";
import es from "./es.json";
import fr from "./fr.json";
import is from "./is.json";
import it from "./it.json";
import ja from "./ja.json";
import ko from "./ko.json";
import nlBE from "./nl-be.json";
import pl from "./pl.json";
import pt from "./pt.json";
import ptBR from "./pt-br.json";
import ro from "./ro.json";
import ru from "./ru.json";
import svSE from "./sv-se.json";
import zhCN from "./zh-cn.json";
import zhTW from "./zh-tw.json";
Vue.use(VueI18n)
Vue.use(VueI18n);
export function detectLocale () {
let locale = (navigator.language || navigator.browserLangugae).toLowerCase()
export function detectLocale() {
let locale = (navigator.language || navigator.browserLangugae).toLowerCase();
switch (true) {
case /^ar.*/i.test(locale):
locale = 'ar'
break
locale = "ar";
break;
case /^es.*/i.test(locale):
locale = 'es'
break
locale = "es";
break;
case /^en.*/i.test(locale):
locale = 'en'
break
locale = "en";
break;
case /^it.*/i.test(locale):
locale = 'it'
break
locale = "it";
break;
case /^fr.*/i.test(locale):
locale = 'fr'
break
locale = "fr";
break;
case /^pt.*/i.test(locale):
locale = 'pt'
break
locale = "pt";
break;
case /^pt-BR.*/i.test(locale):
locale = 'pt-br'
break
locale = "pt-br";
break;
case /^ja.*/i.test(locale):
locale = 'ja'
break
locale = "ja";
break;
case /^zh-CN/i.test(locale):
locale = 'zh-cn'
break
locale = "zh-cn";
break;
case /^zh-TW/i.test(locale):
locale = 'zh-tw'
break
locale = "zh-tw";
break;
case /^zh.*/i.test(locale):
locale = 'zh-cn'
break
locale = "zh-cn";
break;
case /^de.*/i.test(locale):
locale = 'de'
break
locale = "de";
break;
case /^ru.*/i.test(locale):
locale = 'ru'
break
locale = "ru";
break;
case /^pl.*/i.test(locale):
locale = 'pl'
break
locale = "pl";
break;
case /^ko.*/i.test(locale):
locale = 'ko'
break
locale = "ko";
break;
default:
locale = 'en'
locale = "en";
}
return locale
return locale;
}
const removeEmpty = (obj) =>
Object.keys(obj)
.filter((k) => obj[k] !== null && obj[k] !== undefined && obj[k] !== '') // Remove undef. and null and empty.string.
.reduce(
(newObj, k) =>
typeof obj[k] === 'object'
? Object.assign(newObj, { [k]: removeEmpty(obj[k]) }) // Recurse.
: Object.assign(newObj, { [k]: obj[k] }), // Copy value.
{},
);
Object.keys(obj)
.filter((k) => obj[k] !== null && obj[k] !== undefined && obj[k] !== "") // Remove undef. and null and empty.string.
.reduce(
(newObj, k) =>
typeof obj[k] === "object"
? Object.assign(newObj, { [k]: removeEmpty(obj[k]) }) // Recurse.
: Object.assign(newObj, { [k]: obj[k] }), // Copy value.
{}
);
const i18n = new VueI18n({
locale: detectLocale(),
fallbackLocale: 'en',
fallbackLocale: "en",
messages: {
'ar': removeEmpty(ar),
'de': removeEmpty(de),
'en': en,
'es': removeEmpty(es),
'fr': removeEmpty(fr),
'is': removeEmpty(is),
'it': removeEmpty(it),
'ja': removeEmpty(ja),
'ko': removeEmpty(ko),
'nl-be': removeEmpty(nlBE),
'pl': removeEmpty(pl),
'pt-br': removeEmpty(ptBR),
'pt': removeEmpty(pt),
'ru': removeEmpty(ru),
'ro': removeEmpty(ro),
'sv-se': removeEmpty(svSE),
'zh-cn': removeEmpty(zhCN),
'zh-tw': removeEmpty(zhTW)
}
})
ar: removeEmpty(ar),
de: removeEmpty(de),
en: en,
es: removeEmpty(es),
fr: removeEmpty(fr),
is: removeEmpty(is),
it: removeEmpty(it),
ja: removeEmpty(ja),
ko: removeEmpty(ko),
"nl-be": removeEmpty(nlBE),
pl: removeEmpty(pl),
"pt-br": removeEmpty(ptBR),
pt: removeEmpty(pt),
ru: removeEmpty(ru),
ro: removeEmpty(ro),
"sv-se": removeEmpty(svSE),
"zh-cn": removeEmpty(zhCN),
"zh-tw": removeEmpty(zhTW),
},
});
export default i18n
export default i18n;

View File

@@ -1,43 +1,51 @@
import { sync } from 'vuex-router-sync'
import store from '@/store'
import router from '@/router'
import i18n from '@/i18n'
import Vue from '@/utils/vue'
import { recaptcha, loginPage } from '@/utils/constants'
import { login, validateLogin } from '@/utils/auth'
import App from '@/App'
import "whatwg-fetch";
import cssVars from "css-vars-ponyfill";
import { sync } from "vuex-router-sync";
import store from "@/store";
import router from "@/router";
import i18n from "@/i18n";
import Vue from "@/utils/vue";
import { recaptcha, loginPage } from "@/utils/constants";
import { login, validateLogin } from "@/utils/auth";
import App from "@/App";
sync(store, router)
cssVars();
async function start () {
if (loginPage) {
await validateLogin()
} else {
await login('', '', '')
sync(store, router);
async function start() {
try {
if (loginPage) {
await validateLogin();
} else {
await login("", "", "");
}
} catch (e) {
console.log(e);
}
if (recaptcha) {
await new Promise (resolve => {
await new Promise((resolve) => {
const check = () => {
if (typeof window.grecaptcha === 'undefined') {
setTimeout(check, 100)
if (typeof window.grecaptcha === "undefined") {
setTimeout(check, 100);
} else {
resolve()
resolve();
}
}
};
check()
})
check();
});
}
new Vue({
el: '#app',
el: "#app",
store,
router,
i18n,
template: '<App/>',
components: { App }
})
template: "<App/>",
components: { App },
});
}
start()
start();

View File

@@ -1,166 +1,166 @@
import Vue from 'vue'
import Router from 'vue-router'
import Login from '@/views/Login'
import Layout from '@/views/Layout'
import Files from '@/views/Files'
import Share from '@/views/Share'
import Users from '@/views/settings/Users'
import User from '@/views/settings/User'
import Settings from '@/views/Settings'
import GlobalSettings from '@/views/settings/Global'
import ProfileSettings from '@/views/settings/Profile'
import Shares from '@/views/settings/Shares'
import Errors from '@/views/Errors'
import store from '@/store'
import { baseURL } from '@/utils/constants'
import Vue from "vue";
import Router from "vue-router";
import Login from "@/views/Login";
import Layout from "@/views/Layout";
import Files from "@/views/Files";
import Share from "@/views/Share";
import Users from "@/views/settings/Users";
import User from "@/views/settings/User";
import Settings from "@/views/Settings";
import GlobalSettings from "@/views/settings/Global";
import ProfileSettings from "@/views/settings/Profile";
import Shares from "@/views/settings/Shares";
import Errors from "@/views/Errors";
import store from "@/store";
import { baseURL } from "@/utils/constants";
Vue.use(Router)
Vue.use(Router);
const router = new Router({
base: baseURL,
mode: 'history',
mode: "history",
routes: [
{
path: '/login',
name: 'Login',
path: "/login",
name: "Login",
component: Login,
beforeEnter: (to, from, next) => {
if (store.getters.isLogged) {
return next({ path: '/files' })
return next({ path: "/files" });
}
document.title = 'Login'
next()
}
document.title = "Login";
next();
},
},
{
path: '/*',
path: "/*",
component: Layout,
children: [
{
path: '/share/*',
name: 'Share',
component: Share
path: "/share/*",
name: "Share",
component: Share,
},
{
path: '/files/*',
name: 'Files',
path: "/files/*",
name: "Files",
component: Files,
meta: {
requiresAuth: true
}
requiresAuth: true,
},
},
{
path: '/settings',
name: 'Settings',
path: "/settings",
name: "Settings",
component: Settings,
redirect: {
path: '/settings/profile'
path: "/settings/profile",
},
meta: {
requiresAuth: true
requiresAuth: true,
},
children: [
{
path: '/settings/profile',
name: 'Profile Settings',
component: ProfileSettings
path: "/settings/profile",
name: "Profile Settings",
component: ProfileSettings,
},
{
path: '/settings/shares',
name: 'Shares',
component: Shares
path: "/settings/shares",
name: "Shares",
component: Shares,
},
{
path: '/settings/global',
name: 'Global Settings',
path: "/settings/global",
name: "Global Settings",
component: GlobalSettings,
meta: {
requiresAdmin: true
}
requiresAdmin: true,
},
},
{
path: '/settings/users',
name: 'Users',
path: "/settings/users",
name: "Users",
component: Users,
meta: {
requiresAdmin: true
}
requiresAdmin: true,
},
},
{
path: '/settings/users/*',
name: 'User',
path: "/settings/users/*",
name: "User",
component: User,
meta: {
requiresAdmin: true
}
}
]
requiresAdmin: true,
},
},
],
},
{
path: '/403',
name: 'Forbidden',
path: "/403",
name: "Forbidden",
component: Errors,
props: {
errorCode: 403,
showHeader: true
}
showHeader: true,
},
},
{
path: '/404',
name: 'Not Found',
path: "/404",
name: "Not Found",
component: Errors,
props: {
errorCode: 404,
showHeader: true
}
showHeader: true,
},
},
{
path: '/500',
name: 'Internal Server Error',
path: "/500",
name: "Internal Server Error",
component: Errors,
props: {
errorCode: 500,
showHeader: true
}
showHeader: true,
},
},
{
path: '/files',
path: "/files",
redirect: {
path: '/files/'
}
path: "/files/",
},
},
{
path: '/*',
redirect: to => `/files${to.path}`
}
]
}
]
})
path: "/*",
redirect: (to) => `/files${to.path}`,
},
],
},
],
});
router.beforeEach((to, from, next) => {
document.title = to.name
document.title = to.name;
if (to.matched.some(record => record.meta.requiresAuth)) {
if (to.matched.some((record) => record.meta.requiresAuth)) {
if (!store.getters.isLogged) {
next({
path: '/login',
query: { redirect: to.fullPath }
})
path: "/login",
query: { redirect: to.fullPath },
});
return
return;
}
if (to.matched.some(record => record.meta.requiresAdmin)) {
if (to.matched.some((record) => record.meta.requiresAdmin)) {
if (!store.state.user.perm.admin) {
next({ path: '/403' })
return
next({ path: "/403" });
return;
}
}
}
next()
})
next();
});
export default router
export default router;

View File

@@ -1,16 +1,16 @@
const getters = {
isLogged: state => state.user !== null,
isFiles: state => !state.loading && state.route.name === 'Files',
isLogged: (state) => state.user !== null,
isFiles: (state) => !state.loading && state.route.name === "Files",
isListing: (state, getters) => getters.isFiles && state.req.isDir,
selectedCount: state => state.selected.length,
progress : state => {
selectedCount: (state) => state.selected.length,
progress: (state) => {
if (state.upload.progress.length == 0) {
return 0;
}
let sum = state.upload.progress.reduce((acc, val) => acc + val)
return Math.ceil(sum / state.upload.size * 100);
}
}
let sum = state.upload.progress.reduce((acc, val) => acc + val);
return Math.ceil((sum / state.upload.size) * 100);
},
};
export default getters
export default getters;

View File

@@ -1,20 +1,20 @@
import Vue from 'vue'
import Vuex from 'vuex'
import mutations from './mutations'
import getters from './getters'
import upload from './modules/upload'
import Vue from "vue";
import Vuex from "vuex";
import mutations from "./mutations";
import getters from "./getters";
import upload from "./modules/upload";
Vue.use(Vuex)
Vue.use(Vuex);
const state = {
user: null,
req: {},
oldReq: {},
clipboard: {
key: '',
items: []
key: "",
items: [],
},
jwt: '',
jwt: "",
progress: 0,
loading: false,
reload: false,
@@ -22,13 +22,13 @@ const state = {
multiple: false,
show: null,
showShell: false,
showConfirm: null
}
showConfirm: null,
};
export default new Vuex.Store({
strict: true,
state,
getters,
mutations,
modules: { upload }
})
modules: { upload },
});

View File

@@ -1,7 +1,7 @@
import Vue from 'vue'
import { files as api } from '@/api'
import throttle from 'lodash.throttle'
import buttons from '@/utils/buttons'
import Vue from "vue";
import { files as api } from "@/api";
import throttle from "lodash.throttle";
import buttons from "@/utils/buttons";
const UPLOADS_LIMIT = 5;
@@ -10,93 +10,100 @@ const state = {
size: 0,
progress: [],
queue: [],
uploads: {}
}
uploads: {},
};
const mutations = {
setProgress(state, { id, loaded }) {
Vue.set(state.progress, id, loaded)
Vue.set(state.progress, id, loaded);
},
reset: (state) => {
state.id = 0
state.size = 0
state.progress = []
state.id = 0;
state.size = 0;
state.progress = [];
},
addJob: (state, item) => {
state.queue.push(item)
state.size += item.file.size
state.id++
state.queue.push(item);
state.size += item.file.size;
state.id++;
},
moveJob(state) {
const item = state.queue[0]
state.queue.shift()
Vue.set(state.uploads, item.id, item)
const item = state.queue[0];
state.queue.shift();
Vue.set(state.uploads, item.id, item);
},
removeJob(state, id) {
delete state.uploads[id]
}
}
delete state.uploads[id];
},
};
const beforeUnload = (event) => {
event.preventDefault()
event.returnValue = ''
}
event.preventDefault();
event.returnValue = "";
};
const actions = {
upload: (context, item) => {
let uploadsCount = Object.keys(context.state.uploads).length;
let isQueueEmpty = context.state.queue.length == 0
let isUploadsEmpty = uploadsCount == 0
let isQueueEmpty = context.state.queue.length == 0;
let isUploadsEmpty = uploadsCount == 0;
if (isQueueEmpty && isUploadsEmpty) {
window.addEventListener('beforeunload', beforeUnload)
buttons.loading('upload')
window.addEventListener("beforeunload", beforeUnload);
buttons.loading("upload");
}
context.commit('addJob', item)
context.dispatch('processUploads')
context.commit("addJob", item);
context.dispatch("processUploads");
},
finishUpload: (context, item) => {
context.commit('setProgress', { id: item.id, loaded: item.file.size })
context.commit('removeJob', item.id)
context.dispatch('processUploads')
context.commit("setProgress", { id: item.id, loaded: item.file.size });
context.commit("removeJob", item.id);
context.dispatch("processUploads");
},
processUploads: async (context) => {
let uploadsCount = Object.keys(context.state.uploads).length;
let isBellowLimit = uploadsCount < UPLOADS_LIMIT
let isQueueEmpty = context.state.queue.length == 0
let isUploadsEmpty = uploadsCount == 0
let isBellowLimit = uploadsCount < UPLOADS_LIMIT;
let isQueueEmpty = context.state.queue.length == 0;
let isUploadsEmpty = uploadsCount == 0;
let isFinished = isQueueEmpty && isUploadsEmpty
let canProcess = isBellowLimit && !isQueueEmpty
let isFinished = isQueueEmpty && isUploadsEmpty;
let canProcess = isBellowLimit && !isQueueEmpty;
if (isFinished) {
window.removeEventListener('beforeunload', beforeUnload)
buttons.success('upload')
context.commit('reset')
context.commit('setReload', true, { root: true })
window.removeEventListener("beforeunload", beforeUnload);
buttons.success("upload");
context.commit("reset");
context.commit("setReload", true, { root: true });
}
if (canProcess) {
const item = context.state.queue[0];
context.commit('moveJob')
context.commit("moveJob");
if (item.file.isDir) {
await api.post(item.path).catch(Vue.prototype.$showError)
await api.post(item.path).catch(Vue.prototype.$showError);
} else {
let onUpload = throttle(
(event) => context.commit('setProgress', { id: item.id, loaded: event.loaded }),
100, { leading: true, trailing: false }
)
(event) =>
context.commit("setProgress", {
id: item.id,
loaded: event.loaded,
}),
100,
{ leading: true, trailing: false }
);
await api.post(item.path, item.file, item.overwrite, onUpload).catch(Vue.prototype.$showError)
await api
.post(item.path, item.file, item.overwrite, onUpload)
.catch(Vue.prototype.$showError);
}
context.dispatch('finishUpload', item)
context.dispatch("finishUpload", item);
}
}
}
},
};
export default { state, mutations, actions, namespaced: true }
export default { state, mutations, actions, namespaced: true };

View File

@@ -1,83 +1,87 @@
import * as i18n from '@/i18n'
import moment from 'moment'
import * as i18n from "@/i18n";
import moment from "moment";
const mutations = {
closeHovers: state => {
state.show = null
state.showConfirm = null
closeHovers: (state) => {
state.show = null;
state.showConfirm = null;
},
toggleShell: (state) => {
state.showShell = !state.showShell
state.showShell = !state.showShell;
},
showHover: (state, value) => {
if (typeof value !== 'object') {
state.show = value
return
if (typeof value !== "object") {
state.show = value;
return;
}
state.show = value.prompt
state.showConfirm = value.confirm
state.show = value.prompt;
state.showConfirm = value.confirm;
},
showError: (state) => {
state.show = 'error'
state.show = "error";
},
showSuccess: (state) => {
state.show = 'success'
state.show = "success";
},
setLoading: (state, value) => {
state.loading = value;
},
setReload: (state, value) => {
state.reload = value;
},
setLoading: (state, value) => { state.loading = value },
setReload: (state, value) => { state.reload = value },
setUser: (state, value) => {
if (value === null) {
state.user = null
return
state.user = null;
return;
}
let locale = value.locale
let locale = value.locale;
if (locale === '') {
locale = i18n.detectLocale()
if (locale === "") {
locale = i18n.detectLocale();
}
moment.locale(locale)
i18n.default.locale = locale
state.user = value
moment.locale(locale);
i18n.default.locale = locale;
state.user = value;
},
setJWT: (state, value) => (state.jwt = value),
multiple: (state, value) => (state.multiple = value),
addSelected: (state, value) => (state.selected.push(value)),
addSelected: (state, value) => state.selected.push(value),
removeSelected: (state, value) => {
let i = state.selected.indexOf(value)
if (i === -1) return
state.selected.splice(i, 1)
let i = state.selected.indexOf(value);
if (i === -1) return;
state.selected.splice(i, 1);
},
resetSelected: (state) => {
state.selected = []
state.selected = [];
},
updateUser: (state, value) => {
if (typeof value !== 'object') return
if (typeof value !== "object") return;
for (let field in value) {
if (field === 'locale') {
moment.locale(value[field])
i18n.default.locale = value[field]
if (field === "locale") {
moment.locale(value[field]);
i18n.default.locale = value[field];
}
state.user[field] = value[field]
state.user[field] = value[field];
}
},
updateRequest: (state, value) => {
state.oldReq = state.req
state.req = value
state.oldReq = state.req;
state.req = value;
},
updateClipboard: (state, value) => {
state.clipboard.key = value.key
state.clipboard.items = value.items
state.clipboard.path = value.path
state.clipboard.key = value.key;
state.clipboard.items = value.items;
state.clipboard.path = value.path;
},
resetClipboard: (state) => {
state.clipboard.key = ''
state.clipboard.items = []
}
}
state.clipboard.key = "";
state.clipboard.items = [];
},
};
export default mutations
export default mutations;

View File

@@ -1,88 +1,92 @@
import store from '@/store'
import router from '@/router'
import { Base64 } from 'js-base64'
import { baseURL } from '@/utils/constants'
import store from "@/store";
import router from "@/router";
import { Base64 } from "js-base64";
import { baseURL } from "@/utils/constants";
export function parseToken (token) {
const parts = token.split('.')
export function parseToken(token) {
const parts = token.split(".");
if (parts.length !== 3) {
throw new Error('token malformed')
throw new Error("token malformed");
}
const data = JSON.parse(Base64.decode(parts[1]))
const data = JSON.parse(Base64.decode(parts[1]));
localStorage.setItem('jwt', token)
store.commit('setJWT', token)
store.commit('setUser', data.user)
document.cookie = `auth=${token}; path=/`;
localStorage.setItem("jwt", token);
store.commit("setJWT", token);
store.commit("setUser", data.user);
}
export async function validateLogin () {
export async function validateLogin() {
try {
if (localStorage.getItem('jwt')) {
await renew(localStorage.getItem('jwt'))
if (localStorage.getItem("jwt")) {
await renew(localStorage.getItem("jwt"));
}
} catch (_) {
console.warn('Invalid JWT token in storage') // eslint-disable-line
}
}
export async function login (username, password, recaptcha) {
const data = { username, password, recaptcha }
export async function login(username, password, recaptcha) {
const data = { username, password, recaptcha };
const res = await fetch(`${baseURL}/api/login`, {
method: 'POST',
method: "POST",
headers: {
'Content-Type': 'application/json'
"Content-Type": "application/json",
},
body: JSON.stringify(data)
})
body: JSON.stringify(data),
});
const body = await res.text()
const body = await res.text();
if (res.status === 200) {
parseToken(body)
parseToken(body);
} else {
throw new Error(body)
throw new Error(body);
}
}
export async function renew (jwt) {
export async function renew(jwt) {
const res = await fetch(`${baseURL}/api/renew`, {
method: 'POST',
method: "POST",
headers: {
'X-Auth': jwt,
}
})
"X-Auth": jwt,
},
});
const body = await res.text()
const body = await res.text();
if (res.status === 200) {
parseToken(body)
parseToken(body);
} else {
throw new Error(body)
throw new Error(body);
}
}
export async function signup (username, password) {
const data = { username, password }
export async function signup(username, password) {
const data = { username, password };
const res = await fetch(`${baseURL}/api/signup`, {
method: 'POST',
method: "POST",
headers: {
'Content-Type': 'application/json'
"Content-Type": "application/json",
},
body: JSON.stringify(data)
})
body: JSON.stringify(data),
});
if (res.status !== 200) {
throw new Error(res.status)
throw new Error(res.status);
}
}
export function logout () {
store.commit('setJWT', '')
store.commit('setUser', null)
localStorage.setItem('jwt', null)
router.push({path: '/login'})
export function logout() {
document.cookie = "auth=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/";
store.commit("setJWT", "");
store.commit("setUser", null);
localStorage.setItem("jwt", null);
router.push({ path: "/login" });
}

View File

@@ -1,70 +1,70 @@
function loading (button) {
let el = document.querySelector(`#${button}-button > i`)
function loading(button) {
let el = document.querySelector(`#${button}-button > i`);
if (el === undefined || el === null) {
console.log('Error getting button ' + button) // eslint-disable-line
return
return;
}
if (el.innerHTML == 'autorenew' || el.innerHTML == 'done') {
return
if (el.innerHTML == "autorenew" || el.innerHTML == "done") {
return;
}
el.dataset.icon = el.innerHTML
el.style.opacity = 0
el.dataset.icon = el.innerHTML;
el.style.opacity = 0;
setTimeout(() => {
el.classList.add('spin')
el.innerHTML = 'autorenew'
el.style.opacity = 1
}, 100)
el.classList.add("spin");
el.innerHTML = "autorenew";
el.style.opacity = 1;
}, 100);
}
function done (button) {
let el = document.querySelector(`#${button}-button > i`)
function done(button) {
let el = document.querySelector(`#${button}-button > i`);
if (el === undefined || el === null) {
console.log('Error getting button ' + button) // eslint-disable-line
return
return;
}
el.style.opacity = 0
el.style.opacity = 0;
setTimeout(() => {
el.classList.remove('spin')
el.innerHTML = el.dataset.icon
el.style.opacity = 1
}, 100)
el.classList.remove("spin");
el.innerHTML = el.dataset.icon;
el.style.opacity = 1;
}, 100);
}
function success (button) {
let el = document.querySelector(`#${button}-button > i`)
function success(button) {
let el = document.querySelector(`#${button}-button > i`);
if (el === undefined || el === null) {
console.log('Error getting button ' + button) // eslint-disable-line
return
return;
}
el.style.opacity = 0
el.style.opacity = 0;
setTimeout(() => {
el.classList.remove('spin')
el.innerHTML = 'done'
el.style.opacity = 1
el.classList.remove("spin");
el.innerHTML = "done";
el.style.opacity = 1;
setTimeout(() => {
el.style.opacity = 0
el.style.opacity = 0;
setTimeout(() => {
el.innerHTML = el.dataset.icon
el.style.opacity = 1
}, 100)
}, 500)
}, 100)
el.innerHTML = el.dataset.icon;
el.style.opacity = 1;
}, 100);
}, 500);
}, 100);
}
export default {
loading,
done,
success
}
success,
};

View File

@@ -1,19 +1,19 @@
const name = window.FileBrowser.Name || 'File Browser'
const disableExternal = window.FileBrowser.DisableExternal
const baseURL = window.FileBrowser.BaseURL
const staticURL = window.FileBrowser.StaticURL
const recaptcha = window.FileBrowser.ReCaptcha
const recaptchaKey = window.FileBrowser.ReCaptchaKey
const signup = window.FileBrowser.Signup
const version = window.FileBrowser.Version
const logoURL = `${staticURL}/img/logo.svg`
const noAuth = window.FileBrowser.NoAuth
const authMethod = window.FileBrowser.AuthMethod
const loginPage = window.FileBrowser.LoginPage
const theme = window.FileBrowser.Theme
const enableThumbs = window.FileBrowser.EnableThumbs
const resizePreview = window.FileBrowser.ResizePreview
const enableExec = window.FileBrowser.EnableExec
const name = window.FileBrowser.Name || "File Browser";
const disableExternal = window.FileBrowser.DisableExternal;
const baseURL = window.FileBrowser.BaseURL;
const staticURL = window.FileBrowser.StaticURL;
const recaptcha = window.FileBrowser.ReCaptcha;
const recaptchaKey = window.FileBrowser.ReCaptchaKey;
const signup = window.FileBrowser.Signup;
const version = window.FileBrowser.Version;
const logoURL = `${staticURL}/img/logo.svg`;
const noAuth = window.FileBrowser.NoAuth;
const authMethod = window.FileBrowser.AuthMethod;
const loginPage = window.FileBrowser.LoginPage;
const theme = window.FileBrowser.Theme;
const enableThumbs = window.FileBrowser.EnableThumbs;
const resizePreview = window.FileBrowser.ResizePreview;
const enableExec = window.FileBrowser.EnableExec;
export {
name,
@@ -30,5 +30,5 @@ export {
theme,
enableThumbs,
resizePreview,
enableExec
}
enableExec,
};

View File

@@ -1,4 +1,6 @@
export default function (name) {
let re = new RegExp('(?:(?:^|.*;\\s*)' + name + '\\s*\\=\\s*([^;]*).*$)|^.*$')
return document.cookie.replace(re, '$1')
let re = new RegExp(
"(?:(?:^|.*;\\s*)" + name + "\\s*\\=\\s*([^;]*).*$)|^.*$"
);
return document.cookie.replace(re, "$1");
}

View File

@@ -1,28 +1,28 @@
export default function getRule (rules) {
export default function getRule(rules) {
for (let i = 0; i < rules.length; i++) {
rules[i] = rules[i].toLowerCase()
rules[i] = rules[i].toLowerCase();
}
let result = null
let find = Array.prototype.find
let result = null;
let find = Array.prototype.find;
find.call(document.styleSheets, styleSheet => {
result = find.call(styleSheet.cssRules, cssRule => {
let found = false
find.call(document.styleSheets, (styleSheet) => {
result = find.call(styleSheet.cssRules, (cssRule) => {
let found = false;
if (cssRule instanceof window.CSSStyleRule) {
for (let i = 0; i < rules.length; i++) {
if (cssRule.selectorText.toLowerCase() === rules[i]) {
found = true
found = true;
}
}
}
return found
})
return found;
});
return result != null
})
return result != null;
});
return result
return result;
}

View File

@@ -1,124 +1,127 @@
import store from '@/store'
import url from '@/utils/url'
import store from "@/store";
import url from "@/utils/url";
export function checkConflict(files, items) {
if (typeof items === 'undefined' || items === null) {
items = []
if (typeof items === "undefined" || items === null) {
items = [];
}
let folder_upload = files[0].fullPath !== undefined
let folder_upload = files[0].fullPath !== undefined;
let conflict = false
let conflict = false;
for (let i = 0; i < files.length; i++) {
let file = files[i]
let name = file.name
let file = files[i];
let name = file.name;
if (folder_upload) {
let dirs = file.fullPath.split("/")
let dirs = file.fullPath.split("/");
if (dirs.length > 1) {
name = dirs[0]
name = dirs[0];
}
}
let res = items.findIndex(function hasConflict(element) {
return (element.name === this)
}, name)
return element.name === this;
}, name);
if (res >= 0) {
conflict = true
break
conflict = true;
break;
}
}
return conflict
return conflict;
}
export function scanFiles(dt) {
return new Promise((resolve) => {
let reading = 0
const contents = []
let reading = 0;
const contents = [];
if (dt.items !== undefined) {
for (let item of dt.items) {
if (item.kind === "file" && typeof item.webkitGetAsEntry === "function") {
const entry = item.webkitGetAsEntry()
readEntry(entry)
if (
item.kind === "file" &&
typeof item.webkitGetAsEntry === "function"
) {
const entry = item.webkitGetAsEntry();
readEntry(entry);
}
}
} else {
resolve(dt.files)
resolve(dt.files);
}
function readEntry(entry, directory = "") {
if (entry.isFile) {
reading++
entry.file(file => {
reading--
reading++;
entry.file((file) => {
reading--;
file.fullPath = `${directory}${file.name}`
contents.push(file)
file.fullPath = `${directory}${file.name}`;
contents.push(file);
if (reading === 0) {
resolve(contents)
resolve(contents);
}
})
});
} else if (entry.isDirectory) {
const dir = {
isDir: true,
size: 0,
fullPath: `${directory}${entry.name}`
}
fullPath: `${directory}${entry.name}`,
};
contents.push(dir)
contents.push(dir);
readReaderContent(entry.createReader(), `${directory}${entry.name}`)
readReaderContent(entry.createReader(), `${directory}${entry.name}`);
}
}
function readReaderContent(reader, directory) {
reading++
reading++;
reader.readEntries(function (entries) {
reading--
reading--;
if (entries.length > 0) {
for (const entry of entries) {
readEntry(entry, `${directory}/`)
readEntry(entry, `${directory}/`);
}
readReaderContent(reader, `${directory}/`)
readReaderContent(reader, `${directory}/`);
}
if (reading === 0) {
resolve(contents)
resolve(contents);
}
})
});
}
})
});
}
export function handleFiles(files, base, overwrite = false) {
for (let i = 0; i < files.length; i++) {
let id = store.state.upload.id
let path = base
let file = files[i]
let id = store.state.upload.id;
let path = base;
let file = files[i];
if (file.fullPath !== undefined) {
path += url.encodePath(file.fullPath)
path += url.encodePath(file.fullPath);
} else {
path += url.encodeRFC5987ValueChars(file.name)
path += url.encodeRFC5987ValueChars(file.name);
}
if (file.isDir) {
path += '/'
path += "/";
}
const item = {
id,
path,
file,
overwrite
}
overwrite,
};
store.dispatch('upload/upload', item);
store.dispatch("upload/upload", item);
}
}
}

View File

@@ -1,31 +1,36 @@
function removeLastDir (url) {
var arr = url.split('/')
if (arr.pop() === '') {
arr.pop()
function removeLastDir(url) {
var arr = url.split("/");
if (arr.pop() === "") {
arr.pop();
}
return arr.join('/')
return arr.join("/");
}
// this code borrow from mozilla
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#Examples
function encodeRFC5987ValueChars(str) {
return encodeURIComponent(str).
return (
encodeURIComponent(str)
// Note that although RFC3986 reserves "!", RFC5987 does not,
// so we do not need to escape it
replace(/['()]/g, escape). // i.e., %27 %28 %29
replace(/\*/g, '%2A').
// The following are not required for percent-encoding per RFC5987,
// so we can allow for a little better readability over the wire: |`^
replace(/%(?:7C|60|5E)/g, unescape);
.replace(/['()]/g, escape) // i.e., %27 %28 %29
.replace(/\*/g, "%2A")
// The following are not required for percent-encoding per RFC5987,
// so we can allow for a little better readability over the wire: |`^
.replace(/%(?:7C|60|5E)/g, unescape)
);
}
function encodePath(str) {
return str.split('/').map(v => encodeURIComponent(v)).join('/')
return str
.split("/")
.map((v) => encodeURIComponent(v))
.join("/");
}
export default {
encodeRFC5987ValueChars: encodeRFC5987ValueChars,
removeLastDir: removeLastDir,
encodePath: encodePath
}
encodePath: encodePath,
};

View File

@@ -1,58 +1,66 @@
import Vue from 'vue'
import Noty from 'noty'
import VueLazyload from 'vue-lazyload'
import i18n from '@/i18n'
import { disableExternal } from '@/utils/constants'
import Vue from "vue";
import Noty from "noty";
import VueLazyload from "vue-lazyload";
import i18n from "@/i18n";
import { disableExternal } from "@/utils/constants";
Vue.use(VueLazyload)
Vue.use(VueLazyload);
Vue.config.productionTip = true
Vue.config.productionTip = true;
const notyDefault = {
type: 'info',
layout: 'bottomRight',
type: "info",
layout: "bottomRight",
timeout: 1000,
progressBar: true
}
progressBar: true,
};
Vue.prototype.$noty = (opts) => {
new Noty(Object.assign({}, notyDefault, opts)).show()
}
new Noty(Object.assign({}, notyDefault, opts)).show();
};
Vue.prototype.$showSuccess = (message) => {
new Noty(Object.assign({}, notyDefault, {
text: message,
type: 'success'
})).show()
}
new Noty(
Object.assign({}, notyDefault, {
text: message,
type: "success",
})
).show();
};
Vue.prototype.$showError = (error, displayReport = true) => {
let btns = [
Noty.button(i18n.t('buttons.close'), '', function () {
n.close()
})
]
Noty.button(i18n.t("buttons.close"), "", function () {
n.close();
}),
];
if (!disableExternal && displayReport) {
btns.unshift(Noty.button(i18n.t('buttons.reportIssue'), '', function () {
window.open('https://github.com/filebrowser/filebrowser/issues/new/choose')
}))
btns.unshift(
Noty.button(i18n.t("buttons.reportIssue"), "", function () {
window.open(
"https://github.com/filebrowser/filebrowser/issues/new/choose"
);
})
);
}
let n = new Noty(Object.assign({}, notyDefault, {
text: error.message || error,
type: 'error',
timeout: null,
buttons: btns
}))
let n = new Noty(
Object.assign({}, notyDefault, {
text: error.message || error,
type: "error",
timeout: null,
buttons: btns,
})
);
n.show()
}
n.show();
};
Vue.directive('focus', {
Vue.directive("focus", {
inserted: function (el) {
el.focus()
}
})
el.focus();
},
});
export default Vue
export default Vue;

View File

@@ -3,44 +3,51 @@
<header-bar v-if="showHeader" showMenu showLogo />
<h2 class="message">
<i class="material-icons">{{ icon }}</i>
<span>{{ message }}</span>
<i class="material-icons">{{ info.icon }}</i>
<span>{{ $t(info.message) }}</span>
</h2>
</div>
</template>
<script>
import HeaderBar from '@/components/header/HeaderBar'
import HeaderBar from "@/components/header/HeaderBar";
const errors = {
0: {
icon: "cloud_off",
message: "errors.connection",
},
403: {
icon: 'error',
message: 'errors.forbidden'
icon: "error",
message: "errors.forbidden",
},
404: {
icon: 'gps_off',
message: 'errors.notFound'
icon: "gps_off",
message: "errors.notFound",
},
500: {
icon: 'error_outline',
message: 'errors.internal'
}
}
icon: "error_outline",
message: "errors.internal",
},
};
export default {
name: 'errors',
name: "errors",
components: {
HeaderBar
HeaderBar,
},
props: [
'errorCode', 'showHeader'
],
data: function () {
return {
icon: errors[this.errorCode].icon,
message: this.$t(errors[this.errorCode].message)
}
}
}
</script>
props: ["errorCode", "showHeader"],
computed: {
code() {
return this.errorCode === "0" ||
this.errorCode === "404" ||
this.errorCode === "403"
? parseInt(this.errorCode)
: 500;
},
info() {
return errors[this.code];
},
},
};
</script>

View File

@@ -1,144 +1,135 @@
<template>
<div>
<header-bar v-if="error || !req.type" showMenu showLogo />
<header-bar v-if="error || req.type == null" showMenu showLogo />
<breadcrumbs base="/files" />
<errors v-if="error" :errorCode="errorCode" />
<errors v-if="error" :errorCode="error.message" />
<component v-else-if="currentView" :is="currentView"></component>
<div v-else>
<h2 class="message">
<span>{{ $t('files.loading') }}</span>
<h2 class="message delayed">
<div class="spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
<span>{{ $t("files.loading") }}</span>
</h2>
</div>
</div>
</template>
<script>
import { files as api } from '@/api'
import { mapState, mapMutations } from 'vuex'
import { files as api } from "@/api";
import { mapState, mapMutations } from "vuex";
import HeaderBar from '@/components/header/HeaderBar'
import Breadcrumbs from '@/components/Breadcrumbs'
import Errors from '@/views/Errors'
import Preview from '@/views/files/Preview'
import Listing from '@/views/files/Listing'
import HeaderBar from "@/components/header/HeaderBar";
import Breadcrumbs from "@/components/Breadcrumbs";
import Errors from "@/views/Errors";
import Preview from "@/views/files/Preview";
import Listing from "@/views/files/Listing";
function clean (path) {
return path.endsWith('/') ? path.slice(0, -1) : path
function clean(path) {
return path.endsWith("/") ? path.slice(0, -1) : path;
}
export default {
name: 'files',
name: "files",
components: {
HeaderBar,
Breadcrumbs,
Errors,
Preview,
Listing,
Editor: () => import('@/views/files/Editor'),
Editor: () => import("@/views/files/Editor"),
},
data: function () {
return {
error: null,
width: window.innerWidth
}
width: window.innerWidth,
};
},
computed: {
...mapState([
'req',
'reload',
'loading',
'show'
]),
currentView () {
...mapState(["req", "reload", "loading", "show"]),
currentView() {
if (this.req.type == undefined) {
return null
return null;
}
if (this.req.isDir) {
return 'listing'
} else if(this.req.type === 'text' || this.req.type === 'textImmutable') {
return 'editor'
return "listing";
} else if (
this.req.type === "text" ||
this.req.type === "textImmutable"
) {
return "editor";
} else {
return 'preview'
return "preview";
}
},
errorCode() {
return (this.error.message === '404' || this.error.message === '403') ? parseInt(this.error.message) : 500
}
},
created () {
this.fetchData()
created() {
this.fetchData();
},
watch: {
'$route': 'fetchData',
'reload': function (value) {
if (value === true) {
this.fetchData()
}
}
},
mounted () {
window.addEventListener('keydown', this.keyEvent)
},
beforeDestroy () {
window.removeEventListener('keydown', this.keyEvent)
},
destroyed () {
if (this.$store.state.showShell) {
this.$store.commit('toggleShell')
}
this.$store.commit('updateRequest', {})
},
methods: {
...mapMutations([ 'setLoading' ]),
async fetchData () {
// Reset view information.
this.$store.commit('setReload', false)
this.$store.commit('resetSelected')
this.$store.commit('multiple', false)
this.$store.commit('closeHovers')
// Set loading to true and reset the error.
this.setLoading(true)
this.error = null
let url = this.$route.path
if (url === '') url = '/'
if (url[0] !== '/') url = '/' + url
try {
const res = await api.fetch(url)
if (clean(res.path) !== clean(`/${this.$route.params.pathMatch}`)) {
return
}
this.$store.commit('updateRequest', res)
document.title = res.name
} catch (e) {
this.error = e
} finally {
this.setLoading(false)
$route: "fetchData",
reload: function (value) {
if (value === true) {
this.fetchData();
}
},
keyEvent (event) {
if (this.show !== null) {
// Esc!
if (event.keyCode === 27) {
this.$store.commit('closeHovers')
},
mounted() {
window.addEventListener("keydown", this.keyEvent);
},
beforeDestroy() {
window.removeEventListener("keydown", this.keyEvent);
},
destroyed() {
if (this.$store.state.showShell) {
this.$store.commit("toggleShell");
}
this.$store.commit("updateRequest", {});
},
methods: {
...mapMutations(["setLoading"]),
async fetchData() {
// Reset view information.
this.$store.commit("setReload", false);
this.$store.commit("resetSelected");
this.$store.commit("multiple", false);
this.$store.commit("closeHovers");
// Set loading to true and reset the error.
this.setLoading(true);
this.error = null;
let url = this.$route.path;
if (url === "") url = "/";
if (url[0] !== "/") url = "/" + url;
try {
const res = await api.fetch(url);
if (clean(res.path) !== clean(`/${this.$route.params.pathMatch}`)) {
return;
}
return
this.$store.commit("updateRequest", res);
document.title = `${res.name} - ${this.$route.name}`;
} catch (e) {
this.error = e;
} finally {
this.setLoading(false);
}
},
keyEvent(event) {
// F1!
if (event.keyCode === 112) {
event.preventDefault()
this.$store.commit('showHover', 'help')
event.preventDefault();
this.$store.commit("showHover", "help");
}
}
}
}
},
},
};
</script>

View File

@@ -13,30 +13,31 @@
</template>
<script>
import { mapState, mapGetters } from 'vuex'
import Sidebar from '@/components/Sidebar'
import Prompts from '@/components/prompts/Prompts'
import Shell from '@/components/Shell'
import { enableExec } from '@/utils/constants'
import { mapState, mapGetters } from "vuex";
import Sidebar from "@/components/Sidebar";
import Prompts from "@/components/prompts/Prompts";
import Shell from "@/components/Shell";
import { enableExec } from "@/utils/constants";
export default {
name: 'layout',
name: "layout",
components: {
Sidebar,
Prompts,
Shell
Shell,
},
computed: {
...mapGetters([ 'isLogged', 'progress' ]),
...mapState([ 'user' ]),
isExecEnabled: () => enableExec
...mapGetters(["isLogged", "progress"]),
...mapState(["user"]),
isExecEnabled: () => enableExec,
},
watch: {
'$route': function () {
this.$store.commit('resetSelected')
this.$store.commit('multiple', false)
if (this.$store.state.show !== 'success') this.$store.commit('closeHovers')
}
}
}
$route: function () {
this.$store.commit("resetSelected");
this.$store.commit("multiple", false);
if (this.$store.state.show !== "success")
this.$store.commit("closeHovers");
},
},
};
</script>

View File

@@ -1,97 +1,127 @@
<template>
<div id="login" :class="{ recaptcha: recaptcha }">
<form @submit="submit">
<img :src="logoURL" alt="File Browser">
<img :src="logoURL" alt="File Browser" />
<h1>{{ name }}</h1>
<div v-if="error !== ''" class="wrong">{{ error }}</div>
<input class="input input--block" type="text" v-model="username" :placeholder="$t('login.username')">
<input class="input input--block" type="password" v-model="password" :placeholder="$t('login.password')">
<input class="input input--block" v-if="createMode" type="password" v-model="passwordConfirm" :placeholder="$t('login.passwordConfirm')" />
<input
class="input input--block"
type="text"
v-model="username"
:placeholder="$t('login.username')"
/>
<input
class="input input--block"
type="password"
v-model="password"
:placeholder="$t('login.password')"
/>
<input
class="input input--block"
v-if="createMode"
type="password"
v-model="passwordConfirm"
:placeholder="$t('login.passwordConfirm')"
/>
<div v-if="recaptcha" id="recaptcha"></div>
<input class="button button--block" type="submit" :value="createMode ? $t('login.signup') : $t('login.submit')">
<input
class="button button--block"
type="submit"
:value="createMode ? $t('login.signup') : $t('login.submit')"
/>
<p @click="toggleMode" v-if="signup">{{ createMode ? $t('login.loginInstead') : $t('login.createAnAccount') }}</p>
<p @click="toggleMode" v-if="signup">
{{
createMode ? $t("login.loginInstead") : $t("login.createAnAccount")
}}
</p>
</form>
</div>
</template>
<script>
import * as auth from '@/utils/auth'
import { name, logoURL, recaptcha, recaptchaKey, signup } from '@/utils/constants'
import * as auth from "@/utils/auth";
import {
name,
logoURL,
recaptcha,
recaptchaKey,
signup,
} from "@/utils/constants";
export default {
name: 'login',
name: "login",
computed: {
signup: () => signup,
name: () => name,
logoURL: () => logoURL
logoURL: () => logoURL,
},
data: function () {
return {
createMode: false,
error: '',
username: '',
password: '',
error: "",
username: "",
password: "",
recaptcha: recaptcha,
passwordConfirm: ''
}
passwordConfirm: "",
};
},
mounted () {
if (!recaptcha) return
mounted() {
if (!recaptcha) return;
window.grecaptcha.ready(function () {
window.grecaptcha.render('recaptcha', {
sitekey: recaptchaKey
})
})
window.grecaptcha.render("recaptcha", {
sitekey: recaptchaKey,
});
});
},
methods: {
toggleMode () {
this.createMode = !this.createMode
toggleMode() {
this.createMode = !this.createMode;
},
async submit (event) {
event.preventDefault()
event.stopPropagation()
async submit(event) {
event.preventDefault();
event.stopPropagation();
let redirect = this.$route.query.redirect
if (redirect === '' || redirect === undefined || redirect === null) {
redirect = '/files/'
let redirect = this.$route.query.redirect;
if (redirect === "" || redirect === undefined || redirect === null) {
redirect = "/files/";
}
let captcha = ''
let captcha = "";
if (recaptcha) {
captcha = window.grecaptcha.getResponse()
captcha = window.grecaptcha.getResponse();
if (captcha === '') {
this.error = this.$t('login.wrongCredentials')
return
if (captcha === "") {
this.error = this.$t("login.wrongCredentials");
return;
}
}
if (this.createMode) {
if (this.password !== this.passwordConfirm) {
this.error = this.$t('login.passwordsDontMatch')
return
this.error = this.$t("login.passwordsDontMatch");
return;
}
}
try {
if (this.createMode) {
await auth.signup(this.username, this.password)
await auth.signup(this.username, this.password);
}
await auth.login(this.username, this.password, captcha)
this.$router.push({ path: redirect })
await auth.login(this.username, this.password, captcha);
this.$router.push({ path: redirect });
} catch (e) {
if (e.message == 409) {
this.error = this.$t('login.usernameTaken')
this.error = this.$t("login.usernameTaken");
} else {
this.error = this.$t('login.wrongCredentials')
this.error = this.$t("login.wrongCredentials");
}
}
}
}
}
},
},
};
</script>

View File

@@ -5,30 +5,62 @@
<div id="nav">
<div class="wrapper">
<ul>
<router-link to="/settings/profile"><li :class="{ active: $route.path === '/settings/profile' }">{{ $t('settings.profileSettings') }}</li></router-link>
<router-link to="/settings/shares"><li :class="{ active: $route.path === '/settings/shares' }">{{ $t('settings.shareManagement') }}</li></router-link>
<router-link to="/settings/global"><li :class="{ active: $route.path === '/settings/global' }" v-if="user.perm.admin">{{ $t('settings.globalSettings') }}</li></router-link>
<router-link to="/settings/users"><li :class="{ active: $route.path === '/settings/users' || $route.name === 'User' }" v-if="user.perm.admin">{{ $t('settings.userManagement') }}</li></router-link>
<router-link to="/settings/profile"
><li :class="{ active: $route.path === '/settings/profile' }">
{{ $t("settings.profileSettings") }}
</li></router-link
>
<router-link to="/settings/shares" v-if="user.perm.share"
><li :class="{ active: $route.path === '/settings/shares' }">
{{ $t("settings.shareManagement") }}
</li></router-link
>
<router-link to="/settings/global" v-if="user.perm.admin"
><li :class="{ active: $route.path === '/settings/global' }">
{{ $t("settings.globalSettings") }}
</li></router-link
>
<router-link to="/settings/users" v-if="user.perm.admin"
><li
:class="{
active:
$route.path === '/settings/users' || $route.name === 'User',
}"
>
{{ $t("settings.userManagement") }}
</li></router-link
>
</ul>
</div>
</div>
<div v-if="loading">
<h2 class="message delayed">
<div class="spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
<span>{{ $t("files.loading") }}</span>
</h2>
</div>
<router-view></router-view>
</div>
</template>
<script>
import { mapState } from 'vuex'
import { mapState } from "vuex";
import HeaderBar from '@/components/header/HeaderBar'
import HeaderBar from "@/components/header/HeaderBar";
export default {
name: 'settings',
name: "settings",
components: {
HeaderBar
HeaderBar,
},
computed: {
...mapState([ 'user' ])
}
}
...mapState(["user", "loading"]),
},
};
</script>

View File

@@ -3,43 +3,120 @@
<header-bar showMenu showLogo>
<title />
<action v-if="selectedCount" icon="file_download" :label="$t('buttons.download')" @action="download" :counter="selectedCount" />
<action icon="check_circle" :label="$t('buttons.selectMultiple')" @action="toggleMultipleSelection" />
<action
v-if="selectedCount"
icon="file_download"
:label="$t('buttons.download')"
@action="download"
:counter="selectedCount"
/>
<action
icon="check_circle"
:label="$t('buttons.selectMultiple')"
@action="toggleMultipleSelection"
/>
</header-bar>
<breadcrumbs :base="'/share/' + hash" />
<div v-if="!loading">
<div v-if="loading">
<h2 class="message delayed">
<div class="spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
<span>{{ $t("files.loading") }}</span>
</h2>
</div>
<div v-else-if="error">
<div v-if="error.message === '401'">
<div class="card floating" id="password">
<div v-if="attemptedPasswordLogin" class="share__wrong__password">
{{ $t("login.wrongCredentials") }}
</div>
<div class="card-title">
<h2>{{ $t("login.password") }}</h2>
</div>
<div class="card-content">
<input
v-focus
type="password"
:placeholder="$t('login.password')"
v-model="password"
@keyup.enter="fetchData"
/>
</div>
<div class="card-action">
<button
class="button button--flat"
@click="fetchData"
:aria-label="$t('buttons.submit')"
:title="$t('buttons.submit')"
>
{{ $t("buttons.submit") }}
</button>
</div>
</div>
</div>
<errors v-else :errorCode="error.message" />
</div>
<div v-else>
<div class="share">
<div class="share__box share__box__info">
<div class="share__box__header">
{{ req.isDir ? $t('download.downloadFolder') : $t('download.downloadFile') }}
</div>
<div class="share__box__element share__box__center share__box__icon">
<i class="material-icons">{{ icon }}</i>
</div>
<div class="share__box__element">
<strong>{{ $t('prompts.displayName') }}</strong> {{ req.name }}
</div>
<div class="share__box__element">
<strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime }}
</div>
<div class="share__box__element">
<strong>{{ $t('prompts.size') }}:</strong> {{ humanSize }}
</div>
<div class="share__box__element share__box__center">
<a target="_blank" :href="link" class="button button--flat">{{ $t('buttons.download') }}</a>
</div>
<div class="share__box__element share__box__center">
<qrcode-vue :value="link" size="200" level="M"></qrcode-vue>
</div>
<div class="share__box__header">
{{
req.isDir
? $t("download.downloadFolder")
: $t("download.downloadFile")
}}
</div>
<div class="share__box__element share__box__center share__box__icon">
<i class="material-icons">{{ icon }}</i>
</div>
<div class="share__box__element">
<strong>{{ $t("prompts.displayName") }}</strong> {{ req.name }}
</div>
<div class="share__box__element" :title="modTime">
<strong>{{ $t("prompts.lastModified") }}:</strong> {{ humanTime }}
</div>
<div class="share__box__element">
<strong>{{ $t("prompts.size") }}:</strong> {{ humanSize }}
</div>
<div class="share__box__element share__box__center">
<a target="_blank" :href="link" class="button button--flat">
<div>
<i class="material-icons">file_download</i
>{{ $t("buttons.download") }}
</div>
</a>
<a
target="_blank"
:href="inlineLink"
class="button button--flat"
v-if="!req.isDir"
>
<div>
<i class="material-icons">open_in_new</i
>{{ $t("buttons.openFile") }}
</div>
</a>
</div>
<div class="share__box__element share__box__center">
<qrcode-vue :value="fullLink" size="200" level="M"></qrcode-vue>
</div>
</div>
<div v-if="req.isDir && req.items.length > 0" class="share__box share__box__items">
<div
v-if="req.isDir && req.items.length > 0"
class="share__box share__box__items"
>
<div class="share__box__header" v-if="req.isDir">
{{ $t('files.files') }}
{{ $t("files.files") }}
</div>
<div id="listing" class="list">
<item v-for="(item) in req.items.slice(0, this.showLimit)"
<item
v-for="item in req.items.slice(0, this.showLimit)"
:key="base64(item.name)"
v-bind:index="item.index"
v-bind:name="item.name"
@@ -48,202 +125,218 @@
v-bind:modified="item.modified"
v-bind:type="item.type"
v-bind:size="item.size"
readOnly>
readOnly
>
</item>
<div v-if="req.items.length > showLimit" class="item">
<div
v-if="req.items.length > showLimit"
class="item"
@click="showLimit += 100"
>
<div>
<p class="name"> + {{ req.items.length - showLimit }} </p>
<p class="name">+ {{ req.items.length - showLimit }}</p>
</div>
</div>
<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">
<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>
</div>
<div v-else-if="req.isDir && req.items.length === 0" class="share__box share__box__items">
<div
v-else-if="req.isDir && req.items.length === 0"
class="share__box share__box__items"
>
<h2 class="message">
<i class="material-icons">sentiment_dissatisfied</i>
<span>{{ $t('files.lonely') }}</span>
<span>{{ $t("files.lonely") }}</span>
</h2>
</div>
</div>
</div>
<div v-if="error">
<div v-if="error.message === '401'">
<div class="card floating" id="password">
<div v-if="attemptedPasswordLogin" class="share__wrong__password">{{ $t('login.wrongCredentials') }}</div>
<div class="card-title">
<h2>{{ $t('login.password') }}</h2>
</div>
<div class="card-content">
<input v-focus type="password" :placeholder="$t('login.password')" v-model="password" @keyup.enter="fetchData">
</div>
<div class="card-action">
<button class="button button--flat"
@click="fetchData"
:aria-label="$t('buttons.submit')"
:title="$t('buttons.submit')">{{ $t('buttons.submit') }}</button>
</div>
</div>
</div>
<errors v-else :errorCode="errorCode" />
</div>
</div>
</template>
<script>
import {mapState, mapMutations, mapGetters} from 'vuex';
import { pub as api } from '@/api'
import { baseURL } from '@/utils/constants'
import filesize from 'filesize'
import moment from 'moment'
import { mapState, mapMutations, mapGetters } from "vuex";
import { pub as api } from "@/api";
import { baseURL } from "@/utils/constants";
import filesize from "filesize";
import moment from "moment";
import HeaderBar from '@/components/header/HeaderBar'
import Action from '@/components/header/Action'
import Breadcrumbs from '@/components/Breadcrumbs'
import Errors from '@/views/Errors'
import QrcodeVue from 'qrcode.vue'
import Item from "@/components/files/ListingItem"
import HeaderBar from "@/components/header/HeaderBar";
import Action from "@/components/header/Action";
import Breadcrumbs from "@/components/Breadcrumbs";
import Errors from "@/views/Errors";
import QrcodeVue from "qrcode.vue";
import Item from "@/components/files/ListingItem";
export default {
name: 'share',
name: "share",
components: {
HeaderBar,
Action,
Breadcrumbs,
Item,
QrcodeVue,
Errors
Errors,
},
data: () => ({
error: null,
showLimit: 500,
password: '',
showLimit: 100,
password: "",
attemptedPasswordLogin: false,
hash: null,
token: null
token: null,
}),
watch: {
'$route': 'fetchData'
$route: function () {
this.showLimit = 100;
this.fetchData();
},
},
created: async function () {
const hash = this.$route.params.pathMatch.split('/')[0]
this.hash = hash
await this.fetchData()
const hash = this.$route.params.pathMatch.split("/")[0];
this.hash = hash;
await this.fetchData();
},
mounted () {
window.addEventListener('keydown', this.keyEvent)
mounted() {
window.addEventListener("keydown", this.keyEvent);
},
beforeDestroy () {
window.removeEventListener('keydown', this.keyEvent)
beforeDestroy() {
window.removeEventListener("keydown", this.keyEvent);
},
computed: {
...mapState(['req', 'loading', 'multiple', 'selected']),
...mapGetters(['selectedCount', 'selectedCount']),
...mapState(["req", "loading", "multiple", "selected"]),
...mapGetters(["selectedCount", "selectedCount"]),
icon: function () {
if (this.req.isDir) return 'folder'
if (this.req.type === 'image') return 'insert_photo'
if (this.req.type === 'audio') return 'volume_up'
if (this.req.type === 'video') return 'movie'
return 'insert_drive_file'
if (this.req.isDir) return "folder";
if (this.req.type === "image") return "insert_photo";
if (this.req.type === "audio") return "volume_up";
if (this.req.type === "video") return "movie";
return "insert_drive_file";
},
link: function () {
let queryArg = '';
if (this.token !== ''){
queryArg = `?token=${this.token}`
let queryArg = "";
if (this.token !== "") {
queryArg = `?token=${this.token}`;
}
const path = this.$route.path.split('/').splice(2).join('/')
return `${baseURL}/api/public/dl/${path}${queryArg}`
const path = this.$route.path.split("/").splice(2).join("/");
return `${baseURL}/api/public/dl/${path}${queryArg}`;
},
inlineLink: function () {
let url = new URL(this.link);
url.searchParams.set("inline", "true");
return url.href;
},
fullLink: function () {
return window.location.origin + this.link;
},
humanSize: function () {
if (this.req.isDir) {
return this.req.items.length
return this.req.items.length;
}
return filesize(this.req.size)
return filesize(this.req.size);
},
humanTime: function () {
return moment(this.req.modified).fromNow()
return moment(this.req.modified).fromNow();
},
modTime: function () {
return new Date(Date.parse(this.req.modified)).toLocaleString();
},
errorCode() {
return (this.error.message === '404' || this.error.message === '403') ? parseInt(this.error.message) : 500
}
},
methods: {
...mapMutations([ 'resetSelected', 'updateRequest', 'setLoading' ]),
...mapMutations(["resetSelected", "updateRequest", "setLoading"]),
base64: function (name) {
return window.btoa(unescape(encodeURIComponent(name)))
return window.btoa(unescape(encodeURIComponent(name)));
},
fetchData: async function () {
// Reset view information.
this.$store.commit('setReload', false)
this.$store.commit('resetSelected')
this.$store.commit('multiple', false)
this.$store.commit('closeHovers')
this.$store.commit("setReload", false);
this.$store.commit("resetSelected");
this.$store.commit("multiple", false);
this.$store.commit("closeHovers");
// Set loading to true and reset the error.
this.setLoading(true)
this.error = null
this.setLoading(true);
this.error = null;
if (this.password !== ''){
this.attemptedPasswordLogin = true
if (this.password !== "") {
this.attemptedPasswordLogin = true;
}
let url = this.$route.path
if (url === '') url = '/'
if (url[0] !== '/') url = '/' + url
let url = this.$route.path;
if (url === "") url = "/";
if (url[0] !== "/") url = "/" + url;
try {
let file = await api.fetch(url, this.password)
let file = await api.fetch(url, this.password);
this.token = file.token || ''
this.token = file.token || "";
this.updateRequest(file)
this.setLoading(false)
this.updateRequest(file);
document.title = `${file.name} - ${this.$route.name}`;
} catch (e) {
this.error = e
this.error = e;
} finally {
this.setLoading(false);
}
},
keyEvent (event) {
keyEvent(event) {
// Esc!
if (event.keyCode === 27) {
// If we're on a listing, unselect all
// files and folders.
if (this.selectedCount > 0) {
this.resetSelected()
this.resetSelected();
}
}
},
toggleMultipleSelection () {
this.$store.commit('multiple', !this.multiple)
toggleMultipleSelection() {
this.$store.commit("multiple", !this.multiple);
},
download () {
download() {
if (this.selectedCount === 1 && !this.req.items[this.selected[0]].isDir) {
api.download(null, this.hash, this.token, this.req.items[this.selected[0]].path)
return
api.download(
null,
this.hash,
this.token,
this.req.items[this.selected[0]].path
);
return;
}
this.$store.commit('showHover', {
prompt: 'download',
this.$store.commit("showHover", {
prompt: "download",
confirm: (format) => {
this.$store.commit('closeHovers')
this.$store.commit("closeHovers");
let files = []
let files = [];
for (let i of this.selected) {
files.push(this.req.items[i].path)
files.push(this.req.items[i].path);
}
api.download(format, this.hash, this.token, ...files)
}
})
}
}
}
api.download(format, this.hash, this.token, ...files);
},
});
},
},
};
</script>

View File

@@ -5,7 +5,13 @@
<title>{{ req.name }}</title>
<template #actions>
<action id="save-button" icon="save" :label="$t('buttons.save')" @action="save()" />
<action
v-if="user.perm.modify"
id="save-button"
icon="save"
:label="$t('buttons.save')"
@action="save()"
/>
</template>
</header-bar>
@@ -16,120 +22,120 @@
</template>
<script>
import { mapState } from 'vuex'
import { files as api } from '@/api'
import { theme } from '@/utils/constants'
import buttons from '@/utils/buttons'
import url from '@/utils/url'
import { mapState } from "vuex";
import { files as api } from "@/api";
import { theme } from "@/utils/constants";
import buttons from "@/utils/buttons";
import url from "@/utils/url";
import ace from 'ace-builds/src-min-noconflict/ace.js'
import modelist from 'ace-builds/src-min-noconflict/ext-modelist.js'
import 'ace-builds/webpack-resolver'
import ace from "ace-builds/src-min-noconflict/ace.js";
import modelist from "ace-builds/src-min-noconflict/ext-modelist.js";
import "ace-builds/webpack-resolver";
import HeaderBar from '@/components/header/HeaderBar'
import Action from '@/components/header/Action'
import Breadcrumbs from '@/components/Breadcrumbs'
import HeaderBar from "@/components/header/HeaderBar";
import Action from "@/components/header/Action";
import Breadcrumbs from "@/components/Breadcrumbs";
export default {
name: 'editor',
name: "editor",
components: {
HeaderBar,
Action,
Breadcrumbs
Breadcrumbs,
},
data: function () {
return {}
return {};
},
computed: {
...mapState(['req', 'user']),
breadcrumbs () {
let parts = this.$route.path.split('/')
...mapState(["req", "user"]),
breadcrumbs() {
let parts = this.$route.path.split("/");
if (parts[0] === '') {
parts.shift()
if (parts[0] === "") {
parts.shift();
}
if (parts[parts.length - 1] === '') {
parts.pop()
if (parts[parts.length - 1] === "") {
parts.pop();
}
let breadcrumbs = []
let breadcrumbs = [];
for (let i = 0; i < parts.length; i++) {
breadcrumbs.push({ name: decodeURIComponent(parts[i]) })
breadcrumbs.push({ name: decodeURIComponent(parts[i]) });
}
breadcrumbs.shift()
breadcrumbs.shift();
if (breadcrumbs.length > 3) {
while (breadcrumbs.length !== 4) {
breadcrumbs.shift()
breadcrumbs.shift();
}
breadcrumbs[0].name = '...'
breadcrumbs[0].name = "...";
}
return breadcrumbs
}
return breadcrumbs;
},
},
created () {
window.addEventListener('keydown', this.keyEvent)
created() {
window.addEventListener("keydown", this.keyEvent);
},
beforeDestroy () {
window.removeEventListener('keydown', this.keyEvent)
beforeDestroy() {
window.removeEventListener("keydown", this.keyEvent);
this.editor.destroy();
},
mounted: function () {
const fileContent = this.req.content || '';
const fileContent = this.req.content || "";
this.editor = ace.edit('editor', {
this.editor = ace.edit("editor", {
value: fileContent,
showPrintMargin: false,
readOnly: this.req.type === 'textImmutable',
theme: 'ace/theme/chrome',
readOnly: this.req.type === "textImmutable",
theme: "ace/theme/chrome",
mode: modelist.getModeForPath(this.req.name).mode,
wrap: true
})
wrap: true,
});
if (theme == 'dark') {
if (theme == "dark") {
this.editor.setTheme("ace/theme/twilight");
}
},
methods: {
back () {
let uri = url.removeLastDir(this.$route.path) + '/'
this.$router.push({ path: uri })
back() {
let uri = url.removeLastDir(this.$route.path) + "/";
this.$router.push({ path: uri });
},
keyEvent (event) {
keyEvent(event) {
if (!event.ctrlKey && !event.metaKey) {
return
return;
}
if (String.fromCharCode(event.which).toLowerCase() !== 's') {
return
if (String.fromCharCode(event.which).toLowerCase() !== "s") {
return;
}
event.preventDefault()
this.save()
event.preventDefault();
this.save();
},
async save () {
const button = 'save'
buttons.loading('save')
async save() {
const button = "save";
buttons.loading("save");
try {
await api.put(this.$route.path, this.editor.getValue())
buttons.success(button)
await api.put(this.$route.path, this.editor.getValue());
buttons.success(button);
} catch (e) {
buttons.done(button)
this.$showError(e)
buttons.done(button);
this.$showError(e);
}
},
close () {
this.$store.commit('updateRequest', {})
close() {
this.$store.commit("updateRequest", {});
let uri = url.removeLastDir(this.$route.path) + '/'
this.$router.push({ path: uri })
}
}
}
let uri = url.removeLastDir(this.$route.path) + "/";
this.$router.push({ path: uri });
},
},
};
</script>

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More