Update camera screens, configuration, and build setup

This commit is contained in:
2026-05-30 19:13:04 +02:00
parent d8e397eac1
commit 305d8fd449
14 changed files with 144 additions and 373 deletions
+11 -4
View File
@@ -9,6 +9,7 @@ import {
Switch,
Alert,
} from 'react-native';
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
import { THEME } from '../styles/theme';
import KioskMode from '../../modules/kiosk-mode';
import { AppSettings, saveSettings } from '../services/settings';
@@ -82,10 +83,16 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
return;
}
if (!printerIp.trim()) {
const ipTrimmed = printerIp.trim();
const ipRegex = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/;
if (!ipTrimmed) {
Alert.alert('Ungültige Eingabe', 'Die Drucker-IP-Adresse darf nicht leer sein.');
return;
}
if (!ipRegex.test(ipTrimmed)) {
Alert.alert('Ungültige Eingabe', 'Bitte gib eine gültige IP-Adresse ein (z.B. 192.168.1.100).');
return;
}
if (!password.trim() || password.length < 4) {
Alert.alert('Ungültige Eingabe', 'Das Admin-Passwort muss mindestens 4 Ziffern lang sein.');
@@ -94,7 +101,7 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
const updated: AppSettings = {
countdownDuration: duration,
printerIp: printerIp.trim(),
printerIp: ipTrimmed,
adminPassword: password.trim(),
kioskModeEnabled: kioskActive,
};
@@ -109,7 +116,7 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
};
return (
<View style={styles.container}>
<Animated.View entering={FadeIn.duration(400)} exiting={FadeOut.duration(300)} style={styles.container}>
<View style={styles.headerRow}>
<Text style={styles.title}>SCHNAPPIX EINSTELLUNGEN</Text>
<TouchableOpacity style={styles.closeBtn} onPress={onClose}>
@@ -207,7 +214,7 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
<Text style={styles.saveBtnText}>EINSTELLUNGEN SPEICHERN</Text>
</TouchableOpacity>
</ScrollView>
</View>
</Animated.View>
);
}
+24 -6
View File
@@ -10,7 +10,8 @@ import {
TouchableWithoutFeedback,
} from 'react-native';
import { CameraView, useCameraPermissions } from 'expo-camera';
import * as FileSystem from 'expo-file-system/legacy';
import { File, Paths } from 'expo-file-system';
import Animated, { useSharedValue, useAnimatedStyle, withSpring, withTiming, FadeIn, FadeOut } from 'react-native-reanimated';
import { THEME } from '../styles/theme';
import { UsbCameraView, UsbCameraRef } from '../../modules/usb-camera';
import { saveToGallery } from '../services/storage';
@@ -48,6 +49,22 @@ export default function CameraScreen({
const usbCameraRef = useRef<UsbCameraRef>(null);
const expoCameraRef = useRef<any>(null);
// 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 }],
}));
// 1. Request built-in camera permission on mount (just in case we fallback)
useEffect(() => {
if (!permission || !permission.granted) {
@@ -117,7 +134,8 @@ export default function CameraScreen({
// 4. Capture photo function
const capture = async () => {
const filename = `photo_${Date.now()}.jpg`;
const tempUri = `${FileSystem.cacheDirectory}${filename}`;
const tempFile = new File(Paths.cache, filename);
const tempUri = tempFile.uri;
try {
if (Platform.OS === 'android' && isUsbConnected && usbCameraRef.current) {
@@ -289,9 +307,9 @@ export default function CameraScreen({
{/* Countdown overlay */}
{hasStarted && (
<View style={styles.countdownOverlay} pointerEvents="none">
<View style={styles.countdownBox}>
<Animated.View style={[styles.countdownBox, animatedStyle]}>
<Text style={styles.countdownText}>{countdown}</Text>
</View>
</Animated.View>
</View>
)}
@@ -306,7 +324,7 @@ export default function CameraScreen({
};
return (
<View style={styles.container}>
<Animated.View entering={FadeIn.duration(400)} exiting={FadeOut.duration(300)} style={styles.container}>
{/* Camera Preview Background */}
<View style={styles.previewContainer}>
{Platform.OS === 'android' && isUsbConnected ? (
@@ -322,7 +340,7 @@ export default function CameraScreen({
{/* Render UI controls/overlays on top */}
{renderContent()}
</View>
</Animated.View>
);
}
-261
View File
@@ -1,261 +0,0 @@
import React, { useState } from 'react';
import {
StyleSheet,
Text,
View,
TouchableOpacity,
Modal,
TextInput,
TouchableWithoutFeedback,
} from 'react-native';
import { THEME } from '../styles/theme';
interface HomeScreenProps {
onStart: () => void;
onNavigateToAdmin: () => void;
adminPassword?: string;
}
export default function HomeScreen({ onStart, onNavigateToAdmin, adminPassword = '1234' }: HomeScreenProps) {
const [passwordModalVisible, setPasswordModalVisible] = useState(false);
const [enteredPassword, setEnteredPassword] = useState('');
const [errorText, setErrorText] = useState('');
const handleSettingsTap = () => {
setEnteredPassword('');
setErrorText('');
setPasswordModalVisible(true);
};
const handlePasswordSubmit = () => {
if (enteredPassword === adminPassword) {
setPasswordModalVisible(false);
onNavigateToAdmin();
} else {
setErrorText('Falsches Passwort');
setEnteredPassword('');
}
};
return (
<TouchableWithoutFeedback onPress={onStart}>
<View style={styles.container}>
{/* Settings button in the top-right corner */}
<TouchableOpacity
style={styles.settingsButton}
onPress={handleSettingsTap}
activeOpacity={0.7}
>
<Text style={styles.settingsIcon}></Text>
</TouchableOpacity>
<View style={styles.content}>
<Text style={styles.logo}>SCHNAPPIX</Text>
<Text style={styles.title}>Willkommen zu unserer Feier!</Text>
<View style={styles.divider} />
<View style={styles.startButton}>
<Text style={styles.startButtonText}>ZUM STARTEN ÜBERALL TIPPEN</Text>
</View>
<Text style={styles.footer}>Fotos machen Collage erstellen Sofort drucken</Text>
</View>
{/* 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 eingeben"
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}
>
<Text style={styles.buttonText}>Öffnen</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
</View>
</TouchableWithoutFeedback>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: THEME.colors.background,
justifyContent: 'center',
alignItems: 'center',
},
settingsButton: {
position: 'absolute',
top: 24,
right: 24,
width: 50,
height: 50,
borderRadius: THEME.borderRadius.round,
backgroundColor: THEME.colors.surfaceSecondary,
borderWidth: 1,
borderColor: THEME.colors.border,
justifyContent: 'center',
alignItems: 'center',
zIndex: 999,
},
settingsIcon: {
fontSize: 24,
color: THEME.colors.text,
},
content: {
alignItems: 'center',
padding: THEME.spacing.xl,
borderRadius: THEME.borderRadius.lg,
backgroundColor: THEME.colors.surface,
borderWidth: 1,
borderColor: THEME.colors.border,
width: '60%',
shadowColor: '#000',
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.3,
shadowRadius: 20,
elevation: 10,
},
logo: {
fontSize: 54,
fontWeight: '900',
color: THEME.colors.primary,
letterSpacing: 8,
marginBottom: THEME.spacing.sm,
},
title: {
fontSize: 22,
color: THEME.colors.text,
textAlign: 'center',
marginBottom: THEME.spacing.lg,
letterSpacing: 1,
},
divider: {
width: 60,
height: 3,
backgroundColor: THEME.colors.accent,
marginBottom: THEME.spacing.xl,
borderRadius: THEME.borderRadius.sm,
},
startButton: {
paddingVertical: THEME.spacing.md,
paddingHorizontal: THEME.spacing.xl,
backgroundColor: THEME.colors.primary,
borderRadius: THEME.borderRadius.round,
marginBottom: THEME.spacing.xl,
},
startButtonText: {
color: THEME.colors.text,
fontSize: 18,
fontWeight: 'bold',
letterSpacing: 2,
},
footer: {
fontSize: 14,
color: THEME.colors.textMuted,
letterSpacing: 1,
},
modalOverlay: {
flex: 1,
backgroundColor: THEME.colors.overlay,
justifyContent: 'center',
alignItems: 'center',
},
modalContent: {
width: 320,
padding: THEME.spacing.lg,
borderRadius: THEME.borderRadius.md,
backgroundColor: THEME.colors.surfaceSecondary,
borderWidth: 1,
borderColor: THEME.colors.border,
alignItems: 'center',
},
modalTitle: {
fontSize: 20,
fontWeight: 'bold',
color: THEME.colors.text,
marginBottom: THEME.spacing.xs,
},
modalSub: {
fontSize: 14,
color: THEME.colors.textMuted,
marginBottom: THEME.spacing.md,
},
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: {
backgroundColor: THEME.colors.primary,
},
buttonText: {
color: THEME.colors.text,
fontSize: 16,
fontWeight: '600',
},
});
+3 -2
View File
@@ -9,6 +9,7 @@ import {
ActivityIndicator,
} from 'react-native';
import ViewShot from 'react-native-view-shot';
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
import { THEME } from '../styles/theme';
import { printImageLocal } from '../services/printer';
import { saveToGallery } from '../services/storage';
@@ -180,7 +181,7 @@ export default function PreviewScreen({
};
return (
<View style={styles.container}>
<Animated.View entering={FadeIn.duration(400)} exiting={FadeOut.duration(300)} style={styles.container}>
{/* Left panel: Preview Canvas */}
<View style={styles.previewPanel}>
{layout === 'single' ? (
@@ -288,7 +289,7 @@ export default function PreviewScreen({
</View>
</View>
)}
</View>
</Animated.View>
);
}