update settings and views (#41)
Co-authored-by: Graham Steffaniak <graham.steffaniak@autodesk.com>
This commit is contained in:
		
							parent
							
								
									5506135d32
								
							
						
					
					
						commit
						a33eab2a8c
					
				| 
						 | 
				
			
			@ -146,14 +146,11 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	if u == nil {
 | 
			
		||||
		pass, err := users.HashPwd(a.Cred.Password)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		log.Println("creds", a.Cred.Password)
 | 
			
		||||
		// create user with the provided credentials
 | 
			
		||||
		d := &users.User{
 | 
			
		||||
			Username:     a.Cred.Username,
 | 
			
		||||
			Password:     pass,
 | 
			
		||||
			Password:     a.Cred.Password,
 | 
			
		||||
			Scope:        a.Settings.UserDefaults.Scope,
 | 
			
		||||
			Locale:       a.Settings.UserDefaults.Locale,
 | 
			
		||||
			ViewMode:     a.Settings.UserDefaults.ViewMode,
 | 
			
		||||
| 
						 | 
				
			
			@ -178,16 +175,6 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
 | 
			
		|||
		}
 | 
			
		||||
	} else if p := !users.CheckPwd(a.Cred.Password, u.Password); len(a.Fields.Values) > 1 || p {
 | 
			
		||||
		u = a.GetUser(u)
 | 
			
		||||
 | 
			
		||||
		// update the password when it doesn't match the current
 | 
			
		||||
		if p {
 | 
			
		||||
			pass, err := users.HashPwd(a.Cred.Password)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
			u.Password = pass
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// update user with provided fields
 | 
			
		||||
		err := a.Users.Update(u)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,7 +30,6 @@ func (a JSONAuth) Auth(r *http.Request, usr users.Store) (*users.User, error) {
 | 
			
		|||
	if r.Body == nil {
 | 
			
		||||
		return nil, os.ErrPermission
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := json.NewDecoder(r.Body).Decode(&cred)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, os.ErrPermission
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,39 +0,0 @@
 | 
			
		|||
 | 
			
		||||
 == Running benchmark == 
 | 
			
		||||
?   	github.com/gtsteffaniak/filebrowser	[no test files]
 | 
			
		||||
?   	github.com/gtsteffaniak/filebrowser/auth	[no test files]
 | 
			
		||||
?   	github.com/gtsteffaniak/filebrowser/cmd	[no test files]
 | 
			
		||||
PASS
 | 
			
		||||
ok  	github.com/gtsteffaniak/filebrowser/diskcache	1.318s
 | 
			
		||||
?   	github.com/gtsteffaniak/filebrowser/errors	[no test files]
 | 
			
		||||
?   	github.com/gtsteffaniak/filebrowser/files	[no test files]
 | 
			
		||||
PASS
 | 
			
		||||
ok  	github.com/gtsteffaniak/filebrowser/fileutils	1.176s
 | 
			
		||||
2023/09/29 17:01:53 h: 401  <nil>
 | 
			
		||||
2023/09/29 17:01:53 h: 401  <nil>
 | 
			
		||||
2023/09/29 17:01:53 h: 401  <nil>
 | 
			
		||||
2023/09/29 17:01:53 h: 401  <nil>
 | 
			
		||||
2023/09/29 17:01:53 h: 401  <nil>
 | 
			
		||||
2023/09/29 17:01:53 h: 401  <nil>
 | 
			
		||||
PASS
 | 
			
		||||
ok  	github.com/gtsteffaniak/filebrowser/http	1.055s
 | 
			
		||||
PASS
 | 
			
		||||
ok  	github.com/gtsteffaniak/filebrowser/img	0.771s
 | 
			
		||||
goos: darwin
 | 
			
		||||
goarch: arm64
 | 
			
		||||
pkg: github.com/gtsteffaniak/filebrowser/index
 | 
			
		||||
BenchmarkFillIndex-10    	      10	   6355542 ns/op	   12084 B/op	     449 allocs/op
 | 
			
		||||
PASS
 | 
			
		||||
ok  	github.com/gtsteffaniak/filebrowser/index	0.653s
 | 
			
		||||
PASS
 | 
			
		||||
ok  	github.com/gtsteffaniak/filebrowser/rules	0.549s
 | 
			
		||||
PASS
 | 
			
		||||
ok  	github.com/gtsteffaniak/filebrowser/runner	0.661s
 | 
			
		||||
PASS
 | 
			
		||||
ok  	github.com/gtsteffaniak/filebrowser/settings	0.665s
 | 
			
		||||
?   	github.com/gtsteffaniak/filebrowser/share	[no test files]
 | 
			
		||||
?   	github.com/gtsteffaniak/filebrowser/storage	[no test files]
 | 
			
		||||
?   	github.com/gtsteffaniak/filebrowser/storage/bolt	[no test files]
 | 
			
		||||
PASS
 | 
			
		||||
ok  	github.com/gtsteffaniak/filebrowser/users	0.654s
 | 
			
		||||
?   	github.com/gtsteffaniak/filebrowser/version	[no test files]
 | 
			
		||||
| 
						 | 
				
			
			@ -1,25 +0,0 @@
 | 
			
		|||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"github.com/spf13/cobra"
 | 
			
		||||
 | 
			
		||||
	"github.com/gtsteffaniak/filebrowser/users"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	rootCmd.AddCommand(hashCmd)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var hashCmd = &cobra.Command{
 | 
			
		||||
	Use:   "hash <password>",
 | 
			
		||||
	Short: "Hashes a password",
 | 
			
		||||
	Long:  `Hashes a password using bcrypt algorithm.`,
 | 
			
		||||
	Args:  cobra.ExactArgs(1),
 | 
			
		||||
	Run: func(cmd *cobra.Command, args []string) {
 | 
			
		||||
		pwd, err := users.HashPwd(args[0])
 | 
			
		||||
		checkErr(err)
 | 
			
		||||
		fmt.Println(pwd)
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -139,10 +139,21 @@ func quickSetup(d pythonData) {
 | 
			
		|||
	user := &users.User{
 | 
			
		||||
		Username: username,
 | 
			
		||||
		Password: password,
 | 
			
		||||
		LockPassword: false,
 | 
			
		||||
	}
 | 
			
		||||
	settings.GlobalConfiguration.UserDefaults.Apply(user)
 | 
			
		||||
	user.Perm.Admin = true
 | 
			
		||||
	user.DarkMode = true
 | 
			
		||||
	user.ViewMode = "normal"
 | 
			
		||||
	user.LockPassword = false
 | 
			
		||||
	user.Perm = users.Permissions{
 | 
			
		||||
		Create:   true,
 | 
			
		||||
		Rename:   true,
 | 
			
		||||
		Modify:   true,
 | 
			
		||||
		Delete:   true,
 | 
			
		||||
		Share:    true,
 | 
			
		||||
		Download: true,
 | 
			
		||||
		Admin:    true,
 | 
			
		||||
	}
 | 
			
		||||
	err = d.store.Users.Save(user)
 | 
			
		||||
	checkErr(err)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,12 +16,9 @@ var usersAddCmd = &cobra.Command{
 | 
			
		|||
	Long:  `Create a new user and add it to the database.`,
 | 
			
		||||
	Args:  cobra.ExactArgs(2), //nolint:gomnd
 | 
			
		||||
	Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
 | 
			
		||||
		password, err := users.HashPwd(args[1])
 | 
			
		||||
		checkErr(err)
 | 
			
		||||
 | 
			
		||||
		user := &users.User{
 | 
			
		||||
			Username:     args[0],
 | 
			
		||||
			Password:     password,
 | 
			
		||||
			Password:     args[1],
 | 
			
		||||
			LockPassword: mustGetBool(cmd.Flags(), "lockPassword"),
 | 
			
		||||
		}
 | 
			
		||||
		servSettings, err := d.store.Settings.GetServer()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,9 +3,11 @@ server:
 | 
			
		|||
  baseURL: "/"
 | 
			
		||||
  root: "/Users/steffag/git/go"
 | 
			
		||||
auth:
 | 
			
		||||
  method: noauth
 | 
			
		||||
  method: password
 | 
			
		||||
  signup: true
 | 
			
		||||
userDefaults:
 | 
			
		||||
  darkMode: true
 | 
			
		||||
  disableSettings: false
 | 
			
		||||
  scope: "."
 | 
			
		||||
  hideDotfiles: true
 | 
			
		||||
  singleClick: false
 | 
			
		||||
| 
						 | 
				
			
			@ -17,5 +19,3 @@ userDefaults:
 | 
			
		|||
    delete: true
 | 
			
		||||
    share: true
 | 
			
		||||
    download: true
 | 
			
		||||
frontend:
 | 
			
		||||
  theme: dark
 | 
			
		||||
| 
						 | 
				
			
			@ -12,6 +12,7 @@ import (
 | 
			
		|||
	"github.com/golang-jwt/jwt/v4/request"
 | 
			
		||||
 | 
			
		||||
	"github.com/gtsteffaniak/filebrowser/errors"
 | 
			
		||||
	"github.com/gtsteffaniak/filebrowser/settings"
 | 
			
		||||
	"github.com/gtsteffaniak/filebrowser/users"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -19,20 +20,8 @@ const (
 | 
			
		|||
	TokenExpirationTime = time.Hour * 2
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type userInfo struct {
 | 
			
		||||
	ID           uint              `json:"id"`
 | 
			
		||||
	Locale       string            `json:"locale"`
 | 
			
		||||
	ViewMode     string            `json:"viewMode"`
 | 
			
		||||
	SingleClick  bool              `json:"singleClick"`
 | 
			
		||||
	Perm         users.Permissions `json:"perm"`
 | 
			
		||||
	Commands     []string          `json:"commands"`
 | 
			
		||||
	LockPassword bool              `json:"lockPassword"`
 | 
			
		||||
	HideDotfiles bool              `json:"hideDotfiles"`
 | 
			
		||||
	DateFormat   bool              `json:"dateFormat"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type authToken struct {
 | 
			
		||||
	User userInfo `json:"user"`
 | 
			
		||||
	User users.User `json:"user"`
 | 
			
		||||
	jwt.RegisteredClaims
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -143,15 +132,9 @@ var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int,
 | 
			
		|||
 | 
			
		||||
	user := &users.User{
 | 
			
		||||
		Username: info.Username,
 | 
			
		||||
		Password: info.Password,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pwd, err := users.HashPwd(info.Password)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return http.StatusInternalServerError, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user.Password = pwd
 | 
			
		||||
 | 
			
		||||
	settings.GlobalConfiguration.UserDefaults.Apply(user)
 | 
			
		||||
	userHome, err := d.settings.MakeUserDir(user.Username, user.Scope, d.server.Root)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
 | 
			
		||||
| 
						 | 
				
			
			@ -176,17 +159,7 @@ var renewHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data
 | 
			
		|||
 | 
			
		||||
func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.User) (int, error) {
 | 
			
		||||
	claims := &authToken{
 | 
			
		||||
		User: userInfo{
 | 
			
		||||
			ID:           user.ID,
 | 
			
		||||
			Locale:       user.Locale,
 | 
			
		||||
			ViewMode:     user.ViewMode,
 | 
			
		||||
			SingleClick:  user.SingleClick,
 | 
			
		||||
			Perm:         user.Perm,
 | 
			
		||||
			LockPassword: user.LockPassword,
 | 
			
		||||
			Commands:     user.Commands,
 | 
			
		||||
			HideDotfiles: user.HideDotfiles,
 | 
			
		||||
			DateFormat:   user.DateFormat,
 | 
			
		||||
		},
 | 
			
		||||
		User: *user,
 | 
			
		||||
		RegisteredClaims: jwt.RegisteredClaims{
 | 
			
		||||
			IssuedAt:  jwt.NewNumericDate(time.Now()),
 | 
			
		||||
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(TokenExpirationTime)),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,6 @@ import (
 | 
			
		|||
var searchHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
 | 
			
		||||
	response := []map[string]interface{}{}
 | 
			
		||||
	query := r.URL.Query().Get("query")
 | 
			
		||||
 | 
			
		||||
	// Retrieve the User-Agent and X-Auth headers from the request
 | 
			
		||||
	sessionId := r.Header.Get("SessionId")
 | 
			
		||||
	index := *index.GetIndex()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -129,7 +129,7 @@ var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request
 | 
			
		|||
 | 
			
		||||
	var token string
 | 
			
		||||
	if len(hash) > 0 {
 | 
			
		||||
		tokenBuffer := make([]byte, 96) //nolint:gomnd
 | 
			
		||||
		tokenBuffer := make([]byte, 24) //nolint:gomnd
 | 
			
		||||
		if _, err := rand.Read(tokenBuffer); err != nil {
 | 
			
		||||
			return http.StatusInternalServerError, err
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -40,7 +40,6 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys
 | 
			
		|||
		"LoginPage":             auther.LoginPage(),
 | 
			
		||||
		"CSS":                   false,
 | 
			
		||||
		"ReCaptcha":             false,
 | 
			
		||||
		"Theme":                 d.settings.Frontend.Theme,
 | 
			
		||||
		"EnableThumbs":          d.server.EnableThumbnails,
 | 
			
		||||
		"ResizePreview":         d.server.ResizePreview,
 | 
			
		||||
		"EnableExec":            d.server.EnableExec,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -124,11 +124,6 @@ var userPostHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *
 | 
			
		|||
		return http.StatusBadRequest, errors.ErrEmptyPassword
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req.Data.Password, err = users.HashPwd(req.Data.Password)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return http.StatusInternalServerError, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userHome, err := d.settings.MakeUserDir(req.Data.Username, req.Data.Scope, d.server.Root)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
 | 
			
		||||
| 
						 | 
				
			
			@ -184,7 +179,6 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
 | 
			
		|||
			if !d.user.Perm.Admin && d.user.LockPassword {
 | 
			
		||||
				return http.StatusForbidden, nil
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			req.Data.Password, err = users.HashPwd(req.Data.Password)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return http.StatusInternalServerError, err
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
package index
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"mime"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
| 
						 | 
				
			
			@ -120,3 +121,35 @@ func updateSize(given string) int {
 | 
			
		|||
		return size
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func IsMatchingType(extension string, matchType string) bool {
 | 
			
		||||
	mimetype := mime.TypeByExtension(extension)
 | 
			
		||||
	if strings.HasPrefix(mimetype, matchType) {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	switch matchType {
 | 
			
		||||
	case "doc":
 | 
			
		||||
		return isDoc(extension)
 | 
			
		||||
	case "archive":
 | 
			
		||||
		return isArchive(extension)
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func isDoc(extension string) bool {
 | 
			
		||||
	for _, typefile := range documentTypes {
 | 
			
		||||
		if extension == typefile {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func isArchive(extension string) bool {
 | 
			
		||||
	for _, typefile := range compressedFile {
 | 
			
		||||
		if extension == typefile {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,6 @@ package index
 | 
			
		|||
 | 
			
		||||
import (
 | 
			
		||||
	"math/rand"
 | 
			
		||||
	"mime"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"sort"
 | 
			
		||||
| 
						 | 
				
			
			@ -61,10 +60,11 @@ func (si *Index) Search(search string, scope string, sourceSession string) ([]st
 | 
			
		|||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				if isDir {
 | 
			
		||||
					pathName = pathName + "/"
 | 
			
		||||
					fileListTypes[pathName+"/"] = fileType
 | 
			
		||||
				} else {
 | 
			
		||||
					fileListTypes[pathName] = fileType
 | 
			
		||||
				}
 | 
			
		||||
				matching = append(matching, pathName)
 | 
			
		||||
				fileListTypes[pathName] = fileType
 | 
			
		||||
				count++
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -88,18 +88,7 @@ func scopedPathNameFilter(pathName string, scope string) string {
 | 
			
		|||
	return pathName
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func containsSearchTerm(pathName string, searchTerm string, options SearchOptions, isDir bool) (bool, map[string]bool) {
 | 
			
		||||
	conditions := options.Conditions
 | 
			
		||||
	path := getLastPathComponent(pathName)
 | 
			
		||||
	// Convert to lowercase once
 | 
			
		||||
	lowerSearchTerm := searchTerm
 | 
			
		||||
	if !conditions["exact"] {
 | 
			
		||||
		path = strings.ToLower(path)
 | 
			
		||||
		lowerSearchTerm = strings.ToLower(searchTerm)
 | 
			
		||||
	}
 | 
			
		||||
	if strings.Contains(path, lowerSearchTerm) {
 | 
			
		||||
		// Reuse the fileTypes map and clear its values
 | 
			
		||||
		fileTypes := map[string]bool{
 | 
			
		||||
var fileTypes = map[string]bool{
 | 
			
		||||
	"audio":   false,
 | 
			
		||||
	"image":   false,
 | 
			
		||||
	"video":   false,
 | 
			
		||||
| 
						 | 
				
			
			@ -107,20 +96,25 @@ func containsSearchTerm(pathName string, searchTerm string, options SearchOption
 | 
			
		|||
	"archive": false,
 | 
			
		||||
	"dir":     false,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func containsSearchTerm(pathName string, searchTerm string, options SearchOptions, isDir bool) (bool, map[string]bool) {
 | 
			
		||||
	conditions := options.Conditions
 | 
			
		||||
	path := getLastPathComponent(pathName)
 | 
			
		||||
	// Convert to lowercase once
 | 
			
		||||
	if !conditions["exact"] {
 | 
			
		||||
		path = strings.ToLower(path)
 | 
			
		||||
		searchTerm = strings.ToLower(searchTerm)
 | 
			
		||||
	}
 | 
			
		||||
	if strings.Contains(path, searchTerm) {
 | 
			
		||||
		// Calculate fileSize only if needed
 | 
			
		||||
		var fileSize int64
 | 
			
		||||
		if conditions["larger"] || conditions["smaller"] {
 | 
			
		||||
			fileSize = getFileSize(pathName)
 | 
			
		||||
		}
 | 
			
		||||
		matchesAllConditions := true
 | 
			
		||||
		extension := filepath.Ext(path)
 | 
			
		||||
		mimetype := mime.TypeByExtension(extension)
 | 
			
		||||
		fileTypes["audio"] = strings.HasPrefix(mimetype, "audio")
 | 
			
		||||
		fileTypes["image"] = strings.HasPrefix(mimetype, "image")
 | 
			
		||||
		fileTypes["video"] = strings.HasPrefix(mimetype, "video")
 | 
			
		||||
		fileTypes["doc"] = isDoc(extension)
 | 
			
		||||
		fileTypes["archive"] = isArchive(extension)
 | 
			
		||||
		for k := range fileTypes {
 | 
			
		||||
			fileTypes[k] = IsMatchingType(extension, k)
 | 
			
		||||
		}
 | 
			
		||||
		fileTypes["dir"] = isDir
 | 
			
		||||
 | 
			
		||||
		for t, v := range conditions {
 | 
			
		||||
			if t == "exact" {
 | 
			
		||||
				continue
 | 
			
		||||
| 
						 | 
				
			
			@ -128,8 +122,14 @@ func containsSearchTerm(pathName string, searchTerm string, options SearchOption
 | 
			
		|||
			var matchesCondition bool
 | 
			
		||||
			switch t {
 | 
			
		||||
			case "larger":
 | 
			
		||||
				if fileSize == 0 {
 | 
			
		||||
					fileSize = getFileSize(pathName)
 | 
			
		||||
				}
 | 
			
		||||
				matchesCondition = fileSize > int64(options.LargerThan)*bytesInMegabyte
 | 
			
		||||
			case "smaller":
 | 
			
		||||
				if fileSize == 0 {
 | 
			
		||||
					fileSize = getFileSize(pathName)
 | 
			
		||||
				}
 | 
			
		||||
				matchesCondition = fileSize < int64(options.SmallerThan)*bytesInMegabyte
 | 
			
		||||
			default:
 | 
			
		||||
				matchesCondition = v == fileTypes[t]
 | 
			
		||||
| 
						 | 
				
			
			@ -144,15 +144,6 @@ func containsSearchTerm(pathName string, searchTerm string, options SearchOption
 | 
			
		|||
	return false, map[string]bool{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func isDoc(extension string) bool {
 | 
			
		||||
	for _, typefile := range documentTypes {
 | 
			
		||||
		if extension == typefile {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getFileSize(filepath string) int64 {
 | 
			
		||||
	fileInfo, err := os.Stat(rootPath + "/" + filepath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
| 
						 | 
				
			
			@ -161,15 +152,6 @@ func getFileSize(filepath string) int64 {
 | 
			
		|||
	return fileInfo.Size()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func isArchive(extension string) bool {
 | 
			
		||||
	for _, typefile := range compressedFile {
 | 
			
		||||
		if extension == typefile {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getLastPathComponent(path string) string {
 | 
			
		||||
	// Use filepath.Base to extract the last component of the path
 | 
			
		||||
	return filepath.Base(path)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -61,6 +61,7 @@ func setDefaults() Settings {
 | 
			
		|||
		},
 | 
			
		||||
		Auth: Auth{
 | 
			
		||||
			Method: "password",
 | 
			
		||||
			Signup: true,
 | 
			
		||||
			Recaptcha: Recaptcha{
 | 
			
		||||
				Host: "",
 | 
			
		||||
			},
 | 
			
		||||
| 
						 | 
				
			
			@ -69,6 +70,9 @@ func setDefaults() Settings {
 | 
			
		|||
			Scope:           ".",
 | 
			
		||||
			LockPassword:    false,
 | 
			
		||||
			HideDotfiles:    true,
 | 
			
		||||
			DarkMode:        false,
 | 
			
		||||
			DisableSettings: false,
 | 
			
		||||
			Locale:          "en",
 | 
			
		||||
			Permissions: users.Permissions{
 | 
			
		||||
				Create:   true,
 | 
			
		||||
				Rename:   true,
 | 
			
		||||
| 
						 | 
				
			
			@ -76,6 +80,7 @@ func setDefaults() Settings {
 | 
			
		|||
				Delete:   true,
 | 
			
		||||
				Share:    true,
 | 
			
		||||
				Download: true,
 | 
			
		||||
				Admin:    false,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -83,6 +88,8 @@ func setDefaults() Settings {
 | 
			
		|||
 | 
			
		||||
// Apply applies the default options to a user.
 | 
			
		||||
func (d *UserDefaults) Apply(u *users.User) {
 | 
			
		||||
	u.DisableSettings = d.DisableSettings
 | 
			
		||||
	u.DarkMode = d.DarkMode
 | 
			
		||||
	u.Scope = d.Scope
 | 
			
		||||
	u.Locale = d.Locale
 | 
			
		||||
	u.ViewMode = d.ViewMode
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,6 @@ package settings
 | 
			
		|||
import (
 | 
			
		||||
	"github.com/gtsteffaniak/filebrowser/errors"
 | 
			
		||||
	"github.com/gtsteffaniak/filebrowser/rules"
 | 
			
		||||
	"github.com/gtsteffaniak/filebrowser/users"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// StorageBackend is a settings storage backend.
 | 
			
		||||
| 
						 | 
				
			
			@ -59,7 +58,7 @@ func (s *Storage) Save(set *Settings) error {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	if set.UserDefaults.ViewMode == "" {
 | 
			
		||||
		set.UserDefaults.ViewMode = users.MosaicViewMode
 | 
			
		||||
		set.UserDefaults.ViewMode = "normal"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if set.Rules == nil {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -61,13 +61,13 @@ type Frontend struct {
 | 
			
		|||
	DisableExternal       bool   `json:"disableExternal"`
 | 
			
		||||
	DisableUsedPercentage bool   `json:"disableUsedPercentage"`
 | 
			
		||||
	Files                 string `json:"files"`
 | 
			
		||||
	Theme                 string `json:"theme"`
 | 
			
		||||
	Color                 string `json:"color"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UserDefaults is a type that holds the default values
 | 
			
		||||
// for some fields on User.
 | 
			
		||||
type UserDefaults struct {
 | 
			
		||||
	DarkMode        bool         `json:"darkMode"`
 | 
			
		||||
	LockPassword    bool         `json:"lockPassword"`
 | 
			
		||||
	DisableSettings bool         `json:"disableSettings,omitempty"`
 | 
			
		||||
	Scope           string       `json:"scope"`
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,12 +28,13 @@ frontend:
 | 
			
		|||
  disableExternal: false
 | 
			
		||||
  disableUsedPercentage: true
 | 
			
		||||
  files: ""
 | 
			
		||||
  theme: ""
 | 
			
		||||
  color: ""
 | 
			
		||||
userDefaults:
 | 
			
		||||
  scope: ""
 | 
			
		||||
  locale: ""
 | 
			
		||||
  viewMode: ""
 | 
			
		||||
  darkMode: true
 | 
			
		||||
  disableSettings: false
 | 
			
		||||
  singleClick: true
 | 
			
		||||
  sorting:
 | 
			
		||||
    by: ""
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@ package bolt
 | 
			
		|||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log"
 | 
			
		||||
	"reflect"
 | 
			
		||||
 | 
			
		||||
	"github.com/asdine/storm/v3"
 | 
			
		||||
| 
						 | 
				
			
			@ -73,7 +74,13 @@ func (st usersBackend) Update(user *users.User, fields ...string) error {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
func (st usersBackend) Save(user *users.User) error {
 | 
			
		||||
	err := st.db.Save(user)
 | 
			
		||||
	log.Println("userinfo", user.Password)
 | 
			
		||||
	pass, err := users.HashPwd(user.Password)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	user.Password = pass
 | 
			
		||||
	err = st.db.Save(user)
 | 
			
		||||
	if err == storm.ErrAlreadyExists {
 | 
			
		||||
		return errors.ErrExist
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,14 @@
 | 
			
		|||
package users
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"log"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/crypto/bcrypt"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// HashPwd hashes a password.
 | 
			
		||||
func HashPwd(password string) (string, error) {
 | 
			
		||||
	log.Println("hashing password", password)
 | 
			
		||||
	bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
 | 
			
		||||
	return string(bytes), err
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -73,7 +73,7 @@ func (s *Storage) Gets(baseScope string) ([]*User, error) {
 | 
			
		|||
 | 
			
		||||
// Update updates a user in the database.
 | 
			
		||||
func (s *Storage) Update(user *User, fields ...string) error {
 | 
			
		||||
	err := user.Clean("", fields...)
 | 
			
		||||
	err := user.Clean("")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,16 +6,10 @@ import (
 | 
			
		|||
 | 
			
		||||
	"github.com/spf13/afero"
 | 
			
		||||
 | 
			
		||||
	"github.com/gtsteffaniak/filebrowser/errors"
 | 
			
		||||
	"github.com/gtsteffaniak/filebrowser/files"
 | 
			
		||||
	"github.com/gtsteffaniak/filebrowser/rules"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	ListViewMode   = "list"
 | 
			
		||||
	MosaicViewMode = "mosaic"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Permissions struct {
 | 
			
		||||
	Admin    bool `json:"admin"`
 | 
			
		||||
	Execute  bool `json:"execute"`
 | 
			
		||||
| 
						 | 
				
			
			@ -29,6 +23,8 @@ type Permissions struct {
 | 
			
		|||
 | 
			
		||||
// User describes a user.
 | 
			
		||||
type User struct {
 | 
			
		||||
	DarkMode        bool          `json:"darkMode"`
 | 
			
		||||
	DisableSettings bool          `json:"disableSettings"`
 | 
			
		||||
	ID              uint          `storm:"id,increment" json:"id"`
 | 
			
		||||
	Username        string        `storm:"unique" json:"username"`
 | 
			
		||||
	Password        string        `json:"password"`
 | 
			
		||||
| 
						 | 
				
			
			@ -51,53 +47,11 @@ func (u *User) GetRules() []rules.Rule {
 | 
			
		|||
	return u.Rules
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var checkableFields = []string{
 | 
			
		||||
	"Username",
 | 
			
		||||
	"Password",
 | 
			
		||||
	"Scope",
 | 
			
		||||
	"ViewMode",
 | 
			
		||||
	"Commands",
 | 
			
		||||
	"Sorting",
 | 
			
		||||
	"Rules",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Clean cleans up a user and verifies if all its fields
 | 
			
		||||
// are alright to be saved.
 | 
			
		||||
//
 | 
			
		||||
//nolint:gocyclo
 | 
			
		||||
func (u *User) Clean(baseScope string, fields ...string) error {
 | 
			
		||||
	if len(fields) == 0 {
 | 
			
		||||
		fields = checkableFields
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, field := range fields {
 | 
			
		||||
		switch field {
 | 
			
		||||
		case "Username":
 | 
			
		||||
			if u.Username == "" {
 | 
			
		||||
				return errors.ErrEmptyUsername
 | 
			
		||||
			}
 | 
			
		||||
		case "Password":
 | 
			
		||||
			if u.Password == "" {
 | 
			
		||||
				return errors.ErrEmptyPassword
 | 
			
		||||
			}
 | 
			
		||||
		case "ViewMode":
 | 
			
		||||
			if u.ViewMode == "" {
 | 
			
		||||
				u.ViewMode = ListViewMode
 | 
			
		||||
			}
 | 
			
		||||
		case "Commands":
 | 
			
		||||
			if u.Commands == nil {
 | 
			
		||||
				u.Commands = []string{}
 | 
			
		||||
			}
 | 
			
		||||
		case "Sorting":
 | 
			
		||||
			if u.Sorting.By == "" {
 | 
			
		||||
				u.Sorting.By = "name"
 | 
			
		||||
			}
 | 
			
		||||
		case "Rules":
 | 
			
		||||
			if u.Rules == nil {
 | 
			
		||||
				u.Rules = []rules.Rule{}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
func (u *User) Clean(baseScope string) error {
 | 
			
		||||
 | 
			
		||||
	if u.Fs == nil {
 | 
			
		||||
		scope := u.Scope
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@ package version
 | 
			
		|||
 | 
			
		||||
var (
 | 
			
		||||
	// Version is the current File Browser version.
 | 
			
		||||
	Version = "(0.2.0)"
 | 
			
		||||
	Version = "(0.2.1)"
 | 
			
		||||
	// CommitSHA is the commmit sha.
 | 
			
		||||
	CommitSHA = "(unknown)"
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -40,6 +40,8 @@ frontend:
 | 
			
		|||
  theme: ""
 | 
			
		||||
  color: ""
 | 
			
		||||
userDefaults:
 | 
			
		||||
  settingsAllowed: true
 | 
			
		||||
  darkMode: false
 | 
			
		||||
  scope: ""
 | 
			
		||||
  locale: ""
 | 
			
		||||
  viewMode: ""
 | 
			
		||||
| 
						 | 
				
			
			@ -176,6 +178,10 @@ UserDefaults:
 | 
			
		|||
  
 | 
			
		||||
### UserDefaults configuration settings
 | 
			
		||||
 | 
			
		||||
- `darkMode`: Determines whether dark mode is enabled for the user (true or false)
 | 
			
		||||
 | 
			
		||||
- `settingsAllowed`: Determines whether settings page is enabled for the user (true or false)
 | 
			
		||||
 | 
			
		||||
- `scope`: This is a scope of the permissions, "." or "./" means all directories, "./downloads" would mean only the downloads folder.
 | 
			
		||||
 | 
			
		||||
- `locale`: This is the locale configuration.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,7 +35,7 @@
 | 
			
		|||
      "devDependencies": {
 | 
			
		||||
        "@vue/cli-service": "^5.0.8",
 | 
			
		||||
        "compression-webpack-plugin": "^10.0.0",
 | 
			
		||||
        "eslint": "^8.50.0",
 | 
			
		||||
        "eslint": "^8.51.0",
 | 
			
		||||
        "eslint-plugin-vue": "^9.17.0",
 | 
			
		||||
        "vue-template-compiler": "^2.6.10"
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			@ -293,9 +293,9 @@
 | 
			
		|||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@eslint-community/regexpp": {
 | 
			
		||||
      "version": "4.8.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.1.tgz",
 | 
			
		||||
      "integrity": "sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ==",
 | 
			
		||||
      "version": "4.9.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.1.tgz",
 | 
			
		||||
      "integrity": "sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
 | 
			
		||||
| 
						 | 
				
			
			@ -347,9 +347,9 @@
 | 
			
		|||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@eslint/js": {
 | 
			
		||||
      "version": "8.50.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz",
 | 
			
		||||
      "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==",
 | 
			
		||||
      "version": "8.51.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz",
 | 
			
		||||
      "integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
 | 
			
		||||
| 
						 | 
				
			
			@ -3025,15 +3025,15 @@
 | 
			
		|||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/eslint": {
 | 
			
		||||
      "version": "8.50.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz",
 | 
			
		||||
      "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==",
 | 
			
		||||
      "version": "8.51.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz",
 | 
			
		||||
      "integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@eslint-community/eslint-utils": "^4.2.0",
 | 
			
		||||
        "@eslint-community/regexpp": "^4.6.1",
 | 
			
		||||
        "@eslint/eslintrc": "^2.1.2",
 | 
			
		||||
        "@eslint/js": "8.50.0",
 | 
			
		||||
        "@eslint/js": "8.51.0",
 | 
			
		||||
        "@humanwhocodes/config-array": "^0.11.11",
 | 
			
		||||
        "@humanwhocodes/module-importer": "^1.0.1",
 | 
			
		||||
        "@nodelib/fs.walk": "^1.2.8",
 | 
			
		||||
| 
						 | 
				
			
			@ -3774,12 +3774,12 @@
 | 
			
		|||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/flat-cache": {
 | 
			
		||||
      "version": "3.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==",
 | 
			
		||||
      "version": "3.1.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz",
 | 
			
		||||
      "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "flatted": "^3.2.7",
 | 
			
		||||
        "flatted": "^3.2.9",
 | 
			
		||||
        "keyv": "^4.5.3",
 | 
			
		||||
        "rimraf": "^3.0.2"
 | 
			
		||||
      },
 | 
			
		||||
| 
						 | 
				
			
			@ -3970,9 +3970,9 @@
 | 
			
		|||
      "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/globals": {
 | 
			
		||||
      "version": "13.22.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz",
 | 
			
		||||
      "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==",
 | 
			
		||||
      "version": "13.23.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz",
 | 
			
		||||
      "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "type-fest": "^0.20.2"
 | 
			
		||||
| 
						 | 
				
			
			@ -4747,9 +4747,9 @@
 | 
			
		|||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/keyv": {
 | 
			
		||||
      "version": "4.5.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz",
 | 
			
		||||
      "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==",
 | 
			
		||||
      "version": "4.5.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
 | 
			
		||||
      "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "json-buffer": "3.0.1"
 | 
			
		||||
| 
						 | 
				
			
			@ -7974,9 +7974,9 @@
 | 
			
		|||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/vue-eslint-parser": {
 | 
			
		||||
      "version": "9.3.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.1.tgz",
 | 
			
		||||
      "integrity": "sha512-Clr85iD2XFZ3lJ52/ppmUDG/spxQu6+MAeHXjjyI4I1NUYZ9xmenQp4N0oaHJhrA8OOxltCVxMRfANGa70vU0g==",
 | 
			
		||||
      "version": "9.3.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.2.tgz",
 | 
			
		||||
      "integrity": "sha512-q7tWyCVaV9f8iQyIA5Mkj/S6AoJ9KBN8IeUSf3XEmBrOtxOZnfTg5s4KClbZBCK3GtnT/+RyCLZyDHuZwTuBjg==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "debug": "^4.3.4",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -39,7 +39,7 @@
 | 
			
		|||
  "devDependencies": {
 | 
			
		||||
    "@vue/cli-service": "^5.0.8",
 | 
			
		||||
    "compression-webpack-plugin": "^10.0.0",
 | 
			
		||||
    "eslint": "^8.50.0",
 | 
			
		||||
    "eslint": "^8.51.0",
 | 
			
		||||
    "eslint-plugin-vue": "^9.17.0",
 | 
			
		||||
    "vue-template-compiler": "^2.6.10"
 | 
			
		||||
  },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -128,8 +128,8 @@
 | 
			
		|||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  [{[ if .Theme -]}]
 | 
			
		||||
    <link rel="stylesheet" href="[{[ .StaticURL ]}]/themes/[{[ .Theme ]}].css" />
 | 
			
		||||
  [{[ if .darkMode -]}]
 | 
			
		||||
    <link rel="stylesheet" href="[{[ .StaticURL ]}]/themes/dark.css" />
 | 
			
		||||
  [{[ end ]}]
 | 
			
		||||
  [{[ if .CSS -]}]
 | 
			
		||||
    <link rel="stylesheet" href="[{[ .StaticURL ]}]/custom.css" />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,220 +0,0 @@
 | 
			
		|||
:root {
 | 
			
		||||
  --background: #141D24;
 | 
			
		||||
  --surfacePrimary: #20292F;
 | 
			
		||||
  --surfaceSecondary: #3A4147;
 | 
			
		||||
  --divider: rgba(255, 255, 255, 0.12);
 | 
			
		||||
  --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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#login {
 | 
			
		||||
  background: var(--background);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
header {
 | 
			
		||||
  background: var(--surfacePrimary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@supports (backdrop-filter: none) {
 | 
			
		||||
  header {
 | 
			
		||||
    background: transparent;
 | 
			
		||||
    backdrop-filter: blur(16px) invert(0.1);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#search #input {
 | 
			
		||||
  background: var(--surfaceSecondary);
 | 
			
		||||
  border-color: var(--surfaceSecondary);
 | 
			
		||||
}
 | 
			
		||||
#search #input input::placeholder {
 | 
			
		||||
  color: var(--textSecondary);
 | 
			
		||||
}
 | 
			
		||||
#search.active #input {
 | 
			
		||||
  background: var(--surfacePrimary);
 | 
			
		||||
  border-color: white;
 | 
			
		||||
}
 | 
			
		||||
#search.active input {
 | 
			
		||||
  color: var(--textPrimary);
 | 
			
		||||
}
 | 
			
		||||
#search #result {
 | 
			
		||||
  background: var(--background);
 | 
			
		||||
  color: var(--textPrimary);
 | 
			
		||||
}
 | 
			
		||||
#search .boxes h3 {
 | 
			
		||||
  color: var(--textPrimary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.action {
 | 
			
		||||
  color: var(--textPrimary) !important;
 | 
			
		||||
}
 | 
			
		||||
.action:hover {
 | 
			
		||||
  background-color: rgba(255, 255, 255, .1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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 .modified {
 | 
			
		||||
  color: var(--textSecondary);
 | 
			
		||||
}
 | 
			
		||||
#listing h2,
 | 
			
		||||
#listing.list .header span {
 | 
			
		||||
  color: var(--textPrimary) !important;
 | 
			
		||||
}
 | 
			
		||||
#listing.list .header span {
 | 
			
		||||
  color: var(--textPrimary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#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);
 | 
			
		||||
}
 | 
			
		||||
#result-list {
 | 
			
		||||
  background-color:#292929;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card h3,
 | 
			
		||||
.dashboard #nav,
 | 
			
		||||
.dashboard p label {
 | 
			
		||||
  color: var(--textPrimary);
 | 
			
		||||
}
 | 
			
		||||
.card#share input,
 | 
			
		||||
.card#share select,
 | 
			
		||||
.input {
 | 
			
		||||
  background: var(--surfaceSecondary);
 | 
			
		||||
  color: var(--textPrimary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.shell {
 | 
			
		||||
  background: var(--surfacePrimary);
 | 
			
		||||
  color: var(--textPrimary);
 | 
			
		||||
}
 | 
			
		||||
.shell__result {
 | 
			
		||||
  border-top: 1px solid var(--divider);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#editor-container {
 | 
			
		||||
  background: var(--background);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#editor-container .bar {
 | 
			
		||||
  background: var(--surfacePrimary);
 | 
			
		||||
}
 | 
			
		||||
nav {
 | 
			
		||||
  background: var(--surfaceSecondary) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#file-selection {
 | 
			
		||||
  background: var(--surfaceSecondary) !important;
 | 
			
		||||
}
 | 
			
		||||
#file-selection span {
 | 
			
		||||
  color: var(--textPrimary) !important;
 | 
			
		||||
}
 | 
			
		||||
#dropdown {
 | 
			
		||||
  background: var(--surfaceSecondary) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.share__box {
 | 
			
		||||
  background: var(--surfacePrimary) !important;
 | 
			
		||||
  color: var(--textPrimary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.share__box__element {
 | 
			
		||||
  border-top-color: var(--divider);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.helpButton {
 | 
			
		||||
  background: var(--background);
 | 
			
		||||
}
 | 
			
		||||
.sizeInputWrapper {
 | 
			
		||||
  background: var(--background);
 | 
			
		||||
  color: white
 | 
			
		||||
}
 | 
			
		||||
.button-group button {
 | 
			
		||||
  background: var(--background);
 | 
			
		||||
  color: white
 | 
			
		||||
}
 | 
			
		||||
#result-desktop #result-list {
 | 
			
		||||
  background: #2a3137;
 | 
			
		||||
  max-height: unset;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -3,16 +3,18 @@
 | 
			
		|||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line no-undef
 | 
			
		||||
__webpack_public_path__ = window.FileBrowser.StaticURL + "/";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: "app",
 | 
			
		||||
  computed: {
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    const loading = document.getElementById("loading");
 | 
			
		||||
    loading.classList.add("done");
 | 
			
		||||
 | 
			
		||||
    setTimeout(function () {
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      loading.parentNode.removeChild(loading);
 | 
			
		||||
    }, 200);
 | 
			
		||||
  },
 | 
			
		||||
| 
						 | 
				
			
			@ -20,5 +22,7 @@ export default {
 | 
			
		|||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
/* Always load styles.css */
 | 
			
		||||
@import "./css/styles.css";
 | 
			
		||||
@import "./css/dark.css";
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div id="search" @click="open" v-bind:class="{ active, ongoing }">
 | 
			
		||||
  <div id="search" @click="open" v-bind:class="{ active, ongoing, 'dark-mode': isDarkMode }">
 | 
			
		||||
    <div id="input">
 | 
			
		||||
      <button v-if="active" class="action" @click="close" :aria-label="$t('buttons.close')" :title="$t('buttons.close')">
 | 
			
		||||
        <i class="material-icons">close</i>
 | 
			
		||||
| 
						 | 
				
			
			@ -149,6 +149,7 @@
 | 
			
		|||
  padding-bottom: 1em;
 | 
			
		||||
  -webkit-transition: width 0.3s ease 0s;
 | 
			
		||||
  transition: width 0.3s ease 0s;
 | 
			
		||||
  background-color: unset;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#result-desktop {
 | 
			
		||||
| 
						 | 
				
			
			@ -172,6 +173,8 @@
 | 
			
		|||
  background-color: lightgray;
 | 
			
		||||
  max-height: 80vh;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#search.active #result-desktop ul li a {
 | 
			
		||||
| 
						 | 
				
			
			@ -201,6 +204,7 @@
 | 
			
		|||
 | 
			
		||||
/* Search */
 | 
			
		||||
#search {
 | 
			
		||||
  background-color:unset;
 | 
			
		||||
  z-index:3;
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  top: .5em;
 | 
			
		||||
| 
						 | 
				
			
			@ -314,7 +318,7 @@ body.rtl #search #result ul>* {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
#search.active #input {
 | 
			
		||||
  background-color: lightgray;
 | 
			
		||||
  background-color: var(--background);
 | 
			
		||||
  border-color: black;
 | 
			
		||||
  border-style: solid;
 | 
			
		||||
  border-bottom-style: none;
 | 
			
		||||
| 
						 | 
				
			
			@ -485,7 +489,7 @@ export default {
 | 
			
		|||
        { label: "Photos", value: "type:image" },
 | 
			
		||||
        { label: "Audio", value: "type:audio" },
 | 
			
		||||
        { label: "Videos", value: "type:video" },
 | 
			
		||||
        { label: "Documents", value: "type:docs" },
 | 
			
		||||
        { label: "Documents", value: "type:doc" },
 | 
			
		||||
        { label: "Archives", value: "type:archive" },
 | 
			
		||||
      ],
 | 
			
		||||
      value: "",
 | 
			
		||||
| 
						 | 
				
			
			@ -538,6 +542,9 @@ export default {
 | 
			
		|||
  computed: {
 | 
			
		||||
    ...mapState(["user", "show"]),
 | 
			
		||||
    ...mapGetters(["isListing"]),
 | 
			
		||||
    isDarkMode() {
 | 
			
		||||
      return this.user.darkMode === true
 | 
			
		||||
    },
 | 
			
		||||
    showBoxes() {
 | 
			
		||||
      return this.searchTypes == "";
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <nav :class="{ active }">
 | 
			
		||||
  <nav :class="{ active, 'dark-mode': isDarkMode }">
 | 
			
		||||
    <template v-if="isLogged">
 | 
			
		||||
      <button class="action" @click="toRoot" :aria-label="$t('sidebar.myFiles')" :title="$t('sidebar.myFiles')">
 | 
			
		||||
        <i class="material-icons">folder</i>
 | 
			
		||||
| 
						 | 
				
			
			@ -87,6 +87,9 @@ export default {
 | 
			
		|||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapState(["user"]),
 | 
			
		||||
    isDarkMode() {
 | 
			
		||||
      return this.user.darkMode === true
 | 
			
		||||
    },
 | 
			
		||||
    ...mapGetters(["isLogged"]),
 | 
			
		||||
    active() {
 | 
			
		||||
      return this.$store.state.show === "sidebar";
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,24 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <select v-on:change="change" :value="viewMode">
 | 
			
		||||
    <option v-for="mode in viewModes" :key="mode" :value="mode">
 | 
			
		||||
      {{ mode }}
 | 
			
		||||
    </option>
 | 
			
		||||
  </select>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  name: "ViewMode",
 | 
			
		||||
  props: ["viewMode"],
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      viewModes: ['list', 'compact', 'normal', 'gallery'],
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    change(event) {
 | 
			
		||||
      this.$emit("update:viewMode", event.target.value);
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -203,6 +203,7 @@ body.rtl .breadcrumbs a {
 | 
			
		|||
  width: 95%;
 | 
			
		||||
  max-width: 30em;
 | 
			
		||||
  z-index: 1;
 | 
			
		||||
  border-radius: 1em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,292 @@
 | 
			
		|||
/* Define a class .dark-mode for dark mode styles */
 | 
			
		||||
.dark-mode {
 | 
			
		||||
  --background: #141D24;
 | 
			
		||||
  --surfacePrimary: #20292F;
 | 
			
		||||
  --surfaceSecondary: #3A4147;
 | 
			
		||||
  --divider: rgba(255, 255, 255, 0.12);
 | 
			
		||||
  --textPrimary: rgba(255, 255, 255, 0.87);
 | 
			
		||||
  --textSecondary: rgba(255, 255, 255, 0.6);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dark-mode #loading {
 | 
			
		||||
  background: var(--background);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dark-mode #login {
 | 
			
		||||
  background: var(--background);
 | 
			
		||||
}
 | 
			
		||||
/* Loading */
 | 
			
		||||
.dark-mode #loading {
 | 
			
		||||
  background: var(--background);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Login */
 | 
			
		||||
.dark-mode #login {
 | 
			
		||||
  background: var(--background);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Header */
 | 
			
		||||
.dark-mode header {
 | 
			
		||||
  background: var(--surfacePrimary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Header with backdrop-filter support */
 | 
			
		||||
@supports (backdrop-filter: none) {
 | 
			
		||||
  .dark-mode header {
 | 
			
		||||
    background: transparent;
 | 
			
		||||
    backdrop-filter: blur(16px) invert(0.1);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#search.dark-mode input {
 | 
			
		||||
  color:white
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#search.active.dark-mode #input {
 | 
			
		||||
  border-color: white;
 | 
			
		||||
}
 | 
			
		||||
/* Search input */
 | 
			
		||||
.dark-mode #search #input {
 | 
			
		||||
  background: var(--surfaceSecondary);
 | 
			
		||||
  border-color: var(--surfaceSecondary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dark-mode #search #input input::placeholder {
 | 
			
		||||
  color: var(--textSecondary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Active Search input */
 | 
			
		||||
.dark-mode #search.active #input {
 | 
			
		||||
  background: var(--surfacePrimary);
 | 
			
		||||
  border-color: white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dark-mode #search.active input {
 | 
			
		||||
  color: var(--textPrimary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Search result */
 | 
			
		||||
.dark-mode #search #result {
 | 
			
		||||
  background: var(--background);
 | 
			
		||||
  color: var(--textPrimary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Search boxes */
 | 
			
		||||
.dark-mode #search .boxes h3 {
 | 
			
		||||
  color: var(--textPrimary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Action */
 | 
			
		||||
.dark-mode .action {
 | 
			
		||||
  color: var(--textPrimary) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dark-mode .action:hover {
 | 
			
		||||
  background-color: rgba(255, 255, 255, .1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Action counter */
 | 
			
		||||
.dark-mode .action .counter {
 | 
			
		||||
  border-color: var(--surfacePrimary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Navigation */
 | 
			
		||||
.dark-mode nav > div {
 | 
			
		||||
  border-color: var(--divider);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Breadcrumbs */
 | 
			
		||||
.dark-mode .breadcrumbs {
 | 
			
		||||
  border-color: var(--divider);
 | 
			
		||||
  color: var(--textPrimary) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dark-mode .breadcrumbs span {
 | 
			
		||||
  color: var(--textPrimary) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dark-mode .breadcrumbs a:hover {
 | 
			
		||||
  background-color: rgba(255, 255, 255, .1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Listing items */
 | 
			
		||||
.dark-mode #listing .item {
 | 
			
		||||
  background: var(--surfacePrimary);
 | 
			
		||||
  color: var(--textPrimary);
 | 
			
		||||
  border-color: var(--divider) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Listing item modified text */
 | 
			
		||||
.dark-mode #listing .item .modified {
 | 
			
		||||
  color: var(--textSecondary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Listing header and span */
 | 
			
		||||
.dark-mode #listing h2,
 | 
			
		||||
.dark-mode #listing.list .header span {
 | 
			
		||||
  color: var(--textPrimary) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Message */
 | 
			
		||||
.dark-mode .message {
 | 
			
		||||
  color: var(--textPrimary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Card */
 | 
			
		||||
.dark-mode .card {
 | 
			
		||||
  background: var(--surfacePrimary);
 | 
			
		||||
  color: var(--textPrimary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Flat button hover */
 | 
			
		||||
.dark-mode .button--flat:hover {
 | 
			
		||||
  background: var(--surfaceSecondary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Dashboard navigation */
 | 
			
		||||
.dark-mode .dashboard #nav ul li {
 | 
			
		||||
  color: var(--textSecondary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dark-mode .dashboard #nav ul li:hover {
 | 
			
		||||
  background: var(--surfaceSecondary);
 | 
			
		||||
}
 | 
			
		||||
#search.active.dark-mode #result {
 | 
			
		||||
  background-color: black;
 | 
			
		||||
}
 | 
			
		||||
/* Result list */
 | 
			
		||||
.dark-mode #result-list {
 | 
			
		||||
  background-color: var(--surfacePrimary);
 | 
			
		||||
  color:white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Card, Dashboard navigation, and label */
 | 
			
		||||
.dark-mode .card h3,
 | 
			
		||||
.dark-mode .dashboard #nav,
 | 
			
		||||
.dark-mode .dashboard p label {
 | 
			
		||||
  color: var(--textPrimary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dark-mode .card#share input,
 | 
			
		||||
.dark-mode .card#share select,
 | 
			
		||||
.dark-mode .input {
 | 
			
		||||
  background: var(--surfaceSecondary);
 | 
			
		||||
  color: var(--textPrimary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Input hover and focus */
 | 
			
		||||
.dark-mode .input:hover,
 | 
			
		||||
.dark-mode .input:focus {
 | 
			
		||||
  border-color: rgba(255, 255, 255, 0.15);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Red input */
 | 
			
		||||
.dark-mode .input--red {
 | 
			
		||||
  background: #73302D;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Green input */
 | 
			
		||||
.dark-mode .input--green {
 | 
			
		||||
  background: #147A41;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Collapsible and label */
 | 
			
		||||
.dark-mode .dashboard #nav .wrapper,
 | 
			
		||||
.dark-mode .collapsible {
 | 
			
		||||
  border-color: var(--divider);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dark-mode .collapsible > label * {
 | 
			
		||||
  color: var(--textPrimary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Table header */
 | 
			
		||||
.dark-mode table th {
 | 
			
		||||
  color: var(--textSecondary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* File list item */
 | 
			
		||||
.dark-mode .file-list li:hover {
 | 
			
		||||
  background: var(--surfaceSecondary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dark-mode .file-list li:before {
 | 
			
		||||
  color: var(--textSecondary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Shell */
 | 
			
		||||
.dark-mode .shell {
 | 
			
		||||
  background: var(--surfacePrimary);
 | 
			
		||||
  color: var(--textPrimary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Shell result */
 | 
			
		||||
.dark-mode .shell__result {
 | 
			
		||||
  border-top: 1px solid var(--divider);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Editor container */
 | 
			
		||||
.dark-mode #editor-container {
 | 
			
		||||
  background: var(--background);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dark-mode #editor-container .bar {
 | 
			
		||||
  background: var(--surfacePrimary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Navigation */
 | 
			
		||||
.dark-mode nav {
 | 
			
		||||
  background: var(--surfaceSecondary) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* File selection */
 | 
			
		||||
.dark-mode #file-selection {
 | 
			
		||||
  background: var(--surfaceSecondary) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dark-mode #file-selection span {
 | 
			
		||||
  color: var(--textPrimary) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Dropdown */
 | 
			
		||||
.dark-mode #dropdown {
 | 
			
		||||
  background: var(--surfaceSecondary) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Share box */
 | 
			
		||||
.dark-mode .share__box {
 | 
			
		||||
  background: var(--surfacePrimary) !important;
 | 
			
		||||
  color: var(--textPrimary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Share box element */
 | 
			
		||||
.dark-mode .share__box__element {
 | 
			
		||||
  border-top-color: var(--divider);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Help button */
 | 
			
		||||
.dark-mode .helpButton {
 | 
			
		||||
  background: var(--background);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Size input wrapper */
 | 
			
		||||
.dark-mode .sizeInputWrapper {
 | 
			
		||||
  background: var(--background);
 | 
			
		||||
  color: white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Button group button */
 | 
			
		||||
.dark-mode .button-group button {
 | 
			
		||||
  background: var(--background);
 | 
			
		||||
  color: white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Result desktop */
 | 
			
		||||
.dark-mode #result-desktop #result-list {
 | 
			
		||||
  max-height: unset;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Result desktop background */
 | 
			
		||||
.dark-mode #result-desktop {
 | 
			
		||||
  background-color: var(--background);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -85,41 +85,53 @@ body.rtl #listing {
 | 
			
		|||
  display: block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.mosaic {
 | 
			
		||||
#listing {
 | 
			
		||||
  padding-top: 1em;
 | 
			
		||||
  margin: 0 -0.5em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.mosaic .item {
 | 
			
		||||
#listing.gallery .item,
 | 
			
		||||
#listing.compact .item,
 | 
			
		||||
#listing.normal .item,
 | 
			
		||||
#listing.list .item {
 | 
			
		||||
  width: calc(33% - 1em);
 | 
			
		||||
  max-width: 300px;
 | 
			
		||||
  margin: .5em;
 | 
			
		||||
  padding: 0.5em;
 | 
			
		||||
  border-radius: 1em;
 | 
			
		||||
  box-shadow: 0 1px 3px rgba(0, 0, 0, .06), 0 1px 2px rgba(0, 0, 0, .12);
 | 
			
		||||
}
 | 
			
		||||
#listing.gallery .item {
 | 
			
		||||
  max-width: 300px;
 | 
			
		||||
}
 | 
			
		||||
#listing.list .item,
 | 
			
		||||
#listing.compact .item {
 | 
			
		||||
  max-width: 100%;
 | 
			
		||||
  border-radius: 0em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.mosaic .item:hover {
 | 
			
		||||
#listing .item:hover {
 | 
			
		||||
  box-shadow: 0 1px 3px rgba(0, 0, 0, .12), 0 1px 2px rgba(0, 0, 0, .24) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.mosaic .header {
 | 
			
		||||
#listing .header {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.mosaic .item div:first-of-type {
 | 
			
		||||
#listing .item div:first-of-type {
 | 
			
		||||
  width: 5em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.mosaic .item div:last-of-type {
 | 
			
		||||
#listing .item div:last-of-type {
 | 
			
		||||
  width: calc(100% - 5vw);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.mosaic.gallery .item div:first-of-type {
 | 
			
		||||
#listing.gallery .item div:first-of-type {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 12em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.mosaic.gallery .item div:last-of-type {
 | 
			
		||||
#listing.gallery .item div:last-of-type {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  bottom: 0.5em;
 | 
			
		||||
  padding: 1em;
 | 
			
		||||
| 
						 | 
				
			
			@ -127,19 +139,19 @@ body.rtl #listing {
 | 
			
		|||
  text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.mosaic.gallery .item[data-type=image] div:last-of-type {
 | 
			
		||||
#listing.gallery .item[data-type=image] div:last-of-type {
 | 
			
		||||
  color: white;
 | 
			
		||||
  background: linear-gradient(#0000, #0009);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.mosaic.gallery .item i {
 | 
			
		||||
#listing.gallery .item i {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    margin-right: 0;
 | 
			
		||||
    font-size: 8em;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.mosaic.gallery .item img {
 | 
			
		||||
#listing.gallery .item img {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -149,6 +161,109 @@ body.rtl #listing {
 | 
			
		|||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact {
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  max-width: 100%;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .item {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  border: 1px solid rgba(0, 0, 0, 0.1);
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  border-top: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact h2 {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .item div:first-of-type {
 | 
			
		||||
  width: 3em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .item div:first-of-type i {
 | 
			
		||||
  font-size: 2em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .item div:first-of-type img {
 | 
			
		||||
  width: 2em;
 | 
			
		||||
  height: 2em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .item div:last-of-type {
 | 
			
		||||
  width: calc(100% - 3em);
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .item .name {
 | 
			
		||||
  width: 50%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .item .size {
 | 
			
		||||
  width: 25%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .header i {
 | 
			
		||||
  font-size: 1.5em;
 | 
			
		||||
  vertical-align: middle;
 | 
			
		||||
  margin-left: .2em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .header {
 | 
			
		||||
  display: flex !important;
 | 
			
		||||
  background: var(--surfacePrimary);
 | 
			
		||||
  z-index: 999;
 | 
			
		||||
  padding: .85em;
 | 
			
		||||
  border: 0;
 | 
			
		||||
  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .header>div:first-child {
 | 
			
		||||
  width: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .header .name {
 | 
			
		||||
  margin-right: 3em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .header a {
 | 
			
		||||
  color: inherit;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .header>div:first-child {
 | 
			
		||||
  width: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .name {
 | 
			
		||||
  font-weight: normal;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .header .name {
 | 
			
		||||
  margin-right: 3em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .header span {
 | 
			
		||||
  vertical-align: middle;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .header i {
 | 
			
		||||
  opacity: 0;
 | 
			
		||||
  transition: .1s ease all;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .header p:hover i,
 | 
			
		||||
#listing.compact .header .active i {
 | 
			
		||||
  opacity: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .header .active {
 | 
			
		||||
  font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.list {
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
| 
						 | 
				
			
			@ -160,14 +275,10 @@ body.rtl #listing {
 | 
			
		|||
  width: 100%;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  border: 1px solid rgba(0, 0, 0, 0.1);
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  padding: .5em;
 | 
			
		||||
  border-top: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.list h2 {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing .item[aria-selected=true] {
 | 
			
		||||
  background: var(--blue) !important;
 | 
			
		||||
  color: var(--item-selected) !important;
 | 
			
		||||
| 
						 | 
				
			
			@ -200,7 +311,7 @@ body.rtl #listing {
 | 
			
		|||
  width: 25%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing .item.header {
 | 
			
		||||
#listing .header {
 | 
			
		||||
  display: none !important;
 | 
			
		||||
  background-color: #ccc;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -211,20 +322,34 @@ body.rtl #listing {
 | 
			
		|||
  margin-left: .2em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.list .item.header {
 | 
			
		||||
#listing.compact .header,
 | 
			
		||||
#listing.list .header {
 | 
			
		||||
  display: flex !important;
 | 
			
		||||
  background: #fafafa;
 | 
			
		||||
  background: var(--surfacePrimary);
 | 
			
		||||
  border-top-left-radius: 1em;
 | 
			
		||||
  border-top-right-radius: 1em;
 | 
			
		||||
  z-index: 999;
 | 
			
		||||
  padding: .85em;
 | 
			
		||||
  width:100%;
 | 
			
		||||
  border: 0;
 | 
			
		||||
  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
 | 
			
		||||
}
 | 
			
		||||
#listing.list .item:first-child {
 | 
			
		||||
  margin-top: .5em;
 | 
			
		||||
  border-top-left-radius: 1em;
 | 
			
		||||
  border-top-right-radius: 1em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.list .item.header>div:first-child {
 | 
			
		||||
#listing.list .item:last-child  {
 | 
			
		||||
  margin-bottom: .5em;
 | 
			
		||||
   border-bottom-left-radius: 1em;
 | 
			
		||||
   border-bottom-right-radius: 1em;
 | 
			
		||||
}
 | 
			
		||||
#listing.list .header>div:first-child {
 | 
			
		||||
  width: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.list .item.header .name {
 | 
			
		||||
#listing.list .header .name {
 | 
			
		||||
  margin-right: 3em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -232,7 +357,7 @@ body.rtl #listing {
 | 
			
		|||
  color: inherit;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.list .item.header>div:first-child {
 | 
			
		||||
#listing.list .header>div:first-child {
 | 
			
		||||
  width: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -240,7 +365,7 @@ body.rtl #listing {
 | 
			
		|||
  font-weight: normal;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.list .item.header .name {
 | 
			
		||||
#listing.list .header .name {
 | 
			
		||||
  margin-right: 3em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -258,7 +383,7 @@ body.rtl #listing {
 | 
			
		|||
  opacity: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.list .item.header .active {
 | 
			
		||||
#listing.list .header .active {
 | 
			
		||||
  font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,14 +7,14 @@
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
@media (max-width: 800px) {
 | 
			
		||||
  #listing.list .item div:last-of-type{
 | 
			
		||||
    display:block;
 | 
			
		||||
    width:100%;
 | 
			
		||||
  }
 | 
			
		||||
  body {
 | 
			
		||||
    padding-bottom: 5em;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #listing.list .item .size {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #listing.list .item .name {
 | 
			
		||||
    width: 60%;
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -36,7 +36,9 @@
 | 
			
		|||
  #listing {
 | 
			
		||||
    margin-bottom: 5em;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #listing .item {
 | 
			
		||||
    min-width: 100%
 | 
			
		||||
  }
 | 
			
		||||
  body.rtl #listing {
 | 
			
		||||
    margin-right: unset;
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -117,9 +119,6 @@
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
@media (max-width: 450px) {
 | 
			
		||||
  #listing.list .item .modified {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #listing.list .item .name {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,9 +11,7 @@ export function parseToken(token) {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  const data = JSON.parse(Base64.decode(parts[1]));
 | 
			
		||||
 | 
			
		||||
  document.cookie = `auth=${token}; path=/`;
 | 
			
		||||
 | 
			
		||||
  localStorage.setItem("jwt", token);
 | 
			
		||||
  store.commit("setJWT", token);
 | 
			
		||||
  store.commit("setSession", generateRandomCode(8));
 | 
			
		||||
| 
						 | 
				
			
			@ -40,7 +38,6 @@ export async function login(username, password, recaptcha) {
 | 
			
		|||
    },
 | 
			
		||||
    body: JSON.stringify(data),
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const body = await res.text();
 | 
			
		||||
 | 
			
		||||
  if (res.status === 200) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,7 +11,6 @@ const logoURL = `${staticURL}/img/logo.svg`;
 | 
			
		|||
const noAuth = window.FileBrowser.NoAuth;
 | 
			
		||||
const authMethod = window.FileBrowser.AuthMethod;
 | 
			
		||||
const loginPage = window.FileBrowser.LoginPage;
 | 
			
		||||
const theme = window.FileBrowser.Theme;
 | 
			
		||||
const enableThumbs = window.FileBrowser.EnableThumbs;
 | 
			
		||||
const resizePreview = window.FileBrowser.ResizePreview;
 | 
			
		||||
const enableExec = window.FileBrowser.EnableExec;
 | 
			
		||||
| 
						 | 
				
			
			@ -30,7 +29,6 @@ export {
 | 
			
		|||
  noAuth,
 | 
			
		||||
  authMethod,
 | 
			
		||||
  loginPage,
 | 
			
		||||
  theme,
 | 
			
		||||
  enableThumbs,
 | 
			
		||||
  resizePreview,
 | 
			
		||||
  enableExec,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,10 +7,10 @@
 | 
			
		|||
    <editorBar v-else-if="currentView === 'editor'"></editorBar>
 | 
			
		||||
    <defaultBar v-else></defaultBar>
 | 
			
		||||
    <sidebar></sidebar>
 | 
			
		||||
    <main>
 | 
			
		||||
    <main :class="{ 'dark-mode': isDarkMode }">
 | 
			
		||||
      <router-view></router-view>
 | 
			
		||||
    </main>
 | 
			
		||||
    <prompts></prompts>
 | 
			
		||||
    <prompts :class="{ 'dark-mode': isDarkMode }"></prompts>
 | 
			
		||||
    <upload-files></upload-files>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -45,7 +45,9 @@ export default {
 | 
			
		|||
  computed: {
 | 
			
		||||
    ...mapGetters(["isLogged", "progress", "isListing"]),
 | 
			
		||||
    ...mapState(["req", "user", "state"]),
 | 
			
		||||
 | 
			
		||||
    isDarkMode() {
 | 
			
		||||
      return this.user.darkMode === true
 | 
			
		||||
    },
 | 
			
		||||
    isExecEnabled: () => enableExec,
 | 
			
		||||
    currentView() {
 | 
			
		||||
      if (this.req.type == undefined) {
 | 
			
		||||
| 
						 | 
				
			
			@ -82,3 +84,13 @@ export default {
 | 
			
		|||
  },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
 | 
			
		||||
/* Use the class .dark-mode to apply styles conditionally */
 | 
			
		||||
.dark-mode {
 | 
			
		||||
  background: var(--background);
 | 
			
		||||
  color: var(--textPrimary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,10 +1,7 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div class="dashboard">
 | 
			
		||||
    <div id="nav">
 | 
			
		||||
      <div v-if="disabledSettings">
 | 
			
		||||
        nothing to see here
 | 
			
		||||
      </div>
 | 
			
		||||
      <div v-else class="wrapper">
 | 
			
		||||
      <div v-if="settingsEnabled" class="wrapper">
 | 
			
		||||
        <ul>
 | 
			
		||||
          <router-link to="/settings/profile"
 | 
			
		||||
            ><li :class="{ active: $route.path === '/settings/profile' }">
 | 
			
		||||
| 
						 | 
				
			
			@ -60,11 +57,10 @@ export default {
 | 
			
		|||
    this.$store.commit("updateRequest", { name: "Settings" });
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapState(["user", "loading","req"]),
 | 
			
		||||
    disableSettings() {
 | 
			
		||||
      console.log(this.User)
 | 
			
		||||
      return this.User.disableSettings == "true"
 | 
			
		||||
    }
 | 
			
		||||
    ...mapState(["user"]),
 | 
			
		||||
    settingsEnabled() {
 | 
			
		||||
      return this.user.disableSettings == false;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -38,6 +38,7 @@ export default {
 | 
			
		|||
      dragCounter: 0,
 | 
			
		||||
      width: window.innerWidth,
 | 
			
		||||
      itemWeight: 0,
 | 
			
		||||
      viewModes: ['list', 'compact', 'normal', 'gallery'],
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
| 
						 | 
				
			
			@ -106,8 +107,9 @@ export default {
 | 
			
		|||
    viewIcon() {
 | 
			
		||||
      const icons = {
 | 
			
		||||
        list: "view_module",
 | 
			
		||||
        mosaic: "grid_view",
 | 
			
		||||
        "mosaic gallery": "view_list",
 | 
			
		||||
        compact: "view_module",
 | 
			
		||||
        normal: "grid_view",
 | 
			
		||||
        gallery: "view_list",
 | 
			
		||||
      };
 | 
			
		||||
      return icons[this.user.viewMode];
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			@ -259,21 +261,14 @@ export default {
 | 
			
		|||
    },
 | 
			
		||||
    switchView: async function () {
 | 
			
		||||
      this.$store.commit("closeHovers");
 | 
			
		||||
      const modes = {
 | 
			
		||||
        list: "mosaic",
 | 
			
		||||
        mosaic: "mosaic gallery",
 | 
			
		||||
        "mosaic gallery": "list",
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      const currentIndex = this.viewModes.indexOf(this.user.viewMode);
 | 
			
		||||
      const nextIndex = (currentIndex + 1) % this.viewModes.length;
 | 
			
		||||
      const data = {
 | 
			
		||||
        id: this.user.id,
 | 
			
		||||
        viewMode: modes[this.user.viewMode] || "list",
 | 
			
		||||
        viewMode: this.viewModes[nextIndex],
 | 
			
		||||
      };
 | 
			
		||||
      //users.update(data, ["viewMode"]).catch(this.$showError);
 | 
			
		||||
      users.update(data, ["viewMode"]).catch(this.$showError);
 | 
			
		||||
      this.$store.commit("updateUser", data);
 | 
			
		||||
 | 
			
		||||
      //this.setItemWeight();
 | 
			
		||||
      //this.fillWindow();
 | 
			
		||||
    },
 | 
			
		||||
    preventDefault(event) {
 | 
			
		||||
      // Wrapper around prevent default.
 | 
			
		||||
| 
						 | 
				
			
			@ -375,7 +370,7 @@ export default {
 | 
			
		|||
      let columns = Math.floor(
 | 
			
		||||
        document.querySelector("main").offsetWidth / this.columnWidth
 | 
			
		||||
      );
 | 
			
		||||
      let items = css(["#listing.mosaic .item", ".mosaic#listing .item"]);
 | 
			
		||||
      let items = css(["#listing .item", "#listing .item"]);
 | 
			
		||||
      if (columns === 0) columns = 1;
 | 
			
		||||
      items.style.width = `calc(${100 / columns}% - 1em)`;
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			@ -583,7 +578,6 @@ export default {
 | 
			
		|||
      }
 | 
			
		||||
      this.$store.commit("updateRequest", {});
 | 
			
		||||
      let uri = url.removeLastDir(this.$route.path) + "/";
 | 
			
		||||
      console.log(url)
 | 
			
		||||
      this.$router.push({ path: uri });
 | 
			
		||||
    },
 | 
			
		||||
    upload: function () {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -50,6 +50,7 @@ export default {
 | 
			
		|||
      dragCounter: 0,
 | 
			
		||||
      width: window.innerWidth,
 | 
			
		||||
      itemWeight: 0,
 | 
			
		||||
      viewModes: ['list', 'compact', 'normal', 'gallery'],
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
| 
						 | 
				
			
			@ -115,8 +116,9 @@ export default {
 | 
			
		|||
    viewIcon() {
 | 
			
		||||
      const icons = {
 | 
			
		||||
        list: "view_module",
 | 
			
		||||
        mosaic: "grid_view",
 | 
			
		||||
        "mosaic gallery": "view_list",
 | 
			
		||||
        compact: "view_module",
 | 
			
		||||
        normal: "grid_view",
 | 
			
		||||
        gallery: "view_list",
 | 
			
		||||
      };
 | 
			
		||||
      return icons[this.user.viewMode];
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			@ -268,21 +270,14 @@ export default {
 | 
			
		|||
    },
 | 
			
		||||
    switchView: async function () {
 | 
			
		||||
      this.$store.commit("closeHovers");
 | 
			
		||||
      const modes = {
 | 
			
		||||
        list: "mosaic",
 | 
			
		||||
        mosaic: "mosaic gallery",
 | 
			
		||||
        "mosaic gallery": "list",
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      const currentIndex = this.viewModes.indexOf(this.user.viewMode);
 | 
			
		||||
      const nextIndex = (currentIndex + 1) % this.viewModes.length;
 | 
			
		||||
      const data = {
 | 
			
		||||
        id: this.user.id,
 | 
			
		||||
        viewMode: modes[this.user.viewMode] || "list",
 | 
			
		||||
        viewMode: this.viewModes[nextIndex],
 | 
			
		||||
      };
 | 
			
		||||
      //users.update(data, ["viewMode"]).catch(this.$showError);
 | 
			
		||||
      users.update(data, ["viewMode"]).catch(this.$showError);
 | 
			
		||||
      this.$store.commit("updateUser", data);
 | 
			
		||||
 | 
			
		||||
      //this.setItemWeight();
 | 
			
		||||
      //this.fillWindow();
 | 
			
		||||
    },
 | 
			
		||||
    preventDefault(event) {
 | 
			
		||||
      // Wrapper around prevent default.
 | 
			
		||||
| 
						 | 
				
			
			@ -384,7 +379,7 @@ export default {
 | 
			
		|||
      let columns = Math.floor(
 | 
			
		||||
        document.querySelector("main").offsetWidth / this.columnWidth
 | 
			
		||||
      );
 | 
			
		||||
      let items = css(["#listing.mosaic .item", ".mosaic#listing .item"]);
 | 
			
		||||
      let items = css(["#listing .item", "#listing .item"]);
 | 
			
		||||
      if (columns === 0) columns = 1;
 | 
			
		||||
      items.style.width = `calc(${100 / columns}% - 1em)`;
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -87,7 +87,7 @@
 | 
			
		|||
          multiple
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
      <div v-else id="listing" ref="listing" :class="user.viewMode + ' file-icons'">
 | 
			
		||||
      <div v-else id="listing" ref="listing" :class="listingViewMode + ' file-icons'">
 | 
			
		||||
        <div>
 | 
			
		||||
          <div class="item header">
 | 
			
		||||
            <div></div>
 | 
			
		||||
| 
						 | 
				
			
			@ -132,8 +132,11 @@
 | 
			
		|||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <h2 v-if="req.numDirs > 0">{{ $t("files.folders") }}</h2>
 | 
			
		||||
        <div v-if="req.numDirs > 0">
 | 
			
		||||
          <div class="header-items">
 | 
			
		||||
            <h2>{{ $t("files.folders") }}</h2>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div v-if="req.numDirs > 0">
 | 
			
		||||
          <item
 | 
			
		||||
            v-for="item in dirs"
 | 
			
		||||
| 
						 | 
				
			
			@ -150,7 +153,11 @@
 | 
			
		|||
          </item>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <h2 v-if="req.numFiles > 0">{{ $t("files.files") }}</h2>
 | 
			
		||||
        <div v-if="req.numFiles > 0">
 | 
			
		||||
          <div class="header-items">
 | 
			
		||||
            <h2>{{ $t("files.files") }}</h2>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div v-if="req.numFiles > 0">
 | 
			
		||||
          <item
 | 
			
		||||
            v-for="item in files"
 | 
			
		||||
| 
						 | 
				
			
			@ -201,6 +208,15 @@
 | 
			
		|||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
 | 
			
		||||
.header-items {
 | 
			
		||||
  width: 100% !important;
 | 
			
		||||
  max-width: 100% !important;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import Vue from "vue";
 | 
			
		||||
import { mapState, mapGetters, mapMutations } from "vuex";
 | 
			
		||||
| 
						 | 
				
			
			@ -290,11 +306,15 @@ export default {
 | 
			
		|||
    viewIcon() {
 | 
			
		||||
      const icons = {
 | 
			
		||||
        list: "view_module",
 | 
			
		||||
        mosaic: "grid_view",
 | 
			
		||||
        "mosaic gallery": "view_list",
 | 
			
		||||
        compact: "view_module",
 | 
			
		||||
        normal: "grid_view",
 | 
			
		||||
        gallery: "view_list",
 | 
			
		||||
      };
 | 
			
		||||
      return icons[this.user.viewMode];
 | 
			
		||||
    },
 | 
			
		||||
    listingViewMode() {
 | 
			
		||||
      return this.user.viewMode
 | 
			
		||||
    },
 | 
			
		||||
    headerButtons() {
 | 
			
		||||
      return {
 | 
			
		||||
        select: this.selectedCount > 0,
 | 
			
		||||
| 
						 | 
				
			
			@ -527,7 +547,7 @@ export default {
 | 
			
		|||
      let columns = Math.floor(
 | 
			
		||||
        document.querySelector("main").offsetWidth / this.columnWidth
 | 
			
		||||
      );
 | 
			
		||||
      let items = css(["#listing.mosaic .item", ".mosaic#listing .item"]);
 | 
			
		||||
      let items = css(["#listing .item", "#listing .item"]);
 | 
			
		||||
      if (columns === 0) columns = 1;
 | 
			
		||||
      items.style.width = `calc(${100 / columns}% - 1em)`;
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -71,15 +71,6 @@
 | 
			
		|||
            {{ $t("settings.disableUsedDiskPercentage") }}
 | 
			
		||||
          </p>
 | 
			
		||||
 | 
			
		||||
          <p>
 | 
			
		||||
            <label for="theme">{{ $t("settings.themes.title") }}</label>
 | 
			
		||||
            <themes
 | 
			
		||||
              class="input input--block"
 | 
			
		||||
              :theme.sync="settings.frontend.theme"
 | 
			
		||||
              id="theme"
 | 
			
		||||
            ></themes>
 | 
			
		||||
          </p>
 | 
			
		||||
 | 
			
		||||
          <p>
 | 
			
		||||
            <label for="branding-name">{{ $t("settings.instanceName") }}</label>
 | 
			
		||||
            <input
 | 
			
		||||
| 
						 | 
				
			
			@ -194,13 +185,11 @@ import { settings as api } from "@/api";
 | 
			
		|||
import { enableExec } from "@/utils/constants";
 | 
			
		||||
import UserForm from "@/components/settings/UserForm";
 | 
			
		||||
import Rules from "@/components/settings/Rules";
 | 
			
		||||
import Themes from "@/components/settings/Themes";
 | 
			
		||||
import Errors from "@/views/Errors";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: "settings",
 | 
			
		||||
  components: {
 | 
			
		||||
    Themes,
 | 
			
		||||
    UserForm,
 | 
			
		||||
    Rules,
 | 
			
		||||
    Errors,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,10 @@
 | 
			
		|||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="card-content">
 | 
			
		||||
          <p>
 | 
			
		||||
            <input type="checkbox" v-model="darkMode" />
 | 
			
		||||
            Dark Mode
 | 
			
		||||
          </p>
 | 
			
		||||
          <p>
 | 
			
		||||
            <input type="checkbox" v-model="hideDotfiles" />
 | 
			
		||||
            {{ $t("settings.hideDotfiles") }}
 | 
			
		||||
| 
						 | 
				
			
			@ -19,6 +23,11 @@
 | 
			
		|||
            <input type="checkbox" v-model="dateFormat" />
 | 
			
		||||
            {{ $t("settings.setDateFormat") }}
 | 
			
		||||
          </p>
 | 
			
		||||
          <h3>Listing View Style</h3>
 | 
			
		||||
          <ViewMode
 | 
			
		||||
            class="input input--block"
 | 
			
		||||
            :viewMode.sync="viewMode"
 | 
			
		||||
          ></ViewMode>
 | 
			
		||||
          <h3>{{ $t("settings.language") }}</h3>
 | 
			
		||||
          <languages
 | 
			
		||||
            class="input input--block"
 | 
			
		||||
| 
						 | 
				
			
			@ -75,11 +84,13 @@
 | 
			
		|||
import { mapState, mapMutations } from "vuex";
 | 
			
		||||
import { users as api } from "@/api";
 | 
			
		||||
import Languages from "@/components/settings/Languages";
 | 
			
		||||
import ViewMode from "@/components/settings/ViewMode";
 | 
			
		||||
import i18n, { rtlLanguages } from "@/i18n";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: "settings",
 | 
			
		||||
  components: {
 | 
			
		||||
    ViewMode,
 | 
			
		||||
    Languages,
 | 
			
		||||
  },
 | 
			
		||||
  data: function () {
 | 
			
		||||
| 
						 | 
				
			
			@ -89,6 +100,8 @@ export default {
 | 
			
		|||
      hideDotfiles: false,
 | 
			
		||||
      singleClick: false,
 | 
			
		||||
      dateFormat: false,
 | 
			
		||||
      darkMode: false,
 | 
			
		||||
      viewMode: "list",
 | 
			
		||||
      locale: "",
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
| 
						 | 
				
			
			@ -109,8 +122,14 @@ export default {
 | 
			
		|||
    },
 | 
			
		||||
  },
 | 
			
		||||
  created() {
 | 
			
		||||
    if (typeof this.user.darkMode === 'undefined') {
 | 
			
		||||
      this.darkMode = false;
 | 
			
		||||
    } else {
 | 
			
		||||
      this.darkMode = this.user.darkMode
 | 
			
		||||
    }
 | 
			
		||||
    this.setLoading(false);
 | 
			
		||||
    this.locale = this.user.locale;
 | 
			
		||||
    this.viewMode = this.user.viewMode;
 | 
			
		||||
    this.hideDotfiles = this.user.hideDotfiles;
 | 
			
		||||
    this.singleClick = this.user.singleClick;
 | 
			
		||||
    this.dateFormat = this.user.dateFormat;
 | 
			
		||||
| 
						 | 
				
			
			@ -135,11 +154,12 @@ export default {
 | 
			
		|||
    },
 | 
			
		||||
    async updateSettings(event) {
 | 
			
		||||
      event.preventDefault();
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        const data = {
 | 
			
		||||
          id: this.user.id,
 | 
			
		||||
          locale: this.locale,
 | 
			
		||||
          darkMode: this.darkMode,
 | 
			
		||||
          viewMode: this.viewMode,
 | 
			
		||||
          hideDotfiles: this.hideDotfiles,
 | 
			
		||||
          singleClick: this.singleClick,
 | 
			
		||||
          dateFormat: this.dateFormat,
 | 
			
		||||
| 
						 | 
				
			
			@ -149,6 +169,8 @@ export default {
 | 
			
		|||
          rtlLanguages.includes(i18n.locale);
 | 
			
		||||
        await api.update(data, [
 | 
			
		||||
          "locale",
 | 
			
		||||
          "darkMode",
 | 
			
		||||
          "viewMode",
 | 
			
		||||
          "hideDotfiles",
 | 
			
		||||
          "singleClick",
 | 
			
		||||
          "dateFormat",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue