diff --git a/App.tsx b/App.tsx index 0a379eb..ec330d7 100644 --- a/App.tsx +++ b/App.tsx @@ -71,6 +71,8 @@ export default function App() { }, []); const [recipes, setRecipes] = useState([]); const [selectedRecipe, setSelectedRecipe] = useState(null); + // Custom recipe open in the parameter editor (the CREATE form in edit mode). + const [editingRecipe, setEditingRecipe] = useState(null); // Custom temporary overrides for the sliders const [adjustments, setAdjustments] = useState({ ...DEFAULT_ADJUSTMENTS }); @@ -656,6 +658,46 @@ export default function App() { Alert.alert('Recipe Updated', `"${name}" has been saved.`); }; + // The recipe editor (the CREATE form in edit mode) SAVE. The typed name is + // the switch: unchanged = overwrite that recipe in place (same id, new + // parameters), new = keep the original and store the edit as its own recipe. + // The editor only closes first, the confirm dialog comes right after. + const handleUpdateRecipe = async (recipe: Omit) => { + const target = editingRecipe; + setEditingRecipe(null); + if (!target) return; + const name = recipe.name.trim(); + const unchanged = name === target.name.trim(); + const commit = async () => { + if (unchanged) { + await updateCustomRecipe(target.id, { ...recipe, name }); + const list = await getAllRecipes(); + setRecipes(list); + const updated = list.find((r) => r.id === target.id); + if (updated) applyRecipe(updated); + } else { + const created = await saveCustomRecipe({ ...recipe, name }); + setRecipes(await getAllRecipes()); + applyRecipe(created); + } + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + Alert.alert( + unchanged ? 'Recipe Updated' : 'Recipe Saved', + `"${name}" has been saved.` + ); + }; + Alert.alert( + unchanged ? 'Overwrite recipe?' : 'Save as new recipe?', + unchanged + ? `Replace "${target.name}" with the edited parameters?` + : `Keep "${target.name}" and save the edit as "${name}"?`, + [ + { text: 'CANCEL', style: 'cancel' }, + { text: unchanged ? 'OVERWRITE' : 'SAVE', onPress: () => { commit(); } }, + ], + ); + }; + // PRESETS → CREATE quick-input: build a brand-new recipe from the modal's // typed fields, persist it, and apply it (the user sees the look immediately). const handleCreateRecipe = async (recipe: Omit) => { @@ -1182,6 +1224,9 @@ export default function App() { recipeName={activeRecipe.name} onSaveRecipe={handleSaveRecipe} onOverwriteRecipe={selectedRecipe?.isCustom ? handleOverwriteRecipe : undefined} + onEditRecipe={ + selectedRecipe?.isCustom ? () => setEditingRecipe(selectedRecipe) : undefined + } /> {/* Bottom deck: chip panel (progressive disclosure) -> tool rail -> shutter */} @@ -1222,6 +1267,9 @@ export default function App() { onOpenParamChange={setOpenParamKey} currentBaseFilter={selectedRecipe?.baseFilter} onCreateRecipe={handleCreateRecipe} + editingRecipe={editingRecipe} + onCloseEdit={() => setEditingRecipe(null)} + onUpdateRecipe={handleUpdateRecipe} onSelectRecipe={handleSelectRecipe} onUpdateAdjustments={handleUpdateAdjustments} onUpdateFrame={handleUpdateFrame} diff --git a/src/components/AdjustmentPanel.tsx b/src/components/AdjustmentPanel.tsx index 64c2b9d..058867c 100644 --- a/src/components/AdjustmentPanel.tsx +++ b/src/components/AdjustmentPanel.tsx @@ -57,6 +57,13 @@ interface AdjustmentPanelProps { // CREATE quick-input saves a brand-new recipe (name + adjustments from the // modal) and applies it, unlike onSaveRecipe which snapshots the live state. onCreateRecipe: (recipe: Omit) => void; + // Recipe being edited (custom ones only) — non-null puts the CREATE form + // into editor mode, prefilled with that recipe's name and parameters. + editingRecipe?: Recipe | null; + onCloseEdit: () => void; + // SAVE from the editor: the caller decides overwrite (same name) vs + // save-as (new name). + onUpdateRecipe: (recipe: Omit) => void; onSelectRecipe: (recipe: Recipe) => void; onUpdateAdjustments: (adjustments: Partial) => void; onUpdateFrame: (frameId: FrameId) => void; @@ -165,6 +172,9 @@ export default function AdjustmentPanel({ onOpenParamChange, currentBaseFilter, onCreateRecipe, + editingRecipe, + onCloseEdit, + onUpdateRecipe, onSelectRecipe, onUpdateAdjustments, onUpdateFrame, @@ -752,11 +762,16 @@ export default function AdjustmentPanel({ )} {chipsRow} setCreateVisible(false)} + initial={editingRecipe ?? null} + onClose={() => { + setCreateVisible(false); + onCloseEdit(); + }} onSave={(recipe) => { - onCreateRecipe(recipe); + if (editingRecipe) onUpdateRecipe(recipe); + else onCreateRecipe(recipe); setShowRecipes(true); }} /> diff --git a/src/components/RecipeCreateModal.tsx b/src/components/RecipeCreateModal.tsx index 057f635..9b69a07 100644 --- a/src/components/RecipeCreateModal.tsx +++ b/src/components/RecipeCreateModal.tsx @@ -26,6 +26,10 @@ interface RecipeCreateModalProps { // Id of the currently applied film sim (or bundled recipe whose look should // prefill the SIMULATION row); undefined → default to PROVIA. currentBase?: BaseFilter; + // When set the form opens as an EDITOR: every row (name included) is + // prefilled from this recipe instead of the sim's standard values, and + // SAVE hands the edited copy back for the caller to store. + initial?: Recipe | null; onClose: () => void; onSave: (recipe: Omit) => void; } @@ -127,6 +131,7 @@ const NumRow = ({ label, value, onChange, placeholder, width = 58 }: NumRowProps { - const s = FILM_SIMS.find((x) => x.baseFilter === b)?.adjustments ?? DEFAULT_ADJUSTMENTS; + const valuesOf = (b: BaseFilter, src?: ColorAdjustments) => { + const s = src ?? FILM_SIMS.find((x) => x.baseFilter === b)?.adjustments ?? DEFAULT_ADJUSTMENTS; return { dr: String(s.dynamicRange ?? 'auto'), grain: String(s.grain === 0 ? 0 : s.grain >= 6 ? 6 : 3), @@ -199,15 +205,18 @@ export default function RecipeCreateModal({ // quick-input always starts from the current simulation. useEffect(() => { if (!visible) return; - const v = valuesOf(seedBase); - setSim(seedBase); + // Editing a stored recipe wins over the live sim seed: the form must show + // that recipe's own values, not the sim's standard ones. + const base = initial?.baseFilter ?? seedBase; + const v = valuesOf(base, initial?.adjustments); + setSim(base); setDr(v.dr); setGrain(v.grain); setCc(v.cc); setCcb(v.ccb); setWbKey(v.wbKey); - setWbRed('0'); - setWbBlue('0'); + setWbRed(String(initial?.adjustments.wbRed ?? 0)); + setWbBlue(String(initial?.adjustments.wbBlue ?? 0)); setFHigh(v.highlight); setFShadow(v.shadow); setFColor(v.color); @@ -215,9 +224,9 @@ export default function RecipeCreateModal({ setFSharp(v.sharp); setFClarity(v.clarity); setFEv(v.ev); - setName(''); + setName(initial?.name ?? ''); setIso(null); - }, [visible, currentBase]); + }, [visible, currentBase, initial]); // Switching the simulation reloads its standard values into the form. const selectSim = (b: BaseFilter) => { @@ -260,11 +269,14 @@ export default function RecipeCreateModal({ exposureCompensation: int(fEv, simAdj.exposureCompensation, -3, 3), }; onSave({ - name: name.trim() || 'NEW RECIPE', + name: name.trim() || initial?.name || 'NEW RECIPE', baseFilter: sim, adjustments: adj, - frameId: 'none', - useGeotag: true, + // An edit keeps the recipe's own frame and geotag choice; a fresh + // CREATE keeps the old defaults. + frameId: initial?.frameId ?? 'none', + useGeotag: initial?.useGeotag ?? true, + ...(initial?.cinema ? { cinema: initial.cinema } : {}), }); setName(''); onClose(); @@ -289,7 +301,9 @@ export default function RecipeCreateModal({ className="bg-titan border-b border-zinc-800 rounded-b-3xl px-4 pt-1" > - CREATE RECIPE + + {initial ? 'EDIT RECIPE' : 'CREATE RECIPE'} + ✕ @@ -301,6 +315,7 @@ export default function RecipeCreateModal({ CANCEL - SAVE RECIPE + {initial ? 'SAVE' : 'SAVE RECIPE'} diff --git a/src/components/TopBar.tsx b/src/components/TopBar.tsx index c8f595a..6c0f7da 100644 --- a/src/components/TopBar.tsx +++ b/src/components/TopBar.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { View, Text, TouchableOpacity, TextInput, Modal, KeyboardAvoidingView, Platform, Alert } from 'react-native'; +import { View, Text, TouchableOpacity, TextInput, Modal, KeyboardAvoidingView, Platform } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Plus, Pencil } from 'lucide-react-native'; import * as Haptics from 'expo-haptics'; @@ -10,14 +10,15 @@ interface TopBarProps { onSaveRecipe: (name: string) => void; // Present only when the active recipe is user-created: enables Save (overwrite). onOverwriteRecipe?: (name: string) => void; + // Also custom-only: opens the recipe editor — the CREATE form prefilled + // with this recipe's parameters and name. + onEditRecipe?: () => void; } -export default function TopBar({ mode, recipeName, onSaveRecipe, onOverwriteRecipe }: TopBarProps) { +export default function TopBar({ mode, recipeName, onSaveRecipe, onOverwriteRecipe, onEditRecipe }: TopBarProps) { const insets = useSafeAreaInsets(); const [modalVisible, setModalVisible] = useState(false); const [newRecipeName, setNewRecipeName] = useState(''); - const [editVisible, setEditVisible] = useState(false); - const [editName, setEditName] = useState(''); const openModal = () => { // Bundled recipes are read-only -> Save As only, blank name. Custom ones @@ -38,35 +39,6 @@ export default function TopBar({ mode, recipeName, onSaveRecipe, onOverwriteReci const handleSavePress = () => run(onSaveRecipe); const handleOverwritePress = () => onOverwriteRecipe && run(onOverwriteRecipe); - const openEdit = () => { - setEditName(recipeName); - setEditVisible(true); - }; - - // Edit keeps the name as the switch: same name = overwrite this recipe, a new - // name = keep the original and store the edit as a separate recipe. Both ask - // for confirmation first. - const handleEditSave = () => { - const name = editName.trim(); - if (!name || !onOverwriteRecipe) return; - const unchanged = name === recipeName.trim(); - setEditVisible(false); - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); - Alert.alert( - unchanged ? 'Overwrite recipe?' : 'Save as new recipe?', - unchanged - ? `Replace "${recipeName}" with the current settings?` - : `Keep "${recipeName}" and save these settings as "${name}"?`, - [ - { text: 'CANCEL', style: 'cancel' }, - { - text: unchanged ? 'OVERWRITE' : 'SAVE', - onPress: () => (unchanged ? onOverwriteRecipe(name) : onSaveRecipe(name)), - }, - ], - ); - }; - return ( <> - {onOverwriteRecipe && ( + {onEditRecipe && ( @@ -166,51 +138,6 @@ export default function TopBar({ mode, recipeName, onSaveRecipe, onOverwriteReci - - setEditVisible(false)} - > - - - - - EDIT RECIPE - - - - New name saves a copy · same name overwrites - - - setEditVisible(false)} - className="flex-1 bg-zinc-800 py-3 rounded-lg items-center" - > - CANCEL - - - SAVE - - - - - ); }