diff --git a/App.tsx b/App.tsx index 92ceb51d..e2e5afd4 100644 --- a/App.tsx +++ b/App.tsx @@ -87,6 +87,7 @@ export default function App() { countdownDuration={settings.countdownDuration} onPhotoCaptured={handlePhotoCaptured} onCancel={handleReset} + isIdle={false} /> ); case 'preview': @@ -109,8 +110,12 @@ export default function App() { ); default: return ( - diff --git a/src/screens/AdminScreen.tsx b/src/screens/AdminScreen.tsx index fe28e82b..e032cd18 100644 --- a/src/screens/AdminScreen.tsx +++ b/src/screens/AdminScreen.tsx @@ -53,42 +53,42 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS if (success) { setKioskActive(true); Alert.alert( - 'Kiosk Mode Started', + 'Kiosk-Modus gestartet', isDeviceOwner - ? 'True Kiosk Mode active. System navigation buttons are completely locked.' - : 'Screen Pinning initiated. Accept the OS prompt to lock the screen.' + ? 'Echter Kiosk-Modus aktiv. Die System-Navigationsleiste ist vollständig gesperrt.' + : 'Bildschirmheftung (Screen Pinning) gestartet. Bitte bestätige das Systemfenster, um den Bildschirm zu sperren.' ); } else { - Alert.alert('Error', 'Failed to start Kiosk Mode.'); + Alert.alert('Fehler', 'Kiosk-Modus konnte nicht gestartet werden.'); } } else { const success = KioskMode.stopKiosk(); if (success) { setKioskActive(false); - Alert.alert('Kiosk Mode Stopped', 'System navigation buttons are unlocked.'); + Alert.alert('Kiosk-Modus beendet', 'Die Navigationstasten des Systems sind wieder freigegeben.'); } else { - Alert.alert('Error', 'Failed to stop Kiosk Mode.'); + Alert.alert('Fehler', 'Kiosk-Modus konnte nicht beendet werden.'); } } } catch (e: any) { - Alert.alert('Native Error', e.message || 'Kiosk module error'); + Alert.alert('Nativer Fehler', e.message || 'Fehler im Kiosk-Modul'); } }; const handleSave = async () => { const duration = parseInt(countdown, 10); if (isNaN(duration) || duration < 1 || duration > 30) { - Alert.alert('Invalid Input', 'Countdown duration must be a number between 1 and 30 seconds.'); + Alert.alert('Ungültige Eingabe', 'Die Countdown-Dauer muss eine Zahl zwischen 1 und 30 Sekunden sein.'); return; } if (!printerIp.trim()) { - Alert.alert('Invalid Input', 'Printer IP address cannot be empty.'); + Alert.alert('Ungültige Eingabe', 'Die Drucker-IP-Adresse darf nicht leer sein.'); return; } if (!password.trim() || password.length < 4) { - Alert.alert('Invalid Input', 'Admin password must be at least 4 digits.'); + Alert.alert('Ungültige Eingabe', 'Das Admin-Passwort muss mindestens 4 Ziffern lang sein.'); return; } @@ -102,33 +102,33 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS try { await saveSettings(updated); onSave(updated); - Alert.alert('Success', 'Settings saved successfully!'); + Alert.alert('Erfolg', 'Einstellungen erfolgreich gespeichert!'); } catch (e) { - Alert.alert('Error', 'Failed to save settings to disk.'); + Alert.alert('Fehler', 'Einstellungen konnten nicht gespeichert werden.'); } }; return ( - SCHNAPPIX SETTINGS + SCHNAPPIX EINSTELLUNGEN - Back to Booth + Zurück zur Fotobox {/* Printer Configurations */} - Printer Configuration + Drucker-Konfiguration - Configure the local IP address of your Wi-Fi connected Canon CP1300 printer. + Konfiguriere die lokale IP-Adresse deines über WLAN verbundenen Canon CP1300 Druckers. - Printer IP Address + Drucker IP-Adresse - Photo Booth Behavior + Fotobox-Verhalten - Countdown Duration (seconds) + Countdown-Dauer (Sekunden) - Admin Panel Password + Admin-Passwort - Kiosk Mode & Security + Kiosk-Modus & Sicherheit - Lock the screen to prevent guests from closing the app or opening system settings. + Sperre den Bildschirm, um zu verhindern, dass Gäste die App schließen oder auf die Systemeinstellungen zugreifen. - Kiosk Mode State: + Kiosk-Modus Status: - {kioskActive ? 'LOCKED 🔒' : 'UNLOCKED 🔓'} + {kioskActive ? 'GESPERRT 🔒' : 'ENTSPERRT 🔓'} - Device Owner Mode: + Device-Owner-Modus: - {isDeviceOwner ? 'ACTIVE (True Lock) ✅' : 'INACTIVE (Screen Pinning Fallback) ⚠️'} + {isDeviceOwner ? 'AKTIV (Echte Sperre) ✅' : 'INAKTIV (Screen Pinning Fallback) ⚠️'} {!isDeviceOwner && ( - Note: To enable True Lock task mode without prompt bypasses, make the app device owner using ADB. + Hinweis: Um den echten Lock-Task-Modus ohne Bestätigungsaufforderung zu aktivieren, mache die App mittels ADB zum Device Owner. )} - Lock Screen (Kiosk Mode) + Bildschirm sperren (Kiosk-Modus) - SAVE SETTINGS + EINSTELLUNGEN SPEICHERN diff --git a/src/screens/CameraScreen.tsx b/src/screens/CameraScreen.tsx index cde8eb15..04ee3ae4 100644 --- a/src/screens/CameraScreen.tsx +++ b/src/screens/CameraScreen.tsx @@ -1,23 +1,50 @@ import React, { useState, useEffect, useRef } from 'react'; -import { StyleSheet, Text, View, TouchableOpacity, Platform } from 'react-native'; +import { + StyleSheet, + Text, + View, + TouchableOpacity, + Platform, + Modal, + TextInput, + TouchableWithoutFeedback, +} from 'react-native'; import { CameraView, useCameraPermissions } from 'expo-camera'; -import * as FileSystem from 'expo-file-system'; +import * as FileSystem from 'expo-file-system/legacy'; import { THEME } from '../styles/theme'; import { UsbCameraView, UsbCameraRef } from '../../modules/usb-camera'; +import { saveToGallery } from '../services/storage'; interface CameraScreenProps { countdownDuration: number; onPhotoCaptured: (uri: string) => void; onCancel: () => void; + isIdle?: boolean; + onStartBooth?: () => void; + onNavigateToAdmin?: () => void; + adminPassword?: string; } -export default function CameraScreen({ countdownDuration, onPhotoCaptured, onCancel }: CameraScreenProps) { +export default function CameraScreen({ + countdownDuration, + onPhotoCaptured, + onCancel, + isIdle = false, + onStartBooth, + onNavigateToAdmin, + adminPassword = '1234', +}: CameraScreenProps) { const [permission, requestPermission] = useCameraPermissions(); const [isUsbConnected, setIsUsbConnected] = useState(false); const [countdown, setCountdown] = useState(''); const [isCapturing, setIsCapturing] = useState(false); const [hasStarted, setHasStarted] = useState(false); + // Admin Modal States + const [passwordModalVisible, setPasswordModalVisible] = useState(false); + const [enteredPassword, setEnteredPassword] = useState(''); + const [errorText, setErrorText] = useState(''); + const usbCameraRef = useRef(null); const expoCameraRef = useRef(null); @@ -28,10 +55,9 @@ export default function CameraScreen({ countdownDuration, onPhotoCaptured, onCan } }, [permission]); - // 2. Check if a USB camera is connected. - // We can query the USB module or attempt to check every second in the background. + // 2. Check if a USB camera is connected. useEffect(() => { - let checkInterval: NodeJS.Timeout; + let checkInterval: any; if (Platform.OS === 'android') { const checkConnection = async () => { try { @@ -40,12 +66,10 @@ export default function CameraScreen({ countdownDuration, onPhotoCaptured, onCan setIsUsbConnected(connected); } } catch (e) { - // If native view manager is not loaded or errors, fallback to false setIsUsbConnected(false); } }; - // Check initially and run an interval checkConnection(); checkInterval = setInterval(checkConnection, 2000); } @@ -55,13 +79,19 @@ export default function CameraScreen({ countdownDuration, onPhotoCaptured, onCan }; }, []); - // 3. Start the countdown on screen load + // 3. Start the countdown on screen load (only when NOT idle) useEffect(() => { - let timer: NodeJS.Timeout; + if (isIdle) { + setHasStarted(false); + return; + } + + let timer: any; let count = countdownDuration; setCountdown(count); setHasStarted(true); + setIsCapturing(false); const runTimer = () => { if (count > 1) { @@ -69,7 +99,7 @@ export default function CameraScreen({ countdownDuration, onPhotoCaptured, onCan setCountdown(count); timer = setTimeout(runTimer, 1000); } else if (count === 1) { - setCountdown('Cheese! 📸'); + setCountdown('Bitte lächeln! 📸'); setIsCapturing(true); timer = setTimeout(() => { capture(); @@ -82,7 +112,7 @@ export default function CameraScreen({ countdownDuration, onPhotoCaptured, onCan return () => { if (timer) clearTimeout(timer); }; - }, [countdownDuration]); + }, [countdownDuration, isIdle]); // 4. Capture photo function const capture = async () => { @@ -94,6 +124,15 @@ export default function CameraScreen({ countdownDuration, onPhotoCaptured, onCan // USB Camera Capture console.log('Capturing from USB Camera...'); const path = await usbCameraRef.current.takePicture(tempUri); + + // Auto-save individual photo directly to gallery + try { + await saveToGallery(path); + console.log('Auto-saved USB capture to gallery'); + } catch (e) { + console.error('Failed to auto-save USB capture:', e); + } + onPhotoCaptured(path); } else { // Fallback Camera Capture @@ -103,22 +142,47 @@ export default function CameraScreen({ countdownDuration, onPhotoCaptured, onCan quality: 0.95, skipProcessing: false, }); + + // Auto-save individual photo directly to gallery + try { + await saveToGallery(photo.uri); + console.log('Auto-saved fallback capture to gallery'); + } catch (e) { + console.error('Failed to auto-save fallback capture:', e); + } + onPhotoCaptured(photo.uri); } else { - throw new Error('Camera ref is not available.'); + throw new Error('Kamera-Referenz ist nicht verfügbar.'); } } } catch (error: any) { console.error('Capture failed:', error); - alert('Failed to capture photo: ' + error.message); + alert('Fehler beim Aufnehmen des Fotos: ' + error.message); onCancel(); } }; + const handleSettingsTap = () => { + setEnteredPassword(''); + setErrorText(''); + setPasswordModalVisible(true); + }; + + const handlePasswordSubmit = () => { + if (enteredPassword === adminPassword) { + setPasswordModalVisible(false); + onNavigateToAdmin?.(); + } else { + setErrorText('Falsches Passwort'); + setEnteredPassword(''); + } + }; + if (!permission) { return ( - Loading permissions... + Berechtigungen werden geladen... ); } @@ -126,20 +190,124 @@ export default function CameraScreen({ countdownDuration, onPhotoCaptured, onCan if (!permission.granted && !isUsbConnected) { return ( - We need your permission to show the camera + Wir benötigen deine Erlaubnis, um die Kamera anzuzeigen - Grant Permission + Erlaubnis erteilen - Go Back + Zurück ); } + const renderContent = () => { + if (isIdle) { + // Home Overlay (frosty glass box on top of the camera stream) + return ( + + + {/* Settings button in the top-right corner */} + + ⚙️ + + + + SCHNAPPIX + Willkommen zu unserer Feier! + + + + + ZUM STARTEN ÜBERALL TIPPEN + + + Fotos machen • Collage erstellen • Sofort drucken + + + {/* Password Prompt Modal */} + setPasswordModalVisible(false)} + > + + + Admin-Zugang + Passwort eingeben, um Einstellungen zu öffnen + + + + {errorText ? {errorText} : null} + + + setPasswordModalVisible(false)} + > + Abbrechen + + + Öffnen + + + + + + + + ); + } + + // Capture Mode overlays (Countdown and Cancel button) + return ( + + {/* Source indicator */} + + + Kamera: {isUsbConnected ? 'Externe USB-Kamera' : 'Tablet-Frontkamera (Fallback)'} + + + + {/* Countdown overlay */} + {hasStarted && ( + + + {countdown} + + + )} + + {/* Cancel Button */} + {!isCapturing && ( + + ABBRECHEN + + )} + + ); + }; + return ( - {/* Camera Preview Area */} + {/* Camera Preview Background */} {Platform.OS === 'android' && isUsbConnected ? ( @@ -152,28 +320,8 @@ export default function CameraScreen({ countdownDuration, onPhotoCaptured, onCan )} - {/* Camera Info HUD */} - - - Source: {isUsbConnected ? 'External USB Camera' : 'Front Tablet Camera (Fallback)'} - - - - {/* Countdown overlay */} - {hasStarted && ( - - - {countdown} - - - )} - - {/* Cancel Button */} - {!isCapturing && ( - - CANCEL - - )} + {/* Render UI controls/overlays on top */} + {renderContent()} ); } @@ -182,32 +330,119 @@ const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#000', - justifyContent: 'center', - alignItems: 'center', }, previewContainer: { width: '100%', height: '100%', position: 'absolute', + top: 0, + left: 0, }, cameraPreview: { width: '100%', height: '100%', }, + idleOverlayContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0, 0, 0, 0.15)', + }, + captureOverlayContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + settingsButton: { + position: 'absolute', + top: 24, + right: 24, + width: 50, + height: 50, + borderRadius: THEME.borderRadius.round, + backgroundColor: 'rgba(30, 30, 35, 0.75)', + borderWidth: 1, + borderColor: 'rgba(255, 255, 255, 0.1)', + justifyContent: 'center', + alignItems: 'center', + zIndex: 999, + }, + settingsIcon: { + fontSize: 24, + color: THEME.colors.text, + }, + welcomeBox: { + alignItems: 'center', + padding: THEME.spacing.xl, + borderRadius: THEME.borderRadius.lg, + backgroundColor: 'rgba(18, 18, 20, 0.8)', + borderWidth: 1, + borderColor: 'rgba(255, 255, 255, 0.15)', + width: '60%', + shadowColor: '#000', + shadowOffset: { width: 0, height: 10 }, + shadowOpacity: 0.3, + shadowRadius: 20, + elevation: 10, + }, + logo: { + fontSize: 54, + fontWeight: '900', + color: THEME.colors.primary, + letterSpacing: 8, + marginBottom: THEME.spacing.sm, + textShadowColor: 'rgba(0, 0, 0, 0.5)', + textShadowOffset: { width: 0, height: 2 }, + textShadowRadius: 4, + }, + welcomeTitle: { + fontSize: 22, + color: THEME.colors.text, + textAlign: 'center', + marginBottom: THEME.spacing.lg, + letterSpacing: 1, + }, + divider: { + width: 60, + height: 3, + backgroundColor: THEME.colors.accent, + marginBottom: THEME.spacing.xl, + borderRadius: THEME.borderRadius.sm, + }, + startButton: { + paddingVertical: THEME.spacing.md, + paddingHorizontal: THEME.spacing.xl, + backgroundColor: THEME.colors.primary, + borderRadius: THEME.borderRadius.round, + marginBottom: THEME.spacing.xl, + }, + startButtonText: { + color: THEME.colors.text, + fontSize: 18, + fontWeight: 'bold', + letterSpacing: 2, + }, + welcomeFooter: { + fontSize: 14, + color: THEME.colors.textMuted, + letterSpacing: 1, + }, hudContainer: { position: 'absolute', bottom: 20, - backgroundColor: 'rgba(0, 0, 0, 0.6)', + backgroundColor: 'rgba(0, 0, 0, 0.7)', paddingVertical: THEME.spacing.xs, paddingHorizontal: THEME.spacing.md, borderRadius: THEME.borderRadius.sm, + borderWidth: 0.5, + borderColor: 'rgba(255, 255, 255, 0.1)', }, cameraSourceIndicator: { color: THEME.colors.text, fontSize: 14, fontWeight: 'bold', }, - overlay: { + countdownOverlay: { position: 'absolute', top: 0, left: 0, @@ -215,37 +450,39 @@ const styles = StyleSheet.create({ bottom: 0, justifyContent: 'center', alignItems: 'center', - backgroundColor: 'rgba(0, 0, 0, 0.2)', }, countdownBox: { - backgroundColor: THEME.colors.glassBackground, + backgroundColor: 'rgba(20, 20, 25, 0.85)', paddingVertical: THEME.spacing.xl, paddingHorizontal: THEME.spacing.xxl, borderRadius: THEME.borderRadius.lg, borderWidth: 1, - borderColor: THEME.colors.border, + borderColor: 'rgba(255, 255, 255, 0.15)', minWidth: 180, alignItems: 'center', justifyContent: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 10 }, + shadowOpacity: 0.4, + shadowRadius: 15, + elevation: 8, }, countdownText: { - fontSize: 72, + fontSize: 64, fontWeight: '900', color: THEME.colors.text, - textShadowColor: 'rgba(0, 0, 0, 0.75)', - textShadowOffset: { width: -1, height: 1 }, - textShadowRadius: 10, + textAlign: 'center', }, backButton: { position: 'absolute', top: 24, left: 24, - backgroundColor: 'rgba(0, 0, 0, 0.6)', + backgroundColor: 'rgba(30, 30, 35, 0.75)', paddingVertical: THEME.spacing.sm, paddingHorizontal: THEME.spacing.lg, borderRadius: THEME.borderRadius.round, borderWidth: 1, - borderColor: THEME.colors.border, + borderColor: 'rgba(255, 255, 255, 0.1)', }, backButtonText: { color: THEME.colors.text, @@ -277,4 +514,81 @@ const styles = StyleSheet.create({ fontSize: 16, fontWeight: 'bold', }, + modalOverlay: { + flex: 1, + backgroundColor: THEME.colors.overlay, + justifyContent: 'center', + alignItems: 'center', + }, + modalContent: { + width: 340, + padding: THEME.spacing.lg, + borderRadius: THEME.borderRadius.md, + backgroundColor: THEME.colors.surfaceSecondary, + borderWidth: 1, + borderColor: THEME.colors.border, + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 10 }, + shadowOpacity: 0.4, + shadowRadius: 15, + elevation: 10, + }, + modalTitle: { + fontSize: 20, + fontWeight: 'bold', + color: THEME.colors.text, + marginBottom: THEME.spacing.xs, + }, + modalSub: { + fontSize: 13, + color: THEME.colors.textMuted, + marginBottom: THEME.spacing.md, + textAlign: 'center', + }, + input: { + width: '100%', + height: 50, + backgroundColor: THEME.colors.surface, + borderColor: THEME.colors.border, + borderWidth: 1, + borderRadius: THEME.borderRadius.sm, + color: THEME.colors.text, + paddingHorizontal: THEME.spacing.md, + fontSize: 16, + textAlign: 'center', + marginBottom: THEME.spacing.sm, + }, + error: { + color: THEME.colors.error, + fontSize: 14, + marginBottom: THEME.spacing.sm, + }, + modalButtons: { + flexDirection: 'row', + justifyContent: 'space-between', + width: '100%', + marginTop: THEME.spacing.sm, + }, + button: { + flex: 1, + height: 45, + borderRadius: THEME.borderRadius.sm, + justifyContent: 'center', + alignItems: 'center', + marginHorizontal: THEME.spacing.xs, + }, + cancelButton: { + backgroundColor: 'transparent', + borderWidth: 1, + borderColor: THEME.colors.border, + }, + confirmButton: { + backgroundColor: THEME.colors.primary, + }, + buttonText: { + color: THEME.colors.text, + fontSize: 16, + fontWeight: '600', + }, }); diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index 6c53d3e8..1ea25cd1 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -32,7 +32,7 @@ export default function HomeScreen({ onStart, onNavigateToAdmin, adminPassword = setPasswordModalVisible(false); onNavigateToAdmin(); } else { - setErrorText('Incorrect Password'); + setErrorText('Falsches Passwort'); setEnteredPassword(''); } }; @@ -51,15 +51,15 @@ export default function HomeScreen({ onStart, onNavigateToAdmin, adminPassword = SCHNAPPIX - Welcome to our celebration! + Willkommen zu unserer Feier! - TAP ANYWHERE TO START + ZUM STARTEN ÜBERALL TIPPEN - Take photos • Create a collage • Print instantly + Fotos machen • Collage erstellen • Sofort drucken {/* Password Prompt Modal */} @@ -71,13 +71,13 @@ export default function HomeScreen({ onStart, onNavigateToAdmin, adminPassword = > - Admin Access - Enter password to open settings + Admin-Zugang + Passwort eingeben, um Einstellungen zu öffnen setPasswordModalVisible(false)} > - Cancel + Abbrechen - Open + Öffnen diff --git a/src/screens/PreviewScreen.tsx b/src/screens/PreviewScreen.tsx index abb28b32..7081076c 100644 --- a/src/screens/PreviewScreen.tsx +++ b/src/screens/PreviewScreen.tsx @@ -41,36 +41,36 @@ export default function PreviewScreen({ // Triggers the view capture and the print job const handlePrint = async () => { setIsProcessing(true); - setStatusMessage('Preparing your photo...'); + setStatusMessage('Dein Foto wird vorbereitet...'); try { let printUri = currentPhoto; // If we are printing a collage layout, capture the ViewShot container if (layout !== 'single') { - setStatusMessage('Generating collage...'); + setStatusMessage('Collage wird generiert...'); const capturedUri = await viewShotRef.current.capture(); printUri = capturedUri; } - setStatusMessage('Sending print job to printer...'); + setStatusMessage('Druckauftrag wird an Drucker gesendet...'); // 1. Silent Print via IPP await printImageLocal(printUri, { ipAddress: printerIp, - jobName: 'Schnappix Photo Booth', + jobName: 'Schnappix Fotobox', }); - setStatusMessage('Saving to tablet library...'); + setStatusMessage('Wird in Galerie gespeichert...'); // 2. Save permanently to DCIM/Schnappix await saveToGallery(printUri); - setStatusMessage('Print successful! Enjoy!'); + setStatusMessage('Druck erfolgreich! Viel Spaß! 🎉'); setTimeout(() => { setIsProcessing(false); onReset(); // Go back to Home Screen }, 2000); } catch (error: any) { console.error('Printing failed:', error); - alert('Error: ' + error.message); + alert('Fehler: ' + error.message); setIsProcessing(false); } }; @@ -78,7 +78,7 @@ export default function PreviewScreen({ // Saves to gallery without printing const handleSaveOnly = async () => { setIsProcessing(true); - setStatusMessage('Generating image...'); + setStatusMessage('Bild wird generiert...'); try { let saveUri = currentPhoto; @@ -87,21 +87,46 @@ export default function PreviewScreen({ saveUri = capturedUri; } - setStatusMessage('Saving to tablet library...'); + setStatusMessage('Wird in Galerie gespeichert...'); await saveToGallery(saveUri); - setStatusMessage('Saved successfully!'); + setStatusMessage('Erfolgreich gespeichert!'); setTimeout(() => { setIsProcessing(false); onReset(); }, 1500); } catch (error: any) { console.error('Saving failed:', error); - alert('Error: ' + error.message); + alert('Fehler: ' + error.message); setIsProcessing(false); } }; + // Auto-save collage and exit + const handleExit = async () => { + // If layout is single, it has already been auto-saved to gallery right when captured + if (layout === 'single') { + onReset(); + return; + } + + setIsProcessing(true); + setStatusMessage('Collage wird gespeichert...'); + try { + const capturedUri = await viewShotRef.current.capture(); + await saveToGallery(capturedUri); + setStatusMessage('Erfolgreich gespeichert!'); + setTimeout(() => { + setIsProcessing(false); + onReset(); + }, 1000); + } catch (error) { + console.error('Auto-saving collage on exit failed:', error); + setIsProcessing(false); + onReset(); + } + }; + // Render the selected collage inside the capture container const renderCollageView = () => { switch (layout) { @@ -114,8 +139,8 @@ export default function PreviewScreen({ ))} - SCHNAPPIX PHOTO BOOTH - {new Date().toLocaleDateString()} + SCHNAPPIX FOTOBOX + {new Date().toLocaleDateString('de-DE')} ); @@ -128,7 +153,7 @@ export default function PreviewScreen({ ))} - SCHNAPPIX PHOTO BOOTH + SCHNAPPIX FOTOBOX ); @@ -141,7 +166,7 @@ export default function PreviewScreen({ ))} - SCHNAPPIX PHOTO BOOTH + SCHNAPPIX FOTOBOX ); @@ -174,7 +199,7 @@ export default function PreviewScreen({ {/* Right panel: Controls */} - CHOOSE YOUR STYLE + WÄHLE DEINEN STIL {/* Layout Selection */} @@ -182,7 +207,7 @@ export default function PreviewScreen({ style={[styles.layoutBtn, layout === 'single' && styles.layoutBtnActive]} onPress={() => setLayout('single')} > - Single + Einzelbild setLayout('strip')} > - Strip (3) + Streifen (3) setLayout('grid')} > - Grid (4) + Raster (4) - Photos Captured: {photoUris.length} / 4 + Aufgenommene Fotos: {photoUris.length} / 4 {/* Action Buttons */} - PRINT NOW 🖨️ + JETZT DRUCKEN 🖨️ - Save to Tablet Only 💾 + Nur auf Tablet speichern 💾 {photoUris.length < 4 && ( - + Add Another Photo + + Weiteres Foto aufnehmen )} - Retake Last + Letztes wiederholen - - Exit + + Beenden @@ -281,7 +306,6 @@ const styles = StyleSheet.create({ backgroundColor: '#050507', }, viewShotContainer: { - // 3:2 ratio aspect layout matching Canon Selphy postcard paper width: 320, height: 480, justifyContent: 'center', @@ -389,7 +413,6 @@ const styles = StyleSheet.create({ fontSize: 14, fontWeight: '600', }, - // Collage Canvas Layouts (3:2 ratio aspect - width 320, height 480) singleCanvas: { width: 320, height: 480, @@ -433,7 +456,6 @@ const styles = StyleSheet.create({ fontSize: 8, color: '#666666', }, - // Strip (3 images vertical) stripCanvas: {}, stripContent: { flex: 1, @@ -446,7 +468,6 @@ const styles = StyleSheet.create({ resizeMode: 'cover', borderRadius: 2, }, - // Grid (4 images 2x2) gridCanvas: {}, gridContainer: { flex: 1, @@ -462,7 +483,6 @@ const styles = StyleSheet.create({ resizeMode: 'cover', borderRadius: 2, }, - // Duo (2 images side by side or stacked) duoCanvas: {}, duoContainer: { flex: 1, @@ -475,7 +495,6 @@ const styles = StyleSheet.create({ resizeMode: 'cover', borderRadius: 2, }, - // Loading overlay loadingOverlay: { position: 'absolute', top: 0, diff --git a/src/services/printer.ts b/src/services/printer.ts index 98c59dd2..15a1dfcd 100644 --- a/src/services/printer.ts +++ b/src/services/printer.ts @@ -1,6 +1,6 @@ import ipp from 'ipp-encoder'; import { Buffer } from 'buffer'; -import * as FileSystem from 'expo-file-system'; +import * as FileSystem from 'expo-file-system/legacy'; export interface PrintOptions { ipAddress: string; @@ -19,7 +19,7 @@ export async function printImageLocal(imageUri: string, options: PrintOptions): const { ipAddress, jobName = 'Schnappix Photo', username = 'Schnappix Kiosk' } = options; if (!ipAddress) { - throw new Error('Printer IP address is required.'); + throw new Error('Drucker-IP-Adresse ist erforderlich.'); } // 1. Read the image file from local storage as Base64 and convert to binary Buffer @@ -69,7 +69,7 @@ export async function printImageLocal(imageUri: string, options: PrintOptions): }); if (!response.ok) { - throw new Error(`Printer responded with HTTP status ${response.status}: ${response.statusText}`); + throw new Error(`Drucker antwortete mit HTTP-Status ${response.status}: ${response.statusText}`); } // 6. Read and decode the response from the printer to verify success @@ -82,7 +82,7 @@ export async function printImageLocal(imageUri: string, options: PrintOptions): // IPP success status is 0x0000 (successful-ok) if (statusCode !== 0x0000) { - throw new Error(`Printer returned IPP error code: 0x${statusCode.toString(16)}`); + throw new Error(`Drucker lieferte IPP-Fehlercode: 0x${statusCode.toString(16)}`); } console.log('Silent print job accepted successfully!'); } catch (e: any) { diff --git a/src/services/settings.ts b/src/services/settings.ts index 4961af60..969752e7 100644 --- a/src/services/settings.ts +++ b/src/services/settings.ts @@ -1,4 +1,4 @@ -import * as FileSystem from 'expo-file-system'; +import * as FileSystem from 'expo-file-system/legacy'; export interface AppSettings { countdownDuration: number; // in seconds diff --git a/src/services/storage.ts b/src/services/storage.ts index ce1e863f..9098c3e0 100644 --- a/src/services/storage.ts +++ b/src/services/storage.ts @@ -1,4 +1,4 @@ -import * as MediaLibrary from 'expo-media-library'; +import * as MediaLibrary from 'expo-media-library/legacy'; import { Platform } from 'react-native'; const ALBUM_NAME = 'Schnappix'; diff --git a/src/types/ipp-encoder.d.ts b/src/types/ipp-encoder.d.ts new file mode 100644 index 00000000..3f1f3250 --- /dev/null +++ b/src/types/ipp-encoder.d.ts @@ -0,0 +1 @@ +declare module 'ipp-encoder';