From 2c78ff80e87cb886276040087c2850a1f7f40b6a Mon Sep 17 00:00:00 2001 From: 3dtours Date: Mon, 28 Sep 2026 19:34:37 +0700 Subject: [PATCH] web: fold a folder's rows without folding its frames --- docker/frontend/scripts/library-check.mjs | 22 +++++++++++ docker/frontend/src/Library.tsx | 48 ++++++++++++++++++++--- docker/frontend/src/styles/app.css | 17 +++++++- 3 files changed, 81 insertions(+), 6 deletions(-) diff --git a/docker/frontend/scripts/library-check.mjs b/docker/frontend/scripts/library-check.mjs index f9ecb68..b6bc1fb 100644 --- a/docker/frontend/scripts/library-check.mjs +++ b/docker/frontend/scripts/library-check.mjs @@ -229,6 +229,28 @@ step( JSON.stringify(nodes) ); +// A row with children folds its subtree on the same click that opens it, and the +// frames it holds stay in the strip — the tree narrows, the pictures do not. +await page.click('[data-key="lib-node-CheckRoll"]'); +await page.waitForFunction(() => document.querySelectorAll('[data-key^="lib-node-"]').length === 1, null, { + timeout: 10_000, + polling: 50, +}); +const foldedThumbs = await page.$$eval(thumbs, (els) => els.length); +const foldedMeta = await page.locator('[data-key="lib-stage-meta"]').innerText(); +const shut = await page.getAttribute('[data-key="lib-node-CheckRoll"]', 'aria-expanded'); +step( + 'a folder folds its subtree and keeps its frames in the strip', + foldedThumbs === 3 && shut === 'false' && /P1010256/.test(foldedMeta), + `${foldedThumbs} tiles, aria-expanded=${shut}, stage ${foldedMeta.split('\n')[0]}` +); +await page.click('[data-key="lib-node-CheckRoll"]'); +await page.waitForFunction(() => document.querySelectorAll('[data-key^="lib-node-"]').length === 3, null, { + timeout: 10_000, + polling: 50, +}); +step('the same click draws it open again', true, '3 rows'); + // Opening the subfolder redraws the strip and the frame above it. await page.click('[data-key="lib-node-CheckRoll/2026"]'); await page.waitForFunction((sel) => document.querySelectorAll(sel).length === 2, thumbs, { timeout: 30_000 }); diff --git a/docker/frontend/src/Library.tsx b/docker/frontend/src/Library.tsx index a7bc595..a24036c 100644 --- a/docker/frontend/src/Library.tsx +++ b/docker/frontend/src/Library.tsx @@ -26,9 +26,8 @@ import { // thumbnails, and the RAW itself is read only when a frame is opened in the // studio (`/app?lib=`). // -// ponytail: the tree does not collapse, and there is no search, rating or flag. -// A roll of date folders is three levels deep at most; add a collapsing node or -// a filter once the column stops being scrollable in practice. +// ponytail: no search, rating or flag. A roll of date folders is three levels +// deep at most; add a filter once the column stops being scrollable in practice. interface Node { // The tree's own key: a folder name, or `folder/sub/dir`. key: string; @@ -73,6 +72,9 @@ export function Library() { // views (the frame, or every thumbnail of the open node) is on. The open node // is picked up where the last visit left it. const [node, setNode] = useState(() => remembered(NODE_KEY)); + // Rows whose children are folded away. A collapse is a way of looking at the + // tree, not a property of the roll, so it is not remembered between visits. + const [folded, setFolded] = useState>(() => new Set()); const [up, setUp] = useState(null); const [view, setView] = useState<'preview' | 'grid'>('preview'); const [colWidth, setColWidth] = useState(() => { @@ -285,6 +287,24 @@ export function Library() { return out; }, [folders, photos]); + // A row with anything under it is the one that folds: its key is the prefix of + // its children's, in the path order the rows come in. + const parents = useMemo( + () => new Set(nodes.filter((n) => n.depth > 0).map((n) => n.key.slice(0, n.key.lastIndexOf('/')))), + [nodes] + ); + + // What the column draws: a folded row takes its whole subtree with it. + const rows = useMemo( + () => + nodes.filter((n) => { + const parts = n.key.split('/'); + for (let i = 1; i < parts.length; i++) if (folded.has(parts.slice(0, i).join('/'))) return false; + return true; + }), + [nodes, folded] + ); + // The folder the open node belongs to: its name titles the column, and its // handle is what the menu acts on. const root = folders.find((f) => f.name === rootName) ?? null; @@ -423,11 +443,12 @@ export function Library() { {root?.label || root?.name} - {nodes.map((n) => { + {rows.map((n) => { const folder = folders.find((f) => f.name === n.key.split('/')[0]) ?? null; // A roll being read says so on its own row: while a scan runs // the toolbar's progress line is nowhere near the eye. const scanning = !!folder && progress?.folder === folder.name; + const under = parents.has(n.key); return (