feat(recipe): edit opens the prefilled create form; fields select their value on tap
This commit is contained in:
@@ -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<Recipe, 'id' | 'isCustom'>) => 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<Recipe, 'id' | 'isCustom'>) => void;
|
||||
onSelectRecipe: (recipe: Recipe) => void;
|
||||
onUpdateAdjustments: (adjustments: Partial<ColorAdjustments>) => 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}
|
||||
<RecipeCreateModal
|
||||
visible={createVisible}
|
||||
visible={createVisible || !!editingRecipe}
|
||||
currentBase={currentBaseFilter}
|
||||
onClose={() => setCreateVisible(false)}
|
||||
initial={editingRecipe ?? null}
|
||||
onClose={() => {
|
||||
setCreateVisible(false);
|
||||
onCloseEdit();
|
||||
}}
|
||||
onSave={(recipe) => {
|
||||
onCreateRecipe(recipe);
|
||||
if (editingRecipe) onUpdateRecipe(recipe);
|
||||
else onCreateRecipe(recipe);
|
||||
setShowRecipes(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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<Recipe, 'id' | 'isCustom'>) => void;
|
||||
}
|
||||
@@ -127,6 +131,7 @@ const NumRow = ({ label, value, onChange, placeholder, width = 58 }: NumRowProps
|
||||
<TextInput
|
||||
value={value}
|
||||
onChangeText={onChange}
|
||||
selectTextOnFocus
|
||||
keyboardType="numbers-and-punctuation"
|
||||
placeholder={placeholder ?? '0'}
|
||||
placeholderTextColor="#52525b"
|
||||
@@ -139,6 +144,7 @@ const NumRow = ({ label, value, onChange, placeholder, width = 58 }: NumRowProps
|
||||
export default function RecipeCreateModal({
|
||||
visible,
|
||||
currentBase,
|
||||
initial,
|
||||
onClose,
|
||||
onSave,
|
||||
}: RecipeCreateModalProps) {
|
||||
@@ -151,8 +157,8 @@ export default function RecipeCreateModal({
|
||||
|
||||
// Every sim's standard values as form state — used to seed the modal and to
|
||||
// reload the form when the SIMULATION chip changes.
|
||||
const valuesOf = (b: BaseFilter) => {
|
||||
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"
|
||||
>
|
||||
<View className="flex-row items-center justify-between mb-2">
|
||||
<Text className="text-white font-mono text-sm font-bold tracking-wider">CREATE RECIPE</Text>
|
||||
<Text className="text-white font-mono text-sm font-bold tracking-wider">
|
||||
{initial ? 'EDIT RECIPE' : 'CREATE RECIPE'}
|
||||
</Text>
|
||||
<TouchableOpacity onPress={onClose} className="px-2 py-1" activeOpacity={0.7}>
|
||||
<Text className="text-zinc-400 font-mono text-xs font-bold">✕</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -301,6 +315,7 @@ export default function RecipeCreateModal({
|
||||
<TextInput
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
selectTextOnFocus
|
||||
placeholder="Recipe name (e.g. Fuji Retro)"
|
||||
placeholderTextColor="#71717a"
|
||||
className="bg-carbon border border-zinc-800 rounded-lg p-2 text-white font-mono text-sm mb-2"
|
||||
@@ -366,6 +381,7 @@ export default function RecipeCreateModal({
|
||||
<TextInput
|
||||
value={wbRed}
|
||||
onChangeText={setWbRed}
|
||||
selectTextOnFocus
|
||||
keyboardType="numbers-and-punctuation"
|
||||
placeholder="-9..9"
|
||||
placeholderTextColor="#52525b"
|
||||
@@ -376,6 +392,7 @@ export default function RecipeCreateModal({
|
||||
<TextInput
|
||||
value={wbBlue}
|
||||
onChangeText={setWbBlue}
|
||||
selectTextOnFocus
|
||||
keyboardType="numbers-and-punctuation"
|
||||
placeholder="-9..9"
|
||||
placeholderTextColor="#52525b"
|
||||
@@ -407,7 +424,7 @@ export default function RecipeCreateModal({
|
||||
<Text className="text-zinc-300 font-mono text-xs font-semibold">CANCEL</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={handleSave} className="flex-1 bg-amber-500 py-3 rounded-lg items-center" activeOpacity={0.7}>
|
||||
<Text className="text-black font-mono text-xs font-bold">SAVE RECIPE</Text>
|
||||
<Text className="text-black font-mono text-xs font-bold">{initial ? 'SAVE' : 'SAVE RECIPE'}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<View
|
||||
@@ -86,9 +58,9 @@ export default function TopBar({ mode, recipeName, onSaveRecipe, onOverwriteReci
|
||||
</View>
|
||||
|
||||
<View className="flex-row items-center space-x-2">
|
||||
{onOverwriteRecipe && (
|
||||
{onEditRecipe && (
|
||||
<TouchableOpacity
|
||||
onPress={openEdit}
|
||||
onPress={onEditRecipe}
|
||||
className="rounded-full border border-zinc-700/80 bg-black/45 p-2.5"
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
@@ -166,51 +138,6 @@ export default function TopBar({ mode, recipeName, onSaveRecipe, onOverwriteReci
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
transparent
|
||||
animationType="slide"
|
||||
visible={editVisible}
|
||||
onRequestClose={() => setEditVisible(false)}
|
||||
>
|
||||
<View className="absolute inset-0 bg-black/60" />
|
||||
<KeyboardAvoidingView
|
||||
style={{ flex: 1 }}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
>
|
||||
<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">
|
||||
EDIT RECIPE
|
||||
</Text>
|
||||
<TextInput
|
||||
value={editName}
|
||||
onChangeText={setEditName}
|
||||
placeholder="Recipe name"
|
||||
placeholderTextColor="#71717a"
|
||||
className="bg-carbon border border-zinc-800 rounded-lg p-3 text-white font-mono text-sm mb-2"
|
||||
autoFocus
|
||||
onSubmitEditing={handleEditSave}
|
||||
/>
|
||||
<Text className="text-zinc-500 font-mono text-[10px] mb-4">
|
||||
New name saves a copy · same name overwrites
|
||||
</Text>
|
||||
<View className="flex-row space-x-3">
|
||||
<TouchableOpacity
|
||||
onPress={() => setEditVisible(false)}
|
||||
className="flex-1 bg-zinc-800 py-3 rounded-lg items-center"
|
||||
>
|
||||
<Text className="text-zinc-300 font-mono text-xs font-semibold">CANCEL</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={handleEditSave}
|
||||
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>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user