fix(studio): file a catalogue frame's look and its tile ten seconds after the stage goes still
This commit is contained in:
+83
-51
@@ -112,6 +112,12 @@ const MODEL_GAIN = 4;
|
||||
// on every notch.
|
||||
const PREVIEW_QUANTUM = 512;
|
||||
const PREVIEW_DEBOUNCE_MS = 120;
|
||||
// How long the stage has to be still before the look goes back to the shelf: the
|
||||
// tile it shows for a catalogue frame is a develop of that look and a write into
|
||||
// the store the shelf reads, and paying both behind every frame of a slider drag
|
||||
// is the drag the reader feels. Ten seconds of nothing asked of the stage is a
|
||||
// look the reader has stopped making. See the filing effects in the studio.
|
||||
const LIBRARY_EXPORT_MS = 10_000;
|
||||
// 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).
|
||||
@@ -1067,49 +1073,11 @@ 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);
|
||||
}
|
||||
// The tile the shelf shows for this frame is not made here. It is a
|
||||
// develop of the look and a write into the store the shelf reads, and a
|
||||
// develop behind every frame of a slider drag is the drag the reader
|
||||
// feels — it is made on the idle clock below instead, with the look
|
||||
// itself. See `LIBRARY_EXPORT_MS`.
|
||||
if (!comparing) {
|
||||
setCompareUrl((prev) => {
|
||||
if (prev) URL.revokeObjectURL(prev);
|
||||
@@ -2538,16 +2506,80 @@ export function Workspace() {
|
||||
openLibraryPhotoRef.current = openLibraryPhoto;
|
||||
}, [openLibraryPhoto]);
|
||||
|
||||
// What the catalogue remembers about a frame is the look it was left at, filed
|
||||
// back on every change — the whole look, not just the recipe, so the next
|
||||
// session reopens the frame cropped, straightened and marked as it was left.
|
||||
// Debounced, because a slider drag fires this per frame of the drag and
|
||||
// IndexedDB is on the wrong side of that.
|
||||
// What the catalogue remembers about a frame is the look it was left at — the
|
||||
// whole look, not just the recipe, so the next session reopens the frame
|
||||
// cropped, straightened and marked as it was left — and the tile the shelf
|
||||
// draws for it, which is that same look developed once more and written into
|
||||
// the store the shelf reads. Both go back on an idle clock: a slider drag asks
|
||||
// for a preview per frame of the drag, and a develop and a store write behind
|
||||
// every one of those is the drag the reader feels. Ten seconds with nothing
|
||||
// asked of the stage is a look the reader has stopped making — the shelf hears
|
||||
// about it once, and the looks still being made are paid for by nothing.
|
||||
const filedLookRef = useRef<{ id: string; run: () => Promise<void> } | null>(null);
|
||||
const filedTokenRef = useRef(0);
|
||||
const flushFiledLook = useCallback(() => {
|
||||
const job = filedLookRef.current;
|
||||
filedLookRef.current = null;
|
||||
if (job) void job.run().catch(() => undefined);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!libPhotoId || !source) return;
|
||||
const timer = setTimeout(() => void saveEdit(libPhotoId, studioState), 600);
|
||||
if (!libPhotoId || !source || !engine || !previewBytes) return;
|
||||
// A look waiting to go back belongs to the frame it was made on: opening
|
||||
// another frame in the shelf files the one left behind first, or the last
|
||||
// ten seconds of its editing is thrown away by the frame switch.
|
||||
if (filedLookRef.current && filedLookRef.current.id !== libPhotoId) flushFiledLook();
|
||||
const id = libPhotoId;
|
||||
const token = ++filedTokenRef.current;
|
||||
filedLookRef.current = {
|
||||
id,
|
||||
run: async () => {
|
||||
await saveEdit(id, studioState);
|
||||
// A crop still being framed is not the frame's yet, and the tile is the
|
||||
// look alone: the turn, straighten, crop, frame and stamps are the
|
||||
// library's to draw on top of every tile it shows, so the tile is made
|
||||
// with none of them on it. The hovered look is not the frame's either —
|
||||
// the shelf keeps no tile for a look the pointer only crossed.
|
||||
if (cropEditing) return;
|
||||
const bytes = (
|
||||
await renderPhoto({
|
||||
source: previewBytes,
|
||||
recipe,
|
||||
frameId: 'none',
|
||||
useGeotag: false,
|
||||
gpsInfo: null,
|
||||
assets: engine.assets,
|
||||
options: {
|
||||
...stageOptions,
|
||||
photoRotation: 0,
|
||||
photoStraighten: 0,
|
||||
photoFlipH: false,
|
||||
photoFlipV: false,
|
||||
cropRect: null,
|
||||
watermark: null,
|
||||
gpsWatermark: null,
|
||||
},
|
||||
})
|
||||
)?.bytes;
|
||||
const tile = bytes ? await makeTile(new Blob([bytes as BlobPart], { type: 'image/jpeg' })) : null;
|
||||
if (tile && token === filedTokenRef.current) await putPhotoThumb(id, tile);
|
||||
},
|
||||
};
|
||||
const timer = setTimeout(flushFiledLook, LIBRARY_EXPORT_MS);
|
||||
return () => clearTimeout(timer);
|
||||
}, [libPhotoId, source, studioState]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [libPhotoId, source, engine, previewBytes, recipe, studioState, stageOptions, cropEditing]);
|
||||
|
||||
// The look a reader walks away from is the look they made: leaving the studio,
|
||||
// the tab or the app files what is waiting there and then, rather than leaving
|
||||
// the catalogue showing the frame as it stood ten seconds before the last edit.
|
||||
useEffect(() => {
|
||||
window.addEventListener('pagehide', flushFiledLook);
|
||||
return () => {
|
||||
window.removeEventListener('pagehide', flushFiledLook);
|
||||
flushFiledLook();
|
||||
};
|
||||
}, [flushFiledLook]);
|
||||
|
||||
// The same filing for the frame's own standing: the catalogue opened this
|
||||
// frame at an angle, and a turn made here is that number's to keep — the
|
||||
|
||||
Reference in New Issue
Block a user