import type { CropRatio, CropRect, FrameId, GPSInfo, Recipe } from '../../shared/types'; // Studio session: the working photo plus every knob the workspace holds, so a // reload resumes exactly where the user left off — guest or signed in (the // login cookie already outlives the tab; this covers the rest). // // ponytail: one session per browser, not per account — two accounts in the same // browser share the working photo. Key it by user id if that ever matters. const STATE_KEY = 'rc.studio.v1'; const DB_NAME = 'recipescam-studio'; const STORE = 'session'; const PHOTO_KEY = 'photo'; export interface StudioState { recipe?: Recipe; simId?: string | null; frameId?: FrameId; crop?: CropRatio; cropRect?: CropRect; // false = the frame is on screen but not accepted yet (still draggable). cropApplied?: boolean; rotation?: 0 | 90 | 180 | 270; // FRAME's STRAIGHTEN: degrees off the quarter turn, -180..180. straighten?: number; gps?: GPSInfo | null; gpsManual?: { lat: string; lng: string }; place?: string; useGeotag?: boolean; // size/x/y are the GPS box on the photo; older sessions hold only the colour // and the two switches and take the defaults for the rest. font is the family // the FONT strip picked, null for the bundled face. gpsStyle?: { color: string; showName: boolean; showTime: boolean; size?: number; x?: number; y?: number; font?: string | null }; markText?: string; markOn?: boolean; // x/y are fractions of the stamp the mark is drawn on (the drag box writes // them straight back); older sessions hold no x/y and take the default. markStyle?: { color: string; size: number; x?: number; y?: number; font?: string | null }; } export function loadState(): StudioState { try { const raw = localStorage.getItem(STATE_KEY); return raw ? (JSON.parse(raw) as StudioState) : {}; } catch { return {}; } } export function saveState(state: StudioState): void { try { localStorage.setItem(STATE_KEY, JSON.stringify(state)); } catch { // Quota or private mode: the studio still works, it just forgets. } } // --- the photo: IndexedDB, because a 12MP JPEG does not fit in localStorage --- function openDb(): Promise { return new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, 1); req.onupgradeneeded = () => req.result.createObjectStore(STORE); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } async function idb(mode: IDBTransactionMode, run: (store: IDBObjectStore) => IDBRequest): Promise { const db = await openDb(); try { return await new Promise((resolve, reject) => { const req = run(db.transaction(STORE, mode).objectStore(STORE)); req.onsuccess = () => resolve(req.result as T); req.onerror = () => reject(req.error); }); } finally { db.close(); } } export async function savePhoto(name: string, bytes: Uint8Array): Promise { try { await idb('readwrite', (s) => s.put({ name, bytes }, PHOTO_KEY)); } catch { // Private mode / no IndexedDB: the photo just will not survive a reload. } } export async function loadPhoto(): Promise<{ name: string; bytes: Uint8Array } | null> { try { const row = await idb<{ name: string; bytes: Uint8Array } | undefined>('readonly', (s) => s.get(PHOTO_KEY)); return row && row.bytes instanceof Uint8Array ? { name: row.name, bytes: row.bytes } : null; } catch { return null; } } // CLEAR takes the working photo off the stage but keeps the knobs: the look was // the user's work and outlives the picture it was made on. The session's own // copy has to go with it, or the next reload would put the frame straight back. export async function forgetPhoto(): Promise { try { await idb('readwrite', (s) => s.delete(PHOTO_KEY)); } catch { // Private mode / no IndexedDB: the photo just will not survive a reload. } } // Signing out ends the session: drop the knobs and the working photo, so the // next visit — guest or another account — opens the studio empty. The account's // own photos stay in MY PHOTOS, ready to reopen. // // Async on purpose: the caller reloads the moment this returns, and a reload // aborts a delete still in flight. Resolve on the transaction's completion, not // on the request's success, or the bytes outlive the logout. export async function clearSession(): Promise { try { localStorage.removeItem(STATE_KEY); } catch { // Nothing to clear. } try { const db = await openDb(); await new Promise((resolve, reject) => { const tx = db.transaction(STORE, 'readwrite'); tx.objectStore(STORE).delete(PHOTO_KEY); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); tx.onabort = () => reject(tx.error); }); db.close(); } catch { // Private mode / no IndexedDB: there is nothing stored to forget. } }