From 3d12635a626b36de90b5ca8bcb04d1b045476be8 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Thu, 8 Oct 2026 17:37:06 +0700 Subject: [PATCH] fix(library): strictly enforce single folder photo filtering unless subfolder option is enabled --- docker/frontend/src/Library.tsx | 42 +++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/docker/frontend/src/Library.tsx b/docker/frontend/src/Library.tsx index 1b597df..a1dece2 100644 --- a/docker/frontend/src/Library.tsx +++ b/docker/frontend/src/Library.tsx @@ -960,13 +960,33 @@ function Catalogue() { go(`/app?lib=${encodeURIComponent(photo.id)}`); }, [t]); - const rootName = node ? node.split('/')[0] : null; + const rootFolder = useMemo(() => { + if (!node) return folders[0] ?? null; + const normNode = (node || '').replace(/\\/g, '/'); + return ( + folders.find((f) => { + const normF = (f.name || '').replace(/\\/g, '/'); + return normNode === normF || normNode.startsWith(`${normF}/`); + }) ?? folders[0] ?? null + ); + }, [folders, node]); + + const rootName = rootFolder ? rootFolder.name : null; // A row's folder as the walk spells it: '' for the picked folder itself, the // path under it for a subfolder, and nothing for a row belonging to another // folder — that one is not a folder of this roll. - const relOf = (key: string): string | null => - !rootName ? null : key === rootName ? '' : key.startsWith(`${rootName}/`) ? `${key.slice(rootName.length + 1)}/` : null; + const relOf = useCallback( + (key: string): string | null => { + if (!rootName) return null; + const normRoot = normPath(rootName); + const normKey = normPath(key); + if (normKey === normRoot) return ''; + if (normKey.startsWith(`${normRoot}/`)) return `${normKey.slice(normRoot.length + 1)}/`; + return null; + }, + [rootName] + ); // The tree: one row per picked folder, then one per subfolder, indented by its // depth — the folders the walk has named, whether or not a frame has been read @@ -1089,20 +1109,22 @@ function Catalogue() { // it sits in, so the screen never opens on a node that is not drawn. useEffect(() => { if (!loaded || !node || nodes.some((n) => n.key === node)) return; - setNode(nodes.find((n) => n.key === node.split('/')[0])?.key ?? nodes[0]?.key ?? null); - }, [loaded, node, nodes]); + setNode(nodes.find((n) => n.key === rootName || n.key === normPath(rootName ?? ''))?.key ?? nodes[0]?.key ?? null); + }, [loaded, node, nodes, rootName]); // What the open node draws: its own frames, and — when the reader has asked // for the branch — the ones in every subfolder under it. const shown = useMemo(() => { if (!rootName) return photos; - const normRoot = rootName.replace(/\\/g, '/'); - const normNode = (node ?? '').replace(/\\/g, '/'); + const normRoot = normPath(rootName); + const normNode = normPath(node ?? ''); const dir = normNode === normRoot ? '' : normNode.slice(normRoot.length + 1); - const below = dir === '' ? (p: LibraryPhoto) => (p.dir ?? '').replace(/\\/g, '/') !== '' : (p: LibraryPhoto) => (p.dir ?? '').replace(/\\/g, '/').startsWith(`${dir}/`); + const below = dir === '' + ? (p: LibraryPhoto) => normPath(p.dir ?? '') !== '' + : (p: LibraryPhoto) => normPath(p.dir ?? '').startsWith(`${dir}/`); return photos.filter((p) => { - const pFolder = (p.folder ?? '').replace(/\\/g, '/'); - const pDir = (p.dir ?? '').replace(/\\/g, '/'); + const pFolder = normPath(p.folder ?? ''); + const pDir = normPath(p.dir ?? ''); return pFolder === normRoot && (pDir === dir || (deep && below(p))); }); }, [deep, node, photos, rootName]);