diff --git a/App.tsx b/App.tsx index c72c7ead..f9bd7fd3 100644 --- a/App.tsx +++ b/App.tsx @@ -5,7 +5,7 @@ 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 { logger, setLogsEnabled } from './src/services/logger'; import * as FileSystem from 'expo-file-system/legacy'; // Set up global error handler @@ -33,6 +33,7 @@ export default function App() { async function initApp() { const savedSettings = await loadSettings(); setSettings(savedSettings); + setLogsEnabled(savedSettings.enableLogs); // Auto-start Kiosk mode if it was configured as enabled if (savedSettings.kioskModeEnabled) { @@ -129,6 +130,7 @@ export default function App() { const handleSaveSettings = (updated: AppSettings) => { setSettings(updated); + setLogsEnabled(updated.enableLogs); }; const handleOpenAdmin = () => { @@ -160,6 +162,8 @@ export default function App() { burstIntervalSec={settings.burstIntervalSec} frameMode={settings.frameMode} selectedFrameId={settings.selectedFrameId} + footerText={settings.footerText} + useFrontCamera={settings.useFrontCamera} /> @@ -200,11 +204,11 @@ export default function App() { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#050510', + backgroundColor: 'green', }, loadingContainer: { flex: 1, - backgroundColor: '#050510', + backgroundColor: 'red', justifyContent: 'center', alignItems: 'center', }, diff --git a/app-debug.apk b/app-debug.apk index c0fd142d..a455b7d4 100644 Binary files a/app-debug.apk and b/app-debug.apk differ diff --git a/src/components/DraggableSticker.tsx b/src/components/DraggableSticker.tsx index b4902074..262691c1 100644 --- a/src/components/DraggableSticker.tsx +++ b/src/components/DraggableSticker.tsx @@ -154,6 +154,7 @@ const styles = StyleSheet.create({ justifyContent: 'center', zIndex: 100, elevation: 21, + padding: 30, // Large invisible padding to significantly increase the touch/pinch hitbox }, emojiText: { fontSize: 48, @@ -178,8 +179,8 @@ const styles = StyleSheet.create({ }, deleteBtn: { position: 'absolute', - top: -10, - right: -10, + top: 15, + right: 15, width: 24, height: 24, borderRadius: 12, diff --git a/src/screens/AdminScreen.tsx b/src/screens/AdminScreen.tsx index d31ce78c..0bd34e70 100644 --- a/src/screens/AdminScreen.tsx +++ b/src/screens/AdminScreen.tsx @@ -9,6 +9,7 @@ import { Switch, Alert, Modal, + BackHandler, } from 'react-native'; import { LinearGradient } from 'expo-linear-gradient'; import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; @@ -81,6 +82,56 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS const [burstCount, setBurstCount] = useState(String(currentSettings.burstCount)); const [burstInterval, setBurstInterval] = useState(String(currentSettings.burstIntervalSec)); const [welcomeText, setWelcomeText] = useState(currentSettings.welcomeText); + const [footerText, setFooterText] = useState(currentSettings.footerText); + const [useFrontCamera, setUseFrontCamera] = useState(currentSettings.useFrontCamera); + const [enableLogs, setEnableLogs] = useState(currentSettings.enableLogs); + + const hasUnsavedChanges = () => { + return ( + countdown !== String(currentSettings.countdownDuration) || + printerIp !== currentSettings.printerIp || + password !== currentSettings.adminPassword || + kioskActive !== currentSettings.kioskModeEnabled || + frameMode !== currentSettings.frameMode || + selectedFrameId !== currentSettings.selectedFrameId || + availableFrameIds.join() !== (currentSettings.availableFrameIds || []).join() || + dateOverlay !== currentSettings.dateOverlay || + dateOverlayPosition !== currentSettings.dateOverlayPosition || + eventText !== currentSettings.eventText || + burstCount !== String(currentSettings.burstCount) || + burstInterval !== String(currentSettings.burstIntervalSec) || + welcomeText !== currentSettings.welcomeText || + footerText !== currentSettings.footerText || + useFrontCamera !== currentSettings.useFrontCamera || + enableLogs !== currentSettings.enableLogs + ); + }; + + useEffect(() => { + const onBackPress = () => { + if (hasUnsavedChanges()) { + Alert.alert( + 'Änderungen speichern?', + 'Du hast ungespeicherte Änderungen. Möchtest du diese vor dem Verlassen speichern?', + [ + { text: 'Abbrechen', style: 'cancel' }, + { text: 'Nein (Verwerfen)', onPress: handleClose, style: 'destructive' }, + { text: 'Ja (Speichern)', onPress: async () => { await handleSave(); handleClose(); } }, + ] + ); + } else { + handleClose(); + } + return true; // Prevent default + }; + + BackHandler.addEventListener('hardwareBackPress', onBackPress); + return () => BackHandler.removeEventListener('hardwareBackPress', onBackPress); + }, [ + countdown, printerIp, password, kioskActive, frameMode, selectedFrameId, availableFrameIds, + dateOverlay, dateOverlayPosition, eventText, burstCount, burstInterval, welcomeText, + footerText, useFrontCamera, enableLogs, currentSettings + ]); useEffect(() => { const checkKioskStatus = () => { @@ -191,6 +242,9 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS burstCount: burst, burstIntervalSec: interval, welcomeText: welcomeText.trim() || 'Willkommen zu unserer Feier!', + footerText: footerText.trim() || 'ZUM STARTEN TIPPEN • Fotos machen • Collage • Drucken', + useFrontCamera, + enableLogs, }; try { @@ -245,7 +299,21 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS EINSTELLUNGEN - + { + if (hasUnsavedChanges()) { + Alert.alert( + 'Änderungen speichern?', + 'Möchtest du die Änderungen speichern?', + [ + { text: 'Abbrechen', style: 'cancel' }, + { text: 'Verwerfen', onPress: handleClose, style: 'destructive' }, + { text: 'Speichern', onPress: async () => { await handleSave(); handleClose(); } }, + ] + ); + } else { + handleClose(); + } + }}> Zurück zur Fotobox @@ -319,6 +387,16 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS onChangeText={setWelcomeText} /> + + Subtext (Footer) + + Event-Name (für Datum-Sticker) + + Tablet-Frontkamera als Fallback erzwingen + + + + App-Protokollierung (Logs) aktivieren + + diff --git a/src/screens/CameraScreen.tsx b/src/screens/CameraScreen.tsx index f0d05017..2aaa50d3 100644 --- a/src/screens/CameraScreen.tsx +++ b/src/screens/CameraScreen.tsx @@ -51,9 +51,11 @@ interface CameraScreenProps { adminPassword?: string; welcomeText?: string; burstCount?: number; - burstIntervalSec?: number; - frameMode?: 'off' | 'always' | 'available'; - selectedFrameId?: string | null; + burstIntervalSec: number; + frameMode: 'off' | 'always' | 'available'; + selectedFrameId: string | null; + footerText: string; + useFrontCamera: boolean; } export default function CameraScreen({ @@ -67,20 +69,23 @@ export default function CameraScreen({ adminPassword = '1234', welcomeText = 'Willkommen zu unserer Feier!', burstCount = 1, - burstIntervalSec = 5, - frameMode = 'off', - selectedFrameId = null, + burstIntervalSec, + frameMode, + selectedFrameId, + footerText, + useFrontCamera, }: CameraScreenProps) { const [permission, requestPermission] = useCameraPermissions(); const [isUsbConnected, setIsUsbConnected] = useState(false); - const [countdown, setCountdown] = useState(''); + const [countdown, setCountdown] = useState(countdownDuration); const [isCapturing, setIsCapturing] = useState(false); const [hasStarted, setHasStarted] = useState(false); const [pictureSize, setPictureSize] = useState(undefined); - // Burst mode state + // Burst mode state and refs const [burstIndex, setBurstIndex] = useState(0); - const [burstUris, setBurstUris] = useState([]); + const burstIndexRef = useRef(0); + const burstUrisRef = useRef([]); const [showBurstIndicator, setShowBurstIndicator] = useState(''); const [isBurstWaiting, setIsBurstWaiting] = useState(false); @@ -228,14 +233,15 @@ export default function CameraScreen({ if (burstTimerRef.current) clearTimeout(burstTimerRef.current); if (countdownTimerRef.current) clearTimeout(countdownTimerRef.current); if (completionTimerRef.current) clearTimeout(completionTimerRef.current); - const urisToDelete = [...burstUris]; + const urisToDelete = [...burstUrisRef.current]; setCountdown(countdownDuration); setIsCapturing(false); setIsBurstWaiting(false); setHasStarted(false); setBurstIndex(0); - setBurstUris([]); + burstIndexRef.current = 0; + burstUrisRef.current = []; setShowBurstIndicator(''); onCancel(); @@ -254,7 +260,8 @@ export default function CameraScreen({ if (isIdle) { setHasStarted(false); setBurstIndex(0); - setBurstUris([]); + burstIndexRef.current = 0; + burstUrisRef.current = []; return; } @@ -361,13 +368,14 @@ export default function CameraScreen({ // Handle burst mode const effectiveBurst = burstCount || 1; - const newIndex = burstIndex + 1; - const newUris = [...burstUris, capturedUri]; + const newIndex = burstIndexRef.current + 1; + const newUris = [...burstUrisRef.current, capturedUri]; if (effectiveBurst > 1 && newIndex < effectiveBurst) { // More burst photos to take + burstIndexRef.current = newIndex; + burstUrisRef.current = newUris; setBurstIndex(newIndex); - setBurstUris(newUris); setShowBurstIndicator(`Foto ${newIndex} von ${effectiveBurst} ✓`); setIsCapturing(false); setIsBurstWaiting(true); @@ -379,8 +387,9 @@ export default function CameraScreen({ }, burstIntervalSec * 1000); } else if (effectiveBurst > 1) { // Final burst photo + burstIndexRef.current = 0; + burstUrisRef.current = []; setBurstIndex(0); - setBurstUris([]); // Short delay prevents Android expo-camera crash on immediate unmount completionTimerRef.current = setTimeout(() => { onBurstComplete(newUris); @@ -512,7 +521,7 @@ export default function CameraScreen({ adjustsFontSizeToFit numberOfLines={1} > - ZUM STARTEN TIPPEN • Fotos machen • Collage • Drucken + {footerText} @@ -644,7 +653,7 @@ export default function CameraScreen({ {/* Camera Preview Background */} - {Platform.OS === 'android' && isUsbConnected ? ( + {Platform.OS === 'android' && !useFrontCamera ? ( (null); + // Hardware Back Press + useEffect(() => { + const onBackPress = () => { + onReset(); + return true; // Prevent default behavior (closing the app) + }; + BackHandler.addEventListener('hardwareBackPress', onBackPress); + return () => { + BackHandler.removeEventListener('hardwareBackPress', onBackPress); + }; + }, [onReset]); + // Frame state const [selectedFrameId, setSelectedFrameId] = useState( frameMode === 'always' ? initialFrameId : null @@ -106,7 +119,7 @@ export default function PreviewScreen({ const currentPhoto = photoUris[photoUris.length - 1]; // Get the active frame asset - const activeFrame = selectedFrameId && photoUris.length === 1 + const activeFrame = selectedFrameId ? FRAMES.find((f) => f.id === selectedFrameId) : null; @@ -578,7 +591,7 @@ export default function PreviewScreen({ - {frameMode === 'available' && photoUris.length === 1 && ( + {frameMode === 'available' && ( { @@ -603,8 +616,8 @@ export default function PreviewScreen({ )} - {/* Frame Picker (when mode = available and only 1 photo) */} - {frameMode === 'available' && photoUris.length === 1 && ( + {/* Frame Picker (when mode = available) */} + {frameMode === 'available' && ( { diff --git a/src/services/logger.ts b/src/services/logger.ts index ae0c0d4a..01a35a68 100644 --- a/src/services/logger.ts +++ b/src/services/logger.ts @@ -5,6 +5,11 @@ const logFileUri = FileSystem.documentDirectory + 'app_logs.txt'; // Queue to serialize file operations and prevent race conditions const logQueue: string[] = []; let isWriting = false; +let isEnabled = true; + +export const setLogsEnabled = (enabled: boolean) => { + isEnabled = enabled; +}; const processLogQueue = async () => { if (isWriting) return; @@ -48,6 +53,7 @@ const enqueueWrite = async (logLine: string) => { export const logger = { log: async (message: string) => { + if (!isEnabled) return; const timestamp = new Date().toISOString(); const logLine = `[INFO] ${timestamp}: ${message}\n`; console.log(logLine.trim()); @@ -55,6 +61,10 @@ export const logger = { }, error: async (message: string, error?: any) => { + if (!isEnabled) { + console.error(`[ERROR]: ${message}`, error); + return; + } const timestamp = new Date().toISOString(); let errorString = ''; if (error) { diff --git a/src/services/settings.ts b/src/services/settings.ts index ec264c30..7735ddcb 100644 --- a/src/services/settings.ts +++ b/src/services/settings.ts @@ -21,6 +21,10 @@ export interface AppSettings { burstIntervalSec: number; // seconds between burst shots // Welcome screen text welcomeText: string; + footerText: string; + // System + useFrontCamera: boolean; + enableLogs: boolean; } const settingsFile = new File(Paths.document, 'settings.json'); @@ -39,6 +43,9 @@ const DEFAULT_SETTINGS: AppSettings = { burstCount: 1, burstIntervalSec: 5, welcomeText: 'Willkommen zu unserer Feier!', + footerText: 'ZUM STARTEN TIPPEN • Fotos machen • Collage • Drucken', + useFrontCamera: false, + enableLogs: true, }; /**