Files

321 lines
10 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { StyleSheet, View, SafeAreaView, StatusBar, ActivityIndicator, Modal, TextInput, Text, TouchableOpacity } 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 { StickerData } from './src/components/DraggableSticker';
import { logger, setLogsEnabled } from './src/services/logger';
import * as FileSystem from 'expo-file-system/legacy';
// Set up global error handler
const defaultErrorHandler = (global as any).ErrorUtils.getGlobalHandler();
(global as any).ErrorUtils.setGlobalHandler((error: any, isFatal: boolean) => {
logger.error(`Unhandled Exception (Fatal: ${isFatal})`, error)
.catch(console.error)
.finally(() => {
// Delay termination until log is written
defaultErrorHandler(error, isFatal);
});
});
import KioskMode from './modules/kiosk-mode';
type ScreenState = 'home' | 'camera' | 'preview' | 'admin';
export default function App() {
const [currentScreen, setCurrentScreen] = useState<ScreenState>('home');
const [photoUris, setPhotoUris] = useState<string[]>([]);
const [captureHistory, setCaptureHistory] = useState<number[]>([]);
const [settings, setSettings] = useState<AppSettings | null>(null);
const [showEventModal, setShowEventModal] = useState(false);
const [tempEventName, setTempEventName] = useState('');
// Persist edits across camera switches for collages
const [stickers, setStickers] = useState<StickerData[]>([]);
const [selectedFrameId, setSelectedFrameId] = useState<string | null>(null);
// 1. Load configuration settings on app start
useEffect(() => {
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) {
try {
KioskMode.startKiosk();
} catch (e) {
console.warn('Failed to auto-start Kiosk mode:', e);
}
}
if (savedSettings.frameMode === 'always') {
setSelectedFrameId(savedSettings.selectedFrameId);
}
}
initApp();
}, []);
if (!settings) {
return (
<View style={styles.loadingContainer}>
<StatusBar hidden={true} />
<ActivityIndicator size="large" color="#ff2bd6" />
</View>
);
}
// 2. Navigation Actions
const handleStartBooth = () => {
if (!settings?.eventText || settings.eventText.trim() === '') {
setTempEventName('');
setShowEventModal(true);
return;
}
setPhotoUris([]);
setCaptureHistory([]);
setStickers([]);
setSelectedFrameId(settings?.frameMode === 'always' ? settings.selectedFrameId : null);
setCurrentScreen('camera');
};
const handleSaveEventName = async () => {
if (!settings) return;
const newName = tempEventName.trim() || 'Schnappix-Party';
const updated = { ...settings, eventText: newName };
await saveSettings(updated);
setSettings(updated);
setShowEventModal(false);
// Resume start
setPhotoUris([]);
setCaptureHistory([]);
setStickers([]);
setSelectedFrameId(updated.frameMode === 'always' ? updated.selectedFrameId : null);
setCurrentScreen('camera');
};
const handlePhotoCaptured = async (uri: string) => {
await logger.log(`Photo captured via CameraScreen: ${uri}`);
try {
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) {
await logger.error('Error transitioning to preview screen:', e);
}
};
// Called by CameraScreen when burst mode completes all photos
const handleBurstComplete = (uris: string[]) => {
setPhotoUris((prev) => {
const next = [...prev, ...uris];
const maxKeep = Math.max(4, uris.length);
return next.length > maxKeep ? next.slice(-maxKeep) : next;
});
setCaptureHistory((prev) => [...prev, uris.length]);
setCurrentScreen('preview');
};
const handleAddAnother = () => {
setCurrentScreen('camera');
};
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([]);
setStickers([]);
setSelectedFrameId(settings?.frameMode === 'always' ? settings.selectedFrameId : null);
setCurrentScreen('home'); // Go to home when cancelled
};
const handleSaveSettings = (updated: AppSettings) => {
setSettings(updated);
setLogsEnabled(updated.enableLogs);
};
const handleOpenAdmin = () => {
setCurrentScreen('admin');
};
// 3. Conditional Screen Rendering
return (
<GestureHandlerRootView style={styles.container}>
<SafeAreaView style={styles.container}>
<StatusBar hidden={true} />
{/*
Keep CameraScreen ALWAYS mounted in the background to prevent
Android expo-camera hardware crashes when unmounting right after capture
*/}
<View
style={StyleSheet.absoluteFill}
pointerEvents={(currentScreen === 'preview' || currentScreen === 'admin') ? 'none' : 'auto'}
>
<CameraScreen
countdownDuration={settings.countdownDuration}
onPhotoCaptured={handlePhotoCaptured}
onBurstComplete={handleBurstComplete}
onCancel={handleReset}
isIdle={currentScreen !== 'camera'}
isObscured={currentScreen === 'preview' || currentScreen === 'admin'}
onStartBooth={handleStartBooth}
onNavigateToAdmin={handleOpenAdmin}
adminPassword={settings.adminPassword}
welcomeText={settings.welcomeText}
burstCount={settings.burstCount}
burstIntervalSec={settings.burstIntervalSec}
frameMode={settings.frameMode}
selectedFrameId={settings.selectedFrameId}
footerText={settings.footerText}
useFrontCamera={settings.useFrontCamera}
eventText={settings.eventText}
/>
</View>
{/* Overlay PreviewScreen if active */}
{currentScreen === 'preview' && (
<View style={[StyleSheet.absoluteFill, { zIndex: 100, elevation: 100, backgroundColor: 'transparent' }]}>
<PreviewScreen
photoUris={photoUris}
printerIp={settings.printerIp}
onAddAnother={handleAddAnother}
onReset={handleReset}
frameMode={settings.frameMode}
initialFrameId={settings.selectedFrameId}
availableFrameIds={settings.availableFrameIds}
dateOverlay={settings.dateOverlay}
dateOverlayTransform={settings.dateOverlayTransform}
eventText={settings.eventText}
stickers={stickers}
setStickers={setStickers}
selectedFrameId={selectedFrameId}
setSelectedFrameId={setSelectedFrameId}
/>
</View>
)}
{/* Overlay AdminScreen if active */}
{currentScreen === 'admin' && (
<View style={[StyleSheet.absoluteFill, { zIndex: 100, elevation: 100, backgroundColor: 'transparent' }]}>
<AdminScreen
currentSettings={settings}
onSave={handleSaveSettings}
onClose={() => setCurrentScreen('home')}
/>
</View>
)}
{/* Event Name Modal */}
<Modal visible={showEventModal} transparent animationType="fade">
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<Text style={styles.modalTitle}>Kein Event-Name gefunden</Text>
<Text style={styles.modalText}>
Möchtest du jetzt einen festlegen? (Ansonsten kann kein Foto gemacht werden).
</Text>
<TextInput
style={styles.modalInput}
placeholder="z.B. Schnappix-Party"
placeholderTextColor="#666"
value={tempEventName}
onChangeText={setTempEventName}
autoFocus
/>
<View style={styles.modalButtons}>
<TouchableOpacity style={[styles.modalBtn, { backgroundColor: '#333' }]} onPress={() => setShowEventModal(false)}>
<Text style={styles.modalBtnText}>Abbrechen</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.modalBtn, { backgroundColor: '#ff2bd6' }]} onPress={handleSaveEventName}>
<Text style={styles.modalBtnText}>Speichern & Starten</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
</SafeAreaView>
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#000',
},
loadingContainer: {
flex: 1,
backgroundColor: '#000',
justifyContent: 'center',
alignItems: 'center',
},
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.8)',
justifyContent: 'center',
alignItems: 'center',
},
modalContent: {
backgroundColor: '#1a1a24',
padding: 30,
borderRadius: 16,
width: '80%',
maxWidth: 500,
borderWidth: 1,
borderColor: '#333',
},
modalTitle: {
color: '#fff',
fontSize: 24,
fontWeight: 'bold',
marginBottom: 10,
},
modalText: {
color: '#aaa',
fontSize: 16,
marginBottom: 20,
lineHeight: 24,
},
modalInput: {
backgroundColor: '#0a0a14',
borderWidth: 1,
borderColor: '#333',
borderRadius: 8,
color: '#fff',
padding: 15,
fontSize: 18,
marginBottom: 25,
},
modalButtons: {
flexDirection: 'row',
justifyContent: 'flex-end',
gap: 15,
},
modalBtn: {
paddingVertical: 12,
paddingHorizontal: 20,
borderRadius: 8,
},
modalBtnText: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
},
});