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';