diff --git a/docker/frontend/src/App.tsx b/docker/frontend/src/App.tsx index 397a5d3..6d12683 100644 --- a/docker/frontend/src/App.tsx +++ b/docker/frontend/src/App.tsx @@ -324,6 +324,13 @@ export function Workspace() { [user, applyRecipe, t] ); + // CREATE edits the photo live: each knob lands on the render as it is typed so + // the recipe can be judged before saving. No undo entry — this is a draft, and + // SAVE/EXPORT is what commits it. + const previewCreatedRecipe = useCallback((draft: Pick) => { + setRecipe((prev) => ({ ...prev, ...draft, isCustom: true })); + }, []); + // The phone's encrypted .recipe envelope (`shared/utils/recipeShare` is the // app's own file, byte-identical). Signed in only — a guest's recipe is never // written to disk. @@ -781,6 +788,7 @@ export function Workspace() { currentBase={recipe.baseFilter} current={recipe.adjustments} signedIn={!!user} + onPreview={previewCreatedRecipe} onSave={(draft) => void addCreatedRecipe(draft)} onExport={exportRecipeFile} /> diff --git a/docker/frontend/src/ui/RecipeCreatePanel.tsx b/docker/frontend/src/ui/RecipeCreatePanel.tsx index ead5c51..3473b1c 100644 --- a/docker/frontend/src/ui/RecipeCreatePanel.tsx +++ b/docker/frontend/src/ui/RecipeCreatePanel.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { ChipRow, type ChipDef } from './ChipColumn'; import { useI18n } from '../i18n/I18nProvider'; import { DEFAULT_ADJUSTMENTS, FILM_SIMS } from '../../shared/utils/defaultRecipes'; @@ -55,12 +55,14 @@ export function RecipeCreatePanel({ currentBase, current, signedIn, + onPreview, onSave, onExport, }: { currentBase?: BaseFilter; current?: ColorAdjustments | null; signedIn: boolean; + onPreview: (draft: Pick) => void; onSave: (draft: RecipeDraft) => void; onExport: (draft: RecipeDraft) => void; }) { @@ -153,6 +155,20 @@ export function RecipeCreatePanel({ return { name: name.trim() || 'NEW RECIPE', baseFilter: sim, adjustments, frameId: 'none', useGeotag: true }; }; + // Realtime: every knob applies to the photo on screen as it is typed, so the + // effect of the recipe is visible before saving it. Opening the tab is not an + // edit — the first run (the seeded values) is skipped. + const first = useRef(true); + useEffect(() => { + if (first.current) { + first.current = false; + return; + } + const { baseFilter, adjustments } = build(); + onPreview({ baseFilter, adjustments }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sim, dr, grain, cc, ccb, wbKey, wbRed, wbBlue, fHigh, fShadow, fColor, fNr, fSharp, fClarity, fEv]); + const opts = ( prefix: string, options: { v: string; d: string }[],