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); } })();