feat(library): a shelf that fills in as it reads, and one that carries the studio's look and frame back with it
- tiles land one at a time. The screenful a picked folder asks for was built all at once, and a HEIC or a RAW in it is a decode of its own, so the strip stood empty until every one of them was done: 2293ms on a roll of eighteen, with the batch arriving in one go (0/18 then 18/18 in a single step). Tiles are built in two lanes now and each one goes up the moment it is drawn — first picture at 36ms, then 1, 2, 4, 6, 8, 9, 11, 12, 14, 15, 16, 17, 18 of 18, done at 2496ms. The threading is handed back between tiles (`scheduler.yield`), so a click still answers while the strip fills: no long tasks either side, worst frame gap 169ms -> 164ms, a view toggle 49ms -> 41ms. The tiles are the same tiles; the wall's is drawn from the same counter. - the studio's look reaches the shelf. A frame graded in the studio writes the tile the shelf draws, at neutral geometry — no turn, crop, watermark or frame, since the shelf turns a tile itself — and every screen holding the tile is told the moment it is written, so the row repaints without a reload. - the shelf comes back on the frame the studio was holding. The frame a visit opened is kept with the visit, and coming back from the studio raises it: the strip centres it and the wall brings its card to the middle, even when the frame sits in another folder of the same tree. A shelf that opened on whatever it was left standing on sent the reader looking for the frame they had just been working on. - the frame's name stands under the picture, in the studio's own meta bar.
This commit is contained in:
@@ -20,10 +20,11 @@ import { developRaw, isRawName, isTiffName, tiffDevelop } from './engine/rawDeve
|
||||
import { sharePhoto, type ShareSite } from './engine/share';
|
||||
import { heicToJpeg, isHeicName } from './engine/heicDevelop';
|
||||
import { clearRaw, readRaw, storeRaw } from './engine/rawStore';
|
||||
import { getPhoto, loadEdit, readPhotoFile, saveEdit, setRotation as savePhotoRotation } from './engine/library';
|
||||
import { getPhoto, loadEdit, makeTile, putPhotoThumb, readPhotoFile, saveEdit, setRotation as savePhotoRotation } from './engine/library';
|
||||
import { offerInstall } from './pwa/install';
|
||||
import { cameraSupported } from './engine/camera';
|
||||
import { clearSession, forgetPhoto, loadPhoto, loadState, savePhoto, saveState, type StudioState } from './engine/session';
|
||||
import { keep } from './remember';
|
||||
import { preloadSuperRes, upscaleJpeg } from './engine/superRes';
|
||||
import { api, type Photo, type SavedRecipe } from './api';
|
||||
import { refreshAuth, useAuth } from './auth';
|
||||
@@ -106,6 +107,10 @@ const MODEL_GAIN = 4;
|
||||
// on every notch.
|
||||
const PREVIEW_QUANTUM = 512;
|
||||
const PREVIEW_DEBOUNCE_MS = 120;
|
||||
// The catalogue frame the studio is holding, under the catalogue's own key: the
|
||||
// shelf reads it when it comes back up and opens on that frame — the reader has
|
||||
// just been working on it (see Library.tsx).
|
||||
const LIBRARY_LAST_KEY = 'recipescam.library.last';
|
||||
|
||||
const FRAMES: { id: FrameId; label: MsgKey }[] = [
|
||||
{ id: 'none', label: 'frame.none' },
|
||||
@@ -921,6 +926,49 @@ export function Workspace() {
|
||||
if (prev) URL.revokeObjectURL(prev);
|
||||
return URL.createObjectURL(new Blob([out.bytes as BlobPart], { type: 'image/jpeg' }));
|
||||
});
|
||||
// A frame that came out of the catalogue leaves its own tile behind it:
|
||||
// the shelf would otherwise keep showing the negative the reader has just
|
||||
// graded. The tile is the look alone — the frame's turn, straighten, crop,
|
||||
// frame and stamps are the library's to draw, and every tile it draws
|
||||
// carries the frame's own standing on top — so the render in hand serves
|
||||
// as the tile exactly while it carries none of them. The hovered look is
|
||||
// not the frame's: only the committed one is filed.
|
||||
if (libPhotoId && !preview && !cropEditing) {
|
||||
const bare =
|
||||
!options.cropRect &&
|
||||
!options.watermark &&
|
||||
!options.gpsWatermark &&
|
||||
rotation === 0 &&
|
||||
!straighten &&
|
||||
!flipH &&
|
||||
!flipV &&
|
||||
frame === 'none';
|
||||
const graded =
|
||||
bare
|
||||
? out.bytes
|
||||
: (
|
||||
await renderPhoto({
|
||||
source: previewBytes,
|
||||
recipe: shownRecipe,
|
||||
frameId: 'none',
|
||||
useGeotag: false,
|
||||
gpsInfo: null,
|
||||
assets: engine.assets,
|
||||
options: {
|
||||
...options,
|
||||
photoRotation: 0,
|
||||
photoStraighten: 0,
|
||||
photoFlipH: false,
|
||||
photoFlipV: false,
|
||||
cropRect: null,
|
||||
watermark: null,
|
||||
gpsWatermark: null,
|
||||
},
|
||||
})
|
||||
)?.bytes;
|
||||
const tile = graded ? await makeTile(new Blob([graded as BlobPart], { type: 'image/jpeg' })) : null;
|
||||
if (tile && token === renderToken.current) void putPhotoThumb(libPhotoId, tile);
|
||||
}
|
||||
if (!comparing) {
|
||||
setCompareUrl((prev) => {
|
||||
if (prev) URL.revokeObjectURL(prev);
|
||||
@@ -2170,6 +2218,10 @@ export function Workspace() {
|
||||
// frame on the neutral look and so at 0.
|
||||
setRotation(row.rot ?? 0);
|
||||
setLibPhotoId(id);
|
||||
// The shelf is told which frame the studio is holding, so coming back to
|
||||
// it opens on this one rather than on wherever the tree was left — and it
|
||||
// outlives the visit, because the address bar no longer carries the id.
|
||||
keep(LIBRARY_LAST_KEY, id);
|
||||
} catch {
|
||||
setError(t('err.decode'));
|
||||
} finally {
|
||||
@@ -4060,6 +4112,7 @@ export function Workspace() {
|
||||
}}
|
||||
shotSpecs={shotSpecs}
|
||||
place={place}
|
||||
photoName={source?.name ?? null}
|
||||
onCropRectChange={setCropRect}
|
||||
onStageZoom={onStageZoom}
|
||||
canCompare={!!source}
|
||||
|
||||
Reference in New Issue
Block a user