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
+504
View File
@@ -0,0 +1,504 @@
import React, { useState, useRef } from 'react';
import {
StyleSheet,
Text,
View,
Image,
TouchableOpacity,
ScrollView,
ActivityIndicator,
} from 'react-native';
import ViewShot from 'react-native-view-shot';
import { THEME } from '../styles/theme';
import { printImageLocal } from '../services/printer';
import { saveToGallery } from '../services/storage';
interface PreviewScreenProps {
photoUris: string[];
printerIp: string;
onRetakeLast: () => void;
onAddAnother: () => void;
onReset: () => void;
}
type CollageLayout = 'single' | 'strip' | 'grid' | 'duo';
export default function PreviewScreen({
photoUris,
printerIp,
onRetakeLast,
onAddAnother,
onReset,
}: PreviewScreenProps) {
const [layout, setLayout] = useState<CollageLayout>('single');
const [isProcessing, setIsProcessing] = useState<boolean>(false);
const [statusMessage, setStatusMessage] = useState<string>('');
const viewShotRef = useRef<any>(null);
const currentPhoto = photoUris[photoUris.length - 1];
// Triggers the view capture and the print job
const handlePrint = async () => {
setIsProcessing(true);
setStatusMessage('Preparing your photo...');
try {
let printUri = currentPhoto;
// If we are printing a collage layout, capture the ViewShot container
if (layout !== 'single') {
setStatusMessage('Generating collage...');
const capturedUri = await viewShotRef.current.capture();
printUri = capturedUri;
}
setStatusMessage('Sending print job to printer...');
// 1. Silent Print via IPP
await printImageLocal(printUri, {
ipAddress: printerIp,
jobName: 'Schnappix Photo Booth',
});
setStatusMessage('Saving to tablet library...');
// 2. Save permanently to DCIM/Schnappix
await saveToGallery(printUri);
setStatusMessage('Print successful! Enjoy!');
setTimeout(() => {
setIsProcessing(false);
onReset(); // Go back to Home Screen
}, 2000);
} catch (error: any) {
console.error('Printing failed:', error);
alert('Error: ' + error.message);
setIsProcessing(false);
}
};
// Saves to gallery without printing
const handleSaveOnly = async () => {
setIsProcessing(true);
setStatusMessage('Generating image...');
try {
let saveUri = currentPhoto;
if (layout !== 'single') {
const capturedUri = await viewShotRef.current.capture();
saveUri = capturedUri;
}
setStatusMessage('Saving to tablet library...');
await saveToGallery(saveUri);
setStatusMessage('Saved successfully!');
setTimeout(() => {
setIsProcessing(false);
onReset();
}, 1500);
} catch (error: any) {
console.error('Saving failed:', error);
alert('Error: ' + error.message);
setIsProcessing(false);
}
};
// Render the selected collage inside the capture container
const renderCollageView = () => {
switch (layout) {
case 'strip':
return (
<View style={[styles.collageCanvas, styles.stripCanvas]}>
<View style={styles.stripContent}>
{photoUris.slice(-3).map((uri, idx) => (
<Image key={idx} source={{ uri }} style={styles.stripImage} />
))}
</View>
<View style={styles.collageFooter}>
<Text style={styles.collageFooterText}>SCHNAPPIX PHOTO BOOTH</Text>
<Text style={styles.collageFooterDate}>{new Date().toLocaleDateString()}</Text>
</View>
</View>
);
case 'grid':
return (
<View style={[styles.collageCanvas, styles.gridCanvas]}>
<View style={styles.gridContainer}>
{photoUris.slice(-4).map((uri, idx) => (
<Image key={idx} source={{ uri }} style={styles.gridImage} />
))}
</View>
<View style={styles.collageFooter}>
<Text style={styles.collageFooterText}>SCHNAPPIX PHOTO BOOTH</Text>
</View>
</View>
);
case 'duo':
return (
<View style={[styles.collageCanvas, styles.duoCanvas]}>
<View style={styles.duoContainer}>
{photoUris.slice(-2).map((uri, idx) => (
<Image key={idx} source={{ uri }} style={styles.duoImage} />
))}
</View>
<View style={styles.collageFooter}>
<Text style={styles.collageFooterText}>SCHNAPPIX PHOTO BOOTH</Text>
</View>
</View>
);
default:
return (
<View style={styles.singleCanvas}>
<Image source={{ uri: currentPhoto }} style={styles.singleImage} />
</View>
);
}
};
return (
<View style={styles.container}>
{/* Left panel: Preview Canvas */}
<View style={styles.previewPanel}>
{layout === 'single' ? (
renderCollageView()
) : (
// ViewShot wraps the collage to capture it at exactly 3:2 ratio (1200x1800 px) for the printer
<ViewShot
ref={viewShotRef}
options={{ format: 'jpg', quality: 0.95 }}
style={styles.viewShotContainer}
>
{renderCollageView()}
</ViewShot>
)}
</View>
{/* Right panel: Controls */}
<View style={styles.controlPanel}>
<Text style={styles.header}>CHOOSE YOUR STYLE</Text>
{/* Layout Selection */}
<View style={styles.layoutSelector}>
<TouchableOpacity
style={[styles.layoutBtn, layout === 'single' && styles.layoutBtnActive]}
onPress={() => setLayout('single')}
>
<Text style={styles.layoutBtnText}>Single</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.layoutBtn,
photoUris.length < 2 && styles.layoutBtnDisabled,
layout === 'duo' && styles.layoutBtnActive,
]}
disabled={photoUris.length < 2}
onPress={() => setLayout('duo')}
>
<Text style={styles.layoutBtnText}>Duo (2)</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.layoutBtn,
photoUris.length < 3 && styles.layoutBtnDisabled,
layout === 'strip' && styles.layoutBtnActive,
]}
disabled={photoUris.length < 3}
onPress={() => setLayout('strip')}
>
<Text style={styles.layoutBtnText}>Strip (3)</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.layoutBtn,
photoUris.length < 4 && styles.layoutBtnDisabled,
layout === 'grid' && styles.layoutBtnActive,
]}
disabled={photoUris.length < 4}
onPress={() => setLayout('grid')}
>
<Text style={styles.layoutBtnText}>Grid (4)</Text>
</TouchableOpacity>
</View>
<Text style={styles.photosCountText}>
Photos Captured: {photoUris.length} / 4
</Text>
{/* Action Buttons */}
<View style={styles.actions}>
<TouchableOpacity style={[styles.actionBtn, styles.printBtn]} onPress={handlePrint}>
<Text style={styles.actionBtnText}>PRINT NOW 🖨</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.actionBtn, styles.saveBtn]} onPress={handleSaveOnly}>
<Text style={styles.actionBtnText}>Save to Tablet Only 💾</Text>
</TouchableOpacity>
{photoUris.length < 4 && (
<TouchableOpacity style={[styles.actionBtn, styles.addBtn]} onPress={onAddAnother}>
<Text style={styles.actionBtnText}>+ Add Another Photo</Text>
</TouchableOpacity>
)}
<View style={styles.rowActions}>
<TouchableOpacity style={[styles.smallBtn, styles.retakeBtn]} onPress={onRetakeLast}>
<Text style={styles.smallBtnText}>Retake Last</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.smallBtn, styles.resetBtn]} onPress={onReset}>
<Text style={styles.smallBtnText}>Exit</Text>
</TouchableOpacity>
</View>
</View>
</View>
{/* Loading Overlay */}
{isProcessing && (
<View style={styles.loadingOverlay}>
<View style={styles.loadingBox}>
<ActivityIndicator size="large" color={THEME.colors.primary} />
<Text style={styles.loadingText}>{statusMessage}</Text>
</View>
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'row',
backgroundColor: THEME.colors.background,
},
previewPanel: {
flex: 1.2,
justifyContent: 'center',
alignItems: 'center',
padding: THEME.spacing.md,
backgroundColor: '#050507',
},
viewShotContainer: {
// 3:2 ratio aspect layout matching Canon Selphy postcard paper
width: 320,
height: 480,
justifyContent: 'center',
alignItems: 'center',
},
controlPanel: {
flex: 0.8,
backgroundColor: THEME.colors.surface,
padding: THEME.spacing.xl,
justifyContent: 'center',
borderLeftWidth: 1,
borderColor: THEME.colors.border,
},
header: {
fontSize: 22,
fontWeight: 'bold',
color: THEME.colors.text,
textAlign: 'center',
marginBottom: THEME.spacing.md,
letterSpacing: 2,
},
layoutSelector: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: THEME.spacing.md,
},
layoutBtn: {
flex: 1,
paddingVertical: THEME.spacing.sm,
backgroundColor: THEME.colors.surfaceSecondary,
borderWidth: 1,
borderColor: THEME.colors.border,
borderRadius: THEME.borderRadius.sm,
alignItems: 'center',
marginHorizontal: 3,
},
layoutBtnActive: {
backgroundColor: THEME.colors.primary,
borderColor: THEME.colors.primary,
},
layoutBtnDisabled: {
opacity: 0.3,
},
layoutBtnText: {
color: THEME.colors.text,
fontSize: 12,
fontWeight: 'bold',
},
photosCountText: {
fontSize: 14,
color: THEME.colors.textMuted,
textAlign: 'center',
marginBottom: THEME.spacing.lg,
},
actions: {
width: '100%',
},
actionBtn: {
width: '100%',
paddingVertical: THEME.spacing.md,
borderRadius: THEME.borderRadius.md,
alignItems: 'center',
justifyContent: 'center',
marginBottom: THEME.spacing.md,
},
printBtn: {
backgroundColor: THEME.colors.primary,
},
saveBtn: {
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: THEME.colors.border,
},
addBtn: {
backgroundColor: THEME.colors.accent,
},
actionBtnText: {
color: THEME.colors.text,
fontSize: 16,
fontWeight: 'bold',
letterSpacing: 1,
},
rowActions: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: THEME.spacing.sm,
},
smallBtn: {
flex: 0.48,
paddingVertical: THEME.spacing.sm,
borderRadius: THEME.borderRadius.sm,
alignItems: 'center',
borderWidth: 1,
},
retakeBtn: {
borderColor: THEME.colors.error,
backgroundColor: 'transparent',
},
resetBtn: {
borderColor: THEME.colors.border,
backgroundColor: 'transparent',
},
smallBtnText: {
color: THEME.colors.text,
fontSize: 14,
fontWeight: '600',
},
// Collage Canvas Layouts (3:2 ratio aspect - width 320, height 480)
singleCanvas: {
width: 320,
height: 480,
backgroundColor: '#fff',
padding: 10,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 10,
elevation: 5,
},
singleImage: {
width: '100%',
height: '100%',
resizeMode: 'cover',
},
collageCanvas: {
width: 320,
height: 480,
backgroundColor: '#ffffff',
padding: 12,
alignItems: 'center',
justifyContent: 'space-between',
},
collageFooter: {
height: 35,
justifyContent: 'center',
alignItems: 'center',
width: '100%',
borderTopWidth: 0.5,
borderTopColor: '#e0e0e0',
marginTop: 6,
},
collageFooterText: {
fontSize: 10,
fontWeight: 'bold',
color: '#333333',
letterSpacing: 2,
},
collageFooterDate: {
fontSize: 8,
color: '#666666',
},
// Strip (3 images vertical)
stripCanvas: {},
stripContent: {
flex: 1,
width: '100%',
justifyContent: 'space-between',
},
stripImage: {
width: '100%',
height: '31%',
resizeMode: 'cover',
borderRadius: 2,
},
// Grid (4 images 2x2)
gridCanvas: {},
gridContainer: {
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
alignContent: 'space-between',
width: '100%',
},
gridImage: {
width: '49%',
height: '49%',
resizeMode: 'cover',
borderRadius: 2,
},
// Duo (2 images side by side or stacked)
duoCanvas: {},
duoContainer: {
flex: 1,
width: '100%',
justifyContent: 'space-between',
},
duoImage: {
width: '100%',
height: '49%',
resizeMode: 'cover',
borderRadius: 2,
},
// Loading overlay
loadingOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: THEME.colors.overlay,
justifyContent: 'center',
alignItems: 'center',
zIndex: 999,
},
loadingBox: {
backgroundColor: THEME.colors.surfaceSecondary,
padding: THEME.spacing.xl,
borderRadius: THEME.borderRadius.md,
alignItems: 'center',
borderWidth: 1,
borderColor: THEME.colors.border,
},
loadingText: {
color: THEME.colors.text,
fontSize: 16,
fontWeight: 'bold',
marginTop: THEME.spacing.md,
},
});