import React, { useState, useRef, useCallback, useEffect } from 'react'; import { StyleSheet, Text, View, Image, TouchableOpacity, ActivityIndicator, BackHandler, } 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 [isCapturing, setIsCapturing] = useState(false); const [statusMessage, setStatusMessage] = useState(''); // Sticker state const [stickers, setStickers] = useState([]); const [selectedStickerId, setSelectedStickerId] = useState(null); const mountedRef = useRef(true); useEffect(() => { return () => { mountedRef.current = false; }; }, []); 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); // Hardware Back Press useEffect(() => { const onBackPress = () => { onReset(); return true; // Prevent default behavior (closing the app) }; BackHandler.addEventListener('hardwareBackPress', onBackPress); return () => { BackHandler.removeEventListener('hardwareBackPress', onBackPress); }; }, [onReset]); // 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 ? 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) => { logger.log(`Action: Added emoji sticker: ${emoji}`); addSticker('emoji', emoji); }, [addSticker]); const handleAddDateSticker = useCallback((format: 'date' | 'datetime') => { logger.log(`Action: Added date sticker: ${format}`); addSticker('text', formatDate(format)); }, [addSticker, formatDate]); const handleAddEventSticker = useCallback(() => { logger.log('Action: Added event sticker'); 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) => { logger.log(`Action: Deleted sticker: ${id}`); 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); }, []); // Keep latest state in a ref to avoid stale closures and rapid re-renders const stateRef = useRef({ stickers, layout, activeFrame, dateOverlay, onReset, currentPhoto }); React.useEffect(() => { stateRef.current = { stickers, layout, activeFrame, dateOverlay, onReset, currentPhoto }; }, [stickers, layout, activeFrame, dateOverlay, onReset, currentPhoto]); // ── Capture the final image ── const captureComposite = async (fallbackPhoto: string = currentPhoto) => { try { logger.log(`captureComposite started. Has ViewShot ref: ${!!viewShotRef.current}`); if (viewShotRef.current) { setIsCapturing(true); // Wait for React to re-render and remove pointerEvents="none" which blocks ViewShot on Android await new Promise(resolve => setTimeout(resolve, 300)); logger.log('Executing viewShotRef.current.capture()...'); const uri = await viewShotRef.current.capture(); setIsCapturing(false); logger.log(`ViewShot successfully generated collage URI: ${uri}`); return uri; } logger.log('[WARNING] viewShotRef.current was null, falling back to raw photo.'); return fallbackPhoto; } catch (e: any) { setIsCapturing(false); logger.error('Failed to capture composite (ViewShot crashed)', e?.message || e); return fallbackPhoto; } }; // ── Actions ── const handlePrint = async () => { logger.log('Action: Clicked Print button'); if (idleTimerRef.current) clearTimeout(idleTimerRef.current); if (!mountedRef.current) return; if (isProcessing) return; setIsProcessing(true); try { const printUri = await captureComposite(currentPhoto); if (!mountedRef.current) return; setStatusMessage('Dein Foto wird vorbereitet...'); setStatusMessage('Druckauftrag wird an Drucker gesendet...'); await printImageLocal(printUri, { ipAddress: printerIp, jobName: 'Schnappix Fotobox', }); setStatusMessage('Wird in Galerie gespeichert...'); await saveToGallery(printUri); if (!mountedRef.current) return; setStatusMessage('Druck erfolgreich! Viel Spaß! 🎉'); setTimeout(() => { if (!mountedRef.current) return; setIsProcessing(false); onReset(); }, 2000); } catch (error: any) { if (!mountedRef.current) return; console.error('Printing failed:', error); alert('Fehler: ' + error.message); setIsProcessing(false); } }; // Saves to gallery without printing const handleSaveOnly = async () => { logger.log('Action: Clicked Save button'); if (idleTimerRef.current) clearTimeout(idleTimerRef.current); if (!mountedRef.current) return; if (isProcessing) return; setIsProcessing(true); try { const saveUri = await captureComposite(currentPhoto); if (!mountedRef.current) return; setStatusMessage('Bild wird generiert...'); setStatusMessage('Wird in Galerie gespeichert...'); await saveToGallery(saveUri); if (!mountedRef.current) return; setStatusMessage('Erfolgreich gespeichert!'); setTimeout(() => { if (!mountedRef.current) return; setIsProcessing(false); onReset(); }, 1500); } catch (error: any) { if (!mountedRef.current) return; console.error('Saving failed:', error); alert('Fehler: ' + error.message); setIsProcessing(false); } }; const handleExitRef = useRef<(() => void) | null>(null); handleExitRef.current = async () => { if (idleTimerRef.current) clearTimeout(idleTimerRef.current); if (!mountedRef.current) return; if (isProcessing) return; const state = stateRef.current; if (state.stickers.length === 0 && state.layout === 'single' && !state.activeFrame && state.dateOverlay === 'off') { state.onReset(); return; } setIsProcessing(true); try { const capturedUri = await captureComposite(state.currentPhoto); if (!mountedRef.current) return; setStatusMessage('Wird gespeichert...'); await saveToGallery(capturedUri); if (!mountedRef.current) return; setStatusMessage('Erfolgreich gespeichert!'); setTimeout(() => { if (!mountedRef.current) return; setIsProcessing(false); state.onReset(); }, 1000); } catch (error) { if (!mountedRef.current) return; console.error('Auto-saving on exit failed:', error); setIsProcessing(false); state.onReset(); } }; // Auto-save and exit const handleExit = useCallback(async () => { if (handleExitRef.current) handleExitRef.current(); }, []); // Idle timer logic const resetIdleTimer = useCallback(() => { if (idleTimerRef.current) clearTimeout(idleTimerRef.current); idleTimerRef.current = setTimeout(() => { if (handleExitRef.current) handleExitRef.current(); }, 60000); }, []); useEffect(() => { resetIdleTimer(); return () => { if (idleTimerRef.current) clearTimeout(idleTimerRef.current); }; }, [resetIdleTimer]); const handleRetakeClick = async () => { logger.log('Action: Clicked Retake button'); if (idleTimerRef.current) clearTimeout(idleTimerRef.current); if (!mountedRef.current) return; if (isProcessing) return; if (stickers.length > 0 || layout !== 'single' || activeFrame || dateOverlay !== 'off') { setIsProcessing(true); try { const capturedUri = await captureComposite(currentPhoto); if (!mountedRef.current) return; setStatusMessage('Wird vor dem Wiederholen gespeichert...'); await saveToGallery(capturedUri); if (!mountedRef.current) return; } catch (e) { if (!mountedRef.current) return; console.error('Failed to auto-save before retake:', e); } setIsProcessing(false); } onRetakeLast(); }; // ── Render the collage ── const renderCollageView = () => { switch (layout) { case 'strip': return ( {photoUris.slice(-3).map((uri, idx) => ( logger.log(`Image loaded successfully: ${uri}`)} onError={(e) => logger.error(`Image load failed: ${uri}`, e.nativeEvent?.error || e)} /> ))} SCHNAPPIX FOTOBOX {new Date().toLocaleDateString('de-DE')} ); case 'grid': return ( {photoUris.slice(-4).map((uri, idx) => ( logger.log(`Grid Image loaded successfully: ${uri}`)} onError={(e) => logger.error(`Grid Image load failed: ${uri}`, e.nativeEvent?.error || e)} /> ))} SCHNAPPIX FOTOBOX ); case 'duo': return ( {photoUris.slice(-2).map((uri, idx) => ( logger.log(`Duo Image loaded successfully: ${uri}`)} onError={(e) => logger.error(`Duo Image load failed: ${uri}`, e.nativeEvent?.error || e)} /> ))} SCHNAPPIX FOTOBOX ); default: return ( logger.log(`Single Image loaded successfully: ${currentPhoto}`)} onError={(e) => logger.error(`Single Image load failed: ${currentPhoto}`, e.nativeEvent?.error || e)} /> ); } }; 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 */} { logger.log('Action: Selected layout single'); setLayout('single'); }} > Einzelbild { logger.log('Action: Selected layout duo'); setLayout('duo'); }} > Duo (2) { logger.log('Action: Selected layout strip'); setLayout('strip'); }} > Streifen (3) { logger.log('Action: Selected layout grid'); setLayout('grid'); }} > Raster (4) Aufgenommene Fotos: {photoUris.length} / 4 {/* Editing Tools Row */} { logger.log(`Action: Toggled sticker tray ${!showStickerTray}`); setShowStickerTray(!showStickerTray); }} > Sticker {frameMode === 'available' && ( { if (selectedFrameId !== null) { logger.log('Action: Removed frame'); setSelectedFrameId(null); } else { logger.log('Action: Toggled frame selection'); } // FramePicker is shown below }} > Rahmen )} {/* Frame Picker (when mode = available) */} {frameMode === 'available' && ( { logger.log(`Action: Selected frame ID: ${id}`); setSelectedFrameId(id); }} availableFrameIds={availableFrameIds} /> )} {/* 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', backgroundColor: THEME.colors.background, }, 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', }, btnDisabled: { opacity: 0.5, }, // ── 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', elevation: 22, zIndex: 12, }, 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, }, });