Initialize Schnappix Photo Booth application with custom UVC camera, silent Wi-Fi IPP printing, local gallery storage, and kiosk screen pinning support

This commit is contained in:
2026-05-25 16:44:15 +02:00
parent 5958319772
commit 872e1ffc86
51 changed files with 8931 additions and 1 deletions
+280
View File
@@ -0,0 +1,280 @@
import React, { useState, useEffect, useRef } from 'react';
import { StyleSheet, Text, View, TouchableOpacity, Platform } from 'react-native';
import { CameraView, useCameraPermissions } from 'expo-camera';
import * as FileSystem from 'expo-file-system';
import { THEME } from '../styles/theme';
import { UsbCameraView, UsbCameraRef } from '../../modules/usb-camera';
interface CameraScreenProps {
countdownDuration: number;
onPhotoCaptured: (uri: string) => void;
onCancel: () => void;
}
export default function CameraScreen({ countdownDuration, onPhotoCaptured, onCancel }: 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 usbCameraRef = useRef<UsbCameraRef>(null);
const expoCameraRef = useRef<any>(null);
// 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.
// We can query the USB module or attempt to check every second in the background.
useEffect(() => {
let checkInterval: NodeJS.Timeout;
if (Platform.OS === 'android') {
const checkConnection = async () => {
try {
if (usbCameraRef.current) {
const connected = await usbCameraRef.current.isCameraConnected();
setIsUsbConnected(connected);
}
} catch (e) {
// If native view manager is not loaded or errors, fallback to false
setIsUsbConnected(false);
}
};
// Check initially and run an interval
checkConnection();
checkInterval = setInterval(checkConnection, 2000);
}
return () => {
if (checkInterval) clearInterval(checkInterval);
};
}, []);
// 3. Start the countdown on screen load
useEffect(() => {
let timer: NodeJS.Timeout;
let count = countdownDuration;
setCountdown(count);
setHasStarted(true);
const runTimer = () => {
if (count > 1) {
count -= 1;
setCountdown(count);
timer = setTimeout(runTimer, 1000);
} else if (count === 1) {
setCountdown('Cheese! 📸');
setIsCapturing(true);
timer = setTimeout(() => {
capture();
}, 800); // give the user a split second to smile
}
};
timer = setTimeout(runTimer, 1000);
return () => {
if (timer) clearTimeout(timer);
};
}, [countdownDuration]);
// 4. Capture photo function
const capture = async () => {
const filename = `photo_${Date.now()}.jpg`;
const tempUri = `${FileSystem.cacheDirectory}${filename}`;
try {
if (Platform.OS === 'android' && isUsbConnected && usbCameraRef.current) {
// USB Camera Capture
console.log('Capturing from USB Camera...');
const path = await usbCameraRef.current.takePicture(tempUri);
onPhotoCaptured(path);
} else {
// Fallback Camera Capture
console.log('Capturing from built-in camera fallback...');
if (expoCameraRef.current) {
const photo = await expoCameraRef.current.takePictureAsync({
quality: 0.95,
skipProcessing: false,
});
onPhotoCaptured(photo.uri);
} else {
throw new Error('Camera ref is not available.');
}
}
} catch (error: any) {
console.error('Capture failed:', error);
alert('Failed to capture photo: ' + error.message);
onCancel();
}
};
if (!permission) {
return (
<View style={styles.container}>
<Text style={styles.infoText}>Loading permissions...</Text>
</View>
);
}
if (!permission.granted && !isUsbConnected) {
return (
<View style={styles.container}>
<Text style={styles.infoText}>We need your permission to show the camera</Text>
<TouchableOpacity style={styles.btn} onPress={requestPermission}>
<Text style={styles.btnText}>Grant Permission</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.btn, styles.cancelBtn]} onPress={onCancel}>
<Text style={styles.btnText}>Go Back</Text>
</TouchableOpacity>
</View>
);
}
return (
<View style={styles.container}>
{/* Camera Preview Area */}
<View style={styles.previewContainer}>
{Platform.OS === 'android' && isUsbConnected ? (
<UsbCameraView ref={usbCameraRef} style={styles.cameraPreview} />
) : (
<CameraView
ref={expoCameraRef}
style={styles.cameraPreview}
facing="front"
/>
)}
</View>
{/* Camera Info HUD */}
<View style={styles.hudContainer}>
<Text style={styles.cameraSourceIndicator}>
Source: {isUsbConnected ? 'External USB Camera' : 'Front Tablet Camera (Fallback)'}
</Text>
</View>
{/* Countdown overlay */}
{hasStarted && (
<View style={styles.overlay}>
<View style={styles.countdownBox}>
<Text style={styles.countdownText}>{countdown}</Text>
</View>
</View>
)}
{/* Cancel Button */}
{!isCapturing && (
<TouchableOpacity style={styles.backButton} onPress={onCancel}>
<Text style={styles.backButtonText}>CANCEL</Text>
</TouchableOpacity>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#000',
justifyContent: 'center',
alignItems: 'center',
},
previewContainer: {
width: '100%',
height: '100%',
position: 'absolute',
},
cameraPreview: {
width: '100%',
height: '100%',
},
hudContainer: {
position: 'absolute',
bottom: 20,
backgroundColor: 'rgba(0, 0, 0, 0.6)',
paddingVertical: THEME.spacing.xs,
paddingHorizontal: THEME.spacing.md,
borderRadius: THEME.borderRadius.sm,
},
cameraSourceIndicator: {
color: THEME.colors.text,
fontSize: 14,
fontWeight: 'bold',
},
overlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.2)',
},
countdownBox: {
backgroundColor: THEME.colors.glassBackground,
paddingVertical: THEME.spacing.xl,
paddingHorizontal: THEME.spacing.xxl,
borderRadius: THEME.borderRadius.lg,
borderWidth: 1,
borderColor: THEME.colors.border,
minWidth: 180,
alignItems: 'center',
justifyContent: 'center',
},
countdownText: {
fontSize: 72,
fontWeight: '900',
color: THEME.colors.text,
textShadowColor: 'rgba(0, 0, 0, 0.75)',
textShadowOffset: { width: -1, height: 1 },
textShadowRadius: 10,
},
backButton: {
position: 'absolute',
top: 24,
left: 24,
backgroundColor: 'rgba(0, 0, 0, 0.6)',
paddingVertical: THEME.spacing.sm,
paddingHorizontal: THEME.spacing.lg,
borderRadius: THEME.borderRadius.round,
borderWidth: 1,
borderColor: THEME.colors.border,
},
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.primary,
paddingVertical: THEME.spacing.md,
paddingHorizontal: THEME.spacing.xl,
borderRadius: THEME.borderRadius.round,
marginBottom: THEME.spacing.md,
},
cancelBtn: {
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: THEME.colors.border,
},
btnText: {
color: THEME.colors.text,
fontSize: 16,
fontWeight: 'bold',
},
});