From 1e26887a175c53a21afccb7cb8f4711a3b2a4862 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Sat, 10 Oct 2026 09:23:33 +0700 Subject: [PATCH] fix(library): open SINGLE on the card the reader pressed, stage the graded tile Switching from ALL THUMBS to SINGLE opened the frame nearest the middle of the wall, not the one that was pressed, and the strip then centred that stranger. `openFromWall` now prefers the last frame the reader chose when it is still in the listed shelf, and falls back to the nearest card as before. A frame the studio graded kept its look in the tile but the stage was read from the file, so the preview showed the negative the tile was made from. `stageSrc` now draws the graded tile, and the `onThumbWritten` subscription marks the frame edited so the shelf is current when the reader comes back. Verified: scripts/wall-check.mjs, and the D22 harness against the deployed build on 8090 (all ok, stage vs tile diff 0.0). --- docker/frontend/src/Library.tsx | 34 +++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/docker/frontend/src/Library.tsx b/docker/frontend/src/Library.tsx index 647a251..2f137e3 100644 --- a/docker/frontend/src/Library.tsx +++ b/docker/frontend/src/Library.tsx @@ -1497,6 +1497,11 @@ function Catalogue({ admin }: { admin: boolean }) { onThumbWritten((id, blob) => { cacheObjectUrl(id, blob); setTilesAt((n) => n + 1); + // The studio grades a frame behind this screen's back, and what it files + // there is a filed look: the stage reads that off this set (`stageSrc`), + // and the row under the frame reads it for its own note. The set is the + // only thing here that is not already told. + setEdited((prev) => (prev.has(id) ? prev : new Set(prev).add(id))); }), [] ); @@ -1630,10 +1635,22 @@ function Catalogue({ admin }: { admin: boolean }) { if (dir && nodes.some((n) => n.key === dir)) setNode(dir); }, [here, nodes]); - // The way back, and the way in: the frame the wall is standing on is the card - // nearest the middle of its viewport — the same card the reader's eye is on. + // The way back, and the way in. The card the reader picked is the frame that + // goes up: a click on the wall is a pick, so the picked frame is the one the + // eye is on and the one the strip centres. Only with nothing picked does the + // card nearest the middle stand in for it — the wall opens where it was left, + // so the middle of it is where the reader was looking. + // + // `lastSelectedId` rather than the set: a shift-click stacks a run of frames on + // the card that was actually pressed, and that card is the one to raise. It is + // looked up in `listed` too, because a pick made in another folder is not a + // frame this shelf can raise. const openFromWall = () => { const el = wall.ref.current; + const picked = + lastSelectedId && selectedIds.has(lastSelectedId) && listed.some((p) => p.id === lastSelectedId) + ? lastSelectedId + : null; const mid = el ? el.getBoundingClientRect().top + el.clientHeight / 2 : 0; let best: { id: string; d: number } | null = null; for (const card of el ? el.querySelectorAll(':scope > .adm-card') : []) { @@ -1641,7 +1658,8 @@ function Catalogue({ admin }: { admin: boolean }) { const d = Math.abs(box.top + box.height / 2 - mid); if (!best || d < best.d) best = { id: card.dataset.key!.slice('lib-photo-'.length), d }; } - if (best) setUp(best.id); + if (picked) setUp(picked); + else if (best) setUp(best.id); // The strip is built afresh when it comes back, so the frame is centred again // even when it is the frame it was already on. scrolledFrameRef.current = null; @@ -1879,7 +1897,15 @@ function Catalogue({ admin }: { admin: boolean }) { // What the stage hands the frame on as: the file's own pixels, turned by the // angle the row carries. The raised frame's own tile stands in until its file // has been read, and a URL read for another frame is not the frame's at all. - const stageSrc = stageUrl && stageFor === frame?.id ? stageUrl : frame ? urls[frame.id] ?? null : null; + // … and the one exception to it: a frame the studio has graded. The file is the + // negative that frame's tile was made from, so a stage read from the file would + // show a look the tile beside it is not showing — the reader is looking at the + // tile. A graded frame is drawn from its own tile, which is the same picture by + // the same pixels rather than a second develop of it. + // ponytail: the tile is 640px (`THUMB_MAX`), so a stage drawn wider than that is + // soft. Upgrade by developing the frame here, the way the studio does. + const gradedTile = frame && edited.has(frame.id) ? urls[frame.id] ?? null : null; + const stageSrc = gradedTile ?? (stageUrl && stageFor === frame?.id ? stageUrl : frame ? urls[frame.id] ?? null : null); const stageTurned = useTurned(stageSrc, frame?.rot ?? 0);