diff --git a/App.tsx b/App.tsx index cc735b2a..d6614fe4 100644 --- a/App.tsx +++ b/App.tsx @@ -1,11 +1,12 @@ import React, { useState, useEffect } from 'react'; -import { StyleSheet, View, SafeAreaView, StatusBar } from 'react-native'; +import { StyleSheet, View, SafeAreaView, StatusBar, ActivityIndicator } from 'react-native'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import CameraScreen from './src/screens/CameraScreen'; import PreviewScreen from './src/screens/PreviewScreen'; import AdminScreen from './src/screens/AdminScreen'; import { loadSettings, saveSettings, AppSettings } from './src/services/settings'; import { logger } from './src/services/logger'; +import * as FileSystem from 'expo-file-system/legacy'; // Set up global error handler const defaultErrorHandler = (global as any).ErrorUtils.getGlobalHandler(); @@ -24,6 +25,7 @@ type ScreenState = 'home' | 'camera' | 'preview' | 'admin'; export default function App() { const [currentScreen, setCurrentScreen] = useState('home'); const [photoUris, setPhotoUris] = useState([]); + const [captureHistory, setCaptureHistory] = useState([]); const [settings, setSettings] = useState(null); // 1. Load configuration settings on app start @@ -45,19 +47,29 @@ export default function App() { }, []); if (!settings) { - return ; + return ( + + + ); } // 2. Navigation Actions const handleStartBooth = () => { setPhotoUris([]); + setCaptureHistory([]); setCurrentScreen('camera'); }; const handlePhotoCaptured = async (uri: string) => { await logger.log(`Photo captured via CameraScreen: ${uri}`); try { - setPhotoUris((prev) => [...prev, uri]); + setPhotoUris((prev) => { + const next = [...prev, uri]; + return next.length > 4 ? next.slice(-4) : next; + }); + setCaptureHistory((prev) => [...prev, 1]); setCurrentScreen('preview'); await logger.log('State updated to preview screen successfully.'); } catch (e) { @@ -67,15 +79,30 @@ export default function App() { // Called by CameraScreen when burst mode completes all photos const handleBurstComplete = (uris: string[]) => { - setPhotoUris((prev) => [...prev, ...uris]); + setPhotoUris((prev) => { + const next = [...prev, ...uris]; + return next.length > 4 ? next.slice(-4) : next; + }); + setCaptureHistory((prev) => [...prev, uris.length]); setCurrentScreen('preview'); }; - const handleRetakeLast = () => { - // Remove last captured photo + const handleRetakeLast = async () => { + // Remove photos from the last capture event + const lastCaptureCount = captureHistory.length > 0 ? captureHistory[captureHistory.length - 1] : 1; const updatedUris = [...photoUris]; - updatedUris.pop(); + const removedUris = updatedUris.splice(-lastCaptureCount, lastCaptureCount); setPhotoUris(updatedUris); + setCaptureHistory((prev) => prev.slice(0, -1)); + + // Clean up temp files + for (const uri of removedUris) { + try { + await FileSystem.deleteAsync(uri, { idempotent: true }); + } catch (e) { + console.warn('Failed to delete temp file:', e); + } + } // Always let user retake setCurrentScreen('camera'); @@ -85,8 +112,17 @@ export default function App() { setCurrentScreen('camera'); }; - const handleReset = () => { + const handleReset = async () => { + // Clean up temp files + for (const uri of photoUris) { + try { + await FileSystem.deleteAsync(uri, { idempotent: true }); + } catch (e) { + console.warn('Failed to delete temp file:', e); + } + } setPhotoUris([]); + setCaptureHistory([]); setCurrentScreen('home'); // Go to home when cancelled }; diff --git a/src/screens/AdminScreen.tsx b/src/screens/AdminScreen.tsx index fe5fa4ed..7f97efef 100644 --- a/src/screens/AdminScreen.tsx +++ b/src/screens/AdminScreen.tsx @@ -217,6 +217,22 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS ); + const handleClose = () => { + // If user exits without saving, revert Kiosk Mode to match saved settings + if (kioskActive !== currentSettings.kioskModeEnabled) { + try { + if (currentSettings.kioskModeEnabled) { + KioskMode.startKiosk(); + } else { + KioskMode.stopKiosk(); + } + } catch (e) { + console.warn('Failed to revert Kiosk mode:', e); + } + } + onClose(); + }; + return ( @@ -224,7 +240,7 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS EINSTELLUNGEN - + Zurück zur Fotobox @@ -523,7 +539,7 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS Schließen { await logger.clearLogs(); setLogs(await logger.readLogs()); }} > Leeren diff --git a/src/screens/CameraScreen.tsx b/src/screens/CameraScreen.tsx index 6e39bafa..6d5a89c3 100644 --- a/src/screens/CameraScreen.tsx +++ b/src/screens/CameraScreen.tsx @@ -159,8 +159,18 @@ export default function CameraScreen({ }, []); // Cancel capture process - const handleLocalCancel = () => { + const handleLocalCancel = async () => { if (burstTimerRef.current) clearTimeout(burstTimerRef.current); + + // Clean up any temp files from a partially completed burst + for (const uri of burstUris) { + try { + await FileSystem.deleteAsync(uri, { idempotent: true }); + } catch (e) { + console.warn('Failed to delete temp file on cancel:', e); + } + } + setCountdown(countdownDuration); setIsCapturing(false); setBurstIndex(0); diff --git a/src/screens/PreviewScreen.tsx b/src/screens/PreviewScreen.tsx index d57cc1fe..c5be4bcb 100644 --- a/src/screens/PreviewScreen.tsx +++ b/src/screens/PreviewScreen.tsx @@ -183,16 +183,22 @@ export default function PreviewScreen({ setSelectedStickerId(null); }, []); + // Keep latest state in a ref to avoid stale closures and rapid re-renders + const stateRef = useRef({ stickers, layout, activeFrame, dateOverlay, onReset, currentPhoto }); + React.useEffect(() => { + stateRef.current = { stickers, layout, activeFrame, dateOverlay, onReset, currentPhoto }; + }, [stickers, layout, activeFrame, dateOverlay, onReset, currentPhoto]); + // ── Capture the final image ── - const captureComposite = async () => { + const captureComposite = async (fallbackPhoto: string = currentPhoto) => { try { if (viewShotRef.current) { return await viewShotRef.current.capture(); } - return currentPhoto; + return fallbackPhoto; } catch (e) { console.error('Failed to capture composite:', e); - return currentPhoto; + return fallbackPhoto; } }; @@ -202,7 +208,7 @@ export default function PreviewScreen({ setStatusMessage('Dein Foto wird vorbereitet...'); try { setStatusMessage('Bild wird generiert...'); - const printUri = await captureComposite(); + const printUri = await captureComposite(currentPhoto); setStatusMessage('Druckauftrag wird an Drucker gesendet...'); await printImageLocal(printUri, { @@ -230,7 +236,7 @@ export default function PreviewScreen({ setIsProcessing(true); setStatusMessage('Bild wird generiert...'); try { - const saveUri = await captureComposite(); + const saveUri = await captureComposite(currentPhoto); setStatusMessage('Wird in Galerie gespeichert...'); await saveToGallery(saveUri); @@ -249,27 +255,28 @@ export default function PreviewScreen({ // Auto-save and exit const handleExit = useCallback(async () => { - if (stickers.length === 0 && layout === 'single' && !activeFrame && dateOverlay === 'off') { - onReset(); + const state = stateRef.current; + if (state.stickers.length === 0 && state.layout === 'single' && !state.activeFrame && state.dateOverlay === 'off') { + state.onReset(); return; } setIsProcessing(true); setStatusMessage('Wird gespeichert...'); try { - const capturedUri = await captureComposite(); + const capturedUri = await captureComposite(state.currentPhoto); await saveToGallery(capturedUri); setStatusMessage('Erfolgreich gespeichert!'); setTimeout(() => { setIsProcessing(false); - onReset(); + state.onReset(); }, 1000); } catch (error) { console.error('Auto-saving on exit failed:', error); setIsProcessing(false); - onReset(); + state.onReset(); } - }, [stickers, layout, activeFrame, dateOverlay, onReset]); + }, []); // Stable reference // Idle timer logic const resetIdleTimer = useCallback(() => { @@ -291,7 +298,7 @@ export default function PreviewScreen({ setIsProcessing(true); setStatusMessage('Wird vor dem Wiederholen gespeichert...'); try { - const capturedUri = await captureComposite(); + const capturedUri = await captureComposite(currentPhoto); await saveToGallery(capturedUri); } catch (error) { console.error('Auto-saving before retake failed:', error); diff --git a/src/services/logger.ts b/src/services/logger.ts index 5141c980..1a4a73ad 100644 --- a/src/services/logger.ts +++ b/src/services/logger.ts @@ -2,22 +2,17 @@ import * as FileSystem from 'expo-file-system/legacy'; const logFileUri = FileSystem.documentDirectory + 'app_logs.txt'; -export const logger = { - log: async (message: string) => { +// Queue to serialize file operations and prevent race conditions +let writeQueue = Promise.resolve(); + +const enqueueWrite = (logLine: string) => { + writeQueue = writeQueue.then(async () => { try { - const timestamp = new Date().toISOString(); - const logLine = `[INFO] ${timestamp}: ${message}\n`; - console.log(logLine.trim()); - const fileInfo = await FileSystem.getInfoAsync(logFileUri); if (!fileInfo.exists) { await FileSystem.writeAsStringAsync(logFileUri, logLine, { encoding: FileSystem.EncodingType.UTF8 }); } else { - // Read existing and append (simplistic approach for small logs) - // expo-file-system doesn't have an append method out of the box in older versions, - // but wait, we can just read, then write. To avoid OOM we should truncate if too big. const current = await FileSystem.readAsStringAsync(logFileUri, { encoding: FileSystem.EncodingType.UTF8 }); - // Keep last 100000 chars let newContent = current + logLine; if (newContent.length > 100000) { newContent = newContent.slice(-100000); @@ -25,45 +20,45 @@ export const logger = { await FileSystem.writeAsStringAsync(logFileUri, newContent, { encoding: FileSystem.EncodingType.UTF8 }); } } catch (e) { - console.log('Failed to write log:', e); + console.warn('Failed to write to log file:', e); + // We catch here so the queue is not broken for subsequent logs } + }).catch((e) => { + console.error('Queue error:', e); + }); + return writeQueue; +}; + +export const logger = { + log: async (message: string) => { + const timestamp = new Date().toISOString(); + const logLine = `[INFO] ${timestamp}: ${message}\n`; + console.log(logLine.trim()); + return enqueueWrite(logLine); }, error: async (message: string, error?: any) => { - try { - const timestamp = new Date().toISOString(); - let errorString = ''; - if (error) { - if (error instanceof Error) { - errorString = `\n Name: ${error.name}\n Message: ${error.message}\n Stack: ${error.stack}`; - } else if (typeof error === 'object') { - errorString = `\n Object: ${JSON.stringify(error)}`; - } else { - errorString = `\n Value: ${String(error)}`; - } - } - - const logLine = `[ERROR] ${timestamp}: ${message}${errorString}\n`; - console.error(logLine.trim()); - - const fileInfo = await FileSystem.getInfoAsync(logFileUri); - if (!fileInfo.exists) { - await FileSystem.writeAsStringAsync(logFileUri, logLine, { encoding: FileSystem.EncodingType.UTF8 }); + const timestamp = new Date().toISOString(); + let errorString = ''; + if (error) { + if (error instanceof Error) { + errorString = `\n Name: ${error.name}\n Message: ${error.message}\n Stack: ${error.stack}`; + } else if (typeof error === 'object') { + errorString = `\n Object: ${JSON.stringify(error)}`; } else { - const current = await FileSystem.readAsStringAsync(logFileUri, { encoding: FileSystem.EncodingType.UTF8 }); - let newContent = current + logLine; - if (newContent.length > 100000) { - newContent = newContent.slice(-100000); - } - await FileSystem.writeAsStringAsync(logFileUri, newContent, { encoding: FileSystem.EncodingType.UTF8 }); + errorString = `\n Value: ${String(error)}`; } - } catch (e) { - console.log('Failed to write error log:', e); } + + const logLine = `[ERROR] ${timestamp}: ${message}${errorString}\n`; + console.error(logLine.trim()); + return enqueueWrite(logLine); }, readLogs: async (): Promise => { try { + // Ensure we wait for pending writes before reading + await writeQueue; const fileInfo = await FileSystem.getInfoAsync(logFileUri); if (!fileInfo.exists) return 'Keine Logs vorhanden.'; return await FileSystem.readAsStringAsync(logFileUri, { encoding: FileSystem.EncodingType.UTF8 }); @@ -73,10 +68,13 @@ export const logger = { }, clearLogs: async () => { - try { - await FileSystem.deleteAsync(logFileUri, { idempotent: true }); - } catch (e) { - console.log('Failed to clear logs:', e); - } + writeQueue = writeQueue.then(async () => { + try { + await FileSystem.deleteAsync(logFileUri, { idempotent: true }); + } catch (e) { + console.warn('Failed to clear logs:', e); + } + }); + return writeQueue; } };