web: walk a roll layer by layer, and let the strip drop the branch
This commit is contained in:
@@ -75,6 +75,10 @@ export function Library() {
|
||||
// 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<Set<string>>(() => new Set());
|
||||
// Whether the strip draws the whole branch under the open node or only the
|
||||
// node itself. Deep is how a roll reads — its top level holds no file of its
|
||||
// own — and shallow is what a folder of folders is for.
|
||||
const [deep, setDeep] = useState(true);
|
||||
const [up, setUp] = useState<string | null>(null);
|
||||
const [view, setView] = useState<'preview' | 'grid'>('preview');
|
||||
const [colWidth, setColWidth] = useState(() => {
|
||||
@@ -89,6 +93,10 @@ export function Library() {
|
||||
// one more folder.
|
||||
const [menu, setMenu] = useState<{ folder: LibraryFolder | null; x: number; y: number } | null>(null);
|
||||
const stopRef = useRef(false);
|
||||
// The folder the reader last clicked, as the walk spells it ('' for the picked
|
||||
// folder, `2026/04/` under it): the scan in flight pulls it to the front of
|
||||
// its queue, so the branch in front of the eye is read first.
|
||||
const jumpRef = useRef<string | null>(null);
|
||||
// Where the drag of the column divider started, and how wide the column was
|
||||
// then — the pointer only ever reports a position, not a distance moved.
|
||||
const grip = useRef<{ x: number; w: number } | null>(null);
|
||||
@@ -166,7 +174,7 @@ export function Library() {
|
||||
setBusy(true);
|
||||
setNote(null);
|
||||
try {
|
||||
const result = await scanFolder(folder, setProgress, () => stopRef.current);
|
||||
const result = await scanFolder(folder, setProgress, () => stopRef.current, () => jumpRef.current);
|
||||
setNote(t('lib.scanned', { folder: folder.name, added: result.added, total: result.total }));
|
||||
await reload();
|
||||
} catch {
|
||||
@@ -251,6 +259,12 @@ export function Library() {
|
||||
|
||||
const rootName = node ? node.split('/')[0] : 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;
|
||||
|
||||
// The tree: one row per picked folder, then one per subfolder a frame sits in,
|
||||
// indented by its depth, each carrying how many frames it holds at or under it.
|
||||
const nodes = useMemo<Node[]>(() => {
|
||||
@@ -316,13 +330,15 @@ export function Library() {
|
||||
setNode(nodes.find((n) => n.key === node.split('/')[0])?.key ?? nodes[0]?.key ?? null);
|
||||
}, [loaded, node, nodes]);
|
||||
|
||||
// What the open node draws: its own frames, and the ones in every subfolder
|
||||
// under it — a roll's top level usually holds no file of its own.
|
||||
// What the open node draws: its own frames, and — unless the branch has been
|
||||
// shut out of the strip — the ones in every subfolder under it, because a
|
||||
// roll's top level usually holds no file of its own.
|
||||
const shown = useMemo(() => {
|
||||
if (!rootName) return photos;
|
||||
const dir = node === rootName ? '' : (node ?? '').slice(rootName.length + 1);
|
||||
return photos.filter((p) => p.folder === rootName && (dir === '' || p.dir === dir || p.dir.startsWith(`${dir}/`)));
|
||||
}, [node, photos, rootName]);
|
||||
const below = dir === '' ? (p: LibraryPhoto) => p.dir !== '' : (p: LibraryPhoto) => p.dir.startsWith(`${dir}/`);
|
||||
return photos.filter((p) => p.folder === rootName && (p.dir === dir || (deep && below(p))));
|
||||
}, [deep, node, photos, rootName]);
|
||||
|
||||
const frame = shown.find((p) => p.id === up) ?? shown[0] ?? null;
|
||||
const urls = useThumbUrls(shown);
|
||||
@@ -415,6 +431,19 @@ export function Library() {
|
||||
<rect x="9" y="9" width="5" height="5" rx="1" />
|
||||
</svg>
|
||||
</button>
|
||||
{/* Whether the strip draws the branch under the open folder or
|
||||
only the folder itself. The label says what the click does,
|
||||
`on` says where the strip is now. */}
|
||||
<button
|
||||
type="button"
|
||||
className={`chip${deep ? ' on' : ''}`}
|
||||
data-key="lib-subs"
|
||||
aria-pressed={deep}
|
||||
title={t('lib.subsHint')}
|
||||
onClick={() => setDeep((prev) => !prev)}
|
||||
>
|
||||
{t(deep ? 'lib.subsHide' : 'lib.subsShow')}
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -465,6 +494,9 @@ export function Library() {
|
||||
// the rows go.
|
||||
onClick={() => {
|
||||
setNode(n.key);
|
||||
// The folder just clicked is the one a scan in flight
|
||||
// reads next, whatever layer it sits in.
|
||||
jumpRef.current = relOf(n.key);
|
||||
if (!under) return;
|
||||
setFolded((prev) => {
|
||||
const next = new Set(prev);
|
||||
|
||||
Reference in New Issue
Block a user