beta/v0.4.1 release (#307)
This commit is contained in:
parent
a30bfcf7d0
commit
c168599c91
|
@ -6,6 +6,8 @@ _old
|
|||
rice-box.go
|
||||
.idea/
|
||||
/backend/backend
|
||||
/backend/filebrowser
|
||||
/backend/filebrowser.exe
|
||||
/backend/backend.exe
|
||||
/frontend/dist
|
||||
/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).
|
||||
|
||||
## 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
|
||||
|
||||
**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.Info(database)
|
||||
logger.Info(fmt.Sprintf("Sources : %v", sources))
|
||||
|
||||
serverConfig := settings.Config.Server
|
||||
swagInfo := docs.SwaggerInfo
|
||||
swagInfo.BasePath = serverConfig.BaseURL
|
||||
|
|
Binary file not shown.
|
@ -5,6 +5,13 @@ server:
|
|||
auth:
|
||||
method: password
|
||||
signup: false
|
||||
frontend:
|
||||
name: "Graham's Filebrowser"
|
||||
disableDefaultLinks: true
|
||||
externalLinks:
|
||||
- text: "A playwright test"
|
||||
url: "https://playwright.dev/"
|
||||
title: "Playwright"
|
||||
userDefaults:
|
||||
darkMode: true
|
||||
disableSettings: false
|
||||
|
|
|
@ -208,11 +208,11 @@ func MoveResource(source, realsrc, realdst string, isSrcDir bool) error {
|
|||
index := GetIndex(source)
|
||||
// refresh info for source and dest
|
||||
err = index.RefreshFileInfo(FileOptions{
|
||||
Path: filepath.Dir(realsrc),
|
||||
Path: realsrc,
|
||||
IsDir: isSrcDir,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.ErrEmptyKey
|
||||
return fmt.Errorf("could not refresh index for source: %v", err)
|
||||
}
|
||||
refreshConfig := FileOptions{Path: realdst, IsDir: true}
|
||||
if !isSrcDir {
|
||||
|
@ -220,7 +220,7 @@ func MoveResource(source, realsrc, realdst string, isSrcDir bool) error {
|
|||
}
|
||||
err = index.RefreshFileInfo(refreshConfig)
|
||||
if err != nil {
|
||||
return errors.ErrEmptyKey
|
||||
return fmt.Errorf("could not refresh index for dest: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ func (idx *Index) newScanner(origin string) {
|
|||
}
|
||||
|
||||
// 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)
|
||||
|
||||
idx.scannerMu.Lock()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package fileutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
|
@ -11,15 +12,21 @@ import (
|
|||
// By default, the rename system call is used. If src and dst point to different volumes,
|
||||
// the file copy is used as a fallback.
|
||||
func MoveFile(src, dst string) error {
|
||||
fmt.Println("moving", src, dst)
|
||||
if os.Rename(src, dst) == nil {
|
||||
return nil
|
||||
}
|
||||
fmt.Println("copyfile instead", src, dst)
|
||||
|
||||
// fallback
|
||||
err := CopyFile(src, dst)
|
||||
if err != nil {
|
||||
fmt.Println("ok it errored too", err)
|
||||
|
||||
_ = os.Remove(dst)
|
||||
return err
|
||||
}
|
||||
fmt.Println("removing", src)
|
||||
if err := os.Remove(src); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -108,7 +108,6 @@ func withUserHelper(fn handleFunc) handleFunc {
|
|||
}
|
||||
tokenString, err := extractToken(r)
|
||||
if err != nil {
|
||||
logger.Debug(fmt.Sprintf("error extracting from request %v", err))
|
||||
return http.StatusUnauthorized, err
|
||||
}
|
||||
data.token = tokenString
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/gtsteffaniak/filebrowser/backend/cache"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
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 {
|
||||
return http.StatusBadRequest, nil
|
||||
} else if err != nil {
|
||||
|
@ -115,10 +118,7 @@ func resourceDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestCon
|
|||
}
|
||||
|
||||
// delete thumbnails
|
||||
err = delThumbs(r.Context(), fileCache, fileInfo)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
delThumbs(r.Context(), fileCache, fileInfo)
|
||||
|
||||
err = files.DeleteFiles(source, fileInfo.RealPath, filepath.Dir(fileInfo.RealPath))
|
||||
if err != nil {
|
||||
|
@ -184,10 +184,7 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
|||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
err = delThumbs(r.Context(), fileCache, fileInfo)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
delThumbs(r.Context(), fileCache, fileInfo)
|
||||
}
|
||||
err = files.WriteFile(fileOpts, r.Body)
|
||||
if err != nil {
|
||||
|
@ -312,7 +309,9 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont
|
|||
err = d.RunHook(func() error {
|
||||
return patchAction(r.Context(), action, realSrc, realDest, d, fileCache, isSrcDir, source)
|
||||
}, 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
|
||||
}
|
||||
|
||||
|
@ -332,11 +331,11 @@ func addVersionSuffix(source string) string {
|
|||
return source
|
||||
}
|
||||
|
||||
func delThumbs(ctx context.Context, fileCache FileCache, file files.ExtendedFileInfo) error {
|
||||
if err := fileCache.Delete(ctx, previewCacheKey(file.RealPath, "small", file.FileInfo.ModTime)); err != nil {
|
||||
return err
|
||||
func delThumbs(ctx context.Context, fileCache FileCache, file files.ExtendedFileInfo) {
|
||||
err := fileCache.Delete(ctx, previewCacheKey(file.RealPath, "small", file.FileInfo.ModTime))
|
||||
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 {
|
||||
|
@ -348,7 +347,6 @@ func patchAction(ctx context.Context, action, src, dst string, d *requestContext
|
|||
err := files.CopyResource(index, src, dst, isSrcDir)
|
||||
return err
|
||||
case "rename", "move":
|
||||
|
||||
if !d.user.Perm.Rename {
|
||||
return errors.ErrPermissionDenied
|
||||
}
|
||||
|
@ -366,10 +364,7 @@ func patchAction(ctx context.Context, action, src, dst string, d *requestContext
|
|||
}
|
||||
|
||||
// delete thumbnails
|
||||
err = delThumbs(ctx, fileCache, fileInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
delThumbs(ctx, fileCache, fileInfo)
|
||||
return files.MoveResource(index, src, dst, isSrcDir)
|
||||
default:
|
||||
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
|
||||
type Logger struct {
|
||||
logger *log.Logger
|
||||
levels []LogLevel
|
||||
apiLevels []LogLevel
|
||||
stdout bool
|
||||
disabled bool
|
||||
disabledAPI bool
|
||||
colors bool
|
||||
logger *log.Logger
|
||||
levels []LogLevel
|
||||
apiLevels []LogLevel
|
||||
stdout bool
|
||||
disabled bool
|
||||
debugEnabled bool
|
||||
disabledAPI bool
|
||||
colors bool
|
||||
}
|
||||
|
||||
var stdOutLoggerExists bool
|
||||
|
@ -34,7 +35,7 @@ func NewLogger(filepath string, levels, apiLevels []LogLevel, noColors bool) (*L
|
|||
}
|
||||
fileWriter = file
|
||||
}
|
||||
flags := log.Ldate | log.Ltime
|
||||
var flags int
|
||||
if slices.Contains(levels, DEBUG) {
|
||||
flags |= log.Lshortfile
|
||||
}
|
||||
|
@ -46,13 +47,14 @@ func NewLogger(filepath string, levels, apiLevels []LogLevel, noColors bool) (*L
|
|||
stdOutLoggerExists = true
|
||||
}
|
||||
return &Logger{
|
||||
logger: logger,
|
||||
levels: levels,
|
||||
apiLevels: apiLevels,
|
||||
disabled: slices.Contains(levels, DISABLED),
|
||||
disabledAPI: slices.Contains(apiLevels, DISABLED),
|
||||
colors: !noColors,
|
||||
stdout: stdout,
|
||||
logger: logger,
|
||||
levels: levels,
|
||||
apiLevels: apiLevels,
|
||||
disabled: slices.Contains(levels, DISABLED),
|
||||
debugEnabled: slices.Contains(levels, DEBUG),
|
||||
disabledAPI: slices.Contains(apiLevels, DISABLED),
|
||||
colors: !noColors,
|
||||
stdout: stdout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -67,6 +69,9 @@ func SetupLogger(output, levels, apiLevels string, noColors bool) error {
|
|||
if upperLevel == "WARNING" || upperLevel == "WARN" {
|
||||
upperLevel = "WARN "
|
||||
}
|
||||
if upperLevel == "INFO" {
|
||||
upperLevel = "INFO "
|
||||
}
|
||||
// Convert level strings to LogLevel
|
||||
level, ok := stringToLevel[upperLevel]
|
||||
if !ok {
|
||||
|
@ -87,6 +92,9 @@ func SetupLogger(output, levels, apiLevels string, noColors bool) error {
|
|||
if upperLevel == "WARNING" || upperLevel == "WARN" {
|
||||
upperLevel = "WARN "
|
||||
}
|
||||
if upperLevel == "INFO" {
|
||||
upperLevel = "INFO "
|
||||
}
|
||||
// Convert level strings to LogLevel
|
||||
level, ok := stringToLevel[strings.ToUpper(upperLevel)]
|
||||
if !ok {
|
||||
|
@ -119,6 +127,7 @@ func SetupLogger(output, levels, apiLevels string, noColors bool) error {
|
|||
loggers = append(loggers, logger)
|
||||
return nil
|
||||
}
|
||||
|
||||
func SplitByMultiple(str string) []string {
|
||||
delimiters := []rune{'|', ',', ' '}
|
||||
return strings.FieldsFunc(str, func(r rune) bool {
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"log"
|
||||
"slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LogLevel int
|
||||
|
@ -38,7 +39,7 @@ type levelConsts struct {
|
|||
}
|
||||
|
||||
var levels = levelConsts{
|
||||
INFO: "INFO",
|
||||
INFO: "INFO ", // with consistent space padding
|
||||
FATAL: "FATAL",
|
||||
ERROR: "ERROR",
|
||||
WARNING: "WARN ", // with consistent space padding
|
||||
|
@ -50,7 +51,7 @@ var levels = levelConsts{
|
|||
// stringToLevel maps string representation to LogLevel
|
||||
var stringToLevel = map[string]LogLevel{
|
||||
"DEBUG": DEBUG,
|
||||
"INFO": INFO,
|
||||
"INFO ": INFO, // with consistent space padding
|
||||
"ERROR": ERROR,
|
||||
"DISABLED": DISABLED,
|
||||
"WARN ": WARNING, // with consistent space padding
|
||||
|
@ -71,15 +72,21 @@ func Log(level string, msg string, prefix, api bool, color string) {
|
|||
continue
|
||||
}
|
||||
}
|
||||
if logger.stdout && LEVEL == FATAL {
|
||||
if logger.stdout && level == "FATAL" {
|
||||
continue
|
||||
}
|
||||
writeOut := msg
|
||||
if prefix {
|
||||
writeOut = fmt.Sprintf("[%s] ", level) + writeOut
|
||||
formattedTime := time.Now().Format("2006/01/02 15:04:05")
|
||||
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 != "" {
|
||||
writeOut = color + writeOut + "\033[0m"
|
||||
writeOut = writeOut + "\033[0m"
|
||||
}
|
||||
err := logger.logger.Output(3, writeOut) // 3 skips this function for correct file:line
|
||||
if err != nil {
|
||||
|
@ -102,6 +109,8 @@ func Api(msg string, statusCode int) {
|
|||
func Debug(msg string) {
|
||||
if len(loggers) > 0 {
|
||||
Log(levels.DEBUG, msg, true, false, GRAY)
|
||||
} else {
|
||||
log.Println("[DEBUG]: " + msg)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,9 @@ var Config Settings
|
|||
|
||||
func Initialize(configFile string) {
|
||||
yamlData, err := loadConfigFile(configFile)
|
||||
if err != nil && configFile != "config.yaml" {
|
||||
logger.Fatal("Could not load specified config file: " + err.Error())
|
||||
}
|
||||
if err != nil {
|
||||
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))
|
||||
}
|
||||
source.Path = realPath
|
||||
source.Name = "default" // Modify the local copy of the map value
|
||||
Config.Server.Sources["default"] = source // Assign the modified value back to the map
|
||||
source.Name = "default"
|
||||
Config.Server.Sources = []Source{source} // temporary set only one source
|
||||
}
|
||||
} else {
|
||||
realPath, err2 := filepath.Abs(Config.Server.Root)
|
||||
if err2 != nil {
|
||||
logger.Fatal(fmt.Sprintf("Error getting source path: %v", err2))
|
||||
}
|
||||
Config.Server.Sources = map[string]Source{
|
||||
"default": {
|
||||
Config.Server.Sources = []Source{
|
||||
{
|
||||
Name: "default",
|
||||
Path: realPath,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
baseurl := strings.Trim(Config.Server.BaseURL, "/")
|
||||
if baseurl == "" {
|
||||
Config.Server.BaseURL = "/"
|
||||
|
@ -57,10 +61,6 @@ func Initialize(configFile string) {
|
|||
Config.Server.BaseURL = "/" + baseurl + "/"
|
||||
}
|
||||
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{
|
||||
Text: fmt.Sprintf("(%v)", version.Version),
|
||||
Title: version.CommitSHA,
|
||||
|
@ -89,7 +89,6 @@ func Initialize(configFile string) {
|
|||
log.Println("[ERROR] Failed to set up logger:", err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func loadConfigFile(configFile string) ([]byte, error) {
|
||||
|
|
|
@ -36,26 +36,26 @@ type Recaptcha struct {
|
|||
}
|
||||
|
||||
type Server struct {
|
||||
NumImageProcessors int `json:"numImageProcessors"`
|
||||
Socket string `json:"socket"`
|
||||
TLSKey string `json:"tlsKey"`
|
||||
TLSCert string `json:"tlsCert"`
|
||||
EnableThumbnails bool `json:"enableThumbnails"`
|
||||
ResizePreview bool `json:"resizePreview"`
|
||||
EnableExec bool `json:"enableExec"`
|
||||
TypeDetectionByHeader bool `json:"typeDetectionByHeader"`
|
||||
AuthHook string `json:"authHook"`
|
||||
Port int `json:"port"`
|
||||
BaseURL string `json:"baseURL"`
|
||||
Address string `json:"address"`
|
||||
Logging []LogConfig `json:"logging"`
|
||||
Database string `json:"database"`
|
||||
Root string `json:"root"`
|
||||
UserHomeBasePath string `json:"userHomeBasePath"`
|
||||
CreateUserDir bool `json:"createUserDir"`
|
||||
Sources map[string]Source `json:"sources"`
|
||||
ExternalUrl string `json:"externalUrl"`
|
||||
InternalUrl string `json:"internalUrl"` // used by integrations
|
||||
NumImageProcessors int `json:"numImageProcessors"`
|
||||
Socket string `json:"socket"`
|
||||
TLSKey string `json:"tlsKey"`
|
||||
TLSCert string `json:"tlsCert"`
|
||||
EnableThumbnails bool `json:"enableThumbnails"`
|
||||
ResizePreview bool `json:"resizePreview"`
|
||||
EnableExec bool `json:"enableExec"`
|
||||
TypeDetectionByHeader bool `json:"typeDetectionByHeader"`
|
||||
AuthHook string `json:"authHook"`
|
||||
Port int `json:"port"`
|
||||
BaseURL string `json:"baseURL"`
|
||||
Address string `json:"address"`
|
||||
Logging []LogConfig `json:"logging"`
|
||||
Database string `json:"database"`
|
||||
Root string `json:"root"`
|
||||
UserHomeBasePath string `json:"userHomeBasePath"`
|
||||
CreateUserDir bool `json:"createUserDir"`
|
||||
Sources []Source `json:"sources"`
|
||||
ExternalUrl string `json:"externalUrl"`
|
||||
InternalUrl string `json:"internalUrl"` // used by integrations
|
||||
}
|
||||
|
||||
type Integrations struct {
|
||||
|
@ -78,8 +78,8 @@ type LogConfig struct {
|
|||
}
|
||||
|
||||
type Source struct {
|
||||
Path string `json:"path"`
|
||||
Name string
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
Config IndexConfig `json:"config"`
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ async function globalSetup() {
|
|||
await page.waitForURL("**/files/", { timeout: 100 });
|
||||
let cookies = await context.cookies();
|
||||
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 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 {
|
||||
const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
|
||||
return (await data.json()).checksums[algo];
|
||||
const params = {
|
||||
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) {
|
||||
notify.showError(err.message || "Error fetching checksum");
|
||||
throw err;
|
||||
|
|
|
@ -13,21 +13,20 @@
|
|||
<div v-if="selectedCount > 0" class="button selected-count-header">
|
||||
<span>{{ selectedCount }} selected</span>
|
||||
</div>
|
||||
|
||||
<action
|
||||
v-if="!headerButtons.select"
|
||||
v-if="!isSearchActive"
|
||||
icon="create_new_folder"
|
||||
:label="$t('sidebar.newFolder')"
|
||||
@action="showHover('newDir')"
|
||||
/>
|
||||
<action
|
||||
v-if="!headerButtons.select"
|
||||
v-if="!headerButtons.select && !isSearchActive"
|
||||
icon="note_add"
|
||||
:label="$t('sidebar.newFile')"
|
||||
@action="showHover('newFile')"
|
||||
/>
|
||||
<action
|
||||
v-if="!headerButtons.select"
|
||||
v-if="!headerButtons.select && !isSearchActive"
|
||||
icon="file_upload"
|
||||
:label="$t('buttons.upload')"
|
||||
@action="uploadFunc"
|
||||
|
@ -40,7 +39,7 @@
|
|||
show="info"
|
||||
/>
|
||||
<action
|
||||
v-if="!isMultiple"
|
||||
v-if="!isMultiple && !isSearchActive"
|
||||
icon="check_circle"
|
||||
:label="$t('buttons.selectMultiple')"
|
||||
@action="toggleMultipleSelection"
|
||||
|
@ -59,7 +58,7 @@
|
|||
show="share"
|
||||
/>
|
||||
<action
|
||||
v-if="headerButtons.rename"
|
||||
v-if="headerButtons.rename && !isSearchActive"
|
||||
icon="mode_edit"
|
||||
:label="$t('buttons.rename')"
|
||||
show="rename"
|
||||
|
@ -102,6 +101,9 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
isSearchActive() {
|
||||
return state.isSearchActive;
|
||||
},
|
||||
isMultiple() {
|
||||
return state.multiple;
|
||||
},
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
<template>
|
||||
<div id="search" :class="{ active, ongoing, 'dark-mode': isDarkMode }">
|
||||
<div
|
||||
id="search"
|
||||
:class="{ active, ongoing, 'dark-mode': isDarkMode }"
|
||||
@click="clearContext"
|
||||
>
|
||||
<!-- Search input section -->
|
||||
<div id="input" @click="open">
|
||||
<!-- Close button visible when search is active -->
|
||||
|
@ -115,11 +119,18 @@
|
|||
</div>
|
||||
<!-- List of search results -->
|
||||
<ul v-show="results.length > 0">
|
||||
<li v-for="(s, k) in results" :key="k" class="search-entry">
|
||||
<a :href="getRelative(s.path)">
|
||||
<li
|
||||
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" />
|
||||
<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>
|
||||
<div class="filesize">{{ humanSize(s.size) }}</div>
|
||||
</a>
|
||||
|
@ -136,6 +147,7 @@ import { search } from "@/api";
|
|||
import { getters, mutations, state } from "@/store";
|
||||
import { getHumanReadableFilesize } from "@/utils/filesizes";
|
||||
import { removeTrailingSlash, removeLeadingSlash } from "@/utils/url";
|
||||
import { removePrefix } from "@/utils/url.js";
|
||||
|
||||
import Icon from "@/components/Icon.vue";
|
||||
|
||||
|
@ -226,7 +238,7 @@ export default {
|
|||
return this.isTypeSelectDisabled;
|
||||
},
|
||||
active() {
|
||||
return getters.currentPromptName() == "search";
|
||||
return state.isSearchActive;
|
||||
},
|
||||
isDarkMode() {
|
||||
return getters.isDarkMode();
|
||||
|
@ -255,17 +267,22 @@ export default {
|
|||
return this.showHelp;
|
||||
},
|
||||
getContext() {
|
||||
let path = state.route.path;
|
||||
path = path.slice(1);
|
||||
path = "./" + path.substring(path.indexOf("/") + 1);
|
||||
path = path.replace(/\/+$/, "") + "/";
|
||||
if (path == "./files/") {
|
||||
path = "./";
|
||||
}
|
||||
return path;
|
||||
return this.getRelativeContext();
|
||||
},
|
||||
activeStates() {
|
||||
// Create a Set of combined `name` and `type` keys for efficient lookup
|
||||
const selectedSet = new Set(
|
||||
state.selected.map((item) => `${item.name}:${item.type}`)
|
||||
);
|
||||
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: {
|
||||
getRelativeContext() {
|
||||
return removePrefix(decodeURIComponent(state.route.path), "files");
|
||||
},
|
||||
getRelative(path) {
|
||||
return removeTrailingSlash(window.location.href) + "/" + removeLeadingSlash(path);
|
||||
},
|
||||
|
@ -304,15 +321,18 @@ export default {
|
|||
return parts.pop();
|
||||
},
|
||||
open() {
|
||||
if (!this.active) {
|
||||
if (!state.isSearchActive) {
|
||||
mutations.closeHovers();
|
||||
mutations.closeSidebar();
|
||||
mutations.resetSelected();
|
||||
this.resetSearchFilters();
|
||||
mutations.showHover("search");
|
||||
mutations.setSearch(true);
|
||||
}
|
||||
},
|
||||
close(event) {
|
||||
this.value = "";
|
||||
event.stopPropagation();
|
||||
mutations.closeHovers();
|
||||
mutations.setSearch(false);
|
||||
},
|
||||
keyup(event) {
|
||||
if (event.keyCode === 27) {
|
||||
|
@ -381,6 +401,24 @@ export default {
|
|||
toggleHelp() {
|
||||
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>
|
||||
|
@ -511,6 +549,10 @@ export default {
|
|||
border-radius: 0.25em;
|
||||
}
|
||||
|
||||
.search-entry.active {
|
||||
background-color: var(--surfacePrimary);
|
||||
}
|
||||
|
||||
.search-entry:hover {
|
||||
background-color: var(--surfacePrimary);
|
||||
}
|
||||
|
|
|
@ -52,7 +52,6 @@ import FileList from "./FileList.vue";
|
|||
import { filesApi } from "@/api";
|
||||
import buttons from "@/utils/buttons";
|
||||
import * as upload from "@/utils/upload";
|
||||
import { removePrefix } from "@/utils/url";
|
||||
import { notify } from "@/notify";
|
||||
|
||||
export default {
|
||||
|
@ -74,12 +73,21 @@ export default {
|
|||
},
|
||||
},
|
||||
mounted() {
|
||||
for (let item of state.selected) {
|
||||
this.items.push({
|
||||
from: state.req.items[item].url,
|
||||
// add to: dest
|
||||
name: state.req.items[item].name,
|
||||
});
|
||||
if (state.isSearchActive) {
|
||||
this.items = [
|
||||
{
|
||||
from: "/files" + state.selected[0].url,
|
||||
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: {
|
||||
|
@ -88,9 +96,8 @@ export default {
|
|||
try {
|
||||
// Define the action function
|
||||
let action = async (overwrite, rename) => {
|
||||
const loc = removePrefix(this.dest, "files");
|
||||
for (let item of this.items) {
|
||||
item.to = loc + "/" + item.name;
|
||||
item.to = this.dest + item.name;
|
||||
}
|
||||
buttons.loading("copy");
|
||||
await filesApi.moveCopy(this.items, "copy", overwrite, rename);
|
||||
|
@ -123,12 +130,13 @@ export default {
|
|||
await action(overwrite, rename);
|
||||
}
|
||||
mutations.closeHovers();
|
||||
mutations.setSearch(false);
|
||||
notify.showSuccess("Successfully copied file/folder, redirecting...");
|
||||
setTimeout(() => {
|
||||
this.$router.push(this.dest);
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
notify.error(error);
|
||||
notify.showError(error);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
<p v-else>
|
||||
{{ $t("prompts.deleteMessageMultiple", { count: selectedCount }) }}
|
||||
</p>
|
||||
<div style="display: grid" class="searchContext">
|
||||
<span v-for="(item, index) in nav" :key="index"> {{ item }} </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<button
|
||||
|
@ -34,6 +37,7 @@ import { filesApi } from "@/api";
|
|||
import buttons from "@/utils/buttons";
|
||||
import { state, getters, mutations } from "@/store";
|
||||
import { notify } from "@/notify";
|
||||
import { removePrefix } from "@/utils/url";
|
||||
|
||||
export default {
|
||||
name: "delete",
|
||||
|
@ -47,6 +51,16 @@ export default {
|
|||
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: {
|
||||
closeHovers() {
|
||||
|
@ -56,17 +70,24 @@ export default {
|
|||
buttons.loading("delete");
|
||||
|
||||
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) {
|
||||
await filesApi.remove(state.route.path);
|
||||
buttons.success("delete");
|
||||
notify.showSuccess("Deleted item successfully");
|
||||
|
||||
this.currentPrompt?.confirm();
|
||||
this.closeHovers();
|
||||
mutations.closeHovers();
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeHovers();
|
||||
mutations.closeHovers();
|
||||
|
||||
if (getters.selectedCount() === 0) {
|
||||
return;
|
||||
|
|
|
@ -99,6 +99,9 @@ export default {
|
|||
return getters.isListing();
|
||||
},
|
||||
humanSize() {
|
||||
if (state.isSearchActive) {
|
||||
return getHumanReadableFilesize(state.selected[0].size);
|
||||
}
|
||||
if (getters.selectedCount() === 0 || !this.isListing) {
|
||||
return getHumanReadableFilesize(state.req.size);
|
||||
}
|
||||
|
@ -112,6 +115,9 @@ export default {
|
|||
return getHumanReadableFilesize(sum);
|
||||
},
|
||||
humanTime() {
|
||||
if (state.isSearchActive) {
|
||||
return "unknown";
|
||||
}
|
||||
if (getters.selectedCount() === 0) {
|
||||
return formatTimestamp(state.req.modified, state.user.locale);
|
||||
}
|
||||
|
@ -121,19 +127,31 @@ export default {
|
|||
);
|
||||
},
|
||||
modTime() {
|
||||
if (state.isSearchActive) {
|
||||
return "";
|
||||
}
|
||||
return new Date(Date.parse(state.req.modified)).toLocaleString();
|
||||
},
|
||||
name() {
|
||||
if (state.isSearchActive) {
|
||||
return state.selected[0].name;
|
||||
}
|
||||
return getters.selectedCount() === 0
|
||||
? state.req.name
|
||||
: state.req.items[this.selected[0]].name;
|
||||
},
|
||||
type() {
|
||||
if (state.isSearchActive) {
|
||||
return state.selected[0].type;
|
||||
}
|
||||
return getters.selectedCount() === 0
|
||||
? state.req.type
|
||||
: state.req.items[this.selected[0]].type;
|
||||
},
|
||||
dir() {
|
||||
if (state.isSearchActive) {
|
||||
return state.selected[0].type === "directory";
|
||||
}
|
||||
return (
|
||||
getters.selectedCount() > 1 ||
|
||||
(getters.selectedCount() === 0
|
||||
|
@ -145,9 +163,12 @@ export default {
|
|||
methods: {
|
||||
async checksum(event, algo) {
|
||||
event.preventDefault();
|
||||
|
||||
let link;
|
||||
|
||||
if (state.isSearchActive) {
|
||||
const hash = await filesApi.checksum(state.selected[0].url, algo);
|
||||
event.target.innerHTML = hash;
|
||||
return;
|
||||
}
|
||||
if (getters.selectedCount()) {
|
||||
link = state.req.items[this.selected[0]].url;
|
||||
} else {
|
||||
|
|
|
@ -35,7 +35,6 @@
|
|||
<button
|
||||
class="button button--flat"
|
||||
@click="move"
|
||||
:disabled="$route.path === dest"
|
||||
:aria-label="$t('buttons.move')"
|
||||
:title="$t('buttons.move')"
|
||||
>
|
||||
|
@ -53,7 +52,6 @@ import { filesApi } from "@/api";
|
|||
import buttons from "@/utils/buttons";
|
||||
import * as upload from "@/utils/upload";
|
||||
import { notify } from "@/notify";
|
||||
import { removePrefix } from "@/utils/url";
|
||||
|
||||
export default {
|
||||
name: "move",
|
||||
|
@ -74,12 +72,21 @@ export default {
|
|||
},
|
||||
},
|
||||
mounted() {
|
||||
for (let item of state.selected) {
|
||||
this.items.push({
|
||||
from: state.req.items[item].url,
|
||||
// add to: dest
|
||||
name: state.req.items[item].name,
|
||||
});
|
||||
if (state.isSearchActive) {
|
||||
this.items = [
|
||||
{
|
||||
from: "/files" + state.selected[0].url,
|
||||
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: {
|
||||
|
@ -88,18 +95,15 @@ export default {
|
|||
try {
|
||||
// Define the action function
|
||||
let action = async (overwrite, rename) => {
|
||||
const loc = removePrefix(this.dest, "files");
|
||||
for (let item of this.items) {
|
||||
item.to = loc + "/" + item.name;
|
||||
item.to = this.dest + item.name;
|
||||
}
|
||||
buttons.loading("move");
|
||||
await filesApi.moveCopy(this.items, "move", overwrite, rename);
|
||||
};
|
||||
|
||||
// Fetch destination files
|
||||
let dstResp = await filesApi.fetchFiles(this.dest);
|
||||
let conflict = upload.checkConflict(this.items, dstResp.items);
|
||||
|
||||
let overwrite = false;
|
||||
let rename = false;
|
||||
|
||||
|
@ -125,13 +129,13 @@ export default {
|
|||
await action(overwrite, rename);
|
||||
}
|
||||
mutations.closeHovers();
|
||||
mutations.setSearch(false);
|
||||
notify.showSuccess("Successfully moved file/folder, redirecting...");
|
||||
setTimeout(() => {
|
||||
this.$router.push(this.dest);
|
||||
}, 1000);
|
||||
} catch (e) {
|
||||
// Catch any errors from action or other parts of the flow
|
||||
notify.showError(e);
|
||||
} catch (error) {
|
||||
notify.showError(error);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -158,6 +158,9 @@ export default {
|
|||
return getters.isListing(); // Access getter directly from the store
|
||||
},
|
||||
url() {
|
||||
if (state.isSearchActive) {
|
||||
return state.selected[0].url;
|
||||
}
|
||||
if (!this.isListing) {
|
||||
return state.route.path;
|
||||
}
|
||||
|
@ -170,11 +173,15 @@ export default {
|
|||
},
|
||||
async beforeMount() {
|
||||
try {
|
||||
let path = "." + getters.routePath("files");
|
||||
if (getters.selectedCount() === 1) {
|
||||
path = path + state.req.items[this.selected[0]].name;
|
||||
if (state.isSearchActive) {
|
||||
this.subpath = state.selected[0].path;
|
||||
} 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
|
||||
const links = await shareApi.get(this.subpath);
|
||||
this.links = links;
|
||||
|
@ -238,6 +245,9 @@ export default {
|
|||
return shareApi.getShareURL(share);
|
||||
},
|
||||
hasDownloadLink() {
|
||||
if (state.isSearchActive) {
|
||||
return state.selected[0].type != "directory";
|
||||
}
|
||||
return this.selected.length === 1 && !state.req.items[this.selected[0]].isDir;
|
||||
},
|
||||
buildDownloadLink(share) {
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
<div @click="navigateTo('/settings#profile-main')" class="inner-card">
|
||||
<i class="material-icons">person</i>
|
||||
{{ user.username }}
|
||||
<i class="material-icons">settings</i>
|
||||
<i aria-label="settings" class="material-icons">settings</i>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -94,7 +94,7 @@ export default {
|
|||
};
|
||||
},
|
||||
mounted() {
|
||||
this.updateUsage();
|
||||
this.updateUsage();
|
||||
},
|
||||
computed: {
|
||||
isSettings: () => getters.isSettings(),
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<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>
|
||||
<SidebarGeneral v-else-if="isLoggedIn"></SidebarGeneral>
|
||||
|
||||
|
@ -8,14 +11,16 @@
|
|||
<span v-for="item in externalLinks" :key="item.title">
|
||||
<a :href="item.url" target="_blank" :title="item.title">{{ item.text }}</a>
|
||||
</span>
|
||||
<span v-if="name != ''"><h3>{{ name }}</h3></span>
|
||||
<span v-if="name != ''">
|
||||
<h4>{{ name }}</h4>
|
||||
</span>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
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 SidebarSettings from "./Settings.vue";
|
||||
|
||||
|
@ -36,6 +41,7 @@ export default {
|
|||
isLoggedIn: () => getters.isLoggedIn(),
|
||||
isSettings: () => getters.isSettings(),
|
||||
active: () => getters.isSidebarVisible(),
|
||||
behindOverlay: () => state.isSearchActive,
|
||||
},
|
||||
methods: {
|
||||
// Show the help overlay
|
||||
|
@ -65,7 +71,11 @@ export default {
|
|||
transition: 0.5s ease;
|
||||
top: 4em;
|
||||
padding-bottom: 4em;
|
||||
background-color: #DDDDDD
|
||||
background-color: #dddddd;
|
||||
}
|
||||
|
||||
#sidebar.behind-overlay {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
#sidebar.sticky {
|
||||
|
@ -106,7 +116,7 @@ body.rtl .action {
|
|||
text-align: right;
|
||||
}
|
||||
|
||||
#sidebar .action>* {
|
||||
#sidebar .action > * {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
@ -121,7 +131,7 @@ body.rtl .action {
|
|||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
.credits>span {
|
||||
.credits > span {
|
||||
display: block;
|
||||
margin-top: 0.5em;
|
||||
margin-left: 0;
|
||||
|
|
|
@ -200,7 +200,7 @@ button:disabled {
|
|||
}
|
||||
|
||||
#popup-notification.success {
|
||||
background: var(--primaryColor);
|
||||
background: var(--blue);
|
||||
}
|
||||
#popup-notification.error {
|
||||
background: var(--red);
|
||||
|
|
|
@ -129,7 +129,6 @@ router.beforeResolve(async (to, from, next) => {
|
|||
console.warn("Avoiding recursive navigation to the same route.");
|
||||
return next(false);
|
||||
}
|
||||
|
||||
// Set the page title using i18n
|
||||
const title = i18n.global.t(titles[to.name as keyof typeof titles]);
|
||||
document.title = name + " - " + title ;
|
||||
|
|
|
@ -120,8 +120,8 @@ export const getters = {
|
|||
},
|
||||
showOverlay: () => {
|
||||
const hasPrompt = getters.currentPrompt() !== null && getters.currentPromptName() !== "more"
|
||||
const shouldOverlaySidebar = getters.isSidebarVisible() && !getters.isStickySidebar()
|
||||
return hasPrompt || shouldOverlaySidebar;
|
||||
const showForSidebar = getters.isSidebarVisible() && !getters.isStickySidebar()
|
||||
return hasPrompt || showForSidebar || state.isSearchActive;
|
||||
},
|
||||
showBreadCrumbs: () => {
|
||||
return getters.currentView() == "listingView" ;
|
||||
|
|
|
@ -241,6 +241,10 @@ export const mutations = {
|
|||
setSharePassword: (value) => {
|
||||
state.sharePassword = value;
|
||||
emitStateChanged();
|
||||
}
|
||||
},
|
||||
setSearch: (value) => {
|
||||
state.isSearchActive = value;
|
||||
emitStateChanged();
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ export const state = reactive({
|
|||
isSafari: /^((?!chrome|android).)*safari/i.test(navigator.userAgent),
|
||||
activeSettingsView: "",
|
||||
isMobile: window.innerWidth <= 800,
|
||||
isSearchActive: false,
|
||||
showSidebar: false,
|
||||
usage: {
|
||||
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);
|
||||
}
|
||||
data = res;
|
||||
// Verify if the fetched path matches the current route
|
||||
if (url.pathsMatch(res.path, `/${state.route.params.path}`)) {
|
||||
document.title = `${res.name} - ${document.title}`;
|
||||
}
|
||||
document.title = `${document.title} - ${res.name}`;
|
||||
} catch (e) {
|
||||
notify.showError(e);
|
||||
this.error = e;
|
||||
|
|
|
@ -19,8 +19,13 @@
|
|||
></editorBar>
|
||||
<defaultBar v-else :class="{ 'dark-mode-header': isDarkMode }"></defaultBar>
|
||||
<sidebar></sidebar>
|
||||
<search v-if="showSearch"></search>
|
||||
<main :class="{ 'dark-mode': isDarkMode, moveWithSidebar: moveWithSidebar, 'main-padding': showPadding }">
|
||||
<main
|
||||
:class="{
|
||||
'dark-mode': isDarkMode,
|
||||
moveWithSidebar: moveWithSidebar,
|
||||
'main-padding': showPadding,
|
||||
}"
|
||||
>
|
||||
<router-view></router-view>
|
||||
</main>
|
||||
<prompts :class="{ 'dark-mode': isDarkMode }"></prompts>
|
||||
|
@ -38,7 +43,6 @@ import defaultBar from "./bars/Default.vue";
|
|||
import listingBar from "./bars/ListingBar.vue";
|
||||
import Prompts from "@/components/prompts/Prompts.vue";
|
||||
import Sidebar from "@/components/sidebar/Sidebar.vue";
|
||||
import Search from "@/components/Search.vue";
|
||||
import ContextMenu from "@/components/ContextMenu.vue";
|
||||
|
||||
import { notify } from "@/notify";
|
||||
|
@ -49,7 +53,6 @@ export default {
|
|||
name: "layout",
|
||||
components: {
|
||||
ContextMenu,
|
||||
Search,
|
||||
defaultBar,
|
||||
editorBar,
|
||||
listingBar,
|
||||
|
@ -74,9 +77,6 @@ export default {
|
|||
showPadding() {
|
||||
return getters.showBreadCrumbs();
|
||||
},
|
||||
showSearch() {
|
||||
return getters.isLoggedIn() && this.currentView == "listingView";
|
||||
},
|
||||
isLoggedIn() {
|
||||
return getters.isLoggedIn();
|
||||
},
|
||||
|
@ -135,6 +135,7 @@ export default {
|
|||
resetPrompts() {
|
||||
mutations.closeSidebar();
|
||||
mutations.closeHovers();
|
||||
mutations.setSearch(false);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -72,6 +72,8 @@ export default {
|
|||
},
|
||||
},
|
||||
mounted() {
|
||||
mutations.closeHovers();
|
||||
mutations.setSearch(false);
|
||||
mutations.setActiveSettingsView(getters.currentHash());
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -280,7 +280,7 @@ export default {
|
|||
file.hash = this.hash;
|
||||
this.token = file.token;
|
||||
mutations.replaceRequest(file);
|
||||
document.title = `${file.name} - ${document.title}`;
|
||||
document.title = `${document.title} - ${file.name}`;
|
||||
} catch (error) {
|
||||
this.error = error;
|
||||
}
|
||||
|
|
|
@ -31,18 +31,17 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
close() {
|
||||
mutations.closeHovers();
|
||||
|
||||
if (getters.isSettings()) {
|
||||
// Use this.isSettings to access the computed property
|
||||
router.push({ path: "/files/", hash: "" });
|
||||
mutations.closeHovers();
|
||||
return;
|
||||
}
|
||||
mutations.closeHovers();
|
||||
setTimeout(() => {
|
||||
mutations.replaceRequest({});
|
||||
let uri = url.removeLastDir(state.route.path) + "/";
|
||||
router.push({ path: uri });
|
||||
}, 50);
|
||||
|
||||
mutations.replaceRequest({});
|
||||
let uri = url.removeLastDir(state.route.path) + "/";
|
||||
router.push({ path: uri });
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -5,14 +5,15 @@
|
|||
icon="menu"
|
||||
:label="$t('buttons.toggleSidebar')"
|
||||
@action="toggleSidebar()"
|
||||
:disabled="showOverlay"
|
||||
:disabled="isSearchActive"
|
||||
/>
|
||||
<search v-if="showSearch"></search>
|
||||
<action
|
||||
class="menu-button"
|
||||
icon="grid_view"
|
||||
:label="$t('buttons.switchView')"
|
||||
@action="switchView"
|
||||
:disabled="showOverlay"
|
||||
:disabled="isSearchActive"
|
||||
/>
|
||||
</header>
|
||||
</template>
|
||||
|
@ -27,11 +28,13 @@
|
|||
<script>
|
||||
import { state, mutations, getters } from "@/store";
|
||||
import Action from "@/components/Action.vue";
|
||||
import Search from "@/components/Search.vue";
|
||||
|
||||
export default {
|
||||
name: "listingView",
|
||||
components: {
|
||||
Action,
|
||||
Search,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
|
@ -40,8 +43,11 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
showOverlay() {
|
||||
return getters.currentPrompt() !== null && getters.currentPromptName() !== "more";
|
||||
showSearch() {
|
||||
return getters.isLoggedIn() && getters.currentView() == "listingView";
|
||||
},
|
||||
isSearchActive() {
|
||||
return state.isSearchActive;
|
||||
},
|
||||
viewIcon() {
|
||||
const icons = {
|
||||
|
@ -78,7 +84,7 @@ export default {
|
|||
const currentIndex = this.viewModes.indexOf(state.user.viewMode);
|
||||
const nextIndex = (currentIndex + 1) % this.viewModes.length;
|
||||
const newView = this.viewModes[nextIndex];
|
||||
mutations.updateCurrentUser({ "viewMode": newView });
|
||||
mutations.updateCurrentUser({ viewMode: newView });
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -280,6 +280,7 @@ export default {
|
|||
},
|
||||
},
|
||||
mounted() {
|
||||
mutations.setSearch(false);
|
||||
this.lastSelected = state.selected;
|
||||
// Check the columns size for the first time.
|
||||
this.colunmsResize();
|
||||
|
@ -504,6 +505,9 @@ export default {
|
|||
}
|
||||
},
|
||||
keyEvent(event) {
|
||||
if (state.isSearchActive) {
|
||||
return;
|
||||
}
|
||||
const { key, ctrlKey, metaKey, which } = event;
|
||||
// Check if the key is alphanumeric
|
||||
const isAlphanumeric = /^[a-z0-9]$/i.test(key);
|
||||
|
@ -518,10 +522,11 @@ export default {
|
|||
// Handle the space bar key
|
||||
if (key === " ") {
|
||||
event.preventDefault();
|
||||
if (getters.currentPromptName() == "search") {
|
||||
if (state.isSearchActive) {
|
||||
mutations.setSearch(false);
|
||||
mutations.closeHovers();
|
||||
} else {
|
||||
mutations.showHover("search");
|
||||
mutations.setSearch(true);
|
||||
}
|
||||
}
|
||||
if (getters.currentPromptName() != null) {
|
||||
|
|
|
@ -14,12 +14,12 @@ test("logout", async ({ page, context }) => {
|
|||
await page.goto('/');
|
||||
await expect(page.locator("div.wrong")).toBeHidden();
|
||||
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();
|
||||
expect(cookies.find((c) => c.name == "auth")?.value).toBeDefined();
|
||||
await page.locator('div.inner-card.logout-button').click();
|
||||
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();
|
||||
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";
|
||||
|
||||
test("file preview", async ({ page, context }) => {
|
||||
test("blob file preview", async ({ page, context }) => {
|
||||
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"]').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 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