Compare commits

...

105 Commits

Author SHA1 Message Date
Oleg Lobanov
bf303c536a chore(release): 2.17.2 2021-08-27 12:40:51 +02:00
Andrew Kennedy
43a460993c fix: bug with inlineLink not creating url properly (#1515) 2021-08-26 12:43:37 +02:00
Oleg Lobanov
7f0673ee70 chore(release): 2.17.1 2021-08-23 10:03:31 +02:00
Oleg Lobanov
4c3099a086 fix: internal server error if --disable-preview-resize flag is set (closes #1510) 2021-08-23 10:03:11 +02:00
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
Oleg Lobanov
e1f658633d chore(release): 2.13.0 2021-03-14 20:02:02 +01:00
Oleg Lobanov
9c79105c02 chore: prevent deleting .gitignore from dist folder 2021-03-14 19:59:55 +01:00
Jürgen Hötzel
6d5ceae8b4 fix: wait for async command exit (#1326)
This prevents the accumulation of zombie processes when using
async (&) event commands. Also log async command failures.
2021-03-14 19:32:14 +01:00
Oleg Lobanov
381f09087a Merge pull request #1321 from ramiresviana/fixes-6 2021-03-14 14:19:50 +01:00
Ramires Viana
426b38bb33 fix: root path name on archive 2021-03-12 15:52:52 +00:00
Ramires Viana
488d98045e fix: download current dir on file listing 2021-03-12 15:28:49 +00:00
Ramires Viana
7955e0720b fix: encoded file path on share 2021-03-12 15:15:56 +00:00
Ramires Viana
e017a19985 fix: full file path on share 2021-03-12 12:14:58 +00:00
Ramires Viana
f8df76f526 fix: header dropdown icon color on previewer 2021-03-11 16:01:54 +00:00
Ramires Viana
11ebaec5f0 fix: modified time on info prompt 2021-03-11 15:20:37 +00:00
Ramires Viana
326b35a7ac fix: item dragging on file listing 2021-03-11 12:09:12 +00:00
Ramires Viana
5bf15548d0 fix: check rules on http resource handlers 2021-03-10 17:38:11 +00:00
Ramires Viana
6a734c0139 fix: stuck icon on header button 2021-03-10 15:32:10 +00:00
Ramires Viana
81b6f4d6f6 fix: update image cache when replacing 2021-03-10 15:14:01 +00:00
Ramires Viana
0b92d94570 chore: split POST method on resource http handler 2021-03-10 13:32:11 +00:00
Oleg Lobanov
fc5506179a refactor: migrate from rice to embed.FS 2021-03-09 19:09:32 +01:00
Oleg Lobanov
0fe34ad224 Merge pull request #1307 from ramiresviana/tweaks-1
Frontend code quality changes
2021-03-09 18:26:46 +01:00
Ramires Viana
54f35701a2 fix: archive contains parent path on Windows 2021-03-09 15:54:54 +00:00
FrzMtrsprt
a809404ce1 chore: update zh-cn.json (#1311) 2021-03-08 10:24:16 +01:00
Po Chen
fb32e44b47 chore: update zh-cn.json (#1309) 2021-03-07 15:41:05 +01:00
Oleg Lobanov
e9c0369062 chore(release): 2.12.1 2021-03-07 15:25:34 +01:00
Oleg Lobanov
7358b3fe31 fix: add missing default config into the docker image 2021-03-07 15:23:12 +01:00
Ramires Viana
2a1f759e9e chore: remove prompts events 2021-03-04 14:40:18 +00:00
Ramires Viana
edb9e85efd chore: share view logic responsability 2021-03-03 17:46:37 +00:00
Ramires Viana
d8306559fd chore: breadcrumbs component 2021-03-03 12:25:59 +00:00
Ramires Viana
7b6579ac8a chore: files view dynamic component 2021-03-01 16:12:17 +00:00
Ramires Viana
057307181e chore: removed header buttons components 2021-03-01 13:41:35 +00:00
Ramires Viana
4fb832c042 feat: increased header button counter size 2021-03-01 12:57:39 +00:00
Ramires Viana
e503cb69f2 chore: files pages logic responsability 2021-02-26 15:10:21 +00:00
Ramires Viana
95811e99bc chore: header bar component 2021-02-25 18:37:07 +00:00
Ramires Viana
62fff5ca60 feat: larger previewer content 2021-02-22 16:01:13 +00:00
Ramires Viana
5b28aa0848 feat: improved settings navbar 2021-02-19 16:01:43 +00:00
Ramires Viana
db5aad8eb6 feat: dual pane settings view 2021-02-19 13:15:46 +00:00
Ramires Viana
1819377897 feat: improved sharing prompt 2021-02-16 15:39:11 +00:00
155 changed files with 26149 additions and 10421 deletions

8
.docker.json Normal file
View File

@@ -0,0 +1,8 @@
{
"port": 80,
"baseURL": "",
"address": "",
"log": "stdout",
"database": "/database.db",
"root": "/srv"
}

View File

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

View File

@@ -4,19 +4,19 @@ about: Create a report to help us improve
--- ---
**Description** **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** **Expected behaviour**
What did you expect to happen? <!-- What did you expect to happen? -->
**What is happening instead?** **What is happening instead?**
Please, give full error messages and/or log. <!-- Please, give full error messages and/or log. -->
**Additional context** **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?** **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** **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.** **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** **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** **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** **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** **Description**
<!--
Please explain the changes you made here. Please explain the changes you made here.
If the feature changes current behaviour, explain why your solution is better. 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/). :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. - [ ] AVOID breaking the continuous integration build.
**Further comments** **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. 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! :heart: Thank you!
-->

3
.gitignore vendored
View File

@@ -5,11 +5,10 @@ _old
rice-box.go rice-box.go
.idea/ .idea/
filebrowser filebrowser
dist/ filebrowser.exe
.DS_Store .DS_Store
node_modules node_modules
/frontend/dist
# local env files # local env files
.env.local .env.local

View File

@@ -6,6 +6,8 @@ linters-settings:
funlen: funlen:
lines: 100 lines: 100
statements: 50 statements: 50
gci:
local-prefixes: github.com/filebrowser/filebrowser
goconst: goconst:
min-len: 2 min-len: 2
min-occurrences: 2 min-occurrences: 2
@@ -26,8 +28,6 @@ linters-settings:
min-complexity: 15 min-complexity: 15
goimports: goimports:
local-prefixes: github.com/filebrowser/filebrowser local-prefixes: github.com/filebrowser/filebrowser
golint:
min-confidence: 0
gomnd: gomnd:
settings: settings:
mnd: mnd:
@@ -58,26 +58,25 @@ linters:
- dogsled - dogsled
- dupl - dupl
- errcheck - errcheck
- exportloopref
- exhaustive
- funlen - funlen
- gochecknoinits - gochecknoinits
- goconst - goconst
- gocritic - gocritic
- gocyclo - gocyclo
- goimports - goimports
- golint
- gomnd - gomnd
- goprintffuncname - goprintffuncname
- gosec - gosec
- gosimple - gosimple
- govet - govet
- ineffassign - ineffassign
- interfacer
- lll - lll
- misspell - misspell
- nakedret - nakedret
- nolintlint - nolintlint
- rowserrcheck - rowserrcheck
- scopelint
- staticcheck - staticcheck
- structcheck - structcheck
- stylecheck - stylecheck
@@ -89,19 +88,6 @@ linters:
- whitespace - whitespace
- prealloc - 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: issues:
exclude-rules: exclude-rules:
- path: cmd/.*.go - path: cmd/.*.go
@@ -118,6 +104,9 @@ issues:
- text: "Auther" - text: "Auther"
linters: linters:
- misspell - misspell
- text: "strconv.Parse"
linters:
- gomnd
run: run:
skip-dirs: skip-dirs:

View File

@@ -3,10 +3,6 @@ project_name: filebrowser
env: env:
- GO111MODULE=on - GO111MODULE=on
before:
hooks:
- make bundle-frontend
build: build:
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
@@ -59,6 +55,8 @@ dockers:
image_templates: image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-amd64" - "filebrowser/filebrowser:{{ .Tag }}-amd64"
- "filebrowser/filebrowser:v{{ .Major }}-amd64" - "filebrowser/filebrowser:v{{ .Major }}-amd64"
extra_files:
- .docker.json
- -
dockerfile: Dockerfile dockerfile: Dockerfile
use_buildx: true use_buildx: true
@@ -75,6 +73,8 @@ dockers:
image_templates: image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-arm64" - "filebrowser/filebrowser:{{ .Tag }}-arm64"
- "filebrowser/filebrowser:v{{ .Major }}-arm64" - "filebrowser/filebrowser:v{{ .Major }}-arm64"
extra_files:
- .docker.json
- -
dockerfile: Dockerfile dockerfile: Dockerfile
use_buildx: true use_buildx: true
@@ -92,6 +92,8 @@ dockers:
image_templates: image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-armv6" - "filebrowser/filebrowser:{{ .Tag }}-armv6"
- "filebrowser/filebrowser:v{{ .Major }}-armv6" - "filebrowser/filebrowser:v{{ .Major }}-armv6"
extra_files:
- .docker.json
- -
dockerfile: Dockerfile dockerfile: Dockerfile
use_buildx: true use_buildx: true
@@ -109,6 +111,8 @@ dockers:
image_templates: image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-armv7" - "filebrowser/filebrowser:{{ .Tag }}-armv7"
- "filebrowser/filebrowser:v{{ .Major }}-armv7" - "filebrowser/filebrowser:v{{ .Major }}-armv7"
extra_files:
- .docker.json
docker_manifests: docker_manifests:
- name_template: "filebrowser/filebrowser:latest" - name_template: "filebrowser/filebrowser:latest"
image_templates: image_templates:

View File

@@ -2,6 +2,156 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [2.17.2](https://github.com/filebrowser/filebrowser/compare/v2.17.1...v2.17.2) (2021-08-27)
### Bug Fixes
* bug with inlineLink not creating url properly ([#1515](https://github.com/filebrowser/filebrowser/issues/1515)) ([43a4609](https://github.com/filebrowser/filebrowser/commit/43a460993c3f0d158b876db4b20caa7963e9f361))
### [2.17.1](https://github.com/filebrowser/filebrowser/compare/v2.17.0...v2.17.1) (2021-08-23)
### Bug Fixes
* internal server error if --disable-preview-resize flag is set (closes [#1510](https://github.com/filebrowser/filebrowser/issues/1510)) ([4c3099a](https://github.com/filebrowser/filebrowser/commit/4c3099a086c206dcb3bc70ee8c8da02eee61c30b))
## [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)
### Features
* dual pane settings view ([db5aad8](https://github.com/filebrowser/filebrowser/commit/db5aad8eb679cfe1b1ace5142cf342951217f0f7))
* improved settings navbar ([5b28aa0](https://github.com/filebrowser/filebrowser/commit/5b28aa0848710b9d3ee02a2aa912856395f48bd2))
* improved sharing prompt ([1819377](https://github.com/filebrowser/filebrowser/commit/18193778971e27d18b5a35df8c2d0e2953b48111))
* increased header button counter size ([4fb832c](https://github.com/filebrowser/filebrowser/commit/4fb832c0422107e16f22b7aa928224f36de4978f))
* larger previewer content ([62fff5c](https://github.com/filebrowser/filebrowser/commit/62fff5ca60da1f887c1f95fa4808d3753596dab2))
### Bug Fixes
* archive contains parent path on Windows ([54f3570](https://github.com/filebrowser/filebrowser/commit/54f35701a2bd5cb7ec0628ca9789047072c073db))
* check rules on http resource handlers ([5bf1554](https://github.com/filebrowser/filebrowser/commit/5bf15548d0ad147acfad5000277531be2671f7ce))
* download current dir on file listing ([488d980](https://github.com/filebrowser/filebrowser/commit/488d98045e7476ed11e53c13d9498a9db3165bbc))
* encoded file path on share ([7955e07](https://github.com/filebrowser/filebrowser/commit/7955e0720baef3710106c7e69bbbf078d5489220))
* full file path on share ([e017a19](https://github.com/filebrowser/filebrowser/commit/e017a199850e19dd51b960ba59402c215fd8f1af))
* header dropdown icon color on previewer ([f8df76f](https://github.com/filebrowser/filebrowser/commit/f8df76f52684f10722ce123fec2c90e321ddf103))
* item dragging on file listing ([326b35a](https://github.com/filebrowser/filebrowser/commit/326b35a7ac7871afcdf892ca150349665b7f6379))
* modified time on info prompt ([11ebaec](https://github.com/filebrowser/filebrowser/commit/11ebaec5f0671ec02ebe55d4a73a514bce3a6713))
* root path name on archive ([426b38b](https://github.com/filebrowser/filebrowser/commit/426b38bb3362d2d477d0d8aa27d880664d537431))
* stuck icon on header button ([6a734c0](https://github.com/filebrowser/filebrowser/commit/6a734c01391b437c2842f5d97fb63f29a0017510))
* update image cache when replacing ([81b6f4d](https://github.com/filebrowser/filebrowser/commit/81b6f4d6f6a01886583016f61f4f1951a59f244d))
* wait for async command exit ([#1326](https://github.com/filebrowser/filebrowser/issues/1326)) ([6d5ceae](https://github.com/filebrowser/filebrowser/commit/6d5ceae8b454edd749b3b65c88aacc0a31ce9215))
### Refactorings
* migrate from rice to embed.FS ([fc55061](https://github.com/filebrowser/filebrowser/commit/fc5506179a64e9e2f57f7b6d6cce4b95f5ebc235))
### [2.12.1](https://github.com/filebrowser/filebrowser/compare/v2.12.0...v2.12.1) (2021-03-07)
### Bug Fixes
* add missing default config into the docker image ([7358b3f](https://github.com/filebrowser/filebrowser/commit/7358b3fe3178c20007b4b5ef5c03705badd538c4))
## [2.12.0](https://github.com/filebrowser/filebrowser/compare/v2.11.0...v2.12.0) (2021-03-04) ## [2.12.0](https://github.com/filebrowser/filebrowser/compare/v2.11.0...v2.12.0) (2021-03-04)

View File

@@ -1,10 +1,15 @@
FROM alpine:latest FROM alpine:latest
RUN apk --update add ca-certificates RUN apk --update add ca-certificates \
RUN apk --update add mailcap mailcap \
curl
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
CMD curl -f http://localhost/health || exit 1
VOLUME /srv VOLUME /srv
EXPOSE 80 EXPOSE 80
COPY .docker.json /.filebrowser.json
COPY filebrowser /filebrowser COPY filebrowser /filebrowser
ENTRYPOINT [ "/filebrowser" ] ENTRYPOINT [ "/filebrowser" ]

View File

@@ -20,10 +20,10 @@ MODULE = $(shell env GO111MODULE=on $(GO) list -m)
DATE ?= $(shell date +%FT%T%z) DATE ?= $(shell date +%FT%T%z)
VERSION ?= $(shell git describe --tags --always --match=v* 2> /dev/null || \ VERSION ?= $(shell git describe --tags --always --match=v* 2> /dev/null || \
cat $(CURDIR)/.version 2> /dev/null || echo v0) 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) 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 # tools
$(BIN): $(BIN):
@@ -32,13 +32,10 @@ $(BIN)/%: | $(BIN) ; $(info $(M) installing $(PACKAGE)…)
$Q env GOBIN=$(BIN) $(GO) install $(PACKAGE) $Q env GOBIN=$(BIN) $(GO) install $(PACKAGE)
GOLANGCI_LINT = $(BIN)/golangci-lint 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 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
RICE = $(BIN)/rice
$(BIN)/rice: PACKAGE=github.com/GeertJohan/go.rice/rice@v1.0.2
## build: Build ## build: Build
.PHONY: build .PHONY: build
@@ -51,11 +48,8 @@ build-frontend: | ; $(info $(M) building frontend…)
## build-backend: Build backend ## build-backend: Build backend
.PHONY: build-backend .PHONY: build-backend
build-backend: bundle-frontend | ; $(info $(M) building backend) build-backend: | ; $(info $(M) building backend)
$Q $(GO) build -ldflags '$(LDFLAGS)' -o filebrowser $Q $(GO) build -ldflags '$(LDFLAGS)' -o .
bundle-frontend: | $(RICE) ; $(info $(M) building backend)
$Q cd ./http && rm -rf rice-box.go && $(RICE) embed-go
## test: Run all tests ## test: Run all tests
.PHONY: test .PHONY: test
@@ -67,7 +61,7 @@ test-frontend: | ; $(info $(M) running frontend tests…)
## test-backend: Run backend tests ## test-backend: Run backend tests
.PHONY: test-backend .PHONY: test-backend
test-backend: | $(RICE) ; $(info $(M) running backend tests) test-backend: | ; $(info $(M) running backend tests)
$Q $(GO) test -v ./... $Q $(GO) test -v ./...
## lint: Lint ## 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) { func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Auther) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) //nolint:gomnd
fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup) fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup)
fmt.Fprintf(w, "Create User Dir:\t%t\n", set.CreateUserDir) fmt.Fprintf(w, "Create User Dir:\t%t\n", set.CreateUserDir)

View File

@@ -3,6 +3,7 @@ package cmd
import ( import (
"crypto/tls" "crypto/tls"
"errors" "errors"
"io/fs"
"io/ioutil" "io/ioutil"
"log" "log"
"net" "net"
@@ -22,6 +23,7 @@ import (
"github.com/filebrowser/filebrowser/v2/auth" "github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/diskcache" "github.com/filebrowser/filebrowser/v2/diskcache"
"github.com/filebrowser/filebrowser/v2/frontend"
fbhttp "github.com/filebrowser/filebrowser/v2/http" fbhttp "github.com/filebrowser/filebrowser/v2/http"
"github.com/filebrowser/filebrowser/v2/img" "github.com/filebrowser/filebrowser/v2/img"
"github.com/filebrowser/filebrowser/v2/settings" "github.com/filebrowser/filebrowser/v2/settings"
@@ -59,10 +61,10 @@ func addServerFlags(flags *pflag.FlagSet) {
flags.StringP("key", "k", "", "tls key") flags.StringP("key", "k", "", "tls key")
flags.StringP("root", "r", ".", "root to prepend to relative paths") 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.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.StringP("baseurl", "b", "", "base url")
flags.String("cache-dir", "", "file cache directory (disabled if empty)") 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-thumbnails", false, "disable image thumbnails")
flags.Bool("disable-preview-resize", false, "disable resize of image previews") flags.Bool("disable-preview-resize", false, "disable resize of image previews")
flags.Bool("disable-exec", false, "disables Command Runner feature") flags.Bool("disable-exec", false, "disables Command Runner feature")
@@ -126,7 +128,7 @@ user created with the credentials from options "username" and "password".`,
cacheDir, err := cmd.Flags().GetString("cache-dir") cacheDir, err := cmd.Flags().GetString("cache-dir")
checkErr(err) checkErr(err)
if cacheDir != "" { 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) log.Fatalf("can't make directory %s: %s", cacheDir, err)
} }
fileCache = diskcache.New(afero.NewOsFs(), cacheDir) fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
@@ -168,7 +170,12 @@ user created with the credentials from options "username" and "password".`,
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM) signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
go cleanupHandler(listener, sigc) go cleanupHandler(listener, sigc)
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server) assetsFs, err := fs.Sub(frontend.Assets(), "dist")
if err != nil {
panic(err)
}
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server, assetsFs)
checkErr(err) checkErr(err)
defer listener.Close() defer listener.Close()

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', you can remove all commands from 'index' to 'index_end',
including 'index_end'.`, including 'index_end'.`,
Args: func(cmd *cobra.Command, args []string) error { Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.RangeArgs(1, 2)(cmd, args); err != nil { if err := cobra.RangeArgs(1, 2)(cmd, args); err != nil { //nolint:gomnd
return err return err
} }

View File

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

View File

@@ -72,7 +72,7 @@ func dbExists(path string) (bool, error) {
d := filepath.Dir(path) d := filepath.Dir(path)
_, err = os.Stat(d) _, err = os.Stat(d)
if os.IsNotExist(err) { 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, err
} }
return false, nil return false, nil

View File

@@ -37,11 +37,11 @@ func (f *FileCache) Store(ctx context.Context, key string, value []byte) error {
defer mu.Unlock() defer mu.Unlock()
fileName := f.getFileName(key) 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 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 return err
} }

View File

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

12
frontend/assets.go Normal file
View File

@@ -0,0 +1,12 @@
// +build !dev
package frontend
import "embed"
//go:embed dist/*
var assets embed.FS
func Assets() embed.FS {
return assets
}

14
frontend/assets_dev.go Normal file
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 = { module.exports = {
presets: [ presets: ["@vue/app"],
'@vue/app' };
]
}

4
frontend/dist/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

25759
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -16,7 +16,7 @@ body {
#loading { #loading {
background: var(--background); background: var(--background);
} }
#loading .spinner div, #previewer .loading .spinner div { #loading .spinner div, main .spinner div {
background: var(--icon); background: var(--icon);
} }
@@ -69,13 +69,16 @@ nav > div {
border-color: var(--divider); border-color: var(--divider);
} }
#breadcrumbs { .breadcrumbs {
border-color: var(--divider); border-color: var(--divider);
color: var(--textPrimary) !important; color: var(--textPrimary) !important;
} }
#breadcrumbs span { .breadcrumbs span {
color: var(--textPrimary) !important; color: var(--textPrimary) !important;
} }
.breadcrumbs a:hover {
background-color: rgba(255, 255, 255, .1);
}
#listing .item { #listing .item {
background: var(--surfacePrimary); background: var(--surfacePrimary);
@@ -114,13 +117,20 @@ nav > div {
background: var(--surfaceSecondary); background: var(--surfaceSecondary);
} }
.dashboard #nav ul li {
color: var(--textSecondary);
}
.dashboard #nav ul li:hover {
background: var(--surfaceSecondary);
}
.card h3, .card h3,
.dashboard #nav, .dashboard #nav,
.dashboard p label { .dashboard p label {
color: var(--textPrimary); color: var(--textPrimary);
} }
.card#share ul li input, .card#share input,
.card#share ul li select, .card#share select,
.input { .input {
background: var(--surfaceSecondary); background: var(--surfaceSecondary);
color: var(--textPrimary); color: var(--textPrimary);
@@ -138,7 +148,7 @@ nav > div {
background: #147A41; background: #147A41;
} }
.dashboard #nav li, .dashboard #nav .wrapper,
.collapsible { .collapsible {
border-color: var(--divider); border-color: var(--divider);
} }

View File

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

View File

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

View File

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

View File

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

61
frontend/src/api/pub.js Normal file
View File

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

View File

@@ -1,31 +1,31 @@
import { fetchURL, removePrefix } from './utils' import { fetchURL, removePrefix } from "./utils";
import url from '../utils/url' import url from "../utils/url";
export default async function search (base, query) { export default async function search(base, query) {
base = removePrefix(base) base = removePrefix(base);
query = encodeURIComponent(query) query = encodeURIComponent(query);
if (!base.endsWith('/')) { if (!base.endsWith("/")) {
base += '/' base += "/";
} }
let res = await fetchURL(`/api/search${base}?query=${query}`, {}) let res = await fetchURL(`/api/search${base}?query=${query}`, {});
if (res.status === 200) { if (res.status === 200) {
let data = await res.json() let data = await res.json();
data = data.map((item) => { data = data.map((item) => {
item.url = `/files${base}` + url.encodePath(item.path) item.url = `/files${base}` + url.encodePath(item.path);
if (item.dir) { if (item.dir) {
item.url += '/' item.url += "/";
} }
return item return item;
}) });
return data return data;
} else { } 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 () { export function get() {
return fetchJSON(`/api/settings`, {}) return fetchJSON(`/api/settings`, {});
} }
export async function update (settings) { export async function update(settings) {
const res = await fetchURL(`/api/settings`, { const res = await fetchURL(`/api/settings`, {
method: 'PUT', method: "PUT",
body: JSON.stringify(settings) body: JSON.stringify(settings),
}) });
if (res.status !== 200) { if (res.status !== 200) {
throw new Error(res.status) throw new Error(res.status);
} }
} }

View File

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

View File

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

View File

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

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

View File

@@ -1,185 +0,0 @@
<template>
<header v-if="!isEditor && !isPreview">
<div>
<button @click="openSidebar" :aria-label="$t('buttons.toggleSidebar')" :title="$t('buttons.toggleSidebar')" class="action">
<i class="material-icons">menu</i>
</button>
<img :src="logoURL" alt="File Browser">
<search v-if="isLogged"></search>
</div>
<div>
<template v-if="isLogged || isSharing">
<button v-show="!isSharing" @click="openSearch" :aria-label="$t('buttons.search')" :title="$t('buttons.search')" class="search-button action">
<i class="material-icons">search</i>
</button>
<button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
<i class="material-icons">more_vert</i>
</button>
<!-- Menu that shows on listing AND mobile when there are files selected -->
<div id="file-selection" v-if="isMobile && isListing && !isSharing">
<span v-if="selectedCount > 0">{{ selectedCount }} selected</span>
<share-button v-show="showShareButton"></share-button>
<rename-button v-show="showRenameButton"></rename-button>
<copy-button v-show="showCopyButton"></copy-button>
<move-button v-show="showMoveButton"></move-button>
<delete-button v-show="showDeleteButton"></delete-button>
</div>
<!-- This buttons are shown on a dropdown on mobile phones -->
<div id="dropdown" :class="{ active: showMore }">
<div v-if="!isListing || !isMobile">
<share-button v-show="showShareButton"></share-button>
<rename-button v-show="showRenameButton"></rename-button>
<copy-button v-show="showCopyButton"></copy-button>
<move-button v-show="showMoveButton"></move-button>
<delete-button v-show="showDeleteButton"></delete-button>
</div>
<shell-button v-if="isExecEnabled && !isSharing && user.perm.execute" />
<switch-button v-show="isListing"></switch-button>
<download-button v-show="showDownloadButton"></download-button>
<upload-button v-show="showUpload"></upload-button>
<info-button v-show="isFiles"></info-button>
<button v-show="isListing || (isSharing && req.isDir)" @click="toggleMultipleSelection" :aria-label="$t('buttons.selectMultiple')" :title="$t('buttons.selectMultiple')" class="action" >
<i class="material-icons">check_circle</i>
<span>{{ $t('buttons.select') }}</span>
</button>
</div>
</template>
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
</div>
</header>
</template>
<script>
import Search from './Search'
import InfoButton from './buttons/Info'
import DeleteButton from './buttons/Delete'
import RenameButton from './buttons/Rename'
import UploadButton from './buttons/Upload'
import DownloadButton from './buttons/Download'
import SwitchButton from './buttons/SwitchView'
import MoveButton from './buttons/Move'
import CopyButton from './buttons/Copy'
import ShareButton from './buttons/Share'
import ShellButton from './buttons/Shell'
import {mapGetters, mapState} from 'vuex'
import { logoURL, enableExec } from '@/utils/constants'
import * as api from '@/api'
import buttons from '@/utils/buttons'
export default {
name: 'header-layout',
components: {
Search,
InfoButton,
DeleteButton,
ShareButton,
RenameButton,
DownloadButton,
CopyButton,
UploadButton,
SwitchButton,
MoveButton,
ShellButton
},
data: function () {
return {
width: window.innerWidth,
pluginData: {
api,
buttons,
'store': this.$store,
'router': this.$router
}
}
},
created () {
window.addEventListener('resize', () => {
this.width = window.innerWidth
})
},
computed: {
...mapGetters([
'selectedCount',
'isFiles',
'isEditor',
'isPreview',
'isListing',
'isLogged',
'isSharing'
]),
...mapState([
'req',
'user',
'loading',
'reload',
'multiple'
]),
logoURL: () => logoURL,
isExecEnabled: () => enableExec,
isMobile () {
return this.width <= 736
},
showUpload () {
return this.isListing && this.user.perm.create
},
showDownloadButton () {
return (this.isFiles && this.user.perm.download) || (this.isSharing && this.selectedCount > 0)
},
showDeleteButton () {
return this.isFiles && (this.isListing
? (this.selectedCount !== 0 && this.user.perm.delete)
: this.user.perm.delete)
},
showRenameButton () {
return this.isFiles && (this.isListing
? (this.selectedCount === 1 && this.user.perm.rename)
: this.user.perm.rename)
},
showShareButton () {
return this.isFiles && (this.isListing
? (this.selectedCount === 1 && this.user.perm.share)
: this.user.perm.share)
},
showMoveButton () {
return this.isFiles && (this.isListing
? (this.selectedCount > 0 && this.user.perm.rename)
: this.user.perm.rename)
},
showCopyButton () {
return this.isFiles && (this.isListing
? (this.selectedCount > 0 && this.user.perm.create)
: this.user.perm.create)
},
showMore () {
return (this.isFiles || this.isSharing) && this.$store.state.show === 'more'
},
showOverlay () {
return this.showMore
}
},
methods: {
openSidebar () {
this.$store.commit('showHover', 'sidebar')
},
openMore () {
this.$store.commit('showHover', 'more')
},
openSearch () {
this.$store.commit('showHover', 'search')
},
toggleMultipleSelection () {
this.$store.commit('multiple', !this.multiple)
this.resetPrompts()
},
resetPrompts () {
this.$store.commit('closeHovers')
}
}
}
</script>

View File

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

View File

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

View File

@@ -1,82 +1,134 @@
<template> <template>
<nav :class="{active}"> <nav :class="{ active }">
<template v-if="isLogged"> <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> <i class="material-icons">folder</i>
<span>{{ $t('sidebar.myFiles') }}</span> <span>{{ $t("sidebar.myFiles") }}</span>
</router-link> </router-link>
<div v-if="user.perm.create"> <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> <i class="material-icons">create_new_folder</i>
<span>{{ $t('sidebar.newFolder') }}</span> <span>{{ $t("sidebar.newFolder") }}</span>
</button> </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> <i class="material-icons">note_add</i>
<span>{{ $t('sidebar.newFile') }}</span> <span>{{ $t("sidebar.newFile") }}</span>
</button> </button>
</div> </div>
<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> <i class="material-icons">settings_applications</i>
<span>{{ $t('sidebar.settings') }}</span> <span>{{ $t("sidebar.settings") }}</span>
</router-link> </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> <i class="material-icons">exit_to_app</i>
<span>{{ $t('sidebar.logout') }}</span> <span>{{ $t("sidebar.logout") }}</span>
</button> </button>
</div> </div>
</template> </template>
<template v-else> <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> <i class="material-icons">exit_to_app</i>
<span>{{ $t('sidebar.login') }}</span> <span>{{ $t("sidebar.login") }}</span>
</router-link> </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> <i class="material-icons">person_add</i>
<span>{{ $t('sidebar.signup') }}</span> <span>{{ $t("sidebar.signup") }}</span>
</router-link> </router-link>
</template> </template>
<p class="credits"> <p class="credits">
<span> <span>
<span v-if="disableExternal">File Browser</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> {{ version }}</span>
</span> </span>
<span><a @click="help">{{ $t('sidebar.help') }}</a></span> <span
><a @click="help">{{ $t("sidebar.help") }}</a></span
>
</p> </p>
</nav> </nav>
</template> </template>
<script> <script>
import { mapState, mapGetters } from 'vuex' import { mapState, mapGetters } from "vuex";
import * as auth from '@/utils/auth' import * as auth from "@/utils/auth";
import { version, signup, disableExternal, noAuth, authMethod } from '@/utils/constants' import {
version,
signup,
disableExternal,
noAuth,
authMethod,
} from "@/utils/constants";
export default { export default {
name: 'sidebar', name: "sidebar",
computed: { computed: {
...mapState([ 'user' ]), ...mapState(["user"]),
...mapGetters([ 'isLogged' ]), ...mapGetters(["isLogged"]),
active () { active() {
return this.$store.state.show === 'sidebar' return this.$store.state.show === "sidebar";
}, },
signup: () => signup, signup: () => signup,
version: () => version, version: () => version,
disableExternal: () => disableExternal, disableExternal: () => disableExternal,
noAuth: () => noAuth, noAuth: () => noAuth,
authMethod: () => authMethod authMethod: () => authMethod,
}, },
methods: { methods: {
help () { help() {
this.$store.commit('showHover', 'help') this.$store.commit("showHover", "help");
}, },
logout: auth.logout logout: auth.logout,
} },
} };
</script> </script>

View File

@@ -1,17 +0,0 @@
<template>
<button @click="show" :aria-label="$t('buttons.copy')" :title="$t('buttons.copy')" class="action" id="copy-button">
<i class="material-icons">content_copy</i>
<span>{{ $t('buttons.copyFile') }}</span>
</button>
</template>
<script>
export default {
name: 'copy-button',
methods: {
show: function () {
this.$store.commit('showHover', 'copy')
}
}
}
</script>

View File

@@ -1,17 +0,0 @@
<template>
<button @click="show" :aria-label="$t('buttons.delete')" :title="$t('buttons.delete')" class="action" id="delete-button">
<i class="material-icons">delete</i>
<span>{{ $t('buttons.delete') }}</span>
</button>
</template>
<script>
export default {
name: 'delete-button',
methods: {
show: function () {
this.$store.commit('showHover', 'delete')
}
}
}
</script>

View File

@@ -1,35 +0,0 @@
<template>
<button @click="download" :aria-label="$t('buttons.download')" :title="$t('buttons.download')" id="download-button" class="action">
<i class="material-icons">file_download</i>
<span>{{ $t('buttons.download') }}</span>
<span v-if="selectedCount > 0" class="counter">{{ selectedCount }}</span>
</button>
</template>
<script>
import {mapGetters, mapState} from 'vuex'
import { files as api } from '@/api'
export default {
name: 'download-button',
computed: {
...mapState(['req', 'selected']),
...mapGetters(['isListing', 'selectedCount', 'isSharing'])
},
methods: {
download: function () {
if (!this.isListing && !this.isSharing) {
api.download(null, this.$route.path)
return
}
if (this.selectedCount === 1 && !this.req.items[this.selected[0]].isDir) {
api.download(null, this.req.items[this.selected[0]].url)
return
}
this.$store.commit('showHover', 'download')
}
}
}
</script>

View File

@@ -1,17 +0,0 @@
<template>
<button :title="$t('buttons.info')" :aria-label="$t('buttons.info')" class="action" @click="show">
<i class="material-icons">info</i>
<span>{{ $t('buttons.info') }}</span>
</button>
</template>
<script>
export default {
name: 'info-button',
methods: {
show: function () {
this.$store.commit('showHover', 'info')
}
}
}
</script>

View File

@@ -1,17 +0,0 @@
<template>
<button @click="show" :aria-label="$t('buttons.move')" :title="$t('buttons.move')" class="action" id="move-button">
<i class="material-icons">forward</i>
<span>{{ $t('buttons.moveFile') }}</span>
</button>
</template>
<script>
export default {
name: 'move-button',
methods: {
show: function () {
this.$store.commit('showHover', 'move')
}
}
}
</script>

View File

@@ -1,22 +0,0 @@
<template>
<button :title="$t('buttons.info')" :aria-label="$t('buttons.info')" class="action" @click="$emit('change-size')">
<i class="material-icons">{{ this.icon }}</i>
<span>{{ $t('buttons.info') }}</span>
</button>
</template>
<script>
export default {
name: 'preview-size-button',
props: [ 'size' ],
computed: {
icon () {
if (this.size) {
return 'photo_size_select_large'
}
return 'hd'
}
}
}
</script>

View File

@@ -1,17 +0,0 @@
<template>
<button @click="show" :aria-label="$t('buttons.rename')" :title="$t('buttons.rename')" class="action" id="rename-button">
<i class="material-icons">mode_edit</i>
<span>{{ $t('buttons.rename') }}</span>
</button>
</template>
<script>
export default {
name: 'rename-button',
methods: {
show: function () {
this.$store.commit('showHover', 'rename')
}
}
}
</script>

View File

@@ -1,17 +0,0 @@
<template>
<button @click="show" :aria-label="$t('buttons.share')" :title="$t('buttons.share')" class="action">
<i class="material-icons">share</i>
<span>{{ $t('buttons.share') }}</span>
</button>
</template>
<script>
export default {
name: 'share-button',
methods: {
show () {
this.$store.commit('showHover', 'share')
}
}
}
</script>

View File

@@ -1,17 +0,0 @@
<template>
<button @click="show" :aria-label="$t('buttons.shell')" :title="$t('buttons.shell')" class="action">
<i class="material-icons">code</i>
<span>{{ $t('buttons.shell') }}</span>
</button>
</template>
<script>
export default {
name: 'shell-button',
methods: {
show: function () {
this.$store.commit('toggleShell')
}
}
}
</script>

View File

@@ -1,40 +0,0 @@
<template>
<button @click="change" :aria-label="$t('buttons.switchView')" :title="$t('buttons.switchView')" class="action" id="switch-view-button">
<i class="material-icons">{{ icon }}</i>
<span>{{ $t('buttons.switchView') }}</span>
</button>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
import { users as api } from '@/api'
export default {
name: 'switch-button',
computed: {
...mapState(['user']),
icon: function () {
if (this.user.viewMode === 'mosaic') return 'view_list'
return 'view_module'
}
},
methods: {
...mapMutations([ 'updateUser', 'closeHovers' ]),
change: async function () {
this.closeHovers()
const data = {
id: this.user.id,
viewMode: (this.icon === 'view_list') ? 'list' : 'mosaic'
}
try {
await api.update(data, ['viewMode'])
this.updateUser(data)
} catch (e) {
this.$showError(e)
}
}
}
}
</script>

View File

@@ -1,21 +0,0 @@
<template>
<button @click="upload" :aria-label="$t('buttons.upload')" :title="$t('buttons.upload')" class="action" id="upload-button">
<i class="material-icons">file_upload</i>
<span>{{ $t('buttons.upload') }}</span>
</button>
</template>
<script>
export default {
name: 'upload-button',
methods: {
upload: function () {
if (typeof(DataTransferItem.prototype.webkitGetAsEntry) !== 'undefined') {
this.$store.commit('showHover', 'upload')
} else {
document.getElementById('upload-input').click();
}
}
}
}
</script>

View File

@@ -1,132 +0,0 @@
<template>
<div id="editor-container">
<div class="bar">
<button @click="back" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close" class="action">
<i class="material-icons">close</i>
</button>
<div class="title">
<span>{{ req.name }}</span>
</div>
<button @click="save" v-show="user.perm.modify" :aria-label="$t('buttons.save')" :title="$t('buttons.save')" id="save-button" class="action">
<i class="material-icons">save</i>
</button>
</div>
<div id="breadcrumbs">
<span><i class="material-icons">home</i></span>
<span v-for="(link, index) in breadcrumbs" :key="index">
<span class="chevron"><i class="material-icons">keyboard_arrow_right</i></span>
<span>{{ link.name }}</span>
</span>
</div>
<form id="editor"></form>
</div>
</template>
<script>
import { mapState } from 'vuex'
import { files as api } from '@/api'
import buttons from '@/utils/buttons'
import url from '@/utils/url'
import ace from 'ace-builds/src-min-noconflict/ace.js'
import modelist from 'ace-builds/src-min-noconflict/ext-modelist.js'
import 'ace-builds/webpack-resolver'
import { theme } from '@/utils/constants'
export default {
name: 'editor',
data: function () {
return {}
},
computed: {
...mapState(['req', 'user']),
breadcrumbs () {
let parts = this.$route.path.split('/')
if (parts[0] === '') {
parts.shift()
}
if (parts[parts.length - 1] === '') {
parts.pop()
}
let breadcrumbs = []
for (let i = 0; i < parts.length; i++) {
breadcrumbs.push({ name: decodeURIComponent(parts[i]) })
}
breadcrumbs.shift()
if (breadcrumbs.length > 3) {
while (breadcrumbs.length !== 4) {
breadcrumbs.shift()
}
breadcrumbs[0].name = '...'
}
return breadcrumbs
}
},
created () {
window.addEventListener('keydown', this.keyEvent)
},
beforeDestroy () {
window.removeEventListener('keydown', this.keyEvent)
this.editor.destroy();
},
mounted: function () {
const fileContent = this.req.content || '';
this.editor = ace.edit('editor', {
value: fileContent,
showPrintMargin: false,
readOnly: this.req.type === 'textImmutable',
theme: 'ace/theme/chrome',
mode: modelist.getModeForPath(this.req.name).mode,
wrap: true
})
if (theme == 'dark') {
this.editor.setTheme("ace/theme/twilight");
}
},
methods: {
back () {
let uri = url.removeLastDir(this.$route.path) + '/'
this.$router.push({ path: uri })
},
keyEvent (event) {
if (!event.ctrlKey && !event.metaKey) {
return
}
if (String.fromCharCode(event.which).toLowerCase() !== 's') {
return
}
event.preventDefault()
this.save()
},
async save () {
const button = 'save'
buttons.loading('save')
try {
await api.put(this.$route.path, this.editor.getValue())
buttons.success(button)
} catch (e) {
buttons.done(button)
this.$showError(e)
}
}
}
}
</script>

View File

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

View File

@@ -1,464 +0,0 @@
<template>
<div v-if="(req.numDirs + req.numFiles) == 0">
<h2 class="message">
<i class="material-icons">sentiment_dissatisfied</i>
<span>{{ $t('files.lonely') }}</span>
</h2>
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
<input style="display:none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory multiple>
</div>
<div v-else id="listing"
:class="user.viewMode">
<div>
<div class="item header">
<div></div>
<div>
<p :class="{ active: nameSorted }" class="name"
role="button"
tabindex="0"
@click="sort('name')"
:title="$t('files.sortByName')"
:aria-label="$t('files.sortByName')">
<span>{{ $t('files.name') }}</span>
<i class="material-icons">{{ nameIcon }}</i>
</p>
<p :class="{ active: sizeSorted }" class="size"
role="button"
tabindex="0"
@click="sort('size')"
:title="$t('files.sortBySize')"
:aria-label="$t('files.sortBySize')">
<span>{{ $t('files.size') }}</span>
<i class="material-icons">{{ sizeIcon }}</i>
</p>
<p :class="{ active: modifiedSorted }" class="modified"
role="button"
tabindex="0"
@click="sort('modified')"
:title="$t('files.sortByLastModified')"
:aria-label="$t('files.sortByLastModified')">
<span>{{ $t('files.lastModified') }}</span>
<i class="material-icons">{{ modifiedIcon }}</i>
</p>
</div>
</div>
</div>
<h2 v-if="req.numDirs > 0">{{ $t('files.folders') }}</h2>
<div v-if="req.numDirs > 0">
<item v-for="(item) in dirs"
:key="base64(item.name)"
v-bind:index="item.index"
v-bind:name="item.name"
v-bind:isDir="item.isDir"
v-bind:url="item.url"
v-bind:modified="item.modified"
v-bind:type="item.type"
v-bind:size="item.size">
</item>
</div>
<h2 v-if="req.numFiles > 0">{{ $t('files.files') }}</h2>
<div v-if="req.numFiles > 0">
<item v-for="(item) in files"
:key="base64(item.name)"
v-bind:index="item.index"
v-bind:name="item.name"
v-bind:isDir="item.isDir"
v-bind:url="item.url"
v-bind:modified="item.modified"
v-bind:type="item.type"
v-bind:size="item.size">
</item>
</div>
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
<input style="display:none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory multiple>
<div :class="{ active: $store.state.multiple }" id="multiple-selection">
<p>{{ $t('files.multipleSelectionEnabled') }}</p>
<div @click="$store.commit('multiple', false)" tabindex="0" role="button" :title="$t('files.clear')" :aria-label="$t('files.clear')" class="action">
<i class="material-icons">clear</i>
</div>
</div>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
import Item from './ListingItem'
import css from '@/utils/css'
import { users, files as api } from '@/api'
import * as upload from '@/utils/upload'
export default {
name: 'listing',
components: { Item },
data: function () {
return {
showLimit: 50,
dragCounter: 0
}
},
computed: {
...mapState(['req', 'selected', 'user', 'show']),
nameSorted () {
return (this.req.sorting.by === 'name')
},
sizeSorted () {
return (this.req.sorting.by === 'size')
},
modifiedSorted () {
return (this.req.sorting.by === 'modified')
},
ascOrdered () {
return this.req.sorting.asc
},
items () {
const dirs = []
const files = []
this.req.items.forEach((item) => {
if (item.isDir) {
dirs.push(item)
} else {
files.push(item)
}
})
return { dirs, files }
},
dirs () {
return this.items.dirs.slice(0, this.showLimit)
},
files () {
let showLimit = this.showLimit - this.items.dirs.length
if (showLimit < 0) showLimit = 0
return this.items.files.slice(0, showLimit)
},
nameIcon () {
if (this.nameSorted && !this.ascOrdered) {
return 'arrow_upward'
}
return 'arrow_downward'
},
sizeIcon () {
if (this.sizeSorted && this.ascOrdered) {
return 'arrow_downward'
}
return 'arrow_upward'
},
modifiedIcon () {
if (this.modifiedSorted && this.ascOrdered) {
return 'arrow_downward'
}
return 'arrow_upward'
}
},
mounted: function () {
// Check the columns size for the first time.
this.resizeEvent()
// Add the needed event listeners to the window and document.
window.addEventListener('keydown', this.keyEvent)
window.addEventListener('resize', this.resizeEvent)
window.addEventListener('scroll', this.scrollEvent)
document.addEventListener('dragover', this.preventDefault)
document.addEventListener('dragenter', this.dragEnter)
document.addEventListener('dragleave', this.dragLeave)
document.addEventListener('drop', this.drop)
},
beforeDestroy () {
// Remove event listeners before destroying this page.
window.removeEventListener('keydown', this.keyEvent)
window.removeEventListener('resize', this.resizeEvent)
window.removeEventListener('scroll', this.scrollEvent)
document.removeEventListener('dragover', this.preventDefault)
document.removeEventListener('dragenter', this.dragEnter)
document.removeEventListener('dragleave', this.dragLeave)
document.removeEventListener('drop', this.drop)
},
methods: {
...mapMutations([ 'updateUser', 'addSelected' ]),
base64: function (name) {
return window.btoa(unescape(encodeURIComponent(name)))
},
keyEvent (event) {
if (this.show !== null) {
return
}
if (!event.ctrlKey && !event.metaKey) {
return
}
let key = String.fromCharCode(event.which).toLowerCase()
switch (key) {
case 'f':
event.preventDefault()
this.$store.commit('showHover', 'search')
break
case 'c':
case 'x':
this.copyCut(event, key)
break
case 'v':
this.paste(event)
break
case 'a':
event.preventDefault()
for (let file of this.items.files) {
if (this.$store.state.selected.indexOf(file.index) === -1) {
this.addSelected(file.index)
}
}
for (let dir of this.items.dirs) {
if (this.$store.state.selected.indexOf(dir.index) === -1) {
this.addSelected(dir.index)
}
}
break
}
},
preventDefault (event) {
// Wrapper around prevent default.
event.preventDefault()
},
copyCut (event, key) {
if (event.target.tagName.toLowerCase() === 'input') {
return
}
let items = []
for (let i of this.selected) {
items.push({
from: this.req.items[i].url,
name: encodeURIComponent(this.req.items[i].name)
})
}
if (items.length == 0) {
return
}
this.$store.commit('updateClipboard', {
key: key,
items: items,
path: this.$route.path
})
},
paste (event) {
if (event.target.tagName.toLowerCase() === 'input') {
return
}
let items = []
for (let item of this.$store.state.clipboard.items) {
const from = item.from.endsWith('/') ? item.from.slice(0, -1) : item.from
const to = this.$route.path + item.name
items.push({ from, to, name: item.name })
}
if (items.length === 0) {
return
}
let action = (overwrite, rename) => {
api.copy(items, overwrite, rename).then(() => {
this.$store.commit('setReload', true)
}).catch(this.$showError)
}
if (this.$store.state.clipboard.key === 'x') {
action = (overwrite, rename) => {
api.move(items, overwrite, rename).then(() => {
this.$store.commit('resetClipboard')
this.$store.commit('setReload', true)
}).catch(this.$showError)
}
}
if (this.$store.state.clipboard.path == this.$route.path) {
action(false, true)
return
}
let conflict = upload.checkConflict(items, this.req.items)
let overwrite = false
let rename = false
if (conflict) {
this.$store.commit('showHover', {
prompt: 'replace-rename',
confirm: (event, option) => {
overwrite = option == 'overwrite'
rename = option == 'rename'
event.preventDefault()
this.$store.commit('closeHovers')
action(overwrite, rename)
}
})
return
}
action(overwrite, rename)
},
resizeEvent () {
// Update the columns size based on the window width.
let columns = Math.floor(document.querySelector('main').offsetWidth / 300)
let items = css(['#listing.mosaic .item', '.mosaic#listing .item'])
if (columns === 0) columns = 1
items.style.width = `calc(${100 / columns}% - 1em)`
},
scrollEvent () {
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
this.showLimit += 50
}
},
dragEnter () {
this.dragCounter++
// When the user starts dragging an item, put every
// file on the listing with 50% opacity.
let items = document.getElementsByClassName('item')
Array.from(items).forEach(file => {
file.style.opacity = 0.5
})
},
dragLeave () {
this.dragCounter--
if (this.dragCounter == 0) {
this.resetOpacity()
}
},
drop: async function (event) {
event.preventDefault()
this.dragCounter = 0
this.resetOpacity()
let dt = event.dataTransfer
let el = event.target
if (dt.files.length <= 0) return
for (let i = 0; i < 5; i++) {
if (el !== null && !el.classList.contains('item')) {
el = el.parentElement
}
}
let base = ''
if (el !== null && el.classList.contains('item') && el.dataset.dir === 'true') {
base = el.querySelector('.name').innerHTML + '/'
}
let files = await upload.scanFiles(dt)
let path = this.$route.path.endsWith('/') ? this.$route.path + base : this.$route.path + '/' + base
let items = this.req.items
if (base !== '') {
try {
items = (await api.fetch(path)).items
} catch (error) {
this.$showError(error)
}
}
let conflict = upload.checkConflict(files, items)
if (conflict) {
this.$store.commit('showHover', {
prompt: 'replace',
confirm: (event) => {
event.preventDefault()
this.$store.commit('closeHovers')
upload.handleFiles(files, path, true)
}
})
return
}
upload.handleFiles(files, path)
},
uploadInput (event) {
this.$store.commit('closeHovers')
let files = event.currentTarget.files
let folder_upload = files[0].webkitRelativePath !== undefined && files[0].webkitRelativePath !== ''
if (folder_upload) {
for (let i = 0; i < files.length; i++) {
let file = files[i]
files[i].fullPath = file.webkitRelativePath
}
}
let path = this.$route.path.endsWith('/') ? this.$route.path : this.$route.path + '/'
let conflict = upload.checkConflict(files, this.req.items)
if (conflict) {
this.$store.commit('showHover', {
prompt: 'replace',
confirm: (event) => {
event.preventDefault()
this.$store.commit('closeHovers')
upload.handleFiles(files, path, true)
}
})
return
}
upload.handleFiles(files, path)
},
resetOpacity () {
let items = document.getElementsByClassName('item')
Array.from(items).forEach(file => {
file.style.opacity = 1
})
},
async sort (by) {
let asc = false
if (by === 'name') {
if (this.nameIcon === 'arrow_upward') {
asc = true
}
} else if (by === 'size') {
if (this.sizeIcon === 'arrow_upward') {
asc = true
}
} else if (by === 'modified') {
if (this.modifiedIcon === 'arrow_upward') {
asc = true
}
}
try {
await users.update({ id: this.user.id, sorting: { by, asc } }, ['sorting'])
} catch (e) {
this.$showError(e)
}
this.$store.commit('setReload', true)
}
}
}
</script>

View File

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

View File

@@ -1,238 +0,0 @@
<template>
<div id="previewer">
<div class="bar">
<button @click="back" class="action" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close">
<i class="material-icons">close</i>
</button>
<div class="title">{{ this.name }}</div>
<preview-size-button v-if="isResizeEnabled && this.req.type === 'image'" @change-size="toggleSize" v-bind:size="fullSize" :disabled="loading"></preview-size-button>
<button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
<i class="material-icons">more_vert</i>
</button>
<div id="dropdown" :class="{ active : showMore }">
<rename-button :disabled="loading" v-if="user.perm.rename"></rename-button>
<delete-button :disabled="loading" v-if="user.perm.delete"></delete-button>
<download-button :disabled="loading" v-if="user.perm.download"></download-button>
<info-button :disabled="loading"></info-button>
</div>
</div>
<div class="loading" v-if="loading">
<div class="spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
</div>
<button class="action" @click="prev" v-show="hasPrevious" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')">
<i class="material-icons">chevron_left</i>
</button>
<button class="action" @click="next" v-show="hasNext" :aria-label="$t('buttons.next')" :title="$t('buttons.next')">
<i class="material-icons">chevron_right</i>
</button>
<template v-if="!loading">
<div class="preview">
<ExtendedImage v-if="req.type == 'image'" :src="raw"></ExtendedImage>
<audio v-else-if="req.type == 'audio'" :src="raw" autoplay controls></audio>
<video v-else-if="req.type == 'video'" :src="raw" autoplay controls>
<track
kind="captions"
v-for="(sub, index) in subtitles"
:key="index"
:src="sub"
:label="'Subtitle ' + index" :default="index === 0">
Sorry, your browser doesn't support embedded videos,
but don't worry, you can <a :href="download">download it</a>
and watch it with your favorite video player!
</video>
<object v-else-if="req.extension.toLowerCase() == '.pdf'" class="pdf" :data="raw"></object>
<a v-else-if="req.type == 'blob'" :href="download">
<h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2>
</a>
</div>
</template>
<div v-show="showMore" @click="resetPrompts" class="overlay"></div>
</div>
</template>
<script>
import { mapState } from 'vuex'
import url from '@/utils/url'
import { baseURL, resizePreview } from '@/utils/constants'
import { files as api } from '@/api'
import PreviewSizeButton from '@/components/buttons/PreviewSize'
import InfoButton from '@/components/buttons/Info'
import DeleteButton from '@/components/buttons/Delete'
import RenameButton from '@/components/buttons/Rename'
import DownloadButton from '@/components/buttons/Download'
import ExtendedImage from './ExtendedImage'
const mediaTypes = [
"image",
"video",
"audio",
"blob"
]
export default {
name: 'preview',
components: {
PreviewSizeButton,
InfoButton,
DeleteButton,
RenameButton,
DownloadButton,
ExtendedImage
},
data: function () {
return {
previousLink: '',
nextLink: '',
listing: null,
name: '',
subtitles: [],
fullSize: false
}
},
computed: {
...mapState(['req', 'user', 'oldReq', 'jwt', 'loading', 'show']),
hasPrevious () {
return (this.previousLink !== '')
},
hasNext () {
return (this.nextLink !== '')
},
download () {
return `${baseURL}/api/raw${url.encodePath(this.req.path)}?auth=${this.jwt}`
},
previewUrl () {
if (this.req.type === 'image' && !this.fullSize) {
return `${baseURL}/api/preview/big${url.encodePath(this.req.path)}?auth=${this.jwt}`
}
return `${baseURL}/api/raw${url.encodePath(this.req.path)}?auth=${this.jwt}`
},
raw () {
return `${this.previewUrl}&inline=true`
},
showMore () {
return this.$store.state.show === 'more'
},
isResizeEnabled () {
return resizePreview
}
},
watch: {
$route: function () {
this.updatePreview()
}
},
async mounted () {
window.addEventListener('keydown', this.key)
this.$store.commit('setPreviewMode', true)
this.listing = this.oldReq.items
this.$root.$on('preview-deleted', this.deleted)
this.updatePreview()
},
beforeDestroy () {
window.removeEventListener('keydown', this.key)
this.$store.commit('setPreviewMode', false)
this.$root.$off('preview-deleted', this.deleted)
},
methods: {
deleted () {
this.listing = this.listing.filter(item => item.name !== this.name)
if (this.hasNext) {
this.next()
} else if (!this.hasPrevious && !this.hasNext) {
this.back()
} else {
this.prev()
}
},
back () {
this.$store.commit('setPreviewMode', false)
let uri = url.removeLastDir(this.$route.path) + '/'
this.$router.push({ path: uri })
},
prev () {
this.$router.push({ path: this.previousLink })
},
next () {
this.$router.push({ path: this.nextLink })
},
key (event) {
if (this.show !== null) {
return
}
if (event.which === 13 || event.which === 39) { // right arrow
if (this.hasNext) this.next()
} else if (event.which === 37) { // left arrow
if (this.hasPrevious) this.prev()
} else if (event.which === 27) { // esc
this.back()
}
},
async updatePreview () {
if (this.req.subtitles) {
this.subtitles = this.req.subtitles.map(sub => `${baseURL}/api/raw${sub}?auth=${this.jwt}&inline=true`)
}
let dirs = this.$route.fullPath.split("/")
this.name = decodeURIComponent(dirs[dirs.length - 1])
if (!this.listing) {
try {
const path = url.removeLastDir(this.$route.path)
const res = await api.fetch(path)
this.listing = res.items
} catch (e) {
this.$showError(e)
}
}
this.previousLink = ''
this.nextLink = ''
for (let i = 0; i < this.listing.length; i++) {
if (this.listing[i].name !== this.name) {
continue
}
for (let j = i - 1; j >= 0; j--) {
if (mediaTypes.includes(this.listing[j].type)) {
this.previousLink = this.listing[j].url
break
}
}
for (let j = i + 1; j < this.listing.length; j++) {
if (mediaTypes.includes(this.listing[j].type)) {
this.nextLink = this.listing[j].url
break
}
}
return
}
},
openMore () {
this.$store.commit('showHover', 'more')
},
resetPrompts () {
this.$store.commit('closeHovers')
},
toggleSize () {
this.fullSize = !this.fullSize
}
}
}
</script>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,49 +1,43 @@
<template> <template>
<div class="card floating" id="download"> <div class="card floating" id="download">
<div class="card-title"> <div class="card-title">
<h2>{{ $t('prompts.download') }}</h2> <h2>{{ $t("prompts.download") }}</h2>
</div> </div>
<div class="card-content"> <div class="card-content">
<p>{{ $t('prompts.downloadMessage') }}</p> <p>{{ $t("prompts.downloadMessage") }}</p>
<button class="button button--block" @click="download('zip')" v-focus>zip</button> <button
<button class="button button--block" @click="download('tar')" v-focus>tar</button> v-for="(ext, format) in formats"
<button class="button button--block" @click="download('targz')" v-focus>tar.gz</button> :key="format"
<button class="button button--block" @click="download('tarbz2')" v-focus>tar.bz2</button> class="button button--block"
<button class="button button--block" @click="download('tarxz')" v-focus>tar.xz</button> @click="showConfirm(format)"
<button class="button button--block" @click="download('tarlz4')" v-focus>tar.lz4</button> v-focus
<button class="button button--block" @click="download('tarsz')" v-focus>tar.sz</button> >
{{ ext }}
</button>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import {mapGetters, mapState} from 'vuex' import { mapState } from "vuex";
import { files as api } from '@/api'
export default { export default {
name: 'download', name: "download",
computed: { data: function () {
...mapState(['selected', 'req']), return {
...mapGetters(['selectedCount']) formats: {
zip: "zip",
tar: "tar",
targz: "tar.gz",
tarbz2: "tar.bz2",
tarxz: "tar.xz",
tarlz4: "tar.lz4",
tarsz: "tar.sz",
},
};
}, },
methods: { computed: mapState(["showConfirm"]),
download: function (format) { };
if (this.selectedCount === 0) {
api.download(format, this.$route.path)
} else {
let files = []
for (let i of this.selected) {
files.push(this.req.items[i].url)
}
api.download(format, ...files)
}
this.$store.commit('closeHovers')
}
}
}
</script> </script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,47 +1,41 @@
<template> <template>
<div class="card floating"> <div class="card floating">
<div class="card-content"> <div class="card-content">
<p>{{ $t('prompts.deleteMessageShare', {path: hash.path}) }}</p> <p>{{ $t("prompts.deleteMessageShare", { path: "" }) }}</p>
</div> </div>
<div class="card-action"> <div class="card-action">
<button @click="$store.commit('closeHovers')" <button
@click="$store.commit('closeHovers')"
class="button button--flat button--grey" class="button button--flat button--grey"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> :title="$t('buttons.cancel')"
<button @click="submit" >
{{ $t("buttons.cancel") }}
</button>
<button
@click="submit"
class="button button--flat button--red" class="button button--flat button--red"
:aria-label="$t('buttons.delete')" :aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button> :title="$t('buttons.delete')"
>
{{ $t("buttons.delete") }}
</button>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import {mapMutations, mapState} from 'vuex' import { mapState } from "vuex";
import { share as api } from '@/api'
import buttons from '@/utils/buttons'
export default { export default {
name: 'share-delete', name: "share-delete",
computed: { computed: {
...mapState(['hash']) ...mapState(["showConfirm"]),
}, },
methods: { methods: {
...mapMutations(['closeHovers']), submit: function () {
submit: async function () { this.showConfirm();
buttons.loading('delete') },
},
try { };
await api.remove(this.hash.hash)
buttons.success('delete')
this.$root.$emit('share-deleted', this.hash.hash)
this.closeHovers()
} catch (e) {
buttons.done('delete')
this.$showError(e)
}
}
}
}
</script> </script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -34,7 +34,7 @@
} }
.share__box__info { .share__box__info {
flex: 1 1 auto; flex: 1 1 18em;
} }
.share__box__element { .share__box__element {
@@ -43,6 +43,15 @@
word-break: break-all; word-break: break-all;
} }
.share__box__element .button {
display: inline-block;
}
.share__box__element .button i {
display: block;
margin-bottom: 4px;
}
.share__box__items { .share__box__items {
text-align: left; text-align: left;
flex: 10 0 25em; flex: 10 0 25em;
@@ -70,9 +79,4 @@
padding: .5em; padding: .5em;
text-align: center; text-align: center;
animation: .2s opac forwards; animation: .2s opac forwards;
} }
.share__promt__card {
max-width: max-content !important;
width: auto !important;
}

View File

@@ -83,29 +83,29 @@ main {
width: calc(100% - 19em); width: calc(100% - 19em);
} }
#breadcrumbs { .breadcrumbs {
height: 3em; height: 3em;
border-bottom: 1px solid rgba(0, 0, 0, 0.05); border-bottom: 1px solid rgba(0, 0, 0, 0.05);
} }
#breadcrumbs span, .breadcrumbs span,
#breadcrumbs { .breadcrumbs {
display: flex; display: flex;
align-items: center; align-items: center;
color: #6f6f6f; color: #6f6f6f;
} }
#breadcrumbs a { .breadcrumbs a {
color: inherit; color: inherit;
transition: .1s ease-in; transition: .1s ease-in;
border-radius: .125em; border-radius: .125em;
} }
#breadcrumbs a:hover { .breadcrumbs a:hover {
background-color: rgba(0,0,0, 0.05); background-color: rgba(0,0,0, 0.05);
} }
#breadcrumbs span a { .breadcrumbs span a {
padding: .2em; padding: .2em;
} }

View File

@@ -1,8 +1,29 @@
.dashboard { .dashboard {
max-width: 600px;
margin: 1em 0; margin: 1em 0;
} }
.dashboard .row {
display: flex;
margin: 0 -.5em;
flex-wrap: wrap;
}
.dashboard .row .column {
display: flex;
padding: 0 .5em;
width: 50%;
}
.dashboard .row .column .card {
flex-grow: 1;
}
@media(max-width: 1200px) {
.dashboard .row .column {
width: 100%;
}
}
a { a {
color: inherit color: inherit
} }
@@ -28,25 +49,56 @@ p code {
} }
.dashboard #nav { .dashboard #nav {
display: flex;
padding-bottom: 1em;
overflow: auto;
}
.dashboard #nav .wrapper {
display: flex;
flex-grow: 1;
border-bottom: 2px solid rgba(0, 0, 0, 0.05);
}
.dashboard #nav ul {
list-style: none; list-style: none;
display: flex; display: flex;
color: rgb(84, 110, 122); color: rgb(84, 110, 122);
font-weight: 500; font-weight: 500;
margin: 0 0 1em; padding: 0;
margin: 0 0 -2px 0;
font-size: .8em; font-size: .8em;
text-align: center; text-align: center;
justify-content: space-between; justify-content: left;
padding: 0;
} }
.dashboard #nav li { .dashboard #nav ul li {
position: relative;
padding: 1.5em 2em;
white-space: nowrap;
border-bottom: 2px solid transparent;
transition: .1s ease-in-out all;
}
.dashboard #nav ul li:hover {
background: var(--moon-grey);
}
.dashboard #nav ul li.active {
border-color: var(--blue);
color: var(--blue);
}
.dashboard #nav ul li.active::before {
width: 100%; width: 100%;
padding: 0 0 1em; height: 100%;
border-bottom: 2px solid rgba(0, 0, 0, 0.05); position: absolute;
} top: 0;
left: 0;
.dashboard #nav li.active { content: "";
border-color: var(--blue) background: var(--blue);
opacity: 0.08;
} }
.dashboard #nav i { .dashboard #nav i {
@@ -92,7 +144,7 @@ table tr>*:last-child {
.card { .card {
position: relative; position: relative;
margin: .5rem 0 1rem 0; margin: 0 0 1rem 0;
background-color: #fff; background-color: #fff;
border-radius: 2px; border-radius: 2px;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
@@ -151,6 +203,7 @@ table tr>*:last-child {
.card .card-content.full { .card .card-content.full {
padding-bottom: 0; padding-bottom: 0;
overflow: auto;
} }
.card h2 { .card h2 {
@@ -226,6 +279,18 @@ table tr>*:last-child {
opacity: 1; opacity: 1;
} }
.card#share .input-group {
display: flex;
}
.card#share .input-group * {
border: none;
}
.card#share .input-group input {
flex: 1;
}
.overlay { .overlay {
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
position: fixed; position: fixed;

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; 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"; @import "~material-design-icons/iconfont/material-icons.css";

View File

@@ -6,9 +6,25 @@ header {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
height: 4em;
width: 100%; width: 100%;
padding: 0; padding: 0;
display: flex; display: flex;
padding: 0.5em 0.5em 0.5em 1em;
align-items: center;
}
header > * {
flex: 0 0 auto;
}
header title {
display: block;
flex: 1 1 auto;
padding: 0 1em;
overflow: hidden;
text-overflow: ellipsis;
font-size: 1.2em;
} }
header .overlay { header .overlay {
@@ -30,17 +46,6 @@ header img {
height: 2.5em; height: 2.5em;
} }
header>div:first-child>.action {
display: none;
}
header>div {
display: flex;
width: 100%;
padding: 0.5em 0.5em 0.5em 1em;
align-items: center;
}
header .action span { header .action span {
display: none; display: none;
} }
@@ -50,19 +55,8 @@ header>div div {
position: relative; position: relative;
} }
header>div:last-child div { header .search-button,
display: flex; header .menu-button {
}
header>div:first-child {
height: 4em;
}
header>div:last-child {
justify-content: flex-end;
}
header .search-button {
display: none; display: none;
} }

View File

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

View File

@@ -70,6 +70,7 @@
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px; box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
width: 95%; width: 95%;
max-width: 20em; max-width: 20em;
z-index: 1;
} }
#file-selection .action { #file-selection .action {
border-radius: 50%; border-radius: 50%;
@@ -81,6 +82,9 @@
color: #6f6f6f; color: #6f6f6f;
margin-right: auto; margin-right: auto;
} }
#file-selection .action span {
display: none;
}
nav { nav {
top: 0; top: 0;
z-index: 99999; z-index: 99999;
@@ -95,7 +99,7 @@
left: 0; left: 0;
} }
header .search-button, header .search-button,
header>div:first-child>.action { header .menu-button {
display: inherit; display: inherit;
} }
header img { header img {

View File

@@ -17,6 +17,48 @@
color: var(--blue); 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 * * ACTION *
* * * * * * * * * * * * * * * */ * * * * * * * * * * * * * * * */
@@ -96,10 +138,11 @@
color: #fff; color: #fff;
border-radius: 50%; border-radius: 50%;
font-size: .75em; font-size: .75em;
width: 1.5em; width: 1.8em;
height: 1.5em; height: 1.8em;
text-align: center; text-align: center;
line-height: 1.25em; line-height: 1.55em;
font-weight: bold;
border: 2px solid white; border: 2px solid white;
} }
@@ -108,6 +151,7 @@
#previewer { #previewer {
background-color: rgba(0, 0, 0, 0.9); background-color: rgba(0, 0, 0, 0.9);
padding-top: 4em;
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
@@ -117,45 +161,32 @@
overflow: hidden; overflow: hidden;
} }
#previewer .bar { #previewer header {
width: 100%; background: none;
display: flex;
padding: 0.5em;
height: 3.7em;
}
#previewer .bar > * {
flex: 0 0 auto;
}
#previewer .bar .title {
display: block;
flex: 1 1 auto;
padding: 0 1em;
line-height: 2.3em;
overflow: hidden;
text-overflow: ellipsis;
font-size: 1.2em;
color: #fff; color: #fff;
} }
#previewer .action i { #previewer header > .action i {
color: #fff; color: #fff;
} }
#previewer .action:hover { @media (min-width: 738px) {
#previewer header #dropdown .action i {
color: #fff;
}
}
#previewer header .action:hover {
background-color: rgba(255, 255, 255, 0.3) background-color: rgba(255, 255, 255, 0.3)
} }
#previewer .action span { #previewer header .action span {
display: none; display: none;
} }
#previewer .preview { #previewer .preview {
margin: 2em auto 4em;
max-width: 80%;
text-align: center; text-align: center;
height: calc(100vh - 9.7em); height: calc(100vh - 4em);
} }
#previewer .preview pre { #previewer .preview pre {
@@ -170,6 +201,38 @@
margin: 0; margin: 0;
} }
#previewer .preview video {
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 { #previewer .pdf {
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -182,8 +245,25 @@
#previewer>button { #previewer>button {
margin: 0; margin: 0;
position: fixed; position: fixed;
top: 50%; top: calc(50% + 1.85em);
transform: translateY(-50%); transform: translateY(-50%);
background-color: rgba(80, 80, 80, .5);
color: white;
border-radius: 50%;
cursor: pointer;
border: 0;
margin: 0;
padding: 0;
transition: 0.2s ease all;
}
#previewer>button.hidden {
opacity: 0;
visibility: hidden;
}
#previewer>button>i {
padding: 0.4em;
} }
#previewer>button:first-of-type { #previewer>button:first-of-type {
@@ -194,11 +274,26 @@
right: 0.5em; 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 */
#editor-container { #editor-container {
background-color: #fafafa; background-color: #fafafa;
position: fixed; position: fixed;
padding-top: 4em;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
@@ -206,43 +301,23 @@
overflow: hidden; overflow: hidden;
} }
#editor-container .bar {
width: 100%;
text-align: right;
display: flex;
padding: 0.5em;
height: 3.7em;
background-color: #fff;
border-bottom: 1px solid rgba(0, 0, 0, 0.075);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
}
#editor-container .title {
margin-right: auto;
padding: 0 1em;
line-height: 2.7em;
overflow: hidden;
word-break: break-word;
}
#previewer .loading {
height: 100%;
width: 100%;
}
#editor-container #editor { #editor-container #editor {
height: calc(100vh - 8.2em); height: calc(100vh - 8.4em);
} }
#editor-container #breadcrumbs { #editor-container .breadcrumbs {
height: 2.3em; height: 2.3em;
padding: 0 1em; padding: 0 1em;
} }
#editor-container #breadcrumbs span { #editor-container .breadcrumbs span {
font-size: 12px; font-size: 12px;
} }
#editor-container .breadcrumbs i {
font-size: 16px;
}
/* * * * * * * * * * * * * * * * /* * * * * * * * * * * * * * * *
* PROMPT * * PROMPT *
* * * * * * * * * * * * * * * */ * * * * * * * * * * * * * * * */
@@ -287,7 +362,6 @@
@keyframes spin { @keyframes spin {
100% { 100% {
-webkit-transform: rotate(-360deg);
transform: rotate(-360deg); transform: rotate(-360deg);
} }
} }

View File

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

View File

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

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