feat(studio): edits follow the photo — reopening it from LIBRARY restores the whole look
Settings changed in the studio were remembered per photo, but only as a film recipe, so everything outside that recipe (straighten, flips, crop, watermark, geotag, frame, the picked sim) was gone as soon as the photo was reopened from the library in a later session. Keep the whole StudioState instead and restore it when a library photo is opened; rows written by older builds — a flat recipe — are still readable, so photos edited before this change open with their look intact. SAVE PHOTO is untouched: that remains the step that publishes a photo to the landing film strip.
This commit is contained in:
+50
-14
@@ -23,7 +23,7 @@ import { clearRaw, readRaw, storeRaw } from './engine/rawStore';
|
||||
import { getPhoto, loadEdit, 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 } from './engine/session';
|
||||
import { clearSession, forgetPhoto, loadPhoto, loadState, savePhoto, saveState, type StudioState } from './engine/session';
|
||||
import { preloadSuperRes, upscaleJpeg } from './engine/superRes';
|
||||
import { api, type Photo, type SavedRecipe } from './api';
|
||||
import { refreshAuth, useAuth } from './auth';
|
||||
@@ -466,12 +466,17 @@ export function Workspace() {
|
||||
// the box's own position falls back to the default instead of going undefined.
|
||||
const [markStyle, setMarkStyle] = useState<MarkStyle>(() => ({ ...DEFAULT_MARK_STYLE, ...session.markStyle }));
|
||||
|
||||
// Every knob the workspace holds, written back on change — the reload half of
|
||||
// the session. Small JSON, so no debounce is worth the risk of losing the last
|
||||
// Every knob the workspace holds, gathered once — the reload half of the
|
||||
// session and the look a catalogue frame is filed at both write this same
|
||||
// record. Small JSON, so no debounce is worth the risk of losing the last
|
||||
// edit; the photo has its own store (engine/session.ts).
|
||||
const studioState = useMemo<StudioState>(
|
||||
() => ({ recipe, simId, frameId, crop, cropRect, cropApplied, rotation, straighten, flipH, flipV, gps, gpsManual, place, useGeotag, gpsStyle, markText, markOn, markStyle }),
|
||||
[recipe, simId, frameId, crop, cropRect, cropApplied, rotation, straighten, flipH, flipV, gps, gpsManual, place, useGeotag, gpsStyle, markText, markOn, markStyle],
|
||||
);
|
||||
useEffect(() => {
|
||||
saveState({ recipe, simId, frameId, crop, cropRect, cropApplied, rotation, straighten, flipH, flipV, gps, gpsManual, place, useGeotag, gpsStyle, markText, markOn, markStyle });
|
||||
}, [recipe, simId, frameId, crop, cropRect, cropApplied, rotation, straighten, flipH, flipV, gps, gpsManual, place, useGeotag, gpsStyle, markText, markOn, markStyle]);
|
||||
saveState(studioState);
|
||||
}, [studioState]);
|
||||
|
||||
// Crop has three states: none, framed-but-uncommitted (the frame is draggable
|
||||
// and the whole photo is shown), and applied (the crop alone is rendered).
|
||||
@@ -601,6 +606,35 @@ export function Workspace() {
|
||||
gpsStyle: { ...DEFAULT_GPS_STYLE },
|
||||
};
|
||||
}, []);
|
||||
// A catalogue frame opened again comes back on exactly the look it was left
|
||||
// at: restoreLook for everything UNDO remembers, and the three GPS knobs Look
|
||||
// leaves out (they live on the session, not in the undo stack).
|
||||
const restoreState = useCallback(
|
||||
(s: StudioState) => {
|
||||
const d = defaultLook();
|
||||
restoreLook({
|
||||
recipe: s.recipe ?? d.recipe,
|
||||
simId: s.simId ?? d.simId,
|
||||
frameId: s.frameId ?? d.frameId,
|
||||
crop: s.crop ?? d.crop,
|
||||
cropRect: s.cropRect ?? d.cropRect,
|
||||
cropApplied: s.cropApplied ?? d.cropApplied,
|
||||
rotation: s.rotation ?? d.rotation,
|
||||
straighten: s.straighten ?? d.straighten,
|
||||
flipH: s.flipH ?? d.flipH,
|
||||
flipV: s.flipV ?? d.flipV,
|
||||
markOn: s.markOn ?? d.markOn,
|
||||
markText: s.markText ?? d.markText,
|
||||
markStyle: { ...DEFAULT_MARK_STYLE, ...s.markStyle },
|
||||
useGeotag: s.useGeotag ?? d.useGeotag,
|
||||
gpsStyle: { ...DEFAULT_GPS_STYLE, ...s.gpsStyle },
|
||||
});
|
||||
setGps(s.gps ?? null);
|
||||
setGpsManual(s.gpsManual ?? { lat: '', lng: '' });
|
||||
setPlace(s.place ?? '');
|
||||
},
|
||||
[restoreLook, defaultLook],
|
||||
);
|
||||
const [exporting, setExporting] = useState(false);
|
||||
// The export menu: the sizes are picked before the render starts, because a
|
||||
// target bigger than the photo has a model run behind it first.
|
||||
@@ -2112,9 +2146,9 @@ export function Workspace() {
|
||||
);
|
||||
|
||||
// A frame from the local catalogue, handed over as `/app?lib=<id>`: the bytes
|
||||
// come off the disk through the handle the catalogue kept, then the recipe that
|
||||
// frame was last left at goes on top. A frame first opened has no recipe yet
|
||||
// and arrives on the neutral stock, like any other file.
|
||||
// come off the disk through the handle the catalogue kept, then the look that
|
||||
// frame was last left at goes on top. A frame first opened has no look yet and
|
||||
// arrives on the neutral stock, like any other file.
|
||||
const openLibraryPhoto = useCallback(
|
||||
async (id: string) => {
|
||||
setBusy(t('auth.busy'));
|
||||
@@ -2129,7 +2163,7 @@ export function Workspace() {
|
||||
if (!row) throw new Error('not in the catalogue');
|
||||
await loadFile(await readPhotoFile(row));
|
||||
const saved = await loadEdit(id);
|
||||
if (saved) applyRecipe(saved.recipe);
|
||||
if (saved) restoreState(saved.state);
|
||||
// The catalogue's own standing for this frame, which the reader set on
|
||||
// the library's stage: the studio opens the negative the way they last
|
||||
// saw it, at the same angle. It arrives after `loadFile`, which puts the
|
||||
@@ -2142,7 +2176,7 @@ export function Workspace() {
|
||||
setBusy(null);
|
||||
}
|
||||
},
|
||||
[loadFile, applyRecipe, clearPreview, t]
|
||||
[loadFile, restoreState, clearPreview, t]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -2150,13 +2184,15 @@ export function Workspace() {
|
||||
}, [openLibraryPhoto]);
|
||||
|
||||
// What the catalogue remembers about a frame is the look it was left at, filed
|
||||
// back on every change — debounced, because a slider drag fires this per frame
|
||||
// of the drag and IndexedDB is on the wrong side of that.
|
||||
// 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.
|
||||
useEffect(() => {
|
||||
if (!libPhotoId || !source) return;
|
||||
const timer = setTimeout(() => void saveEdit(libPhotoId, recipe), 600);
|
||||
const timer = setTimeout(() => void saveEdit(libPhotoId, studioState), 600);
|
||||
return () => clearTimeout(timer);
|
||||
}, [libPhotoId, source, recipe]);
|
||||
}, [libPhotoId, source, studioState]);
|
||||
|
||||
// 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