import React, { useState, useRef, useCallback } from 'react'; import { StyleSheet, Text, View, Image, TouchableOpacity, ActivityIndicator, } from 'react-native'; import ViewShot from 'react-native-view-shot'; import { LinearGradient } from 'expo-linear-gradient'; import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; import { faPrint, faDownload, faPlus, faRotateLeft, faRightFromBracket, faFaceGrinStars, faBorderAll, } from '@fortawesome/free-solid-svg-icons'; import { THEME } from '../styles/theme'; import { printImageLocal } from '../services/printer'; import { saveToGallery } from '../services/storage'; import { logger } from '../services/logger'; import DraggableSticker, { StickerData } from '../components/DraggableSticker'; import StickerTray from '../components/StickerTray'; import FrameOverlay from '../components/FrameOverlay'; import FramePicker from '../components/FramePicker'; import { FRAMES } from '../data/frames'; interface PreviewScreenProps { photoUris: string[]; printerIp: string; onRetakeLast: () => void; onAddAnother: () => void; onReset: () => void; // Frame settings frameMode: 'off' | 'always' | 'available'; initialFrameId: string | null; availableFrameIds: string[]; // Date overlay settings dateOverlay: 'off' | 'date' | 'datetime'; dateOverlayPosition: string; // Event text eventText: string; } type CollageLayout = 'single' | 'strip' | 'grid' | 'duo'; export default function PreviewScreen({ photoUris, printerIp, onRetakeLast, onAddAnother, onReset, frameMode, initialFrameId, availableFrameIds, dateOverlay, dateOverlayPosition, eventText, }: PreviewScreenProps) { const [layout, setLayout] = useState('single'); const [isProcessing, setIsProcessing] = useState(false); const [statusMessage, setStatusMessage] = useState(''); // Sticker state const [stickers, setStickers] = useState([]); const [selectedStickerId, setSelectedStickerId] = useState(null); const [showStickerTray, setShowStickerTray] = useState(false); React.useEffect(() => { logger.log(`PreviewScreen mounted with ${photoUris.length} photos.`); if (photoUris.length > 0) { logger.log(`PreviewScreen currentPhoto: ${photoUris[photoUris.length - 1]}`); } else { logger.log(`[WARNING] PreviewScreen mounted but photoUris is empty!`); } return () => { logger.log('PreviewScreen unmounting.'); }; }, [photoUris]); // Idle Timer state const idleTimerRef = useRef(null); // Frame state const [selectedFrameId, setSelectedFrameId] = useState( frameMode === 'always' ? initialFrameId : null ); const viewShotRef = useRef(null); const currentPhoto = photoUris[photoUris.length - 1]; // Get the active frame asset const activeFrame = selectedFrameId && photoUris.length === 1 ? FRAMES.find((f) => f.id === selectedFrameId) : null; // Format date/time for overlays and stickers const formatDate = useCallback((mode: 'date' | 'datetime'): string => { const now = new Date(); const date = now.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', }); if (mode === 'datetime') { const time = now.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', }); return `${date} ${time}`; } return date; }, []); // Get date overlay position styles const getDatePositionStyle = () => { switch (dateOverlayPosition) { case 'bottom-left': return { bottom: 12, left: 12 }; case 'top-right': return { top: 12, right: 12 }; case 'top-left': return { top: 12, left: 12 }; case 'bottom-center': return { bottom: 12, left: 0, right: 0, alignItems: 'center' as const }; case 'bottom-right': default: return { bottom: 12, right: 12 }; } }; // ── Sticker handlers ── const addSticker = useCallback((type: 'emoji' | 'text', content: string) => { const newSticker: StickerData = { id: `sticker_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`, type, content, x: 100 + Math.random() * 40 - 20, y: 100 + Math.random() * 40 - 20, scale: 1, rotation: 0, }; setStickers((prev) => [...prev, newSticker]); setSelectedStickerId(newSticker.id); }, []); const handleAddEmoji = useCallback((emoji: string) => { addSticker('emoji', emoji); }, [addSticker]); const handleAddDateSticker = useCallback((format: 'date' | 'datetime') => { addSticker('text', formatDate(format)); }, [addSticker, formatDate]); const handleAddEventSticker = useCallback(() => { const date = formatDate('date'); addSticker('text', `${eventText} • ${date}`); }, [addSticker, formatDate, eventText]); const handleStickerSelect = useCallback((id: string) => { setSelectedStickerId((prev) => (prev === id ? null : id)); }, []); const handleStickerDelete = useCallback((id: string) => { setStickers((prev) => prev.filter((s) => s.id !== id)); setSelectedStickerId(null); }, []); const handleStickerUpdate = useCallback((id: string, updates: Partial) => { setStickers((prev) => prev.map((s) => (s.id === id ? { ...s, ...updates } : s)) ); }, []); // Deselect stickers when tapping the photo area const handleCanvasPress = useCallback(() => { setSelectedStickerId(null); }, []); // ── Capture the final image ── const captureComposite = async () => { try { if (viewShotRef.current) { return await viewShotRef.current.capture(); } return currentPhoto; } catch (e) { console.error('Failed to capture composite:', e); return currentPhoto; } }; // Triggers the view capture and the print job const handlePrint = async () => { setIsProcessing(true); setStatusMessage('Dein Foto wird vorbereitet...'); try { setStatusMessage('Bild wird generiert...'); const printUri = await captureComposite(); setStatusMessage('Druckauftrag wird an Drucker gesendet...'); await printImageLocal(printUri, { ipAddress: printerIp, jobName: 'Schnappix Fotobox', }); setStatusMessage('Wird in Galerie gespeichert...'); await saveToGallery(printUri); setStatusMessage('Druck erfolgreich! Viel Spaß! 🎉'); setTimeout(() => { setIsProcessing(false); onReset(); }, 2000); } catch (error: any) { console.error('Printing failed:', error); alert('Fehler: ' + error.message); setIsProcessing(false); } }; // Saves to gallery without printing const handleSaveOnly = async () => { setIsProcessing(true); setStatusMessage('Bild wird generiert...'); try { const saveUri = await captureComposite(); setStatusMessage('Wird in Galerie gespeichert...'); await saveToGallery(saveUri); setStatusMessage('Erfolgreich gespeichert!'); setTimeout(() => { setIsProcessing(false); onReset(); }, 1500); } catch (error: any) { console.error('Saving failed:', error); alert('Fehler: ' + error.message); setIsProcessing(false); } }; // Auto-save and exit const handleExit = useCallback(async () => { if (stickers.length === 0 && layout === 'single' && !activeFrame && dateOverlay === 'off') { onReset(); return; } setIsProcessing(true); setStatusMessage('Wird gespeichert...'); try { const capturedUri = await captureComposite(); await saveToGallery(capturedUri); setStatusMessage('Erfolgreich gespeichert!'); setTimeout(() => { setIsProcessing(false); onReset(); }, 1000); } catch (error) { console.error('Auto-saving on exit failed:', error); setIsProcessing(false); onReset(); } }, [stickers, layout, activeFrame, dateOverlay, onReset]); // Idle timer logic const resetIdleTimer = useCallback(() => { if (idleTimerRef.current) clearTimeout(idleTimerRef.current); idleTimerRef.current = setTimeout(() => { handleExit(); }, 60000); }, [handleExit]); useEffect(() => { resetIdleTimer(); return () => { if (idleTimerRef.current) clearTimeout(idleTimerRef.current); }; }, [resetIdleTimer]); const handleRetakeClick = async () => { if (stickers.length > 0 || layout !== 'single' || activeFrame || dateOverlay !== 'off') { setIsProcessing(true); setStatusMessage('Wird vor dem Wiederholen gespeichert...'); try { const capturedUri = await captureComposite(); await saveToGallery(capturedUri); } catch (error) { console.error('Auto-saving before retake failed:', error); } setIsProcessing(false); } onRetakeLast(); }; // ── Render the collage ── const renderCollageView = () => { switch (layout) { case 'strip': return ( {photoUris.slice(-3).map((uri, idx) => ( ))} SCHNAPPIX FOTOBOX {new Date().toLocaleDateString('de-DE')} ); case 'grid': return ( {photoUris.slice(-4).map((uri, idx) => ( ))} SCHNAPPIX FOTOBOX ); case 'duo': return ( {photoUris.slice(-2).map((uri, idx) => ( ))} SCHNAPPIX FOTOBOX ); default: return ( ); } }; return ( {/* Left panel: Preview Canvas */} {renderCollageView()} {/* Frame Overlay */} {activeFrame && } {/* Date/Time Overlay */} {dateOverlay !== 'off' && ( {formatDate(dateOverlay)} )} {/* Stickers */} {stickers.map((sticker) => ( ))} {/* Sticker Tray (outside ViewShot — not captured) */} {showStickerTray && ( setShowStickerTray(false)} eventText={eventText} /> )} {/* Right panel: Controls */} WÄHLE DEINEN STIL {/* Layout Selection */} setLayout('single')} > Einzelbild setLayout('duo')} > Duo (2) setLayout('strip')} > Streifen (3) setLayout('grid')} > Raster (4) Aufgenommene Fotos: {photoUris.length} / 4 {/* Editing Tools Row */} setShowStickerTray(!showStickerTray)} > Sticker {frameMode === 'available' && photoUris.length === 1 && ( { if (selectedFrameId !== null) { setSelectedFrameId(null); } // FramePicker is shown below }} > Rahmen )} {/* Frame Picker (when mode = available and only 1 photo) */} {frameMode === 'available' && photoUris.length === 1 && ( )} {/* Action Buttons */} JETZT DRUCKEN Nur auf Tablet speichern {photoUris.length < 4 && ( Weiteres Foto aufnehmen )} Wiederholen Beenden {/* Loading Overlay */} {isProcessing && ( {statusMessage} )} ); } 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: '#030308', }, viewShotContainer: { width: 480, height: 320, 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, textShadowColor: THEME.colors.cyanGlow, textShadowOffset: { width: 0, height: 0 }, textShadowRadius: 12, }, 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.accent, borderColor: THEME.colors.accent, shadowColor: THEME.colors.accent, shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.5, shadowRadius: 8, elevation: 4, }, 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.sm, }, editToolsRow: { flexDirection: 'row', justifyContent: 'center', gap: 10, marginBottom: THEME.spacing.md, }, editToolBtn: { flexDirection: 'row', alignItems: 'center', gap: 6, paddingVertical: 8, paddingHorizontal: 14, borderRadius: THEME.borderRadius.round, borderWidth: 1, borderColor: THEME.colors.border, backgroundColor: THEME.colors.surfaceSecondary, }, editToolBtnActive: { backgroundColor: THEME.colors.accent, borderColor: THEME.colors.accent, }, editToolText: { fontSize: 13, fontWeight: '600', color: THEME.colors.accent, }, editToolTextActive: { color: THEME.colors.text, }, actions: { width: '100%', }, actionBtn: { width: '100%', flexDirection: 'row', paddingVertical: THEME.spacing.md, borderRadius: THEME.borderRadius.md, alignItems: 'center', justifyContent: 'center', marginBottom: THEME.spacing.md, }, printBtn: { overflow: 'hidden', shadowColor: THEME.colors.gradientGlow, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.5, shadowRadius: 14, elevation: 8, }, printGradient: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: THEME.spacing.md, width: '100%', }, saveBtn: { backgroundColor: 'transparent', borderWidth: 1, borderColor: THEME.colors.border, }, addBtn: { backgroundColor: THEME.colors.accentDark, }, 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, flexDirection: 'row', paddingVertical: THEME.spacing.sm, borderRadius: THEME.borderRadius.sm, alignItems: 'center', justifyContent: 'center', borderWidth: 1, }, retakeBtn: { borderColor: THEME.colors.error, backgroundColor: 'transparent', }, resetBtn: { borderColor: THEME.colors.border, backgroundColor: 'transparent', }, smallBtnText: { color: THEME.colors.textMuted, fontSize: 14, fontWeight: '600', }, // ── Photo Canvases ── singleCanvas: { width: 480, height: 320, backgroundColor: '#fff', padding: 0, shadowColor: THEME.colors.gradientGlow, shadowOffset: { width: 0, height: 0 }, shadowOpacity: 0.35, shadowRadius: 18, elevation: 5, }, singleImage: { width: '100%', height: '100%', resizeMode: 'cover', }, collageCanvas: { width: 480, height: 320, 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', }, stripCanvas: {}, stripContent: { flex: 1, width: '100%', justifyContent: 'space-between', }, stripImage: { width: '100%', height: '31%', resizeMode: 'cover', borderRadius: 2, }, 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, }, duoCanvas: {}, duoContainer: { flex: 1, width: '100%', justifyContent: 'space-between', }, duoImage: { width: '100%', height: '49%', resizeMode: 'cover', borderRadius: 2, }, // ── Date Overlay ── dateOverlay: { position: 'absolute', zIndex: 60, }, dateOverlayText: { fontSize: 11, fontWeight: 'bold', color: '#FFFFFF', textShadowColor: 'rgba(0, 0, 0, 0.8)', textShadowOffset: { width: 1, height: 1 }, textShadowRadius: 3, backgroundColor: 'rgba(0, 0, 0, 0.35)', paddingVertical: 2, paddingHorizontal: 6, borderRadius: 3, }, // ── Loading ── 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, shadowColor: THEME.colors.gradientGlow, shadowOffset: { width: 0, height: 0 }, shadowOpacity: 0.35, shadowRadius: 20, elevation: 10, }, loadingText: { color: THEME.colors.text, fontSize: 16, fontWeight: 'bold', marginTop: THEME.spacing.md, }, });