feat(library): read the walk without tiling, and scope the filters to the open folder

The scan made a JPEG tile for every frame as it walked, so a wall of 12MP
files spent its whole budget in the walk; the tile now waits until the card
is actually drawn (makeTile + putPhotoThumb), and a rawish frame whose date
is unreadable retries at 4MB instead of stopping at the head.

The year menu was built from the whole catalogue, so it offered values the
open folder could not produce; it now follows the rows on screen and clears
the choice when a move leaves it stale.
This commit is contained in:
2026-10-08 11:28:14 +07:00
parent 1f30750db2
commit f7ecc084d8
2 changed files with 81 additions and 85 deletions
+30 -8
View File
@@ -19,9 +19,11 @@ import {
jumpScan,
listEditedIds,
listFolders,
makeTile,
normPath,
onLibraryBlocked,
pickFolder,
putPhotoThumb,
readPhotoFile,
readPhotos,
reconnectPhotosForFolders,
@@ -1150,11 +1152,23 @@ function Catalogue() {
[listed, strip.from, strip.to]
);
// The years the open folder holds, newest first: the filter offers the years
// there are frames to find in rather than a century of empty ones.
// there are frames to find in rather than a century of empty ones. Scoped to
// `shown` — the open node (+ its subfolders when that chip is on) — not the
// whole library, so picking a year never points at frames elsewhere.
const years = useMemo(
() => [...new Set(photos.map((p) => new Date(p.taken).getFullYear()))].sort((a, b) => b - a),
[photos]
() => [...new Set(shown.map((p) => new Date(p.taken).getFullYear()))].sort((a, b) => b - a),
[shown]
);
// A year the open folder does not hold is not a year to look at, and leaving
// it set would draw an empty shelf with no way back but the dropdown. Fires on
// a move, not on every frame landing, so a scan of the open folder filling in
// its rows cannot clear a year the reader just picked.
useEffect(() => {
if (year !== 'all' && !years.includes(Number(year))) setYear('all');
// ponytail: only the year option list is folder-derived; stars and hours are
// fixed lists that cannot go stale. Re-check the others if they ever grow.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [node, deep, rootName]);
const handleSelectPhoto = useCallback(
(photo: LibraryPhoto, e: React.MouseEvent) => {
@@ -1495,9 +1509,11 @@ function Catalogue() {
// The tile: the thumbnail the catalogue kept, or a stand-in for the frame
// whose own small copy the file never carried — a RAW whose preview the camera
// never wrote, a HEIC with no thumbnail item (see engine/heicDevelop.ts). A tile
// The tile: the thumbnail the catalogue kept, or — the first time a frame is
// drawn — the one the wall makes and keeps for the visits after. A frame whose
// own small copy the file never carried says so while its tile is made: a RAW
// the camera wrote no preview in, a HEIC with no thumbnail item (see
// engine/heicDevelop.ts). A tile
// only carries a handler where a click
// on the picture means "open it" — in the strip the click belongs to the frame
// it raises, not to the studio. The word is only for a frame that has no
@@ -1513,7 +1529,7 @@ function Catalogue() {
/>
) : (
<div className="lib-noimg" onClick={onPick} title={onPick ? t('lib.open') : undefined}>
{photo.thumb ? '' : isRawName(photo.name) ? 'RAW' : 'HEIC'}
{photo.thumb ? '' : isRawName(photo.name) ? 'RAW' : isHeicName(photo.name) ? 'HEIC' : ''}
</div>
);
@@ -2550,7 +2566,13 @@ function useThumbUrls(photos: LibraryPhoto[], also?: LibraryPhoto | null): Recor
if (!prevBytes && /\.(tiff?)$/i.test(name)) prevBytes = tiffThumbnail(fullBytes);
if (prevBytes) blob = new Blob([prevBytes as BlobPart], { type: 'image/jpeg' });
} else {
blob = file;
// A frame the wall can decode itself: the tile is made here, at the
// size the wall paints, because this is the first time anyone has
// looked at the frame. The walk never made one — a roll reads in a
// fraction of the time without it — so the decode is paid on the
// frame the reader draws and kept for every visit after.
blob = (await makeTile(file)) ?? file;
if (blob !== file) void putPhotoThumb(photo.id, blob);
}
} catch {
blob = null;