From 75ff44462177162241535c9128b4d53dd2e7b9a4 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Wed, 30 Sep 2026 15:41:18 +0700 Subject: [PATCH] library: the screen reads the catalogue back when a batch lands, not on a 700ms timer, and a tile keeps the object URL it was given --- docker/frontend/index.html | 4 ++ docker/frontend/src/Library.tsx | 58 ++++++++++++++++++++++----- docker/frontend/src/engine/library.ts | 12 +++++- 3 files changed, 62 insertions(+), 12 deletions(-) diff --git a/docker/frontend/index.html b/docker/frontend/index.html index ce45688..a0649c1 100644 --- a/docker/frontend/index.html +++ b/docker/frontend/index.html @@ -13,6 +13,10 @@ without Safari's chrome, 'black-translucent' gives it the app's black bars, and 'title' is the name under the icon. --> + + diff --git a/docker/frontend/src/Library.tsx b/docker/frontend/src/Library.tsx index fe8cca8..d376114 100644 --- a/docker/frontend/src/Library.tsx +++ b/docker/frontend/src/Library.tsx @@ -153,10 +153,6 @@ export function Library() { // holding has moved on. const live = scanSession(); const [, bump] = useReducer((n: number) => n + 1, 0); - // When the catalogue was last read back while a scan runs: the refresh reads the - // whole catalogue, so it keeps to one every few hundred milliseconds rather than - // one per frame. - const readAt = useRef(0); const [note, setNote] = useState(null); // The folder menu: what a right click opens, and where. No folder means the // right click landed on the empty part of the column, which is the way in for @@ -186,6 +182,13 @@ export function Library() { // one that says the scan is through — is recognisable as an end rather than // as a screen with nothing to do. let was: string | null = null; + // How much of the roll the catalogue was last read back at, and the shape the + // column was last drawn at: reading the whole catalogue back costs every + // thumbnail in it, so it waits for a batch to have landed rather than for a + // frame to have been read, and the column is redrawn only when a pass has + // walked into something new. + let wrote = -1; + let drew = ''; const watching = () => { bump(); const session = scanSession(); @@ -202,9 +205,17 @@ export function Library() { was = folder; if (!session) return; const { progress } = session; - setDirs(progress.dirs); - if (progress.done === 0 || Date.now() - readAt.current > 700) { - readAt.current = Date.now(); + const shape = progress.dirs.length ? `${progress.dirs.length}:${progress.dirs[progress.dirs.length - 1].id}` : ''; + if (shape !== drew) { + drew = shape; + setDirs(progress.dirs); + } + // A window beside the reading is handed the progress of another build, which + // may have no such count: the frames it has stored are then read back on a + // frame, as they were before there was one. + const written = progress.written ?? progress.done; + if (written !== wrote) { + wrote = written; void reload(); } }; @@ -1140,13 +1151,38 @@ export function Library() { // the panels paint. function useThumbUrls(photos: LibraryPhoto[]): Record { const [urls, setUrls] = useState>({}); + // The URL a row was given, kept for as long as the row is on screen. A scan lands + // in batches, and each batch hands the screen the whole list again — so a URL + // made afresh per list is a picture the browser decodes afresh per batch: + // thousands of bitmaps over a roll, for tiles that have not moved. A row that + // leaves the list takes its URL with it. + const made = useRef(new Map()); useEffect(() => { const next: Record = {}; - for (const photo of photos) if (photo.thumb) next[photo.id] = URL.createObjectURL(photo.thumb); + for (const photo of photos) { + if (!photo.thumb) continue; + const had = made.current.get(photo.id); + if (had) next[photo.id] = had; + else { + const url = URL.createObjectURL(photo.thumb); + made.current.set(photo.id, url); + next[photo.id] = url; + } + } + for (const [id, url] of made.current) { + if (id in next) continue; + URL.revokeObjectURL(url); + made.current.delete(id); + } setUrls(next); - return () => { - for (const url of Object.values(next)) URL.revokeObjectURL(url); - }; }, [photos]); + // The screen going away takes every URL with it. + useEffect( + () => () => { + for (const url of made.current.values()) URL.revokeObjectURL(url); + made.current.clear(); + }, + [] + ); return urls; } diff --git a/docker/frontend/src/engine/library.ts b/docker/frontend/src/engine/library.ts index bc12409..74b3276 100644 --- a/docker/frontend/src/engine/library.ts +++ b/docker/frontend/src/engine/library.ts @@ -320,6 +320,12 @@ export interface ScanProgress { total: number; done: number; added: number; + // How many frames have reached the catalogue. `done` counts the frames the + // reading has got to, which is nearly all of them long before they are stored, + // so a screen that read the catalogue back on `done` would read it again every + // frame — and reading it back means every thumbnail in it again. This moves a + // batch at a time, which is exactly when there is something new to read. + written: number; // How many frames the scan has read out of each row of the tree, keyed the way // the tree spells a row (`folder` for the picked folder, `folder/sub/dir` for // the rest) and counted the way the rows count: a frame sits on its own row and @@ -478,10 +484,13 @@ export async function scanFolder( total: 0, done: 0, added: 0, + written: 0, dirs: [], counts: {}, }; progress.folder = folder.name; + // A position written down by an older visit has no count to carry on from. + progress.written ??= 0; const found = new Set(progress.dirs.map((d) => d.id)); const counts: Record = progress.counts; // Count the frame the moment the scan gets to it, before it knows whether the @@ -538,6 +547,7 @@ export async function scanFolder( tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); + progress.written += rows.length; write(); }; // One frame, end to end: its bytes, its tile, its shutter time, and the row the @@ -848,7 +858,7 @@ export function startScan(folder: LibraryFolder): Promise { if (live) return Promise.reject(new Error('a scan is already running')); const session: ScanSession = { folder: folder.name, - progress: { folder: folder.name, total: 0, done: 0, added: 0, dirs: [], counts: {} }, + progress: { folder: folder.name, total: 0, done: 0, added: 0, written: 0, dirs: [], counts: {} }, stop: false, jump: null, };