897 lines
26 KiB
TypeScript
897 lines
26 KiB
TypeScript
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
import {
|
|
StyleSheet,
|
|
Text,
|
|
View,
|
|
TouchableOpacity,
|
|
Platform,
|
|
Modal,
|
|
TextInput,
|
|
TouchableWithoutFeedback,
|
|
} from 'react-native';
|
|
import { CameraView, useCameraPermissions } from 'expo-camera';
|
|
import * as FileSystem from 'expo-file-system';
|
|
import { File, Paths } from 'expo-file-system/next';
|
|
import * as ImageManipulator from 'expo-image-manipulator';
|
|
import { LinearGradient } from 'expo-linear-gradient';
|
|
import Animated, {
|
|
useSharedValue,
|
|
useAnimatedStyle,
|
|
withSpring,
|
|
withTiming,
|
|
FadeIn,
|
|
FadeOut,
|
|
withSequence,
|
|
Easing,
|
|
runOnJS,
|
|
} from 'react-native-reanimated';
|
|
import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome';
|
|
import {
|
|
faUserGear,
|
|
faCameraRetro,
|
|
faCamera,
|
|
faXmark,
|
|
} from '@fortawesome/free-solid-svg-icons';
|
|
import { THEME } from '../styles/theme';
|
|
import { UsbCameraView, UsbCameraRef } from '../../modules/usb-camera';
|
|
import { saveToGallery } from '../services/storage';
|
|
import FrameOverlay from '../components/FrameOverlay';
|
|
import { FRAMES } from '../data/frames';
|
|
import { logger } from '../services/logger';
|
|
|
|
interface CameraScreenProps {
|
|
countdownDuration: number;
|
|
onPhotoCaptured: (uri: string) => void;
|
|
onBurstComplete: (uris: string[]) => void;
|
|
onCancel: () => void;
|
|
isIdle?: boolean;
|
|
onStartBooth?: () => void;
|
|
onNavigateToAdmin?: () => void;
|
|
adminPassword?: string;
|
|
welcomeText?: string;
|
|
burstCount?: number;
|
|
burstIntervalSec?: number;
|
|
frameMode?: 'off' | 'always' | 'available';
|
|
selectedFrameId?: string | null;
|
|
}
|
|
|
|
export default function CameraScreen({
|
|
countdownDuration,
|
|
onPhotoCaptured,
|
|
onBurstComplete,
|
|
onCancel,
|
|
isIdle = false,
|
|
onStartBooth,
|
|
onNavigateToAdmin,
|
|
adminPassword = '1234',
|
|
welcomeText = 'Willkommen zu unserer Feier!',
|
|
burstCount = 1,
|
|
burstIntervalSec = 5,
|
|
frameMode = 'off',
|
|
selectedFrameId = null,
|
|
}: CameraScreenProps) {
|
|
const [permission, requestPermission] = useCameraPermissions();
|
|
const [isUsbConnected, setIsUsbConnected] = useState<boolean>(false);
|
|
const [countdown, setCountdown] = useState<number | string>('');
|
|
const [isCapturing, setIsCapturing] = useState<boolean>(false);
|
|
const [hasStarted, setHasStarted] = useState<boolean>(false);
|
|
const [pictureSize, setPictureSize] = useState<string | undefined>(undefined);
|
|
|
|
// Burst mode state
|
|
const [burstIndex, setBurstIndex] = useState<number>(0);
|
|
const [burstUris, setBurstUris] = useState<string[]>([]);
|
|
const [showBurstIndicator, setShowBurstIndicator] = useState<string>('');
|
|
const [isBurstWaiting, setIsBurstWaiting] = useState<boolean>(false);
|
|
|
|
// Flash effect
|
|
const flashOpacity = useSharedValue(0);
|
|
|
|
// Admin Modal States
|
|
const [passwordModalVisible, setPasswordModalVisible] = useState(false);
|
|
const [enteredPassword, setEnteredPassword] = useState('');
|
|
const [errorText, setErrorText] = useState('');
|
|
|
|
const usbCameraRef = useRef<UsbCameraRef>(null);
|
|
const expoCameraRef = useRef<any>(null);
|
|
const burstTimerRef = useRef<any>(null);
|
|
const countdownTimerRef = useRef<any>(null);
|
|
const isCancelledRef = useRef<boolean>(false);
|
|
|
|
// Reanimated countdown pulse animation
|
|
const scale = useSharedValue(1);
|
|
|
|
useEffect(() => {
|
|
if (countdown !== '') {
|
|
scale.value = 0.5;
|
|
scale.value = withSpring(1.25, { damping: 10, stiffness: 120 }, () => {
|
|
scale.value = withTiming(1.0, { duration: 150 });
|
|
});
|
|
}
|
|
}, [countdown]);
|
|
|
|
const animatedStyle = useAnimatedStyle(() => ({
|
|
transform: [{ scale: scale.value }],
|
|
}));
|
|
|
|
const flashAnimatedStyle = useAnimatedStyle(() => ({
|
|
opacity: flashOpacity.value,
|
|
}));
|
|
|
|
// Trigger white flash effect
|
|
const triggerFlash = useCallback(() => {
|
|
flashOpacity.value = withSequence(
|
|
withTiming(1, { duration: 80 }),
|
|
withTiming(0, { duration: 400 })
|
|
);
|
|
}, []);
|
|
|
|
// 1. Request built-in camera permission on mount (just in case we fallback)
|
|
useEffect(() => {
|
|
if (!permission || !permission.granted) {
|
|
requestPermission();
|
|
}
|
|
}, [permission]);
|
|
|
|
// 2. Check if a USB camera is connected.
|
|
useEffect(() => {
|
|
let checkInterval: any;
|
|
if (Platform.OS === 'android') {
|
|
const checkConnection = async () => {
|
|
try {
|
|
if (usbCameraRef.current) {
|
|
const connected = await usbCameraRef.current.isCameraConnected();
|
|
setIsUsbConnected(connected);
|
|
}
|
|
} catch (e) {
|
|
setIsUsbConnected(false);
|
|
}
|
|
};
|
|
|
|
checkConnection();
|
|
checkInterval = setInterval(checkConnection, 2000);
|
|
}
|
|
|
|
return () => {
|
|
if (checkInterval) clearInterval(checkInterval);
|
|
};
|
|
}, []);
|
|
|
|
// Cleanup burst timer on unmount
|
|
useEffect(() => {
|
|
return () => {
|
|
if (burstTimerRef.current) clearTimeout(burstTimerRef.current);
|
|
if (countdownTimerRef.current) clearTimeout(countdownTimerRef.current);
|
|
};
|
|
}, []);
|
|
|
|
// Set safe picture size to avoid OOM
|
|
const handleCameraReady = useCallback(async () => {
|
|
if (expoCameraRef.current) {
|
|
try {
|
|
const sizes = await expoCameraRef.current.getAvailablePictureSizes();
|
|
if (sizes && sizes.length > 0) {
|
|
console.log('Available camera sizes:', sizes);
|
|
const parsedSizes = sizes.map((s: string) => {
|
|
const parts = s.split('x');
|
|
if (parts.length === 2) {
|
|
const w = parseInt(parts[0], 10);
|
|
const h = parseInt(parts[1], 10);
|
|
return { size: s, pixels: w * h, w, h };
|
|
}
|
|
return { size: s, pixels: 0, w: 0, h: 0 };
|
|
}).filter((s: { pixels: number }) => s.pixels > 0);
|
|
|
|
parsedSizes.sort((a, b) => a.pixels - b.pixels);
|
|
// Find the smallest size that has at least 1920 width or height (approx 2MP)
|
|
const optimal = parsedSizes.find((s: { w: number; h: number }) => Math.max(s.w, s.h) >= 1920);
|
|
|
|
if (optimal) {
|
|
console.log('Setting optimal pictureSize to prevent OOM:', optimal.size);
|
|
setPictureSize(optimal.size);
|
|
} else if (parsedSizes.length > 0) {
|
|
setPictureSize(parsedSizes[parsedSizes.length - 1].size);
|
|
} else {
|
|
setPictureSize('fallback');
|
|
}
|
|
} else {
|
|
setPictureSize('fallback');
|
|
}
|
|
} catch (e) {
|
|
console.warn('Failed to fetch picture sizes:', e);
|
|
setPictureSize('fallback');
|
|
}
|
|
}
|
|
}, []);
|
|
|
|
// Cancel capture process
|
|
const handleLocalCancel = async () => {
|
|
isCancelledRef.current = true;
|
|
if (burstTimerRef.current) clearTimeout(burstTimerRef.current);
|
|
if (countdownTimerRef.current) clearTimeout(countdownTimerRef.current);
|
|
|
|
// Clean up any temp files from a partially completed burst
|
|
for (const uri of burstUris) {
|
|
try {
|
|
await FileSystem.deleteAsync(uri, { idempotent: true });
|
|
} catch (e) {
|
|
console.warn('Failed to delete temp file on cancel:', e);
|
|
}
|
|
}
|
|
|
|
setCountdown(countdownDuration);
|
|
setIsCapturing(false);
|
|
setIsBurstWaiting(false);
|
|
setHasStarted(false);
|
|
setBurstIndex(0);
|
|
setBurstUris([]);
|
|
setShowBurstIndicator('');
|
|
onCancel();
|
|
};
|
|
|
|
// 3. Start the countdown on screen load (only when NOT idle)
|
|
useEffect(() => {
|
|
if (isIdle) {
|
|
setHasStarted(false);
|
|
setBurstIndex(0);
|
|
setBurstUris([]);
|
|
return;
|
|
}
|
|
|
|
// Delay start until safe picture size is negotiated (or fallback is set)
|
|
// Only applies to built-in camera, USB camera ignores pictureSize
|
|
if (pictureSize === undefined && !isUsbConnected) {
|
|
console.log('Waiting for pictureSize negotiation...');
|
|
return;
|
|
}
|
|
|
|
startCountdown();
|
|
|
|
return () => {};
|
|
}, [countdownDuration, isIdle, pictureSize, isUsbConnected]);
|
|
|
|
const startCountdown = () => {
|
|
isCancelledRef.current = false;
|
|
let count = countdownDuration;
|
|
|
|
setCountdown(count);
|
|
setHasStarted(true);
|
|
setIsCapturing(false);
|
|
setShowBurstIndicator('');
|
|
|
|
const runTimer = () => {
|
|
if (count > 1) {
|
|
count -= 1;
|
|
setCountdown(count);
|
|
countdownTimerRef.current = setTimeout(runTimer, 1000);
|
|
} else if (count === 1) {
|
|
setCountdown('smile');
|
|
setIsCapturing(true);
|
|
countdownTimerRef.current = setTimeout(() => {
|
|
capture();
|
|
}, 800);
|
|
}
|
|
};
|
|
|
|
countdownTimerRef.current = setTimeout(runTimer, 1000);
|
|
};
|
|
|
|
// 4. Capture photo function
|
|
const capture = async () => {
|
|
const filename = `photo_${Date.now()}.jpg`;
|
|
const tempFile = new File(Paths.cache, filename);
|
|
const tempUri = tempFile.uri;
|
|
|
|
try {
|
|
triggerFlash();
|
|
|
|
let capturedUri: string;
|
|
let rawWidth: number = 0;
|
|
let rawHeight: number = 0;
|
|
|
|
if (Platform.OS === 'android' && isUsbConnected && usbCameraRef.current) {
|
|
// USB Camera Capture
|
|
console.log('Capturing from USB Camera...');
|
|
const path = await usbCameraRef.current.takePicture(tempUri);
|
|
capturedUri = path;
|
|
} else {
|
|
// Fallback Camera Capture
|
|
logger.log(`Capturing from built-in camera fallback. Size constraint: ${pictureSize || 'default'}`);
|
|
console.log('Capturing from built-in camera fallback...');
|
|
if (expoCameraRef.current) {
|
|
const photo = await expoCameraRef.current.takePictureAsync({
|
|
quality: 0.7, // Lower quality slightly to reduce file size
|
|
skipProcessing: Platform.OS === 'android', // Prevent memory crash on Android
|
|
});
|
|
capturedUri = photo.uri;
|
|
} else {
|
|
throw new Error('Kamera-Referenz ist nicht verfügbar.');
|
|
}
|
|
}
|
|
|
|
// If user cancelled while the camera promise was processing, abort before side-effects
|
|
if (isCancelledRef.current) {
|
|
console.log('Capture cancelled by user during processing.');
|
|
return;
|
|
}
|
|
|
|
// Instead of manipulating the image and risking an OOM or BitmapFactory crash,
|
|
// we directly use the captured photo and rely on React Native's Image component
|
|
// to safely downsample it during rendering via resizeMethod="resize".
|
|
console.log('Photo captured successfully, proceeding to PreviewScreen.');
|
|
|
|
// Auto-save individual photo to gallery
|
|
try {
|
|
await saveToGallery(capturedUri);
|
|
console.log('Auto-saved capture to gallery');
|
|
} catch (e) {
|
|
console.error('Failed to auto-save capture:', e);
|
|
}
|
|
|
|
// Handle burst mode
|
|
const effectiveBurst = burstCount || 1;
|
|
const newIndex = burstIndex + 1;
|
|
const newUris = [...burstUris, capturedUri];
|
|
|
|
if (effectiveBurst > 1 && newIndex < effectiveBurst) {
|
|
// More burst photos to take
|
|
setBurstIndex(newIndex);
|
|
setBurstUris(newUris);
|
|
setShowBurstIndicator(`Foto ${newIndex} von ${effectiveBurst} ✓`);
|
|
setIsCapturing(false);
|
|
setIsBurstWaiting(true);
|
|
|
|
// Wait burstIntervalSec then start next countdown
|
|
burstTimerRef.current = setTimeout(() => {
|
|
setIsBurstWaiting(false);
|
|
startCountdown();
|
|
}, burstIntervalSec * 1000);
|
|
} else if (effectiveBurst > 1) {
|
|
// Final burst photo
|
|
setBurstIndex(0);
|
|
setBurstUris([]);
|
|
// Short delay prevents Android expo-camera crash on immediate unmount
|
|
setTimeout(() => {
|
|
onBurstComplete(newUris);
|
|
}, 400);
|
|
} else {
|
|
// Single shot mode
|
|
setTimeout(() => {
|
|
onPhotoCaptured(capturedUri);
|
|
}, 400);
|
|
}
|
|
} catch (error: any) {
|
|
console.error('Capture failed:', error);
|
|
alert('Fehler beim Aufnehmen des Fotos: ' + error.message);
|
|
onCancel();
|
|
}
|
|
};
|
|
|
|
const handleSettingsTap = () => {
|
|
setEnteredPassword('');
|
|
setErrorText('');
|
|
setPasswordModalVisible(true);
|
|
};
|
|
|
|
const handlePasswordSubmit = () => {
|
|
if (enteredPassword === adminPassword) {
|
|
setPasswordModalVisible(false);
|
|
setTimeout(() => {
|
|
onNavigateToAdmin?.();
|
|
}, 350); // Delay navigation on iOS to avoid unmounting a view with an open modal
|
|
} else {
|
|
setErrorText('Falsches Passwort');
|
|
setEnteredPassword('');
|
|
}
|
|
};
|
|
|
|
if (!permission) {
|
|
return (
|
|
<View style={styles.container}>
|
|
<Text style={styles.infoText}>Berechtigungen werden geladen...</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (!permission.granted && !isUsbConnected) {
|
|
return (
|
|
<View style={styles.container}>
|
|
<Text style={styles.infoText}>Wir benötigen deine Erlaubnis, um die Kamera anzuzeigen</Text>
|
|
<TouchableOpacity style={styles.btn} onPress={requestPermission}>
|
|
<Text style={styles.btnText}>Erlaubnis erteilen</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity style={[styles.btn, styles.cancelBtn]} onPress={handleLocalCancel}>
|
|
<Text style={styles.btnText}>Zurück</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const renderContent = () => {
|
|
if (isIdle) {
|
|
// Home Overlay (frosty glass box on top of the camera stream)
|
|
return (
|
|
<TouchableWithoutFeedback onPress={onStartBooth}>
|
|
<View style={styles.idleOverlayContainer}>
|
|
{/* Settings button in the top-right corner */}
|
|
<TouchableOpacity
|
|
style={styles.settingsButton}
|
|
onPress={handleSettingsTap}
|
|
activeOpacity={0.7}
|
|
>
|
|
<FontAwesomeIcon icon={faUserGear} size={22} color={THEME.colors.accent} />
|
|
</TouchableOpacity>
|
|
|
|
{/* Header Event Text */}
|
|
<Text style={styles.headerTitle}>{welcomeText}</Text>
|
|
|
|
{/* Footer Instructions */}
|
|
<Text style={styles.footerInstructions}>
|
|
ZUM STARTEN TIPPEN • Fotos machen • Collage • Drucken
|
|
</Text>
|
|
|
|
{/* Password Prompt Modal */}
|
|
<Modal
|
|
visible={passwordModalVisible}
|
|
transparent={true}
|
|
animationType="fade"
|
|
onRequestClose={() => setPasswordModalVisible(false)}
|
|
>
|
|
<View style={styles.modalOverlay}>
|
|
<View style={styles.modalContent}>
|
|
<Text style={styles.modalTitle}>Admin-Zugang</Text>
|
|
<Text style={styles.modalSub}>Passwort eingeben, um Einstellungen zu öffnen</Text>
|
|
|
|
<TextInput
|
|
style={styles.input}
|
|
secureTextEntry
|
|
placeholder="Passwort"
|
|
placeholderTextColor={THEME.colors.textMuted}
|
|
value={enteredPassword}
|
|
onChangeText={setEnteredPassword}
|
|
keyboardType="numeric"
|
|
autoFocus
|
|
/>
|
|
|
|
{errorText ? <Text style={styles.error}>{errorText}</Text> : null}
|
|
|
|
<View style={styles.modalButtons}>
|
|
<TouchableOpacity
|
|
style={[styles.button, styles.cancelButton]}
|
|
onPress={() => setPasswordModalVisible(false)}
|
|
>
|
|
<Text style={styles.buttonText}>Abbrechen</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
style={[styles.button, styles.confirmButton]}
|
|
onPress={handlePasswordSubmit}
|
|
>
|
|
<LinearGradient
|
|
colors={THEME.gradient.primary}
|
|
start={{ x: 0, y: 0.5 }}
|
|
end={{ x: 1, y: 0.5 }}
|
|
style={styles.confirmGradient}
|
|
>
|
|
<Text style={styles.buttonText}>Öffnen</Text>
|
|
</LinearGradient>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
</View>
|
|
</TouchableWithoutFeedback>
|
|
);
|
|
}
|
|
|
|
// Capture Mode overlays (Countdown, Burst indicator, and Cancel button)
|
|
const activeFrame = selectedFrameId && burstCount === 1
|
|
? FRAMES.find((f) => f.id === selectedFrameId)
|
|
: null;
|
|
|
|
return (
|
|
<View style={styles.captureOverlayContainer} pointerEvents="box-none">
|
|
{/* Frame Overlay */}
|
|
{frameMode === 'always' && activeFrame && (
|
|
<View style={styles.cameraWrapper} pointerEvents="none">
|
|
<FrameOverlay frameAsset={activeFrame.asset} />
|
|
</View>
|
|
)}
|
|
|
|
{/* Source indicator */}
|
|
<View style={styles.hudContainer}>
|
|
<Text style={styles.cameraSourceIndicator}>
|
|
Kamera: {isUsbConnected ? 'Externe USB-Kamera' : 'Tablet-Frontkamera (Fallback)'}
|
|
</Text>
|
|
</View>
|
|
|
|
{/* Burst progress indicator */}
|
|
{(burstCount || 1) > 1 && (
|
|
<View style={styles.burstBadge}>
|
|
<FontAwesomeIcon icon={faCameraRetro} size={12} color={THEME.colors.accent} />
|
|
<Text style={styles.burstBadgeText}>
|
|
Foto {burstIndex + 1} / {burstCount}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{/* Burst waiting indicator */}
|
|
{isBurstWaiting && showBurstIndicator !== '' && (
|
|
<View style={styles.countdownOverlay} pointerEvents="none">
|
|
<View style={styles.burstWaitingBox}>
|
|
<Text style={styles.burstWaitingText}>{showBurstIndicator}</Text>
|
|
<Text style={styles.burstWaitingSub}>Nächstes Foto in {burstIntervalSec}s…</Text>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{/* Countdown overlay */}
|
|
{hasStarted && !isBurstWaiting && (
|
|
<View style={styles.countdownOverlay} pointerEvents="none">
|
|
<Animated.View style={[styles.countdownBox, animatedStyle]}>
|
|
{countdown === 'smile' ? (
|
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
|
|
<Text style={styles.countdownText}>Bitte lächeln!</Text>
|
|
<FontAwesomeIcon icon={faCamera} size={48} color={THEME.colors.accent} />
|
|
</View>
|
|
) : (
|
|
<Text style={styles.countdownText}>{countdown}</Text>
|
|
)}
|
|
</Animated.View>
|
|
</View>
|
|
)}
|
|
|
|
{/* Cancel Button */}
|
|
{!isCapturing && !isBurstWaiting && (
|
|
<TouchableOpacity style={styles.backButton} onPress={handleLocalCancel}>
|
|
<FontAwesomeIcon icon={faXmark} size={16} color={THEME.colors.text} style={{ marginRight: 6 }} />
|
|
<Text style={styles.backButtonText}>ABBRECHEN</Text>
|
|
</TouchableOpacity>
|
|
)}
|
|
</View>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<Animated.View entering={FadeIn.duration(400)} exiting={FadeOut.duration(300)} style={styles.container}>
|
|
{/* Camera Preview Background */}
|
|
<View style={styles.previewContainer}>
|
|
<View style={styles.cameraWrapper}>
|
|
{Platform.OS === 'android' && isUsbConnected ? (
|
|
<UsbCameraView ref={usbCameraRef} style={styles.cameraPreview} />
|
|
) : (
|
|
<CameraView
|
|
ref={expoCameraRef}
|
|
style={styles.cameraPreview}
|
|
facing="front"
|
|
pictureSize={pictureSize === 'fallback' ? undefined : pictureSize}
|
|
onCameraReady={handleCameraReady}
|
|
/>
|
|
)}
|
|
</View>
|
|
</View>
|
|
|
|
{/* Render UI controls/overlays on top */}
|
|
{renderContent()}
|
|
|
|
{/* White Flash Overlay */}
|
|
<Animated.View
|
|
style={[styles.flashOverlay, flashAnimatedStyle]}
|
|
pointerEvents="none"
|
|
/>
|
|
</Animated.View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: '#000',
|
|
},
|
|
previewContainer: {
|
|
width: '100%',
|
|
height: '100%',
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
backgroundColor: '#000',
|
|
},
|
|
cameraWrapper: {
|
|
height: '100%',
|
|
aspectRatio: 3 / 2,
|
|
},
|
|
cameraPreview: {
|
|
width: '100%',
|
|
height: '100%',
|
|
},
|
|
flashOverlay: {
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
backgroundColor: '#FFFFFF',
|
|
zIndex: 9999,
|
|
},
|
|
idleOverlayContainer: {
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
backgroundColor: 'transparent',
|
|
},
|
|
captureOverlayContainer: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
settingsButton: {
|
|
position: 'absolute',
|
|
top: 24,
|
|
right: 24,
|
|
width: 50,
|
|
height: 50,
|
|
borderRadius: THEME.borderRadius.round,
|
|
backgroundColor: 'rgba(13, 13, 26, 0.8)',
|
|
borderWidth: 1,
|
|
borderColor: THEME.colors.gradientBorder,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
zIndex: 999,
|
|
},
|
|
headerTitle: {
|
|
position: 'absolute',
|
|
top: 40,
|
|
width: '100%',
|
|
textAlign: 'center',
|
|
fontSize: 48,
|
|
fontWeight: '900',
|
|
color: THEME.colors.accent,
|
|
letterSpacing: 6,
|
|
textShadowColor: THEME.colors.cyanGlow,
|
|
textShadowOffset: { width: 0, height: 0 },
|
|
textShadowRadius: 25,
|
|
},
|
|
footerInstructions: {
|
|
position: 'absolute',
|
|
bottom: 40,
|
|
width: '100%',
|
|
textAlign: 'center',
|
|
fontSize: 20,
|
|
fontWeight: 'bold',
|
|
color: THEME.colors.text,
|
|
letterSpacing: 2,
|
|
textShadowColor: 'rgba(0,0,0,0.9)',
|
|
textShadowOffset: { width: 0, height: 2 },
|
|
textShadowRadius: 15,
|
|
},
|
|
hudContainer: {
|
|
position: 'absolute',
|
|
bottom: 20,
|
|
backgroundColor: 'rgba(5, 5, 16, 0.8)',
|
|
paddingVertical: THEME.spacing.xs,
|
|
paddingHorizontal: THEME.spacing.md,
|
|
borderRadius: THEME.borderRadius.sm,
|
|
borderWidth: 0.5,
|
|
borderColor: THEME.colors.gradientBorder,
|
|
},
|
|
cameraSourceIndicator: {
|
|
color: THEME.colors.text,
|
|
fontSize: 14,
|
|
fontWeight: 'bold',
|
|
},
|
|
burstBadge: {
|
|
position: 'absolute',
|
|
top: 24,
|
|
right: 24,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 6,
|
|
backgroundColor: 'rgba(5, 5, 16, 0.85)',
|
|
paddingVertical: 6,
|
|
paddingHorizontal: 14,
|
|
borderRadius: THEME.borderRadius.round,
|
|
borderWidth: 1,
|
|
borderColor: THEME.colors.accent,
|
|
},
|
|
burstBadgeText: {
|
|
color: THEME.colors.accent,
|
|
fontSize: 14,
|
|
fontWeight: 'bold',
|
|
},
|
|
burstWaitingBox: {
|
|
backgroundColor: 'rgba(5, 5, 16, 0.9)',
|
|
paddingVertical: THEME.spacing.xl,
|
|
paddingHorizontal: THEME.spacing.xxl,
|
|
borderRadius: THEME.borderRadius.lg,
|
|
borderWidth: 1,
|
|
borderColor: THEME.colors.accent,
|
|
alignItems: 'center',
|
|
shadowColor: THEME.colors.accent,
|
|
shadowOffset: { width: 0, height: 0 },
|
|
shadowOpacity: 0.4,
|
|
shadowRadius: 20,
|
|
elevation: 8,
|
|
},
|
|
burstWaitingText: {
|
|
fontSize: 28,
|
|
fontWeight: '900',
|
|
color: THEME.colors.accent,
|
|
textAlign: 'center',
|
|
},
|
|
burstWaitingSub: {
|
|
fontSize: 16,
|
|
color: THEME.colors.textMuted,
|
|
marginTop: THEME.spacing.sm,
|
|
},
|
|
countdownOverlay: {
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
countdownBox: {
|
|
backgroundColor: 'rgba(5, 5, 16, 0.88)',
|
|
paddingVertical: THEME.spacing.xl,
|
|
paddingHorizontal: THEME.spacing.xxl,
|
|
borderRadius: THEME.borderRadius.lg,
|
|
borderWidth: 1,
|
|
borderColor: THEME.colors.gradientBorder,
|
|
minWidth: 180,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
shadowColor: THEME.colors.accent,
|
|
shadowOffset: { width: 0, height: 0 },
|
|
shadowOpacity: 0.4,
|
|
shadowRadius: 20,
|
|
elevation: 8,
|
|
},
|
|
countdownText: {
|
|
fontSize: 64,
|
|
fontWeight: '900',
|
|
color: THEME.colors.text,
|
|
textAlign: 'center',
|
|
textShadowColor: THEME.colors.cyanGlow,
|
|
textShadowOffset: { width: 0, height: 0 },
|
|
textShadowRadius: 15,
|
|
},
|
|
backButton: {
|
|
position: 'absolute',
|
|
top: 24,
|
|
left: 24,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: 'rgba(13, 13, 26, 0.8)',
|
|
paddingVertical: THEME.spacing.sm,
|
|
paddingHorizontal: THEME.spacing.lg,
|
|
borderRadius: THEME.borderRadius.round,
|
|
borderWidth: 1,
|
|
borderColor: THEME.colors.gradientBorder,
|
|
},
|
|
backButtonText: {
|
|
color: THEME.colors.text,
|
|
fontSize: 16,
|
|
fontWeight: 'bold',
|
|
letterSpacing: 1,
|
|
},
|
|
infoText: {
|
|
color: THEME.colors.text,
|
|
fontSize: 18,
|
|
textAlign: 'center',
|
|
marginBottom: THEME.spacing.lg,
|
|
paddingHorizontal: THEME.spacing.xl,
|
|
},
|
|
btn: {
|
|
backgroundColor: THEME.colors.accent,
|
|
paddingVertical: THEME.spacing.md,
|
|
paddingHorizontal: THEME.spacing.xl,
|
|
borderRadius: THEME.borderRadius.round,
|
|
marginBottom: THEME.spacing.md,
|
|
shadowColor: THEME.colors.accent,
|
|
shadowOffset: { width: 0, height: 4 },
|
|
shadowOpacity: 0.4,
|
|
shadowRadius: 10,
|
|
elevation: 6,
|
|
},
|
|
cancelBtn: {
|
|
backgroundColor: 'transparent',
|
|
borderWidth: 1,
|
|
borderColor: THEME.colors.border,
|
|
},
|
|
btnText: {
|
|
color: THEME.colors.text,
|
|
fontSize: 16,
|
|
fontWeight: 'bold',
|
|
},
|
|
modalOverlay: {
|
|
flex: 1,
|
|
backgroundColor: THEME.colors.overlay,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
modalContent: {
|
|
width: 340,
|
|
padding: THEME.spacing.lg,
|
|
borderRadius: THEME.borderRadius.md,
|
|
backgroundColor: THEME.colors.surfaceSecondary,
|
|
borderWidth: 1,
|
|
borderColor: THEME.colors.border,
|
|
alignItems: 'center',
|
|
shadowColor: THEME.colors.primary,
|
|
shadowOffset: { width: 0, height: 0 },
|
|
shadowOpacity: 0.3,
|
|
shadowRadius: 20,
|
|
elevation: 10,
|
|
},
|
|
modalTitle: {
|
|
fontSize: 20,
|
|
fontWeight: 'bold',
|
|
color: THEME.colors.text,
|
|
marginBottom: THEME.spacing.xs,
|
|
},
|
|
modalSub: {
|
|
fontSize: 13,
|
|
color: THEME.colors.textMuted,
|
|
marginBottom: THEME.spacing.md,
|
|
textAlign: 'center',
|
|
},
|
|
input: {
|
|
width: '100%',
|
|
height: 50,
|
|
backgroundColor: THEME.colors.surface,
|
|
borderColor: THEME.colors.border,
|
|
borderWidth: 1,
|
|
borderRadius: THEME.borderRadius.sm,
|
|
color: THEME.colors.text,
|
|
paddingHorizontal: THEME.spacing.md,
|
|
fontSize: 16,
|
|
textAlign: 'center',
|
|
marginBottom: THEME.spacing.sm,
|
|
},
|
|
error: {
|
|
color: THEME.colors.error,
|
|
fontSize: 14,
|
|
marginBottom: THEME.spacing.sm,
|
|
},
|
|
modalButtons: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
width: '100%',
|
|
marginTop: THEME.spacing.sm,
|
|
},
|
|
button: {
|
|
flex: 1,
|
|
height: 45,
|
|
borderRadius: THEME.borderRadius.sm,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
marginHorizontal: THEME.spacing.xs,
|
|
},
|
|
cancelButton: {
|
|
backgroundColor: 'transparent',
|
|
borderWidth: 1,
|
|
borderColor: THEME.colors.border,
|
|
},
|
|
confirmButton: {
|
|
overflow: 'hidden',
|
|
borderRadius: THEME.borderRadius.sm,
|
|
},
|
|
confirmGradient: {
|
|
flex: 1,
|
|
width: '100%',
|
|
height: '100%',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
buttonText: {
|
|
color: THEME.colors.text,
|
|
fontSize: 16,
|
|
fontWeight: '600',
|
|
},
|
|
});
|