212 lines
6.8 KiB
TypeScript
212 lines
6.8 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 } 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);
|
|
|
|
// 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);
|
|
};
|
|
|
|
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}>
|
|
<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}
|
|
/>
|
|
</View>
|
|
|
|
{/* Overlay PreviewScreen if active */}
|
|
{currentScreen === 'preview' && (
|
|
<View style={StyleSheet.absoluteFill}>
|
|
<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}>
|
|
<AdminScreen
|
|
currentSettings={settings}
|
|
onSave={handleSaveSettings}
|
|
onClose={() => setCurrentScreen('home')}
|
|
/>
|
|
</View>
|
|
)}
|
|
</SafeAreaView>
|
|
</GestureHandlerRootView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: '#050510',
|
|
},
|
|
loadingContainer: {
|
|
flex: 1,
|
|
backgroundColor: '#050510',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
});
|