beta/v0.4.1 release (#307)
This commit is contained in:
parent
a30bfcf7d0
commit
c168599c91
|
@ -6,6 +6,8 @@ _old
|
||||||
rice-box.go
|
rice-box.go
|
||||||
.idea/
|
.idea/
|
||||||
/backend/backend
|
/backend/backend
|
||||||
|
/backend/filebrowser
|
||||||
|
/backend/filebrowser.exe
|
||||||
/backend/backend.exe
|
/backend/backend.exe
|
||||||
/frontend/dist
|
/frontend/dist
|
||||||
/frontend/pkg
|
/frontend/pkg
|
||||||
|
|
15
CHANGELOG.md
15
CHANGELOG.md
|
@ -2,6 +2,21 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
|
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
|
||||||
|
|
||||||
|
## v0.4.1-beta
|
||||||
|
|
||||||
|
**New Features**
|
||||||
|
- right-click actions are available on search. https://github.com/gtsteffaniak/filebrowser/issues/273
|
||||||
|
|
||||||
|
**Notes**
|
||||||
|
- delete prompt now lists all items that will be affected by delete
|
||||||
|
- Debug and logger output tweaks.
|
||||||
|
|
||||||
|
**Bugfixes**:
|
||||||
|
- calculating checksums errors.
|
||||||
|
- copy/move issues for some circumstances.
|
||||||
|
- The previous position wasn't returned when closing a preview window https://github.com/gtsteffaniak/filebrowser/issues/298
|
||||||
|
- fixed sources configuration mapping error (advanced `server.sources` config)
|
||||||
|
|
||||||
## v0.4.0-beta
|
## v0.4.0-beta
|
||||||
|
|
||||||
**New Features**
|
**New Features**
|
||||||
|
|
|
@ -130,7 +130,6 @@ Release Info : https://github.com/gtsteffaniak/filebrowser/releases/tag/%v
|
||||||
logger.Debug(fmt.Sprintf("Embeded frontend : %v", os.Getenv("FILEBROWSER_NO_EMBEDED") != "true"))
|
logger.Debug(fmt.Sprintf("Embeded frontend : %v", os.Getenv("FILEBROWSER_NO_EMBEDED") != "true"))
|
||||||
logger.Info(database)
|
logger.Info(database)
|
||||||
logger.Info(fmt.Sprintf("Sources : %v", sources))
|
logger.Info(fmt.Sprintf("Sources : %v", sources))
|
||||||
|
|
||||||
serverConfig := settings.Config.Server
|
serverConfig := settings.Config.Server
|
||||||
swagInfo := docs.SwaggerInfo
|
swagInfo := docs.SwaggerInfo
|
||||||
swagInfo.BasePath = serverConfig.BaseURL
|
swagInfo.BasePath = serverConfig.BaseURL
|
||||||
|
|
Binary file not shown.
|
@ -5,6 +5,13 @@ server:
|
||||||
auth:
|
auth:
|
||||||
method: password
|
method: password
|
||||||
signup: false
|
signup: false
|
||||||
|
frontend:
|
||||||
|
name: "Graham's Filebrowser"
|
||||||
|
disableDefaultLinks: true
|
||||||
|
externalLinks:
|
||||||
|
- text: "A playwright test"
|
||||||
|
url: "https://playwright.dev/"
|
||||||
|
title: "Playwright"
|
||||||
userDefaults:
|
userDefaults:
|
||||||
darkMode: true
|
darkMode: true
|
||||||
disableSettings: false
|
disableSettings: false
|
||||||
|
|
|
@ -208,11 +208,11 @@ func MoveResource(source, realsrc, realdst string, isSrcDir bool) error {
|
||||||
index := GetIndex(source)
|
index := GetIndex(source)
|
||||||
// refresh info for source and dest
|
// refresh info for source and dest
|
||||||
err = index.RefreshFileInfo(FileOptions{
|
err = index.RefreshFileInfo(FileOptions{
|
||||||
Path: filepath.Dir(realsrc),
|
Path: realsrc,
|
||||||
IsDir: isSrcDir,
|
IsDir: isSrcDir,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.ErrEmptyKey
|
return fmt.Errorf("could not refresh index for source: %v", err)
|
||||||
}
|
}
|
||||||
refreshConfig := FileOptions{Path: realdst, IsDir: true}
|
refreshConfig := FileOptions{Path: realdst, IsDir: true}
|
||||||
if !isSrcDir {
|
if !isSrcDir {
|
||||||
|
@ -220,7 +220,7 @@ func MoveResource(source, realsrc, realdst string, isSrcDir bool) error {
|
||||||
}
|
}
|
||||||
err = index.RefreshFileInfo(refreshConfig)
|
err = index.RefreshFileInfo(refreshConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.ErrEmptyKey
|
return fmt.Errorf("could not refresh index for dest: %v", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ func (idx *Index) newScanner(origin string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log and sleep before indexing
|
// Log and sleep before indexing
|
||||||
logger.Debug(fmt.Sprintf("Next scan in %v\n", sleepTime))
|
logger.Debug(fmt.Sprintf("Next scan in %v", sleepTime))
|
||||||
time.Sleep(sleepTime)
|
time.Sleep(sleepTime)
|
||||||
|
|
||||||
idx.scannerMu.Lock()
|
idx.scannerMu.Lock()
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package fileutils
|
package fileutils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
@ -11,15 +12,21 @@ import (
|
||||||
// By default, the rename system call is used. If src and dst point to different volumes,
|
// By default, the rename system call is used. If src and dst point to different volumes,
|
||||||
// the file copy is used as a fallback.
|
// the file copy is used as a fallback.
|
||||||
func MoveFile(src, dst string) error {
|
func MoveFile(src, dst string) error {
|
||||||
|
fmt.Println("moving", src, dst)
|
||||||
if os.Rename(src, dst) == nil {
|
if os.Rename(src, dst) == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
fmt.Println("copyfile instead", src, dst)
|
||||||
|
|
||||||
// fallback
|
// fallback
|
||||||
err := CopyFile(src, dst)
|
err := CopyFile(src, dst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fmt.Println("ok it errored too", err)
|
||||||
|
|
||||||
_ = os.Remove(dst)
|
_ = os.Remove(dst)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
fmt.Println("removing", src)
|
||||||
if err := os.Remove(src); err != nil {
|
if err := os.Remove(src); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,7 +108,6 @@ func withUserHelper(fn handleFunc) handleFunc {
|
||||||
}
|
}
|
||||||
tokenString, err := extractToken(r)
|
tokenString, err := extractToken(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Debug(fmt.Sprintf("error extracting from request %v", err))
|
|
||||||
return http.StatusUnauthorized, err
|
return http.StatusUnauthorized, err
|
||||||
}
|
}
|
||||||
data.token = tokenString
|
data.token = tokenString
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/gtsteffaniak/filebrowser/backend/cache"
|
"github.com/gtsteffaniak/filebrowser/backend/cache"
|
||||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||||
|
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// resourceGetHandler retrieves information about a resource.
|
// resourceGetHandler retrieves information about a resource.
|
||||||
|
@ -60,7 +61,9 @@ func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||||
return renderJSON(w, r, fileInfo)
|
return renderJSON(w, r, fileInfo)
|
||||||
}
|
}
|
||||||
if algo := r.URL.Query().Get("checksum"); algo != "" {
|
if algo := r.URL.Query().Get("checksum"); algo != "" {
|
||||||
checksums, err := files.GetChecksum(fileInfo.Path, algo)
|
idx := files.GetIndex(source)
|
||||||
|
realPath, _, _ := idx.GetRealPath(d.user.Scope, path)
|
||||||
|
checksums, err := files.GetChecksum(realPath, algo)
|
||||||
if err == errors.ErrInvalidOption {
|
if err == errors.ErrInvalidOption {
|
||||||
return http.StatusBadRequest, nil
|
return http.StatusBadRequest, nil
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
@ -115,10 +118,7 @@ func resourceDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestCon
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete thumbnails
|
// delete thumbnails
|
||||||
err = delThumbs(r.Context(), fileCache, fileInfo)
|
delThumbs(r.Context(), fileCache, fileInfo)
|
||||||
if err != nil {
|
|
||||||
return errToStatus(err), err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = files.DeleteFiles(source, fileInfo.RealPath, filepath.Dir(fileInfo.RealPath))
|
err = files.DeleteFiles(source, fileInfo.RealPath, filepath.Dir(fileInfo.RealPath))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -184,10 +184,7 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||||
return http.StatusForbidden, nil
|
return http.StatusForbidden, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err = delThumbs(r.Context(), fileCache, fileInfo)
|
delThumbs(r.Context(), fileCache, fileInfo)
|
||||||
if err != nil {
|
|
||||||
return errToStatus(err), err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
err = files.WriteFile(fileOpts, r.Body)
|
err = files.WriteFile(fileOpts, r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -312,7 +309,9 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||||
err = d.RunHook(func() error {
|
err = d.RunHook(func() error {
|
||||||
return patchAction(r.Context(), action, realSrc, realDest, d, fileCache, isSrcDir, source)
|
return patchAction(r.Context(), action, realSrc, realDest, d, fileCache, isSrcDir, source)
|
||||||
}, action, realSrc, realDest, d.user)
|
}, action, realSrc, realDest, d.user)
|
||||||
|
if err != nil {
|
||||||
|
logger.Debug(fmt.Sprintf("Could not run patch action. src=%v dst=%v err=%v", realSrc, realDest, err))
|
||||||
|
}
|
||||||
return errToStatus(err), err
|
return errToStatus(err), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -332,11 +331,11 @@ func addVersionSuffix(source string) string {
|
||||||
return source
|
return source
|
||||||
}
|
}
|
||||||
|
|
||||||
func delThumbs(ctx context.Context, fileCache FileCache, file files.ExtendedFileInfo) error {
|
func delThumbs(ctx context.Context, fileCache FileCache, file files.ExtendedFileInfo) {
|
||||||
if err := fileCache.Delete(ctx, previewCacheKey(file.RealPath, "small", file.FileInfo.ModTime)); err != nil {
|
err := fileCache.Delete(ctx, previewCacheKey(file.RealPath, "small", file.FileInfo.ModTime))
|
||||||
return err
|
if err != nil {
|
||||||
|
logger.Debug(fmt.Sprintf("Could not delete small thumbnail: %v", err))
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func patchAction(ctx context.Context, action, src, dst string, d *requestContext, fileCache FileCache, isSrcDir bool, index string) error {
|
func patchAction(ctx context.Context, action, src, dst string, d *requestContext, fileCache FileCache, isSrcDir bool, index string) error {
|
||||||
|
@ -348,7 +347,6 @@ func patchAction(ctx context.Context, action, src, dst string, d *requestContext
|
||||||
err := files.CopyResource(index, src, dst, isSrcDir)
|
err := files.CopyResource(index, src, dst, isSrcDir)
|
||||||
return err
|
return err
|
||||||
case "rename", "move":
|
case "rename", "move":
|
||||||
|
|
||||||
if !d.user.Perm.Rename {
|
if !d.user.Perm.Rename {
|
||||||
return errors.ErrPermissionDenied
|
return errors.ErrPermissionDenied
|
||||||
}
|
}
|
||||||
|
@ -366,10 +364,7 @@ func patchAction(ctx context.Context, action, src, dst string, d *requestContext
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete thumbnails
|
// delete thumbnails
|
||||||
err = delThumbs(ctx, fileCache, fileInfo)
|
delThumbs(ctx, fileCache, fileInfo)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return files.MoveResource(index, src, dst, isSrcDir)
|
return files.MoveResource(index, src, dst, isSrcDir)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported action %s: %w", action, errors.ErrInvalidRequestParams)
|
return fmt.Errorf("unsupported action %s: %w", action, errors.ErrInvalidRequestParams)
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
../../frontend/tests/playwright-files/myfolder/testdata/
|
|
@ -11,13 +11,14 @@ import (
|
||||||
|
|
||||||
// Logger wraps the standard log.Logger with log level functionality
|
// Logger wraps the standard log.Logger with log level functionality
|
||||||
type Logger struct {
|
type Logger struct {
|
||||||
logger *log.Logger
|
logger *log.Logger
|
||||||
levels []LogLevel
|
levels []LogLevel
|
||||||
apiLevels []LogLevel
|
apiLevels []LogLevel
|
||||||
stdout bool
|
stdout bool
|
||||||
disabled bool
|
disabled bool
|
||||||
disabledAPI bool
|
debugEnabled bool
|
||||||
colors bool
|
disabledAPI bool
|
||||||
|
colors bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var stdOutLoggerExists bool
|
var stdOutLoggerExists bool
|
||||||
|
@ -34,7 +35,7 @@ func NewLogger(filepath string, levels, apiLevels []LogLevel, noColors bool) (*L
|
||||||
}
|
}
|
||||||
fileWriter = file
|
fileWriter = file
|
||||||
}
|
}
|
||||||
flags := log.Ldate | log.Ltime
|
var flags int
|
||||||
if slices.Contains(levels, DEBUG) {
|
if slices.Contains(levels, DEBUG) {
|
||||||
flags |= log.Lshortfile
|
flags |= log.Lshortfile
|
||||||
}
|
}
|
||||||
|
@ -46,13 +47,14 @@ func NewLogger(filepath string, levels, apiLevels []LogLevel, noColors bool) (*L
|
||||||
stdOutLoggerExists = true
|
stdOutLoggerExists = true
|
||||||
}
|
}
|
||||||
return &Logger{
|
return &Logger{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
levels: levels,
|
levels: levels,
|
||||||
apiLevels: apiLevels,
|
apiLevels: apiLevels,
|
||||||
disabled: slices.Contains(levels, DISABLED),
|
disabled: slices.Contains(levels, DISABLED),
|
||||||
disabledAPI: slices.Contains(apiLevels, DISABLED),
|
debugEnabled: slices.Contains(levels, DEBUG),
|
||||||
colors: !noColors,
|
disabledAPI: slices.Contains(apiLevels, DISABLED),
|
||||||
stdout: stdout,
|
colors: !noColors,
|
||||||
|
stdout: stdout,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,6 +69,9 @@ func SetupLogger(output, levels, apiLevels string, noColors bool) error {
|
||||||
if upperLevel == "WARNING" || upperLevel == "WARN" {
|
if upperLevel == "WARNING" || upperLevel == "WARN" {
|
||||||
upperLevel = "WARN "
|
upperLevel = "WARN "
|
||||||
}
|
}
|
||||||
|
if upperLevel == "INFO" {
|
||||||
|
upperLevel = "INFO "
|
||||||
|
}
|
||||||
// Convert level strings to LogLevel
|
// Convert level strings to LogLevel
|
||||||
level, ok := stringToLevel[upperLevel]
|
level, ok := stringToLevel[upperLevel]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -87,6 +92,9 @@ func SetupLogger(output, levels, apiLevels string, noColors bool) error {
|
||||||
if upperLevel == "WARNING" || upperLevel == "WARN" {
|
if upperLevel == "WARNING" || upperLevel == "WARN" {
|
||||||
upperLevel = "WARN "
|
upperLevel = "WARN "
|
||||||
}
|
}
|
||||||
|
if upperLevel == "INFO" {
|
||||||
|
upperLevel = "INFO "
|
||||||
|
}
|
||||||
// Convert level strings to LogLevel
|
// Convert level strings to LogLevel
|
||||||
level, ok := stringToLevel[strings.ToUpper(upperLevel)]
|
level, ok := stringToLevel[strings.ToUpper(upperLevel)]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -119,6 +127,7 @@ func SetupLogger(output, levels, apiLevels string, noColors bool) error {
|
||||||
loggers = append(loggers, logger)
|
loggers = append(loggers, logger)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SplitByMultiple(str string) []string {
|
func SplitByMultiple(str string) []string {
|
||||||
delimiters := []rune{'|', ',', ' '}
|
delimiters := []rune{'|', ',', ' '}
|
||||||
return strings.FieldsFunc(str, func(r rune) bool {
|
return strings.FieldsFunc(str, func(r rune) bool {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"slices"
|
"slices"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LogLevel int
|
type LogLevel int
|
||||||
|
@ -38,7 +39,7 @@ type levelConsts struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
var levels = levelConsts{
|
var levels = levelConsts{
|
||||||
INFO: "INFO",
|
INFO: "INFO ", // with consistent space padding
|
||||||
FATAL: "FATAL",
|
FATAL: "FATAL",
|
||||||
ERROR: "ERROR",
|
ERROR: "ERROR",
|
||||||
WARNING: "WARN ", // with consistent space padding
|
WARNING: "WARN ", // with consistent space padding
|
||||||
|
@ -50,7 +51,7 @@ var levels = levelConsts{
|
||||||
// stringToLevel maps string representation to LogLevel
|
// stringToLevel maps string representation to LogLevel
|
||||||
var stringToLevel = map[string]LogLevel{
|
var stringToLevel = map[string]LogLevel{
|
||||||
"DEBUG": DEBUG,
|
"DEBUG": DEBUG,
|
||||||
"INFO": INFO,
|
"INFO ": INFO, // with consistent space padding
|
||||||
"ERROR": ERROR,
|
"ERROR": ERROR,
|
||||||
"DISABLED": DISABLED,
|
"DISABLED": DISABLED,
|
||||||
"WARN ": WARNING, // with consistent space padding
|
"WARN ": WARNING, // with consistent space padding
|
||||||
|
@ -71,15 +72,21 @@ func Log(level string, msg string, prefix, api bool, color string) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if logger.stdout && LEVEL == FATAL {
|
if logger.stdout && level == "FATAL" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
writeOut := msg
|
writeOut := msg
|
||||||
if prefix {
|
formattedTime := time.Now().Format("2006/01/02 15:04:05")
|
||||||
writeOut = fmt.Sprintf("[%s] ", level) + writeOut
|
if logger.colors && color != "" {
|
||||||
|
formattedTime = formattedTime + color
|
||||||
|
}
|
||||||
|
if prefix || logger.debugEnabled {
|
||||||
|
logger.logger.SetPrefix(fmt.Sprintf("%s [%s] ", formattedTime, level))
|
||||||
|
} else {
|
||||||
|
logger.logger.SetPrefix(formattedTime + " ")
|
||||||
}
|
}
|
||||||
if logger.colors && color != "" {
|
if logger.colors && color != "" {
|
||||||
writeOut = color + writeOut + "\033[0m"
|
writeOut = writeOut + "\033[0m"
|
||||||
}
|
}
|
||||||
err := logger.logger.Output(3, writeOut) // 3 skips this function for correct file:line
|
err := logger.logger.Output(3, writeOut) // 3 skips this function for correct file:line
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -102,6 +109,8 @@ func Api(msg string, statusCode int) {
|
||||||
func Debug(msg string) {
|
func Debug(msg string) {
|
||||||
if len(loggers) > 0 {
|
if len(loggers) > 0 {
|
||||||
Log(levels.DEBUG, msg, true, false, GRAY)
|
Log(levels.DEBUG, msg, true, false, GRAY)
|
||||||
|
} else {
|
||||||
|
log.Println("[DEBUG]: " + msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,9 @@ var Config Settings
|
||||||
|
|
||||||
func Initialize(configFile string) {
|
func Initialize(configFile string) {
|
||||||
yamlData, err := loadConfigFile(configFile)
|
yamlData, err := loadConfigFile(configFile)
|
||||||
|
if err != nil && configFile != "config.yaml" {
|
||||||
|
logger.Fatal("Could not load specified config file: " + err.Error())
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warning(fmt.Sprintf("Could not load config file '%v', using default settings: %v", configFile, err))
|
logger.Warning(fmt.Sprintf("Could not load config file '%v', using default settings: %v", configFile, err))
|
||||||
}
|
}
|
||||||
|
@ -35,21 +38,22 @@ func Initialize(configFile string) {
|
||||||
logger.Fatal(fmt.Sprintf("Error getting source path: %v", err2))
|
logger.Fatal(fmt.Sprintf("Error getting source path: %v", err2))
|
||||||
}
|
}
|
||||||
source.Path = realPath
|
source.Path = realPath
|
||||||
source.Name = "default" // Modify the local copy of the map value
|
source.Name = "default"
|
||||||
Config.Server.Sources["default"] = source // Assign the modified value back to the map
|
Config.Server.Sources = []Source{source} // temporary set only one source
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
realPath, err2 := filepath.Abs(Config.Server.Root)
|
realPath, err2 := filepath.Abs(Config.Server.Root)
|
||||||
if err2 != nil {
|
if err2 != nil {
|
||||||
logger.Fatal(fmt.Sprintf("Error getting source path: %v", err2))
|
logger.Fatal(fmt.Sprintf("Error getting source path: %v", err2))
|
||||||
}
|
}
|
||||||
Config.Server.Sources = map[string]Source{
|
Config.Server.Sources = []Source{
|
||||||
"default": {
|
{
|
||||||
Name: "default",
|
Name: "default",
|
||||||
Path: realPath,
|
Path: realPath,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
baseurl := strings.Trim(Config.Server.BaseURL, "/")
|
baseurl := strings.Trim(Config.Server.BaseURL, "/")
|
||||||
if baseurl == "" {
|
if baseurl == "" {
|
||||||
Config.Server.BaseURL = "/"
|
Config.Server.BaseURL = "/"
|
||||||
|
@ -57,10 +61,6 @@ func Initialize(configFile string) {
|
||||||
Config.Server.BaseURL = "/" + baseurl + "/"
|
Config.Server.BaseURL = "/" + baseurl + "/"
|
||||||
}
|
}
|
||||||
if !Config.Frontend.DisableDefaultLinks {
|
if !Config.Frontend.DisableDefaultLinks {
|
||||||
Config.Frontend.ExternalLinks = append(Config.Frontend.ExternalLinks, ExternalLink{
|
|
||||||
Text: "FileBrowser Quantum",
|
|
||||||
Url: "https://github.com/gtsteffaniak/filebrowser",
|
|
||||||
})
|
|
||||||
Config.Frontend.ExternalLinks = append(Config.Frontend.ExternalLinks, ExternalLink{
|
Config.Frontend.ExternalLinks = append(Config.Frontend.ExternalLinks, ExternalLink{
|
||||||
Text: fmt.Sprintf("(%v)", version.Version),
|
Text: fmt.Sprintf("(%v)", version.Version),
|
||||||
Title: version.CommitSHA,
|
Title: version.CommitSHA,
|
||||||
|
@ -89,7 +89,6 @@ func Initialize(configFile string) {
|
||||||
log.Println("[ERROR] Failed to set up logger:", err)
|
log.Println("[ERROR] Failed to set up logger:", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfigFile(configFile string) ([]byte, error) {
|
func loadConfigFile(configFile string) ([]byte, error) {
|
||||||
|
|
|
@ -36,26 +36,26 @@ type Recaptcha struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
NumImageProcessors int `json:"numImageProcessors"`
|
NumImageProcessors int `json:"numImageProcessors"`
|
||||||
Socket string `json:"socket"`
|
Socket string `json:"socket"`
|
||||||
TLSKey string `json:"tlsKey"`
|
TLSKey string `json:"tlsKey"`
|
||||||
TLSCert string `json:"tlsCert"`
|
TLSCert string `json:"tlsCert"`
|
||||||
EnableThumbnails bool `json:"enableThumbnails"`
|
EnableThumbnails bool `json:"enableThumbnails"`
|
||||||
ResizePreview bool `json:"resizePreview"`
|
ResizePreview bool `json:"resizePreview"`
|
||||||
EnableExec bool `json:"enableExec"`
|
EnableExec bool `json:"enableExec"`
|
||||||
TypeDetectionByHeader bool `json:"typeDetectionByHeader"`
|
TypeDetectionByHeader bool `json:"typeDetectionByHeader"`
|
||||||
AuthHook string `json:"authHook"`
|
AuthHook string `json:"authHook"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
BaseURL string `json:"baseURL"`
|
BaseURL string `json:"baseURL"`
|
||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
Logging []LogConfig `json:"logging"`
|
Logging []LogConfig `json:"logging"`
|
||||||
Database string `json:"database"`
|
Database string `json:"database"`
|
||||||
Root string `json:"root"`
|
Root string `json:"root"`
|
||||||
UserHomeBasePath string `json:"userHomeBasePath"`
|
UserHomeBasePath string `json:"userHomeBasePath"`
|
||||||
CreateUserDir bool `json:"createUserDir"`
|
CreateUserDir bool `json:"createUserDir"`
|
||||||
Sources map[string]Source `json:"sources"`
|
Sources []Source `json:"sources"`
|
||||||
ExternalUrl string `json:"externalUrl"`
|
ExternalUrl string `json:"externalUrl"`
|
||||||
InternalUrl string `json:"internalUrl"` // used by integrations
|
InternalUrl string `json:"internalUrl"` // used by integrations
|
||||||
}
|
}
|
||||||
|
|
||||||
type Integrations struct {
|
type Integrations struct {
|
||||||
|
@ -78,8 +78,8 @@ type LogConfig struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Source struct {
|
type Source struct {
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Name string
|
Name string `json:"name"`
|
||||||
Config IndexConfig `json:"config"`
|
Config IndexConfig `json:"config"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ async function globalSetup() {
|
||||||
await page.waitForURL("**/files/", { timeout: 100 });
|
await page.waitForURL("**/files/", { timeout: 100 });
|
||||||
let cookies = await context.cookies();
|
let cookies = await context.cookies();
|
||||||
expect(cookies.find((c) => c.name == "auth")?.value).toBeDefined();
|
expect(cookies.find((c) => c.name == "auth")?.value).toBeDefined();
|
||||||
await expect(page).toHaveTitle('playwright-files - FileBrowser Quantum - Files');
|
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
|
||||||
await page.context().storageState({ path: "./loginAuth.json" });
|
await page.context().storageState({ path: "./loginAuth.json" });
|
||||||
await browser.close();
|
await browser.close();
|
||||||
}
|
}
|
||||||
|
|
|
@ -158,10 +158,16 @@ export async function moveCopy(items, action = "copy", overwrite = false, rename
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function checksum(url, algo) {
|
export async function checksum(path, algo) {
|
||||||
try {
|
try {
|
||||||
const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
|
const params = {
|
||||||
return (await data.json()).checksums[algo];
|
path: encodeURIComponent(removePrefix(path, "files")),
|
||||||
|
checksum: algo,
|
||||||
|
};
|
||||||
|
const apiPath = getApiPath("api/resources", params);
|
||||||
|
const res = await fetchURL(apiPath);
|
||||||
|
const data = await res.json();
|
||||||
|
return data.checksums[algo];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notify.showError(err.message || "Error fetching checksum");
|
notify.showError(err.message || "Error fetching checksum");
|
||||||
throw err;
|
throw err;
|
||||||
|
|
|
@ -13,21 +13,20 @@
|
||||||
<div v-if="selectedCount > 0" class="button selected-count-header">
|
<div v-if="selectedCount > 0" class="button selected-count-header">
|
||||||
<span>{{ selectedCount }} selected</span>
|
<span>{{ selectedCount }} selected</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<action
|
<action
|
||||||
v-if="!headerButtons.select"
|
v-if="!isSearchActive"
|
||||||
icon="create_new_folder"
|
icon="create_new_folder"
|
||||||
:label="$t('sidebar.newFolder')"
|
:label="$t('sidebar.newFolder')"
|
||||||
@action="showHover('newDir')"
|
@action="showHover('newDir')"
|
||||||
/>
|
/>
|
||||||
<action
|
<action
|
||||||
v-if="!headerButtons.select"
|
v-if="!headerButtons.select && !isSearchActive"
|
||||||
icon="note_add"
|
icon="note_add"
|
||||||
:label="$t('sidebar.newFile')"
|
:label="$t('sidebar.newFile')"
|
||||||
@action="showHover('newFile')"
|
@action="showHover('newFile')"
|
||||||
/>
|
/>
|
||||||
<action
|
<action
|
||||||
v-if="!headerButtons.select"
|
v-if="!headerButtons.select && !isSearchActive"
|
||||||
icon="file_upload"
|
icon="file_upload"
|
||||||
:label="$t('buttons.upload')"
|
:label="$t('buttons.upload')"
|
||||||
@action="uploadFunc"
|
@action="uploadFunc"
|
||||||
|
@ -40,7 +39,7 @@
|
||||||
show="info"
|
show="info"
|
||||||
/>
|
/>
|
||||||
<action
|
<action
|
||||||
v-if="!isMultiple"
|
v-if="!isMultiple && !isSearchActive"
|
||||||
icon="check_circle"
|
icon="check_circle"
|
||||||
:label="$t('buttons.selectMultiple')"
|
:label="$t('buttons.selectMultiple')"
|
||||||
@action="toggleMultipleSelection"
|
@action="toggleMultipleSelection"
|
||||||
|
@ -59,7 +58,7 @@
|
||||||
show="share"
|
show="share"
|
||||||
/>
|
/>
|
||||||
<action
|
<action
|
||||||
v-if="headerButtons.rename"
|
v-if="headerButtons.rename && !isSearchActive"
|
||||||
icon="mode_edit"
|
icon="mode_edit"
|
||||||
:label="$t('buttons.rename')"
|
:label="$t('buttons.rename')"
|
||||||
show="rename"
|
show="rename"
|
||||||
|
@ -102,6 +101,9 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isSearchActive() {
|
||||||
|
return state.isSearchActive;
|
||||||
|
},
|
||||||
isMultiple() {
|
isMultiple() {
|
||||||
return state.multiple;
|
return state.multiple;
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="search" :class="{ active, ongoing, 'dark-mode': isDarkMode }">
|
<div
|
||||||
|
id="search"
|
||||||
|
:class="{ active, ongoing, 'dark-mode': isDarkMode }"
|
||||||
|
@click="clearContext"
|
||||||
|
>
|
||||||
<!-- Search input section -->
|
<!-- Search input section -->
|
||||||
<div id="input" @click="open">
|
<div id="input" @click="open">
|
||||||
<!-- Close button visible when search is active -->
|
<!-- Close button visible when search is active -->
|
||||||
|
@ -115,11 +119,18 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- List of search results -->
|
<!-- List of search results -->
|
||||||
<ul v-show="results.length > 0">
|
<ul v-show="results.length > 0">
|
||||||
<li v-for="(s, k) in results" :key="k" class="search-entry">
|
<li
|
||||||
<a :href="getRelative(s.path)">
|
v-for="(s, k) in results"
|
||||||
|
:key="k"
|
||||||
|
class="search-entry"
|
||||||
|
:class="{ active: activeStates[k] }"
|
||||||
|
:aria-label="baseName(s.path)"
|
||||||
|
>
|
||||||
|
<a :href="getRelative(s.path)" @contextmenu="addSelected(event, s)">
|
||||||
<Icon :mimetype="s.type" />
|
<Icon :mimetype="s.type" />
|
||||||
<span class="text-container">
|
<span class="text-container">
|
||||||
{{ basePath(s.path, s.type === "directory")}}<b>{{ baseName(s.path) }}</b>
|
{{ basePath(s.path, s.type === "directory")
|
||||||
|
}}<b>{{ baseName(s.path) }}</b>
|
||||||
</span>
|
</span>
|
||||||
<div class="filesize">{{ humanSize(s.size) }}</div>
|
<div class="filesize">{{ humanSize(s.size) }}</div>
|
||||||
</a>
|
</a>
|
||||||
|
@ -136,6 +147,7 @@ import { search } from "@/api";
|
||||||
import { getters, mutations, state } from "@/store";
|
import { getters, mutations, state } from "@/store";
|
||||||
import { getHumanReadableFilesize } from "@/utils/filesizes";
|
import { getHumanReadableFilesize } from "@/utils/filesizes";
|
||||||
import { removeTrailingSlash, removeLeadingSlash } from "@/utils/url";
|
import { removeTrailingSlash, removeLeadingSlash } from "@/utils/url";
|
||||||
|
import { removePrefix } from "@/utils/url.js";
|
||||||
|
|
||||||
import Icon from "@/components/Icon.vue";
|
import Icon from "@/components/Icon.vue";
|
||||||
|
|
||||||
|
@ -226,7 +238,7 @@ export default {
|
||||||
return this.isTypeSelectDisabled;
|
return this.isTypeSelectDisabled;
|
||||||
},
|
},
|
||||||
active() {
|
active() {
|
||||||
return getters.currentPromptName() == "search";
|
return state.isSearchActive;
|
||||||
},
|
},
|
||||||
isDarkMode() {
|
isDarkMode() {
|
||||||
return getters.isDarkMode();
|
return getters.isDarkMode();
|
||||||
|
@ -255,17 +267,22 @@ export default {
|
||||||
return this.showHelp;
|
return this.showHelp;
|
||||||
},
|
},
|
||||||
getContext() {
|
getContext() {
|
||||||
let path = state.route.path;
|
return this.getRelativeContext();
|
||||||
path = path.slice(1);
|
},
|
||||||
path = "./" + path.substring(path.indexOf("/") + 1);
|
activeStates() {
|
||||||
path = path.replace(/\/+$/, "") + "/";
|
// Create a Set of combined `name` and `type` keys for efficient lookup
|
||||||
if (path == "./files/") {
|
const selectedSet = new Set(
|
||||||
path = "./";
|
state.selected.map((item) => `${item.name}:${item.type}`)
|
||||||
}
|
);
|
||||||
return path;
|
const result = this.results.map((s) => selectedSet.has(`${s.name}:${s.type}`));
|
||||||
|
// Build a map of active states for the `results` array
|
||||||
|
return result;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
getRelativeContext() {
|
||||||
|
return removePrefix(decodeURIComponent(state.route.path), "files");
|
||||||
|
},
|
||||||
getRelative(path) {
|
getRelative(path) {
|
||||||
return removeTrailingSlash(window.location.href) + "/" + removeLeadingSlash(path);
|
return removeTrailingSlash(window.location.href) + "/" + removeLeadingSlash(path);
|
||||||
},
|
},
|
||||||
|
@ -304,15 +321,18 @@ export default {
|
||||||
return parts.pop();
|
return parts.pop();
|
||||||
},
|
},
|
||||||
open() {
|
open() {
|
||||||
if (!this.active) {
|
if (!state.isSearchActive) {
|
||||||
|
mutations.closeHovers();
|
||||||
|
mutations.closeSidebar();
|
||||||
|
mutations.resetSelected();
|
||||||
this.resetSearchFilters();
|
this.resetSearchFilters();
|
||||||
mutations.showHover("search");
|
mutations.setSearch(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
close(event) {
|
close(event) {
|
||||||
this.value = "";
|
this.value = "";
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
mutations.closeHovers();
|
mutations.setSearch(false);
|
||||||
},
|
},
|
||||||
keyup(event) {
|
keyup(event) {
|
||||||
if (event.keyCode === 27) {
|
if (event.keyCode === 27) {
|
||||||
|
@ -381,6 +401,24 @@ export default {
|
||||||
toggleHelp() {
|
toggleHelp() {
|
||||||
this.showHelp = !this.showHelp;
|
this.showHelp = !this.showHelp;
|
||||||
},
|
},
|
||||||
|
clearContext() {
|
||||||
|
mutations.closeHovers();
|
||||||
|
},
|
||||||
|
addSelected(event, s) {
|
||||||
|
const pathParts = s.path.split("/");
|
||||||
|
const path = removePrefix(decodeURIComponent(state.route.path), "files") + s.path;
|
||||||
|
const modifiedItem = {
|
||||||
|
name: pathParts.pop(),
|
||||||
|
path: path,
|
||||||
|
size: s.size,
|
||||||
|
type: s.type,
|
||||||
|
source: "",
|
||||||
|
url: path,
|
||||||
|
fullPath: path,
|
||||||
|
};
|
||||||
|
mutations.resetSelected();
|
||||||
|
mutations.addSelected(modifiedItem);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -511,6 +549,10 @@ export default {
|
||||||
border-radius: 0.25em;
|
border-radius: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-entry.active {
|
||||||
|
background-color: var(--surfacePrimary);
|
||||||
|
}
|
||||||
|
|
||||||
.search-entry:hover {
|
.search-entry:hover {
|
||||||
background-color: var(--surfacePrimary);
|
background-color: var(--surfacePrimary);
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,6 @@ import FileList from "./FileList.vue";
|
||||||
import { filesApi } from "@/api";
|
import { filesApi } from "@/api";
|
||||||
import buttons from "@/utils/buttons";
|
import buttons from "@/utils/buttons";
|
||||||
import * as upload from "@/utils/upload";
|
import * as upload from "@/utils/upload";
|
||||||
import { removePrefix } from "@/utils/url";
|
|
||||||
import { notify } from "@/notify";
|
import { notify } from "@/notify";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -74,12 +73,21 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
for (let item of state.selected) {
|
if (state.isSearchActive) {
|
||||||
this.items.push({
|
this.items = [
|
||||||
from: state.req.items[item].url,
|
{
|
||||||
// add to: dest
|
from: "/files" + state.selected[0].url,
|
||||||
name: state.req.items[item].name,
|
name: state.selected[0].name,
|
||||||
});
|
},
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
for (let item of state.selected) {
|
||||||
|
this.items.push({
|
||||||
|
from: state.req.items[item].url,
|
||||||
|
// add to: dest
|
||||||
|
name: state.req.items[item].name,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -88,9 +96,8 @@ export default {
|
||||||
try {
|
try {
|
||||||
// Define the action function
|
// Define the action function
|
||||||
let action = async (overwrite, rename) => {
|
let action = async (overwrite, rename) => {
|
||||||
const loc = removePrefix(this.dest, "files");
|
|
||||||
for (let item of this.items) {
|
for (let item of this.items) {
|
||||||
item.to = loc + "/" + item.name;
|
item.to = this.dest + item.name;
|
||||||
}
|
}
|
||||||
buttons.loading("copy");
|
buttons.loading("copy");
|
||||||
await filesApi.moveCopy(this.items, "copy", overwrite, rename);
|
await filesApi.moveCopy(this.items, "copy", overwrite, rename);
|
||||||
|
@ -123,12 +130,13 @@ export default {
|
||||||
await action(overwrite, rename);
|
await action(overwrite, rename);
|
||||||
}
|
}
|
||||||
mutations.closeHovers();
|
mutations.closeHovers();
|
||||||
|
mutations.setSearch(false);
|
||||||
notify.showSuccess("Successfully copied file/folder, redirecting...");
|
notify.showSuccess("Successfully copied file/folder, redirecting...");
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.$router.push(this.dest);
|
this.$router.push(this.dest);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notify.error(error);
|
notify.showError(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,6 +7,9 @@
|
||||||
<p v-else>
|
<p v-else>
|
||||||
{{ $t("prompts.deleteMessageMultiple", { count: selectedCount }) }}
|
{{ $t("prompts.deleteMessageMultiple", { count: selectedCount }) }}
|
||||||
</p>
|
</p>
|
||||||
|
<div style="display: grid" class="searchContext">
|
||||||
|
<span v-for="(item, index) in nav" :key="index"> {{ item }} </span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-action">
|
<div class="card-action">
|
||||||
<button
|
<button
|
||||||
|
@ -34,6 +37,7 @@ import { filesApi } from "@/api";
|
||||||
import buttons from "@/utils/buttons";
|
import buttons from "@/utils/buttons";
|
||||||
import { state, getters, mutations } from "@/store";
|
import { state, getters, mutations } from "@/store";
|
||||||
import { notify } from "@/notify";
|
import { notify } from "@/notify";
|
||||||
|
import { removePrefix } from "@/utils/url";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "delete",
|
name: "delete",
|
||||||
|
@ -47,6 +51,16 @@ export default {
|
||||||
currentPrompt() {
|
currentPrompt() {
|
||||||
return getters.currentPrompt();
|
return getters.currentPrompt();
|
||||||
},
|
},
|
||||||
|
nav() {
|
||||||
|
if (state.isSearchActive) {
|
||||||
|
return [state.selected[0].path];
|
||||||
|
}
|
||||||
|
let paths = [];
|
||||||
|
for (let index of state.selected) {
|
||||||
|
paths.push(removePrefix(state.req.items[index].url, "files"));
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
closeHovers() {
|
closeHovers() {
|
||||||
|
@ -56,17 +70,24 @@ export default {
|
||||||
buttons.loading("delete");
|
buttons.loading("delete");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (state.isSearchActive) {
|
||||||
|
await filesApi.remove(state.selected[0].url);
|
||||||
|
buttons.success("delete");
|
||||||
|
notify.showSuccess("Deleted item successfully");
|
||||||
|
mutations.closeHovers();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!this.isListing) {
|
if (!this.isListing) {
|
||||||
await filesApi.remove(state.route.path);
|
await filesApi.remove(state.route.path);
|
||||||
buttons.success("delete");
|
buttons.success("delete");
|
||||||
notify.showSuccess("Deleted item successfully");
|
notify.showSuccess("Deleted item successfully");
|
||||||
|
|
||||||
this.currentPrompt?.confirm();
|
this.currentPrompt?.confirm();
|
||||||
this.closeHovers();
|
mutations.closeHovers();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.closeHovers();
|
mutations.closeHovers();
|
||||||
|
|
||||||
if (getters.selectedCount() === 0) {
|
if (getters.selectedCount() === 0) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -99,6 +99,9 @@ export default {
|
||||||
return getters.isListing();
|
return getters.isListing();
|
||||||
},
|
},
|
||||||
humanSize() {
|
humanSize() {
|
||||||
|
if (state.isSearchActive) {
|
||||||
|
return getHumanReadableFilesize(state.selected[0].size);
|
||||||
|
}
|
||||||
if (getters.selectedCount() === 0 || !this.isListing) {
|
if (getters.selectedCount() === 0 || !this.isListing) {
|
||||||
return getHumanReadableFilesize(state.req.size);
|
return getHumanReadableFilesize(state.req.size);
|
||||||
}
|
}
|
||||||
|
@ -112,6 +115,9 @@ export default {
|
||||||
return getHumanReadableFilesize(sum);
|
return getHumanReadableFilesize(sum);
|
||||||
},
|
},
|
||||||
humanTime() {
|
humanTime() {
|
||||||
|
if (state.isSearchActive) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
if (getters.selectedCount() === 0) {
|
if (getters.selectedCount() === 0) {
|
||||||
return formatTimestamp(state.req.modified, state.user.locale);
|
return formatTimestamp(state.req.modified, state.user.locale);
|
||||||
}
|
}
|
||||||
|
@ -121,19 +127,31 @@ export default {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
modTime() {
|
modTime() {
|
||||||
|
if (state.isSearchActive) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
return new Date(Date.parse(state.req.modified)).toLocaleString();
|
return new Date(Date.parse(state.req.modified)).toLocaleString();
|
||||||
},
|
},
|
||||||
name() {
|
name() {
|
||||||
|
if (state.isSearchActive) {
|
||||||
|
return state.selected[0].name;
|
||||||
|
}
|
||||||
return getters.selectedCount() === 0
|
return getters.selectedCount() === 0
|
||||||
? state.req.name
|
? state.req.name
|
||||||
: state.req.items[this.selected[0]].name;
|
: state.req.items[this.selected[0]].name;
|
||||||
},
|
},
|
||||||
type() {
|
type() {
|
||||||
|
if (state.isSearchActive) {
|
||||||
|
return state.selected[0].type;
|
||||||
|
}
|
||||||
return getters.selectedCount() === 0
|
return getters.selectedCount() === 0
|
||||||
? state.req.type
|
? state.req.type
|
||||||
: state.req.items[this.selected[0]].type;
|
: state.req.items[this.selected[0]].type;
|
||||||
},
|
},
|
||||||
dir() {
|
dir() {
|
||||||
|
if (state.isSearchActive) {
|
||||||
|
return state.selected[0].type === "directory";
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
getters.selectedCount() > 1 ||
|
getters.selectedCount() > 1 ||
|
||||||
(getters.selectedCount() === 0
|
(getters.selectedCount() === 0
|
||||||
|
@ -145,9 +163,12 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
async checksum(event, algo) {
|
async checksum(event, algo) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
let link;
|
let link;
|
||||||
|
if (state.isSearchActive) {
|
||||||
|
const hash = await filesApi.checksum(state.selected[0].url, algo);
|
||||||
|
event.target.innerHTML = hash;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (getters.selectedCount()) {
|
if (getters.selectedCount()) {
|
||||||
link = state.req.items[this.selected[0]].url;
|
link = state.req.items[this.selected[0]].url;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -35,7 +35,6 @@
|
||||||
<button
|
<button
|
||||||
class="button button--flat"
|
class="button button--flat"
|
||||||
@click="move"
|
@click="move"
|
||||||
:disabled="$route.path === dest"
|
|
||||||
:aria-label="$t('buttons.move')"
|
:aria-label="$t('buttons.move')"
|
||||||
:title="$t('buttons.move')"
|
:title="$t('buttons.move')"
|
||||||
>
|
>
|
||||||
|
@ -53,7 +52,6 @@ import { filesApi } from "@/api";
|
||||||
import buttons from "@/utils/buttons";
|
import buttons from "@/utils/buttons";
|
||||||
import * as upload from "@/utils/upload";
|
import * as upload from "@/utils/upload";
|
||||||
import { notify } from "@/notify";
|
import { notify } from "@/notify";
|
||||||
import { removePrefix } from "@/utils/url";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "move",
|
name: "move",
|
||||||
|
@ -74,12 +72,21 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
for (let item of state.selected) {
|
if (state.isSearchActive) {
|
||||||
this.items.push({
|
this.items = [
|
||||||
from: state.req.items[item].url,
|
{
|
||||||
// add to: dest
|
from: "/files" + state.selected[0].url,
|
||||||
name: state.req.items[item].name,
|
name: state.selected[0].name,
|
||||||
});
|
},
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
for (let item of state.selected) {
|
||||||
|
this.items.push({
|
||||||
|
from: state.req.items[item].url,
|
||||||
|
// add to: dest
|
||||||
|
name: state.req.items[item].name,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -88,18 +95,15 @@ export default {
|
||||||
try {
|
try {
|
||||||
// Define the action function
|
// Define the action function
|
||||||
let action = async (overwrite, rename) => {
|
let action = async (overwrite, rename) => {
|
||||||
const loc = removePrefix(this.dest, "files");
|
|
||||||
for (let item of this.items) {
|
for (let item of this.items) {
|
||||||
item.to = loc + "/" + item.name;
|
item.to = this.dest + item.name;
|
||||||
}
|
}
|
||||||
buttons.loading("move");
|
buttons.loading("move");
|
||||||
await filesApi.moveCopy(this.items, "move", overwrite, rename);
|
await filesApi.moveCopy(this.items, "move", overwrite, rename);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch destination files
|
// Fetch destination files
|
||||||
let dstResp = await filesApi.fetchFiles(this.dest);
|
let dstResp = await filesApi.fetchFiles(this.dest);
|
||||||
let conflict = upload.checkConflict(this.items, dstResp.items);
|
let conflict = upload.checkConflict(this.items, dstResp.items);
|
||||||
|
|
||||||
let overwrite = false;
|
let overwrite = false;
|
||||||
let rename = false;
|
let rename = false;
|
||||||
|
|
||||||
|
@ -125,13 +129,13 @@ export default {
|
||||||
await action(overwrite, rename);
|
await action(overwrite, rename);
|
||||||
}
|
}
|
||||||
mutations.closeHovers();
|
mutations.closeHovers();
|
||||||
|
mutations.setSearch(false);
|
||||||
notify.showSuccess("Successfully moved file/folder, redirecting...");
|
notify.showSuccess("Successfully moved file/folder, redirecting...");
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.$router.push(this.dest);
|
this.$router.push(this.dest);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
// Catch any errors from action or other parts of the flow
|
notify.showError(error);
|
||||||
notify.showError(e);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -158,6 +158,9 @@ export default {
|
||||||
return getters.isListing(); // Access getter directly from the store
|
return getters.isListing(); // Access getter directly from the store
|
||||||
},
|
},
|
||||||
url() {
|
url() {
|
||||||
|
if (state.isSearchActive) {
|
||||||
|
return state.selected[0].url;
|
||||||
|
}
|
||||||
if (!this.isListing) {
|
if (!this.isListing) {
|
||||||
return state.route.path;
|
return state.route.path;
|
||||||
}
|
}
|
||||||
|
@ -170,11 +173,15 @@ export default {
|
||||||
},
|
},
|
||||||
async beforeMount() {
|
async beforeMount() {
|
||||||
try {
|
try {
|
||||||
let path = "." + getters.routePath("files");
|
if (state.isSearchActive) {
|
||||||
if (getters.selectedCount() === 1) {
|
this.subpath = state.selected[0].path;
|
||||||
path = path + state.req.items[this.selected[0]].name;
|
} else {
|
||||||
|
let path = "." + getters.routePath("files");
|
||||||
|
if (getters.selectedCount() === 1) {
|
||||||
|
path = path + state.req.items[this.selected[0]].name;
|
||||||
|
}
|
||||||
|
this.subpath = decodeURIComponent(path);
|
||||||
}
|
}
|
||||||
this.subpath = decodeURIComponent(path);
|
|
||||||
// get last element of the path
|
// get last element of the path
|
||||||
const links = await shareApi.get(this.subpath);
|
const links = await shareApi.get(this.subpath);
|
||||||
this.links = links;
|
this.links = links;
|
||||||
|
@ -238,6 +245,9 @@ export default {
|
||||||
return shareApi.getShareURL(share);
|
return shareApi.getShareURL(share);
|
||||||
},
|
},
|
||||||
hasDownloadLink() {
|
hasDownloadLink() {
|
||||||
|
if (state.isSearchActive) {
|
||||||
|
return state.selected[0].type != "directory";
|
||||||
|
}
|
||||||
return this.selected.length === 1 && !state.req.items[this.selected[0]].isDir;
|
return this.selected.length === 1 && !state.req.items[this.selected[0]].isDir;
|
||||||
},
|
},
|
||||||
buildDownloadLink(share) {
|
buildDownloadLink(share) {
|
||||||
|
|
|
@ -4,10 +4,10 @@
|
||||||
<div @click="navigateTo('/settings#profile-main')" class="inner-card">
|
<div @click="navigateTo('/settings#profile-main')" class="inner-card">
|
||||||
<i class="material-icons">person</i>
|
<i class="material-icons">person</i>
|
||||||
{{ user.username }}
|
{{ user.username }}
|
||||||
<i class="material-icons">settings</i>
|
<i aria-label="settings" class="material-icons">settings</i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="inner-card logout-button" @click="logout" >
|
<div class="inner-card logout-button" @click="logout">
|
||||||
<i v-if="canLogout" class="material-icons">exit_to_app</i>
|
<i v-if="canLogout" class="material-icons">exit_to_app</i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -94,7 +94,7 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.updateUsage();
|
this.updateUsage();
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isSettings: () => getters.isSettings(),
|
isSettings: () => getters.isSettings(),
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<nav id="sidebar" :class="{ active: active, 'dark-mode': isDarkMode }">
|
<nav
|
||||||
|
id="sidebar"
|
||||||
|
:class="{ active: active, 'dark-mode': isDarkMode, 'behind-overlay': behindOverlay }"
|
||||||
|
>
|
||||||
<SidebarSettings v-if="isSettings"></SidebarSettings>
|
<SidebarSettings v-if="isSettings"></SidebarSettings>
|
||||||
<SidebarGeneral v-else-if="isLoggedIn"></SidebarGeneral>
|
<SidebarGeneral v-else-if="isLoggedIn"></SidebarGeneral>
|
||||||
|
|
||||||
|
@ -8,14 +11,16 @@
|
||||||
<span v-for="item in externalLinks" :key="item.title">
|
<span v-for="item in externalLinks" :key="item.title">
|
||||||
<a :href="item.url" target="_blank" :title="item.title">{{ item.text }}</a>
|
<a :href="item.url" target="_blank" :title="item.title">{{ item.text }}</a>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="name != ''"><h3>{{ name }}</h3></span>
|
<span v-if="name != ''">
|
||||||
|
<h4>{{ name }}</h4>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { externalLinks, name } from "@/utils/constants";
|
import { externalLinks, name } from "@/utils/constants";
|
||||||
import { getters, mutations } from "@/store"; // Import your custom store
|
import { getters, mutations, state } from "@/store"; // Import your custom store
|
||||||
import SidebarGeneral from "./General.vue";
|
import SidebarGeneral from "./General.vue";
|
||||||
import SidebarSettings from "./Settings.vue";
|
import SidebarSettings from "./Settings.vue";
|
||||||
|
|
||||||
|
@ -36,6 +41,7 @@ export default {
|
||||||
isLoggedIn: () => getters.isLoggedIn(),
|
isLoggedIn: () => getters.isLoggedIn(),
|
||||||
isSettings: () => getters.isSettings(),
|
isSettings: () => getters.isSettings(),
|
||||||
active: () => getters.isSidebarVisible(),
|
active: () => getters.isSidebarVisible(),
|
||||||
|
behindOverlay: () => state.isSearchActive,
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// Show the help overlay
|
// Show the help overlay
|
||||||
|
@ -65,7 +71,11 @@ export default {
|
||||||
transition: 0.5s ease;
|
transition: 0.5s ease;
|
||||||
top: 4em;
|
top: 4em;
|
||||||
padding-bottom: 4em;
|
padding-bottom: 4em;
|
||||||
background-color: #DDDDDD
|
background-color: #dddddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar.behind-overlay {
|
||||||
|
z-index: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar.sticky {
|
#sidebar.sticky {
|
||||||
|
@ -106,7 +116,7 @@ body.rtl .action {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar .action>* {
|
#sidebar .action > * {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,7 +131,7 @@ body.rtl .action {
|
||||||
padding-bottom: 1em;
|
padding-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.credits>span {
|
.credits > span {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
|
|
@ -200,7 +200,7 @@ button:disabled {
|
||||||
}
|
}
|
||||||
|
|
||||||
#popup-notification.success {
|
#popup-notification.success {
|
||||||
background: var(--primaryColor);
|
background: var(--blue);
|
||||||
}
|
}
|
||||||
#popup-notification.error {
|
#popup-notification.error {
|
||||||
background: var(--red);
|
background: var(--red);
|
||||||
|
|
|
@ -129,7 +129,6 @@ router.beforeResolve(async (to, from, next) => {
|
||||||
console.warn("Avoiding recursive navigation to the same route.");
|
console.warn("Avoiding recursive navigation to the same route.");
|
||||||
return next(false);
|
return next(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the page title using i18n
|
// Set the page title using i18n
|
||||||
const title = i18n.global.t(titles[to.name as keyof typeof titles]);
|
const title = i18n.global.t(titles[to.name as keyof typeof titles]);
|
||||||
document.title = name + " - " + title ;
|
document.title = name + " - " + title ;
|
||||||
|
|
|
@ -120,8 +120,8 @@ export const getters = {
|
||||||
},
|
},
|
||||||
showOverlay: () => {
|
showOverlay: () => {
|
||||||
const hasPrompt = getters.currentPrompt() !== null && getters.currentPromptName() !== "more"
|
const hasPrompt = getters.currentPrompt() !== null && getters.currentPromptName() !== "more"
|
||||||
const shouldOverlaySidebar = getters.isSidebarVisible() && !getters.isStickySidebar()
|
const showForSidebar = getters.isSidebarVisible() && !getters.isStickySidebar()
|
||||||
return hasPrompt || shouldOverlaySidebar;
|
return hasPrompt || showForSidebar || state.isSearchActive;
|
||||||
},
|
},
|
||||||
showBreadCrumbs: () => {
|
showBreadCrumbs: () => {
|
||||||
return getters.currentView() == "listingView" ;
|
return getters.currentView() == "listingView" ;
|
||||||
|
|
|
@ -241,6 +241,10 @@ export const mutations = {
|
||||||
setSharePassword: (value) => {
|
setSharePassword: (value) => {
|
||||||
state.sharePassword = value;
|
state.sharePassword = value;
|
||||||
emitStateChanged();
|
emitStateChanged();
|
||||||
}
|
},
|
||||||
|
setSearch: (value) => {
|
||||||
|
state.isSearchActive = value;
|
||||||
|
emitStateChanged();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ export const state = reactive({
|
||||||
isSafari: /^((?!chrome|android).)*safari/i.test(navigator.userAgent),
|
isSafari: /^((?!chrome|android).)*safari/i.test(navigator.userAgent),
|
||||||
activeSettingsView: "",
|
activeSettingsView: "",
|
||||||
isMobile: window.innerWidth <= 800,
|
isMobile: window.innerWidth <= 800,
|
||||||
|
isSearchActive: false,
|
||||||
showSidebar: false,
|
showSidebar: false,
|
||||||
usage: {
|
usage: {
|
||||||
used: "0 B",
|
used: "0 B",
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { removePrefix } from './url.js';
|
||||||
|
|
||||||
|
describe('testurl', () => {
|
||||||
|
|
||||||
|
it('url prefix', () => {
|
||||||
|
let tests = [
|
||||||
|
{input: "test",trimArg: "",expected:"/test"},
|
||||||
|
{input: "/test", trimArg: "test",expected:"/"},
|
||||||
|
{input: "/my/test/file", trimArg: "",expected:"/my/test/file"},
|
||||||
|
{input: "/my/test/file", trimArg: "my",expected:"/test/file"},
|
||||||
|
{input: "/files/my/test/file", trimArg: "files",expected:"/my/test/file"},
|
||||||
|
]
|
||||||
|
for (let test of tests) {
|
||||||
|
expect(removePrefix(test.input, test.trimArg)).toEqual(test.expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -124,10 +124,7 @@ export default {
|
||||||
res = await filesApi.fetchFiles(getters.routePath(), content);
|
res = await filesApi.fetchFiles(getters.routePath(), content);
|
||||||
}
|
}
|
||||||
data = res;
|
data = res;
|
||||||
// Verify if the fetched path matches the current route
|
document.title = `${document.title} - ${res.name}`;
|
||||||
if (url.pathsMatch(res.path, `/${state.route.params.path}`)) {
|
|
||||||
document.title = `${res.name} - ${document.title}`;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
notify.showError(e);
|
notify.showError(e);
|
||||||
this.error = e;
|
this.error = e;
|
||||||
|
|
|
@ -19,8 +19,13 @@
|
||||||
></editorBar>
|
></editorBar>
|
||||||
<defaultBar v-else :class="{ 'dark-mode-header': isDarkMode }"></defaultBar>
|
<defaultBar v-else :class="{ 'dark-mode-header': isDarkMode }"></defaultBar>
|
||||||
<sidebar></sidebar>
|
<sidebar></sidebar>
|
||||||
<search v-if="showSearch"></search>
|
<main
|
||||||
<main :class="{ 'dark-mode': isDarkMode, moveWithSidebar: moveWithSidebar, 'main-padding': showPadding }">
|
:class="{
|
||||||
|
'dark-mode': isDarkMode,
|
||||||
|
moveWithSidebar: moveWithSidebar,
|
||||||
|
'main-padding': showPadding,
|
||||||
|
}"
|
||||||
|
>
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</main>
|
</main>
|
||||||
<prompts :class="{ 'dark-mode': isDarkMode }"></prompts>
|
<prompts :class="{ 'dark-mode': isDarkMode }"></prompts>
|
||||||
|
@ -38,7 +43,6 @@ import defaultBar from "./bars/Default.vue";
|
||||||
import listingBar from "./bars/ListingBar.vue";
|
import listingBar from "./bars/ListingBar.vue";
|
||||||
import Prompts from "@/components/prompts/Prompts.vue";
|
import Prompts from "@/components/prompts/Prompts.vue";
|
||||||
import Sidebar from "@/components/sidebar/Sidebar.vue";
|
import Sidebar from "@/components/sidebar/Sidebar.vue";
|
||||||
import Search from "@/components/Search.vue";
|
|
||||||
import ContextMenu from "@/components/ContextMenu.vue";
|
import ContextMenu from "@/components/ContextMenu.vue";
|
||||||
|
|
||||||
import { notify } from "@/notify";
|
import { notify } from "@/notify";
|
||||||
|
@ -49,7 +53,6 @@ export default {
|
||||||
name: "layout",
|
name: "layout",
|
||||||
components: {
|
components: {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
Search,
|
|
||||||
defaultBar,
|
defaultBar,
|
||||||
editorBar,
|
editorBar,
|
||||||
listingBar,
|
listingBar,
|
||||||
|
@ -74,9 +77,6 @@ export default {
|
||||||
showPadding() {
|
showPadding() {
|
||||||
return getters.showBreadCrumbs();
|
return getters.showBreadCrumbs();
|
||||||
},
|
},
|
||||||
showSearch() {
|
|
||||||
return getters.isLoggedIn() && this.currentView == "listingView";
|
|
||||||
},
|
|
||||||
isLoggedIn() {
|
isLoggedIn() {
|
||||||
return getters.isLoggedIn();
|
return getters.isLoggedIn();
|
||||||
},
|
},
|
||||||
|
@ -135,6 +135,7 @@ export default {
|
||||||
resetPrompts() {
|
resetPrompts() {
|
||||||
mutations.closeSidebar();
|
mutations.closeSidebar();
|
||||||
mutations.closeHovers();
|
mutations.closeHovers();
|
||||||
|
mutations.setSearch(false);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -72,6 +72,8 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
mutations.closeHovers();
|
||||||
|
mutations.setSearch(false);
|
||||||
mutations.setActiveSettingsView(getters.currentHash());
|
mutations.setActiveSettingsView(getters.currentHash());
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -280,7 +280,7 @@ export default {
|
||||||
file.hash = this.hash;
|
file.hash = this.hash;
|
||||||
this.token = file.token;
|
this.token = file.token;
|
||||||
mutations.replaceRequest(file);
|
mutations.replaceRequest(file);
|
||||||
document.title = `${file.name} - ${document.title}`;
|
document.title = `${document.title} - ${file.name}`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.error = error;
|
this.error = error;
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,18 +31,17 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
close() {
|
close() {
|
||||||
|
mutations.closeHovers();
|
||||||
|
|
||||||
if (getters.isSettings()) {
|
if (getters.isSettings()) {
|
||||||
// Use this.isSettings to access the computed property
|
// Use this.isSettings to access the computed property
|
||||||
router.push({ path: "/files/", hash: "" });
|
router.push({ path: "/files/", hash: "" });
|
||||||
mutations.closeHovers();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mutations.closeHovers();
|
|
||||||
setTimeout(() => {
|
mutations.replaceRequest({});
|
||||||
mutations.replaceRequest({});
|
let uri = url.removeLastDir(state.route.path) + "/";
|
||||||
let uri = url.removeLastDir(state.route.path) + "/";
|
router.push({ path: uri });
|
||||||
router.push({ path: uri });
|
|
||||||
}, 50);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,14 +5,15 @@
|
||||||
icon="menu"
|
icon="menu"
|
||||||
:label="$t('buttons.toggleSidebar')"
|
:label="$t('buttons.toggleSidebar')"
|
||||||
@action="toggleSidebar()"
|
@action="toggleSidebar()"
|
||||||
:disabled="showOverlay"
|
:disabled="isSearchActive"
|
||||||
/>
|
/>
|
||||||
|
<search v-if="showSearch"></search>
|
||||||
<action
|
<action
|
||||||
class="menu-button"
|
class="menu-button"
|
||||||
icon="grid_view"
|
icon="grid_view"
|
||||||
:label="$t('buttons.switchView')"
|
:label="$t('buttons.switchView')"
|
||||||
@action="switchView"
|
@action="switchView"
|
||||||
:disabled="showOverlay"
|
:disabled="isSearchActive"
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
@ -27,11 +28,13 @@
|
||||||
<script>
|
<script>
|
||||||
import { state, mutations, getters } from "@/store";
|
import { state, mutations, getters } from "@/store";
|
||||||
import Action from "@/components/Action.vue";
|
import Action from "@/components/Action.vue";
|
||||||
|
import Search from "@/components/Search.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "listingView",
|
name: "listingView",
|
||||||
components: {
|
components: {
|
||||||
Action,
|
Action,
|
||||||
|
Search,
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
|
@ -40,8 +43,11 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
showOverlay() {
|
showSearch() {
|
||||||
return getters.currentPrompt() !== null && getters.currentPromptName() !== "more";
|
return getters.isLoggedIn() && getters.currentView() == "listingView";
|
||||||
|
},
|
||||||
|
isSearchActive() {
|
||||||
|
return state.isSearchActive;
|
||||||
},
|
},
|
||||||
viewIcon() {
|
viewIcon() {
|
||||||
const icons = {
|
const icons = {
|
||||||
|
@ -78,7 +84,7 @@ export default {
|
||||||
const currentIndex = this.viewModes.indexOf(state.user.viewMode);
|
const currentIndex = this.viewModes.indexOf(state.user.viewMode);
|
||||||
const nextIndex = (currentIndex + 1) % this.viewModes.length;
|
const nextIndex = (currentIndex + 1) % this.viewModes.length;
|
||||||
const newView = this.viewModes[nextIndex];
|
const newView = this.viewModes[nextIndex];
|
||||||
mutations.updateCurrentUser({ "viewMode": newView });
|
mutations.updateCurrentUser({ viewMode: newView });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -280,6 +280,7 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
mutations.setSearch(false);
|
||||||
this.lastSelected = state.selected;
|
this.lastSelected = state.selected;
|
||||||
// Check the columns size for the first time.
|
// Check the columns size for the first time.
|
||||||
this.colunmsResize();
|
this.colunmsResize();
|
||||||
|
@ -504,6 +505,9 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
keyEvent(event) {
|
keyEvent(event) {
|
||||||
|
if (state.isSearchActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const { key, ctrlKey, metaKey, which } = event;
|
const { key, ctrlKey, metaKey, which } = event;
|
||||||
// Check if the key is alphanumeric
|
// Check if the key is alphanumeric
|
||||||
const isAlphanumeric = /^[a-z0-9]$/i.test(key);
|
const isAlphanumeric = /^[a-z0-9]$/i.test(key);
|
||||||
|
@ -518,10 +522,11 @@ export default {
|
||||||
// Handle the space bar key
|
// Handle the space bar key
|
||||||
if (key === " ") {
|
if (key === " ") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (getters.currentPromptName() == "search") {
|
if (state.isSearchActive) {
|
||||||
|
mutations.setSearch(false);
|
||||||
mutations.closeHovers();
|
mutations.closeHovers();
|
||||||
} else {
|
} else {
|
||||||
mutations.showHover("search");
|
mutations.setSearch(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (getters.currentPromptName() != null) {
|
if (getters.currentPromptName() != null) {
|
||||||
|
|
|
@ -14,12 +14,12 @@ test("logout", async ({ page, context }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
await expect(page.locator("div.wrong")).toBeHidden();
|
await expect(page.locator("div.wrong")).toBeHidden();
|
||||||
await page.waitForURL("**/files/", { timeout: 100 });
|
await page.waitForURL("**/files/", { timeout: 100 });
|
||||||
await expect(page).toHaveTitle('playwright-files - FileBrowser Quantum - Files');
|
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
|
||||||
let cookies = await context.cookies();
|
let cookies = await context.cookies();
|
||||||
expect(cookies.find((c) => c.name == "auth")?.value).toBeDefined();
|
expect(cookies.find((c) => c.name == "auth")?.value).toBeDefined();
|
||||||
await page.locator('div.inner-card.logout-button').click();
|
await page.locator('div.inner-card.logout-button').click();
|
||||||
await page.waitForURL("**/login", { timeout: 100 });
|
await page.waitForURL("**/login", { timeout: 100 });
|
||||||
await expect(page).toHaveTitle('FileBrowser Quantum - Login');
|
await expect(page).toHaveTitle("Graham's Filebrowser - Login");
|
||||||
cookies = await context.cookies();
|
cookies = await context.cookies();
|
||||||
expect(cookies.find((c) => c.name == "auth")?.value).toBeUndefined();
|
expect(cookies.find((c) => c.name == "auth")?.value).toBeUndefined();
|
||||||
});
|
});
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
test("info from listing", async ({ page, context }) => {
|
||||||
|
await page.goto("/files/");
|
||||||
|
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
|
||||||
|
await page.locator('a[aria-label="file.tar.gz"]').waitFor({ state: 'visible' });
|
||||||
|
await page.locator('a[aria-label="file.tar.gz"]').click( { button: "right" });
|
||||||
|
await page.locator('.selected-count-header').waitFor({ state: 'visible' });
|
||||||
|
await expect(page.locator('.selected-count-header')).toHaveText('1 selected');
|
||||||
|
await page.locator('button[aria-label="Info"]').click();
|
||||||
|
await expect(page.locator('.break-word')).toHaveText('Display Name: file.tar.gz');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("info from search", async ({ page, context }) => {
|
||||||
|
await page.goto("/files/");
|
||||||
|
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
|
||||||
|
await page.locator('#search').click()
|
||||||
|
await page.locator('#main-input').fill('file.tar.gz');
|
||||||
|
await expect(page.locator('#result-list')).toHaveCount(1);
|
||||||
|
await page.locator('li[aria-label="file.tar.gz"]').click({ button: "right" });
|
||||||
|
await page.locator('.selected-count-header').waitFor({ state: 'visible' });
|
||||||
|
await expect(page.locator('.selected-count-header')).toHaveText('1 selected');
|
||||||
|
await page.locator('button[aria-label="Info"]').click();
|
||||||
|
await expect(page.locator('.break-word')).toHaveText('Display Name: file.tar.gz');
|
||||||
|
})
|
||||||
|
|
||||||
|
test("copy from listing", async ({ page, context }) => {
|
||||||
|
await page.goto("/files/");
|
||||||
|
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
|
||||||
|
await page.locator('a[aria-label="copyme.txt"]').waitFor({ state: 'visible' });
|
||||||
|
await page.locator('a[aria-label="copyme.txt"]').click( { button: "right" });
|
||||||
|
await page.locator('.selected-count-header').waitFor({ state: 'visible' });
|
||||||
|
await expect(page.locator('.selected-count-header')).toHaveText('1 selected');
|
||||||
|
await page.locator('button[aria-label="Copy file"]').click();
|
||||||
|
//await expect(page.locator('.searchContext')).toHaveText('Path: /');
|
||||||
|
await page.locator('li[aria-label="myfolder"]').click();
|
||||||
|
await page.locator('button[aria-label="Copy"]').click();
|
||||||
|
const popup = page.locator('#popup-notification-content');
|
||||||
|
await popup.waitFor({ state: 'visible' });
|
||||||
|
await expect(popup).toHaveText("Successfully copied file/folder, redirecting...");
|
||||||
|
await page.waitForURL('**/myfolder/');
|
||||||
|
await expect(page).toHaveTitle("Graham's Filebrowser - Files - myfolder");
|
||||||
|
// verify exists and copy again
|
||||||
|
await page.locator('a[aria-label="copyme.txt"]').waitFor({ state: 'visible' });
|
||||||
|
await page.locator('a[aria-label="copyme.txt"]').click( { button: "right" });
|
||||||
|
await page.locator('.selected-count-header').waitFor({ state: 'visible' });
|
||||||
|
await expect(page.locator('.selected-count-header')).toHaveText('1 selected');
|
||||||
|
await page.locator('button[aria-label="Copy file"]').click();
|
||||||
|
//await expect(page.locator('.searchContext')).toHaveText('Path: /');
|
||||||
|
await page.locator('li[aria-label=".."]').click();
|
||||||
|
await page.locator('button[aria-label="Copy"]').click();
|
||||||
|
const popup2 = page.locator('#popup-notification-content');
|
||||||
|
await popup2.waitFor({ state: 'visible' });
|
||||||
|
//await expect(popup2).toHaveText("Successfully copied file/folder, redirecting...");
|
||||||
|
//await page.waitForURL('**/testdata/');
|
||||||
|
//await expect(page).toHaveTitle("Graham's Filebrowser - Files - testdata");
|
||||||
|
})
|
|
@ -0,0 +1 @@
|
||||||
|
test file for playwright
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.8 MiB |
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
@ -1,11 +1,55 @@
|
||||||
import { test, expect } from "@playwright/test";
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
test("file preview", async ({ page, context }) => {
|
test("blob file preview", async ({ page, context }) => {
|
||||||
await page.goto("/files/");
|
await page.goto("/files/");
|
||||||
await expect(page).toHaveTitle('playwright-files - FileBrowser Quantum - Files');
|
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
|
||||||
await page.locator('a[aria-label="file.tar.gz"]').waitFor({ state: 'visible' });
|
await page.locator('a[aria-label="file.tar.gz"]').waitFor({ state: 'visible' });
|
||||||
await page.locator('a[aria-label="file.tar.gz"]').dblclick();
|
await page.locator('a[aria-label="file.tar.gz"]').dblclick();
|
||||||
await expect(page).toHaveTitle('file.tar.gz - FileBrowser Quantum - Files');
|
await expect(page).toHaveTitle("Graham's Filebrowser - Files - file.tar.gz");
|
||||||
await page.locator('button[title="Close"]').click();
|
await page.locator('button[title="Close"]').click();
|
||||||
await expect(page).toHaveTitle('playwright-files - FileBrowser Quantum - Files');
|
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("text file editor", async ({ page, context }) => {
|
||||||
|
await page.goto("/files/");
|
||||||
|
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
|
||||||
|
await page.locator('a[aria-label="copyme.txt"]').waitFor({ state: 'visible' });
|
||||||
|
await page.locator('a[aria-label="copyme.txt"]').dblclick();
|
||||||
|
await expect(page).toHaveTitle("Graham's Filebrowser - Files - copyme.txt");
|
||||||
|
const firstLineText = await page.locator('.ace_text-layer .ace_line').first().textContent();
|
||||||
|
expect(firstLineText).toBe('test file for playwright');
|
||||||
|
await page.locator('button[title="Close"]').click();
|
||||||
|
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("navigate folders", async ({ page, context }) => {
|
||||||
|
await page.goto("/files/");
|
||||||
|
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
|
||||||
|
await page.locator('a[aria-label="myfolder"]').waitFor({ state: 'visible' });
|
||||||
|
await page.locator('a[aria-label="myfolder"]').dblclick();
|
||||||
|
await expect(page).toHaveTitle("Graham's Filebrowser - Files - myfolder");
|
||||||
|
await page.locator('a[aria-label="testdata"]').waitFor({ state: 'visible' });
|
||||||
|
await page.locator('a[aria-label="testdata"]').dblclick();
|
||||||
|
await expect(page).toHaveTitle("Graham's Filebrowser - Files - testdata");
|
||||||
|
await page.locator('a[aria-label="gray-sample.jpg"]').waitFor({ state: 'visible' });
|
||||||
|
await page.locator('a[aria-label="gray-sample.jpg"]').dblclick();
|
||||||
|
await expect(page).toHaveTitle("Graham's Filebrowser - Files - gray-sample.jpg");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("navigating images", async ({ page, context }) => {
|
||||||
|
await page.goto("/files/myfolder/testdata/20130612_142406.jpg");
|
||||||
|
await expect(page).toHaveTitle("Graham's Filebrowser - Files - 20130612_142406.jpg");
|
||||||
|
await page.locator('button[aria-label="Previous"]').waitFor({ state: 'hidden' });
|
||||||
|
await page.mouse.move(100, 100);
|
||||||
|
await page.locator('button[aria-label="Next"]').waitFor({ state: 'visible' });
|
||||||
|
await page.locator('button[aria-label="Next"]').click();
|
||||||
|
// went to next image
|
||||||
|
await expect(page).toHaveTitle("Graham's Filebrowser - Files - gray-sample.jpg");
|
||||||
|
await page.locator('button[aria-label="Previous"]').waitFor({ state: 'hidden' });
|
||||||
|
await page.locator('button[aria-label="Next"]').waitFor({ state: 'hidden' });
|
||||||
|
await page.mouse.move(100, 100);
|
||||||
|
await page.locator('button[aria-label="Next"]').waitFor({ state: 'visible' });
|
||||||
|
await page.locator('button[aria-label="Next"]').click();
|
||||||
|
// went to next image
|
||||||
|
await expect(page).toHaveTitle("Graham's Filebrowser - Files - IMG_2578.JPG");
|
||||||
});
|
});
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
test("sidebar links", async ({ page }) => {
|
||||||
|
await page.goto("/files/");
|
||||||
|
|
||||||
|
// Verify the page title
|
||||||
|
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
|
||||||
|
|
||||||
|
// Locate the credits container
|
||||||
|
const credits = page.locator('.credits'); // Fix the selector to match the HTML structure
|
||||||
|
|
||||||
|
// Assert that the <h3> contains the text 'FileBrowser Quantum'
|
||||||
|
await expect(credits.locator("h4")).toHaveText("Graham's Filebrowser");
|
||||||
|
|
||||||
|
// Assert that the <a> contains the text 'A playwright test'
|
||||||
|
await expect(credits.locator("span").locator("a")).toHaveText('A playwright test');
|
||||||
|
|
||||||
|
// Assert that the <a> does not contain the text 'Help'
|
||||||
|
await expect(credits.locator("span").locator("a")).not.toHaveText('Help');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("adjusting theme colors", async ({ page }) => {
|
||||||
|
await page.goto("/files/");
|
||||||
|
const originalPrimaryColor = await page.evaluate(() => {
|
||||||
|
return getComputedStyle(document.documentElement).getPropertyValue('--primaryColor').trim();
|
||||||
|
});
|
||||||
|
await expect(originalPrimaryColor).toBe('#2196f3');
|
||||||
|
|
||||||
|
// Verify the page title
|
||||||
|
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
|
||||||
|
await page.locator('i[aria-label="settings"]').click();
|
||||||
|
await expect(page).toHaveTitle("Graham's Filebrowser - Settings");
|
||||||
|
await page.locator('button', { hasText: 'violet' }).click();
|
||||||
|
const popup = page.locator('#popup-notification-content');
|
||||||
|
await popup.waitFor({ state: 'visible' });
|
||||||
|
await expect(popup).toHaveText('Settings updated!');
|
||||||
|
const newPrimaryColor = await page.evaluate(() => {
|
||||||
|
return getComputedStyle(document.documentElement).getPropertyValue('--primaryColor').trim();
|
||||||
|
});
|
||||||
|
await expect(newPrimaryColor).toBe('#9b59b6');
|
||||||
|
});
|
Loading…
Reference in New Issue