Compare commits
297 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ce44f7092 | ||
|
|
b320419088 | ||
|
|
ca183a4fb8 | ||
|
|
895bb755cd | ||
|
|
a9e715dc50 | ||
|
|
7cb046c542 | ||
|
|
cd03faf0fc | ||
|
|
87ba03b224 | ||
|
|
6458f91e1c | ||
|
|
312ebbbcc0 | ||
|
|
060a7ad80c | ||
|
|
ae893abc5f | ||
|
|
12d6415f7f | ||
|
|
897ac75281 | ||
|
|
cec551c3de | ||
|
|
cb98c913d4 | ||
|
|
55a9d945cc | ||
|
|
cc7ec4f0c5 | ||
|
|
265b81a52b | ||
|
|
b42b09ccbe | ||
|
|
118071ba4b | ||
|
|
73b85eced4 | ||
|
|
997a0a433f | ||
|
|
0d7e344ca3 | ||
|
|
1884d50c3b | ||
|
|
f5fad7a01d | ||
|
|
5c2ed2b2f9 | ||
|
|
05475eb4fc | ||
|
|
9e6cc302c0 | ||
|
|
d422421cf9 | ||
|
|
23a3ef069e | ||
|
|
2a81ea90db | ||
|
|
5fb7207d65 | ||
|
|
d79d864825 | ||
|
|
d249b8b202 | ||
|
|
e9bd68f3b0 | ||
|
|
506e088236 | ||
|
|
c906d296be | ||
|
|
3b7f6ccf8e | ||
|
|
f1a7d2f8d0 | ||
|
|
fb13ded8e8 | ||
|
|
85e4ff67e4 | ||
|
|
6250efa208 | ||
|
|
f1e1a27408 | ||
|
|
076358ab79 | ||
|
|
d1efc14bb9 | ||
|
|
508b7b326f | ||
|
|
d1284972a3 | ||
|
|
cdba1d0c52 | ||
|
|
ec28375208 | ||
|
|
01068a9217 | ||
|
|
7f01753bc5 | ||
|
|
0e223a056e | ||
|
|
9d08f9bed1 | ||
|
|
2cabeb8f68 | ||
|
|
7aaebab348 | ||
|
|
928cdfe2ae | ||
|
|
edb7b4dc17 | ||
|
|
85ee63af43 | ||
|
|
74b23a0bc5 | ||
|
|
be6c0bb850 | ||
|
|
ddb670ae1e | ||
|
|
4752758cf7 | ||
|
|
bd8aab4cba | ||
|
|
c61ede4153 | ||
|
|
efd46d6bd3 | ||
|
|
f2a8abb264 | ||
|
|
978aadc9b1 | ||
|
|
0626f07270 | ||
|
|
0afc8c9e5c | ||
|
|
28480c6acd | ||
|
|
826cdddca7 | ||
|
|
ec92ad9f47 | ||
|
|
ce97b9b9fd | ||
|
|
bbd93e111d | ||
|
|
f7c7d50e54 | ||
|
|
854d8bb705 | ||
|
|
7c0f261a97 | ||
|
|
7b9861b2c6 | ||
|
|
3a8fcbf4bc | ||
|
|
83d9247df6 | ||
|
|
d32286a13d | ||
|
|
621936f461 | ||
|
|
802e715fae | ||
|
|
adcafff384 | ||
|
|
a5ce1cf1e1 | ||
|
|
87d18a3089 | ||
|
|
e7fc0e97d6 | ||
|
|
34bdb8fcfc | ||
|
|
13b04f7672 | ||
|
|
76b9b2fa37 | ||
|
|
896d7cfbed | ||
|
|
0fb1b0840f | ||
|
|
1c0250075b | ||
|
|
cf2af817b9 | ||
|
|
ec24f79204 | ||
|
|
be902be453 | ||
|
|
888e08792e | ||
|
|
adc6ef22d9 | ||
|
|
0318d39112 | ||
|
|
abcfa0a05b | ||
|
|
a4b5c99ebb | ||
|
|
546e61a403 | ||
|
|
dcb68bd7a8 | ||
|
|
d411720234 | ||
|
|
1ae887d77c | ||
|
|
30465a771e | ||
|
|
f004b48b99 | ||
|
|
fc5e2247f6 | ||
|
|
5956647bd0 | ||
|
|
87eaf3ce5c | ||
|
|
73eba60210 | ||
|
|
4597ceb3a6 | ||
|
|
c0c25344c8 | ||
|
|
5efb36103f | ||
|
|
c0575a68ee | ||
|
|
ffd8a3a637 | ||
|
|
425ec275e9 | ||
|
|
3b9f336634 | ||
|
|
f792c31046 | ||
|
|
55a54ff89e | ||
|
|
78a40c9b14 | ||
|
|
17f32d16cc | ||
|
|
d57a0f2ae1 | ||
|
|
d6fdfef243 | ||
|
|
62d28dc89e | ||
|
|
57c65156f7 | ||
|
|
6e54dff40d | ||
|
|
0e722c8df1 | ||
|
|
f05479865c | ||
|
|
4e4055e7a8 | ||
|
|
7414ca10b3 | ||
|
|
1e17dfa6cb | ||
|
|
64d6d9e93b | ||
|
|
68902312cc | ||
|
|
a52b50b706 | ||
|
|
2527bdbfe1 | ||
|
|
473aaf13be | ||
|
|
0844b597f8 | ||
|
|
d45d7f92fb | ||
|
|
b3a822b4e8 | ||
|
|
788fadbd5e | ||
|
|
40f29e1e9b | ||
|
|
a036a25e1d | ||
|
|
abed362dc5 | ||
|
|
ce78299464 | ||
|
|
030f6607f0 | ||
|
|
c3a4e33245 | ||
|
|
aabf0843ab | ||
|
|
748e4acfb6 | ||
|
|
6e48a6b512 | ||
|
|
88500ab219 | ||
|
|
d0f8c141e1 | ||
|
|
34a1bf1380 | ||
|
|
b87ba12a7d | ||
|
|
bb0d048235 | ||
|
|
b991c65d8b | ||
|
|
b3b5db351f | ||
|
|
9562e06b92 | ||
|
|
7fc4899507 | ||
|
|
d649ae6ff7 | ||
|
|
633579e738 | ||
|
|
a65cb32d70 | ||
|
|
ead7fb4233 | ||
|
|
31b70a7736 | ||
|
|
bbeadee98e | ||
|
|
df42e352f7 | ||
|
|
1f985fe72f | ||
|
|
575296d7fc | ||
|
|
b93dc9f200 | ||
|
|
6255f737ba | ||
|
|
3ed2144a0e | ||
|
|
a437761d03 | ||
|
|
b432e1bf46 | ||
|
|
8dd59e3e67 | ||
|
|
fd5543407a | ||
|
|
48d012ff92 | ||
|
|
2c4eae5ca2 | ||
|
|
629646122f | ||
|
|
d4f284f1a3 | ||
|
|
ff3b5b39a5 | ||
|
|
9667980f2d | ||
|
|
188a34f835 | ||
|
|
f9cd5f11d9 | ||
|
|
adedf0178b | ||
|
|
4e15b82896 | ||
|
|
ed0ea34161 | ||
|
|
e2ffd36073 | ||
|
|
6bd2a1852f | ||
|
|
371236e364 | ||
|
|
6cbdc9d7c5 | ||
|
|
a94125f3f2 | ||
|
|
2f5f5d75a7 | ||
|
|
02f2284f3b | ||
|
|
2b1305a315 | ||
|
|
ec78f67abd | ||
|
|
25c04af500 | ||
|
|
c0391d866e | ||
|
|
dcb97be587 | ||
|
|
c6eb98aef2 | ||
|
|
1c6e15c064 | ||
|
|
711a3a30b0 | ||
|
|
e203ca14f4 | ||
|
|
0f1b69b625 | ||
|
|
176eaad70b | ||
|
|
486dfe4e63 | ||
|
|
81cf4bab99 | ||
|
|
9c3f563f83 | ||
|
|
6b42781c21 | ||
|
|
891a0d1bd0 | ||
|
|
3b9063dc63 | ||
|
|
2bfdffb9c4 | ||
|
|
9f8685bf10 | ||
|
|
b58bc414bf | ||
|
|
0b81723118 | ||
|
|
66418ec064 | ||
|
|
d87640a4f1 | ||
|
|
e5580ac0c4 | ||
|
|
b92c800e00 | ||
|
|
e370fbe500 | ||
|
|
89d4d828b9 | ||
|
|
d004015f03 | ||
|
|
ba5b5fbfe3 | ||
|
|
dd29a87107 | ||
|
|
b394540f53 | ||
|
|
3ed5f8c0bd | ||
|
|
5d5cef2a87 | ||
|
|
9264e344d7 | ||
|
|
0a46ac3e1b | ||
|
|
a438fc746f | ||
|
|
0c8ffaf73e | ||
|
|
6c1bbb3248 | ||
|
|
e663c60a89 | ||
|
|
e1e8979e0b | ||
|
|
de53b24536 | ||
|
|
bc518a0e82 | ||
|
|
2ed87febcb | ||
|
|
ee169b3a46 | ||
|
|
90d690c187 | ||
|
|
0e7d4ef110 | ||
|
|
7a6397af22 | ||
|
|
ac512612e7 | ||
|
|
95fc3dfdfb | ||
|
|
39be89780e | ||
|
|
2642333928 | ||
|
|
7e1d745435 | ||
|
|
218e638f88 | ||
|
|
cad2a989c1 | ||
|
|
f844aeb2b4 | ||
|
|
7847763a31 | ||
|
|
3f49bc382e | ||
|
|
3ae9e518a3 | ||
|
|
1e5ced6737 | ||
|
|
fa67652ba4 | ||
|
|
62106cc0a4 | ||
|
|
99740e3eab | ||
|
|
c12adbb594 | ||
|
|
157b2da133 | ||
|
|
208f21728f | ||
|
|
0377080da6 | ||
|
|
f5c48c9679 | ||
|
|
99ef1308ea | ||
|
|
9f075c16c5 | ||
|
|
2f17f19425 | ||
|
|
06f00e9664 | ||
|
|
c681174adf | ||
|
|
4a12ce1888 | ||
|
|
4e39f2387a | ||
|
|
e354098b96 | ||
|
|
9086720c3c | ||
|
|
2bb9171e32 | ||
|
|
294efef38c | ||
|
|
4f1d25fba7 | ||
|
|
d3e363a4d5 | ||
|
|
a10f286f0f | ||
|
|
390c53097f | ||
|
|
4c30b2c665 | ||
|
|
325e6e0904 | ||
|
|
56ec440272 | ||
|
|
0af5e07eed | ||
|
|
8a764ceb67 | ||
|
|
fe829aa850 | ||
|
|
2ca22656d6 | ||
|
|
cb7fa99fd3 | ||
|
|
441639a8d5 | ||
|
|
a78e1d504b | ||
|
|
02f6b0ec61 | ||
|
|
8c60cc7084 | ||
|
|
a7e4596e97 | ||
|
|
97d53ceb2e | ||
|
|
a49fb20885 | ||
|
|
2d5e97e140 | ||
|
|
ebdf5a0601 | ||
|
|
9ca02c90ed | ||
|
|
0595638228 | ||
|
|
1f4d0cc3cd | ||
|
|
e6c0d1c28a |
@@ -11,11 +11,6 @@ jobs:
|
|||||||
- image: circleci/node
|
- image: circleci/node
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- run:
|
|
||||||
name: "Pull Submodules"
|
|
||||||
command: |
|
|
||||||
git submodule init
|
|
||||||
git submodule update --remote
|
|
||||||
- run:
|
- run:
|
||||||
name: "Build"
|
name: "Build"
|
||||||
command: ./wizard.sh -a
|
command: ./wizard.sh -a
|
||||||
@@ -34,7 +29,7 @@ jobs:
|
|||||||
at: '~/project'
|
at: '~/project'
|
||||||
- run:
|
- run:
|
||||||
name: "Compile"
|
name: "Compile"
|
||||||
command: ./wizard.sh -c
|
command: GOOS=linux GOARCH=amd64 ./wizard.sh -c
|
||||||
- run:
|
- run:
|
||||||
name: "Cleanup"
|
name: "Cleanup"
|
||||||
command: |
|
command: |
|
||||||
@@ -44,21 +39,6 @@ jobs:
|
|||||||
root: .
|
root: .
|
||||||
paths:
|
paths:
|
||||||
- '*'
|
- '*'
|
||||||
docker-latest:
|
|
||||||
docker:
|
|
||||||
- image: docker
|
|
||||||
steps:
|
|
||||||
- attach_workspace:
|
|
||||||
at: '~/project'
|
|
||||||
- setup_remote_docker
|
|
||||||
- run: docker build -t filebrowser/filebrowser .
|
|
||||||
- run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
|
|
||||||
- run: docker push filebrowser/filebrowser
|
|
||||||
- run: docker logout
|
|
||||||
- persist_to_workspace:
|
|
||||||
root: .
|
|
||||||
paths:
|
|
||||||
- '*'
|
|
||||||
release:
|
release:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/golang:1.12
|
- image: circleci/golang:1.12
|
||||||
@@ -96,13 +76,4 @@ workflows:
|
|||||||
tags:
|
tags:
|
||||||
only: /^v.*/
|
only: /^v.*/
|
||||||
branches:
|
branches:
|
||||||
ignore: /.*/
|
ignore: /.*/
|
||||||
- docker-latest:
|
|
||||||
context: deploy
|
|
||||||
requires:
|
|
||||||
- build-go
|
|
||||||
filters:
|
|
||||||
branches:
|
|
||||||
only: master
|
|
||||||
tags:
|
|
||||||
ignore: /.*/
|
|
||||||
22
.gitignore
vendored
@@ -5,3 +5,25 @@ _old
|
|||||||
rice-box.go
|
rice-box.go
|
||||||
.idea/
|
.idea/
|
||||||
filebrowser
|
filebrowser
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/frontend/dist
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw*
|
||||||
|
|||||||
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
|||||||
[submodule "frontend"]
|
|
||||||
path = frontend
|
|
||||||
url = https://github.com/filebrowser/frontend
|
|
||||||
@@ -1,8 +1,17 @@
|
|||||||
project_name: filebrowser
|
project_name: filebrowser
|
||||||
|
|
||||||
|
env:
|
||||||
|
- GO111MODULE=on
|
||||||
|
|
||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
- go mod download
|
||||||
|
|
||||||
build:
|
build:
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X github.com/filebrowser/filebrowser/v2/version.Version={{ .Version }} -X github.com/filebrowser/filebrowser/v2/version.CommitSHA={{ .ShortCommit }}
|
||||||
main: main.go
|
main: main.go
|
||||||
binary: filebrowser
|
binary: filebrowser
|
||||||
goos:
|
goos:
|
||||||
|
|||||||
10
.tx/config
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[main]
|
||||||
|
host = https://www.transifex.com
|
||||||
|
lang_map = pt_BR: pt-br, zh_CN: zh-cn, zh_HK: zh-hk, zh_TW: zh-tw, nl_BE: nl-be, sv_SE: sv-se
|
||||||
|
|
||||||
|
[file-browser.file-browser]
|
||||||
|
file_filter = frontend/src/i18n/<lang>.json
|
||||||
|
minimum_perc = 50
|
||||||
|
source_file = frontend/src/i18n/en.json
|
||||||
|
source_lang = en
|
||||||
|
type = KEYVALUEJSON
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
FROM alpine:latest as certs
|
FROM alpine:latest as alpine
|
||||||
RUN apk --update add ca-certificates
|
RUN apk --update add ca-certificates
|
||||||
|
RUN apk --update add mailcap
|
||||||
|
|
||||||
FROM scratch
|
FROM scratch
|
||||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||||
|
COPY --from=alpine /etc/mime.types /etc/mime.types
|
||||||
|
|
||||||
VOLUME /srv
|
VOLUME /srv
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|||||||
12
README.md
@@ -2,6 +2,8 @@
|
|||||||
<img src="https://raw.githubusercontent.com/filebrowser/logo/master/banner.png" width="550"/>
|
<img src="https://raw.githubusercontent.com/filebrowser/logo/master/banner.png" width="550"/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
⚠️ WARN: **This project will not be developed anymore. If you're willing to take over this project, please read [#532](https://github.com/filebrowser/filebrowser/issues/532) for more info!**
|
||||||
|
|
||||||

|

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