import React, { useState } from 'react'; import { View, Text, TouchableOpacity, ScrollView, FlatList, TextInput, Modal, } from 'react-native'; import Slider from '@react-native-community/slider'; import * as Haptics from 'expo-haptics'; import { Trash2, Plus, Check } from 'lucide-react-native'; import { Recipe, ColorAdjustments, FrameId } from '../types'; import { FRAMES } from '../utils/frameUtils'; interface AdjustmentPanelProps { activeTab: 'recipes' | 'iq' | 'wb' | 'filters' | 'frame'; recipes: Recipe[]; currentRecipeId: string; adjustments: ColorAdjustments; selectedFrame: FrameId; useGeotag: boolean; onSelectRecipe: (recipe: Recipe) => void; onUpdateAdjustments: (adjustments: Partial) => void; onUpdateFrame: (frameId: FrameId) => void; onToggleGeotag: (enabled: boolean) => void; onSaveRecipe: (name: string) => void; onDeleteRecipe: (id: string) => void; } export default function AdjustmentPanel({ activeTab, recipes, currentRecipeId, adjustments, selectedFrame, useGeotag, onSelectRecipe, onUpdateAdjustments, onUpdateFrame, onToggleGeotag, onSaveRecipe, onDeleteRecipe, }: AdjustmentPanelProps) { const [modalVisible, setModalVisible] = useState(false); const [newRecipeName, setNewRecipeName] = useState(''); const triggerHaptic = () => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); }; const renderRecipeItem = React.useCallback( ({ item }: { item: Recipe }) => ( ), [currentRecipeId, onSelectRecipe, onDeleteRecipe] ); const handleSliderValueChange = () => { // optional: light haptic while sliding, or only on completion. // Usually Haptic on sliding is too heavy, so we don't trigger it on every frame, // or we only trigger on sliding complete. }; const handleSavePress = () => { if (newRecipeName.trim()) { onSaveRecipe(newRecipeName.trim()); setNewRecipeName(''); setModalVisible(false); triggerHaptic(); } }; const renderSlider = ( label: string, value: number, min: number, max: number, step: number, onValueChange: (val: number) => void, displayValueMapper?: (val: number) => string ) => { const displayVal = displayValueMapper ? displayValueMapper(value) : value.toString(); return ( {label.toUpperCase()} {displayVal} ); }; switch (activeTab) { case 'recipes': return ( SELECT OR CREATE PRESETS setModalVisible(true)} className="flex-row items-center bg-amber-500 px-3 py-1 rounded space-x-1" activeOpacity={0.7} > SAVE CURRENT item.id} numColumns={2} className="p-2" columnWrapperStyle={{ justifyContent: 'space-between' }} renderItem={renderRecipeItem} /> {/* Save Recipe Modal */} setModalVisible(false)} > SAVE NEW RECIPE setModalVisible(false)} className="flex-1 bg-zinc-800 py-3 rounded-lg items-center" > CANCEL SAVE RECIPE ); case 'iq': return ( {renderSlider('Exposure', adjustments.exposure, -10, 10, 1, (val) => onUpdateAdjustments({ exposure: val }) )} {renderSlider('Contrast', adjustments.contrast, -10, 10, 1, (val) => onUpdateAdjustments({ contrast: val }) )} {renderSlider('Saturation', adjustments.saturation, -10, 10, 1, (val) => onUpdateAdjustments({ saturation: val }) )} ); case 'wb': return ( {renderSlider( 'Color Temp (Kelvin)', adjustments.temperature, 2500, 10000, 100, (val) => onUpdateAdjustments({ temperature: val }), (val) => `${val}K` )} {renderSlider('Tint (Green-Magenta)', adjustments.tint, -10, 10, 1, (val) => onUpdateAdjustments({ tint: val }) )} COLOR CHROME EFFECT {(['none', 'weak', 'strong'] as const).map((chrome) => { const isActive = adjustments.colorChrome === chrome; return ( { onUpdateAdjustments({ colorChrome: chrome }); triggerHaptic(); }} className={`flex-1 py-1.5 rounded-md items-center ${ isActive ? 'bg-amber-500' : 'bg-transparent' }`} activeOpacity={0.7} > {chrome.toUpperCase()} ); })} ); case 'filters': return ( {renderSlider('Highlight Contrast', adjustments.highlight, -10, 10, 1, (val) => onUpdateAdjustments({ highlight: val }) )} {renderSlider('Shadow Contrast', adjustments.shadow, -10, 10, 1, (val) => onUpdateAdjustments({ shadow: val }) )} {renderSlider('Denoise (Blur)', adjustments.denoise, 0, 10, 1, (val) => onUpdateAdjustments({ denoise: val }) )} {renderSlider('Clarity (Sharp/Soft)', adjustments.clarity, -10, 10, 1, (val) => onUpdateAdjustments({ clarity: val }) )} {renderSlider('Monochrome Grain', adjustments.grain, 0, 10, 1, (val) => onUpdateAdjustments({ grain: val }) )} ); case 'frame': return ( SELECT PNG FRAME OVERLAY {FRAMES.map((f: { id: FrameId; name: string }) => { const isActive = selectedFrame === f.id; return ( { onUpdateFrame(f.id); triggerHaptic(); }} className={`px-3 py-2 m-1 rounded border font-mono ${ isActive ? 'bg-amber-500/10 border-amber-500 text-amber-500' : 'bg-titan border-zinc-800 text-zinc-400' }`} activeOpacity={0.7} > {f.name} ); })} GPS GEOTAG WATERMARK Stamps coordinates & place names onto photo { onToggleGeotag(!useGeotag); triggerHaptic(); }} className={`w-12 h-6 rounded-full p-0.5 justify-center ${ useGeotag ? 'bg-amber-500 items-end' : 'bg-zinc-800 items-start' }`} activeOpacity={0.8} > {useGeotag && } ); default: return null; } } interface RecipeItemProps { item: Recipe; isActive: boolean; onSelect: (recipe: Recipe) => void; onDelete: (id: string) => void; triggerHaptic: () => void; } const RecipeItem = React.memo(({ item, isActive, onSelect, onDelete, triggerHaptic }: RecipeItemProps) => { return ( { onSelect(item); triggerHaptic(); }} className={`flex-1 m-1.5 p-3 rounded-lg border ${ isActive ? 'bg-amber-500/10 border-amber-500' : 'bg-titan border-zinc-800/80' }`} activeOpacity={0.7} > {item.name} {item.baseFilter.toUpperCase()} {item.isCustom && ( { e.stopPropagation(); onDelete(item.id); triggerHaptic(); }} className="p-1" > )} ); });