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.
This commit is contained in:
@@ -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<Recipe, 'id' | 'isCustom'>) => {
|
||||
@@ -1160,7 +1177,12 @@ export default function App() {
|
||||
</View>
|
||||
|
||||
{/* Top overlay: mode label + recipe name (left), Save current (right) */}
|
||||
<TopBar mode={mode} recipeName={activeRecipe.name} onSaveRecipe={handleSaveRecipe} />
|
||||
<TopBar
|
||||
mode={mode}
|
||||
recipeName={activeRecipe.name}
|
||||
onSaveRecipe={handleSaveRecipe}
|
||||
onOverwriteRecipe={selectedRecipe?.isCustom ? handleOverwriteRecipe : undefined}
|
||||
/>
|
||||
|
||||
{/* Bottom deck: chip panel (progressive disclosure) -> tool rail -> shutter */}
|
||||
<View
|
||||
|
||||
+40
-11
@@ -8,22 +8,34 @@ interface TopBarProps {
|
||||
mode: 'camera' | 'library';
|
||||
recipeName: string;
|
||||
onSaveRecipe: (name: string) => 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 (
|
||||
<>
|
||||
<View
|
||||
@@ -43,7 +55,7 @@ export default function TopBar({ mode, recipeName, onSaveRecipe }: TopBarProps)
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => 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)
|
||||
>
|
||||
<View style={{ paddingBottom: insets.bottom + 16 }} className="mt-auto bg-titan border-t border-zinc-800 rounded-t-3xl p-6">
|
||||
<Text className="text-white font-mono text-sm font-bold tracking-wider mb-4">
|
||||
SAVE CURRENT AS RECIPE
|
||||
{onOverwriteRecipe ? 'SAVE RECIPE' : 'SAVE CURRENT AS RECIPE'}
|
||||
</Text>
|
||||
<TextInput
|
||||
value={newRecipeName}
|
||||
@@ -85,12 +97,29 @@ export default function TopBar({ mode, recipeName, onSaveRecipe }: TopBarProps)
|
||||
>
|
||||
<Text className="text-zinc-300 font-mono text-xs font-semibold">CANCEL</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={handleSavePress}
|
||||
className="flex-1 bg-amber-500 py-3 rounded-lg items-center"
|
||||
>
|
||||
<Text className="text-black font-mono text-xs font-bold">SAVE RECIPE</Text>
|
||||
</TouchableOpacity>
|
||||
{onOverwriteRecipe ? (
|
||||
<>
|
||||
<TouchableOpacity
|
||||
onPress={handleSavePress}
|
||||
className="flex-1 border border-amber-500 py-3 rounded-lg items-center"
|
||||
>
|
||||
<Text className="text-amber-500 font-mono text-xs font-bold">SAVE AS</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={handleOverwritePress}
|
||||
className="flex-1 bg-amber-500 py-3 rounded-lg items-center"
|
||||
>
|
||||
<Text className="text-black font-mono text-xs font-bold">SAVE</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
onPress={handleSavePress}
|
||||
className="flex-1 bg-amber-500 py-3 rounded-lg items-center"
|
||||
>
|
||||
<Text className="text-black font-mono text-xs font-bold">SAVE RECIPE</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
|
||||
@@ -35,6 +35,13 @@ export async function saveCustomRecipe(recipe: Omit<Recipe, 'id' | 'isCustom'>):
|
||||
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<Recipe, 'id' | 'isCustom'>): Promise<void> {
|
||||
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<void> {
|
||||
const custom = await getCustomRecipes();
|
||||
const updated = custom.filter(r => r.id !== id);
|
||||
|
||||
Reference in New Issue
Block a user