From 91109cba54f52b29abd8236368201fe4ba294589 Mon Sep 17 00:00:00 2001 From: locphamtran Date: Thu, 10 Sep 2026 20:57:19 +0700 Subject: [PATCH] feat(recipe): Save and Save As for recipes Custom recipes can be overwritten in place (TopBar SAVE now offers CANCEL / SAVE AS / SAVE and prefills the current name); bundled recipes stay read-only and only offer Save As. Adds updateCustomRecipe to storageUtils. --- App.tsx | 26 ++++++++++++++++++-- src/components/TopBar.tsx | 51 ++++++++++++++++++++++++++++++--------- src/utils/storageUtils.ts | 7 ++++++ 3 files changed, 71 insertions(+), 13 deletions(-) diff --git a/App.tsx b/App.tsx index d9b3a5a..0a379eb 100644 --- a/App.tsx +++ b/App.tsx @@ -17,7 +17,7 @@ import SettingsModal from './src/components/SettingsModal'; import PhotoViewerModal from './src/components/PhotoViewerModal'; import { Recipe, ColorAdjustments, CustomWatermark, GpsWatermark, FrameId, GPSInfo, AspectRatio, MeterMode, PhotoEntry, ShutterSound, StartupMode, CropRatio, CropRect, CROP_W_H, DEFAULT_CROP_RECT } from './src/types'; -import { getAllRecipes, saveCustomRecipe, deleteCustomRecipe } from './src/utils/storageUtils'; +import { getAllRecipes, saveCustomRecipe, updateCustomRecipe, deleteCustomRecipe } from './src/utils/storageUtils'; import { DEFAULT_ADJUSTMENTS, FILM_SIMS, filmSimToRecipe } from './src/utils/defaultRecipes'; import { getCurrentGPS, requestLocationPermissions, reverseGeocode } from './src/utils/locationUtils'; import { resolvePhotoGPS } from './src/utils/photoGps'; @@ -639,6 +639,23 @@ export default function App() { Alert.alert('Recipe Saved', `"${name}" has been created.`); }; + // TopBar "SAVE" on a user-created recipe: overwrite it in place, same id. + const handleOverwriteRecipe = async (name: string) => { + if (!selectedRecipe) return; + await updateCustomRecipe(selectedRecipe.id, { + name, + baseFilter: selectedRecipe.baseFilter, + adjustments, + frameId: selectedFrame, + useGeotag, + }); + const list = await getAllRecipes(); + setRecipes(list); + const updated = list.find((r) => r.id === selectedRecipe.id); + if (updated) applyRecipe(updated); + Alert.alert('Recipe Updated', `"${name}" has been saved.`); + }; + // 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) => { @@ -1160,7 +1177,12 @@ export default function App() { {/* Top overlay: mode label + recipe name (left), Save current (right) */} - + {/* Bottom deck: chip panel (progressive disclosure) -> tool rail -> shutter */} void; + // Present only when the active recipe is user-created: enables Save (overwrite). + onOverwriteRecipe?: (name: string) => void; } -export default function TopBar({ mode, recipeName, onSaveRecipe }: TopBarProps) { +export default function TopBar({ mode, recipeName, onSaveRecipe, onOverwriteRecipe }: TopBarProps) { const insets = useSafeAreaInsets(); const [modalVisible, setModalVisible] = useState(false); const [newRecipeName, setNewRecipeName] = useState(''); - const handleSavePress = () => { + const openModal = () => { + // Bundled recipes are read-only -> Save As only, blank name. Custom ones + // prefill with the current name so Save (overwrite) is one tap. + setNewRecipeName(onOverwriteRecipe ? recipeName : ''); + setModalVisible(true); + }; + + const run = (fn: (name: string) => void) => { const name = newRecipeName.trim(); if (!name) return; - onSaveRecipe(name); + fn(name); setNewRecipeName(''); setModalVisible(false); Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); }; + const handleSavePress = () => run(onSaveRecipe); + const handleOverwritePress = () => onOverwriteRecipe && run(onOverwriteRecipe); + return ( <> setModalVisible(true)} + onPress={openModal} className="flex-row items-center rounded-full border border-zinc-700/80 bg-black/45 px-4 py-2.5" activeOpacity={0.7} > @@ -67,7 +79,7 @@ export default function TopBar({ mode, recipeName, onSaveRecipe }: TopBarProps) > - SAVE CURRENT AS RECIPE + {onOverwriteRecipe ? 'SAVE RECIPE' : 'SAVE CURRENT AS RECIPE'} CANCEL - - SAVE RECIPE - + {onOverwriteRecipe ? ( + <> + + SAVE AS + + + SAVE + + + ) : ( + + SAVE RECIPE + + )} diff --git a/src/utils/storageUtils.ts b/src/utils/storageUtils.ts index fb1b65f..8ecc1ac 100644 --- a/src/utils/storageUtils.ts +++ b/src/utils/storageUtils.ts @@ -35,6 +35,13 @@ export async function saveCustomRecipe(recipe: Omit): return newRecipe; } +// Overwrite an existing custom recipe in place (Save). No-op if the id is gone. +export async function updateCustomRecipe(id: string, recipe: Omit): Promise { + const custom = await getCustomRecipes(); + const updated = custom.map((r) => (r.id === id ? { ...r, ...recipe, id, isCustom: true } : r)); + await AsyncStorage.setItem(CUSTOM_RECIPES_KEY, JSON.stringify(updated)); +} + export async function deleteCustomRecipe(id: string): Promise { const custom = await getCustomRecipes(); const updated = custom.filter(r => r.id !== id);