feat: Implement UI enhancements and fix burst bug

This commit is contained in:
2026-06-10 14:43:06 +02:00
parent ac7542d2f9
commit 7fd5b22879
8 changed files with 168 additions and 28 deletions
+7 -3
View File
@@ -5,7 +5,7 @@ import CameraScreen from './src/screens/CameraScreen';
import PreviewScreen from './src/screens/PreviewScreen'; import PreviewScreen from './src/screens/PreviewScreen';
import AdminScreen from './src/screens/AdminScreen'; import AdminScreen from './src/screens/AdminScreen';
import { loadSettings, saveSettings, AppSettings } from './src/services/settings'; import { loadSettings, saveSettings, AppSettings } from './src/services/settings';
import { logger } from './src/services/logger'; import { logger, setLogsEnabled } from './src/services/logger';
import * as FileSystem from 'expo-file-system/legacy'; import * as FileSystem from 'expo-file-system/legacy';
// Set up global error handler // Set up global error handler
@@ -33,6 +33,7 @@ export default function App() {
async function initApp() { async function initApp() {
const savedSettings = await loadSettings(); const savedSettings = await loadSettings();
setSettings(savedSettings); setSettings(savedSettings);
setLogsEnabled(savedSettings.enableLogs);
// Auto-start Kiosk mode if it was configured as enabled // Auto-start Kiosk mode if it was configured as enabled
if (savedSettings.kioskModeEnabled) { if (savedSettings.kioskModeEnabled) {
@@ -129,6 +130,7 @@ export default function App() {
const handleSaveSettings = (updated: AppSettings) => { const handleSaveSettings = (updated: AppSettings) => {
setSettings(updated); setSettings(updated);
setLogsEnabled(updated.enableLogs);
}; };
const handleOpenAdmin = () => { const handleOpenAdmin = () => {
@@ -160,6 +162,8 @@ export default function App() {
burstIntervalSec={settings.burstIntervalSec} burstIntervalSec={settings.burstIntervalSec}
frameMode={settings.frameMode} frameMode={settings.frameMode}
selectedFrameId={settings.selectedFrameId} selectedFrameId={settings.selectedFrameId}
footerText={settings.footerText}
useFrontCamera={settings.useFrontCamera}
/> />
</View> </View>
@@ -200,11 +204,11 @@ export default function App() {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: '#050510', backgroundColor: 'green',
}, },
loadingContainer: { loadingContainer: {
flex: 1, flex: 1,
backgroundColor: '#050510', backgroundColor: 'red',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}, },
BIN
View File
Binary file not shown.
+3 -2
View File
@@ -154,6 +154,7 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
zIndex: 100, zIndex: 100,
elevation: 21, elevation: 21,
padding: 30, // Large invisible padding to significantly increase the touch/pinch hitbox
}, },
emojiText: { emojiText: {
fontSize: 48, fontSize: 48,
@@ -178,8 +179,8 @@ const styles = StyleSheet.create({
}, },
deleteBtn: { deleteBtn: {
position: 'absolute', position: 'absolute',
top: -10, top: 15,
right: -10, right: 15,
width: 24, width: 24,
height: 24, height: 24,
borderRadius: 12, borderRadius: 12,
+97 -1
View File
@@ -9,6 +9,7 @@ import {
Switch, Switch,
Alert, Alert,
Modal, Modal,
BackHandler,
} from 'react-native'; } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
@@ -81,6 +82,56 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
const [burstCount, setBurstCount] = useState<string>(String(currentSettings.burstCount)); const [burstCount, setBurstCount] = useState<string>(String(currentSettings.burstCount));
const [burstInterval, setBurstInterval] = useState<string>(String(currentSettings.burstIntervalSec)); const [burstInterval, setBurstInterval] = useState<string>(String(currentSettings.burstIntervalSec));
const [welcomeText, setWelcomeText] = useState<string>(currentSettings.welcomeText); const [welcomeText, setWelcomeText] = useState<string>(currentSettings.welcomeText);
const [footerText, setFooterText] = useState<string>(currentSettings.footerText);
const [useFrontCamera, setUseFrontCamera] = useState<boolean>(currentSettings.useFrontCamera);
const [enableLogs, setEnableLogs] = useState<boolean>(currentSettings.enableLogs);
const hasUnsavedChanges = () => {
return (
countdown !== String(currentSettings.countdownDuration) ||
printerIp !== currentSettings.printerIp ||
password !== currentSettings.adminPassword ||
kioskActive !== currentSettings.kioskModeEnabled ||
frameMode !== currentSettings.frameMode ||
selectedFrameId !== currentSettings.selectedFrameId ||
availableFrameIds.join() !== (currentSettings.availableFrameIds || []).join() ||
dateOverlay !== currentSettings.dateOverlay ||
dateOverlayPosition !== currentSettings.dateOverlayPosition ||
eventText !== currentSettings.eventText ||
burstCount !== String(currentSettings.burstCount) ||
burstInterval !== String(currentSettings.burstIntervalSec) ||
welcomeText !== currentSettings.welcomeText ||
footerText !== currentSettings.footerText ||
useFrontCamera !== currentSettings.useFrontCamera ||
enableLogs !== currentSettings.enableLogs
);
};
useEffect(() => {
const onBackPress = () => {
if (hasUnsavedChanges()) {
Alert.alert(
'Änderungen speichern?',
'Du hast ungespeicherte Änderungen. Möchtest du diese vor dem Verlassen speichern?',
[
{ text: 'Abbrechen', style: 'cancel' },
{ text: 'Nein (Verwerfen)', onPress: handleClose, style: 'destructive' },
{ text: 'Ja (Speichern)', onPress: async () => { await handleSave(); handleClose(); } },
]
);
} else {
handleClose();
}
return true; // Prevent default
};
BackHandler.addEventListener('hardwareBackPress', onBackPress);
return () => BackHandler.removeEventListener('hardwareBackPress', onBackPress);
}, [
countdown, printerIp, password, kioskActive, frameMode, selectedFrameId, availableFrameIds,
dateOverlay, dateOverlayPosition, eventText, burstCount, burstInterval, welcomeText,
footerText, useFrontCamera, enableLogs, currentSettings
]);
useEffect(() => { useEffect(() => {
const checkKioskStatus = () => { const checkKioskStatus = () => {
@@ -191,6 +242,9 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
burstCount: burst, burstCount: burst,
burstIntervalSec: interval, burstIntervalSec: interval,
welcomeText: welcomeText.trim() || 'Willkommen zu unserer Feier!', welcomeText: welcomeText.trim() || 'Willkommen zu unserer Feier!',
footerText: footerText.trim() || 'ZUM STARTEN TIPPEN • Fotos machen • Collage • Drucken',
useFrontCamera,
enableLogs,
}; };
try { try {
@@ -245,7 +299,21 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
<FontAwesomeIcon icon={faUserGear} size={22} color={THEME.colors.accent} /> <FontAwesomeIcon icon={faUserGear} size={22} color={THEME.colors.accent} />
<Text style={styles.title}>EINSTELLUNGEN</Text> <Text style={styles.title}>EINSTELLUNGEN</Text>
</View> </View>
<TouchableOpacity style={styles.closeBtn} onPress={handleClose}> <TouchableOpacity style={styles.closeBtn} onPress={() => {
if (hasUnsavedChanges()) {
Alert.alert(
'Änderungen speichern?',
'Möchtest du die Änderungen speichern?',
[
{ text: 'Abbrechen', style: 'cancel' },
{ text: 'Verwerfen', onPress: handleClose, style: 'destructive' },
{ text: 'Speichern', onPress: async () => { await handleSave(); handleClose(); } },
]
);
} else {
handleClose();
}
}}>
<FontAwesomeIcon icon={faArrowLeft} size={14} color={THEME.colors.text} /> <FontAwesomeIcon icon={faArrowLeft} size={14} color={THEME.colors.text} />
<Text style={styles.closeBtnText}>Zurück zur Fotobox</Text> <Text style={styles.closeBtnText}>Zurück zur Fotobox</Text>
</TouchableOpacity> </TouchableOpacity>
@@ -319,6 +387,16 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
onChangeText={setWelcomeText} onChangeText={setWelcomeText}
/> />
</View> </View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Subtext (Footer)</Text>
<TextInput
style={styles.input}
placeholder="ZUM STARTEN TIPPEN..."
placeholderTextColor={THEME.colors.textMuted}
value={footerText}
onChangeText={setFooterText}
/>
</View>
<View style={styles.inputGroup}> <View style={styles.inputGroup}>
<Text style={styles.label}>Event-Name (für Datum-Sticker)</Text> <Text style={styles.label}>Event-Name (für Datum-Sticker)</Text>
<TextInput <TextInput
@@ -361,6 +439,24 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
keyboardType="number-pad" keyboardType="number-pad"
/> />
</View> </View>
<View style={styles.toggleRow}>
<Text style={styles.toggleLabel}>Tablet-Frontkamera als Fallback erzwingen</Text>
<Switch
value={useFrontCamera}
onValueChange={setUseFrontCamera}
trackColor={{ false: THEME.colors.surfaceSecondary, true: THEME.colors.accent }}
thumbColor={THEME.colors.text}
/>
</View>
<View style={styles.toggleRow}>
<Text style={styles.toggleLabel}>App-Protokollierung (Logs) aktivieren</Text>
<Switch
value={enableLogs}
onValueChange={setEnableLogs}
trackColor={{ false: THEME.colors.surfaceSecondary, true: THEME.colors.accent }}
thumbColor={THEME.colors.text}
/>
</View>
</View> </View>
<View style={styles.card}> <View style={styles.card}>
+27 -18
View File
@@ -51,9 +51,11 @@ interface CameraScreenProps {
adminPassword?: string; adminPassword?: string;
welcomeText?: string; welcomeText?: string;
burstCount?: number; burstCount?: number;
burstIntervalSec?: number; burstIntervalSec: number;
frameMode?: 'off' | 'always' | 'available'; frameMode: 'off' | 'always' | 'available';
selectedFrameId?: string | null; selectedFrameId: string | null;
footerText: string;
useFrontCamera: boolean;
} }
export default function CameraScreen({ export default function CameraScreen({
@@ -67,20 +69,23 @@ export default function CameraScreen({
adminPassword = '1234', adminPassword = '1234',
welcomeText = 'Willkommen zu unserer Feier!', welcomeText = 'Willkommen zu unserer Feier!',
burstCount = 1, burstCount = 1,
burstIntervalSec = 5, burstIntervalSec,
frameMode = 'off', frameMode,
selectedFrameId = null, selectedFrameId,
footerText,
useFrontCamera,
}: CameraScreenProps) { }: CameraScreenProps) {
const [permission, requestPermission] = useCameraPermissions(); const [permission, requestPermission] = useCameraPermissions();
const [isUsbConnected, setIsUsbConnected] = useState<boolean>(false); const [isUsbConnected, setIsUsbConnected] = useState<boolean>(false);
const [countdown, setCountdown] = useState<number | string>(''); const [countdown, setCountdown] = useState<number | 'smile'>(countdownDuration);
const [isCapturing, setIsCapturing] = useState<boolean>(false); const [isCapturing, setIsCapturing] = useState<boolean>(false);
const [hasStarted, setHasStarted] = useState<boolean>(false); const [hasStarted, setHasStarted] = useState<boolean>(false);
const [pictureSize, setPictureSize] = useState<string | undefined>(undefined); const [pictureSize, setPictureSize] = useState<string | undefined>(undefined);
// Burst mode state // Burst mode state and refs
const [burstIndex, setBurstIndex] = useState<number>(0); const [burstIndex, setBurstIndex] = useState<number>(0);
const [burstUris, setBurstUris] = useState<string[]>([]); const burstIndexRef = useRef<number>(0);
const burstUrisRef = useRef<string[]>([]);
const [showBurstIndicator, setShowBurstIndicator] = useState<string>(''); const [showBurstIndicator, setShowBurstIndicator] = useState<string>('');
const [isBurstWaiting, setIsBurstWaiting] = useState<boolean>(false); const [isBurstWaiting, setIsBurstWaiting] = useState<boolean>(false);
@@ -228,14 +233,15 @@ export default function CameraScreen({
if (burstTimerRef.current) clearTimeout(burstTimerRef.current); if (burstTimerRef.current) clearTimeout(burstTimerRef.current);
if (countdownTimerRef.current) clearTimeout(countdownTimerRef.current); if (countdownTimerRef.current) clearTimeout(countdownTimerRef.current);
if (completionTimerRef.current) clearTimeout(completionTimerRef.current); if (completionTimerRef.current) clearTimeout(completionTimerRef.current);
const urisToDelete = [...burstUris]; const urisToDelete = [...burstUrisRef.current];
setCountdown(countdownDuration); setCountdown(countdownDuration);
setIsCapturing(false); setIsCapturing(false);
setIsBurstWaiting(false); setIsBurstWaiting(false);
setHasStarted(false); setHasStarted(false);
setBurstIndex(0); setBurstIndex(0);
setBurstUris([]); burstIndexRef.current = 0;
burstUrisRef.current = [];
setShowBurstIndicator(''); setShowBurstIndicator('');
onCancel(); onCancel();
@@ -254,7 +260,8 @@ export default function CameraScreen({
if (isIdle) { if (isIdle) {
setHasStarted(false); setHasStarted(false);
setBurstIndex(0); setBurstIndex(0);
setBurstUris([]); burstIndexRef.current = 0;
burstUrisRef.current = [];
return; return;
} }
@@ -361,13 +368,14 @@ export default function CameraScreen({
// Handle burst mode // Handle burst mode
const effectiveBurst = burstCount || 1; const effectiveBurst = burstCount || 1;
const newIndex = burstIndex + 1; const newIndex = burstIndexRef.current + 1;
const newUris = [...burstUris, capturedUri]; const newUris = [...burstUrisRef.current, capturedUri];
if (effectiveBurst > 1 && newIndex < effectiveBurst) { if (effectiveBurst > 1 && newIndex < effectiveBurst) {
// More burst photos to take // More burst photos to take
burstIndexRef.current = newIndex;
burstUrisRef.current = newUris;
setBurstIndex(newIndex); setBurstIndex(newIndex);
setBurstUris(newUris);
setShowBurstIndicator(`Foto ${newIndex} von ${effectiveBurst}`); setShowBurstIndicator(`Foto ${newIndex} von ${effectiveBurst}`);
setIsCapturing(false); setIsCapturing(false);
setIsBurstWaiting(true); setIsBurstWaiting(true);
@@ -379,8 +387,9 @@ export default function CameraScreen({
}, burstIntervalSec * 1000); }, burstIntervalSec * 1000);
} else if (effectiveBurst > 1) { } else if (effectiveBurst > 1) {
// Final burst photo // Final burst photo
burstIndexRef.current = 0;
burstUrisRef.current = [];
setBurstIndex(0); setBurstIndex(0);
setBurstUris([]);
// Short delay prevents Android expo-camera crash on immediate unmount // Short delay prevents Android expo-camera crash on immediate unmount
completionTimerRef.current = setTimeout(() => { completionTimerRef.current = setTimeout(() => {
onBurstComplete(newUris); onBurstComplete(newUris);
@@ -512,7 +521,7 @@ export default function CameraScreen({
adjustsFontSizeToFit adjustsFontSizeToFit
numberOfLines={1} numberOfLines={1}
> >
ZUM STARTEN TIPPEN Fotos machen Collage Drucken {footerText}
</Text> </Text>
</View> </View>
@@ -644,7 +653,7 @@ export default function CameraScreen({
{/* Camera Preview Background */} {/* Camera Preview Background */}
<View style={styles.previewContainer}> <View style={styles.previewContainer}>
<View style={styles.cameraWrapper}> <View style={styles.cameraWrapper}>
{Platform.OS === 'android' && isUsbConnected ? ( {Platform.OS === 'android' && !useFrontCamera ? (
<UsbCameraView <UsbCameraView
ref={usbCameraRef} ref={usbCameraRef}
style={[styles.cameraPreview, { position: 'absolute', opacity: 1, zIndex: 10 }]} style={[styles.cameraPreview, { position: 'absolute', opacity: 1, zIndex: 10 }]}
+17 -4
View File
@@ -6,6 +6,7 @@ import {
Image, Image,
TouchableOpacity, TouchableOpacity,
ActivityIndicator, ActivityIndicator,
BackHandler,
} from 'react-native'; } from 'react-native';
import ViewShot from 'react-native-view-shot'; import ViewShot from 'react-native-view-shot';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
@@ -96,6 +97,18 @@ export default function PreviewScreen({
// Idle Timer state // Idle Timer state
const idleTimerRef = useRef<any>(null); const idleTimerRef = useRef<any>(null);
// Hardware Back Press
useEffect(() => {
const onBackPress = () => {
onReset();
return true; // Prevent default behavior (closing the app)
};
BackHandler.addEventListener('hardwareBackPress', onBackPress);
return () => {
BackHandler.removeEventListener('hardwareBackPress', onBackPress);
};
}, [onReset]);
// Frame state // Frame state
const [selectedFrameId, setSelectedFrameId] = useState<string | null>( const [selectedFrameId, setSelectedFrameId] = useState<string | null>(
frameMode === 'always' ? initialFrameId : null frameMode === 'always' ? initialFrameId : null
@@ -106,7 +119,7 @@ export default function PreviewScreen({
const currentPhoto = photoUris[photoUris.length - 1]; const currentPhoto = photoUris[photoUris.length - 1];
// Get the active frame asset // Get the active frame asset
const activeFrame = selectedFrameId && photoUris.length === 1 const activeFrame = selectedFrameId
? FRAMES.find((f) => f.id === selectedFrameId) ? FRAMES.find((f) => f.id === selectedFrameId)
: null; : null;
@@ -578,7 +591,7 @@ export default function PreviewScreen({
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
{frameMode === 'available' && photoUris.length === 1 && ( {frameMode === 'available' && (
<TouchableOpacity <TouchableOpacity
style={[styles.editToolBtn, selectedFrameId !== null && styles.editToolBtnActive]} style={[styles.editToolBtn, selectedFrameId !== null && styles.editToolBtnActive]}
onPress={() => { onPress={() => {
@@ -603,8 +616,8 @@ export default function PreviewScreen({
)} )}
</View> </View>
{/* Frame Picker (when mode = available and only 1 photo) */} {/* Frame Picker (when mode = available) */}
{frameMode === 'available' && photoUris.length === 1 && ( {frameMode === 'available' && (
<FramePicker <FramePicker
selectedFrameId={selectedFrameId} selectedFrameId={selectedFrameId}
onSelectFrame={(id) => { onSelectFrame={(id) => {
+10
View File
@@ -5,6 +5,11 @@ const logFileUri = FileSystem.documentDirectory + 'app_logs.txt';
// Queue to serialize file operations and prevent race conditions // Queue to serialize file operations and prevent race conditions
const logQueue: string[] = []; const logQueue: string[] = [];
let isWriting = false; let isWriting = false;
let isEnabled = true;
export const setLogsEnabled = (enabled: boolean) => {
isEnabled = enabled;
};
const processLogQueue = async () => { const processLogQueue = async () => {
if (isWriting) return; if (isWriting) return;
@@ -48,6 +53,7 @@ const enqueueWrite = async (logLine: string) => {
export const logger = { export const logger = {
log: async (message: string) => { log: async (message: string) => {
if (!isEnabled) return;
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
const logLine = `[INFO] ${timestamp}: ${message}\n`; const logLine = `[INFO] ${timestamp}: ${message}\n`;
console.log(logLine.trim()); console.log(logLine.trim());
@@ -55,6 +61,10 @@ export const logger = {
}, },
error: async (message: string, error?: any) => { error: async (message: string, error?: any) => {
if (!isEnabled) {
console.error(`[ERROR]: ${message}`, error);
return;
}
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
let errorString = ''; let errorString = '';
if (error) { if (error) {
+7
View File
@@ -21,6 +21,10 @@ export interface AppSettings {
burstIntervalSec: number; // seconds between burst shots burstIntervalSec: number; // seconds between burst shots
// Welcome screen text // Welcome screen text
welcomeText: string; welcomeText: string;
footerText: string;
// System
useFrontCamera: boolean;
enableLogs: boolean;
} }
const settingsFile = new File(Paths.document, 'settings.json'); const settingsFile = new File(Paths.document, 'settings.json');
@@ -39,6 +43,9 @@ const DEFAULT_SETTINGS: AppSettings = {
burstCount: 1, burstCount: 1,
burstIntervalSec: 5, burstIntervalSec: 5,
welcomeText: 'Willkommen zu unserer Feier!', welcomeText: 'Willkommen zu unserer Feier!',
footerText: 'ZUM STARTEN TIPPEN • Fotos machen • Collage • Drucken',
useFrontCamera: false,
enableLogs: true,
}; };
/** /**