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