From 6fdd0f3240c60e446f0d8b40a5dda5fcce82113f Mon Sep 17 00:00:00 2001 From: locpham Date: Sat, 10 Oct 2026 07:43:55 +0700 Subject: [PATCH] fix(library): the wall counts its columns off the layout, not off the run it drew MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A column count read back off the cards the window drew is an input the window feeds itself with: the drawn run is a whole number of rows, and those rows were read back into the count. A wall hidden behind the studio read nothing but zeroes off its own cards and reset itself to the top on the way out. Either way a window was written from a window, pass after pass, until React gave up with #185 and the whole screen went white — there is nothing between that throw and the floor. The count now comes off the grid, which is the same read the settle effect above already scrolls the wall by; a wall with no box is not measured at all, and is measured on the frame it comes back up; the one reset is written off the shelf's own length rather than off the last window; the scroller is not a place the browser may anchor; and a chain of writes past four is cut until something outside moves — a scroll, a resize, or another shelf. --- docker/frontend/src/Library.tsx | 67 +++++++++++++++++++++++------- docker/frontend/src/styles/app.css | 2 +- 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/docker/frontend/src/Library.tsx b/docker/frontend/src/Library.tsx index fff2e1a..e0897b3 100644 --- a/docker/frontend/src/Library.tsx +++ b/docker/frontend/src/Library.tsx @@ -197,6 +197,12 @@ const WALL_LANES = 2; // What `.adm-grid` leaves between its rows. A spacer has to stand the same // distance past the last row it replaces as the row itself would have. const WALL_GAP = 12; +// How many windows the wall may write in a row before it is talking to itself. +// A window read off the window before it is a loop, and React answers the fifty +// turns of one with #185 and a blank screen; four is more than an honest change +// needs — a re-column takes two passes — and the count is handed back by the +// first scroll, resize or change of shelf the reader makes. +const WALL_CARRIES = 4; // The hours a frame's own clock can read, and what the time filter offers: the // whole day in the 24-hour form a camera writes into EXIF. const HOURS = Array.from({ length: 24 }, (_, i) => i); @@ -3088,32 +3094,60 @@ function useWallWindow(count: number, on: boolean): { ref: RefObject { const prev = winRef.current; if (prev.from === next.from && prev.to === next.to && prev.above === next.above && prev.below === next.below) return; + if (carries >= WALL_CARRIES) return; + carries++; winRef.current = next; setWin(next); }; const measure = () => { - const cards = el.querySelectorAll(':scope > .adm-card'); - const first = cards[0]; - let cols = 0; - if (first) while (cols < cards.length && cards[cols].offsetTop === first.offsetTop) cols++; - // One row, or a wall not yet drawn: open on the first screenful and let the - // next pass read the step off what it drew. - if (cards.length <= cols) { - const to = Math.min(count, 24); - show({ from: 0, to, above: 0, below: 0 }); + // 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; + // 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 + // landed. This is the one reset, and it is written off the shelf's own + // length rather than off the cards the last window drew, which is what made + // the reset a window the window fed. + const held = winRef.current.to - winRef.current.from; + if (held <= 0 || winRef.current.from >= count || winRef.current.to > count) { + show({ from: 0, to: Math.min(count, 24), above: 0, below: 0 }); return; } + // How many columns is the grid's own business, read off its layout and not + // counted off the cards the window drew: the drawn run is a whole number of + // rows, those rows were read back into the count, and a count read off the + // run is a count the window feeds itself with. This is the same read the + // settle effect above scrolls the wall by. + const cols = Math.max(1, getComputedStyle(el).gridTemplateColumns.split(' ').length); + // The run the step below is read off. A run of a single row is no run at + // all — that is the last row of a shelf scrolled to its end — and a wall + // that has drawn nothing is nothing to read: the state's own first + // screenful stands until there is one. + const cards = el.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. // The cards do NOT all stand the same height — a caption that wraps makes // its row taller — and `offsetTop` is rounded to whole pixels, so the // smallest step would be a pixel short on every one of thousands of rows. // The average of the rows in hand is the honest guess at the ones that are // not in hand. - const rowsDrawn = Math.floor(cards.length / cols); - if (rowsDrawn < 2) return; // The step is read off the drawn rows the first time the wall stands at this // many columns, and kept until it stands at another: the average of a run of // rows moves as the run grows, so a step re-read on every pass made the window @@ -3144,13 +3178,16 @@ function useWallWindow(count: number, on: boolean): { ref: RefObject measure(); - el.addEventListener('scroll', onScroll, { passive: true }); - const ro = new ResizeObserver(onScroll); + const onOutside = () => { + carries = 0; + measure(); + }; + el.addEventListener('scroll', onOutside, { passive: true }); + const ro = new ResizeObserver(onOutside); ro.observe(el); return () => { measureRef.current = () => {}; - el.removeEventListener('scroll', onScroll); + el.removeEventListener('scroll', onOutside); ro.disconnect(); }; }, [count, on]); diff --git a/docker/frontend/src/styles/app.css b/docker/frontend/src/styles/app.css index 3ed4a58..c985612 100644 --- a/docker/frontend/src/styles/app.css +++ b/docker/frontend/src/styles/app.css @@ -1342,7 +1342,7 @@ button.hint:disabled { cursor: default; } } /* All thumbs: the wall scrolls down inside the stage, and its own scrollbar on the right edge is what reaches the frames below. */ -.lib-root .adm-grid { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-right: 6px; } +.lib-root .adm-grid { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-right: 6px; overflow-anchor: none; } /* What the thumbnail wall is read through: the four selects and the count of what is left. One row above the wall, and it does not scroll with it. */ .lib-root .adm-stage > .lib-filters,