Compare commits

...
10 Commits
28 changed files with 357 additions and 117 deletions

No files matched your search

+18
View File
@@ -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**:
+37 -23
View File
@@ -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)
+2
View File
@@ -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
}
+1
View File
@@ -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
+1
View File
@@ -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,
}
}
-3
View File
@@ -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 != "" {
+2
View File
@@ -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)
+5 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+30 -2
View File
@@ -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, "/"))
+6 -1
View File
@@ -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)
})
}
+16
View File
@@ -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",
+16
View File
@@ -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",
+12
View File
@@ -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:
+1 -7
View File
@@ -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;
+28 -21
View File
@@ -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);
+34 -8
View File
@@ -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;
+12 -1
View File
@@ -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;
+33 -1
View File
@@ -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
};
}
};
-3
View File
@@ -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();
+2 -10
View File
@@ -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 || []) {
+1 -1
View File
@@ -132,7 +132,7 @@ export default {
if (!state.user || state.user?.username == "") {
return false;
}
return state.user.preview.popup;
return getters.previewPerms().popup;
},
},
watch: {
+57 -6
View File
@@ -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];
+2 -2
View File
@@ -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'));