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
+97 -1
View File
@@ -9,6 +9,7 @@ import {
Switch,
Alert,
Modal,
BackHandler,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
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 [burstInterval, setBurstInterval] = useState<string>(String(currentSettings.burstIntervalSec));
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(() => {
const checkKioskStatus = () => {
@@ -191,6 +242,9 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
burstCount: burst,
burstIntervalSec: interval,
welcomeText: welcomeText.trim() || 'Willkommen zu unserer Feier!',
footerText: footerText.trim() || 'ZUM STARTEN TIPPEN • Fotos machen • Collage • Drucken',
useFrontCamera,
enableLogs,
};
try {
@@ -245,7 +299,21 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
<FontAwesomeIcon icon={faUserGear} size={22} color={THEME.colors.accent} />
<Text style={styles.title}>EINSTELLUNGEN</Text>
</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} />
<Text style={styles.closeBtnText}>Zurück zur Fotobox</Text>
</TouchableOpacity>
@@ -319,6 +387,16 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
onChangeText={setWelcomeText}
/>
</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}>
<Text style={styles.label}>Event-Name (für Datum-Sticker)</Text>
<TextInput
@@ -361,6 +439,24 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
keyboardType="number-pad"
/>
</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 style={styles.card}>
+27 -18
View File
@@ -51,9 +51,11 @@ interface CameraScreenProps {
adminPassword?: string;
welcomeText?: string;
burstCount?: number;
burstIntervalSec?: number;
frameMode?: 'off' | 'always' | 'available';
selectedFrameId?: string | null;
burstIntervalSec: number;
frameMode: 'off' | 'always' | 'available';
selectedFrameId: string | null;
footerText: string;
useFrontCamera: boolean;
}
export default function CameraScreen({
@@ -67,20 +69,23 @@ export default function CameraScreen({
adminPassword = '1234',
welcomeText = 'Willkommen zu unserer Feier!',
burstCount = 1,
burstIntervalSec = 5,
frameMode = 'off',
selectedFrameId = null,
burstIntervalSec,
frameMode,
selectedFrameId,
footerText,
useFrontCamera,
}: CameraScreenProps) {
const [permission, requestPermission] = useCameraPermissions();
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 [hasStarted, setHasStarted] = useState<boolean>(false);
const [pictureSize, setPictureSize] = useState<string | undefined>(undefined);
// Burst mode state
// Burst mode state and refs
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 [isBurstWaiting, setIsBurstWaiting] = useState<boolean>(false);
@@ -228,14 +233,15 @@ export default function CameraScreen({
if (burstTimerRef.current) clearTimeout(burstTimerRef.current);
if (countdownTimerRef.current) clearTimeout(countdownTimerRef.current);
if (completionTimerRef.current) clearTimeout(completionTimerRef.current);
const urisToDelete = [...burstUris];
const urisToDelete = [...burstUrisRef.current];
setCountdown(countdownDuration);
setIsCapturing(false);
setIsBurstWaiting(false);
setHasStarted(false);
setBurstIndex(0);
setBurstUris([]);
burstIndexRef.current = 0;
burstUrisRef.current = [];
setShowBurstIndicator('');
onCancel();
@@ -254,7 +260,8 @@ export default function CameraScreen({
if (isIdle) {
setHasStarted(false);
setBurstIndex(0);
setBurstUris([]);
burstIndexRef.current = 0;
burstUrisRef.current = [];
return;
}
@@ -361,13 +368,14 @@ export default function CameraScreen({
// Handle burst mode
const effectiveBurst = burstCount || 1;
const newIndex = burstIndex + 1;
const newUris = [...burstUris, capturedUri];
const newIndex = burstIndexRef.current + 1;
const newUris = [...burstUrisRef.current, capturedUri];
if (effectiveBurst > 1 && newIndex < effectiveBurst) {
// More burst photos to take
burstIndexRef.current = newIndex;
burstUrisRef.current = newUris;
setBurstIndex(newIndex);
setBurstUris(newUris);
setShowBurstIndicator(`Foto ${newIndex} von ${effectiveBurst}`);
setIsCapturing(false);
setIsBurstWaiting(true);
@@ -379,8 +387,9 @@ export default function CameraScreen({
}, burstIntervalSec * 1000);
} else if (effectiveBurst > 1) {
// Final burst photo
burstIndexRef.current = 0;
burstUrisRef.current = [];
setBurstIndex(0);
setBurstUris([]);
// Short delay prevents Android expo-camera crash on immediate unmount
completionTimerRef.current = setTimeout(() => {
onBurstComplete(newUris);
@@ -512,7 +521,7 @@ export default function CameraScreen({
adjustsFontSizeToFit
numberOfLines={1}
>
ZUM STARTEN TIPPEN Fotos machen Collage Drucken
{footerText}
</Text>
</View>
@@ -644,7 +653,7 @@ export default function CameraScreen({
{/* Camera Preview Background */}
<View style={styles.previewContainer}>
<View style={styles.cameraWrapper}>
{Platform.OS === 'android' && isUsbConnected ? (
{Platform.OS === 'android' && !useFrontCamera ? (
<UsbCameraView
ref={usbCameraRef}
style={[styles.cameraPreview, { position: 'absolute', opacity: 1, zIndex: 10 }]}
+17 -4
View File
@@ -6,6 +6,7 @@ import {
Image,
TouchableOpacity,
ActivityIndicator,
BackHandler,
} from 'react-native';
import ViewShot from 'react-native-view-shot';
import { LinearGradient } from 'expo-linear-gradient';
@@ -96,6 +97,18 @@ export default function PreviewScreen({
// Idle Timer state
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
const [selectedFrameId, setSelectedFrameId] = useState<string | null>(
frameMode === 'always' ? initialFrameId : null
@@ -106,7 +119,7 @@ export default function PreviewScreen({
const currentPhoto = photoUris[photoUris.length - 1];
// Get the active frame asset
const activeFrame = selectedFrameId && photoUris.length === 1
const activeFrame = selectedFrameId
? FRAMES.find((f) => f.id === selectedFrameId)
: null;
@@ -578,7 +591,7 @@ export default function PreviewScreen({
</Text>
</TouchableOpacity>
{frameMode === 'available' && photoUris.length === 1 && (
{frameMode === 'available' && (
<TouchableOpacity
style={[styles.editToolBtn, selectedFrameId !== null && styles.editToolBtnActive]}
onPress={() => {
@@ -603,8 +616,8 @@ export default function PreviewScreen({
)}
</View>
{/* Frame Picker (when mode = available and only 1 photo) */}
{frameMode === 'available' && photoUris.length === 1 && (
{/* Frame Picker (when mode = available) */}
{frameMode === 'available' && (
<FramePicker
selectedFrameId={selectedFrameId}
onSelectFrame={(id) => {