321 lines
10 KiB
TypeScript
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',
|
|
},
|
|
});
|