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:
2026-10-09 21:19:06 +07:00
parent 6c21c2f374
commit c2e27b8e9d
2 changed files with 68 additions and 20 deletions
+18 -6
View File
@@ -12,7 +12,7 @@
// FileSystemHandle by itself and the whole surface is seven calls — a dependency
// does not pay for itself here. Revisit if the catalogue grows queries (tags,
// smart collections) that hand-rolled cursors would make ugly.
import type { Recipe } from '../../shared/types';
import type { StudioState } from './session';
import { readCapturedAt } from './imageOps';
import { isRawName, isTiffName, rawThumbnail } from './rawDevelop';
import { heicThumbnail, heicToJpeg, isHeicName } from './heicDevelop';
@@ -131,7 +131,10 @@ export interface LibraryDir {
export interface LibraryEdit {
photoId: string;
recipe: Recipe;
// The whole look the frame was left at, the same record the studio keeps for
// the working photo — frame, crop, rotation, mirrors, marks and geotag change
// the picture as much as the recipe does, so the recipe alone was not enough.
state: StudioState;
updatedAt: number;
}
@@ -2150,18 +2153,27 @@ export async function readPhotoFile(photo: LibraryPhoto): Promise<File> {
// --- edits -----------------------------------------------------------------
export async function saveEdit(id: string, recipe: Recipe): Promise<void> {
export async function saveEdit(id: string, state: StudioState): Promise<void> {
try {
const edit: LibraryEdit = { photoId: id, recipe, updatedAt: Date.now() };
const edit: LibraryEdit = { photoId: id, state, updatedAt: Date.now() };
await ask(EDITS, 'readwrite', (s) => s.put(edit));
} catch {
// Private mode: the frame is still editable, it just forgets the recipe.
// Private mode: the frame is still editable, it just forgets the look.
}
}
export async function loadEdit(id: string): Promise<LibraryEdit | null> {
try {
return (await ask<LibraryEdit | undefined>(EDITS, 'readonly', (s) => s.get(id))) ?? null;
// A row filed before the whole look was kept carries the recipe alone: lift
// it into a state so the caller reads one shape, old rows included.
const row = await ask<(Partial<LibraryEdit> & { recipe?: StudioState['recipe'] }) | undefined>(
EDITS,
'readonly',
(s) => s.get(id),
);
if (!row) return null;
if (row.state) return row as LibraryEdit;
return { photoId: row.photoId ?? id, state: { recipe: row.recipe }, updatedAt: row.updatedAt ?? 0 };
} catch {
return null;
}