mirror of
https://github.com/gtsteffaniak/filebrowser.git
synced 2026-11-05 09:10:14 -05:00
Compare commits
10
Commits
main
...
v1.1.6-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0219b891a2 | ||
|
|
eee7b7828f | ||
|
|
7f40776c28 | ||
|
|
9ddafd2df7 | ||
|
|
82da2c31ff | ||
|
|
783b9962bb | ||
|
|
65d2e4a05b | ||
|
|
2ad25a0459 | ||
|
|
eedf63a8de | ||
|
|
58b1d53bbb |
No files matched your search
@@ -2,6 +2,24 @@
|
||||
|
||||
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).
|
||||
|
||||
## v1.1.6-beta
|
||||
|
||||
**Notes**:
|
||||
- "showHidden" is now a backend attribute. Shares will NOT show hidden files by default unless configured to.
|
||||
- improved listing view UI performance, especially for folders with many items. (#1773)
|
||||
|
||||
**BugFixes**:
|
||||
- SortBy config is not respected when navigating next/previous (#1786)
|
||||
- unauthenticated users unable to preview files (#1792)
|
||||
- File permission not correctly applied( #1790)
|
||||
- API Key loses "download" permission after creation despite being enabled (#1799)
|
||||
- never exclude root folder (#1749)
|
||||
- sources with "files" name not showing after 1.1.x update (#1772)
|
||||
- Symbolic links can't be deleted from webui. (#1566)
|
||||
- panic: close of closed channel during indexing (#1771)
|
||||
- Public sharing returns 404 errors for album art (#1792)
|
||||
- Directories with some symbols in their name "fail to open" (#1808)
|
||||
|
||||
## v1.1.5-beta
|
||||
|
||||
**Notes**:
|
||||
|
||||
@@ -28,17 +28,19 @@ import (
|
||||
|
||||
func FileInfoFaster(opts utils.FileOptions, access *access.Storage) (*iteminfo.ExtendedFileInfo, error) {
|
||||
response := &iteminfo.ExtendedFileInfo{}
|
||||
index := indexing.GetIndex(opts.Source)
|
||||
if index == nil {
|
||||
idx := indexing.GetIndex(opts.Source)
|
||||
if idx == nil {
|
||||
return response, fmt.Errorf("could not get index: %v ", opts.Source)
|
||||
}
|
||||
if !strings.HasPrefix(opts.Path, "/") {
|
||||
opts.Path = "/" + opts.Path
|
||||
}
|
||||
realPath, isDir, err := index.GetRealPath(opts.Path)
|
||||
if err != nil {
|
||||
|
||||
realPath, isDir, err := idx.GetRealPath(opts.Path)
|
||||
if err != nil && opts.FollowSymlinks {
|
||||
return response, fmt.Errorf("could not get real path for requested path: %v, error: %v", opts.Path, err)
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(opts.Path, "/") && isDir {
|
||||
opts.Path = opts.Path + "/"
|
||||
}
|
||||
@@ -46,28 +48,21 @@ func FileInfoFaster(opts utils.FileOptions, access *access.Storage) (*iteminfo.E
|
||||
var info *iteminfo.FileInfo
|
||||
|
||||
// Check if path is viewable (allows filesystem access without indexing)
|
||||
isViewable := index.IsViewable(isDir, opts.Path)
|
||||
isViewable := idx.IsViewable(isDir, opts.Path)
|
||||
|
||||
// For non-viewable paths, verify they are indexed
|
||||
// Skip this check if indexing is disabled for the entire source
|
||||
if !isViewable && !index.Config.DisableIndexing {
|
||||
err = index.RefreshFileInfo(opts)
|
||||
if !isViewable && !idx.Config.DisableIndexing {
|
||||
err = idx.RefreshFileInfo(opts)
|
||||
if err != nil {
|
||||
return response, fmt.Errorf("path not accessible: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if isDir {
|
||||
info, err = index.GetFsDirInfo(opts.Path)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
} else {
|
||||
// For files, get info from parent directory to ensure HasPreview is set correctly
|
||||
info, err = index.GetFsDirInfo(opts.Path)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
info, err = idx.GetFsInfo(opts.Path, opts.FollowSymlinks)
|
||||
if err != nil {
|
||||
fmt.Println("Graham 2: ", err)
|
||||
return response, err
|
||||
}
|
||||
|
||||
response.FileInfo = *info
|
||||
@@ -75,12 +70,31 @@ func FileInfoFaster(opts utils.FileOptions, access *access.Storage) (*iteminfo.E
|
||||
response.Source = opts.Source
|
||||
|
||||
if access != nil {
|
||||
err := access.CheckChildItemAccess(response, index, opts.Username)
|
||||
err := access.CheckChildItemAccess(response, idx, opts.Username)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
}
|
||||
|
||||
// Filter hidden files if ShowHidden is false
|
||||
if !opts.ShowHidden && isDir {
|
||||
filteredFiles := []iteminfo.ExtendedItemInfo{}
|
||||
for _, file := range response.Files {
|
||||
if !file.Hidden {
|
||||
filteredFiles = append(filteredFiles, file)
|
||||
}
|
||||
}
|
||||
response.Files = filteredFiles
|
||||
|
||||
filteredFolders := []iteminfo.ItemInfo{}
|
||||
for _, folder := range response.Folders {
|
||||
if !folder.Hidden {
|
||||
filteredFolders = append(filteredFolders, folder)
|
||||
}
|
||||
}
|
||||
response.Folders = filteredFolders
|
||||
}
|
||||
|
||||
// For directories, populate metadata for audio/video files ONLY if explicitly requested
|
||||
// This avoids expensive ffprobe calls on every directory listing
|
||||
if isDir && opts.Metadata {
|
||||
@@ -101,7 +115,7 @@ func FileInfoFaster(opts utils.FileOptions, access *access.Storage) (*iteminfo.E
|
||||
|
||||
if isItemAudio || isItemVideo {
|
||||
// Get the real path for this file
|
||||
itemRealPath, _, _ := index.GetRealPath(opts.Path, fileItem.Name)
|
||||
itemRealPath, _, _ := idx.GetRealPath(opts.Path, fileItem.Name)
|
||||
|
||||
// Capture loop variables in local copies to avoid closure issues
|
||||
item := fileItem
|
||||
@@ -148,7 +162,7 @@ func FileInfoFaster(opts utils.FileOptions, access *access.Storage) (*iteminfo.E
|
||||
// Extract content/metadata when explicitly requested OR for single file audio/video requests
|
||||
isAudioVideo := strings.HasPrefix(info.Type, "audio") || strings.HasPrefix(info.Type, "video")
|
||||
if opts.Content || opts.Metadata || (!isDir && isAudioVideo) {
|
||||
processContent(response, index, opts)
|
||||
processContent(response, idx, opts)
|
||||
}
|
||||
|
||||
if settings.Config.Integrations.OnlyOffice.Secret != "" && info.Type != "directory" && iteminfo.IsOnlyOffice(info.Name) {
|
||||
@@ -634,8 +648,8 @@ func WriteFile(source, path string, in io.Reader) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Explicitly set directory permissions to bypass umask
|
||||
err = os.Chmod(realPath, fileutils.PermDir)
|
||||
// Explicitly set file permissions to bypass umask
|
||||
err = os.Chmod(realPath, fileutils.PermFile)
|
||||
if err != nil {
|
||||
// Handle chmod error gracefully
|
||||
logger.Debugf("Could not set file permissions for %s (this may be expected in restricted environments): %v", realPath, err)
|
||||
|
||||
@@ -55,4 +55,6 @@ type FileOptions struct {
|
||||
Metadata bool // whether to get metadata
|
||||
ExtractEmbeddedSubtitles bool // whether to extract embedded subtitles from media files
|
||||
AlbumArt bool // whether to get album art from media files
|
||||
ShowHidden bool // whether to show hidden files (true = show, false = hide)
|
||||
FollowSymlinks bool // whether to follow symlinks
|
||||
}
|
||||
@@ -41,6 +41,7 @@ type CommonShare struct {
|
||||
AllowReplacements bool `json:"allowReplacements,omitempty"` // allow replacements of files
|
||||
SidebarLinks []users.SidebarLink `json:"sidebarLinks"` // customizable sidebar links
|
||||
HasPassword bool `json:"hasPassword,omitempty"`
|
||||
ShowHidden bool `json:"showHidden,omitempty"` // show hidden files in share (true = show, false = hide)
|
||||
}
|
||||
type CreateBody struct {
|
||||
CommonShare
|
||||
|
||||
@@ -96,6 +96,7 @@ func createApiKeyHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||
Create: strings.Contains(permissionsStr, "create") && d.user.Permissions.Create,
|
||||
Share: strings.Contains(permissionsStr, "share") && d.user.Permissions.Share,
|
||||
Realtime: strings.Contains(permissionsStr, "realtime") && d.user.Permissions.Realtime,
|
||||
Download: strings.Contains(permissionsStr, "download") && d.user.Permissions.Download,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -211,9 +211,6 @@ func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete ch
|
||||
Handler: muxWithMiddleware(router),
|
||||
}
|
||||
listenAddress := config.Server.ListenAddress
|
||||
if listenAddress == "0.0.0.0" {
|
||||
listenAddress = "localhost"
|
||||
}
|
||||
go func() {
|
||||
// Determine whether to use HTTPS (TLS) or HTTP
|
||||
if config.Server.TLSCert != "" && config.Server.TLSKey != "" {
|
||||
|
||||
@@ -111,6 +111,8 @@ func withHashFileHelper(fn handleFunc) handleFunc {
|
||||
Expand: true,
|
||||
Content: getContent,
|
||||
ExtractEmbeddedSubtitles: settings.Config.Integrations.Media.ExtractEmbeddedSubtitles && link.ExtractEmbeddedSubtitles,
|
||||
ShowHidden: link.ShowHidden,
|
||||
FollowSymlinks: true,
|
||||
}, store.Access)
|
||||
if err != nil {
|
||||
logger.Errorf("error fetching file info for share. hash=%v path=%v error=%v", hash, path, err)
|
||||
|
||||
@@ -109,10 +109,11 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
|
||||
path = utils.JoinPathAsUnix(userScope, path)
|
||||
logger.Debugf("OnlyOffice user request: resolved path=%s", path)
|
||||
fileInfo, err := files.FileInfoFaster(utils.FileOptions{
|
||||
Username: d.user.Username,
|
||||
Path: path,
|
||||
Source: source,
|
||||
Expand: false,
|
||||
Username: d.user.Username,
|
||||
Path: path,
|
||||
Source: source,
|
||||
Expand: false,
|
||||
FollowSymlinks: true,
|
||||
}, store.Access)
|
||||
if err != nil {
|
||||
logger.Errorf("OnlyOffice: failed to get file info for source=%s, path=%s: %v", source, path, err)
|
||||
|
||||
+11
-7
@@ -238,9 +238,11 @@ func publicPreviewHandler(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
return http.StatusNotFound, fmt.Errorf("source not available")
|
||||
}
|
||||
fileInfo, err := FileInfoFasterFunc(utils.FileOptions{
|
||||
Path: utils.JoinPathAsUnix(d.share.Path, d.fileInfo.Path),
|
||||
Source: source,
|
||||
Metadata: true,
|
||||
Path: utils.JoinPathAsUnix(d.share.Path, d.fileInfo.Path),
|
||||
Source: source,
|
||||
Metadata: true,
|
||||
AlbumArt: true,
|
||||
ShowHidden: d.share.ShowHidden,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, fmt.Errorf("resource not available")
|
||||
@@ -314,10 +316,12 @@ func publicDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||
}
|
||||
|
||||
fileInfo, err := files.FileInfoFaster(utils.FileOptions{
|
||||
Username: d.user.Username,
|
||||
Path: indexPath,
|
||||
Source: source,
|
||||
Expand: false,
|
||||
Username: d.user.Username,
|
||||
FollowSymlinks: true,
|
||||
Path: indexPath,
|
||||
Source: source,
|
||||
Expand: false,
|
||||
ShowHidden: d.share.ShowHidden,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, fmt.Errorf("resource not available")
|
||||
|
||||
+5
-4
@@ -145,9 +145,10 @@ func addFile(path string, d *requestContext, tarWriter *tar.Writer, zipWriter *z
|
||||
|
||||
// Verify file exists
|
||||
_, err = files.FileInfoFaster(utils.FileOptions{
|
||||
Path: path,
|
||||
Source: source,
|
||||
Expand: false,
|
||||
Path: path,
|
||||
Source: source,
|
||||
Expand: false,
|
||||
FollowSymlinks: true,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -549,7 +550,7 @@ func computeArchiveSize(fileList []string, d *requestContext) (int64, error) {
|
||||
indexPath := idx.MakeIndexPath(realPath)
|
||||
info, ok := idx.GetReducedMetadata(indexPath, isDir)
|
||||
if !ok {
|
||||
info, err = idx.GetFsDirInfo(indexPath)
|
||||
info, err = idx.GetFsInfo(indexPath, false)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get file info for %s : %v", path, err)
|
||||
}
|
||||
|
||||
+18
-12
@@ -89,12 +89,14 @@ func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
getContent := r.URL.Query().Get("content") == "true"
|
||||
fileInfo, err := files.FileInfoFaster(utils.FileOptions{
|
||||
Username: d.user.Username,
|
||||
FollowSymlinks: true,
|
||||
Path: scopePath,
|
||||
Source: source,
|
||||
Expand: true,
|
||||
Content: getContent,
|
||||
Metadata: true,
|
||||
ExtractEmbeddedSubtitles: settings.Config.Integrations.Media.ExtractEmbeddedSubtitles,
|
||||
ShowHidden: d.user.ShowHidden,
|
||||
}, store.Access)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
@@ -172,10 +174,11 @@ func resourceDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestCon
|
||||
}
|
||||
userscope = strings.TrimRight(userscope, "/")
|
||||
fileInfo, err := files.FileInfoFaster(utils.FileOptions{
|
||||
Username: d.user.Username,
|
||||
Path: utils.JoinPathAsUnix(userscope, path),
|
||||
Source: source,
|
||||
Expand: false,
|
||||
Username: d.user.Username,
|
||||
Path: utils.JoinPathAsUnix(userscope, path),
|
||||
Source: source,
|
||||
Expand: false,
|
||||
ShowHidden: d.user.ShowHidden,
|
||||
}, store.Access)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
@@ -248,10 +251,11 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||
isDirParam := r.URL.Query().Get("isDir")
|
||||
isDir := isDirParam == "true" || strings.HasSuffix(unescapedPath, "/")
|
||||
fileOpts := utils.FileOptions{
|
||||
Username: d.user.Username,
|
||||
Path: path,
|
||||
Source: source,
|
||||
Expand: false,
|
||||
Username: d.user.Username,
|
||||
Path: path,
|
||||
Source: source,
|
||||
Expand: false,
|
||||
FollowSymlinks: true,
|
||||
}
|
||||
idx := indexing.GetIndex(source)
|
||||
if idx == nil {
|
||||
@@ -614,10 +618,12 @@ func patchAction(ctx context.Context, params patchActionParams) error {
|
||||
srcPath = strings.TrimSuffix(srcPath, "/")
|
||||
}
|
||||
fileInfo, err := files.FileInfoFaster(utils.FileOptions{
|
||||
Username: params.d.user.Username,
|
||||
Path: srcPath,
|
||||
Source: params.srcIndex,
|
||||
IsDir: params.isSrcDir,
|
||||
Username: params.d.user.Username,
|
||||
FollowSymlinks: true,
|
||||
Path: srcPath,
|
||||
Source: params.srcIndex,
|
||||
IsDir: params.isSrcDir,
|
||||
ShowHidden: params.d.user.ShowHidden,
|
||||
}, store.Access)
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -215,7 +215,32 @@ func (idx *Index) indexDirectory(adjustedPath string, config actionConfig) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (idx *Index) GetFsDirInfo(adjustedPath string) (*iteminfo.FileInfo, error) {
|
||||
func (idx *Index) GetFsInfo(adjustedPath string, followSymlinks bool) (*iteminfo.FileInfo, error) {
|
||||
// If not following symlinks, check if it's a symlink first
|
||||
if !followSymlinks {
|
||||
realPath := filepath.Join(idx.Path, adjustedPath)
|
||||
symlinkInfo, err := os.Lstat(realPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If it's a symlink, return info about the symlink itself (not the target)
|
||||
if symlinkInfo.Mode()&os.ModeSymlink != 0 {
|
||||
realSize, _ := idx.handleFile(symlinkInfo, adjustedPath, realPath)
|
||||
size := int64(realSize)
|
||||
fileInfo := iteminfo.FileInfo{
|
||||
Path: adjustedPath,
|
||||
ItemInfo: iteminfo.ItemInfo{
|
||||
Name: filepath.Base(strings.TrimSuffix(adjustedPath, "/")),
|
||||
Size: size,
|
||||
ModTime: symlinkInfo.ModTime(),
|
||||
Type: "symlink",
|
||||
},
|
||||
}
|
||||
return &fileInfo, nil
|
||||
}
|
||||
}
|
||||
|
||||
realPath, isDir, err := idx.GetRealPath(adjustedPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -396,7 +421,7 @@ func (idx *Index) GetDirInfo(dirInfo *os.File, stat os.FileInfo, realPath, adjus
|
||||
}
|
||||
}
|
||||
}
|
||||
if totalSize == 0 && idx.Config.Conditionals.ZeroSizeFolders {
|
||||
if totalSize == 0 && idx.Config.Conditionals.ZeroSizeFolders && combinedPath != "/" {
|
||||
return nil, errors.ErrNotIndexed
|
||||
}
|
||||
dirFileInfo := &iteminfo.FileInfo{
|
||||
@@ -524,6 +549,9 @@ func setFilePreviewFlags(fileInfo *iteminfo.ItemInfo, realPath string) {
|
||||
|
||||
// IsViewable checks if a path has viewable:true (allows FS access without indexing)
|
||||
func (idx *Index) IsViewable(isDir bool, adjustedPath string) bool {
|
||||
if adjustedPath == "/" {
|
||||
return true
|
||||
}
|
||||
rules := idx.Config.ResolvedConditionals
|
||||
|
||||
baseName := filepath.Base(strings.TrimSuffix(adjustedPath, "/"))
|
||||
|
||||
@@ -3,6 +3,7 @@ package indexing
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
@@ -36,6 +37,7 @@ type Scanner struct {
|
||||
|
||||
// Control
|
||||
stopChan chan struct{}
|
||||
stopOnce sync.Once // Ensures stop() is only executed once
|
||||
}
|
||||
|
||||
// start begins the scanner's main loop
|
||||
@@ -404,6 +406,9 @@ func (s *Scanner) removeSelf() {
|
||||
}
|
||||
|
||||
// stop gracefully stops the scanner
|
||||
// This method is idempotent and safe to call multiple times
|
||||
func (s *Scanner) stop() {
|
||||
close(s.stopChan)
|
||||
s.stopOnce.Do(func() {
|
||||
close(s.stopChan)
|
||||
})
|
||||
}
|
||||
@@ -3127,6 +3127,10 @@ const docTemplate = `{
|
||||
"shareURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"showHidden": {
|
||||
"description": "show hidden files in share (true = show, false = hide)",
|
||||
"type": "boolean"
|
||||
},
|
||||
"sidebarLinks": {
|
||||
"description": "customizable sidebar links",
|
||||
"type": "array",
|
||||
@@ -4415,6 +4419,10 @@ const docTemplate = `{
|
||||
"shareURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"showHidden": {
|
||||
"description": "show hidden files in share (true = show, false = hide)",
|
||||
"type": "boolean"
|
||||
},
|
||||
"sidebarLinks": {
|
||||
"description": "customizable sidebar links",
|
||||
"type": "array",
|
||||
@@ -4548,6 +4556,10 @@ const docTemplate = `{
|
||||
"shareURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"showHidden": {
|
||||
"description": "show hidden files in share (true = show, false = hide)",
|
||||
"type": "boolean"
|
||||
},
|
||||
"sidebarLinks": {
|
||||
"description": "customizable sidebar links",
|
||||
"type": "array",
|
||||
@@ -4687,6 +4699,10 @@ const docTemplate = `{
|
||||
"shareURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"showHidden": {
|
||||
"description": "show hidden files in share (true = show, false = hide)",
|
||||
"type": "boolean"
|
||||
},
|
||||
"sidebarLinks": {
|
||||
"description": "customizable sidebar links",
|
||||
"type": "array",
|
||||
|
||||
@@ -3116,6 +3116,10 @@
|
||||
"shareURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"showHidden": {
|
||||
"description": "show hidden files in share (true = show, false = hide)",
|
||||
"type": "boolean"
|
||||
},
|
||||
"sidebarLinks": {
|
||||
"description": "customizable sidebar links",
|
||||
"type": "array",
|
||||
@@ -4404,6 +4408,10 @@
|
||||
"shareURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"showHidden": {
|
||||
"description": "show hidden files in share (true = show, false = hide)",
|
||||
"type": "boolean"
|
||||
},
|
||||
"sidebarLinks": {
|
||||
"description": "customizable sidebar links",
|
||||
"type": "array",
|
||||
@@ -4537,6 +4545,10 @@
|
||||
"shareURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"showHidden": {
|
||||
"description": "show hidden files in share (true = show, false = hide)",
|
||||
"type": "boolean"
|
||||
},
|
||||
"sidebarLinks": {
|
||||
"description": "customizable sidebar links",
|
||||
"type": "array",
|
||||
@@ -4676,6 +4688,10 @@
|
||||
"shareURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"showHidden": {
|
||||
"description": "show hidden files in share (true = show, false = hide)",
|
||||
"type": "boolean"
|
||||
},
|
||||
"sidebarLinks": {
|
||||
"description": "customizable sidebar links",
|
||||
"type": "array",
|
||||
|
||||
@@ -121,6 +121,9 @@ definitions:
|
||||
type: string
|
||||
shareURL:
|
||||
type: string
|
||||
showHidden:
|
||||
description: show hidden files in share (true = show, false = hide)
|
||||
type: boolean
|
||||
sidebarLinks:
|
||||
description: customizable sidebar links
|
||||
items:
|
||||
@@ -1082,6 +1085,9 @@ definitions:
|
||||
type: string
|
||||
shareURL:
|
||||
type: string
|
||||
showHidden:
|
||||
description: show hidden files in share (true = show, false = hide)
|
||||
type: boolean
|
||||
sidebarLinks:
|
||||
description: customizable sidebar links
|
||||
items:
|
||||
@@ -1175,6 +1181,9 @@ definitions:
|
||||
type: string
|
||||
shareURL:
|
||||
type: string
|
||||
showHidden:
|
||||
description: show hidden files in share (true = show, false = hide)
|
||||
type: boolean
|
||||
sidebarLinks:
|
||||
description: customizable sidebar links
|
||||
items:
|
||||
@@ -1272,6 +1281,9 @@ definitions:
|
||||
type: string
|
||||
shareURL:
|
||||
type: string
|
||||
showHidden:
|
||||
description: show hidden files in share (true = show, false = hide)
|
||||
type: boolean
|
||||
sidebarLinks:
|
||||
description: customizable sidebar links
|
||||
items:
|
||||
|
||||
@@ -366,14 +366,8 @@ export default {
|
||||
this.submit();
|
||||
},
|
||||
getRelative(path) {
|
||||
// double encode # to fix issue with # in path
|
||||
// replace all # with %23
|
||||
path = path.replaceAll('#', "%23");
|
||||
if (path.startsWith("/")) {
|
||||
path = path.slice(1); // remove leading slash
|
||||
}
|
||||
const context = url.removeTrailingSlash(this.getContext)
|
||||
const encodedPath = encodeURIComponent(context + "/" + path).replaceAll("%2F", "/");
|
||||
const encodedPath = url.encodePath(context + "/" + path)
|
||||
let fullpath = encodedPath;
|
||||
fullpath = globalVars.baseURL+"files/" + this.selectedSource + encodedPath;
|
||||
return fullpath;
|
||||
|
||||
@@ -7,25 +7,25 @@
|
||||
>
|
||||
<i
|
||||
v-if="hasMotion && isFile"
|
||||
class="material-icons"
|
||||
class="material-icons icon-optimized"
|
||||
:class="{ larger: showLarger, smaller: !showLarger }"
|
||||
>animation</i
|
||||
>
|
||||
<i
|
||||
v-else-if="!isFile"
|
||||
class="material-icons"
|
||||
class="material-icons icon-optimized"
|
||||
:class="{ larger: showLarger, smaller: !showLarger }"
|
||||
>folder</i
|
||||
>
|
||||
<img
|
||||
:key="imageTargetSrc"
|
||||
:src="imageDisplaySrc"
|
||||
class="icon"
|
||||
class="icon icon-optimized"
|
||||
ref="thumbnail"
|
||||
/>
|
||||
</span>
|
||||
<span v-else>
|
||||
<i :class="[classes, { active: active, clickable: clickable }]" class="icon"> {{ materialIcon }} </i>
|
||||
<i :class="[classes, { active: active, clickable: clickable }]" class="icon icon-optimized"> {{ materialIcon }} </i>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -93,17 +93,17 @@ export default {
|
||||
return false;
|
||||
}
|
||||
const simpleType = this.getIconForType().simpleType;
|
||||
if (simpleType === "video" && !state.user.preview?.video) {
|
||||
if (simpleType === "video" && !getters.previewPerms().video) {
|
||||
return false;
|
||||
}
|
||||
if (simpleType === "image" && !state.user.preview?.image) {
|
||||
if (simpleType === "image" && !getters.previewPerms().image) {
|
||||
return false;
|
||||
}
|
||||
// office files
|
||||
if ((simpleType === "document" || simpleType === "text") && !state.user.preview?.office) {
|
||||
if ((simpleType === "document" || simpleType === "text") && !getters.previewPerms().office) {
|
||||
return false;
|
||||
}
|
||||
if (!state.user.preview.folder && this.mimetype == "directory") {
|
||||
if (!getters.previewPerms().folder && this.mimetype == "directory") {
|
||||
return false;
|
||||
}
|
||||
return this.imageState !== 'error' && !this.disablePreviewExt && !this.officeFileDisabled
|
||||
@@ -153,7 +153,7 @@ export default {
|
||||
return this.imageTargetSrc;
|
||||
},
|
||||
showLargeIcon() {
|
||||
return getters.viewMode() === "gallery" && state.user.preview.highQuality;
|
||||
return getters.viewMode() === "gallery" && getters.previewPerms().highQuality;
|
||||
},
|
||||
showLarger() {
|
||||
return getters.viewMode() === "gallery" || getters.viewMode() === "normal";
|
||||
@@ -161,10 +161,9 @@ export default {
|
||||
hasMotion() {
|
||||
return (
|
||||
this.getIconForType().simpleType === "video" &&
|
||||
state.user.preview?.video &&
|
||||
getters.previewPerms().video &&
|
||||
globalVars.mediaAvailable &&
|
||||
// @ts-ignore
|
||||
state.user.preview.motionVideoPreview
|
||||
getters.previewPerms().motionVideoPreview
|
||||
);
|
||||
},
|
||||
isMaterialIcon() {
|
||||
@@ -206,8 +205,7 @@ export default {
|
||||
if (this.imageState == "loaded") {
|
||||
mutations.setPreviewSource(imageUrl);
|
||||
}
|
||||
// @ts-ignore
|
||||
if (!state.user.preview.motionVideoPreview || !this.hasMotion) {
|
||||
if (!getters.previewPerms().motionVideoPreview || !this.hasMotion) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -226,7 +224,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
// Set the thumbnail or popup preview
|
||||
if (state.user.preview.popup) {
|
||||
if (getters.previewPerms().popup) {
|
||||
mutations.setPreviewSource(sequence[index]);
|
||||
} else {
|
||||
this.currentThumbnail = sequence[index];
|
||||
@@ -272,7 +270,6 @@ export default {
|
||||
showLargeIcon() {
|
||||
this.updateImageTargetSrc();
|
||||
},
|
||||
// UPDATED: Load image when URL changes, let hasPreviewImage control display
|
||||
imageTargetSrc: {
|
||||
handler(newSrc) {
|
||||
// Check all conditions EXCEPT imageState to avoid circular dependency
|
||||
@@ -283,17 +280,17 @@ export default {
|
||||
return;
|
||||
}
|
||||
const simpleType = this.getIconForType().simpleType;
|
||||
// Check user preview settings
|
||||
if (simpleType === "video" && !state.user.preview?.video) {
|
||||
// Check preview settings (share or user)
|
||||
if (simpleType === "video" && !getters.previewPerms().video) {
|
||||
return;
|
||||
}
|
||||
if (simpleType === "image" && !state.user.preview?.image) {
|
||||
if (simpleType === "image" && !getters.previewPerms().image) {
|
||||
return;
|
||||
}
|
||||
if ((simpleType === "document" || simpleType === "text") && !state.user.preview?.office) {
|
||||
if ((simpleType === "document" || simpleType === "text") && !getters.previewPerms().office) {
|
||||
return;
|
||||
}
|
||||
if (!state.user.preview.folder && this.mimetype === "directory") {
|
||||
if (!getters.previewPerms().folder && this.mimetype === "directory") {
|
||||
return;
|
||||
}
|
||||
if (this.disablePreviewExt || this.officeFileDisabled) {
|
||||
@@ -319,6 +316,13 @@ export default {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Performance optimization for icons */
|
||||
.icon-optimized {
|
||||
will-change: auto;
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.larger {
|
||||
position: absolute;
|
||||
opacity: 0.5;
|
||||
@@ -354,6 +358,9 @@ export default {
|
||||
border-radius: 0.2em;
|
||||
padding: 0.1em;
|
||||
background: var(--iconBackground);
|
||||
/* Performance optimization */
|
||||
will-change: auto;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
.icon.active {
|
||||
background: var(--background);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
hiddenFile: isHiddenNotSelected && this && !this.isDraggedOver,
|
||||
'half-selected': isDraggedOver,
|
||||
'drag-hover': isDraggedOver,
|
||||
'out-of-view': !isInView && !isSelected,
|
||||
}"
|
||||
:id="getID"
|
||||
role="button"
|
||||
@@ -89,6 +90,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
isThumbnailInView: false,
|
||||
isInView: false, // Track if item is in viewport
|
||||
touches: 0,
|
||||
touchStartX: 0,
|
||||
touchStartY: 0,
|
||||
@@ -96,6 +98,7 @@ export default {
|
||||
isSwipe: false,
|
||||
isDraggedOver: false,
|
||||
contextTimeout: null,
|
||||
observer: null, // Store observer reference
|
||||
};
|
||||
},
|
||||
props: [
|
||||
@@ -209,14 +212,21 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
// Prevent default navigation for left-clicks
|
||||
const observer = new IntersectionObserver(this.handleIntersect, {
|
||||
// Set up IntersectionObserver with larger margins for smoother rendering
|
||||
this.observer = new IntersectionObserver(this.handleIntersect, {
|
||||
root: null,
|
||||
rootMargin: "0px",
|
||||
threshold: 0.1, // Adjust threshold as needed
|
||||
rootMargin: "1500px", // Start rendering 1500px before entering viewport for smooth scrolling
|
||||
threshold: 0,
|
||||
});
|
||||
|
||||
observer.observe(this.$el);
|
||||
this.observer.observe(this.$el);
|
||||
},
|
||||
beforeUnmount() {
|
||||
// Clean up observer
|
||||
if (this.observer) {
|
||||
this.observer.disconnect();
|
||||
this.observer = null;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** @param {MouseEvent} event */
|
||||
@@ -287,12 +297,12 @@ export default {
|
||||
* @param {IntersectionObserverEntry[]} entries
|
||||
* @param {IntersectionObserver} observer
|
||||
*/
|
||||
handleIntersect(entries, observer) {
|
||||
handleIntersect(entries) {
|
||||
entries.forEach((entry) => {
|
||||
// Update both view state and thumbnail state
|
||||
this.isInView = entry.isIntersecting;
|
||||
if (entry.isIntersecting) {
|
||||
this.isThumbnailInView = true;
|
||||
// Stop observing once thumbnail is in view
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -634,6 +644,22 @@ export default {
|
||||
-webkit-touch-callout: none; /* Disable the default long press preview */
|
||||
}
|
||||
|
||||
/* Disable transitions and hide content for out-of-view items for better performance */
|
||||
.listing-item.out-of-view {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.listing-item.out-of-view * {
|
||||
transition: none !important;
|
||||
opacity: 0 !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
/* Ensure items maintain their height even when content is hidden */
|
||||
.listing-item > div {
|
||||
min-height: 1em; /* Forces layout calculation even with hidden content */
|
||||
}
|
||||
|
||||
.hiddenFile {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@@ -202,6 +202,8 @@
|
||||
:description="$t('share.keepAfterExpirationDescription')" />
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="disableThumbnails"
|
||||
:name="$t('share.disableThumbnails')" :description="$t('share.disableThumbnailsDescription')" />
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="showHidden"
|
||||
:name="$t('profileSettings.showHiddenFiles')" :description="$t('profileSettings.showHiddenFilesDescription')" />
|
||||
<ToggleSwitch class="item" v-model="disableNavButtons" :name="$t('share.hideNavButtons')"
|
||||
:description="$t('share.hideNavButtonsDescription')" />
|
||||
<ToggleSwitch class="item" v-model="disableShareCard" :name="$t('share.disableShareCard')"
|
||||
@@ -367,6 +369,7 @@ export default {
|
||||
maxBandwidth: "",
|
||||
disableFileViewer: false,
|
||||
disableThumbnails: false,
|
||||
showHidden: false,
|
||||
enableAllowedUsernames: false,
|
||||
allowedUsernames: "",
|
||||
keepAfterExpiration: false,
|
||||
@@ -485,6 +488,7 @@ export default {
|
||||
this.disableAnonymous = this.link.disableAnonymous || false;
|
||||
this.disableThumbnails = this.link.disableThumbnails || false;
|
||||
this.disableFileViewer = this.link.disableFileViewer || false;
|
||||
this.showHidden = this.link.showHidden || false;
|
||||
this.enableAllowedUsernames = Array.isArray(this.link.allowedUsernames) && this.link.allowedUsernames.length > 0;
|
||||
this.allowedUsernames = this.enableAllowedUsernames ? this.link.allowedUsernames.join(", ") : "";
|
||||
this.keepAfterExpiration = this.link.keepAfterExpiration || false;
|
||||
@@ -600,6 +604,7 @@ export default {
|
||||
shareTheme: this.shareTheme,
|
||||
disableFileViewer: this.disableFileViewer,
|
||||
disableThumbnails: this.disableThumbnails,
|
||||
showHidden: this.showHidden,
|
||||
allowedUsernames: this.enableAllowedUsernames ? this.allowedUsernames.split(',').map(u => u.trim()) : [],
|
||||
keepAfterExpiration: this.keepAfterExpiration,
|
||||
hash: '',
|
||||
@@ -698,6 +703,7 @@ export default {
|
||||
this.disableAnonymous = link.disableAnonymous || false;
|
||||
this.disableThumbnails = link.disableThumbnails || false;
|
||||
this.disableFileViewer = link.disableFileViewer || false;
|
||||
this.showHidden = link.showHidden || false;
|
||||
this.enableAllowedUsernames = Array.isArray(link.allowedUsernames) && link.allowedUsernames.length > 0;
|
||||
this.allowedUsernames = this.enableAllowedUsernames ? link.allowedUsernames.join(", ") : "";
|
||||
this.keepAfterExpiration = link.keepAfterExpiration || false;
|
||||
|
||||
@@ -44,7 +44,11 @@ body.rtl #listingView {
|
||||
background: var(--item-secondary);
|
||||
color: var(--textPrimary);
|
||||
border-color: var(--divider);
|
||||
background: var(--surfacePrimary)
|
||||
background: var(--surfacePrimary);
|
||||
/* Performance optimizations */
|
||||
contain: layout style paint;
|
||||
content-visibility: auto;
|
||||
will-change: auto;
|
||||
}
|
||||
|
||||
.listing-item div:last-of-type {
|
||||
@@ -66,6 +70,8 @@ body.rtl #listingView {
|
||||
.listing-item i {
|
||||
font-size: 4em;
|
||||
vertical-align: bottom;
|
||||
/* Performance optimization for icon rendering */
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.listing-item img {
|
||||
@@ -166,6 +172,11 @@ body.rtl #listingView {
|
||||
transition: width 0.25s ease-out, height 0.25s ease-out, box-shadow 0.2s ease-in-out, transform 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
/* Disable transitions during resize for better performance */
|
||||
#listingView.resizing .listing-item {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
#listingView.gallery .listing-item .text {
|
||||
color: white;
|
||||
text-shadow: 0 0 2px black;
|
||||
|
||||
@@ -205,7 +205,7 @@ export const getters = {
|
||||
if (getters.currentPromptName() && !getters.isStickySidebar()) {
|
||||
visible = false
|
||||
}
|
||||
if (previewViews.includes(cv) && !state.user.preview?.disableHideSidebar) {
|
||||
if (previewViews.includes(cv) && !getters.previewPerms().disableHideSidebar) {
|
||||
visible = false
|
||||
}
|
||||
if (state.shareInfo?.singleFileShare) {
|
||||
@@ -555,5 +555,37 @@ export const getters = {
|
||||
admin: state.user?.permissions?.admin,
|
||||
api: state.user?.permissions?.api,
|
||||
};
|
||||
},
|
||||
previewPerms: () => {
|
||||
if (getters.isShare()) {
|
||||
// For shares, use defaults for preview settings (shares don't have per-share preview config)
|
||||
return {
|
||||
video: state.user?.preview?.video ?? true,
|
||||
image: state.user?.preview?.image ?? true,
|
||||
office: state.user?.preview?.office ?? true,
|
||||
folder: state.user?.preview?.folder ?? true,
|
||||
popup: state.user?.preview?.popup ?? true,
|
||||
highQuality: state.user?.preview?.highQuality ?? false,
|
||||
motionVideoPreview: state.user?.preview?.motionVideoPreview ?? false,
|
||||
disableHideSidebar: state.user?.preview?.disableHideSidebar ?? false,
|
||||
autoplayMedia: state.user?.preview?.autoplayMedia ?? false,
|
||||
defaultMediaPlayer: false,
|
||||
showHidden: state.shareInfo?.showHidden !== undefined ? state.shareInfo.showHidden : false,
|
||||
};
|
||||
}
|
||||
// For regular users, use their preview settings -- unless is share
|
||||
return {
|
||||
video: state.user?.preview?.video ?? true,
|
||||
image: state.user?.preview?.image ?? true,
|
||||
office: state.user?.preview?.office ?? true,
|
||||
folder: state.user?.preview?.folder ?? true,
|
||||
popup: state.user?.preview?.popup ?? true,
|
||||
highQuality: state.user?.preview?.highQuality ?? false,
|
||||
motionVideoPreview: state.user?.preview?.motionVideoPreview ?? false,
|
||||
disableHideSidebar: state.user?.preview?.disableHideSidebar ?? false,
|
||||
autoplayMedia: state.user?.preview?.autoplayMedia ?? false,
|
||||
defaultMediaPlayer: state.user?.preview?.defaultMediaPlayer ?? false,
|
||||
showHidden: false, // Backend handles this now, but kept for API compatibility
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -499,9 +499,6 @@ export const mutations = {
|
||||
emitStateChanged();
|
||||
return
|
||||
}
|
||||
if (!state.user.showHidden) {
|
||||
value.items = value.items.filter((item) => !item.hidden);
|
||||
}
|
||||
let sortby = "name"
|
||||
let asc = true
|
||||
const sorting = getters.sorting();
|
||||
|
||||
@@ -30,7 +30,6 @@ import MarkdownViewer from "./files/MarkdownViewer.vue";
|
||||
import { state, mutations, getters } from "@/store";
|
||||
import { url } from "@/utils";
|
||||
import router from "@/router";
|
||||
import { globalVars } from "@/utils/constants";
|
||||
import { extractSourceFromPath } from "@/utils/url";
|
||||
|
||||
export default {
|
||||
@@ -391,16 +390,9 @@ export default {
|
||||
|
||||
// Clear share data when accessing files
|
||||
mutations.clearShareData();
|
||||
|
||||
const routePath = url.removeTrailingSlash(getters.routePath(`${globalVars.baseURL}files`));
|
||||
const rootRoute =
|
||||
routePath == "/files" ||
|
||||
routePath == "/files/" ||
|
||||
routePath == "" ||
|
||||
routePath == "/";
|
||||
|
||||
const routePath = url.removeTrailingSlash(getters.routePath());
|
||||
// lets redirect if multiple sources and user went to /files/
|
||||
if (rootRoute) {
|
||||
if (routePath == "/files") {
|
||||
// Check if user has custom sidebar links with sources
|
||||
let targetPath = `/files/${state.sources.current}`;
|
||||
for (const link of state.user?.sidebarLinks || []) {
|
||||
|
||||
@@ -132,7 +132,7 @@ export default {
|
||||
if (!state.user || state.user?.username == "") {
|
||||
return false;
|
||||
}
|
||||
return state.user.preview.popup;
|
||||
return getters.previewPerms().popup;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
||||
@@ -219,6 +219,10 @@ export default {
|
||||
rectangleEnd: { x: 0, y: 0 },
|
||||
rectangleSelection: [],
|
||||
cssVariables: {},
|
||||
rafId: null, // For requestAnimationFrame
|
||||
selectionUpdatePending: false, // Flag to batch updates
|
||||
isResizing: false, // Track resize state
|
||||
resizeTimeout: null, // Timeout for resize end detection
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
@@ -490,10 +494,10 @@ export default {
|
||||
window.addEventListener("click", this.clickClear);
|
||||
window.addEventListener("keyup", this.clearCtrKey);
|
||||
window.addEventListener("dragover", this.preventDefault);
|
||||
document.addEventListener('mousemove', this.updateRectangleSelection);
|
||||
document.addEventListener('mousemove', this.updateRectangleSelection, { passive: true });
|
||||
document.addEventListener('mouseup', this.endRectangleSelection);
|
||||
this.$el.addEventListener('mousedown', this.startRectangleSelection);
|
||||
this.$el.addEventListener("touchmove", this.handleTouchMove);
|
||||
this.$el.addEventListener("touchmove", this.handleTouchMove, { passive: true });
|
||||
|
||||
this.$el.addEventListener("contextmenu", this.openContext);
|
||||
// Adjust contextmenu listener based on browser
|
||||
@@ -517,6 +521,16 @@ export default {
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
// Cancel any pending animation frames
|
||||
if (this.rafId) {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
this.rafId = null;
|
||||
}
|
||||
if (this.resizeTimeout) {
|
||||
clearTimeout(this.resizeTimeout);
|
||||
this.resizeTimeout = null;
|
||||
}
|
||||
|
||||
// Remove event listeners before destroying this page.
|
||||
window.removeEventListener("keydown", this.keyEvent);
|
||||
window.removeEventListener("resize", this.windowsResize);
|
||||
@@ -1057,9 +1071,32 @@ export default {
|
||||
showMultipleSelection();
|
||||
},
|
||||
windowsResize: throttle(function () {
|
||||
// Mark as resizing to disable transitions
|
||||
if (!this.isResizing) {
|
||||
this.isResizing = true;
|
||||
if (this.$refs.listingView) {
|
||||
this.$refs.listingView.classList.add('resizing');
|
||||
}
|
||||
}
|
||||
|
||||
// Clear existing timeout
|
||||
if (this.resizeTimeout) {
|
||||
clearTimeout(this.resizeTimeout);
|
||||
}
|
||||
|
||||
// Do the resize work
|
||||
this.colunmsResize();
|
||||
this.width = window.innerWidth;
|
||||
mutations.setMobile();
|
||||
|
||||
// Re-enable transitions after resize is complete
|
||||
this.resizeTimeout = setTimeout(() => {
|
||||
this.isResizing = false;
|
||||
if (this.$refs.listingView) {
|
||||
this.$refs.listingView.classList.remove('resizing');
|
||||
}
|
||||
}, 150); // Wait 150ms after last resize event
|
||||
|
||||
// Listing element is not displayed
|
||||
if (this.$refs.listingView == null) return;
|
||||
}, 100),
|
||||
@@ -1175,13 +1212,27 @@ export default {
|
||||
y: event.clientY - listingRect.top
|
||||
};
|
||||
|
||||
this.updateSelectedItemsInRectangle(event.ctrlKey || event.metaKey);
|
||||
// Use requestAnimationFrame to batch updates
|
||||
if (!this.selectionUpdatePending) {
|
||||
this.selectionUpdatePending = true;
|
||||
this.rafId = requestAnimationFrame(() => {
|
||||
this.updateSelectedItemsInRectangle(event.ctrlKey || event.metaKey);
|
||||
this.selectionUpdatePending = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
endRectangleSelection(event) {
|
||||
if (!this.isRectangleSelecting) return;
|
||||
|
||||
// Cancel any pending animation frame
|
||||
if (this.rafId) {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
this.rafId = null;
|
||||
}
|
||||
|
||||
this.isRectangleSelecting = false;
|
||||
this.selectionUpdatePending = false;
|
||||
this.updateSelectedItemsInRectangle(event.ctrlKey || event.metaKey);
|
||||
|
||||
// Clear rectangle after a short delay
|
||||
@@ -1205,8 +1256,8 @@ export default {
|
||||
|
||||
const rectangleSelectedIndexes = [];
|
||||
|
||||
// Get all item elements
|
||||
const itemElements = this.$el.querySelectorAll('.listing-item');
|
||||
// Get all item elements - use querySelectorAll with specific selector for better performance
|
||||
const itemElements = this.$el.querySelectorAll('.listing-item[data-index]');
|
||||
|
||||
itemElements.forEach((element) => {
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
@@ -1233,7 +1284,7 @@ export default {
|
||||
}
|
||||
});
|
||||
|
||||
// Update selection based on modifier keys (ctrl/meta)
|
||||
// Batch DOM updates to minimize reflows
|
||||
if (isAdditive) {
|
||||
// only add more items to the current selection without reset selection
|
||||
const newSelection = [...state.selected];
|
||||
|
||||
@@ -103,10 +103,10 @@ export default {
|
||||
return this.previewType == 'image' || this.pdfConvertable;
|
||||
},
|
||||
autoPlay() {
|
||||
return state.user.preview.autoplayMedia;
|
||||
return getters.previewPerms().autoplayMedia;
|
||||
},
|
||||
useDefaultMediaPlayer() {
|
||||
return state.user.preview.defaultMediaPlayer === true;
|
||||
return getters.previewPerms().defaultMediaPlayer === true;
|
||||
},
|
||||
isMobileSafari() {
|
||||
const userAgent = window.navigator.userAgent;
|
||||
|
||||
@@ -24,7 +24,7 @@ test("share file works", async ({ page, checkForErrors, context }) => {
|
||||
});
|
||||
|
||||
test("share download single file", async ({ page, checkForErrors, context }) => {
|
||||
await page.goto("/files/files/");
|
||||
await page.goto("/files/");
|
||||
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
|
||||
|
||||
const shareHash = await page.evaluate(() => localStorage.getItem('shareHash'));
|
||||
|
||||
Reference in new issue
Block a user