diff --git a/App.tsx b/App.tsx index 55e5715..f0bb21f 100644 --- a/App.tsx +++ b/App.tsx @@ -30,6 +30,9 @@ import './global.css'; const SESSION_KEY = 'saved_session'; const STARTUP_MODE_KEY = 'startup_mode'; +// Camera/device settings that survive restarts regardless of startup mode +// (the session file only restores the full look under 'last session'). +const CAMERA_SETTINGS_KEY = 'camera_settings'; // P5: native Kotlin export path. EXPO_PUBLIC_NATIVE_EXPORT=1 swaps the Skia // render engine for the RecipesCamExport native pipeline (same look contract, @@ -204,10 +207,31 @@ export default function App() { return () => subscription.remove(); }, [mode, libraryImageUri, adjustments, selectedFrame, useGeotag, selectedRecipe, gpsInfo, customWm, aspectRatio, meterMode, shutterSound]); + // Guards the camera-settings persist effect below: until initSession has + // read the stored values, changes are just the initial defaults — writing + // them would overwrite the saved settings (async restore loses the race + // against the mount effect). + const settingsHydratedRef = useRef(false); + const initSession = async () => { const list = await getAllRecipes(); setRecipes(list); + // Device/camera settings restore first: they apply under BOTH startup + // modes ('default' resets only the look, not these hardware choices). + try { + const raw = await AsyncStorage.getItem(CAMERA_SETTINGS_KEY); + if (raw) { + const s = JSON.parse(raw); + if (s.shutterSound === 'on' || s.shutterSound === 'off') setShutterSound(s.shutterSound); + if (s.meterMode === 'evaluative' || s.meterMode === 'highlight') setMeterMode(s.meterMode); + if (typeof s.rawEnabled === 'boolean') setRawEnabled(s.rawEnabled); + } + } catch (e) { + console.warn('Failed to read camera settings:', e); + } + settingsHydratedRef.current = true; + // Startup mode: 'default' opens camera + PROVIA film sim (neutral // adjustments, DEFAULT_ADJUSTMENTS) at 4:3. 'last' restores the previous // session from AsyncStorage; with no saved session it falls back to the @@ -266,9 +290,11 @@ export default function App() { setAdjustments({ ...DEFAULT_ADJUSTMENTS, ...session.adjustments }); if (session.selectedFrame) setSelectedFrame(session.selectedFrame); if (session.useGeotag !== undefined) setUseGeotag(session.useGeotag); - if (session.aspectRatio === '4:3' || session.aspectRatio === '3:2') setAspectRatio(session.aspectRatio); - if (session.meterMode === 'evaluative' || session.meterMode === 'highlight') setMeterMode(session.meterMode); - if (session.shutterSound === 'on' || session.shutterSound === 'off') setShutterSound(session.shutterSound); + if (session.aspectRatio === 'full' || session.aspectRatio === '4:3' || session.aspectRatio === '3:2') setAspectRatio(session.aspectRatio); + // NOTE: meterMode/shutterSound/rawEnabled are NOT restored from the + // session snapshot here — the session file is only written on background, + // so it can be stale after a force-kill. CAMERA_SETTINGS_KEY above + // persists them on every change and is authoritative for these three. if (session.gpsInfo) setGpsInfo(session.gpsInfo); if (session.customWm && typeof session.customWm === 'object') { // Restore defensively: a session saved by an older build may miss the @@ -384,6 +410,17 @@ export default function App() { AsyncStorage.setItem(STARTUP_MODE_KEY, mode).catch(() => {}); }; + // Persist device/camera settings on every change — not just on background — + // so they survive a kill/relaunch even under 'default' startup, which only + // resets the look (mode/ratio/recipe/geotag), not these. + useEffect(() => { + if (!settingsHydratedRef.current) return; + AsyncStorage.setItem( + CAMERA_SETTINGS_KEY, + JSON.stringify({ shutterSound, meterMode, rawEnabled }) + ).catch(() => {}); + }, [shutterSound, meterMode, rawEnabled]); + // Latest-rawEnabled ref so the stable session-error callback below can read // the live toggle without re-subscribing the Camera session on every render. const rawEnabledRef = useRef(rawEnabled);