Files
Schnappix/App.tsx
T

222 lines
7.3 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { StyleSheet, View, SafeAreaView, StatusBar, ActivityIndicator } 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 { 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);
// 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);
}
}
}
initApp();
}, []);
if (!settings) {
return (
<View style={styles.loadingContainer}>
<StatusBar hidden={true} />
<ActivityIndicator size="large" color="#ff2bd6" />
</View>
);
}
// 2. Navigation Actions
const handleStartBooth = () => {
setPhotoUris([]);
setCaptureHistory([]);
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 handleRetakeLast = async () => {
// Remove photos from the last capture event
const lastCaptureCount = captureHistory.length > 0 ? captureHistory[captureHistory.length - 1] : 1;
const updatedUris = [...photoUris];
const removedUris = updatedUris.splice(-lastCaptureCount, lastCaptureCount);
setPhotoUris(updatedUris);
setCaptureHistory((prev) => prev.slice(0, -1));
// Clean up temp files
for (const uri of removedUris) {
try {
await FileSystem.deleteAsync(uri, { idempotent: true });
} catch (e) {
console.warn('Failed to delete temp file:', e);
}
}
// Always let user retake
setCurrentScreen('camera');
};
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([]);
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,
(currentScreen === 'preview' || currentScreen === 'admin') && { zIndex: -10 }
]}
pointerEvents={(currentScreen === 'preview' || currentScreen === 'admin') ? 'none' : 'auto'}
>
<CameraScreen
countdownDuration={settings.countdownDuration}
onPhotoCaptured={handlePhotoCaptured}
onBurstComplete={handleBurstComplete}
onCancel={handleReset}
isIdle={currentScreen !== 'camera'}
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}
/>
</View>
{/* Overlay PreviewScreen if active */}
{currentScreen === 'preview' && (
<View style={[StyleSheet.absoluteFill, { zIndex: 100, elevation: 100, backgroundColor: '#000' }]}>
<PreviewScreen
photoUris={photoUris}
printerIp={settings.printerIp}
onRetakeLast={handleRetakeLast}
onAddAnother={handleAddAnother}
onReset={handleReset}
frameMode={settings.frameMode}
initialFrameId={settings.selectedFrameId}
availableFrameIds={settings.availableFrameIds}
dateOverlay={settings.dateOverlay}
dateOverlayPosition={settings.dateOverlayPosition}
eventText={settings.eventText}
/>
</View>
)}
{/* Overlay AdminScreen if active */}
{currentScreen === 'admin' && (
<View style={[StyleSheet.absoluteFill, { zIndex: 100, elevation: 100, backgroundColor: '#000' }]}>
<AdminScreen
currentSettings={settings}
onSave={handleSaveSettings}
onClose={() => setCurrentScreen('home')}
/>
</View>
)}
</SafeAreaView>
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#000',
},
loadingContainer: {
flex: 1,
backgroundColor: '#000',
justifyContent: 'center',
alignItems: 'center',
},
});