diff --git a/App.tsx b/App.tsx index a3a0821..aa0cccc 100644 --- a/App.tsx +++ b/App.tsx @@ -95,6 +95,22 @@ export default function App() { const [shutterSound, setShutterSound] = useState('on'); const photoSeq = useRef(0); const [isProcessing, setIsProcessing] = useState(false); + // Short shutter-burst lock: guards the capturePhotoToFile calls from + // overlapping. Released as soon as the frames are written — NOT after the + // render, so a new shot can start while the previous photo is still being + // processed in the background. + const captureBusyRef = useRef(false); + // FIFO background render queue: processAndExportPhoto (Skia render + gallery + // save, ~1s) runs OFF the shutter path, one at a time, so the next photo can + // be taken immediately. FIFO keeps history order == shutter order. Renders + // are heavy on the JS thread but yield between awaits — UI keeps responding. + const exportQueueRef = useRef>(Promise.resolve()); + const enqueueExport = (task: () => Promise) => { + exportQueueRef.current = exportQueueRef.current + .catch(() => {}) // a failed render must not stall the queue + .then(task) + .catch((e) => console.error('Background export failed:', e)); + }; // Hardware Permissions (vision-camera) const { hasPermission: cameraPermissionGranted, requestPermission: requestCameraPermission } = useCameraPermission(); @@ -435,7 +451,7 @@ export default function App() { Alert.alert('Camera Not Ready', 'Please wait for the camera to start, then try again.'); return; } - setIsProcessing(true); + captureBusyRef.current = true; Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); try { // A photo taken now is at the device's current spot — resolve device GPS @@ -449,7 +465,6 @@ export default function App() { } } - const tShot = Date.now(); // Hardware AE bias for this shot (best-effort, bounded retries inside). // Applied here — right before the capture — instead of live during EV // slider drags, which re-locked AE on the Xiaomi and flashed preview. @@ -478,42 +493,56 @@ export default function App() { } } - const tExp = Date.now(); - const result = await processAndExportPhoto( - sourceUri, - { ...activeRecipe, adjustments: renderAdj, frameId: selectedFrame, useGeotag }, - selectedFrame, - useGeotag, - captureGps, - // 300dpi header + full-res screen sharpen. evFromCamera: the AE bias - // already applied exposure compensation at capture — don't add 2^EV. - // aspect: crop to the on-screen composition ratio (full = keep 4:3 native). - { - dpi: 300, - sharpen: true, - evFromCamera: true, - aspect: aspectRatio === 'full' ? undefined : aspectRatio, + // Snapshot the look at shutter time, then render OFF the shutter path: + // processAndExportPhoto (~1s Skia render + gallery save) is enqueued on + // the FIFO background queue — the shutter returns in tens of ms and the + // next shot can be taken immediately; no full-screen RENDERING overlay. + // Finished photos land in history (and the gallery) when their turn + // completes; the thumbnail button shows each as it arrives. + const recipeName = activeRecipe.name; + const render = () => + processAndExportPhoto( + sourceUri, + { ...activeRecipe, adjustments: renderAdj, frameId: selectedFrame, useGeotag }, + selectedFrame, + useGeotag, + captureGps, + // 300dpi header + full-res screen sharpen. evFromCamera: the AE bias + // already applied exposure compensation at capture — don't add 2^EV. + // aspect: crop to the on-screen composition ratio (full = keep 4:3 native). + { + dpi: 300, + sharpen: true, + evFromCamera: true, + aspect: aspectRatio === 'full' ? undefined : aspectRatio, + } + ); + enqueueExport(async () => { + try { + const result = await render(); + if (result) { + // No auto preview after a capture — the thumbnail button opens + // the swipe viewer. Saved to session history either way. + pushPhoto({ + uri: result.uri, + recipeName, + savedToLibrary: result.savedToLibrary, + assetId: result.assetId, + }); + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + } else { + Alert.alert('Failed', 'Error occurred while processing the captured photo.'); + } + } catch (error) { + console.error('Camera export failed:', error); + Alert.alert('Failed', 'Error occurred while processing the captured photo.'); } - ); - - if (result) { - // No auto preview after a capture — the thumbnail button opens the - // swipe viewer. Saved to session history either way. - pushPhoto({ - uri: result.uri, - recipeName: activeRecipe.name, - savedToLibrary: result.savedToLibrary, - assetId: result.assetId, - }); - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); - } else { - Alert.alert('Failed', 'Error occurred while processing the captured photo.'); - } + }); } catch (error) { console.error('Camera capture failed:', error); Alert.alert('Capture Failed', 'Could not take the photo. See logs for details.'); } finally { - setIsProcessing(false); + captureBusyRef.current = false; } return; }