Fix bugs from code review (state flow, idle timer, logger, kiosk, temp files)

This commit is contained in:
2026-05-31 20:10:23 +02:00
parent 58015340a2
commit a922ec29c4
5 changed files with 133 additions and 66 deletions
+44 -8
View File
@@ -1,11 +1,12 @@
import React, { useState, useEffect } from 'react'; 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 { GestureHandlerRootView } from 'react-native-gesture-handler';
import CameraScreen from './src/screens/CameraScreen'; import CameraScreen from './src/screens/CameraScreen';
import PreviewScreen from './src/screens/PreviewScreen'; import PreviewScreen from './src/screens/PreviewScreen';
import AdminScreen from './src/screens/AdminScreen'; import AdminScreen from './src/screens/AdminScreen';
import { loadSettings, saveSettings, AppSettings } from './src/services/settings'; import { loadSettings, saveSettings, AppSettings } from './src/services/settings';
import { logger } from './src/services/logger'; import { logger } from './src/services/logger';
import * as FileSystem from 'expo-file-system/legacy';
// Set up global error handler // Set up global error handler
const defaultErrorHandler = (global as any).ErrorUtils.getGlobalHandler(); const defaultErrorHandler = (global as any).ErrorUtils.getGlobalHandler();
@@ -24,6 +25,7 @@ type ScreenState = 'home' | 'camera' | 'preview' | 'admin';
export default function App() { export default function App() {
const [currentScreen, setCurrentScreen] = useState<ScreenState>('home'); const [currentScreen, setCurrentScreen] = useState<ScreenState>('home');
const [photoUris, setPhotoUris] = useState<string[]>([]); const [photoUris, setPhotoUris] = useState<string[]>([]);
const [captureHistory, setCaptureHistory] = useState<number[]>([]);
const [settings, setSettings] = useState<AppSettings | null>(null); const [settings, setSettings] = useState<AppSettings | null>(null);
// 1. Load configuration settings on app start // 1. Load configuration settings on app start
@@ -45,19 +47,29 @@ export default function App() {
}, []); }, []);
if (!settings) { if (!settings) {
return <View style={styles.loadingContainer} />; return (
<View style={styles.loadingContainer}>
<StatusBar hidden={true} />
<ActivityIndicator size="large" color="#ff2bd6" />
</View>
);
} }
// 2. Navigation Actions // 2. Navigation Actions
const handleStartBooth = () => { const handleStartBooth = () => {
setPhotoUris([]); setPhotoUris([]);
setCaptureHistory([]);
setCurrentScreen('camera'); setCurrentScreen('camera');
}; };
const handlePhotoCaptured = async (uri: string) => { const handlePhotoCaptured = async (uri: string) => {
await logger.log(`Photo captured via CameraScreen: ${uri}`); await logger.log(`Photo captured via CameraScreen: ${uri}`);
try { 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'); setCurrentScreen('preview');
await logger.log('State updated to preview screen successfully.'); await logger.log('State updated to preview screen successfully.');
} catch (e) { } catch (e) {
@@ -67,15 +79,30 @@ export default function App() {
// Called by CameraScreen when burst mode completes all photos // Called by CameraScreen when burst mode completes all photos
const handleBurstComplete = (uris: string[]) => { 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'); setCurrentScreen('preview');
}; };
const handleRetakeLast = () => { const handleRetakeLast = async () => {
// Remove last captured photo // Remove photos from the last capture event
const lastCaptureCount = captureHistory.length > 0 ? captureHistory[captureHistory.length - 1] : 1;
const updatedUris = [...photoUris]; const updatedUris = [...photoUris];
updatedUris.pop(); const removedUris = updatedUris.splice(-lastCaptureCount, lastCaptureCount);
setPhotoUris(updatedUris); 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 // Always let user retake
setCurrentScreen('camera'); setCurrentScreen('camera');
@@ -85,8 +112,17 @@ export default function App() {
setCurrentScreen('camera'); 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([]); setPhotoUris([]);
setCaptureHistory([]);
setCurrentScreen('home'); // Go to home when cancelled setCurrentScreen('home'); // Go to home when cancelled
}; };
+18 -2
View File
@@ -217,6 +217,22 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
</View> </View>
); );
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 ( return (
<Animated.View entering={FadeIn.duration(400)} exiting={FadeOut.duration(300)} style={styles.container}> <Animated.View entering={FadeIn.duration(400)} exiting={FadeOut.duration(300)} style={styles.container}>
<View style={styles.headerRow}> <View style={styles.headerRow}>
@@ -224,7 +240,7 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
<FontAwesomeIcon icon={faUserGear} size={22} color={THEME.colors.accent} /> <FontAwesomeIcon icon={faUserGear} size={22} color={THEME.colors.accent} />
<Text style={styles.title}>EINSTELLUNGEN</Text> <Text style={styles.title}>EINSTELLUNGEN</Text>
</View> </View>
<TouchableOpacity style={styles.closeBtn} onPress={onClose}> <TouchableOpacity style={styles.closeBtn} onPress={handleClose}>
<FontAwesomeIcon icon={faArrowLeft} size={14} color={THEME.colors.text} /> <FontAwesomeIcon icon={faArrowLeft} size={14} color={THEME.colors.text} />
<Text style={styles.closeBtnText}>Zurück zur Fotobox</Text> <Text style={styles.closeBtnText}>Zurück zur Fotobox</Text>
</TouchableOpacity> </TouchableOpacity>
@@ -523,7 +539,7 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Schließen</Text> <Text style={{ color: '#fff', fontWeight: 'bold' }}>Schließen</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
style={{ backgroundColor: THEME.colors.danger, padding: 15, borderRadius: 8, flex: 0.48, alignItems: 'center' }} style={{ backgroundColor: THEME.colors.error, padding: 15, borderRadius: 8, flex: 0.48, alignItems: 'center' }}
onPress={async () => { await logger.clearLogs(); setLogs(await logger.readLogs()); }} onPress={async () => { await logger.clearLogs(); setLogs(await logger.readLogs()); }}
> >
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Leeren</Text> <Text style={{ color: '#fff', fontWeight: 'bold' }}>Leeren</Text>
+11 -1
View File
@@ -159,8 +159,18 @@ export default function CameraScreen({
}, []); }, []);
// Cancel capture process // Cancel capture process
const handleLocalCancel = () => { const handleLocalCancel = async () => {
if (burstTimerRef.current) clearTimeout(burstTimerRef.current); 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); setCountdown(countdownDuration);
setIsCapturing(false); setIsCapturing(false);
setBurstIndex(0); setBurstIndex(0);
+19 -12
View File
@@ -183,16 +183,22 @@ export default function PreviewScreen({
setSelectedStickerId(null); 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 ── // ── Capture the final image ──
const captureComposite = async () => { const captureComposite = async (fallbackPhoto: string = currentPhoto) => {
try { try {
if (viewShotRef.current) { if (viewShotRef.current) {
return await viewShotRef.current.capture(); return await viewShotRef.current.capture();
} }
return currentPhoto; return fallbackPhoto;
} catch (e) { } catch (e) {
console.error('Failed to capture composite:', 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...'); setStatusMessage('Dein Foto wird vorbereitet...');
try { try {
setStatusMessage('Bild wird generiert...'); setStatusMessage('Bild wird generiert...');
const printUri = await captureComposite(); const printUri = await captureComposite(currentPhoto);
setStatusMessage('Druckauftrag wird an Drucker gesendet...'); setStatusMessage('Druckauftrag wird an Drucker gesendet...');
await printImageLocal(printUri, { await printImageLocal(printUri, {
@@ -230,7 +236,7 @@ export default function PreviewScreen({
setIsProcessing(true); setIsProcessing(true);
setStatusMessage('Bild wird generiert...'); setStatusMessage('Bild wird generiert...');
try { try {
const saveUri = await captureComposite(); const saveUri = await captureComposite(currentPhoto);
setStatusMessage('Wird in Galerie gespeichert...'); setStatusMessage('Wird in Galerie gespeichert...');
await saveToGallery(saveUri); await saveToGallery(saveUri);
@@ -249,27 +255,28 @@ export default function PreviewScreen({
// Auto-save and exit // Auto-save and exit
const handleExit = useCallback(async () => { const handleExit = useCallback(async () => {
if (stickers.length === 0 && layout === 'single' && !activeFrame && dateOverlay === 'off') { const state = stateRef.current;
onReset(); if (state.stickers.length === 0 && state.layout === 'single' && !state.activeFrame && state.dateOverlay === 'off') {
state.onReset();
return; return;
} }
setIsProcessing(true); setIsProcessing(true);
setStatusMessage('Wird gespeichert...'); setStatusMessage('Wird gespeichert...');
try { try {
const capturedUri = await captureComposite(); const capturedUri = await captureComposite(state.currentPhoto);
await saveToGallery(capturedUri); await saveToGallery(capturedUri);
setStatusMessage('Erfolgreich gespeichert!'); setStatusMessage('Erfolgreich gespeichert!');
setTimeout(() => { setTimeout(() => {
setIsProcessing(false); setIsProcessing(false);
onReset(); state.onReset();
}, 1000); }, 1000);
} catch (error) { } catch (error) {
console.error('Auto-saving on exit failed:', error); console.error('Auto-saving on exit failed:', error);
setIsProcessing(false); setIsProcessing(false);
onReset(); state.onReset();
} }
}, [stickers, layout, activeFrame, dateOverlay, onReset]); }, []); // Stable reference
// Idle timer logic // Idle timer logic
const resetIdleTimer = useCallback(() => { const resetIdleTimer = useCallback(() => {
@@ -291,7 +298,7 @@ export default function PreviewScreen({
setIsProcessing(true); setIsProcessing(true);
setStatusMessage('Wird vor dem Wiederholen gespeichert...'); setStatusMessage('Wird vor dem Wiederholen gespeichert...');
try { try {
const capturedUri = await captureComposite(); const capturedUri = await captureComposite(currentPhoto);
await saveToGallery(capturedUri); await saveToGallery(capturedUri);
} catch (error) { } catch (error) {
console.error('Auto-saving before retake failed:', error); console.error('Auto-saving before retake failed:', error);
+41 -43
View File
@@ -2,22 +2,17 @@ import * as FileSystem from 'expo-file-system/legacy';
const logFileUri = FileSystem.documentDirectory + 'app_logs.txt'; const logFileUri = FileSystem.documentDirectory + 'app_logs.txt';
export const logger = { // Queue to serialize file operations and prevent race conditions
log: async (message: string) => { let writeQueue = Promise.resolve();
try {
const timestamp = new Date().toISOString();
const logLine = `[INFO] ${timestamp}: ${message}\n`;
console.log(logLine.trim());
const enqueueWrite = (logLine: string) => {
writeQueue = writeQueue.then(async () => {
try {
const fileInfo = await FileSystem.getInfoAsync(logFileUri); const fileInfo = await FileSystem.getInfoAsync(logFileUri);
if (!fileInfo.exists) { if (!fileInfo.exists) {
await FileSystem.writeAsStringAsync(logFileUri, logLine, { encoding: FileSystem.EncodingType.UTF8 }); await FileSystem.writeAsStringAsync(logFileUri, logLine, { encoding: FileSystem.EncodingType.UTF8 });
} else { } 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 }); const current = await FileSystem.readAsStringAsync(logFileUri, { encoding: FileSystem.EncodingType.UTF8 });
// Keep last 100000 chars
let newContent = current + logLine; let newContent = current + logLine;
if (newContent.length > 100000) { if (newContent.length > 100000) {
newContent = newContent.slice(-100000); newContent = newContent.slice(-100000);
@@ -25,45 +20,45 @@ export const logger = {
await FileSystem.writeAsStringAsync(logFileUri, newContent, { encoding: FileSystem.EncodingType.UTF8 }); await FileSystem.writeAsStringAsync(logFileUri, newContent, { encoding: FileSystem.EncodingType.UTF8 });
} }
} catch (e) { } 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) => { error: async (message: string, error?: any) => {
try { const timestamp = new Date().toISOString();
const timestamp = new Date().toISOString(); let errorString = '';
let errorString = ''; if (error) {
if (error) { if (error instanceof Error) {
if (error instanceof Error) { errorString = `\n Name: ${error.name}\n Message: ${error.message}\n Stack: ${error.stack}`;
errorString = `\n Name: ${error.name}\n Message: ${error.message}\n Stack: ${error.stack}`; } else if (typeof error === 'object') {
} else if (typeof error === 'object') { errorString = `\n Object: ${JSON.stringify(error)}`;
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 });
} else { } else {
const current = await FileSystem.readAsStringAsync(logFileUri, { encoding: FileSystem.EncodingType.UTF8 }); errorString = `\n Value: ${String(error)}`;
let newContent = current + logLine;
if (newContent.length > 100000) {
newContent = newContent.slice(-100000);
}
await FileSystem.writeAsStringAsync(logFileUri, newContent, { encoding: FileSystem.EncodingType.UTF8 });
} }
} 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<string> => { readLogs: async (): Promise<string> => {
try { try {
// Ensure we wait for pending writes before reading
await writeQueue;
const fileInfo = await FileSystem.getInfoAsync(logFileUri); const fileInfo = await FileSystem.getInfoAsync(logFileUri);
if (!fileInfo.exists) return 'Keine Logs vorhanden.'; if (!fileInfo.exists) return 'Keine Logs vorhanden.';
return await FileSystem.readAsStringAsync(logFileUri, { encoding: FileSystem.EncodingType.UTF8 }); return await FileSystem.readAsStringAsync(logFileUri, { encoding: FileSystem.EncodingType.UTF8 });
@@ -73,10 +68,13 @@ export const logger = {
}, },
clearLogs: async () => { clearLogs: async () => {
try { writeQueue = writeQueue.then(async () => {
await FileSystem.deleteAsync(logFileUri, { idempotent: true }); try {
} catch (e) { await FileSystem.deleteAsync(logFileUri, { idempotent: true });
console.log('Failed to clear logs:', e); } catch (e) {
} console.warn('Failed to clear logs:', e);
}
});
return writeQueue;
} }
}; };