From dd9310b344e93467814b3b5ebb9b8bd9e6b64993 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Mon, 12 Oct 2026 18:43:13 +0700 Subject: [PATCH] fix(library): a reading of the cloud says when it has finished, and a window is measured off the shelf that is up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The background walk behind a cloud node read its pages after the answer the screen had already been given, and no one told the screen when the last page landed: the note went on saying a reading was running for the rest of the visit. The walk now gives its own last word, with the count it read. The strip and the wall windows were measured off the node the effect was set up on. The stage is drawn while a node is open, so a visit that loses its nodes and gets them back draws a new strip and a new wall, and a window measured off the old node — a box of nothing, a scroll always nought — never moved again for the rest of the visit. Both now read the element that is up. --- docker/frontend/src/Library.tsx | 37 ++++++++++++++++++++++------ docker/frontend/src/engine/immich.ts | 8 ++++++ 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/docker/frontend/src/Library.tsx b/docker/frontend/src/Library.tsx index 9bc22b4..635bbe8 100644 --- a/docker/frontend/src/Library.tsx +++ b/docker/frontend/src/Library.tsx @@ -1005,7 +1005,7 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) { syncingServer.current.add(folder.name); setNote(null); try { - const res = await syncImmich(folder, (batch) => { + const res = await syncImmich(folder, (batch, progress) => { setPhotos((prev) => { const existing = new Set(prev.map((p) => p.id)); const toAdd = batch.filter((p) => !existing.has(p.id)); @@ -1024,6 +1024,13 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) { saveCachedPhotos(next); return next; }); + // The reading's own last word. The answer below is the foreground + // window's, and the pages behind it are read after it: the note that + // said a reading was running is spent here, when the last page of the + // whole walk lands, rather than never. + if (!progress.hasMore) { + setNote(t('lib.immich.added', { n: progress.loaded, folder: folder.label ?? folder.name })); + } }); setDirs((prev) => [...prev.filter((d) => d.folder !== folder.name), ...res.dirs]); setNote( @@ -3961,8 +3968,15 @@ function useStripWindow(count: number, on: boolean): { ref: RefObject { - const from = Math.max(0, Math.floor(el.scrollLeft / TILE_STEP) - STRIP_KEEP); - const to = Math.min(count, Math.ceil((el.scrollLeft + el.clientWidth) / TILE_STEP) + STRIP_KEEP); + // The window is measured off the strip that is up, not off the one this + // effect was set up on: the stage is drawn while a node is open, so a visit + // that loses its nodes and gets them back draws a new strip, and a window + // measured off the old node — a box of nothing, a scroll always nought — is + // a window that never moves again for the rest of the visit. + const now = ref.current; + if (!now) return; + const from = Math.max(0, Math.floor(now.scrollLeft / TILE_STEP) - STRIP_KEEP); + const to = Math.min(count, Math.ceil((now.scrollLeft + now.clientWidth) / TILE_STEP) + STRIP_KEEP); setWin((prev) => (prev.from === from && prev.to === to ? prev : { from, to })); }; measure(); @@ -4031,13 +4045,20 @@ function useWallWindow(count: number, on: boolean): { ref: RefObject { + // The window is measured off the wall that is up, not off the one this + // effect was set up on: the stage is drawn while a node is open, so a visit + // that loses its nodes and gets them back draws a new wall, and a window + // measured off the old node — a box of nothing, a scroll always nought — is + // a window that never moves again for the rest of the visit. + const now = ref.current; + if (!now) return; // A wall that is not standing has no box to measure: the library is // `display:none` while the studio is up, and a window read off it would be // a window read off nothing — every card reports `offsetTop` 0 and every // scroll 0 — which threw the shelf back to its top every time the studio // handed back to it. It is read on the frame it comes back up, which is a // box change. - if (!el.clientHeight) return; + if (!now.clientHeight) return; // And a window that is nowhere on the shelf stands nowhere: outside a shelf // that shrank under it — another folder, a filter, a shorter roll — or empty // while the shelf is not, which is the first paint, before the catalogue has @@ -4054,12 +4075,12 @@ function useWallWindow(count: number, on: boolean): { ref: RefObject(':scope > .adm-card'); + const cards = now.querySelectorAll(':scope > .adm-card'); const rowsDrawn = Math.floor(cards.length / cols); if (rowsDrawn < 2) return; // The step from one row to the next, taken as the drawn run's own average. @@ -4087,8 +4108,8 @@ function useWallWindow(count: number, on: boolean): { ref: RefObject 0)) return; const rows = Math.ceil(count / cols); - const row0 = Math.max(0, Math.floor(el.scrollTop / pitch) - WALL_ROWS); - const row1 = Math.min(rows, Math.ceil((el.scrollTop + el.clientHeight) / pitch) + WALL_ROWS); + const row0 = Math.max(0, Math.floor(now.scrollTop / pitch) - WALL_ROWS); + const row1 = Math.min(rows, Math.ceil((now.scrollTop + now.clientHeight) / pitch) + WALL_ROWS); const from = row0 * cols; const to = Math.min(count, row1 * cols); // A spacer's own height keeps the gap the grid would have left under the diff --git a/docker/frontend/src/engine/immich.ts b/docker/frontend/src/engine/immich.ts index 80bd10d..526fc2a 100644 --- a/docker/frontend/src/engine/immich.ts +++ b/docker/frontend/src/engine/immich.ts @@ -588,6 +588,14 @@ export async function syncImmich( } } } finally { + // The reading's last word. The answer `syncImmich` gave was the + // foreground window's, and the pages behind it are read after it: without + // this the screen goes on saying a reading is running for the rest of the + // visit, and a reading nobody is doing is the note the reader reads as a + // reading that never stops. A walk that ended at a page the server would + // not answer says the same thing — what the catalogue was given is what it + // was given — and the place stays where it stopped either way. + onBatch?.([], { loaded: rootCount, hasMore: false }); walking.delete(root); } })();