Merge branch 'master' into add-username-in-sidebar

This commit is contained in:
Jonathan Bout 2024-04-19 23:19:59 +02:00 committed by GitHub
commit 86fef39a40
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
218 changed files with 12846 additions and 8968 deletions

View File

@ -24,7 +24,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version: 1.21.0 go-version: 1.22.1
- run: make lint-backend - run: make lint-backend
lint-commits: lint-commits:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -57,7 +57,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version: 1.21.0 go-version: 1.22.1
- run: make test-backend - run: make test-backend
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -76,7 +76,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version: 1.21.0 go-version: 1.22.1
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: '18' node-version: '18'

9
.gitignore vendored
View File

@ -30,5 +30,14 @@ yarn-error.log*
bin/ bin/
build/ build/
# Vue distributable files
/frontend/dist/* /frontend/dist/*
!/frontend/dist/.gitkeep !/frontend/dist/.gitkeep
# Playwright files
/frontend/test-results/
/frontend/playwright-report/
/frontend/playwright/.cache/
default.nix
Dockerfile.dev

View File

@ -6,8 +6,6 @@ linters-settings:
funlen: funlen:
lines: 100 lines: 100
statements: 50 statements: 50
gci:
local-prefixes: github.com/filebrowser/filebrowser
goconst: goconst:
min-len: 2 min-len: 2
min-occurrences: 2 min-occurrences: 2
@ -29,23 +27,31 @@ linters-settings:
goimports: goimports:
local-prefixes: github.com/filebrowser/filebrowser local-prefixes: github.com/filebrowser/filebrowser
gomnd: gomnd:
settings:
mnd:
# don't include the "operation" and "assign" # don't include the "operation" and "assign"
checks: argument,case,condition,return checks:
- argument
- case
- condition
- return
ignored-numbers:
- '0'
- '1'
- '2'
- '3'
ignored-functions:
- strings.SplitN
govet: govet:
check-shadowing: true enable:
- nilness
- shadow
lll: lll:
line-length: 140 line-length: 140
maligned:
suggest-new: true
misspell: misspell:
locale: US locale: US
nolintlint: nolintlint:
allow-leading-space: true # don't require machine-readable nolint directives (i.e. with no leading space)
allow-unused: false # report any unused nolint directives allow-unused: false # report any unused nolint directives
require-explanation: false # don't require an explanation for nolint directives require-explanation: false # require an explanation for nolint directives
require-specific: false # don't require nolint directives to be specific about which linter is being skipped require-specific: true # require nolint directives to be specific about which linter is being skipped
linters: linters:
# please, do not use `enable-all`: it's deprecated and will be removed soon. # please, do not use `enable-all`: it's deprecated and will be removed soon.
@ -53,17 +59,19 @@ linters:
disable-all: true disable-all: true
enable: enable:
- bodyclose - bodyclose
- deadcode
- dogsled - dogsled
- dupl - dupl
- errcheck - errcheck
- errorlint
- exportloopref - exportloopref
- exhaustive - exhaustive
- funlen - funlen
- gocheckcompilerdirectives
- gochecknoinits - gochecknoinits
- goconst - goconst
- gocritic - gocritic
- gocyclo - gocyclo
- godox
- goimports - goimports
- gomnd - gomnd
- goprintffuncname - goprintffuncname
@ -75,19 +83,21 @@ linters:
- misspell - misspell
- nakedret - nakedret
- nolintlint - nolintlint
- prealloc
- revive
- rowserrcheck - rowserrcheck
- staticcheck - staticcheck
- structcheck
- stylecheck - stylecheck
- testifylint
- typecheck - typecheck
- unconvert - unconvert
- unparam - unparam
- unused - unused
- varcheck
- whitespace - whitespace
- prealloc
issues: issues:
exclude-dirs:
- frontend/
exclude-rules: exclude-rules:
- path: cmd/.*.go - path: cmd/.*.go
linters: linters:
@ -108,13 +118,4 @@ issues:
- gomnd - gomnd
run: run:
go: '1.18' timeout: 5m
skip-dirs:
- frontend/
skip-files:
- http/rice-box.go
# golangci.com configuration
# https://github.com/golangci/golangci/wiki/Configuration
service:
golangci-lint-version: 1.27.x # use the fixed version to not introduce new linters unexpectedly

View File

@ -2,6 +2,40 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [2.28.0](https://github.com/filebrowser/filebrowser/compare/v2.27.0...v2.28.0) (2024-04-01)
### Features
* allow to configure if home directory is automatically created from cli ([#2963](https://github.com/filebrowser/filebrowser/issues/2963)) ([a4b089a](https://github.com/filebrowser/filebrowser/commit/a4b089a6dbf9821ecede428cd7d13e69c8b85231))
* auto hiding header bar in preview to enlarge the preview window ([#3024](https://github.com/filebrowser/filebrowser/issues/3024)) ([d706506](https://github.com/filebrowser/filebrowser/commit/d70650689c34ce9f631fda6a453fd521faef22fa))
* close editor when click escape key ([#2947](https://github.com/filebrowser/filebrowser/issues/2947)) ([70c8261](https://github.com/filebrowser/filebrowser/commit/70c826133b8578b8712e6db8f762a15a076cd9a9))
* enable preview in shared folder ([#3055](https://github.com/filebrowser/filebrowser/issues/3055)) ([4c233c3](https://github.com/filebrowser/filebrowser/commit/4c233c3db39ea5a00d6e602ec0ecbddecb590877))
* focus editor when opened ([#2946](https://github.com/filebrowser/filebrowser/issues/2946)) ([b19710e](https://github.com/filebrowser/filebrowser/commit/b19710efca6daa7af56dc211d0051d500d2eea22))
* freezing the list in the backgroud while previewing a file ([#3004](https://github.com/filebrowser/filebrowser/issues/3004)) ([e167c3e](https://github.com/filebrowser/filebrowser/commit/e167c3e1efed8b16be45d994a8d443fda1d8cf49))
* prompt to confirm discard editor changes ([#2948](https://github.com/filebrowser/filebrowser/issues/2948)) ([fb1a09c](https://github.com/filebrowser/filebrowser/commit/fb1a09c7c172b913c12b30975ca545e505df0c05))
* select multiple files with ctrl even with singleClick option ([#2953](https://github.com/filebrowser/filebrowser/issues/2953)) ([d49c3df](https://github.com/filebrowser/filebrowser/commit/d49c3dfacfc0ff07e620b3ad2700e64927b06235))
### Bug Fixes
* dashboard buttons position in rtl layout ([#2949](https://github.com/filebrowser/filebrowser/issues/2949)) ([2cfee21](https://github.com/filebrowser/filebrowser/commit/2cfee2183c98d0cb67fc4e9788644ed4278e25bc))
* editor discard prompt ([#2990](https://github.com/filebrowser/filebrowser/issues/2990)) ([34a0817](https://github.com/filebrowser/filebrowser/commit/34a08170c894321d49bb843e259a0e59e2245998))
* files and directories are created with the correct permissions ([#2966](https://github.com/filebrowser/filebrowser/issues/2966)) ([5c5ab6b](https://github.com/filebrowser/filebrowser/commit/5c5ab6b8750a5168f0ae2a26bd5de41e0b6d9637))
* fix lint warnings ([#2976](https://github.com/filebrowser/filebrowser/issues/2976)) ([fe5ca74](https://github.com/filebrowser/filebrowser/commit/fe5ca74aa1e4257e5cb36f1de58daa0c3548319f))
* **healthcheck:** use address configured if not empty ([#2938](https://github.com/filebrowser/filebrowser/issues/2938)) ([81cd8fc](https://github.com/filebrowser/filebrowser/commit/81cd8fc6d307b00af278beefcdbad4158a128fea))
* keyboard shortcut to confirm prompts ([#2932](https://github.com/filebrowser/filebrowser/issues/2932)) ([ff9502f](https://github.com/filebrowser/filebrowser/commit/ff9502ff34790c46f31d175911cd51c9b62804fb))
* moment locale ([#2952](https://github.com/filebrowser/filebrowser/issues/2952)) ([883383a](https://github.com/filebrowser/filebrowser/commit/883383a5715d82883c51138dfb547805dfad2a3c))
* shell direction ([#2980](https://github.com/filebrowser/filebrowser/issues/2980)) ([6d7ba65](https://github.com/filebrowser/filebrowser/commit/6d7ba65faf576ee4ed095f3d0c41775b21e498de))
* stay in the same position after renaming or deleting ([#3039](https://github.com/filebrowser/filebrowser/issues/3039)) ([cdf8def](https://github.com/filebrowser/filebrowser/commit/cdf8def3304315bef261da7f52f8599d90b1f0f0))
### Build
* **deps-dev:** bump vite from 4.4.12 to 4.5.2 in /frontend ([#2951](https://github.com/filebrowser/filebrowser/issues/2951)) ([bf36cc0](https://github.com/filebrowser/filebrowser/commit/bf36cc00f1369dd10a422f230ccabcbeefae1517))
* **deps:** bump google.golang.org/protobuf from 1.31.0 to 1.33.0 ([#3045](https://github.com/filebrowser/filebrowser/issues/3045)) ([05bfae2](https://github.com/filebrowser/filebrowser/commit/05bfae264a7a477d1b7db582f06f4efb24d26ec9))
* **deps:** bump google.golang.org/protobuf in /tools ([#3044](https://github.com/filebrowser/filebrowser/issues/3044)) ([7797a4e](https://github.com/filebrowser/filebrowser/commit/7797a4ef18038a877df31bd34f2ebf70d18823f8))
## [2.27.0](https://github.com/filebrowser/filebrowser/compare/v2.26.0...v2.27.0) (2024-01-02) ## [2.27.0](https://github.com/filebrowser/filebrowser/compare/v2.26.0...v2.27.0) (2024-01-02)

View File

@ -2,6 +2,7 @@ package auth
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
@ -9,7 +10,7 @@ import (
"os/exec" "os/exec"
"strings" "strings"
"github.com/filebrowser/filebrowser/v2/errors" fbErrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/files" "github.com/filebrowser/filebrowser/v2/files"
"github.com/filebrowser/filebrowser/v2/settings" "github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users" "github.com/filebrowser/filebrowser/v2/users"
@ -123,10 +124,10 @@ func (a *HookAuth) GetValues(s string) {
// iterate input lines // iterate input lines
for _, val := range strings.Split(s, "\n") { for _, val := range strings.Split(s, "\n") {
v := strings.SplitN(val, "=", 2) //nolint: gomnd v := strings.SplitN(val, "=", 2)
// skips non key and value format // skips non key and value format
if len(v) != 2 { //nolint: gomnd if len(v) != 2 {
continue continue
} }
@ -144,7 +145,7 @@ func (a *HookAuth) GetValues(s string) {
// SaveUser updates the existing user or creates a new one when not found // SaveUser updates the existing user or creates a new one when not found
func (a *HookAuth) SaveUser() (*users.User, error) { func (a *HookAuth) SaveUser() (*users.User, error) {
u, err := a.Users.Get(a.Server.Root, a.Cred.Username) u, err := a.Users.Get(a.Server.Root, a.Cred.Username)
if err != nil && err != errors.ErrNotExist { if err != nil && !errors.Is(err, fbErrors.ErrNotExist) {
return nil, err return nil, err
} }

View File

@ -26,7 +26,7 @@ type JSONAuth struct {
} }
// Auth authenticates the user via a json in content body. // Auth authenticates the user via a json in content body.
func (a JSONAuth) Auth(r *http.Request, usr users.Store, stg *settings.Settings, srv *settings.Server) (*users.User, error) { func (a JSONAuth) Auth(r *http.Request, usr users.Store, _ *settings.Settings, srv *settings.Server) (*users.User, error) {
var cred jsonCred var cred jsonCred
if r.Body == nil { if r.Body == nil {
@ -39,7 +39,7 @@ func (a JSONAuth) Auth(r *http.Request, usr users.Store, stg *settings.Settings,
} }
// If ReCaptcha is enabled, check the code. // If ReCaptcha is enabled, check the code.
if a.ReCaptcha != nil && len(a.ReCaptcha.Secret) > 0 { if a.ReCaptcha != nil && a.ReCaptcha.Secret != "" {
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:govet ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:govet
if err != nil { if err != nil {

View File

@ -14,7 +14,7 @@ const MethodNoAuth settings.AuthMethod = "noauth"
type NoAuth struct{} type NoAuth struct{}
// Auth uses authenticates user 1. // Auth uses authenticates user 1.
func (a NoAuth) Auth(r *http.Request, usr users.Store, stg *settings.Settings, srv *settings.Server) (*users.User, error) { func (a NoAuth) Auth(_ *http.Request, usr users.Store, _ *settings.Settings, srv *settings.Server) (*users.User, error) {
return usr.Get(srv.Root, uint(1)) return usr.Get(srv.Root, uint(1))
} }

View File

@ -1,10 +1,11 @@
package auth package auth
import ( import (
"errors"
"net/http" "net/http"
"os" "os"
"github.com/filebrowser/filebrowser/v2/errors" fbErrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/settings" "github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users" "github.com/filebrowser/filebrowser/v2/users"
) )
@ -18,10 +19,10 @@ type ProxyAuth struct {
} }
// Auth authenticates the user via an HTTP header. // Auth authenticates the user via an HTTP header.
func (a ProxyAuth) Auth(r *http.Request, usr users.Store, stg *settings.Settings, srv *settings.Server) (*users.User, error) { func (a ProxyAuth) Auth(r *http.Request, usr users.Store, _ *settings.Settings, srv *settings.Server) (*users.User, error) {
username := r.Header.Get(a.Header) username := r.Header.Get(a.Header)
user, err := usr.Get(srv.Root, username) user, err := usr.Get(srv.Root, username)
if err == errors.ErrNotExist { if errors.Is(err, fbErrors.ErrNotExist) {
return nil, os.ErrPermission return nil, os.ErrPermission
} }

View File

@ -14,8 +14,8 @@ var cmdsAddCmd = &cobra.Command{
Use: "add <event> <command>", Use: "add <event> <command>",
Short: "Add a command to run on a specific event", Short: "Add a command to run on a specific event",
Long: `Add a command to run on a specific event.`, Long: `Add a command to run on a specific event.`,
Args: cobra.MinimumNArgs(2), //nolint:gomnd Args: cobra.MinimumNArgs(2),
Run: python(func(cmd *cobra.Command, args []string, d pythonData) { Run: python(func(_ *cobra.Command, args []string, d pythonData) {
s, err := d.store.Settings.Get() s, err := d.store.Settings.Get()
checkErr(err) checkErr(err)
command := strings.Join(args[1:], " ") command := strings.Join(args[1:], " ")

View File

@ -14,7 +14,7 @@ var cmdsLsCmd = &cobra.Command{
Short: "List all commands for each event", Short: "List all commands for each event",
Long: `List all commands for each event.`, Long: `List all commands for each event.`,
Args: cobra.NoArgs, Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, args []string, d pythonData) { Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
s, err := d.store.Settings.Get() s, err := d.store.Settings.Get()
checkErr(err) checkErr(err)
evt := mustGetString(cmd.Flags(), "event") evt := mustGetString(cmd.Flags(), "event")

View File

@ -23,7 +23,7 @@ You can also specify an optional parameter (index_end) so
you can remove all commands from 'index' to 'index_end', you can remove all commands from 'index' to 'index_end',
including 'index_end'.`, including 'index_end'.`,
Args: func(cmd *cobra.Command, args []string) error { Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.RangeArgs(2, 3)(cmd, args); err != nil { //nolint:gomnd if err := cobra.RangeArgs(2, 3)(cmd, args); err != nil {
return err return err
} }
@ -35,7 +35,7 @@ including 'index_end'.`,
return nil return nil
}, },
Run: python(func(cmd *cobra.Command, args []string, d pythonData) { Run: python(func(_ *cobra.Command, args []string, d pythonData) {
s, err := d.store.Settings.Get() s, err := d.store.Settings.Get()
checkErr(err) checkErr(err)
evt := args[0] evt := args[0]
@ -43,7 +43,7 @@ including 'index_end'.`,
i, err := strconv.Atoi(args[1]) i, err := strconv.Atoi(args[1])
checkErr(err) checkErr(err)
f := i f := i
if len(args) == 3 { //nolint:gomnd if len(args) == 3 {
f, err = strconv.Atoi(args[2]) f, err = strconv.Atoi(args[2])
checkErr(err) checkErr(err)
} }

View File

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

View File

@ -13,7 +13,7 @@ var configCatCmd = &cobra.Command{
Short: "Prints the configuration", Short: "Prints the configuration",
Long: `Prints the configuration.`, Long: `Prints the configuration.`,
Args: cobra.NoArgs, Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, args []string, d pythonData) { Run: python(func(_ *cobra.Command, _ []string, d pythonData) {
set, err := d.store.Settings.Get() set, err := d.store.Settings.Get()
checkErr(err) checkErr(err)
ser, err := d.store.Settings.GetServer() ser, err := d.store.Settings.GetServer()

View File

@ -15,7 +15,7 @@ var configExportCmd = &cobra.Command{
json or yaml file. This exported configuration can be changed, json or yaml file. This exported configuration can be changed,
and imported again with 'config import' command.`, and imported again with 'config import' command.`,
Args: jsonYamlArg, Args: jsonYamlArg,
Run: python(func(cmd *cobra.Command, args []string, d pythonData) { Run: python(func(_ *cobra.Command, args []string, d pythonData) {
settings, err := d.store.Settings.Get() settings, err := d.store.Settings.Get()
checkErr(err) checkErr(err)

View File

@ -34,7 +34,7 @@ database.
The path must be for a json or yaml file.`, The path must be for a json or yaml file.`,
Args: jsonYamlArg, Args: jsonYamlArg,
Run: python(func(cmd *cobra.Command, args []string, d pythonData) { Run: python(func(_ *cobra.Command, args []string, d pythonData) {
var key []byte var key []byte
if d.hadDB { if d.hadDB {
settings, err := d.store.Settings.Get() settings, err := d.store.Settings.Get()

View File

@ -22,7 +22,7 @@ this options can be changed in the future with the command
to the defaults when creating new users and you don't to the defaults when creating new users and you don't
override the options.`, override the options.`,
Args: cobra.NoArgs, Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, args []string, d pythonData) { Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
defaults := settings.UserDefaults{} defaults := settings.UserDefaults{}
flags := cmd.Flags() flags := cmd.Flags()
getUserDefaults(flags, &defaults, true) getUserDefaults(flags, &defaults, true)

View File

@ -16,7 +16,7 @@ var configSetCmd = &cobra.Command{
Long: `Updates the configuration. Set the flags for the options Long: `Updates the configuration. Set the flags for the options
you want to change. Other options will remain unchanged.`, you want to change. Other options will remain unchanged.`,
Args: cobra.NoArgs, Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, args []string, d pythonData) { Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
flags := cmd.Flags() flags := cmd.Flags()
set, err := d.store.Settings.Get() set, err := d.store.Settings.Get()
checkErr(err) checkErr(err)

View File

@ -39,12 +39,12 @@ var docsCmd = &cobra.Command{
Use: "docs", Use: "docs",
Hidden: true, Hidden: true,
Args: cobra.NoArgs, Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, _ []string) {
dir := mustGetString(cmd.Flags(), "path") dir := mustGetString(cmd.Flags(), "path")
generateDocs(rootCmd, dir) generateDocs(rootCmd, dir)
names := []string{} names := []string{}
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { err := filepath.Walk(dir, func(_ string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() { if err != nil || info.IsDir() {
return err return err
} }
@ -101,7 +101,7 @@ func generateMarkdown(cmd *cobra.Command, w io.Writer) {
_, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.UseLine()) _, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.UseLine())
} }
if len(cmd.Example) > 0 { if cmd.Example != "" {
buf.WriteString("## Examples\n\n") buf.WriteString("## Examples\n\n")
_, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.Example) _, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.Example)
} }

View File

@ -17,7 +17,7 @@ var hashCmd = &cobra.Command{
Short: "Hashes a password", Short: "Hashes a password",
Long: `Hashes a password using bcrypt algorithm.`, Long: `Hashes a password using bcrypt algorithm.`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(_ *cobra.Command, args []string) {
pwd, err := users.HashPwd(args[0]) pwd, err := users.HashPwd(args[0])
checkErr(err) checkErr(err)
fmt.Println(pwd) fmt.Println(pwd)

View File

@ -110,7 +110,7 @@ set FB_DATABASE.
Also, if the database path doesn't exist, File Browser will enter into Also, if the database path doesn't exist, File Browser will enter into
the quick setup mode and a new database will be bootstraped and a new the quick setup mode and a new database will be bootstraped and a new
user created with the credentials from options "username" and "password".`, user created with the credentials from options "username" and "password".`,
Run: python(func(cmd *cobra.Command, args []string, d pythonData) { Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
log.Println(cfgFile) log.Println(cfgFile)
if !d.hadDB { if !d.hadDB {
@ -416,7 +416,8 @@ func initConfig() {
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
if err := v.ReadInConfig(); err != nil { if err := v.ReadInConfig(); err != nil {
if _, ok := err.(v.ConfigParseError); ok { var configParseError v.ConfigParseError
if errors.As(err, &configParseError) {
panic(err) panic(err)
} }
cfgFile = "No config file used" cfgFile = "No config file used"

View File

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

View File

@ -13,7 +13,7 @@ var rulesLsCommand = &cobra.Command{
Short: "List global rules or user specific rules", Short: "List global rules or user specific rules",
Long: `List global rules or user specific rules.`, Long: `List global rules or user specific rules.`,
Args: cobra.NoArgs, Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, args []string, d pythonData) { Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
runRules(d.store, cmd, nil, nil) runRules(d.store, cmd, nil, nil)
}, pythonConfig{}), }, pythonConfig{}),
} }

View File

@ -21,7 +21,7 @@ var upgradeCmd = &cobra.Command{
import share links because they are incompatible with import share links because they are incompatible with
this version.`, this version.`,
Args: cobra.NoArgs, Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, _ []string) {
flags := cmd.Flags() flags := cmd.Flags()
oldDB := mustGetString(flags, "old.database") oldDB := mustGetString(flags, "old.database")
oldConf := mustGetString(flags, "old.config") oldConf := mustGetString(flags, "old.config")

View File

@ -26,7 +26,7 @@ var usersCmd = &cobra.Command{
} }
func printUsers(usrs []*users.User) { func printUsers(usrs []*users.User) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) //nolint:gomnd w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tS.Click\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock") fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tS.Click\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock")
for _, u := range usrs { for _, u := range usrs {

View File

@ -15,7 +15,7 @@ var usersAddCmd = &cobra.Command{
Use: "add <username> <password>", Use: "add <username> <password>",
Short: "Create a new user", Short: "Create a new user",
Long: `Create a new user and add it to the database.`, Long: `Create a new user and add it to the database.`,
Args: cobra.ExactArgs(2), //nolint:gomnd Args: cobra.ExactArgs(2),
Run: python(func(cmd *cobra.Command, args []string, d pythonData) { Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
s, err := d.store.Settings.Get() s, err := d.store.Settings.Get()
checkErr(err) checkErr(err)

View File

@ -14,7 +14,7 @@ var usersExportCmd = &cobra.Command{
Long: `Export all users to a json or yaml file. Please indicate the Long: `Export all users to a json or yaml file. Please indicate the
path to the file where you want to write the users.`, path to the file where you want to write the users.`,
Args: jsonYamlArg, Args: jsonYamlArg,
Run: python(func(cmd *cobra.Command, args []string, d pythonData) { Run: python(func(_ *cobra.Command, args []string, d pythonData) {
list, err := d.store.Users.Gets("") list, err := d.store.Users.Gets("")
checkErr(err) checkErr(err)

View File

@ -26,7 +26,7 @@ var usersLsCmd = &cobra.Command{
Run: findUsers, Run: findUsers,
} }
var findUsers = python(func(cmd *cobra.Command, args []string, d pythonData) { var findUsers = python(func(_ *cobra.Command, args []string, d pythonData) {
var ( var (
list []*users.User list []*users.User
user *users.User user *users.User

View File

@ -15,7 +15,7 @@ var usersRmCmd = &cobra.Command{
Short: "Delete a user by username or id", Short: "Delete a user by username or id",
Long: `Delete a user by username or id`, Long: `Delete a user by username or id`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
Run: python(func(cmd *cobra.Command, args []string, d pythonData) { Run: python(func(_ *cobra.Command, args []string, d pythonData) {
username, id := parseUsernameOrID(args[0]) username, id := parseUsernameOrID(args[0])
var err error var err error

View File

@ -15,7 +15,7 @@ func init() {
var versionCmd = &cobra.Command{ 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(_ *cobra.Command, _ []string) {
fmt.Println("File Browser v" + version.Version + "/" + version.CommitSHA) fmt.Println("File Browser v" + version.Version + "/" + version.CommitSHA)
}, },
} }

View File

@ -31,7 +31,7 @@ func New(fs afero.Fs, root string) *FileCache {
} }
} }
func (f *FileCache) Store(ctx context.Context, key string, value []byte) error { func (f *FileCache) Store(_ context.Context, key string, value []byte) error {
mu := f.getScopedLocks(key) mu := f.getScopedLocks(key)
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()
@ -48,7 +48,7 @@ func (f *FileCache) Store(ctx context.Context, key string, value []byte) error {
return nil return nil
} }
func (f *FileCache) Load(ctx context.Context, key string) (value []byte, exist bool, err error) { func (f *FileCache) Load(_ context.Context, key string) (value []byte, exist bool, err error) {
r, ok, err := f.open(key) r, ok, err := f.open(key)
if err != nil || !ok { if err != nil || !ok {
return nil, ok, err return nil, ok, err
@ -62,7 +62,7 @@ func (f *FileCache) Load(ctx context.Context, key string) (value []byte, exist b
return value, true, nil return value, true, nil
} }
func (f *FileCache) Delete(ctx context.Context, key string) error { func (f *FileCache) Delete(_ context.Context, key string) error {
mu := f.getScopedLocks(key) mu := f.getScopedLocks(key)
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()

View File

@ -40,7 +40,7 @@ func TestFileCache(t *testing.T) {
require.False(t, exists) require.False(t, exists)
} }
func checkValue(t *testing.T, ctx context.Context, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) { //nolint:golint func checkValue(t *testing.T, ctx context.Context, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) { //nolint:revive
t.Helper() t.Helper()
// check actual file content // check actual file content
b, err := afero.ReadFile(fs, fileFullPath) b, err := afero.ReadFile(fs, fileFullPath)

View File

@ -11,14 +11,14 @@ func NewNoOp() *NoOp {
return &NoOp{} return &NoOp{}
} }
func (n *NoOp) Store(ctx context.Context, key string, value []byte) error { func (n *NoOp) Store(_ context.Context, _ string, _ []byte) error {
return nil return nil
} }
func (n *NoOp) Load(ctx context.Context, key string) (value []byte, exist bool, err error) { func (n *NoOp) Load(_ context.Context, _ string) (value []byte, exist bool, err error) {
return nil, false, nil return nil, false, nil
} }
func (n *NoOp) Delete(ctx context.Context, key string) error { func (n *NoOp) Delete(_ context.Context, _ string) error {
return nil return nil
} }

View File

@ -6,27 +6,35 @@ import (
"crypto/sha256" "crypto/sha256"
"crypto/sha512" "crypto/sha512"
"encoding/hex" "encoding/hex"
"errors"
"hash" "hash"
"image" "image"
"io" "io"
"io/fs"
"log" "log"
"mime" "mime"
"net/http" "net/http"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"time" "time"
"github.com/spf13/afero" "github.com/spf13/afero"
"github.com/filebrowser/filebrowser/v2/errors" fbErrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/rules" "github.com/filebrowser/filebrowser/v2/rules"
) )
const PermFile = 0644 const PermFile = 0644
const PermDir = 0755 const PermDir = 0755
var (
reSubDirs = regexp.MustCompile("(?i)^sub(s|titles)$")
reSubExts = regexp.MustCompile("(?i)(.vtt|.srt|.ass|.ssa)$")
)
// FileInfo describes a file. // FileInfo describes a file.
type FileInfo struct { type FileInfo struct {
*Listing *Listing
@ -68,7 +76,7 @@ type ImageResolution struct {
// NewFileInfo creates a File object from a path and a given user. This File // NewFileInfo creates a File object from a path and a given user. This File
// object will be automatically filled depending on if it is a directory // object will be automatically filled depending on if it is a directory
// or a file. If it's a video file, it will also detect any subtitles. // or a file. If it's a video file, it will also detect any subtitles.
func NewFileInfo(opts FileOptions) (*FileInfo, error) { func NewFileInfo(opts *FileOptions) (*FileInfo, error) {
if !opts.Checker.Check(opts.Path) { if !opts.Checker.Check(opts.Path) {
return nil, os.ErrPermission return nil, os.ErrPermission
} }
@ -95,7 +103,7 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
return file, err return file, err
} }
func stat(opts FileOptions) (*FileInfo, error) { func stat(opts *FileOptions) (*FileInfo, error) {
var file *FileInfo var file *FileInfo
if lstaterFs, ok := opts.Fs.(afero.Lstater); ok { if lstaterFs, ok := opts.Fs.(afero.Lstater); ok {
@ -158,7 +166,7 @@ func stat(opts FileOptions) (*FileInfo, error) {
// algorithm. The checksums data is saved on File object. // algorithm. The checksums data is saved on File object.
func (i *FileInfo) Checksum(algo string) error { func (i *FileInfo) Checksum(algo string) error {
if i.IsDir { if i.IsDir {
return errors.ErrIsDirectory return fbErrors.ErrIsDirectory
} }
if i.Checksums == nil { if i.Checksums == nil {
@ -184,7 +192,7 @@ func (i *FileInfo) Checksum(algo string) error {
case "sha512": case "sha512":
h = sha512.New() h = sha512.New()
default: default:
return errors.ErrInvalidOption return fbErrors.ErrInvalidOption
} }
_, err = io.Copy(h, reader) _, err = io.Copy(h, reader)
@ -209,8 +217,6 @@ func (i *FileInfo) RealPath() string {
return i.Path return i.Path
} }
// TODO: use constants
//
//nolint:goconst //nolint:goconst
func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error { func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
if IsNamedPipe(i.Mode) { if IsNamedPipe(i.Mode) {
@ -277,8 +283,8 @@ func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
return nil return nil
} }
func calculateImageResolution(fs afero.Fs, filePath string) (*ImageResolution, error) { func calculateImageResolution(fSys afero.Fs, filePath string) (*ImageResolution, error) {
file, err := fs.Open(filePath) file, err := fSys.Open(filePath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -310,7 +316,7 @@ func (i *FileInfo) readFirstBytes() []byte {
buffer := make([]byte, 512) //nolint:gomnd buffer := make([]byte, 512) //nolint:gomnd
n, err := reader.Read(buffer) n, err := reader.Read(buffer)
if err != nil && err != io.EOF { if err != nil && !errors.Is(err, io.EOF) {
log.Print(err) log.Print(err)
i.Type = "blob" i.Type = "blob"
return nil return nil
@ -328,7 +334,6 @@ func (i *FileInfo) detectSubtitles() {
ext := filepath.Ext(i.Path) ext := filepath.Ext(i.Path)
// detect multiple languages. Base*.vtt // detect multiple languages. Base*.vtt
// TODO: give subtitles descriptive names (lang) and track attributes
parentDir := strings.TrimRight(i.Path, i.Name) parentDir := strings.TrimRight(i.Path, i.Name)
var dir []os.FileInfo var dir []os.FileInfo
if len(i.currentDir) > 0 { if len(i.currentDir) > 0 {
@ -343,12 +348,45 @@ func (i *FileInfo) detectSubtitles() {
base := strings.TrimSuffix(i.Name, ext) base := strings.TrimSuffix(i.Name, ext)
for _, f := range dir { for _, f := range dir {
if !f.IsDir() && strings.HasPrefix(f.Name(), base) && strings.HasSuffix(f.Name(), ".vtt") { // load all supported subtitles from subs directories
i.Subtitles = append(i.Subtitles, path.Join(parentDir, f.Name())) // should cover all instances of subtitle distributions
// like tv-shows with multiple episodes in single dir
if f.IsDir() && reSubDirs.MatchString(f.Name()) {
subsDir := path.Join(parentDir, f.Name())
i.loadSubtitles(subsDir, base, true)
} else if isSubtitleMatch(f, base) {
i.addSubtitle(path.Join(parentDir, f.Name()))
} }
} }
} }
func (i *FileInfo) loadSubtitles(subsPath, baseName string, recursive bool) {
dir, err := afero.ReadDir(i.Fs, subsPath)
if err == nil {
for _, f := range dir {
if isSubtitleMatch(f, "") {
i.addSubtitle(path.Join(subsPath, f.Name()))
} else if f.IsDir() && recursive && strings.HasPrefix(f.Name(), baseName) {
subsDir := path.Join(subsPath, f.Name())
i.loadSubtitles(subsDir, baseName, false)
}
}
}
}
func IsSupportedSubtitle(fileName string) bool {
return reSubExts.MatchString(fileName)
}
func isSubtitleMatch(f fs.FileInfo, baseName string) bool {
return !f.IsDir() && strings.HasPrefix(f.Name(), baseName) &&
IsSupportedSubtitle(f.Name())
}
func (i *FileInfo) addSubtitle(fPath string) {
i.Subtitles = append(i.Subtitles, fPath)
}
func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error { func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
afs := &afero.Afero{Fs: i.Fs} afs := &afero.Afero{Fs: i.Fs}
dir, err := afs.ReadDir(i.Path) dir, err := afs.ReadDir(i.Path)

View File

@ -20,7 +20,6 @@ type Listing struct {
//nolint:goconst //nolint:goconst
func (l Listing) ApplySort() { func (l Listing) ApplySort() {
// Check '.Order' to know how to sort // Check '.Order' to know how to sort
// TODO: use enum
if !l.Sorting.Asc { if !l.Sorting.Asc {
switch l.Sorting.By { switch l.Sorting.By {
case "name": case "name":

View File

@ -4,14 +4,21 @@
"node": true "node": true
}, },
"extends": [ "extends": [
"plugin:vue/essential", "plugin:vue/vue3-essential",
"eslint:recommended", "eslint:recommended",
"@vue/eslint-config-typescript",
"@vue/eslint-config-prettier" "@vue/eslint-config-prettier"
], ],
"rules": { "rules": {
"vue/multi-word-component-names": "off", "vue/multi-word-component-names": "off",
"vue/no-reserved-component-names": "warn", "vue/no-mutating-props": [
"vue/no-mutating-props": "warn" "error",
{
"shallowOnly": true
}
]
// no-undef is already included in
// @vue/eslint-config-typescript
}, },
"parserOptions": { "parserOptions": {
"ecmaVersion": "latest", "ecmaVersion": "latest",

View File

@ -187,6 +187,6 @@
</div> </div>
</div> </div>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>

File diff suppressed because it is too large Load Diff

View File

@ -1,64 +1,71 @@
{ {
"name": "filebrowser-frontend", "name": "filebrowser-frontend",
"version": "2.0.0", "version": "3.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"engines": {
"npm": ">=7.0.0",
"node": ">=18.0.0"
},
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"serve": "vite serve", "build": "npm run typecheck && vite build",
"build": "vite build",
"watch": "vite build --watch",
"clean": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitkeep' -exec rm -r {} +", "clean": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitkeep' -exec rm -r {} +",
"lint": "eslint --ext .vue,.js src/", "typecheck": "vue-tsc -p ./tsconfig.json --noEmit",
"lint:fix": "eslint --ext .vue,.js --fix src/", "lint": "npm run typecheck && eslint --ext .vue,.ts src/",
"format": "prettier --write ." "lint:fix": "eslint --ext .vue,.ts --fix src/",
"format": "prettier --write .",
"test": "playwright test"
}, },
"dependencies": { "dependencies": {
"ace-builds": "^1.23.4", "@chenfengyuan/vue-number-input": "^2.0.1",
"clipboard": "^2.0.11", "@vueuse/core": "^10.9.0",
"core-js": "^3.32.0", "@vueuse/integrations": "^10.9.0",
"css-vars-ponyfill": "^2.4.8", "ace-builds": "^1.32.9",
"filesize": "^10.0.8", "core-js": "^3.36.1",
"js-base64": "^3.7.5", "dayjs": "^1.11.10",
"lodash.clonedeep": "^4.5.0", "filesize": "^10.1.1",
"lodash.throttle": "^4.1.1", "js-base64": "^3.7.7",
"material-icons": "^1.13.9", "jwt-decode": "^4.0.0",
"moment": "^2.29.4", "lodash-es": "^4.17.21",
"material-icons": "^1.13.12",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"noty": "^3.2.0-beta", "pinia": "^2.1.7",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
"qrcode.vue": "^1.7.0", "qrcode.vue": "^3.4.1",
"tus-js-client": "^3.1.1", "tus-js-client": "^4.1.0",
"utif": "^3.1.0", "utif": "^3.1.0",
"vue": "^2.7.14", "video.js": "^8.10.0",
"vue-async-computed": "^3.9.0", "videojs-hotkeys": "^0.2.28",
"vue-i18n": "^8.28.2", "videojs-mobile-ui": "^1.1.1",
"vue-lazyload": "^1.3.5", "vue": "^3.4.21",
"vue-router": "^3.6.5", "vue-final-modal": "^4.5.4",
"vue-simple-progress": "^1.1.1", "vue-i18n": "^9.10.2",
"vuex": "^3.6.2", "vue-lazyload": "^3.0.0",
"vuex-router-sync": "^5.0.0", "vue-router": "^4.3.0",
"whatwg-fetch": "^3.6.17" "vue-toastification": "^2.0.0-rc.5"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-legacy": "^4.1.1", "@intlify/unplugin-vue-i18n": "^4.0.0",
"@vitejs/plugin-vue2": "^2.2.0", "@playwright/test": "^1.42.1",
"@vue/eslint-config-prettier": "^8.0.0", "@types/lodash-es": "^4.17.12",
"autoprefixer": "^10.4.14", "@types/node": "^20.12.2",
"eslint": "^8.46.0", "@typescript-eslint/eslint-plugin": "^7.4.0",
"eslint-plugin-prettier": "^5.0.0", "@vitejs/plugin-legacy": "^5.3.2",
"eslint-plugin-vue": "^9.16.1", "@vitejs/plugin-vue": "^5.0.4",
"jsdom": "^22.1.0", "@vue/eslint-config-prettier": "^9.0.0",
"postcss": "^8.4.31", "@vue/eslint-config-typescript": "^13.0.0",
"prettier": "^3.0.1", "autoprefixer": "^10.4.19",
"terser": "^5.19.2", "concurrently": "^8.2.2",
"vite": "^4.5.2", "eslint": "^8.57.0",
"vite-plugin-compression2": "^0.10.3", "eslint-plugin-prettier": "^5.1.3",
"vite-plugin-rewrite-all": "^1.0.1" "eslint-plugin-vue": "^9.24.0",
}, "jsdom": "^24.0.0",
"browserslist": [ "postcss": "^8.4.38",
"> 1%", "prettier": "^3.2.5",
"last 2 versions", "terser": "^5.30.0",
"not ie < 11" "vite": "^5.2.7",
] "vite-plugin-compression2": "^1.0.0",
"vue-tsc": "^2.0.7"
}
} }

View File

@ -0,0 +1,80 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://127.0.0.1:5173",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
/* Set default locale to English (US) */
locale: "en-US",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
// {
// name: "webkit",
// use: { ...devices["Desktop Safari"] },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: "npm run dev",
url: "http://127.0.0.1:5173",
reuseExistingServer: !process.env.CI,
},
});

View File

@ -16,7 +16,7 @@
[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}] [{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]
</title> </title>
<meta name="robots" content="noindex,nofollow"> <meta name="robots" content="noindex,nofollow" />
<link <link
rel="icon" rel="icon"
@ -181,14 +181,9 @@
</div> </div>
</div> </div>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.ts"></script>
[{[ if .Theme -]}] [{[ if .CSS -]}]
<link
rel="stylesheet"
href="[{[ .StaticURL ]}]/themes/[{[ .Theme ]}].css"
/>
[{[ end ]}] [{[ if .CSS -]}]
<link rel="stylesheet" href="[{[ .StaticURL ]}]/custom.css" /> <link rel="stylesheet" href="[{[ .StaticURL ]}]/custom.css" />
[{[ end ]}] [{[ end ]}]
</body> </body>

View File

@ -1,217 +0,0 @@
:root {
--background: #141D24;
--surfacePrimary: #20292F;
--surfaceSecondary: #3A4147;
--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, main .spinner div {
background: var(--icon);
}
#login {
background: var(--background);
}
header {
background: var(--surfacePrimary);
}
#search #input {
background: var(--surfaceSecondary);
border-color: var(--surfacePrimary);
}
#search #input input::placeholder {
color: var(--textSecondary);
}
#search.active #input {
background: var(--surfacePrimary);
}
#search.active input {
color: var(--textPrimary);
}
#search #result {
background: var(--background);
color: var(--textPrimary);
}
#search .boxes {
background: var(--surfaceSecondary);
}
#search .boxes h3 {
color: var(--textPrimary);
}
.action {
color: var(--textPrimary) !important;
}
.action:hover {
background-color: rgba(255, 255, 255, .1);
}
.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;
}
.breadcrumbs a:hover {
background-color: rgba(255, 255, 255, .1);
}
#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);
}
.message {
color: var(--textPrimary);
}
.card {
background: var(--surfacePrimary);
color: var(--textPrimary);
}
.button--flat:hover {
background: var(--surfaceSecondary);
}
.dashboard #nav ul li {
color: var(--textSecondary);
}
.dashboard #nav ul li:hover {
background: var(--surfaceSecondary);
}
.card h3,
.dashboard #nav,
.dashboard p label {
color: var(--textPrimary);
}
.card#share input,
.card#share select,
.input {
background: var(--surfaceSecondary);
color: var(--textPrimary);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.input:hover,
.input:focus {
border-color: rgba(255, 255, 255, 0.15);
}
.input--red {
background: #73302D;
}
.input--green {
background: #147A41;
}
.dashboard #nav .wrapper,
.collapsible {
border-color: var(--divider);
}
.collapsible > label * {
color: var(--textPrimary);
}
table th {
color: var(--textSecondary);
}
.file-list li:hover {
background: var(--surfaceSecondary);
}
.file-list li:before {
color: var(--textSecondary);
}
.file-list li[aria-selected=true]:before {
color: var(--icon);
}
.shell {
background: var(--surfacePrimary);
color: var(--textPrimary);
}
.shell__divider {
background: rgba(255, 255, 255, 0.1);
}
.shell__divider:hover {
background: rgba(255, 255, 255, 0.4);
}
.shell__result {
border-top: 1px solid var(--divider);
}
#editor-container {
background: var(--background);
}
#editor-container .bar {
background: var(--surfacePrimary);
}
@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;
}
}
.share__box {
background: var(--surfacePrimary) !important;
color: var(--textPrimary);
}
.share__box__element {
border-top-color: var(--divider);
}

View File

@ -4,23 +4,30 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
// eslint-disable-next-line no-undef import { ref, onMounted, watch } from "vue";
// __webpack_public_path__ = window.FileBrowser.StaticURL + "/"; import { useI18n } from "vue-i18n";
import { setHtmlLocale } from "./i18n";
import { getMediaPreference, getTheme, setTheme } from "./utils/theme";
export default { const { locale } = useI18n();
name: "app",
mounted() { const userTheme = ref<UserTheme>(getTheme() || getMediaPreference());
onMounted(() => {
setTheme(userTheme.value);
setHtmlLocale(locale.value);
// this might be null during HMR
const loading = document.getElementById("loading"); const loading = document.getElementById("loading");
loading.classList.add("done"); loading?.classList.add("done");
setTimeout(function () { setTimeout(function () {
loading.parentNode.removeChild(loading); loading?.parentNode?.removeChild(loading);
}, 200); }, 200);
}, });
};
</script>
<style> // handles ltr/rtl changes
@import "./css/styles.css"; watch(locale, (newValue) => {
</style> newValue && setHtmlLocale(newValue);
});
</script>

View File

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

View File

@ -1,19 +1,20 @@
import { createURL, fetchURL, removePrefix } from "./utils"; import { createURL, fetchURL, removePrefix } from "./utils";
import { baseURL } from "@/utils/constants"; import { baseURL } from "@/utils/constants";
import store from "@/store"; import { useAuthStore } from "@/stores/auth";
import { upload as postTus, useTus } from "./tus"; import { upload as postTus, useTus } from "./tus";
export async function fetch(url) { export async function fetch(url: string) {
url = removePrefix(url); url = removePrefix(url);
const res = await fetchURL(`/api/resources${url}`, {}); const res = await fetchURL(`/api/resources${url}`, {});
let data = await res.json(); const data = (await res.json()) as Resource;
data.url = `/files${url}`; data.url = `/files${url}`;
if (data.isDir) { if (data.isDir) {
if (!data.url.endsWith("/")) data.url += "/"; if (!data.url.endsWith("/")) data.url += "/";
data.items = data.items.map((item, index) => { // Perhaps change the any
data.items = data.items.map((item: any, index: any) => {
item.index = index; item.index = index;
item.url = `${data.url}${encodeURIComponent(item.name)}`; item.url = `${data.url}${encodeURIComponent(item.name)}`;
@ -28,10 +29,12 @@ export async function fetch(url) {
return data; return data;
} }
async function resourceAction(url, method, content) { async function resourceAction(url: string, method: ApiMethod, content?: any) {
url = removePrefix(url); url = removePrefix(url);
let opts = { method }; const opts: ApiOpts = {
method,
};
if (content) { if (content) {
opts.body = content; opts.body = content;
@ -42,15 +45,15 @@ async function resourceAction(url, method, content) {
return res; return res;
} }
export async function remove(url) { export async function remove(url: string) {
return resourceAction(url, "DELETE"); return resourceAction(url, "DELETE");
} }
export async function put(url, content = "") { export async function put(url: string, content = "") {
return resourceAction(url, "PUT", content); return resourceAction(url, "PUT", content);
} }
export function download(format, ...files) { export function download(format: any, ...files: string[]) {
let url = `${baseURL}/api/raw`; let url = `${baseURL}/api/raw`;
if (files.length === 1) { if (files.length === 1) {
@ -58,7 +61,7 @@ export function download(format, ...files) {
} else { } else {
let arg = ""; let arg = "";
for (let file of files) { for (const file of files) {
arg += removePrefix(file) + ","; arg += removePrefix(file) + ",";
} }
@ -71,14 +74,20 @@ export function download(format, ...files) {
url += `algo=${format}&`; url += `algo=${format}&`;
} }
if (store.state.jwt) { const authStore = useAuthStore();
url += `auth=${store.state.jwt}&`; if (authStore.jwt) {
url += `auth=${authStore.jwt}&`;
} }
window.open(url); window.open(url);
} }
export async function post(url, content = "", overwrite = false, onupload) { export async function post(
url: string,
content: ApiContent = "",
overwrite = false,
onupload: any = () => {}
) {
// Use the pre-existing API if: // Use the pre-existing API if:
const useResourcesApi = const useResourcesApi =
// a folder is being created // a folder is being created
@ -93,10 +102,15 @@ export async function post(url, content = "", overwrite = false, onupload) {
: postTus(url, content, overwrite, onupload); : postTus(url, content, overwrite, onupload);
} }
async function postResources(url, content = "", overwrite = false, onupload) { async function postResources(
url: string,
content: ApiContent = "",
overwrite = false,
onupload: any
) {
url = removePrefix(url); url = removePrefix(url);
let bufferContent; let bufferContent: ArrayBuffer;
if ( if (
content instanceof Blob && content instanceof Blob &&
!["http:", "https:"].includes(window.location.protocol) !["http:", "https:"].includes(window.location.protocol)
@ -104,14 +118,15 @@ async function postResources(url, content = "", overwrite = false, onupload) {
bufferContent = await new Response(content).arrayBuffer(); bufferContent = await new Response(content).arrayBuffer();
} }
const authStore = useAuthStore();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let request = new XMLHttpRequest(); const request = new XMLHttpRequest();
request.open( request.open(
"POST", "POST",
`${baseURL}/api/resources${url}?override=${overwrite}`, `${baseURL}/api/resources${url}?override=${overwrite}`,
true true
); );
request.setRequestHeader("X-Auth", store.state.jwt); request.setRequestHeader("X-Auth", authStore.jwt);
if (typeof onupload === "function") { if (typeof onupload === "function") {
request.upload.onprogress = onupload; request.upload.onprogress = onupload;
@ -135,12 +150,17 @@ async function postResources(url, content = "", overwrite = false, onupload) {
}); });
} }
function moveCopy(items, copy = false, overwrite = false, rename = false) { function moveCopy(
let promises = []; items: any[],
copy = false,
overwrite = false,
rename = false
) {
const promises = [];
for (let item of items) { for (const item of items) {
const from = item.from; const from = item.from;
const to = encodeURIComponent(removePrefix(item.to)); const to = encodeURIComponent(removePrefix(item.to ?? ""));
const url = `${from}?action=${ const url = `${from}?action=${
copy ? "copy" : "rename" copy ? "copy" : "rename"
}&destination=${to}&override=${overwrite}&rename=${rename}`; }&destination=${to}&override=${overwrite}&rename=${rename}`;
@ -150,20 +170,20 @@ function moveCopy(items, copy = false, overwrite = false, rename = false) {
return Promise.all(promises); return Promise.all(promises);
} }
export function move(items, overwrite = false, rename = false) { export function move(items: any[], overwrite = false, rename = false) {
return moveCopy(items, false, overwrite, rename); return moveCopy(items, false, overwrite, rename);
} }
export function copy(items, overwrite = false, rename = false) { export function copy(items: any[], overwrite = false, rename = false) {
return moveCopy(items, true, overwrite, rename); return moveCopy(items, true, overwrite, rename);
} }
export async function checksum(url, algo) { export async function checksum(url: string, algo: ChecksumAlg) {
const data = await resourceAction(`${url}?checksum=${algo}`, "GET"); const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
return (await data.json()).checksums[algo]; return (await data.json()).checksums[algo];
} }
export function getDownloadURL(file, inline) { export function getDownloadURL(file: ResourceItem, inline: any) {
const params = { const params = {
...(inline && { inline: "true" }), ...(inline && { inline: "true" }),
}; };
@ -171,7 +191,7 @@ export function getDownloadURL(file, inline) {
return createURL("api/raw" + file.path, params); return createURL("api/raw" + file.path, params);
} }
export function getPreviewURL(file, size) { export function getPreviewURL(file: ResourceItem, size: string) {
const params = { const params = {
inline: "true", inline: "true",
key: Date.parse(file.modified), key: Date.parse(file.modified),
@ -180,20 +200,15 @@ export function getPreviewURL(file, size) {
return createURL("api/preview/" + size + file.path, params); return createURL("api/preview/" + size + file.path, params);
} }
export function getSubtitlesURL(file) { export function getSubtitlesURL(file: ResourceItem) {
const params = { const params = {
inline: "true", inline: "true",
}; };
const subtitles = []; return file.subtitles?.map((d) => createURL("api/subtitle" + d, params));
for (const sub of file.subtitles) {
subtitles.push(createURL("api/raw" + sub, params));
} }
return subtitles; export async function usage(url: string) {
}
export async function usage(url) {
url = removePrefix(url); url = removePrefix(url);
const res = await fetchURL(`/api/usage${url}`, {}); const res = await fetchURL(`/api/usage${url}`, {});

View File

@ -1,7 +1,7 @@
import { fetchURL, removePrefix, createURL } from "./utils"; import { fetchURL, removePrefix, createURL } from "./utils";
import { baseURL } from "@/utils/constants"; import { baseURL } from "@/utils/constants";
export async function fetch(url, password = "") { export async function fetch(url: string, password: string = "") {
url = removePrefix(url); url = removePrefix(url);
const res = await fetchURL( const res = await fetchURL(
@ -12,12 +12,12 @@ export async function fetch(url, password = "") {
false false
); );
let data = await res.json(); const data = (await res.json()) as Resource;
data.url = `/share${url}`; data.url = `/share${url}`;
if (data.isDir) { if (data.isDir) {
if (!data.url.endsWith("/")) data.url += "/"; if (!data.url.endsWith("/")) data.url += "/";
data.items = data.items.map((item, index) => { data.items = data.items.map((item: any, index: any) => {
item.index = index; item.index = index;
item.url = `${data.url}${encodeURIComponent(item.name)}`; item.url = `${data.url}${encodeURIComponent(item.name)}`;
@ -32,7 +32,12 @@ export async function fetch(url, password = "") {
return data; return data;
} }
export function download(format, hash, token, ...files) { export function download(
format: DownloadFormat,
hash: string,
token: string,
...files: string[]
) {
let url = `${baseURL}/api/public/dl/${hash}`; let url = `${baseURL}/api/public/dl/${hash}`;
if (files.length === 1) { if (files.length === 1) {
@ -40,7 +45,7 @@ export function download(format, hash, token, ...files) {
} else { } else {
let arg = ""; let arg = "";
for (let file of files) { for (const file of files) {
arg += encodeURIComponent(file) + ","; arg += encodeURIComponent(file) + ",";
} }
@ -60,11 +65,11 @@ export function download(format, hash, token, ...files) {
window.open(url); window.open(url);
} }
export function getDownloadURL(share, inline = false) { export function getDownloadURL(res: Resource, inline = false) {
const params = { const params = {
...(inline && { inline: "true" }), ...(inline && { inline: "true" }),
...(share.token && { token: share.token }), ...(res.token && { token: res.token }),
}; };
return createURL("api/public/dl/" + share.hash + share.path, params, false); return createURL("api/public/dl/" + res.hash + res.path, params, false);
} }

View File

@ -1,7 +1,7 @@
import { fetchURL, removePrefix } from "./utils"; import { fetchURL, removePrefix } from "./utils";
import url from "../utils/url"; import url from "../utils/url";
export default async function search(base, query) { export default async function search(base: string, query: string) {
base = removePrefix(base); base = removePrefix(base);
query = encodeURIComponent(query); query = encodeURIComponent(query);
@ -9,11 +9,11 @@ export default async function search(base, query) {
base += "/"; base += "/";
} }
let res = await fetchURL(`/api/search${base}?query=${query}`, {}); const res = await fetchURL(`/api/search${base}?query=${query}`, {});
let data = await res.json(); let data = await res.json();
data = data.map((item) => { data = data.map((item: UploadItem) => {
item.url = `/files${base}` + url.encodePath(item.path); item.url = `/files${base}` + url.encodePath(item.path);
if (item.dir) { if (item.dir) {

View File

@ -1,10 +1,10 @@
import { fetchURL, fetchJSON } from "./utils"; import { fetchURL, fetchJSON } from "./utils";
export function get() { export function get() {
return fetchJSON(`/api/settings`, {}); return fetchJSON<ISettings>(`/api/settings`, {});
} }
export async function update(settings) { export async function update(settings: ISettings) {
await fetchURL(`/api/settings`, { await fetchURL(`/api/settings`, {
method: "PUT", method: "PUT",
body: JSON.stringify(settings), body: JSON.stringify(settings),

View File

@ -1,21 +1,26 @@
import { fetchURL, fetchJSON, removePrefix, createURL } from "./utils"; import { fetchURL, fetchJSON, removePrefix, createURL } from "./utils";
export async function list() { export async function list() {
return fetchJSON("/api/shares"); return fetchJSON<Share[]>("/api/shares");
} }
export async function get(url) { export async function get(url: string) {
url = removePrefix(url); url = removePrefix(url);
return fetchJSON(`/api/share${url}`); return fetchJSON<Share>(`/api/share${url}`);
} }
export async function remove(hash) { export async function remove(hash: string) {
await fetchURL(`/api/share/${hash}`, { await fetchURL(`/api/share/${hash}`, {
method: "DELETE", method: "DELETE",
}); });
} }
export async function create(url, password = "", expires = "", unit = "hours") { export async function create(
url: string,
password = "",
expires = "",
unit = "hours"
) {
url = removePrefix(url); url = removePrefix(url);
url = `/api/share${url}`; url = `/api/share${url}`;
if (expires !== "") { if (expires !== "") {
@ -23,7 +28,11 @@ export async function create(url, password = "", expires = "", unit = "hours") {
} }
let body = "{}"; let body = "{}";
if (password != "" || expires !== "" || unit !== "hours") { if (password != "" || expires !== "" || unit !== "hours") {
body = JSON.stringify({ password: password, expires: expires, unit: unit }); body = JSON.stringify({
password: password,
expires: expires.toString(), // backend expects string not number
unit: unit,
});
} }
return fetchJSON(url, { return fetchJSON(url, {
method: "POST", method: "POST",
@ -31,6 +40,6 @@ export async function create(url, password = "", expires = "", unit = "hours") {
}); });
} }
export function getShareURL(share) { export function getShareURL(share: Share) {
return createURL("share/" + share.hash, {}, false); return createURL("share/" + share.hash, {}, false);
} }

View File

@ -1,6 +1,7 @@
import * as tus from "tus-js-client"; import * as tus from "tus-js-client";
import { baseURL, tusEndpoint, tusSettings } from "@/utils/constants"; import { baseURL, tusEndpoint, tusSettings } from "@/utils/constants";
import store from "@/store"; import { useAuthStore } from "@/stores/auth";
import { useUploadStore } from "@/stores/upload";
import { removePrefix } from "@/api/utils"; import { removePrefix } from "@/api/utils";
import { fetchURL } from "./utils"; import { fetchURL } from "./utils";
@ -11,13 +12,13 @@ const ALPHA = 0.2;
const ONE_MINUS_ALPHA = 1 - ALPHA; const ONE_MINUS_ALPHA = 1 - ALPHA;
const RECENT_SPEEDS_LIMIT = 5; const RECENT_SPEEDS_LIMIT = 5;
const MB_DIVISOR = 1024 * 1024; const MB_DIVISOR = 1024 * 1024;
const CURRENT_UPLOAD_LIST = {}; const CURRENT_UPLOAD_LIST: CurrentUploadList = {};
export async function upload( export async function upload(
filePath, filePath: string,
content = "", content: ApiContent = "",
overwrite = false, overwrite = false,
onupload onupload: any
) { ) {
if (!tusSettings) { if (!tusSettings) {
// Shouldn't happen as we check for tus support before calling this function // Shouldn't happen as we check for tus support before calling this function
@ -25,29 +26,35 @@ export async function upload(
} }
filePath = removePrefix(filePath); filePath = removePrefix(filePath);
let resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`; const resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`;
await createUpload(resourcePath); await createUpload(resourcePath);
return new Promise((resolve, reject) => { const authStore = useAuthStore();
let upload = new tus.Upload(content, {
// Exit early because of typescript, tus content can't be a string
if (content === "") {
return false;
}
return new Promise<void | string>((resolve, reject) => {
const upload = new tus.Upload(content, {
uploadUrl: `${baseURL}${resourcePath}`, uploadUrl: `${baseURL}${resourcePath}`,
chunkSize: tusSettings.chunkSize, chunkSize: tusSettings.chunkSize,
retryDelays: computeRetryDelays(tusSettings), retryDelays: computeRetryDelays(tusSettings),
parallelUploads: 1, parallelUploads: 1,
storeFingerprintForResuming: false, storeFingerprintForResuming: false,
headers: { headers: {
"X-Auth": store.state.jwt, "X-Auth": authStore.jwt,
}, },
onError: function (error) { onError: function (error) {
if (CURRENT_UPLOAD_LIST[filePath].interval) { if (CURRENT_UPLOAD_LIST[filePath].interval) {
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval); clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
} }
delete CURRENT_UPLOAD_LIST[filePath]; delete CURRENT_UPLOAD_LIST[filePath];
reject("Upload failed: " + error); reject(new Error(`Upload failed: ${error.message}`));
}, },
onProgress: function (bytesUploaded) { onProgress: function (bytesUploaded) {
let fileData = CURRENT_UPLOAD_LIST[filePath]; const fileData = CURRENT_UPLOAD_LIST[filePath];
fileData.currentBytesUploaded = bytesUploaded; fileData.currentBytesUploaded = bytesUploaded;
if (!fileData.hasStarted) { if (!fileData.hasStarted) {
@ -79,14 +86,14 @@ export async function upload(
lastProgressTimestamp: null, lastProgressTimestamp: null,
sumOfRecentSpeeds: 0, sumOfRecentSpeeds: 0,
hasStarted: false, hasStarted: false,
interval: null, interval: undefined,
}; };
upload.start(); upload.start();
}); });
} }
async function createUpload(resourcePath) { async function createUpload(resourcePath: string) {
let headResp = await fetchURL(resourcePath, { const headResp = await fetchURL(resourcePath, {
method: "POST", method: "POST",
}); });
if (headResp.status !== 201) { if (headResp.status !== 201) {
@ -96,10 +103,10 @@ async function createUpload(resourcePath) {
} }
} }
function computeRetryDelays(tusSettings) { function computeRetryDelays(tusSettings: TusSettings): number[] | undefined {
if (!tusSettings.retryCount || tusSettings.retryCount < 1) { if (!tusSettings.retryCount || tusSettings.retryCount < 1) {
// Disable retries altogether // Disable retries altogether
return null; return undefined;
} }
// The tus client expects our retries as an array with computed backoffs // The tus client expects our retries as an array with computed backoffs
// E.g.: [0, 3000, 5000, 10000, 20000] // E.g.: [0, 3000, 5000, 10000, 20000]
@ -115,7 +122,7 @@ function computeRetryDelays(tusSettings) {
return retryDelays; return retryDelays;
} }
export async function useTus(content) { export async function useTus(content: ApiContent) {
return isTusSupported() && content instanceof Blob; return isTusSupported() && content instanceof Blob;
} }
@ -123,25 +130,34 @@ function isTusSupported() {
return tus.isSupported === true; return tus.isSupported === true;
} }
function computeETA(state) { function computeETA(state: ETAState, speed?: number) {
if (state.speedMbyte === 0) { if (state.speedMbyte === 0) {
return Infinity; return Infinity;
} }
const totalSize = state.sizes.reduce((acc, size) => acc + size, 0); const totalSize = state.sizes.reduce(
(acc: number, size: number) => acc + size,
0
);
const uploadedSize = state.progress.reduce( const uploadedSize = state.progress.reduce(
(acc, progress) => acc + progress, (acc: number, progress: Progress) => {
if (typeof progress === "number") {
return acc + progress;
}
return acc;
},
0 0
); );
const remainingSize = totalSize - uploadedSize; const remainingSize = totalSize - uploadedSize;
const speedBytesPerSecond = state.speedMbyte * 1024 * 1024; const speedBytesPerSecond = (speed ?? state.speedMbyte) * 1024 * 1024;
return remainingSize / speedBytesPerSecond; return remainingSize / speedBytesPerSecond;
} }
function computeGlobalSpeedAndETA() { function computeGlobalSpeedAndETA() {
const uploadStore = useUploadStore();
let totalSpeed = 0; let totalSpeed = 0;
let totalCount = 0; let totalCount = 0;
for (let filePath in CURRENT_UPLOAD_LIST) { for (const filePath in CURRENT_UPLOAD_LIST) {
totalSpeed += CURRENT_UPLOAD_LIST[filePath].currentAverageSpeed; totalSpeed += CURRENT_UPLOAD_LIST[filePath].currentAverageSpeed;
totalCount++; totalCount++;
} }
@ -149,41 +165,43 @@ function computeGlobalSpeedAndETA() {
if (totalCount === 0) return { speed: 0, eta: Infinity }; if (totalCount === 0) return { speed: 0, eta: Infinity };
const averageSpeed = totalSpeed / totalCount; const averageSpeed = totalSpeed / totalCount;
const averageETA = computeETA(store.state.upload, averageSpeed); const averageETA = computeETA(uploadStore, averageSpeed);
return { speed: averageSpeed, eta: averageETA }; return { speed: averageSpeed, eta: averageETA };
} }
function calcProgress(filePath) { function calcProgress(filePath: string) {
let fileData = CURRENT_UPLOAD_LIST[filePath]; const uploadStore = useUploadStore();
const fileData = CURRENT_UPLOAD_LIST[filePath];
let elapsedTime = (Date.now() - fileData.lastProgressTimestamp) / 1000; const elapsedTime =
let bytesSinceLastUpdate = (Date.now() - (fileData.lastProgressTimestamp ?? 0)) / 1000;
const bytesSinceLastUpdate =
fileData.currentBytesUploaded - fileData.initialBytesUploaded; fileData.currentBytesUploaded - fileData.initialBytesUploaded;
let currentSpeed = bytesSinceLastUpdate / MB_DIVISOR / elapsedTime; const currentSpeed = bytesSinceLastUpdate / MB_DIVISOR / elapsedTime;
if (fileData.recentSpeeds.length >= RECENT_SPEEDS_LIMIT) { if (fileData.recentSpeeds.length >= RECENT_SPEEDS_LIMIT) {
fileData.sumOfRecentSpeeds -= fileData.recentSpeeds.shift(); fileData.sumOfRecentSpeeds -= fileData.recentSpeeds.shift() ?? 0;
} }
fileData.recentSpeeds.push(currentSpeed); fileData.recentSpeeds.push(currentSpeed);
fileData.sumOfRecentSpeeds += currentSpeed; fileData.sumOfRecentSpeeds += currentSpeed;
let avgRecentSpeed = const avgRecentSpeed =
fileData.sumOfRecentSpeeds / fileData.recentSpeeds.length; fileData.sumOfRecentSpeeds / fileData.recentSpeeds.length;
fileData.currentAverageSpeed = fileData.currentAverageSpeed =
ALPHA * avgRecentSpeed + ONE_MINUS_ALPHA * fileData.currentAverageSpeed; ALPHA * avgRecentSpeed + ONE_MINUS_ALPHA * fileData.currentAverageSpeed;
const { speed, eta } = computeGlobalSpeedAndETA(); const { speed, eta } = computeGlobalSpeedAndETA();
store.commit("setUploadSpeed", speed); uploadStore.setUploadSpeed(speed);
store.commit("setETA", eta); uploadStore.setETA(eta);
fileData.initialBytesUploaded = fileData.currentBytesUploaded; fileData.initialBytesUploaded = fileData.currentBytesUploaded;
fileData.lastProgressTimestamp = Date.now(); fileData.lastProgressTimestamp = Date.now();
} }
export function abortAllUploads() { export function abortAllUploads() {
for (let filePath in CURRENT_UPLOAD_LIST) { for (const filePath in CURRENT_UPLOAD_LIST) {
if (CURRENT_UPLOAD_LIST[filePath].interval) { if (CURRENT_UPLOAD_LIST[filePath].interval) {
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval); clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
} }

View File

@ -1,14 +1,14 @@
import { fetchURL, fetchJSON } from "./utils"; import { fetchURL, fetchJSON, StatusError } from "./utils";
export async function getAll() { export async function getAll() {
return fetchJSON(`/api/users`, {}); return fetchJSON<IUser[]>(`/api/users`, {});
} }
export async function get(id) { export async function get(id: number) {
return fetchJSON(`/api/users/${id}`, {}); return fetchJSON<IUser>(`/api/users/${id}`, {});
} }
export async function create(user) { export async function create(user: IUser) {
const res = await fetchURL(`/api/users`, { const res = await fetchURL(`/api/users`, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
@ -21,9 +21,11 @@ export async function create(user) {
if (res.status === 201) { if (res.status === 201) {
return res.headers.get("Location"); return res.headers.get("Location");
} }
throw new StatusError(await res.text(), res.status);
} }
export async function update(user, which = ["all"]) { export async function update(user: IUser, which = ["all"]) {
await fetchURL(`/api/users/${user.id}`, { await fetchURL(`/api/users/${user.id}`, {
method: "PUT", method: "PUT",
body: JSON.stringify({ body: JSON.stringify({
@ -34,7 +36,7 @@ export async function update(user, which = ["all"]) {
}); });
} }
export async function remove(id) { export async function remove(id: number) {
await fetchURL(`/api/users/${id}`, { await fetchURL(`/api/users/${id}`, {
method: "DELETE", method: "DELETE",
}); });

View File

@ -1,80 +0,0 @@
import store from "@/store";
import { renew, logout } from "@/utils/auth";
import { baseURL } from "@/utils/constants";
import { encodePath } from "@/utils/url";
export async function fetchURL(url, opts, auth = true) {
opts = opts || {};
opts.headers = opts.headers || {};
let { headers, ...rest } = opts;
let res;
try {
res = await fetch(`${baseURL}${url}`, {
headers: {
"X-Auth": store.state.jwt,
...headers,
},
...rest,
});
} catch {
const error = new Error("000 No connection");
error.status = 0;
throw error;
}
if (auth && res.headers.get("X-Renew-Token") === "true") {
await renew(store.state.jwt);
}
if (res.status < 200 || res.status > 299) {
const error = new Error(await res.text());
error.status = res.status;
if (auth && res.status == 401) {
logout();
}
throw error;
}
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) {
url = url.split("/").splice(2).join("/");
if (url === "") url = "/";
if (url[0] !== "/") url = "/" + url;
return url;
}
export function createURL(endpoint, params = {}, auth = true) {
let prefix = baseURL;
if (!prefix.endsWith("/")) {
prefix = prefix + "/";
}
const url = new URL(prefix + encodePath(endpoint), origin);
const searchParams = {
...(auth && { auth: store.state.jwt }),
...params,
};
for (const key in searchParams) {
url.searchParams.set(key, searchParams[key]);
}
return url.toString();
}

98
frontend/src/api/utils.ts Normal file
View File

@ -0,0 +1,98 @@
import { useAuthStore } from "@/stores/auth";
import { renew, logout } from "@/utils/auth";
import { baseURL } from "@/utils/constants";
import { encodePath } from "@/utils/url";
export class StatusError extends Error {
constructor(
message: any,
public status?: number
) {
super(message);
this.name = "StatusError";
}
}
export async function fetchURL(
url: string,
opts: ApiOpts,
auth = true
): Promise<Response> {
const authStore = useAuthStore();
opts = opts || {};
opts.headers = opts.headers || {};
const { headers, ...rest } = opts;
let res;
try {
res = await fetch(`${baseURL}${url}`, {
headers: {
"X-Auth": authStore.jwt,
...headers,
},
...rest,
});
} catch {
throw new StatusError("000 No connection", 0);
}
if (auth && res.headers.get("X-Renew-Token") === "true") {
await renew(authStore.jwt);
}
if (res.status < 200 || res.status > 299) {
const body = await res.text();
const error = new StatusError(
body || `${res.status} ${res.statusText}`,
res.status
);
if (auth && res.status == 401) {
logout();
}
throw error;
}
return res;
}
export async function fetchJSON<T>(url: string, opts?: any): Promise<T> {
const res = await fetchURL(url, opts);
if (res.status === 200) {
return res.json() as Promise<T>;
}
throw new StatusError(`${res.status} ${res.statusText}`, res.status);
}
export function removePrefix(url: string): string {
url = url.split("/").splice(2).join("/");
if (url === "") url = "/";
if (url[0] !== "/") url = "/" + url;
return url;
}
export function createURL(endpoint: string, params = {}, auth = true): string {
const authStore = useAuthStore();
let prefix = baseURL;
if (!prefix.endsWith("/")) {
prefix = prefix + "/";
}
const url = new URL(prefix + encodePath(endpoint), origin);
const searchParams: SearchParams = {
...(auth && { auth: authStore.jwt }),
...params,
};
for (const key in searchParams) {
url.searchParams.set(key, searchParams[key]);
}
return url.toString();
}

View File

@ -3,8 +3,8 @@
<component <component
:is="element" :is="element"
:to="base || ''" :to="base || ''"
:aria-label="$t('files.home')" :aria-label="t('files.home')"
:title="$t('files.home')" :title="t('files.home')"
> >
<i class="material-icons">home</i> <i class="material-icons">home</i>
</component> </component>
@ -18,13 +18,22 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
export default { import { computed } from "vue";
name: "breadcrumbs", import { useI18n } from "vue-i18n";
props: ["base", "noLink"], import { useRoute } from "vue-router";
computed: {
items() { const { t } = useI18n();
const relativePath = this.$route.path.replace(this.base, "");
const route = useRoute();
const props = defineProps<{
base: string;
noLink?: boolean;
}>();
const items = computed(() => {
const relativePath = route.path.replace(props.base, "");
let parts = relativePath.split("/"); let parts = relativePath.split("/");
if (parts[0] === "") { if (parts[0] === "") {
@ -35,13 +44,13 @@ export default {
parts.pop(); parts.pop();
} }
let breadcrumbs = []; let breadcrumbs: BreadCrumb[] = [];
for (let i = 0; i < parts.length; i++) { for (let i = 0; i < parts.length; i++) {
if (i === 0) { if (i === 0) {
breadcrumbs.push({ breadcrumbs.push({
name: decodeURIComponent(parts[i]), name: decodeURIComponent(parts[i]),
url: this.base + "/" + parts[i] + "/", url: props.base + "/" + parts[i] + "/",
}); });
} else { } else {
breadcrumbs.push({ breadcrumbs.push({
@ -60,16 +69,15 @@ export default {
} }
return breadcrumbs; return breadcrumbs;
}, });
element() {
if (this.noLink !== undefined) { const element = computed(() => {
if (props.noLink) {
return "span"; return "span";
} }
return "router-link"; return "router-link";
}, });
},
};
</script> </script>
<style></style> <style></style>

View File

@ -0,0 +1,45 @@
<template>
<div class="t-container">
<span>{{ message }}</span>
<button v-if="isReport" class="action" @click.stop="clicked">
{{ reportText }}
</button>
</div>
</template>
<script setup lang="ts">
defineProps<{
message: string;
reportText?: string;
isReport?: boolean;
}>();
const clicked = () => {
window.open("https://github.com/filebrowser/filebrowser/issues/new/choose");
};
</script>
<style scoped>
.t-container {
width: 100%;
padding: 5px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.action {
text-align: center;
height: 40px;
padding: 0 10px;
margin-left: 20px;
border-radius: 5px;
color: white;
cursor: pointer;
border: thin solid currentColor;
}
html[dir="rtl"] .action {
margin-left: initial;
margin-right: 20px;
}
</style>

View File

@ -0,0 +1,224 @@
<!-- This component taken directly from vue-simple-progress
since it didnt support Vue 3 but the component itself does
https://raw.githubusercontent.com/dzwillia/vue-simple-progress/master/src/components/Progress.vue -->
<template>
<div>
<div
class="vue-simple-progress-text"
:style="text_style"
v-if="text.length > 0 && textPosition == 'top'"
>
{{ text }}
</div>
<div class="vue-simple-progress" :style="progress_style">
<div
class="vue-simple-progress-text"
:style="text_style"
v-if="text.length > 0 && textPosition == 'middle'"
>
{{ text }}
</div>
<div
style="position: relative; left: -9999px"
:style="text_style"
v-if="text.length > 0 && textPosition == 'inside'"
>
{{ text }}
</div>
<div class="vue-simple-progress-bar" :style="bar_style">
<div
:style="text_style"
v-if="text.length > 0 && textPosition == 'inside'"
>
{{ text }}
</div>
</div>
</div>
<div
class="vue-simple-progress-text"
:style="text_style"
v-if="text.length > 0 && textPosition == 'bottom'"
>
{{ text }}
</div>
</div>
</template>
<script>
// We're leaving this untouched as you can read in the beginning
var isNumber = function (n) {
return !isNaN(parseFloat(n)) && isFinite(n);
};
export default {
name: "progress-bar",
props: {
val: {
default: 0,
},
max: {
default: 100,
},
size: {
// either a number (pixel width/height) or 'tiny', 'small',
// 'medium', 'large', 'huge', 'massive' for common sizes
default: 3,
},
"bg-color": {
type: String,
default: "#eee",
},
"bar-color": {
type: String,
default: "#2196f3", // match .blue color to Material Design's 'Blue 500' color
},
"bar-transition": {
type: String,
default: "all 0.5s ease",
},
"bar-border-radius": {
type: Number,
default: 0,
},
spacing: {
type: Number,
default: 4,
},
text: {
type: String,
default: "",
},
"text-align": {
type: String,
default: "center", // 'left', 'right'
},
"text-position": {
type: String,
default: "bottom", // 'bottom', 'top', 'middle', 'inside'
},
"font-size": {
type: Number,
default: 13,
},
"text-fg-color": {
type: String,
default: "#222",
},
},
computed: {
pct() {
var pct = (this.val / this.max) * 100;
pct = pct.toFixed(2);
return Math.min(pct, this.max);
},
size_px() {
switch (this.size) {
case "tiny":
return 2;
case "small":
return 4;
case "medium":
return 8;
case "large":
return 12;
case "big":
return 16;
case "huge":
return 32;
case "massive":
return 64;
}
return isNumber(this.size) ? this.size : 32;
},
text_padding() {
switch (this.size) {
case "tiny":
case "small":
case "medium":
case "large":
case "big":
case "huge":
case "massive":
return Math.min(Math.max(Math.ceil(this.size_px / 8), 3), 12);
}
return isNumber(this.spacing) ? this.spacing : 4;
},
text_font_size() {
switch (this.size) {
case "tiny":
case "small":
case "medium":
case "large":
case "big":
case "huge":
case "massive":
return Math.min(Math.max(Math.ceil(this.size_px * 1.4), 11), 32);
}
return isNumber(this.fontSize) ? this.fontSize : 13;
},
progress_style() {
var style = {
background: this.bgColor,
};
if (this.textPosition == "middle" || this.textPosition == "inside") {
style["position"] = "relative";
style["min-height"] = this.size_px + "px";
style["z-index"] = "-2";
}
if (this.barBorderRadius > 0) {
style["border-radius"] = this.barBorderRadius + "px";
}
return style;
},
bar_style() {
var style = {
background: this.barColor,
width: this.pct + "%",
height: this.size_px + "px",
transition: this.barTransition,
};
if (this.barBorderRadius > 0) {
style["border-radius"] = this.barBorderRadius + "px";
}
if (this.textPosition == "middle" || this.textPosition == "inside") {
style["position"] = "absolute";
style["top"] = "0";
style["height"] = "100%";
(style["min-height"] = this.size_px + "px"), (style["z-index"] = "-1");
}
return style;
},
text_style() {
var style = {
color: this.textFgColor,
"font-size": this.text_font_size + "px",
"text-align": this.textAlign,
};
if (
this.textPosition == "top" ||
this.textPosition == "middle" ||
this.textPosition == "inside"
)
style["padding-bottom"] = this.text_padding + "px";
if (
this.textPosition == "bottom" ||
this.textPosition == "middle" ||
this.textPosition == "inside"
)
style["padding-top"] = this.text_padding + "px";
return style;
},
},
};
</script>

View File

@ -17,7 +17,7 @@
@keyup.enter="submit" @keyup.enter="submit"
ref="input" ref="input"
:autofocus="active" :autofocus="active"
v-model.trim="value" v-model.trim="prompt"
:aria-label="$t('search.search')" :aria-label="$t('search.search')"
:placeholder="$t('search.search')" :placeholder="$t('search.search')"
/> />
@ -28,7 +28,7 @@
<template v-if="isEmpty"> <template v-if="isEmpty">
<p>{{ text }}</p> <p>{{ text }}</p>
<template v-if="value.length === 0"> <template v-if="prompt.length === 0">
<div class="boxes"> <div class="boxes">
<h3>{{ $t("search.types") }}</h3> <h3>{{ $t("search.types") }}</h3>
<div> <div>
@ -49,7 +49,7 @@
</template> </template>
<ul v-show="results.length > 0"> <ul v-show="results.length > 0">
<li v-for="(s, k) in filteredResults" :key="k"> <li v-for="(s, k) in filteredResults" :key="k">
<router-link @click.native="close" :to="s.url"> <router-link v-on:click="close" :to="s.url">
<i v-if="s.dir" class="material-icons">folder</i> <i v-if="s.dir" class="material-icons">folder</i>
<i v-else class="material-icons">insert_drive_file</i> <i v-else class="material-icons">insert_drive_file</i>
<span>./{{ s.path }}</span> <span>./{{ s.path }}</span>
@ -64,138 +64,155 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapState, mapGetters, mapMutations } from "vuex"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import url from "@/utils/url"; import url from "@/utils/url";
import { search } from "@/api"; import { search } from "@/api";
import { computed, inject, onMounted, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { storeToRefs } from "pinia";
var boxes = { const boxes = {
image: { label: "images", icon: "insert_photo" }, image: { label: "images", icon: "insert_photo" },
audio: { label: "music", icon: "volume_up" }, audio: { label: "music", icon: "volume_up" },
video: { label: "video", icon: "movie" }, video: { label: "video", icon: "movie" },
pdf: { label: "pdf", icon: "picture_as_pdf" }, pdf: { label: "pdf", icon: "picture_as_pdf" },
}; };
export default { const layoutStore = useLayoutStore();
name: "search", const fileStore = useFileStore();
data: function () {
return {
value: "",
active: false,
ongoing: false,
results: [],
reload: false,
resultsCount: 50,
scrollable: null,
};
},
watch: {
currentPrompt(val, old) {
this.active = val?.prompt === "search";
if (old?.prompt === "search" && !this.active) { const { currentPromptName } = storeToRefs(layoutStore);
if (this.reload) {
this.setReload(true); const prompt = ref<string>("");
const active = ref<boolean>(false);
const ongoing = ref<boolean>(false);
const results = ref<any[]>([]);
const reload = ref<boolean>(false);
const resultsCount = ref<number>(50);
const $showError = inject<IToastError>("$showError")!;
const input = ref<HTMLInputElement | null>(null);
const result = ref<HTMLElement | null>(null);
const { t } = useI18n();
const route = useRoute();
watch(currentPromptName, (newVal, oldVal) => {
active.value = newVal === "search";
if (oldVal === "search" && !active.value) {
if (reload.value) {
fileStore.reload = true;
} }
document.body.style.overflow = "auto"; document.body.style.overflow = "auto";
this.reset(); reset();
this.value = ""; prompt.value = "";
this.active = false; active.value = false;
this.$refs.input.blur(); input.value?.blur();
} else if (this.active) { } else if (active.value) {
this.reload = false; reload.value = false;
this.$refs.input.focus(); input.value?.focus();
document.body.style.overflow = "hidden"; document.body.style.overflow = "hidden";
} }
}, });
value() {
if (this.results.length) { watch(prompt, () => {
this.reset(); if (results.value.length) {
reset();
} }
}, });
},
computed: { // ...mapState(useFileStore, ["isListing"]),
...mapState(["user"]), // ...mapState(useLayoutStore, ["show"]),
...mapGetters(["isListing", "currentPrompt"]), // ...mapWritableState(useFileStore, { sReload: "reload" }),
boxes() {
return boxes; const isEmpty = computed(() => {
}, return results.value.length === 0;
isEmpty() { });
return this.results.length === 0; const text = computed(() => {
}, if (ongoing.value) {
text() {
if (this.ongoing) {
return ""; return "";
} }
return this.value === "" return prompt.value === ""
? this.$t("search.typeToSearch") ? t("search.typeToSearch")
: this.$t("search.pressToSearch"); : t("search.pressToSearch");
}, });
filteredResults() { const filteredResults = computed(() => {
return this.results.slice(0, this.resultsCount); return results.value.slice(0, resultsCount.value);
}, });
},
mounted() { onMounted(() => {
this.$refs.result.addEventListener("scroll", (event) => { if (result.value === null) {
return;
}
result.value.addEventListener("scroll", (event: Event) => {
if ( if (
event.target.offsetHeight + event.target.scrollTop >= (event.target as HTMLElement).offsetHeight +
event.target.scrollHeight - 100 (event.target as HTMLElement).scrollTop >=
(event.target as HTMLElement).scrollHeight - 100
) { ) {
this.resultsCount += 50; resultsCount.value += 50;
} }
}); });
}, });
methods: {
...mapMutations(["showHover", "closeHovers", "setReload"]), const open = () => {
open() { !active.value && layoutStore.showHover("search");
this.showHover("search"); };
},
close(event) { const close = (event: Event) => {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
this.closeHovers(); layoutStore.closeHovers();
}, };
keyup(event) {
if (event.keyCode === 27) { const keyup = (event: KeyboardEvent) => {
this.close(event); if (event.key === "Escape") {
close(event);
return; return;
} }
results.value.length = 0;
};
this.results.length = 0; const init = (string: string) => {
}, prompt.value = `${string} `;
init(string) { input.value !== null ? input.value.focus() : "";
this.value = `${string} `; };
this.$refs.input.focus();
}, const reset = () => {
reset() { ongoing.value = false;
this.ongoing = false; resultsCount.value = 50;
this.resultsCount = 50; results.value = [];
this.results = []; };
},
async submit(event) { const submit = async (event: Event) => {
event.preventDefault(); event.preventDefault();
if (this.value === "") { if (prompt.value === "") {
return; return;
} }
let path = this.$route.path; let path = route.path;
if (!this.isListing) { if (!fileStore.isListing) {
path = url.removeLastDir(path) + "/"; path = url.removeLastDir(path) + "/";
} }
this.ongoing = true; ongoing.value = true;
try { try {
this.results = await search(path, this.value); results.value = await search(path, prompt.value);
} catch (error) { } catch (error: any) {
this.$showError(error); $showError(error);
} }
this.ongoing = false; ongoing.value = false;
},
},
}; };
</script> </script>

View File

@ -29,9 +29,9 @@
tabindex="0" tabindex="0"
ref="input" ref="input"
class="shell__text" class="shell__text"
contenteditable="true" :contenteditable="true"
@keydown.prevent.38="historyUp" @keydown.prevent.arrow-up="historyUp"
@keydown.prevent.40="historyDown" @keydown.prevent.arrow-down="historyDown"
@keypress.prevent.enter="submit" @keypress.prevent.enter="submit"
/> />
</div> </div>
@ -45,7 +45,10 @@
</template> </template>
<script> <script>
import { mapMutations, mapState, mapGetters } from "vuex"; import { mapState, mapActions } from "pinia";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { commands } from "@/api"; import { commands } from "@/api";
import { throttle } from "lodash"; import { throttle } from "lodash";
import { theme } from "@/utils/constants"; import { theme } from "@/utils/constants";
@ -53,8 +56,8 @@ import { theme } from "@/utils/constants";
export default { export default {
name: "shell", name: "shell",
computed: { computed: {
...mapState(["user", "showShell"]), ...mapState(useLayoutStore, ["showShell"]),
...mapGetters(["isFiles", "isLogged"]), ...mapState(useFileStore, ["isFiles"]),
path: function () { path: function () {
if (this.isFiles) { if (this.isFiles) {
return this.$route.path; return this.$route.path;
@ -75,11 +78,11 @@ export default {
mounted() { mounted() {
window.addEventListener("resize", this.resize); window.addEventListener("resize", this.resize);
}, },
beforeDestroy() { beforeUnmount() {
window.removeEventListener("resize", this.resize); window.removeEventListener("resize", this.resize);
}, },
methods: { methods: {
...mapMutations(["toggleShell"]), ...mapActions(useLayoutStore, ["toggleShell"]),
checkTheme() { checkTheme() {
if (theme == "dark") { if (theme == "dark") {
return "rgba(255, 255, 255, 0.4)"; return "rgba(255, 255, 255, 0.4)";

View File

@ -15,7 +15,7 @@
<i class="material-icons">exit_to_app</i> <i class="material-icons">exit_to_app</i>
<span>{{ $t("sidebar.logout") }}</span> <span>{{ $t("sidebar.logout") }}</span>
</button> </button>
<template v-if="isLogged"> <template v-if="isLoggedIn">
<button <button
class="action" class="action"
@click="toRoot" @click="toRoot"
@ -28,7 +28,7 @@
<div v-if="user.perm.create"> <div v-if="user.perm.create">
<button <button
@click="$store.commit('showHover', 'newDir')" @click="showHover('newDir')"
class="action" class="action"
:aria-label="$t('sidebar.newFolder')" :aria-label="$t('sidebar.newFolder')"
:title="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')"
@ -38,7 +38,7 @@
</button> </button>
<button <button
@click="$store.commit('showHover', 'newFile')" @click="showHover('newFile')"
class="action" class="action"
:aria-label="$t('sidebar.newFile')" :aria-label="$t('sidebar.newFile')"
:title="$t('sidebar.newFile')" :title="$t('sidebar.newFile')"
@ -85,9 +85,7 @@
<div <div
class="credits" class="credits"
v-if=" v-if="isFiles && !disableUsedPercentage"
$router.currentRoute.path.includes('/files/') && !disableUsedPercentage
"
style="width: 90%; margin: 2em 2.5em 3em 2.5em" style="width: 90%; margin: 2em 2.5em 3em 2.5em"
> >
<progress-bar :val="usage.usedPercentage" size="small"></progress-bar> <progress-bar :val="usage.usedPercentage" size="small"></progress-bar>
@ -115,7 +113,12 @@
</template> </template>
<script> <script>
import { mapState, mapGetters } from "vuex"; import { reactive } from "vue";
import { mapActions, mapState } from "pinia";
import { useAuthStore } from "@/stores/auth";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import * as auth from "@/utils/auth"; import * as auth from "@/utils/auth";
import { import {
version, version,
@ -126,19 +129,27 @@ import {
loginPage, loginPage,
} from "@/utils/constants"; } from "@/utils/constants";
import { files as api } from "@/api"; import { files as api } from "@/api";
import ProgressBar from "vue-simple-progress"; import ProgressBar from "@/components/ProgressBar.vue";
import prettyBytes from "pretty-bytes"; import prettyBytes from "pretty-bytes";
const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 };
export default { export default {
name: "sidebar", name: "sidebar",
setup() {
const usage = reactive(USAGE_DEFAULT);
return { usage };
},
components: { components: {
ProgressBar, ProgressBar,
}, },
inject: ["$showError"],
computed: { computed: {
...mapState(["user"]), ...mapState(useAuthStore, ["user", "isLoggedIn"]),
...mapGetters(["isLogged", "currentPrompt"]), ...mapState(useFileStore, ["isFiles", "reload"]),
...mapState(useLayoutStore, ["currentPromptName"]),
active() { active() {
return this.currentPrompt?.prompt === "sidebar"; return this.currentPromptName === "sidebar";
}, },
signup: () => signup, signup: () => signup,
version: () => version, version: () => version,
@ -146,15 +157,15 @@ export default {
disableUsedPercentage: () => disableUsedPercentage, disableUsedPercentage: () => disableUsedPercentage,
canLogout: () => !noAuth && loginPage, canLogout: () => !noAuth && loginPage,
}, },
asyncComputed: { methods: {
usage: { ...mapActions(useLayoutStore, ["closeHovers", "showHover"]),
async get() { async fetchUsage() {
let path = this.$route.path.endsWith("/") let path = this.$route.path.endsWith("/")
? this.$route.path ? this.$route.path
: this.$route.path + "/"; : this.$route.path + "/";
let usageStats = { used: 0, total: 0, usedPercentage: 0 }; let usageStats = USAGE_DEFAULT;
if (this.disableUsedPercentage) { if (this.disableUsedPercentage) {
return usageStats; return Object.assign(this.usage, usageStats);
} }
try { try {
let usage = await api.usage(path); let usage = await api.usage(path);
@ -166,18 +177,11 @@ export default {
} catch (error) { } catch (error) {
this.$showError(error); this.$showError(error);
} }
return usageStats; return Object.assign(this.usage, usageStats);
}, },
default: { used: "0 B", total: "0 B", usedPercentage: 0 },
shouldUpdate() {
return this.$router.currentRoute.path.includes("/files/");
},
},
},
methods: {
toRoot() { toRoot() {
this.$router.push({ path: "/files/" }, () => {}); this.$router.push({ path: "/files" });
this.$store.commit("closeHovers"); this.closeHovers();
}, },
toAccountSettings() { toAccountSettings() {
this.$router.push({ path: "/settings/profile" }, () => {}); this.$router.push({ path: "/settings/profile" }, () => {});
@ -185,12 +189,17 @@ export default {
}, },
toSettings() { toSettings() {
this.$router.push({ path: "/settings/global" }, () => {}); this.$router.push({ path: "/settings/global" }, () => {});
this.$store.commit("closeHovers"); this.closeHovers();
}, },
help() { help() {
this.$store.commit("showHover", "help"); this.showHover("help");
}, },
logout: auth.logout, logout: auth.logout,
}, },
watch: {
isFiles(newValue) {
newValue && this.fetchUsage();
},
},
}; };
</script> </script>

View File

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

View File

@ -15,7 +15,7 @@
> >
<div> <div>
<img <img
v-if="readOnly == undefined && type === 'image' && isThumbsEnabled" v-if="!readOnly && type === 'image' && isThumbsEnabled"
v-lazy="thumbnailUrl" v-lazy="thumbnailUrl"
/> />
<i v-else class="material-icons"></i> <i v-else class="material-icons"></i>
@ -34,137 +34,153 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { useAuthStore } from "@/stores/auth";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { enableThumbs } from "@/utils/constants"; import { enableThumbs } from "@/utils/constants";
import { mapMutations, mapGetters, mapState } from "vuex";
import { filesize } from "@/utils"; import { filesize } from "@/utils";
import moment from "moment/min/moment-with-locales"; import dayjs from "dayjs";
import { files as api } from "@/api"; import { files as api } from "@/api";
import * as upload from "@/utils/upload"; import * as upload from "@/utils/upload";
import { computed, inject, ref } from "vue";
import { useRouter } from "vue-router";
export default { const touches = ref<number>(0);
name: "item",
data: function () {
return {
touches: 0,
};
},
props: [
"name",
"isDir",
"url",
"type",
"size",
"modified",
"index",
"readOnly",
"path",
],
computed: {
...mapState(["user", "selected", "req", "jwt"]),
...mapGetters(["selectedCount"]),
singleClick() {
return this.readOnly == undefined && this.user.singleClick;
},
isSelected() {
return this.selected.indexOf(this.index) !== -1;
},
isDraggable() {
return this.readOnly == undefined && this.user.perm.rename;
},
canDrop() {
if (!this.isDir || this.readOnly !== undefined) return false;
for (let i of this.selected) { const $showError = inject<IToastError>("$showError")!;
if (this.req.items[i].url === this.url) { const router = useRouter();
const props = defineProps<{
name: string;
isDir: boolean;
url: string;
type: string;
size: number;
modified: string;
index: number;
readOnly?: boolean;
path?: string;
}>();
const authStore = useAuthStore();
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
const singleClick = computed(
() => !props.readOnly && authStore.user?.singleClick
);
const isSelected = computed(
() => fileStore.selected.indexOf(props.index) !== -1
);
const isDraggable = computed(
() => !props.readOnly && authStore.user?.perm.rename
);
const canDrop = computed(() => {
if (!props.isDir || props.readOnly) return false;
for (let i of fileStore.selected) {
if (fileStore.req?.items[i].url === props.url) {
return false; return false;
} }
} }
return true; return true;
}, });
thumbnailUrl() {
const thumbnailUrl = computed(() => {
const file = { const file = {
path: this.path, path: props.path,
modified: this.modified, modified: props.modified,
}; };
return api.getPreviewURL(file, "thumb"); return api.getPreviewURL(file as Resource, "thumb");
}, });
isThumbsEnabled() {
const isThumbsEnabled = computed(() => {
return enableThumbs; return enableThumbs;
}, });
},
methods: { const humanSize = () => {
...mapMutations(["addSelected", "removeSelected", "resetSelected"]), return props.type == "invalid_link" ? "invalid link" : filesize(props.size);
humanSize: function () { };
return this.type == "invalid_link" ? "invalid link" : filesize(this.size);
}, const humanTime = () => {
humanTime: function () { if (!props.readOnly && authStore.user?.dateFormat) {
if (this.readOnly == undefined && this.user.dateFormat) { return dayjs(props.modified).format("L LT");
return moment(this.modified).format("L LT");
} }
return moment(this.modified).fromNow(); return dayjs(props.modified).fromNow();
}, };
dragStart: function () {
if (this.selectedCount === 0) { const dragStart = () => {
this.addSelected(this.index); if (fileStore.selectedCount === 0) {
fileStore.selected.push(props.index);
return; return;
} }
if (!this.isSelected) { if (!isSelected.value) {
this.resetSelected(); fileStore.selected = [];
this.addSelected(this.index); fileStore.selected.push(props.index);
} }
}, };
dragOver: function (event) {
if (!this.canDrop) return; const dragOver = (event: Event) => {
if (!canDrop.value) return;
event.preventDefault(); event.preventDefault();
let el = event.target; let el = event.target as HTMLElement | null;
if (el !== null) {
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
if (!el.classList.contains("item")) { if (!el?.classList.contains("item")) {
el = el.parentElement; el = el?.parentElement ?? null;
} }
} }
el.style.opacity = 1; if (el !== null) el.style.opacity = "1";
}, }
drop: async function (event) { };
if (!this.canDrop) return;
const drop = async (event: Event) => {
if (!canDrop.value) return;
event.preventDefault(); event.preventDefault();
if (this.selectedCount === 0) return; if (fileStore.selectedCount === 0) return;
let el = event.target; let el = event.target as HTMLElement | null;
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
if (el !== null && !el.classList.contains("item")) { if (el !== null && !el.classList.contains("item")) {
el = el.parentElement; el = el.parentElement;
} }
} }
let items = []; let items: any[] = [];
for (let i of this.selected) { for (let i of fileStore.selected) {
if (fileStore.req) {
items.push({ items.push({
from: this.req.items[i].url, from: fileStore.req?.items[i].url,
to: this.url + encodeURIComponent(this.req.items[i].name), to: props.url + encodeURIComponent(fileStore.req?.items[i].name),
name: this.req.items[i].name, name: fileStore.req?.items[i].name,
}); });
} }
}
// Get url from ListingItem instance // Get url from ListingItem instance
if (el === null) {
return;
}
let path = el.__vue__.url; let path = el.__vue__.url;
let baseItems = (await api.fetch(path)).items; let baseItems = (await api.fetch(path)).items;
let action = (overwrite, rename) => { let action = (overwrite: boolean, rename: boolean) => {
api api
.move(items, overwrite, rename) .move(items, overwrite, rename)
.then(() => { .then(() => {
this.$store.commit("setReload", true); fileStore.reload = true;
}) })
.catch(this.$showError); .catch($showError);
}; };
let conflict = upload.checkConflict(items, baseItems); let conflict = upload.checkConflict(items, baseItems);
@ -173,14 +189,14 @@ export default {
let rename = false; let rename = false;
if (conflict) { if (conflict) {
this.$store.commit("showHover", { layoutStore.showHover({
prompt: "replace-rename", prompt: "replace-rename",
confirm: (event, option) => { confirm: (event: Event, option: any) => {
overwrite = option == "overwrite"; overwrite = option == "overwrite";
rename = option == "rename"; rename = option == "rename";
event.preventDefault(); event.preventDefault();
this.$store.commit("closeHovers"); layoutStore.closeHovers();
action(overwrite, rename); action(overwrite, rename);
}, },
}); });
@ -189,48 +205,51 @@ export default {
} }
action(overwrite, rename); action(overwrite, rename);
}, };
itemClick: function (event) {
const itemClick = (event: Event | KeyboardEvent) => {
if ( if (
!(event.ctrlKey || event.metaKey) && !((event as KeyboardEvent).ctrlKey || (event as KeyboardEvent).metaKey) &&
this.singleClick && singleClick.value &&
!this.$store.state.multiple !fileStore.multiple
) )
this.open(); open();
else this.click(event); else click(event);
}, };
click: function (event) {
if (!this.singleClick && this.selectedCount !== 0) event.preventDefault(); const click = (event: Event | KeyboardEvent) => {
if (!singleClick.value && fileStore.selectedCount !== 0)
event.preventDefault();
setTimeout(() => { setTimeout(() => {
this.touches = 0; touches.value = 0;
}, 300); }, 300);
this.touches++; touches.value++;
if (this.touches > 1) { if (touches.value > 1) {
this.open(); open();
} }
if (this.$store.state.selected.indexOf(this.index) !== -1) { if (fileStore.selected.indexOf(props.index) !== -1) {
this.removeSelected(this.index); fileStore.removeSelected(props.index);
return; return;
} }
if (event.shiftKey && this.selected.length > 0) { if ((event as KeyboardEvent).shiftKey && fileStore.selected.length > 0) {
let fi = 0; let fi = 0;
let la = 0; let la = 0;
if (this.index > this.selected[0]) { if (props.index > fileStore.selected[0]) {
fi = this.selected[0] + 1; fi = fileStore.selected[0] + 1;
la = this.index; la = props.index;
} else { } else {
fi = this.index; fi = props.index;
la = this.selected[0] - 1; la = fileStore.selected[0] - 1;
} }
for (; fi <= la; fi++) { for (; fi <= la; fi++) {
if (this.$store.state.selected.indexOf(fi) == -1) { if (fileStore.selected.indexOf(fi) == -1) {
this.addSelected(fi); fileStore.selected.push(fi);
} }
} }
@ -238,17 +257,17 @@ export default {
} }
if ( if (
!this.singleClick && !singleClick.value &&
!event.ctrlKey && !(event as KeyboardEvent).ctrlKey &&
!event.metaKey && !(event as KeyboardEvent).metaKey &&
!this.$store.state.multiple !fileStore.multiple
) ) {
this.resetSelected(); fileStore.selected = [];
this.addSelected(this.index); }
}, fileStore.selected.push(props.index);
open: function () { };
this.$router.push({ path: this.url });
}, const open = () => {
}, router.push({ path: props.url });
}; };
</script> </script>

View File

@ -0,0 +1,104 @@
<template>
<video ref="videoPlayer" class="video-max video-js" controls>
<source :src="source" />
<track
kind="subtitles"
v-for="(sub, index) in subtitles"
:key="index"
:src="sub"
:label="subLabel(sub)"
:default="index === 0"
/>
<p class="vjs-no-js">
Sorry, your browser doesn't support embedded videos, but don't worry, you
can <a :href="source">download it</a>
and watch it with your favorite video player!
</p>
</video>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from "vue";
import videojs from "video.js";
import type Player from "video.js/dist/types/player";
import "videojs-mobile-ui";
import "videojs-hotkeys";
import "video.js/dist/video-js.min.css";
import "videojs-mobile-ui/dist/videojs-mobile-ui.css";
const videoPlayer = ref<HTMLElement | null>(null);
const player = ref<Player | null>(null);
const props = withDefaults(
defineProps<{
source: string;
subtitles?: string[];
options?: any;
}>(),
{
options: {},
}
);
onMounted(() => {
player.value = videojs(
videoPlayer.value!,
{
html5: {
// needed for customizable subtitles
// TODO: add to user settings
nativeTextTracks: false,
},
plugins: {
hotkeys: {
volumeStep: 0.1,
seekStep: 10,
enableModifiersForNumbers: false,
},
},
...props.options,
},
// onReady callback
async () => {
// player.value!.log("onPlayerReady", this);
}
);
// TODO: need to test on mobile
// @ts-ignore
player.value!.mobileUi();
});
onBeforeUnmount(() => {
if (player.value) {
player.value.dispose();
player.value = null;
}
});
const subLabel = (subUrl: string) => {
let url: URL;
try {
url = new URL(subUrl);
} catch (_) {
// treat it as a relative url
// we only need this for filename
url = new URL(subUrl, window.location.origin);
}
const label = decodeURIComponent(
url.pathname
.split("/")
.pop()!
.replace(/\.[^/.]+$/, "")
);
return label;
};
</script>
<style scoped>
.video-max {
width: 100%;
height: 100%;
}
</style>

View File

@ -2,24 +2,31 @@
<button @click="action" :aria-label="label" :title="label" class="action"> <button @click="action" :aria-label="label" :title="label" class="action">
<i class="material-icons">{{ icon }}</i> <i class="material-icons">{{ icon }}</i>
<span>{{ label }}</span> <span>{{ label }}</span>
<span v-if="counter > 0" class="counter">{{ counter }}</span> <span v-if="counter && counter > 0" class="counter">{{ counter }}</span>
</button> </button>
</template> </template>
<script> <script setup lang="ts">
export default { import { useLayoutStore } from "@/stores/layout";
name: "action",
props: ["icon", "label", "counter", "show"], const props = defineProps<{
methods: { icon?: string;
action: function () { label?: string;
if (this.show) { counter?: number;
this.$store.commit("showHover", this.show); show?: string;
}>();
const emit = defineEmits<{
(e: "action"): any;
}>();
const layoutStore = useLayoutStore();
const action = () => {
if (props.show) {
layoutStore.showHover(props.show);
} }
this.$emit("action"); emit("action");
},
},
}; };
</script> </script>
<style></style>

View File

@ -1,62 +1,59 @@
<template> <template>
<header> <header>
<img v-if="showLogo !== undefined" :src="logoURL" /> <img v-if="showLogo" :src="logoURL" />
<action <Action
v-if="showMenu !== undefined" v-if="showMenu"
class="menu-button" class="menu-button"
icon="menu" icon="menu"
:label="$t('buttons.toggleSidebar')" :label="t('buttons.toggleSidebar')"
@action="openSidebar()" @action="layoutStore.showHover('sidebar')"
/> />
<slot /> <slot />
<div id="dropdown" :class="{ active: this.currentPromptName === 'more' }"> <div
id="dropdown"
:class="{ active: layoutStore.currentPromptName === 'more' }"
>
<slot name="actions" /> <slot name="actions" />
</div> </div>
<action <Action
v-if="this.$slots.actions" v-if="ifActionsSlot"
id="more" id="more"
icon="more_vert" icon="more_vert"
:label="$t('buttons.more')" :label="t('buttons.more')"
@action="$store.commit('showHover', 'more')" @action="layoutStore.showHover('more')"
/> />
<div <div
class="overlay" class="overlay"
v-show="this.currentPromptName == 'more'" v-show="layoutStore.currentPromptName == 'more'"
@click="$store.commit('closeHovers')" @click="layoutStore.closeHovers"
/> />
</header> </header>
</template> </template>
<script> <script setup lang="ts">
import { useLayoutStore } from "@/stores/layout";
import { logoURL } from "@/utils/constants"; import { logoURL } from "@/utils/constants";
import Action from "@/components/header/Action.vue"; import Action from "@/components/header/Action.vue";
import { mapGetters } from "vuex"; import { computed, useSlots } from "vue";
import { useI18n } from "vue-i18n";
export default { defineProps<{
name: "header-bar", showLogo?: boolean;
props: ["showLogo", "showMenu"], showMenu?: boolean;
components: { }>();
Action,
}, const layoutStore = useLayoutStore();
data: function () { const slots = useSlots();
return {
logoURL, const { t } = useI18n();
};
}, const ifActionsSlot = computed(() => (slots.actions ? true : false));
methods: {
openSidebar() {
this.$store.commit("showHover", "sidebar");
},
},
computed: {
...mapGetters(["currentPromptName"]),
},
};
</script> </script>
<style></style> <style></style>

View File

@ -0,0 +1,21 @@
<template>
<VueFinalModal
overlay-transition="vfm-fade"
content-transition="vfm-fade"
@closed="layoutStore.closeHovers"
:focus-trap="{
initialFocus: '#focus-prompt',
fallbackFocus: 'div.vfm__content',
}"
style="z-index: 9999999"
>
<slot />
</VueFinalModal>
</template>
<script setup lang="ts">
import { VueFinalModal } from "vue-final-modal";
import { useLayoutStore } from "@/stores/layout";
const layoutStore = useLayoutStore();
</script>

View File

@ -6,8 +6,11 @@
<div class="card-content"> <div class="card-content">
<p>{{ $t("prompts.copyMessage") }}</p> <p>{{ $t("prompts.copyMessage") }}</p>
<file-list ref="fileList" @update:selected="(val) => (dest = val)"> <file-list
</file-list> ref="fileList"
@update:selected="(val) => (dest = val)"
tabindex="1"
/>
</div> </div>
<div <div
@ -28,17 +31,20 @@
<div> <div>
<button <button
class="button button--flat button--grey" class="button button--flat button--grey"
@click="$store.commit('closeHovers')" @click="closeHovers"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
tabindex="3"
> >
{{ $t("buttons.cancel") }} {{ $t("buttons.cancel") }}
</button> </button>
<button <button
id="focus-prompt"
class="button button--flat" class="button button--flat"
@click="copy" @click="copy"
:aria-label="$t('buttons.copy')" :aria-label="$t('buttons.copy')"
:title="$t('buttons.copy')" :title="$t('buttons.copy')"
tabindex="2"
> >
{{ $t("buttons.copy") }} {{ $t("buttons.copy") }}
</button> </button>
@ -48,7 +54,10 @@
</template> </template>
<script> <script>
import { mapState } from "vuex"; import { mapActions, mapState, mapWritableState } from "pinia";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { useAuthStore } from "@/stores/auth";
import FileList from "./FileList.vue"; import FileList from "./FileList.vue";
import { files as api } from "@/api"; import { files as api } from "@/api";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
@ -63,8 +72,14 @@ export default {
dest: null, dest: null,
}; };
}, },
computed: mapState(["req", "selected", "user"]), inject: ["$showError"],
computed: {
...mapState(useFileStore, ["req", "selected"]),
...mapState(useAuthStore, ["user"]),
...mapWritableState(useFileStore, ["reload"]),
},
methods: { methods: {
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
copy: async function (event) { copy: async function (event) {
event.preventDefault(); event.preventDefault();
let items = []; let items = [];
@ -87,7 +102,7 @@ export default {
buttons.success("copy"); buttons.success("copy");
if (this.$route.path === this.dest) { if (this.$route.path === this.dest) {
this.$store.commit("setReload", true); this.reload = true;
return; return;
} }
@ -101,7 +116,7 @@ export default {
}; };
if (this.$route.path === this.dest) { if (this.$route.path === this.dest) {
this.$store.commit("closeHovers"); this.closeHovers();
action(false, true); action(false, true);
return; return;
@ -114,14 +129,14 @@ export default {
let rename = false; let rename = false;
if (conflict) { if (conflict) {
this.$store.commit("showHover", { this.showHover({
prompt: "replace-rename", prompt: "replace-rename",
confirm: (event, option) => { confirm: (event, option) => {
overwrite = option == "overwrite"; overwrite = option == "overwrite";
rename = option == "rename"; rename = option == "rename";
event.preventDefault(); event.preventDefault();
this.$store.commit("closeHovers"); this.closeHovers();
action(overwrite, rename); action(overwrite, rename);
}, },
}); });

View File

@ -10,18 +10,21 @@
</div> </div>
<div class="card-action"> <div class="card-action">
<button <button
@click="$store.commit('closeHovers')" @click="closeHovers"
class="button button--flat button--grey" class="button button--flat button--grey"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
tabindex="2"
> >
{{ $t("buttons.cancel") }} {{ $t("buttons.cancel") }}
</button> </button>
<button <button
id="focus-prompt"
@click="submit" @click="submit"
class="button button--flat button--red" class="button button--flat button--red"
:aria-label="$t('buttons.delete')" :aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')" :title="$t('buttons.delete')"
tabindex="1"
> >
{{ $t("buttons.delete") }} {{ $t("buttons.delete") }}
</button> </button>
@ -30,18 +33,27 @@
</template> </template>
<script> <script>
import { mapGetters, mapMutations, mapState } from "vuex"; import { mapActions, mapState, mapWritableState } from "pinia";
import { files as api } from "@/api"; import { files as api } from "@/api";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
export default { export default {
name: "delete", name: "delete",
inject: ["$showError"],
computed: { computed: {
...mapGetters(["isListing", "selectedCount", "currentPrompt"]), ...mapState(useFileStore, [
...mapState(["req", "selected"]), "isListing",
"selectedCount",
"req",
"selected",
"currentPrompt",
]),
...mapWritableState(useFileStore, ["reload"]),
}, },
methods: { methods: {
...mapMutations(["closeHovers"]), ...mapActions(useLayoutStore, ["closeHovers"]),
submit: async function () { submit: async function () {
buttons.loading("delete"); buttons.loading("delete");
@ -69,11 +81,11 @@ export default {
await Promise.all(promises); await Promise.all(promises);
buttons.success("delete"); buttons.success("delete");
this.$store.commit("setReload", true); this.reload = true;
} catch (e) { } catch (e) {
buttons.done("delete"); buttons.done("delete");
this.$showError(e); this.$showError(e);
if (this.isListing) this.$store.commit("setReload", true); if (this.isListing) this.reload = true;
} }
}, },
}, },

View File

@ -0,0 +1,40 @@
<template>
<div class="card floating">
<div class="card-content">
<p>{{ t("prompts.deleteUser") }}</p>
</div>
<div class="card-action">
<button
id="focus-prompt"
class="button button--flat button--grey"
@click="layoutStore.closeHovers"
:aria-label="t('buttons.cancel')"
:title="t('buttons.cancel')"
tabindex="1"
>
{{ t("buttons.cancel") }}
</button>
<button
class="button button--flat"
@click="layoutStore.currentPrompt?.confirm()"
tabindex="2"
>
{{ t("buttons.delete") }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { useLayoutStore } from "@/stores/layout";
import { useI18n } from "vue-i18n";
const layoutStore = useLayoutStore();
const { t } = useI18n();
// const emit = defineEmits<{
// (e: "confirm"): void;
// }>();
</script>

View File

@ -7,18 +7,21 @@
</div> </div>
<div class="card-action"> <div class="card-action">
<button <button
@click="$store.commit('closeHovers')"
class="button button--flat button--grey" class="button button--flat button--grey"
@click="closeHovers"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
tabindex="2"
> >
{{ $t("buttons.cancel") }} {{ $t("buttons.cancel") }}
</button> </button>
<button <button
id="focus-prompt"
@click="submit" @click="submit"
class="button button--flat button--red" class="button button--flat button--red"
:aria-label="$t('buttons.discardChanges')" :aria-label="$t('buttons.discardChanges')"
:title="$t('buttons.discardChanges')" :title="$t('buttons.discardChanges')"
tabindex="1"
> >
{{ $t("buttons.discardChanges") }} {{ $t("buttons.discardChanges") }}
</button> </button>
@ -27,15 +30,18 @@
</template> </template>
<script> <script>
import { mapMutations } from "vuex"; import { mapActions } from "pinia";
import url from "@/utils/url"; import url from "@/utils/url";
import { useLayoutStore } from "@/stores/layout";
import { useFileStore } from "@/stores/file";
export default { export default {
name: "discardEditorChanges", name: "discardEditorChanges",
methods: { methods: {
...mapMutations(["closeHovers"]), ...mapActions(useLayoutStore, ["closeHovers"]),
...mapActions(useFileStore, ["updateRequest"]),
submit: async function () { submit: async function () {
this.$store.commit("updateRequest", {}); this.updateRequest(null);
let uri = url.removeLastDir(this.$route.path) + "/"; let uri = url.removeLastDir(this.$route.path) + "/";
this.$router.push({ path: uri }); this.$router.push({ path: uri });

View File

@ -1,18 +1,18 @@
<template> <template>
<div class="card floating" id="download"> <div class="card floating" id="download">
<div class="card-title"> <div class="card-title">
<h2>{{ $t("prompts.download") }}</h2> <h2>{{ t("prompts.download") }}</h2>
</div> </div>
<div class="card-content"> <div class="card-content">
<p>{{ $t("prompts.downloadMessage") }}</p> <p>{{ t("prompts.downloadMessage") }}</p>
<button <button
id="focus-prompt"
v-for="(ext, format) in formats" v-for="(ext, format) in formats"
:key="format" :key="format"
class="button button--block" class="button button--block"
@click="currentPrompt.confirm(format)" @click="layoutStore.currentPrompt?.confirm(format)"
v-focus
> >
{{ ext }} {{ ext }}
</button> </button>
@ -20,14 +20,15 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapGetters } from "vuex"; import { useI18n } from "vue-i18n";
import { useLayoutStore } from "@/stores/layout";
export default { const layoutStore = useLayoutStore();
name: "download",
data: function () { const { t } = useI18n();
return {
formats: { const formats = {
zip: "zip", zip: "zip",
tar: "tar", tar: "tar",
targz: "tar.gz", targz: "tar.gz",
@ -35,11 +36,5 @@ export default {
tarxz: "tar.xz", tarxz: "tar.xz",
tarlz4: "tar.lz4", tarlz4: "tar.lz4",
tarsz: "tar.sz", tarsz: "tar.sz",
},
};
},
computed: {
...mapGetters(["currentPrompt"]),
},
}; };
</script> </script>

View File

@ -25,7 +25,10 @@
</template> </template>
<script> <script>
import { mapState } from "vuex"; import { mapState } from "pinia";
import { useAuthStore } from "@/stores/auth";
import { useFileStore } from "@/stores/file";
import url from "@/utils/url"; import url from "@/utils/url";
import { files } from "@/api"; import { files } from "@/api";
@ -42,8 +45,10 @@ export default {
current: window.location.pathname, current: window.location.pathname,
}; };
}, },
inject: ["$showError"],
computed: { computed: {
...mapState(["req", "user"]), ...mapState(useAuthStore, ["user"]),
...mapState(useFileStore, ["req"]),
nav() { nav() {
return decodeURIComponent(this.current); return decodeURIComponent(this.current);
}, },

View File

@ -20,11 +20,13 @@
<div class="card-action"> <div class="card-action">
<button <button
id="focus-prompt"
type="submit" type="submit"
@click="$store.commit('closeHovers')" @click="closeHovers"
class="button button--flat" class="button button--flat"
:aria-label="$t('buttons.ok')" :aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')" :title="$t('buttons.ok')"
tabindex="1"
> >
{{ $t("buttons.ok") }} {{ $t("buttons.ok") }}
</button> </button>
@ -33,5 +35,13 @@
</template> </template>
<script> <script>
export default { name: "help" }; import { mapActions } from "pinia";
import { useLayoutStore } from "@/stores/layout";
export default {
name: "help",
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
};
</script> </script>

View File

@ -40,33 +40,45 @@
<p> <p>
<strong>MD5: </strong <strong>MD5: </strong
><code ><code
><a @click="checksum($event, 'md5')">{{ ><a
$t("prompts.show") @click="checksum($event, 'md5')"
}}</a></code @keypress.enter="checksum($event, 'md5')"
tabindex="2"
>{{ $t("prompts.show") }}</a
></code
> >
</p> </p>
<p> <p>
<strong>SHA1: </strong <strong>SHA1: </strong
><code ><code
><a @click="checksum($event, 'sha1')">{{ ><a
$t("prompts.show") @click="checksum($event, 'sha1')"
}}</a></code @keypress.enter="checksum($event, 'sha1')"
tabindex="3"
>{{ $t("prompts.show") }}</a
></code
> >
</p> </p>
<p> <p>
<strong>SHA256: </strong <strong>SHA256: </strong
><code ><code
><a @click="checksum($event, 'sha256')">{{ ><a
$t("prompts.show") @click="checksum($event, 'sha256')"
}}</a></code @keypress.enter="checksum($event, 'sha256')"
tabindex="4"
>{{ $t("prompts.show") }}</a
></code
> >
</p> </p>
<p> <p>
<strong>SHA512: </strong <strong>SHA512: </strong
><code ><code
><a @click="checksum($event, 'sha512')">{{ ><a
$t("prompts.show") @click="checksum($event, 'sha512')"
}}</a></code @keypress.enter="checksum($event, 'sha512')"
tabindex="5"
>{{ $t("prompts.show") }}</a
></code
> >
</p> </p>
</template> </template>
@ -74,8 +86,9 @@
<div class="card-action"> <div class="card-action">
<button <button
id="focus-prompt"
type="submit" type="submit"
@click="$store.commit('closeHovers')" @click="closeHovers"
class="button button--flat" class="button button--flat"
:aria-label="$t('buttons.ok')" :aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')" :title="$t('buttons.ok')"
@ -87,16 +100,23 @@
</template> </template>
<script> <script>
import { mapState, mapGetters } from "vuex"; import { mapActions, mapState } from "pinia";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { filesize } from "@/utils"; import { filesize } from "@/utils";
import moment from "moment/min/moment-with-locales"; import dayjs from "dayjs";
import { files as api } from "@/api"; import { files as api } from "@/api";
export default { export default {
name: "info", name: "info",
inject: ["$showError"],
computed: { computed: {
...mapState(["req", "selected"]), ...mapState(useFileStore, [
...mapGetters(["selectedCount", "isListing"]), "req",
"selected",
"selectedCount",
"isListing",
]),
humanSize: function () { humanSize: function () {
if (this.selectedCount === 0 || !this.isListing) { if (this.selectedCount === 0 || !this.isListing) {
return filesize(this.req.size); return filesize(this.req.size);
@ -112,13 +132,19 @@ export default {
}, },
humanTime: function () { humanTime: function () {
if (this.selectedCount === 0) { if (this.selectedCount === 0) {
return moment(this.req.modified).fromNow(); return dayjs(this.req.modified).fromNow();
} }
return moment(this.req.items[this.selected[0]].modified).fromNow(); return dayjs(this.req.items[this.selected[0]].modified).fromNow();
}, },
modTime: function () { modTime: function () {
if (this.selectedCount === 0) {
return new Date(Date.parse(this.req.modified)).toLocaleString(); return new Date(Date.parse(this.req.modified)).toLocaleString();
}
return new Date(
Date.parse(this.req.items[this.selected[0]].modified)
).toLocaleString();
}, },
name: function () { name: function () {
return this.selectedCount === 0 return this.selectedCount === 0
@ -146,6 +172,7 @@ export default {
}, },
}, },
methods: { methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
checksum: async function (event, algo) { checksum: async function (event, algo) {
event.preventDefault(); event.preventDefault();
@ -159,8 +186,7 @@ export default {
try { try {
const hash = await api.checksum(link, algo); const hash = await api.checksum(link, algo);
// eslint-disable-next-line event.target.textContent = hash;
event.target.innerHTML = hash;
} catch (e) { } catch (e) {
this.$showError(e); this.$showError(e);
} }

View File

@ -5,8 +5,11 @@
</div> </div>
<div class="card-content"> <div class="card-content">
<file-list ref="fileList" @update:selected="(val) => (dest = val)"> <file-list
</file-list> ref="fileList"
@update:selected="(val) => (dest = val)"
tabindex="1"
/>
</div> </div>
<div <div
@ -27,18 +30,21 @@
<div> <div>
<button <button
class="button button--flat button--grey" class="button button--flat button--grey"
@click="$store.commit('closeHovers')" @click="closeHovers"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
tabindex="3"
> >
{{ $t("buttons.cancel") }} {{ $t("buttons.cancel") }}
</button> </button>
<button <button
id="focus-prompt"
class="button button--flat" class="button button--flat"
@click="move" @click="move"
:disabled="$route.path === dest" :disabled="$route.path === dest"
:aria-label="$t('buttons.move')" :aria-label="$t('buttons.move')"
:title="$t('buttons.move')" :title="$t('buttons.move')"
tabindex="2"
> >
{{ $t("buttons.move") }} {{ $t("buttons.move") }}
</button> </button>
@ -48,7 +54,10 @@
</template> </template>
<script> <script>
import { mapState } from "vuex"; import { mapActions, mapState } from "pinia";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { useAuthStore } from "@/stores/auth";
import FileList from "./FileList.vue"; import FileList from "./FileList.vue";
import { files as api } from "@/api"; import { files as api } from "@/api";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
@ -63,8 +72,13 @@ export default {
dest: null, dest: null,
}; };
}, },
computed: mapState(["req", "selected", "user"]), inject: ["$showError"],
computed: {
...mapState(useFileStore, ["req", "selected"]),
...mapState(useAuthStore, ["user"]),
},
methods: { methods: {
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
move: async function (event) { move: async function (event) {
event.preventDefault(); event.preventDefault();
let items = []; let items = [];
@ -99,14 +113,14 @@ export default {
let rename = false; let rename = false;
if (conflict) { if (conflict) {
this.$store.commit("showHover", { this.showHover({
prompt: "replace-rename", prompt: "replace-rename",
confirm: (event, option) => { confirm: (event, option) => {
overwrite = option == "overwrite"; overwrite = option == "overwrite";
rename = option == "rename"; rename = option == "rename";
event.preventDefault(); event.preventDefault();
this.$store.commit("closeHovers"); this.closeHovers();
action(overwrite, rename); action(overwrite, rename);
}, },
}); });

View File

@ -1,98 +1,104 @@
<template> <template>
<div class="card floating"> <div class="card floating">
<div class="card-title"> <div class="card-title">
<h2>{{ $t("prompts.newDir") }}</h2> <h2>{{ t("prompts.newDir") }}</h2>
</div> </div>
<div class="card-content"> <div class="card-content">
<p>{{ $t("prompts.newDirMessage") }}</p> <p>{{ t("prompts.newDirMessage") }}</p>
<input <input
id="focus-prompt"
class="input input--block" class="input input--block"
type="text" type="text"
@keyup.enter="submit" @keyup.enter="submit"
v-model.trim="name" v-model.trim="name"
v-focus tabindex="1"
/> />
</div> </div>
<div class="card-action"> <div class="card-action">
<button <button
class="button button--flat button--grey" class="button button--flat button--grey"
@click="$store.commit('closeHovers')" @click="layoutStore.closeHovers"
:aria-label="$t('buttons.cancel')" :aria-label="t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="t('buttons.cancel')"
tabindex="3"
> >
{{ $t("buttons.cancel") }} {{ t("buttons.cancel") }}
</button> </button>
<button <button
class="button button--flat" class="button button--flat"
:aria-label="$t('buttons.create')" :aria-label="$t('buttons.create')"
:title="$t('buttons.create')" :title="t('buttons.create')"
@click="submit" @click="submit"
tabindex="2"
> >
{{ $t("buttons.create") }} {{ t("buttons.create") }}
</button> </button>
</div> </div>
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapGetters } from "vuex"; import { inject, ref } from "vue";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { files as api } from "@/api"; import { files as api } from "@/api";
import url from "@/utils/url"; import url from "@/utils/url";
import { useRoute, useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
export default { const $showError = inject<IToastError>("$showError")!;
name: "new-dir",
props: { const props = defineProps({
base: String,
redirect: { redirect: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
base: { });
type: [String, null],
default: null, const fileStore = useFileStore();
}, const layoutStore = useLayoutStore();
},
data: function () { const route = useRoute();
return { const router = useRouter();
name: "", const { t } = useI18n();
};
}, const name = ref<string>("");
computed: {
...mapGetters(["isFiles", "isListing"]), const submit = async (event: Event) => {
},
methods: {
submit: async function (event) {
event.preventDefault(); event.preventDefault();
if (this.new === "") return; if (name.value === "") return;
// Build the path of the new directory. // Build the path of the new directory.
let uri; let uri: string;
if (props.base) uri = props.base;
if (this.base) uri = this.base; else if (fileStore.isFiles) uri = route.path + "/";
else if (this.isFiles) uri = this.$route.path + "/";
else uri = "/"; else uri = "/";
if (!this.isListing) { if (!fileStore.isListing) {
uri = url.removeLastDir(uri) + "/"; uri = url.removeLastDir(uri) + "/";
} }
uri += encodeURIComponent(this.name) + "/"; uri += encodeURIComponent(name.value) + "/";
uri = uri.replace("//", "/"); uri = uri.replace("//", "/");
try { try {
await api.post(uri); await api.post(uri);
if (this.redirect) { if (props.redirect) {
this.$router.push({ path: uri }); router.push({ path: uri });
} else if (!this.base) { } else if (!props.base) {
const res = await api.fetch(url.removeLastDir(uri) + "/"); const res = await api.fetch(url.removeLastDir(uri) + "/");
this.$store.commit("updateRequest", res); fileStore.updateRequest(res);
} }
} catch (e) { } catch (e) {
this.$showError(e); if (e instanceof Error) {
$showError(e);
}
} }
this.$store.commit("closeHovers"); layoutStore.closeHovers();
},
},
}; };
</script> </script>

View File

@ -1,14 +1,14 @@
<template> <template>
<div class="card floating"> <div class="card floating">
<div class="card-title"> <div class="card-title">
<h2>{{ $t("prompts.newFile") }}</h2> <h2>{{ t("prompts.newFile") }}</h2>
</div> </div>
<div class="card-content"> <div class="card-content">
<p>{{ $t("prompts.newFileMessage") }}</p> <p>{{ t("prompts.newFileMessage") }}</p>
<input <input
id="focus-prompt"
class="input input--block" class="input input--block"
v-focus
type="text" type="text"
@keyup.enter="submit" @keyup.enter="submit"
v-model.trim="name" v-model.trim="name"
@ -18,63 +18,68 @@
<div class="card-action"> <div class="card-action">
<button <button
class="button button--flat button--grey" class="button button--flat button--grey"
@click="$store.commit('closeHovers')" @click="layoutStore.closeHovers"
:aria-label="$t('buttons.cancel')" :aria-label="t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="t('buttons.cancel')"
> >
{{ $t("buttons.cancel") }} {{ t("buttons.cancel") }}
</button> </button>
<button <button
class="button button--flat" class="button button--flat"
@click="submit" @click="submit"
:aria-label="$t('buttons.create')" :aria-label="t('buttons.create')"
:title="$t('buttons.create')" :title="t('buttons.create')"
> >
{{ $t("buttons.create") }} {{ t("buttons.create") }}
</button> </button>
</div> </div>
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapGetters } from "vuex"; import { inject, ref } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute, useRouter } from "vue-router";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { files as api } from "@/api"; import { files as api } from "@/api";
import url from "@/utils/url"; import url from "@/utils/url";
export default { const $showError = inject<IToastError>("$showError")!;
name: "new-file",
data: function () { const fileStore = useFileStore();
return { const layoutStore = useLayoutStore();
name: "",
}; const route = useRoute();
}, const router = useRouter();
computed: { const { t } = useI18n();
...mapGetters(["isFiles", "isListing"]),
}, const name = ref<string>("");
methods: {
submit: async function (event) { const submit = async (event: Event) => {
event.preventDefault(); event.preventDefault();
if (this.new === "") return; if (name.value === "") return;
// Build the path of the new directory. // Build the path of the new directory.
let uri = this.isFiles ? this.$route.path + "/" : "/"; let uri = fileStore.isFiles ? route.path + "/" : "/";
if (!this.isListing) { if (!fileStore.isListing) {
uri = url.removeLastDir(uri) + "/"; uri = url.removeLastDir(uri) + "/";
} }
uri += encodeURIComponent(this.name); uri += encodeURIComponent(name.value);
uri = uri.replace("//", "/"); uri = uri.replace("//", "/");
try { try {
await api.post(uri); await api.post(uri);
this.$router.push({ path: uri }); router.push({ path: uri });
} catch (e) { } catch (e) {
this.$showError(e); if (e instanceof Error) {
$showError(e);
}
} }
this.$store.commit("closeHovers"); layoutStore.closeHovers();
},
},
}; };
</script> </script>

View File

@ -1,22 +1,20 @@
<template> <template>
<div> <ModalsContainer />
<component
v-if="showOverlay"
:ref="currentPromptName"
:is="currentPromptName"
v-bind="currentPrompt.props"
>
</component>
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
</div>
</template> </template>
<script> <script setup lang="ts">
import { ref, watch } from "vue";
import { ModalsContainer, useModal } from "vue-final-modal";
import { storeToRefs } from "pinia";
import { useLayoutStore } from "@/stores/layout";
import BaseModal from "./BaseModal.vue";
import Help from "./Help.vue"; import Help from "./Help.vue";
import Info from "./Info.vue"; import Info from "./Info.vue";
import Delete from "./Delete.vue"; import Delete from "./Delete.vue";
import Rename from "./Rename.vue"; import DeleteUser from "./DeleteUser.vue";
import Download from "./Download.vue"; import Download from "./Download.vue";
import Rename from "./Rename.vue";
import Move from "./Move.vue"; import Move from "./Move.vue";
import Copy from "./Copy.vue"; import Copy from "./Copy.vue";
import NewFile from "./NewFile.vue"; import NewFile from "./NewFile.vue";
@ -24,87 +22,61 @@ import NewDir from "./NewDir.vue";
import Replace from "./Replace.vue"; import Replace from "./Replace.vue";
import ReplaceRename from "./ReplaceRename.vue"; import ReplaceRename from "./ReplaceRename.vue";
import Share from "./Share.vue"; import Share from "./Share.vue";
import Upload from "./Upload.vue";
import ShareDelete from "./ShareDelete.vue"; import ShareDelete from "./ShareDelete.vue";
import Sidebar from "../Sidebar.vue"; import Upload from "./Upload.vue";
import DiscardEditorChanges from "./DiscardEditorChanges.vue"; import DiscardEditorChanges from "./DiscardEditorChanges.vue";
import { mapGetters, mapState } from "vuex";
import buttons from "@/utils/buttons";
export default { const layoutStore = useLayoutStore();
name: "prompts",
components: { const { currentPromptName } = storeToRefs(layoutStore);
Info,
Delete, const closeModal = ref<() => Promise<string>>();
Rename,
Download, const components = new Map<string, any>([
Move, ["info", Info],
Copy, ["help", Help],
Share, ["delete", Delete],
NewFile, ["rename", Rename],
NewDir, ["move", Move],
Help, ["copy", Copy],
Replace, ["newFile", NewFile],
ReplaceRename, ["newDir", NewDir],
Upload, ["download", Download],
ShareDelete, ["replace", Replace],
Sidebar, ["replace-rename", ReplaceRename],
DiscardEditorChanges, ["share", Share],
["upload", Upload],
["share-delete", ShareDelete],
["deleteUser", DeleteUser],
["discardEditorChanges", DiscardEditorChanges],
]);
watch(currentPromptName, (newValue) => {
if (closeModal.value) {
closeModal.value();
closeModal.value = undefined;
}
const modal = components.get(newValue!);
if (!modal) return;
const { open, close } = useModal({
component: BaseModal,
slots: {
default: modal,
}, },
data: function () { });
return {
pluginData: { closeModal.value = close;
buttons, open();
store: this.$store, });
router: this.$router,
},
};
},
created() {
window.addEventListener("keydown", (event) => { window.addEventListener("keydown", (event) => {
if (this.currentPrompt == null) return; if (!layoutStore.currentPrompt) return;
const promptName = this.currentPrompt.prompt; if (event.key === "Escape") {
const prompt = this.$refs[promptName];
if (event.code === "Escape") {
event.stopImmediatePropagation(); event.stopImmediatePropagation();
this.$store.commit("closeHovers"); layoutStore.closeHovers();
}
if (event.code === "Enter") {
switch (promptName) {
case "delete":
prompt.submit();
break;
case "copy":
prompt.copy(event);
break;
case "move":
prompt.move(event);
break;
case "replace":
prompt.showConfirm(event);
break;
}
} }
}); });
},
computed: {
...mapState(["plugins"]),
...mapGetters(["currentPrompt", "currentPromptName"]),
showOverlay: function () {
return (
this.currentPrompt !== null &&
this.currentPrompt.prompt !== "search" &&
this.currentPrompt.prompt !== "more"
);
},
},
methods: {
resetPrompts() {
this.$store.commit("closeHovers");
},
},
};
</script> </script>

View File

@ -10,8 +10,8 @@
>: >:
</p> </p>
<input <input
id="focus-prompt"
class="input input--block" class="input input--block"
v-focus
type="text" type="text"
@keyup.enter="submit" @keyup.enter="submit"
v-model.trim="name" v-model.trim="name"
@ -21,7 +21,7 @@
<div class="card-action"> <div class="card-action">
<button <button
class="button button--flat button--grey" class="button button--flat button--grey"
@click="$store.commit('closeHovers')" @click="closeHovers"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
> >
@ -41,7 +41,9 @@
</template> </template>
<script> <script>
import { mapState, mapGetters } from "vuex"; import { mapActions, mapState, mapWritableState } from "pinia";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import url from "@/utils/url"; import url from "@/utils/url";
import { files as api } from "@/api"; import { files as api } from "@/api";
@ -55,13 +57,20 @@ export default {
created() { created() {
this.name = this.oldName(); this.name = this.oldName();
}, },
inject: ["$showError"],
computed: { computed: {
...mapState(["req", "selected", "selectedCount"]), ...mapState(useFileStore, [
...mapGetters(["isListing"]), "req",
"selected",
"selectedCount",
"isListing",
]),
...mapWritableState(useFileStore, ["reload"]),
}, },
methods: { methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
cancel: function () { cancel: function () {
this.$store.commit("closeHovers"); this.closeHovers();
}, },
oldName: function () { oldName: function () {
if (!this.isListing) { if (!this.isListing) {
@ -96,12 +105,12 @@ export default {
return; return;
} }
this.$store.commit("setReload", true); this.reload = true;
} catch (e) { } catch (e) {
this.$showError(e); this.$showError(e);
} }
this.$store.commit("closeHovers"); this.closeHovers();
}, },
}, },
}; };

View File

@ -11,9 +11,10 @@
<div class="card-action"> <div class="card-action">
<button <button
class="button button--flat button--grey" class="button button--flat button--grey"
@click="$store.commit('closeHovers')" @click="closeHovers"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
tabindex="3"
> >
{{ $t("buttons.cancel") }} {{ $t("buttons.cancel") }}
</button> </button>
@ -22,14 +23,17 @@
@click="currentPrompt.action" @click="currentPrompt.action"
:aria-label="$t('buttons.continue')" :aria-label="$t('buttons.continue')"
:title="$t('buttons.continue')" :title="$t('buttons.continue')"
tabindex="2"
> >
{{ $t("buttons.continue") }} {{ $t("buttons.continue") }}
</button> </button>
<button <button
id="focus-prompt"
class="button button--flat button--red" class="button button--flat button--red"
@click="currentPrompt.confirm" @click="currentPrompt.confirm"
:aria-label="$t('buttons.replace')" :aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')" :title="$t('buttons.replace')"
tabindex="1"
> >
{{ $t("buttons.replace") }} {{ $t("buttons.replace") }}
</button> </button>
@ -38,10 +42,16 @@
</template> </template>
<script> <script>
import { mapGetters } from "vuex"; import { mapActions, mapState } from "pinia";
import { useLayoutStore } from "@/stores/layout";
export default { export default {
name: "replace", name: "replace",
computed: mapGetters(["currentPrompt"]), computed: {
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
}; };
</script> </script>

View File

@ -11,9 +11,10 @@
<div class="card-action"> <div class="card-action">
<button <button
class="button button--flat button--grey" class="button button--flat button--grey"
@click="$store.commit('closeHovers')" @click="closeHovers"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
tabindex="3"
> >
{{ $t("buttons.cancel") }} {{ $t("buttons.cancel") }}
</button> </button>
@ -22,14 +23,17 @@
@click="(event) => currentPrompt.confirm(event, 'rename')" @click="(event) => currentPrompt.confirm(event, 'rename')"
:aria-label="$t('buttons.rename')" :aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')" :title="$t('buttons.rename')"
tabindex="2"
> >
{{ $t("buttons.rename") }} {{ $t("buttons.rename") }}
</button> </button>
<button <button
id="focus-prompt"
class="button button--flat button--red" class="button button--flat button--red"
@click="(event) => currentPrompt.confirm(event, 'overwrite')" @click="(event) => currentPrompt.confirm(event, 'overwrite')"
:aria-label="$t('buttons.replace')" :aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')" :title="$t('buttons.replace')"
tabindex="1"
> >
{{ $t("buttons.replace") }} {{ $t("buttons.replace") }}
</button> </button>
@ -38,10 +42,16 @@
</template> </template>
<script> <script>
import { mapGetters } from "vuex"; import { mapActions, mapState } from "pinia";
import { useLayoutStore } from "@/stores/layout";
export default { export default {
name: "replace-rename", name: "replace-rename",
computed: mapGetters(["currentPrompt"]), computed: {
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
}; };
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="card floating share__promt__card" id="share"> <div class="card floating" id="share">
<div class="card-title"> <div class="card-title">
<h2>{{ $t("buttons.share") }}</h2> <h2>{{ $t("buttons.share") }}</h2>
</div> </div>
@ -25,9 +25,9 @@
<td class="small"> <td class="small">
<button <button
class="action copy-clipboard" class="action copy-clipboard"
:data-clipboard-text="buildLink(link)"
:aria-label="$t('buttons.copyToClipboard')" :aria-label="$t('buttons.copyToClipboard')"
:title="$t('buttons.copyToClipboard')" :title="$t('buttons.copyToClipboard')"
@click="copyToClipboard(buildLink(link))"
> >
<i class="material-icons">content_paste</i> <i class="material-icons">content_paste</i>
</button> </button>
@ -35,9 +35,9 @@
<td class="small" v-if="hasDownloadLink()"> <td class="small" v-if="hasDownloadLink()">
<button <button
class="action copy-clipboard" class="action copy-clipboard"
:data-clipboard-text="buildDownloadLink(link)"
:aria-label="$t('buttons.copyDownloadLinkToClipboard')" :aria-label="$t('buttons.copyDownloadLinkToClipboard')"
:title="$t('buttons.copyDownloadLinkToClipboard')" :title="$t('buttons.copyDownloadLinkToClipboard')"
@click="copyToClipboard(buildDownloadLink(link))"
> >
<i class="material-icons">content_paste_go</i> <i class="material-icons">content_paste_go</i>
</button> </button>
@ -59,17 +59,20 @@
<div class="card-action"> <div class="card-action">
<button <button
class="button button--flat button--grey" class="button button--flat button--grey"
@click="$store.commit('closeHovers')" @click="closeHovers"
:aria-label="$t('buttons.close')" :aria-label="$t('buttons.close')"
:title="$t('buttons.close')" :title="$t('buttons.close')"
tabindex="2"
> >
{{ $t("buttons.close") }} {{ $t("buttons.close") }}
</button> </button>
<button <button
id="focus-prompt"
class="button button--flat button--blue" class="button button--flat button--blue"
@click="() => switchListing()" @click="() => switchListing()"
:aria-label="$t('buttons.new')" :aria-label="$t('buttons.new')"
:title="$t('buttons.new')" :title="$t('buttons.new')"
tabindex="1"
> >
{{ $t("buttons.new") }} {{ $t("buttons.new") }}
</button> </button>
@ -80,15 +83,22 @@
<div class="card-content"> <div class="card-content">
<p>{{ $t("settings.shareDuration") }}</p> <p>{{ $t("settings.shareDuration") }}</p>
<div class="input-group input"> <div class="input-group input">
<input <vue-number-input
v-focus center
type="number" controls
max="2147483647" size="small"
min="1" :max="2147483647"
:min="0"
@keyup.enter="submit" @keyup.enter="submit"
v-model.trim="time" v-model="time"
tabindex="1"
/> />
<select class="right" v-model="unit" :aria-label="$t('time.unit')"> <select
class="right"
v-model="unit"
:aria-label="$t('time.unit')"
tabindex="2"
>
<option value="seconds">{{ $t("time.seconds") }}</option> <option value="seconds">{{ $t("time.seconds") }}</option>
<option value="minutes">{{ $t("time.minutes") }}</option> <option value="minutes">{{ $t("time.minutes") }}</option>
<option value="hours">{{ $t("time.hours") }}</option> <option value="hours">{{ $t("time.hours") }}</option>
@ -100,6 +110,7 @@
class="input input--block" class="input input--block"
type="password" type="password"
v-model.trim="password" v-model.trim="password"
tabindex="3"
/> />
</div> </div>
@ -109,14 +120,17 @@
@click="() => switchListing()" @click="() => switchListing()"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
tabindex="5"
> >
{{ $t("buttons.cancel") }} {{ $t("buttons.cancel") }}
</button> </button>
<button <button
id="focus-prompt"
class="button button--flat button--blue" class="button button--flat button--blue"
@click="submit" @click="submit"
:aria-label="$t('buttons.share')" :aria-label="$t('buttons.share')"
:title="$t('buttons.share')" :title="$t('buttons.share')"
tabindex="4"
> >
{{ $t("buttons.share") }} {{ $t("buttons.share") }}
</button> </button>
@ -126,16 +140,18 @@
</template> </template>
<script> <script>
import { mapState, mapGetters } from "vuex"; import { mapActions, mapState } from "pinia";
import { useFileStore } from "@/stores/file";
import { share as api, pub as pub_api } from "@/api"; import { share as api, pub as pub_api } from "@/api";
import moment from "moment/min/moment-with-locales"; import dayjs from "dayjs";
import Clipboard from "clipboard"; import { useLayoutStore } from "@/stores/layout";
import { copy } from "@/utils/clipboard";
export default { export default {
name: "share", name: "share",
data: function () { data: function () {
return { return {
time: "", time: 0,
unit: "hours", unit: "hours",
links: [], links: [],
clip: null, clip: null,
@ -143,9 +159,14 @@ export default {
listing: true, listing: true,
}; };
}, },
inject: ["$showError", "$showSuccess"],
computed: { computed: {
...mapState(["req", "selected", "selectedCount"]), ...mapState(useFileStore, [
...mapGetters(["isListing"]), "req",
"selected",
"selectedCount",
"isListing",
]),
url() { url() {
if (!this.isListing) { if (!this.isListing) {
return this.$route.path; return this.$route.path;
@ -172,23 +193,24 @@ export default {
this.$showError(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: { methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
copyToClipboard: function (text) {
copy(text).then(
() => {
// clipboard successfully set
this.$showSuccess(this.$t("success.linkCopied"));
},
() => {
// clipboard write failed
}
);
},
submit: async function () { submit: async function () {
let isPermanent = !this.time || this.time == 0;
try { try {
let res = null; let res = null;
if (isPermanent) { if (!this.time) {
res = await api.create(this.url, this.password); res = await api.create(this.url, this.password);
} else { } else {
res = await api.create(this.url, this.password, this.time, this.unit); res = await api.create(this.url, this.password, this.time, this.unit);
@ -197,7 +219,7 @@ export default {
this.links.push(res); this.links.push(res);
this.sort(); this.sort();
this.time = ""; this.time = 0;
this.unit = "hours"; this.unit = "hours";
this.password = ""; this.password = "";
@ -220,7 +242,7 @@ export default {
} }
}, },
humanTime(time) { humanTime(time) {
return moment(time * 1000).fromNow(); return dayjs(time * 1000).fromNow();
}, },
buildLink(share) { buildLink(share) {
return api.getShareURL(share); return api.getShareURL(share);
@ -242,7 +264,7 @@ export default {
}, },
switchListing() { switchListing() {
if (this.links.length == 0 && !this.listing) { if (this.links.length == 0 && !this.listing) {
this.$store.commit("closeHovers"); this.closeHovers();
} }
this.listing = !this.listing; this.listing = !this.listing;

View File

@ -5,18 +5,21 @@
</div> </div>
<div class="card-action"> <div class="card-action">
<button <button
@click="$store.commit('closeHovers')" @click="closeHovers"
class="button button--flat button--grey" class="button button--flat button--grey"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
tabindex="2"
> >
{{ $t("buttons.cancel") }} {{ $t("buttons.cancel") }}
</button> </button>
<button <button
id="focus-prompt"
@click="submit" @click="submit"
class="button button--flat button--red" class="button button--flat button--red"
:aria-label="$t('buttons.delete')" :aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')" :title="$t('buttons.delete')"
tabindex="1"
> >
{{ $t("buttons.delete") }} {{ $t("buttons.delete") }}
</button> </button>
@ -25,14 +28,16 @@
</template> </template>
<script> <script>
import { mapGetters } from "vuex"; import { mapActions, mapState } from "pinia";
import { useLayoutStore } from "@/stores/layout";
export default { export default {
name: "share-delete", name: "share-delete",
computed: { computed: {
...mapGetters(["currentPrompt"]), ...mapState(useLayoutStore, ["currentPrompt"]),
}, },
methods: { methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
submit: function () { submit: function () {
this.currentPrompt?.confirm(); this.currentPrompt?.confirm();
}, },

View File

@ -1,38 +1,111 @@
<template> <template>
<div class="card floating"> <div class="card floating">
<div class="card-title"> <div class="card-title">
<h2>{{ $t("prompts.upload") }}</h2> <h2>{{ t("prompts.upload") }}</h2>
</div> </div>
<div class="card-content"> <div class="card-content">
<p>{{ $t("prompts.uploadMessage") }}</p> <p>{{ t("prompts.uploadMessage") }}</p>
</div> </div>
<div class="card-action full"> <div class="card-action full">
<div @click="uploadFile" class="action"> <div
@click="uploadFile"
@keypress.enter="uploadFile"
class="action"
id="focus-prompt"
tabindex="1"
>
<i class="material-icons">insert_drive_file</i> <i class="material-icons">insert_drive_file</i>
<div class="title">{{ $t("buttons.file") }}</div> <div class="title">{{ t("buttons.file") }}</div>
</div> </div>
<div @click="uploadFolder" class="action"> <div
@click="uploadFolder"
@keypress.enter="uploadFolder"
class="action"
tabindex="2"
>
<i class="material-icons">folder</i> <i class="material-icons">folder</i>
<div class="title">{{ $t("buttons.folder") }}</div> <div class="title">{{ t("buttons.folder") }}</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script setup lang="ts">
export default { import { useI18n } from "vue-i18n";
name: "upload", import { useRoute } from "vue-router";
methods: { import { useFileStore } from "@/stores/file";
uploadFile: function () { import { useLayoutStore } from "@/stores/layout";
document.getElementById("upload-input").value = "";
document.getElementById("upload-input").click(); import * as upload from "@/utils/upload";
},
uploadFolder: function () { const { t } = useI18n();
document.getElementById("upload-folder-input").value = ""; const route = useRoute();
document.getElementById("upload-folder-input").click();
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
// TODO: this is a copy of the same function in FileListing.vue
const uploadInput = (event: Event) => {
layoutStore.closeHovers();
let files = (event.currentTarget as HTMLInputElement)?.files;
if (files === null) return;
let folder_upload = !!files[0].webkitRelativePath;
const uploadFiles: UploadList = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
const fullPath = folder_upload ? file.webkitRelativePath : undefined;
uploadFiles.push({
file,
name: file.name,
size: file.size,
isDir: false,
fullPath,
});
}
let path = route.path.endsWith("/") ? route.path : route.path + "/";
let conflict = upload.checkConflict(uploadFiles, fileStore.req!.items);
if (conflict) {
layoutStore.showHover({
prompt: "replace",
action: (event: Event) => {
event.preventDefault();
layoutStore.closeHovers();
upload.handleFiles(uploadFiles, path, false);
}, },
confirm: (event: Event) => {
event.preventDefault();
layoutStore.closeHovers();
upload.handleFiles(uploadFiles, path, true);
}, },
});
return;
}
upload.handleFiles(uploadFiles, path);
};
const openUpload = (isFolder: boolean) => {
const input = document.createElement("input");
input.type = "file";
input.multiple = true;
input.webkitdirectory = isFolder;
// TODO: call the function in FileListing.vue instead
input.onchange = uploadInput;
input.click();
};
const uploadFile = () => {
openUpload(false);
};
const uploadFolder = () => {
openUpload(true);
}; };
</script> </script>

View File

@ -53,7 +53,9 @@
</template> </template>
<script> <script>
import { mapGetters, mapMutations } from "vuex"; import { mapState, mapWritableState, mapActions } from "pinia";
import { useUploadStore } from "@/stores/upload";
import { useFileStore } from "@/stores/file";
import { abortAllUploads } from "@/api/tus"; import { abortAllUploads } from "@/api/tus";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
@ -65,19 +67,20 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters([ ...mapState(useUploadStore, [
"filesInUpload", "filesInUpload",
"filesInUploadCount", "filesInUploadCount",
"uploadSpeed", "uploadSpeed",
"eta", "getETA",
]), ]),
...mapMutations(["resetUpload"]), ...mapWritableState(useFileStore, ["reload"]),
...mapActions(useUploadStore, ["reset"]),
formattedETA() { formattedETA() {
if (!this.eta || this.eta === Infinity) { if (!this.getETA || this.getETA === Infinity) {
return "--:--:--"; return "--:--:--";
} }
let totalSeconds = this.eta; let totalSeconds = this.getETA;
const hours = Math.floor(totalSeconds / 3600); const hours = Math.floor(totalSeconds / 3600);
totalSeconds %= 3600; totalSeconds %= 3600;
const minutes = Math.floor(totalSeconds / 60); const minutes = Math.floor(totalSeconds / 60);
@ -97,8 +100,8 @@ export default {
abortAllUploads(); abortAllUploads();
buttons.done("upload"); buttons.done("upload");
this.open = false; this.open = false;
this.$store.commit("resetUpload"); this.reset();
this.$store.commit("setReload", true); this.reload = true;
} }
}, },
}, },

View File

@ -1,5 +1,5 @@
<template> <template>
<select v-on:change="change" :value="locale"> <select name="selectLanguage" v-on:change="change" :value="locale">
<option v-for="(language, value) in locales" :key="value" :value="value"> <option v-for="(language, value) in locales" :key="value" :value="value">
{{ $t("languages." + language) }} {{ $t("languages." + language) }}
</option> </option>
@ -7,12 +7,14 @@
</template> </template>
<script> <script>
import { markRaw } from "vue";
export default { export default {
name: "languages", name: "languages",
props: ["locale"], props: ["locale"],
data() { data() {
let dataObj = { let dataObj = {};
locales: { const locales = {
he: "he", he: "he",
hu: "hu", hu: "hu",
ar: "ar", ar: "ar",
@ -34,13 +36,16 @@ export default {
sk: "sk", sk: "sk",
"sv-se": "svSE", "sv-se": "svSE",
tr: "tr", tr: "tr",
ua: "ua", uk: "uk",
"zh-cn": "zhCN", "zh-cn": "zhCN",
"zh-tw": "zhTW", "zh-tw": "zhTW",
},
}; };
// Vue3 reactivity breaks with this configuration
// so we need to use markRaw as a workaround
// https://github.com/vuejs/core/issues/3024
Object.defineProperty(dataObj, "locales", { Object.defineProperty(dataObj, "locales", {
value: markRaw(locales),
configurable: false, configurable: false,
writable: false, writable: false,
}); });

View File

@ -1,18 +1,27 @@
<template> <template>
<select v-on:change="change" :value="theme"> <select v-on:change="change" :value="theme">
<option value="">{{ $t("settings.themes.light") }}</option> <option value="">{{ t("settings.themes.default") }}</option>
<option value="dark">{{ $t("settings.themes.dark") }}</option> <option value="light">{{ t("settings.themes.light") }}</option>
<option value="dark">{{ t("settings.themes.dark") }}</option>
</select> </select>
</template> </template>
<script> <script setup lang="ts">
export default { import { SelectHTMLAttributes } from "vue";
name: "themes", import { useI18n } from "vue-i18n";
props: ["theme"],
methods: { const { t } = useI18n();
change(event) {
this.$emit("update:theme", event.target.value); defineProps<{
}, theme: UserTheme;
}, }>();
const emit = defineEmits<{
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(e: "update:theme", val: string | null): void;
}>();
const change = (event: Event) => {
emit("update:theme", (event.target as SelectHTMLAttributes)?.value);
}; };
</script> </script>

View File

@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<p v-if="!isDefault"> <p v-if="!isDefault && props.user !== null">
<label for="username">{{ $t("settings.username") }}</label> <label for="username">{{ t("settings.username") }}</label>
<input <input
class="input input--block" class="input input--block"
type="text" type="text"
@ -11,7 +11,7 @@
</p> </p>
<p v-if="!isDefault"> <p v-if="!isDefault">
<label for="password">{{ $t("settings.password") }}</label> <label for="password">{{ t("settings.password") }}</label>
<input <input
class="input input--block" class="input input--block"
type="password" type="password"
@ -22,9 +22,9 @@
</p> </p>
<p> <p>
<label for="scope">{{ $t("settings.scope") }}</label> <label for="scope">{{ t("settings.scope") }}</label>
<input <input
:disabled="createUserDirData" :disabled="createUserDirData ?? false"
:placeholder="scopePlaceholder" :placeholder="scopePlaceholder"
class="input input--block" class="input input--block"
type="text" type="text"
@ -34,86 +34,89 @@
</p> </p>
<p class="small" v-if="displayHomeDirectoryCheckbox"> <p class="small" v-if="displayHomeDirectoryCheckbox">
<input type="checkbox" v-model="createUserDirData" /> <input type="checkbox" v-model="createUserDirData" />
{{ $t("settings.createUserHomeDirectory") }} {{ t("settings.createUserHomeDirectory") }}
</p> </p>
<p> <p>
<label for="locale">{{ $t("settings.language") }}</label> <label for="locale">{{ t("settings.language") }}</label>
<languages <languages
class="input input--block" class="input input--block"
id="locale" id="locale"
:locale.sync="user.locale" v-model:locale="user.locale"
></languages> ></languages>
</p> </p>
<p v-if="!isDefault"> <p v-if="!isDefault && user.perm">
<input <input
type="checkbox" type="checkbox"
:disabled="user.perm.admin" :disabled="user.perm.admin"
v-model="user.lockPassword" v-model="user.lockPassword"
/> />
{{ $t("settings.lockPassword") }} {{ t("settings.lockPassword") }}
</p> </p>
<permissions :perm.sync="user.perm" /> <permissions v-model:perm="user.perm" />
<commands v-if="isExecEnabled" :commands.sync="user.commands" /> <commands v-if="enableExec" v-model:commands="user.commands" />
<div v-if="!isDefault"> <div v-if="!isDefault">
<h3>{{ $t("settings.rules") }}</h3> <h3>{{ t("settings.rules") }}</h3>
<p class="small">{{ $t("settings.rulesHelp") }}</p> <p class="small">{{ t("settings.rulesHelp") }}</p>
<rules :rules.sync="user.rules" /> <rules v-model:rules="user.rules" />
</div> </div>
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import Languages from "./Languages.vue"; import Languages from "./Languages.vue";
import Rules from "./Rules.vue"; import Rules from "./Rules.vue";
import Permissions from "./Permissions.vue"; import Permissions from "./Permissions.vue";
import Commands from "./Commands.vue"; import Commands from "./Commands.vue";
import { enableExec } from "@/utils/constants"; import { enableExec } from "@/utils/constants";
import { computed, onMounted, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
export default { const { t } = useI18n();
name: "user",
data: () => { const createUserDirData = ref<boolean | null>(null);
return { const originalUserScope = ref<string | null>(null);
createUserDirData: false,
originalUserScope: "/", const props = defineProps<{
}; user: IUserForm;
}, isNew: boolean;
components: { isDefault: boolean;
Permissions, createUserDir?: boolean;
Languages, }>();
Rules,
Commands, onMounted(() => {
}, if (props.user.scope) {
props: ["user", "createUserDir", "isNew", "isDefault"], originalUserScope.value = props.user.scope;
created() { createUserDirData.value = props.createUserDir;
this.originalUserScope = this.user.scope; }
this.createUserDirData = this.createUserDir; });
},
computed: { const passwordPlaceholder = computed(() =>
passwordPlaceholder() { props.isNew ? "" : t("settings.avoidChanges")
return this.isNew ? "" : this.$t("settings.avoidChanges"); );
}, const scopePlaceholder = computed(() =>
scopePlaceholder() { createUserDirData.value ? t("settings.userScopeGenerationPlaceholder") : ""
return this.createUserDir );
? this.$t("settings.userScopeGenerationPlaceholder") const displayHomeDirectoryCheckbox = computed(
: ""; () => props.isNew && createUserDirData.value
}, );
displayHomeDirectoryCheckbox() {
return this.isNew && this.createUserDir; watch(
}, () => props.user,
isExecEnabled: () => enableExec, () => {
}, if (!props.user?.perm?.admin) return;
watch: { props.user.lockPassword = false;
"user.perm.admin": function () { }
if (!this.user.perm.admin) return; );
this.user.lockPassword = false;
}, watch(createUserDirData, () => {
createUserDirData() { if (props.user?.scope) {
this.user.scope = this.createUserDirData ? "" : this.originalUserScope; props.user.scope = createUserDirData.value
}, ? ""
}, : originalUserScope.value ?? "";
}; }
});
</script> </script>

View File

@ -1,14 +1,14 @@
.button { .button {
outline: 0; outline: 0;
border: 0; border: 0;
padding: .5em 1em; padding: 0.5em 1em;
border-radius: .1em; border-radius: 0.1em;
cursor: pointer; cursor: pointer;
background: var(--blue); background: var(--blue);
color: white; color: white;
border: 1px solid rgba(0, 0, 0, 0.05); border: 1px solid var(--divider);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.05); box-shadow: 0 0 5px var(--divider);
transition: .1s ease all; transition: 0.1s ease all;
} }
.button:hover { .button:hover {
@ -38,7 +38,7 @@
} }
.button--flat:hover { .button--flat:hover {
background: var(--moon-grey); background: var(--surfaceSecondary);
} }
.button--flat.button--red { .button--flat.button--red {
@ -50,6 +50,6 @@
} }
.button[disabled] { .button[disabled] {
opacity: .5; opacity: 0.5;
cursor: not-allowed; cursor: not-allowed;
} }

View File

@ -1,20 +1,20 @@
.input { .input {
border-radius: .1em; background: var(--surfacePrimary);
padding: .5em 1em; color: var(--textSecondary);
background: white; border: 1px solid var(--borderPrimary);
border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 0.1em;
transition: .2s ease all; padding: 0.5em 1em;
color: #333; transition: 0.2s ease all;
margin: 0; margin: 0;
} }
.input:hover, .input:hover,
.input:focus { .input:focus {
border-color: rgba(0, 0, 0, 0.2); border-color: var(--borderSecondary);
} }
.input--block { .input--block {
margin-bottom: .5em; margin-bottom: 0.5em;
display: block; display: block;
width: 100%; width: 100%;
} }
@ -27,9 +27,9 @@
} }
.input--red { .input--red {
background: #fcd0cd; background: var(--input-red) !important;
} }
.input--green { .input--green {
background: #c9f2da; background: var(--input-green) !important;
} }

View File

@ -12,8 +12,11 @@
} }
.share__box { .share__box {
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px; box-shadow:
background: #fff; rgba(0, 0, 0, 0.06) 0px 1px 3px,
rgba(0, 0, 0, 0.12) 0px 1px 2px;
background: var(--surfacePrimary);
color: var(--textPrimary);
border-radius: 0.2em; border-radius: 0.2em;
margin: 5px; margin: 5px;
overflow: hidden; overflow: hidden;
@ -39,7 +42,7 @@
.share__box__element { .share__box__element {
padding: 1em; padding: 1em;
border-top: 1px solid rgba(0, 0, 0, 0.1); border-top: 1px solid var(--borderPrimary);
word-break: break-all; word-break: break-all;
} }
@ -62,7 +65,7 @@
border-left: 0; border-left: 0;
border-right: 0; border-right: 0;
border-bottom: 0; border-bottom: 0;
border-top: 1px solid rgba(0, 0, 0, 0.1); border-top: 1px solid var(--borderPrimary);
} }
.share__box__items #listing.list .item .name { .share__box__items #listing.list .item .name {
@ -76,7 +79,7 @@
.share__wrong__password { .share__wrong__password {
background: var(--red); background: var(--red);
color: #fff; color: #fff;
padding: .5em; padding: 0.5em;
text-align: center; text-align: center;
animation: .2s opac forwards; animation: 0.2s opac forwards;
} }

View File

@ -3,17 +3,8 @@
bottom: 0; bottom: 0;
left: 0; left: 0;
max-height: calc(100% - 4em); max-height: calc(100% - 4em);
background: white; background: var(--surfacePrimary);
color: #212121; color: var(--textPrimary);
z-index: 9997;
width: 100%;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
transition: .2s ease transform;
}
.shell__divider {
position: relative;
height: 8px;
z-index: 9999; z-index: 9999;
background: rgba(127, 127, 127, 0.1); background: rgba(127, 127, 127, 0.1);
transition: 0.2s ease background; transition: 0.2s ease background;
@ -32,6 +23,8 @@
overflow: auto; overflow: auto;
font-size: 1rem; font-size: 1rem;
cursor: text; cursor: text;
box-shadow: 0 0 5px var(--borderPrimary);
transition: 0.2s ease transform;
} }
.shell__overlay { .shell__overlay {
@ -52,7 +45,7 @@ body.rtl .shell-content {
display: flex; display: flex;
padding: 0.5em; padding: 0.5em;
align-items: flex-start; align-items: flex-start;
border-top: 1px solid rgba(0, 0, 0, 0.05); border-top: 1px solid var(--divider);
} }
.shell--hidden { .shell--hidden {

View File

@ -1,8 +1,8 @@
:root { :root {
--blue: #2196f3; --blue: #2196f3;
--dark-blue: #1E88E5; --dark-blue: #1e88e5;
--red: #F44336; --red: #f44336;
--dark-red: #D32F2F; --dark-red: #d32f2f;
--moon-grey: #f2f2f2; --moon-grey: #f2f2f2;
--icon-red: #da4453; --icon-red: #da4453;
@ -11,4 +11,44 @@
--icon-green: #2ecc71; --icon-green: #2ecc71;
--icon-blue: #1d99f3; --icon-blue: #1d99f3;
--icon-violet: #9b59b6; --icon-violet: #9b59b6;
--input-red: rgb(252, 208, 205);
--input-green: rgb(201, 242, 218);
--item-selected: white;
--action: rgb(84, 110, 122);
--background: rgb(250, 250, 250);
--surfacePrimary: rgb(255, 255, 255);
--surfaceSecondary: rgb(230, 230, 230);
--divider: rgba(0, 0, 0, 0.05);
--iconPrimary: var(--icon-blue);
--iconSecondary: rgb(255, 255, 255);
--iconTertiary: rgb(204, 204, 204);
--textPrimary: rgb(111, 111, 111);
--textSecondary: rgb(51, 51, 51);
--hover: rgba(0, 0, 0, 0.1);
--borderPrimary: rgba(0, 0, 0, 0.1);
--borderSecondary: rgba(0, 0, 0, 0.2);
}
:root.dark {
--input-red: rgb(115, 48, 45);
--input-green: rgb(20, 122, 65);
--action: rgb(255, 255, 255);
--background: rgb(20, 29, 36);
--surfacePrimary: rgb(32, 41, 47);
--surfaceSecondary: rgb(58, 65, 71);
--textPrimary: rgba(255, 255, 255, 0.6);
--textSecondary: rgba(255, 255, 255, 0.87);
--divider: rgba(255, 255, 255, 0.12);
--iconPrimary: rgb(255, 255, 255);
--iconSecondary: rgb(255, 255, 255);
--iconTertiary: rgb(255, 255, 255);
--hover: rgba(255, 255, 255, 0.1);
--borderPrimary: rgba(255, 255, 255, 0.05);
--borderSecondary: rgba(255, 255, 255, 0.15);
} }

View File

@ -1,12 +1,8 @@
body { body {
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
padding-top: 4em; padding-top: 4em;
background-color: #fafafa; background: var(--background);
color: #333333; color: var(--textSecondary);
}
body.rtl {
direction: rtl;
} }
* { * {
@ -62,8 +58,8 @@ nav {
left: 0; left: 0;
} }
body.rtl nav { html[dir="rtl"] nav {
left: unset; left: initial;
right: 0; right: 0;
} }
@ -78,13 +74,12 @@ nav .action {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
body.rtl .action { html[dir="rtl"] nav .action {
direction: rtl;
text-align: right; text-align: right;
} }
nav > div { nav > div {
border-top: 1px solid rgba(0, 0, 0, 0.05); border-top: 1px solid var(--divider);
} }
nav .action > * { nav .action > * {
@ -99,14 +94,15 @@ main {
.breadcrumbs { .breadcrumbs {
height: 3em; height: 3em;
border-bottom: 1px solid rgba(0, 0, 0, 0.05); background: var(--background);
border-bottom: 1px solid var(--divider);
} }
.breadcrumbs span, .breadcrumbs span,
.breadcrumbs { .breadcrumbs {
display: flex; display: flex;
align-items: center; align-items: center;
color: #6f6f6f; color: var(--textPrimary);
} }
.breadcrumbs a { .breadcrumbs a {
@ -115,12 +111,12 @@ main {
border-radius: 0.125em; border-radius: 0.125em;
} }
body.rtl .breadcrumbs a { html[dir="rtl"] .breadcrumbs a {
transform: translateX(-16em); transform: translateX(-16em);
} }
.breadcrumbs a:hover { .breadcrumbs a:hover {
background-color: rgba(0, 0, 0, 0.05); background-color: var(--divider);
} }
.breadcrumbs span a { .breadcrumbs span a {
@ -152,3 +148,33 @@ body.rtl .breadcrumbs a {
.break-word { .break-word {
word-break: break-all; word-break: break-all;
} }
.vue-number-input > input {
background: var(--surfacePrimary) !important;
border-color: var(--surfaceSecondary) !important;
color: var(--textSecondary) !important;
}
.vue-number-input--small > input {
height: 1rem !important;
font-size: 1rem !important;
}
.vue-number-input :hover,
.vue-number-input :focus {
border-color: var(--borderSecondary) !important;
}
.vue-number-input__button {
background: var(--surfacePrimary) !important;
}
.vue-number-input__button--minus,
.vue-number-input__button--plus {
border-color: var(--surfaceSecondary) !important;
}
.vue-number-input__button::before,
.vue-number-input__button::after {
background: var(--textSecondary) !important;
}

View File

@ -4,17 +4,17 @@
.dashboard .row { .dashboard .row {
display: flex; display: flex;
margin: 0 -.5em; margin: 0 -0.5em;
flex-wrap: wrap; flex-wrap: wrap;
} }
body.rtl .dashboard .row { html[dir="rtl"] .dashboard .row {
margin-right: 16em; margin-right: 16em;
} }
.dashboard .row .column { .dashboard .row .column {
display: flex; display: flex;
padding: 0 .5em; padding: 0 0.5em;
width: 50%; width: 50%;
} }
@ -29,26 +29,26 @@ body.rtl .dashboard .row {
} }
a { a {
color: inherit color: inherit;
} }
.dashboard p label { .dashboard p label {
margin-bottom: .2em; margin-bottom: 0.2em;
display: block; display: block;
font-size: .8em; font-size: 0.8em;
font-weight: 500; font-weight: 500;
color: rgba(0, 0, 0, 0.57); color: var(--textPrimary);
} }
li code, li code,
p code { p code {
background: rgba(0, 0, 0, 0.05); background: var(--divider);
padding: .1em; padding: 0.1em;
border-radius: .2em; border-radius: 0.2em;
} }
.small { .small {
font-size: .8em; font-size: 0.8em;
line-height: 1.5; line-height: 1.5;
} }
@ -61,21 +61,21 @@ p code {
.dashboard #nav .wrapper { .dashboard #nav .wrapper {
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;
border-bottom: 2px solid rgba(0, 0, 0, 0.05); border-bottom: 2px solid var(--divider);
} }
body.rtl #nav .wrapper { html[dir="rtl"] .dashboard #nav .wrapper {
margin-right: 16em; margin-right: 16em;
} }
.dashboard #nav ul { .dashboard #nav ul {
list-style: none; list-style: none;
display: flex; display: flex;
color: rgb(84, 110, 122); color: var(--action);
font-weight: 500; font-weight: 500;
padding: 0; padding: 0;
margin: 0 0 -2px 0; margin: 0 0 -2px 0;
font-size: .8em; font-size: 0.8em;
text-align: center; text-align: center;
justify-content: left; justify-content: left;
} }
@ -85,12 +85,11 @@ body.rtl #nav .wrapper {
padding: 1.5em 2em; padding: 1.5em 2em;
white-space: nowrap; white-space: nowrap;
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
transition: .1s ease-in-out all; transition: 0.1s ease-in-out all;
} }
.dashboard #nav ul li:hover { .dashboard #nav ul li:hover {
background: var(--moon-grey); background: var(--surfaceSecondary);
} }
.dashboard #nav ul li.active { .dashboard #nav ul li.active {
@ -120,7 +119,7 @@ table {
} }
table tr { table tr {
border-bottom: 1px solid #ccc; border-bottom: 1px solid var(--iconTertiary);
} }
table tr:last-child { table tr:last-child {
@ -129,13 +128,13 @@ table tr:last-child {
table th { table th {
font-weight: 500; font-weight: 500;
color: #757575; color: var(--textSecondary);
text-align: left; text-align: left;
} }
table th, table th,
table td { table td {
padding: .5em 0; padding: 0.5em 0;
} }
table td.small { table td.small {
@ -146,7 +145,7 @@ table tr>*:first-child {
padding-left: 1em; padding-left: 1em;
} }
body.rtl table tr>* { html[dir="rtl"] table tr > * {
padding-left: unset; padding-left: unset;
padding-right: 1em; padding-right: 1em;
text-align: right; text-align: right;
@ -160,9 +159,13 @@ table tr>*:last-child {
.card { .card {
position: relative; position: relative;
margin: 0 0 1rem 0; margin: 0 0 1rem 0;
background-color: #fff; background: var(--surfacePrimary);
color: var(--textSecondary);
border-radius: 2px; border-radius: 2px;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); box-shadow:
0 2px 2px 0 rgba(0, 0, 0, 0.14),
0 1px 5px 0 rgba(0, 0, 0, 0.12),
0 3px 1px -2px rgba(0, 0, 0, 0.2);
overflow: auto; overflow: auto;
} }
@ -171,11 +174,11 @@ table tr>*:last-child {
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
z-index: 99999;
max-width: 25em; max-width: 25em;
width: 90%; width: 90%;
max-height: 95%; max-height: 95%;
animation: .1s show forwards; /* animation-duration: 0.3s;
animation-fill-mode: forwards; */
} }
.card > * > *:first-child { .card > * > *:first-child {
@ -195,7 +198,7 @@ table tr>*:last-child {
margin-right: auto; margin-right: auto;
} }
body.rtl .card .card-title>*:first-child { html[dir="rtl"] .card .card-title > *:first-child {
margin-right: 0; margin-right: 0;
text-align: right; text-align: right;
} }
@ -234,7 +237,7 @@ body.rtl .card .card-action {
} }
.card h3 { .card h3 {
color: rgba(0, 0, 0, 0.53); color: var(--textPrimary);
font-size: 1em; font-size: 1em;
font-weight: 500; font-weight: 500;
margin: 2em 0 1em; margin: 2em 0 1em;
@ -253,6 +256,14 @@ body.rtl .card .card-action {
max-width: 15em; max-width: 15em;
} }
.card#share input,
.card#share select,
.card#share input::-webkit-inner-spin-button,
.card#share input::-webkit-outer-spin-button {
background: var(--surfacePrimary);
color: var(--textSecondary);
}
.card#share ul { .card#share ul {
list-style: none; list-style: none;
padding: 0; padding: 0;
@ -277,24 +288,24 @@ body.rtl .card .card-action {
.card#share ul li input, .card#share ul li input,
.card#share ul li select { .card#share ul li select {
padding: .2em; padding: 0.2em;
margin-right: .5em; margin-right: 0.5em;
border: 1px solid #dadada; border: 1px solid var(--borderPrimary);
} }
.card#share .action.copy-clipboard::after { .card#share .action.copy-clipboard::after {
content: 'Copied!'; content: "Copied!";
position: absolute; position: absolute;
left: -25%; left: -25%;
width: 150%; width: 150%;
font-size: .6em; font-size: 0.6em;
text-align: center; text-align: center;
background: #44a6f5; background: #44a6f5;
color: #fff; color: #fff;
padding: .5em .2em; padding: 0.5em 0.2em;
border-radius: .4em; border-radius: 0.4em;
top: -2em; top: -2em;
transition: .1s ease opacity; transition: 0.1s ease opacity;
opacity: 0; opacity: 0;
} }
@ -324,10 +335,9 @@ body.rtl .card .card-action {
z-index: 9999; z-index: 9999;
visibility: hidden; visibility: hidden;
opacity: 0; opacity: 0;
animation: .1s show forwards; animation: 0.1s show forwards;
} }
/* * * * * * * * * * * * * * * * /* * * * * * * * * * * * * * * *
* PROMPT - MOVE * * PROMPT - MOVE *
* * * * * * * * * * * * * * * */ * * * * * * * * * * * * * * * */
@ -344,33 +354,33 @@ body.rtl .card .card-action {
.file-list li { .file-list li {
width: 100%; width: 100%;
user-select: none; user-select: none;
border-radius: .2em; border-radius: 0.2em;
padding: .3em; padding: 0.3em;
} }
.file-list li[aria-selected=true] { .file-list li[aria-selected="true"] {
background: var(--blue) !important; background: var(--blue) !important;
color: #fff !important; color: var(--iconSecondary) !important;
transition: .1s ease all; transition: 0.1s ease all;
} }
.file-list li:hover { .file-list li:hover {
background-color: #e9eaeb; background: var(--surfaceSecondary);
cursor: pointer; cursor: pointer;
} }
.file-list li:before { .file-list li:before {
content: "folder"; content: "folder";
color: #6f6f6f; color: var(--textPrimary);
vertical-align: middle; vertical-align: middle;
line-height: 1.4; line-height: 1.4;
font-family: 'Material Icons'; font-family: "Material Icons";
font-size: 1.75em; font-size: 1.75em;
margin-right: .25em; margin-right: 0.25em;
} }
.file-list li[aria-selected=true]:before { .file-list li[aria-selected="true"]:before {
color: white; color: var(--iconSecondary);
} }
.help { .help {
@ -399,11 +409,11 @@ body.rtl .card .card-action {
} }
.collapsible { .collapsible {
border-top: 1px solid rgba(0,0,0,0.1); border-top: 1px solid var(--borderPrimary);
} }
.collapsible:last-of-type { .collapsible:last-of-type {
border-bottom: 1px solid rgba(0,0,0,0.1); border-bottom: 1px solid var(--borderPrimary);
} }
.collapsible > input { .collapsible > input {
@ -421,18 +431,18 @@ body.rtl .card .card-action {
.collapsible > label * { .collapsible > label * {
margin: 0; margin: 0;
color: rgba(0,0,0,0.57); color: var(--textPrimary);
} }
.collapsible > label i { .collapsible > label i {
transition: .2s ease transform; transition: 0.2s ease transform;
user-select: none; user-select: none;
} }
.collapsible .collapse { .collapsible .collapse {
max-height: 0; max-height: 0;
overflow: hidden; overflow: hidden;
transition: .2s ease all; transition: 0.2s ease all;
} }
.collapsible > input:checked ~ .collapse { .collapsible > input:checked ~ .collapse {
@ -442,7 +452,7 @@ body.rtl .card .card-action {
} }
.collapsible > input:checked ~ label i { .collapsible > input:checked ~ label i {
transform: rotate(180deg) transform: rotate(180deg);
} }
.card .collapsible { .card .collapsible {
@ -468,12 +478,12 @@ body.rtl .card .card-action {
flex: 1; flex: 1;
padding: 2em; padding: 2em;
border-radius: 0.2em; border-radius: 0.2em;
border: 1px solid rgba(0, 0, 0, 0.1); border: 1px solid var(--borderPrimary);
text-align: center; text-align: center;
} }
.card .card-action.full .action { .card .card-action.full .action {
margin: 0 0.25em 0.50em; margin: 0 0.25em 0.5em;
} }
.card .card-action.full .action i { .card .card-action.full .action i {
@ -489,7 +499,7 @@ body.rtl .card .card-action {
} }
/*** RTL - Fix disk usage information (in english) ***/ /*** RTL - Fix disk usage information (in english) ***/
body.rtl .credits { html[dir="rtl"] .credits {
text-align: right; text-align: right;
direction: ltr; direction: ltr;
} }

View File

@ -1,8 +1,8 @@
header { header {
z-index: 1000; z-index: 1000;
background-color: #fff; background: var(--surfacePrimary);
border-bottom: 1px solid rgba(0, 0, 0, 0.075); border-bottom: 1px solid var(--divider);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); box-shadow: 0 0 5px var(--borderPrimary);
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
@ -82,20 +82,25 @@ header .menu-button {
} }
#search #input { #search #input {
background-color: #f5f5f5; background: var(--surfaceSecondary);
border-color: var(--surfacePrimary);
display: flex; display: flex;
height: 100%; height: 100%;
padding: 0em 0.75em; padding: 0em 0.75em;
border-radius: 0.3em; border-radius: 0.3em;
transition: .1s ease all; transition: 0.1s ease all;
align-items: center; align-items: center;
z-index: 2; z-index: 2;
} }
#search #input input::placeholder {
color: var(--textSecondary);
}
#search.active #input { #search.active #input {
border-bottom: 1px solid rgba(0, 0, 0, 0.075); border-bottom: 1px solid var(--borderPrimary);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); box-shadow: 0 0 5px var(--borderPrimary);
background-color: #fff; background: var(--surfacePrimary);
height: 4em; height: 4em;
} }
@ -105,7 +110,7 @@ header .menu-button {
#search.active i, #search.active i,
#search.active input { #search.active input {
color: #212121; color: var(--textPrimary);
} }
#search #input > .action, #search #input > .action,
@ -124,18 +129,20 @@ header .menu-button {
#search #result { #search #result {
visibility: visible; visibility: visible;
max-height: none; max-height: none;
background-color: #f8f8f8; background: var(--background);
text-align: left; text-align: left;
padding: 0; padding: 0;
color: rgba(0, 0, 0, 0.6); color: var(--textPrimary);
height: 0; height: 0;
transition: .1s ease height, .1s ease padding; transition:
0.1s ease height,
0.1s ease padding;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
z-index: 1; z-index: 1;
} }
body.rtl #search #result { html[dir="rtl"] #search #result {
direction: ltr; direction: ltr;
} }
@ -143,19 +150,18 @@ body.rtl #search #result {
margin-top: 0; margin-top: 0;
} }
body.rtl #search #result { html[dir="rtl"] #search #result {
direction: rtl;
text-align: right; text-align: right;
} }
/*** RTL - Keep search result LTR because it has paths (in english) ***/ /*** RTL - Keep search result LTR because it has paths (in english) ***/
body.rtl #search #result ul>* { html[dir="rtl"] #search #result ul > * {
direction: ltr; direction: ltr;
text-align: left; text-align: left;
} }
#search.active #result { #search.active #result {
padding: .5em; padding: 0.5em;
height: calc(100% - 4em); height: calc(100% - 4em);
} }
@ -166,7 +172,7 @@ body.rtl #search #result ul>* {
} }
#search li { #search li {
margin-bottom: .5em; margin-bottom: 0.5em;
} }
#search #result > div { #search #result > div {
@ -187,7 +193,7 @@ body.rtl #search #result ul>* {
} }
#search.active #result i { #search.active #result i {
color: #ccc; color: var(--iconTertiary);
} }
#search.active #result > p > i { #search.active #result > p > i {
@ -199,35 +205,35 @@ body.rtl #search #result ul>* {
#search.active #result ul li a { #search.active #result ul li a {
display: flex; display: flex;
align-items: center; align-items: center;
padding: .3em 0; padding: 0.3em 0;
} }
#search.active #result ul li a i { #search.active #result ul li a i {
margin-right: .3em; margin-right: 0.3em;
} }
#search::-webkit-input-placeholder { /* I dont think we need these anymore */
color: rgba(255, 255, 255, .5); /* #search::-webkit-input-placeholder {
} color: var(--textPrimary);
#search:-moz-placeholder {
opacity: 1;
color: rgba(255, 255, 255, .5);
} }
#search::-moz-placeholder { #search::-moz-placeholder {
opacity: 1; opacity: 1;
color: rgba(255, 255, 255, .5); color: var(--textPrimary);
} }
#search:-ms-input-placeholder { #search:-ms-input-placeholder {
color: rgba(255, 255, 255, .5); color: var(--textPrimary);
} }
#search #input input::placeholder {
color: var(--textPrimary);
} */
#search .boxes { #search .boxes {
border: 1px solid rgba(0, 0, 0, 0.075); border: 1px solid var(--borderPrimary);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); box-shadow: 0 0 5px var(--borderPrimary);
background: #fff; background: var(--surfacePrimary);
margin: 1em 0; margin: 1em 0;
} }
@ -235,11 +241,11 @@ body.rtl #search #result ul>* {
margin: 0; margin: 0;
font-weight: 500; font-weight: 500;
font-size: 1em; font-size: 1em;
color: #212121; color: var(--textSecondary);
padding: .5em; padding: 0.5em;
} }
body.rtl #search .boxes h3 { html[dir="rtl"] #search .boxes h3 {
text-align: right; text-align: right;
} }

View File

@ -2,30 +2,50 @@
/* General */ /* General */
.file-icons [aria-label^="."] { opacity: 0.33 } .file-icons [aria-label^="."] {
.file-icons [aria-label$=".bak"] { opacity: 0.33 } opacity: 0.33;
}
.file-icons [aria-label$=".bak"] {
opacity: 0.33;
}
.file-icons [data-type=audio] i::before { content: 'volume_up' } .file-icons [data-type="audio"] i::before {
.file-icons [data-type=blob] i::before { content: 'insert_drive_file' } content: "volume_up";
.file-icons [data-type=image] i::before { content: 'image' } }
.file-icons [data-type=pdf] i::before { content: 'description' } .file-icons [data-type="blob"] i::before {
.file-icons [data-type=text] i::before { content: 'description' } content: "insert_drive_file";
.file-icons [data-type=video] i::before { content: 'movie' } }
.file-icons [data-type=invalid_link] i::before { content: 'link_off' } .file-icons [data-type="image"] i::before {
content: "image";
}
.file-icons [data-type="pdf"] i::before {
content: "description";
}
.file-icons [data-type="text"] i::before {
content: "description";
}
.file-icons [data-type="video"] i::before {
content: "movie";
}
.file-icons [data-type="invalid_link"] i::before {
content: "link_off";
}
/* #f90 - Image */ /* #f90 - Image */
.file-icons [aria-label$=".ai"] i::before, .file-icons [aria-label$=".ai"] i::before,
.file-icons [aria-label$=".odg"] i::before, .file-icons [aria-label$=".odg"] i::before,
.file-icons [aria-label$=".xcf"] i::before .file-icons [aria-label$=".xcf"] i::before {
{ content: 'image' } content: "image";
}
/* #f90 - Presentation */ /* #f90 - Presentation */
.file-icons [aria-label$=".odp"] i::before, .file-icons [aria-label$=".odp"] i::before,
.file-icons [aria-label$=".ppt"] i::before, .file-icons [aria-label$=".ppt"] i::before,
.file-icons [aria-label$=".pptx"] i::before .file-icons [aria-label$=".pptx"] i::before {
{ content: 'slideshow' } content: "slideshow";
}
/* #0f0 - Spreadsheet/Database */ /* #0f0 - Spreadsheet/Database */
@ -34,8 +54,9 @@
.file-icons [aria-label$=".odb"] i::before, .file-icons [aria-label$=".odb"] i::before,
.file-icons [aria-label$=".ods"] i::before, .file-icons [aria-label$=".ods"] i::before,
.file-icons [aria-label$=".xls"] i::before, .file-icons [aria-label$=".xls"] i::before,
.file-icons [aria-label$=".xlsx"] i::before .file-icons [aria-label$=".xlsx"] i::before {
{ content: 'border_all' } content: "border_all";
}
/* #00f - Document */ /* #00f - Document */
@ -43,8 +64,9 @@
.file-icons [aria-label$=".docx"] i::before, .file-icons [aria-label$=".docx"] i::before,
.file-icons [aria-label$=".log"] i::before, .file-icons [aria-label$=".log"] i::before,
.file-icons [aria-label$=".odt"] i::before, .file-icons [aria-label$=".odt"] i::before,
.file-icons [aria-label$=".rtf"] i::before .file-icons [aria-label$=".rtf"] i::before {
{ content: 'description' } content: "description";
}
/* #999 - Code */ /* #999 - Code */
@ -65,8 +87,9 @@
.file-icons [aria-label$=".rs"] i::before, .file-icons [aria-label$=".rs"] i::before,
.file-icons [aria-label$=".vue"] i::before, .file-icons [aria-label$=".vue"] i::before,
.file-icons [aria-label$=".xml"] i::before, .file-icons [aria-label$=".xml"] i::before,
.file-icons [aria-label$=".yml"] i::before .file-icons [aria-label$=".yml"] i::before {
{ content: 'code' } content: "code";
}
/* #999 - Executable */ /* #999 - Executable */
@ -75,16 +98,18 @@
.file-icons [aria-label$=".exe"] i::before, .file-icons [aria-label$=".exe"] i::before,
.file-icons [aria-label$=".jar"] i::before, .file-icons [aria-label$=".jar"] i::before,
.file-icons [aria-label$=".ps1"] i::before, .file-icons [aria-label$=".ps1"] i::before,
.file-icons [aria-label$=".sh"] i::before .file-icons [aria-label$=".sh"] i::before {
{ content: 'web_asset' } content: "web_asset";
}
/* #999 - Installer */ /* #999 - Installer */
.file-icons [aria-label$=".deb"] i::before, .file-icons [aria-label$=".deb"] i::before,
.file-icons [aria-label$=".msi"] i::before, .file-icons [aria-label$=".msi"] i::before,
.file-icons [aria-label$=".pkg"] i::before, .file-icons [aria-label$=".pkg"] i::before,
.file-icons [aria-label$=".rpm"] i::before .file-icons [aria-label$=".rpm"] i::before {
{ content: 'archive' } content: "archive";
}
/* #999 - Compressed */ /* #999 - Compressed */
@ -96,8 +121,9 @@
.file-icons [aria-label$=".tar"] i::before, .file-icons [aria-label$=".tar"] i::before,
.file-icons [aria-label$=".xz"] i::before, .file-icons [aria-label$=".xz"] i::before,
.file-icons [aria-label$=".zip"] i::before, .file-icons [aria-label$=".zip"] i::before,
.file-icons [aria-label$=".zst"] i::before .file-icons [aria-label$=".zst"] i::before {
{ content: 'folder_zip' } content: "folder_zip";
}
/* #999 - Disk */ /* #999 - Disk */
@ -108,25 +134,35 @@
.file-icons [aria-label$=".vdi"] i::before, .file-icons [aria-label$=".vdi"] i::before,
.file-icons [aria-label$=".vhd"] i::before, .file-icons [aria-label$=".vhd"] i::before,
.file-icons [aria-label$=".vmdk"] i::before, .file-icons [aria-label$=".vmdk"] i::before,
.file-icons [aria-label$=".wim"] i::before .file-icons [aria-label$=".wim"] i::before {
{ content: 'album' } content: "album";
}
/* #999 - Font */ /* #999 - Font */
.file-icons [aria-label$=".otf"] i::before, .file-icons [aria-label$=".otf"] i::before,
.file-icons [aria-label$=".ttf"] i::before, .file-icons [aria-label$=".ttf"] i::before,
.file-icons [aria-label$=".woff"] i::before, .file-icons [aria-label$=".woff"] i::before,
.file-icons [aria-label$=".woff2"] i::before .file-icons [aria-label$=".woff2"] i::before {
{ content: 'font_download' } content: "font_download";
}
/* Colors */ /* Colors */
/* General */ /* General */
.file-icons [data-type=audio] i { color: var(--icon-yellow) } .file-icons [data-type="audio"] i {
.file-icons [data-type=image] i { color: var(--icon-orange) } color: var(--icon-yellow);
.file-icons [data-type=video] i { color: var(--icon-violet) } }
.file-icons [data-type=invalid_link] i { color: var(--icon-red) } .file-icons [data-type="image"] i {
color: var(--icon-orange);
}
.file-icons [data-type="video"] i {
color: var(--icon-violet);
}
.file-icons [data-type="invalid_link"] i {
color: var(--icon-red);
}
/* #f00 - Adobe/Oracle */ /* #f00 - Adobe/Oracle */
@ -135,8 +171,9 @@
.file-icons [aria-label$=".jar"] i, .file-icons [aria-label$=".jar"] i,
.file-icons [aria-label$=".psd"] i, .file-icons [aria-label$=".psd"] i,
.file-icons [aria-label$=".rb"] i, .file-icons [aria-label$=".rb"] i,
.file-icons [data-type=pdf] i .file-icons [data-type="pdf"] i {
{ color: var(--icon-red) } color: var(--icon-red);
}
/* #f90 - Image/Presentation */ /* #f90 - Image/Presentation */
@ -146,16 +183,18 @@
.file-icons [aria-label$=".ppt"] i, .file-icons [aria-label$=".ppt"] i,
.file-icons [aria-label$=".pptx"] i, .file-icons [aria-label$=".pptx"] i,
.file-icons [aria-label$=".vue"] i, .file-icons [aria-label$=".vue"] i,
.file-icons [aria-label$=".xcf"] i .file-icons [aria-label$=".xcf"] i {
{ color: var(--icon-orange) } color: var(--icon-orange);
}
/* #ff0 - Various */ /* #ff0 - Various */
.file-icons [aria-label$=".css"] i, .file-icons [aria-label$=".css"] i,
.file-icons [aria-label$=".js"] i, .file-icons [aria-label$=".js"] i,
.file-icons [aria-label$=".json"] i, .file-icons [aria-label$=".json"] i,
.file-icons [aria-label$=".zip"] i .file-icons [aria-label$=".zip"] i {
{ color: var(--icon-yellow) } color: var(--icon-yellow);
}
/* #0f0 - Spreadsheet/Google */ /* #0f0 - Spreadsheet/Google */
@ -164,8 +203,9 @@
.file-icons [aria-label$=".go"] i, .file-icons [aria-label$=".go"] i,
.file-icons [aria-label$=".ods"] i, .file-icons [aria-label$=".ods"] i,
.file-icons [aria-label$=".xls"] i, .file-icons [aria-label$=".xls"] i,
.file-icons [aria-label$=".xlsx"] i .file-icons [aria-label$=".xlsx"] i {
{ color: var(--icon-green) } color: var(--icon-green);
}
/* #00f - Document/Microsoft/Apple/Closed */ /* #00f - Document/Microsoft/Apple/Closed */
@ -188,18 +228,26 @@
.file-icons [aria-label$=".ps1"] i, .file-icons [aria-label$=".ps1"] i,
.file-icons [aria-label$=".rtf"] i, .file-icons [aria-label$=".rtf"] i,
.file-icons [aria-label$=".vob"] i, .file-icons [aria-label$=".vob"] i,
.file-icons [aria-label$=".wim"] i .file-icons [aria-label$=".wim"] i {
{ color: var(--icon-blue) } color: var(--icon-blue);
}
/* #60f - Various */ /* #60f - Various */
.file-icons [aria-label$=".iso"] i, .file-icons [aria-label$=".iso"] i,
.file-icons [aria-label$=".php"] i, .file-icons [aria-label$=".php"] i,
.file-icons [aria-label$=".rar"] i .file-icons [aria-label$=".rar"] i {
{ color: var(--icon-violet) } color: var(--icon-violet);
}
/* Overrides */ /* Overrides */
.file-icons [data-dir=true] i { color: var(--icon-blue) } .file-icons [data-dir="true"] i {
.file-icons [data-dir=true] i::before { content: 'folder' } color: var(--icon-blue);
.file-icons [aria-selected=true] i { color: var(--item-selected) } }
.file-icons [data-dir="true"] i::before {
content: "folder";
}
.file-icons [aria-selected="true"] i {
color: var(--iconSecondary);
}

View File

@ -1,15 +1,11 @@
#listing { html[dir="rtl"] #listing {
--item-selected: white;
}
body.rtl #listing {
margin-right: 16em; margin-right: 16em;
} }
#listing h2 { #listing h2 {
margin: 0 0 0 0.5em; margin: 0 0 0 0.5em;
font-size: .9em; font-size: 0.9em;
color: rgba(0, 0, 0, 0.38); color: var(--textPrimary);
font-weight: 500; font-weight: 500;
} }
@ -25,12 +21,15 @@ body.rtl #listing {
} }
#listing .item { #listing .item {
background-color: #fff; background: var(--surfacePrimary);
border-color: var(--divider);
position: relative; position: relative;
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
color: #6f6f6f; color: var(--textPrimary);
transition: .1s ease background, .1s ease opacity; transition:
0.1s ease background,
0.1s ease opacity;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
@ -75,13 +74,13 @@ body.rtl #listing {
margin: 1em auto; margin: 1em auto;
display: block !important; display: block !important;
width: 95%; width: 95%;
color: rgba(0, 0, 0, 0.3); color: var(--textPrimary);
font-weight: 500; font-weight: 500;
} }
.message i { .message i {
font-size: 2.5em; font-size: 2.5em;
margin-bottom: .2em; margin-bottom: 0.2em;
display: block; display: block;
} }
@ -92,14 +91,18 @@ body.rtl #listing {
#listing.mosaic .item { #listing.mosaic .item {
width: calc(33% - 1em); width: calc(33% - 1em);
margin: .5em; margin: 0.5em;
padding: 0.5em; padding: 0.5em;
border-radius: 0.2em; border-radius: 0.2em;
box-shadow: 0 1px 3px rgba(0, 0, 0, .06), 0 1px 2px rgba(0, 0, 0, .12); box-shadow:
0 1px 3px rgba(0, 0, 0, 0.06),
0 1px 2px rgba(0, 0, 0, 0.12);
} }
#listing.mosaic .item:hover { #listing.mosaic .item:hover {
box-shadow: 0 1px 3px rgba(0, 0, 0, .12), 0 1px 2px rgba(0, 0, 0, .24) !important; box-shadow:
0 1px 3px rgba(0, 0, 0, 0.12),
0 1px 2px rgba(0, 0, 0, 0.24) !important;
} }
#listing.mosaic .header { #listing.mosaic .header {
@ -127,7 +130,7 @@ body.rtl #listing {
text-align: center; text-align: center;
} }
#listing.mosaic.gallery .item[data-type=image] div:last-of-type { #listing.mosaic.gallery .item[data-type="image"] div:last-of-type {
color: white; color: white;
background: linear-gradient(#0000, #0009); background: linear-gradient(#0000, #0009);
} }
@ -159,7 +162,7 @@ body.rtl #listing {
#listing.list .item { #listing.list .item {
width: 100%; width: 100%;
margin: 0; margin: 0;
border: 1px solid rgba(0, 0, 0, 0.1); border: 1px solid var(--borderPrimary);
padding: 1em; padding: 1em;
border-top: 0; border-top: 0;
} }
@ -168,9 +171,9 @@ body.rtl #listing {
display: none; display: none;
} }
#listing .item[aria-selected=true] { #listing .item[aria-selected="true"] {
background: var(--blue) !important; background: var(--blue) !important;
color: var(--item-selected) !important; color: var(--iconSecondary) !important;
} }
#listing.list .item div:first-of-type { #listing.list .item div:first-of-type {
@ -202,22 +205,22 @@ body.rtl #listing {
#listing .item.header { #listing .item.header {
display: none !important; display: none !important;
background-color: #ccc; background-color: var(--iconTertiary);
} }
#listing.list .header i { #listing.list .header i {
font-size: 1.5em; font-size: 1.5em;
vertical-align: middle; vertical-align: middle;
margin-left: .2em; margin-left: 0.2em;
} }
#listing.list .item.header { #listing.list .item.header {
display: flex !important; display: flex !important;
background: #fafafa; background: var(--background);
z-index: 999; z-index: 999;
padding: .85em; padding: 0.85em;
border: 0; border: 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.1); border-bottom: 1px solid var(--borderPrimary);
} }
#listing.list .item.header > div:first-child { #listing.list .item.header > div:first-child {
@ -250,7 +253,7 @@ body.rtl #listing {
#listing.list .header i { #listing.list .header i {
opacity: 0; opacity: 0;
transition: .1s ease all; transition: 0.1s ease all;
} }
#listing.list .header p:hover i, #listing.list .header p:hover i,
@ -272,7 +275,7 @@ body.rtl #listing {
height: 4em; height: 4em;
padding: 0.5em 0.5em 0.5em 1em; padding: 0.5em 0.5em 0.5em 1em;
justify-content: space-between; justify-content: space-between;
transition: .2s ease bottom; transition: 0.2s ease bottom;
} }
#listing #multiple-selection.active { #listing #multiple-selection.active {
@ -281,5 +284,5 @@ body.rtl #listing {
#listing #multiple-selection p, #listing #multiple-selection p,
#listing #multiple-selection i { #listing #multiple-selection i {
color: var(--item-selected); color: var(--iconSecondary);
} }

View File

@ -1,5 +1,5 @@
#login { #login {
background: #fff; background: var(--surfacePrimary);
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
@ -17,7 +17,7 @@
#login h1 { #login h1 {
text-align: center; text-align: center;
font-size: 2.5em; font-size: 2.5em;
margin: .4em 0 .67em; margin: 0.4em 0 0.67em;
} }
#login form { #login form {
@ -34,15 +34,15 @@
} }
#login #recaptcha { #login #recaptcha {
margin: .5em 0 0; margin: 0.5em 0 0;
} }
#login .wrong { #login .wrong {
background: var(--red); background: var(--red);
color: #fff; color: #fff;
padding: .5em; padding: 0.5em;
text-align: center; text-align: center;
animation: .2s opac forwards; animation: 0.2s opac forwards;
} }
@keyframes opac { @keyframes opac {
@ -61,5 +61,5 @@
text-transform: lowercase; text-transform: lowercase;
font-weight: 500; font-weight: 500;
font-size: 0.9rem; font-size: 0.9rem;
margin: .5rem 0; margin: 0.5rem 0;
} }

View File

@ -1,12 +1,12 @@
@media (max-width: 1024px) { @media (max-width: 1024px) {
nav { nav {
width: 10em width: 10em;
} }
} }
@media (max-width: 1024px) { @media (max-width: 1024px) {
main { main {
width: calc(100% - 13em) width: calc(100% - 13em);
} }
} }
@ -21,27 +21,27 @@
width: 60%; width: 60%;
} }
#more { #more {
display: inherit display: inherit;
} }
header .overlay { header .overlay {
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: rgba(0, 0, 0, 0.1); background-color: var(--borderPrimary);
} }
#dropdown { #dropdown {
position: fixed; position: fixed;
top: 1em; top: 1em;
right: 1em; right: 1em;
display: block; display: block;
background-color: #fff; background: var(--surfaceSecondary);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); box-shadow: 0 0 5px var(--borderPrimary);
transform: scale(0); transform: scale(0);
transition: .1s ease-in-out transform; transition: 0.1s ease-in-out transform;
transform-origin: top right; transform-origin: top right;
z-index: 99999; z-index: 99999;
} }
body.rtl #dropdown { html[dir="rtl"] #dropdown {
right: unset; right: unset;
left: 1em; left: 1em;
transform-origin: top left; transform-origin: top left;
@ -61,7 +61,7 @@
} }
#dropdown .action span:not(.counter) { #dropdown .action span:not(.counter) {
display: inline-block; display: inline-block;
padding: .4em; padding: 0.4em;
} }
#dropdown .counter { #dropdown .counter {
left: 2.25em; left: 2.25em;
@ -73,8 +73,10 @@
transform: translateX(-50%); transform: translateX(-50%);
display: flex; display: flex;
align-items: center; align-items: center;
background: #fff; background: var(--surfaceSecondary);
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px; box-shadow:
rgba(0, 0, 0, 0.06) 0px 1px 3px,
rgba(0, 0, 0, 0.12) 0px 1px 2px;
width: 95%; width: 95%;
max-width: 20em; max-width: 20em;
z-index: 1; z-index: 1;
@ -86,7 +88,7 @@
#file-selection > span { #file-selection > span {
display: inline-block; display: inline-block;
margin-left: 1em; margin-left: 1em;
color: #6f6f6f; color: var(--textPrimary);
margin-right: auto; margin-right: auto;
} }
#file-selection .action span { #file-selection .action span {
@ -95,15 +97,15 @@
nav { nav {
top: 0; top: 0;
z-index: 99999; z-index: 99999;
background: #fff; background: var(--surfaceSecondary);
height: 100%; height: 100%;
width: 16em; width: 16em;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); box-shadow: 0 0 5px var(--borderPrimary);
transition: .1s ease left; transition: 0.1s ease left;
left: -17em; left: -17em;
} }
body.rtl nav { html[dir="rtl"] nav {
left: unset; left: unset;
right: -17em; right: -17em;
} }
@ -111,7 +113,7 @@
left: 0; left: 0;
} }
body.rtl nav.active { html[dir="rtl"] nav.active {
left: unset; left: unset;
right: 0; right: 0;
} }
@ -131,19 +133,19 @@
margin-bottom: 5em; margin-bottom: 5em;
} }
body.rtl #listing { html[dir="rtl"] #listing {
margin-right: unset; margin-right: unset;
} }
body.rtl .breadcrumbs { html[dir="rtl"] .breadcrumbs {
transform: translateX(16em); transform: translateX(16em);
} }
body.rtl #nav .wrapper { html[dir="rtl"] #nav .wrapper {
margin-right: unset; margin-right: unset;
} }
body.rtl .dashboard .row { html[dir="rtl"] .dashboard .row {
margin-right: unset; margin-right: unset;
} }

View File

@ -1,6 +1,6 @@
@import "normalize.css/normalize.css"; @import "normalize.css/normalize.css";
@import "noty/lib/noty.css"; @import "vue-toastification/dist/index.css";
@import "noty/lib/themes/mint.css"; @import "vue-final-modal/style.css";
@import "./_variables.css"; @import "./_variables.css";
@import "./_buttons.css"; @import "./_buttons.css";
@import "./_inputs.css"; @import "./_inputs.css";
@ -16,10 +16,23 @@
@import "./login.css"; @import "./login.css";
@import "./mobile.css"; @import "./mobile.css";
/* For testing only
:focus {
outline: 2px solid crimson !important;
border-radius: 3px !important;
} */
.link { .link {
color: var(--blue); color: var(--blue);
} }
#loading {
background: var(--background);
}
#loading .spinner > div {
background: var(--iconPrimary);
}
main .spinner { main .spinner {
display: block; display: block;
text-align: center; text-align: center;
@ -32,7 +45,7 @@ main .spinner > div {
height: 0.8em; height: 0.8em;
margin: 0 0.1em; margin: 0 0.1em;
font-size: 1em; font-size: 1em;
background-color: rgba(0, 0, 0, 0.3); background: var(--iconPrimary);
border-radius: 100%; border-radius: 100%;
display: inline-block; display: inline-block;
animation: sk-bouncedelay 1.4s infinite ease-in-out both; animation: sk-bouncedelay 1.4s infinite ease-in-out both;
@ -72,7 +85,7 @@ main .spinner .bounce2 {
transition: 0.2s ease all; transition: 0.2s ease all;
border: 0; border: 0;
margin: 0; margin: 0;
color: #546e7a; color: var(--action);
border-radius: 50%; border-radius: 50%;
background: transparent; background: transparent;
padding: 0; padding: 0;
@ -94,7 +107,7 @@ main .spinner .bounce2 {
} }
.action:hover { .action:hover {
background-color: rgba(0, 0, 0, 0.1); background-color: var(--hover);
} }
.action ul { .action ul {
@ -115,7 +128,7 @@ main .spinner .bounce2 {
} }
.action ul li:hover { .action ul li:hover {
background-color: rgba(0, 0, 0, 0.04); background-color: var(--divider);
} }
#click-overlay { #click-overlay {
@ -138,7 +151,7 @@ main .spinner .bounce2 {
bottom: 0; bottom: 0;
right: 0; right: 0;
background: var(--blue); background: var(--blue);
color: #fff; color: var(--iconSecondary);
border-radius: 50%; border-radius: 50%;
font-size: 0.75em; font-size: 0.75em;
width: 1.8em; width: 1.8em;
@ -146,7 +159,7 @@ main .spinner .bounce2 {
text-align: center; text-align: center;
line-height: 1.55em; line-height: 1.55em;
font-weight: bold; font-weight: bold;
border: 2px solid white; border: 2px solid var(--borderPrimary);
} }
/* PREVIEWER */ /* PREVIEWER */
@ -217,7 +230,6 @@ main .spinner .bounce2 {
height: 88%; height: 88%;
} }
#previewer .preview video { #previewer .preview video {
height: 100%; height: 100%;
} }
@ -302,7 +314,7 @@ main .spinner .bounce2 {
#previewer .spinner > div { #previewer .spinner > div {
width: 18px; width: 18px;
height: 18px; height: 18px;
background-color: white; background: var(--iconPrimary);
} }
/* EDITOR */ /* EDITOR */
@ -310,17 +322,21 @@ main .spinner .bounce2 {
#editor-container { #editor-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: #fafafa; background-color: var(--background);
position: fixed; position: fixed;
padding-top: 4em; padding-top: 4em;
top: 0; top: 0;
left: 0; left: 0;
height: 100%; height: 100%;
width: 100%; width: 100%;
z-index: 9999; z-index: 9998;
overflow: hidden; overflow: hidden;
} }
#editor-container .bar {
background: var(--surfacePrimary);
}
#editor-container #editor { #editor-container #editor {
flex: 1; flex: 1;
} }
@ -331,7 +347,7 @@ main .spinner .bounce2 {
} }
/*** RTL - flip and position arrow of path ***/ /*** RTL - flip and position arrow of path ***/
body.rtl .breadcrumbs .chevron { html[dir="rtl"] .breadcrumbs .chevron {
transform: scaleX(-1) translateX(16em); transform: scaleX(-1) translateX(16em);
} }
@ -343,22 +359,6 @@ body.rtl .breadcrumbs .chevron {
font-size: 1rem; font-size: 1rem;
} }
/* * * * * * * * * * * * * * * *
* PROMPT *
* * * * * * * * * * * * * * * */
.noty_buttons {
text-align: right;
padding: 0 10px 10px !important;
}
.noty_buttons button {
background: rgba(0, 0, 0, 0.05);
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 0 0 0;
font-size: 1rem;
}
/* * * * * * * * * * * * * * * * /* * * * * * * * * * * * * * * *
* FOOTER * * FOOTER *
* * * * * * * * * * * * * * * */ * * * * * * * * * * * * * * * */
@ -436,17 +436,17 @@ body.rtl .breadcrumbs .chevron {
* RTL overrides * * RTL overrides *
* * * * * * * * * * * * * * * */ * * * * * * * * * * * * * * * */
body.rtl .card-content textarea { html[dir="rtl"] .card-content textarea {
direction: ltr; direction: ltr;
text-align: left; text-align: left;
} }
body.rtl .card-content .small + input { html[dir="rtl"] .card-content .small + input {
direction: ltr; direction: ltr;
text-align: left; text-align: left;
} }
body.rtl .card.floating .card-content .file-list { html[dir="rtl"] .card.floating .card-content .file-list {
direction: ltr; direction: ltr;
text-align: left; text-align: left;
} }

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