import React, { useState, useRef, useCallback, useEffect } from 'react'; import { ScrollView, StyleSheet, Text, View, Image, TouchableOpacity, ActivityIndicator, BackHandler, useWindowDimensions } 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, faTrash, } 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'; dateOverlayTransform: { x: number; y: number; rotation: number; scale: number }; // Event text eventText: string; } type CollageLayout = 'single' | 'strip' | 'grid' | 'duo'; export default function PreviewScreen({ photoUris, printerIp, onRetakeLast, onAddAnother, onReset, frameMode, initialFrameId, availableFrameIds, dateOverlay, dateOverlayTransform, eventText, }: PreviewScreenProps) { const [layout, setLayout] = useState('single'); const [isProcessing, setIsProcessing] = useState(false); const [isCapturing, setIsCapturing] = useState(false); const [statusMessage, setStatusMessage] = useState(''); const { width, height } = useWindowDimensions(); const isPortrait = height > width; const isSmallDevice = width < 768; const [activeUris, setActiveUris] = useState([]); // Initialize activeUris with all available photos up to 4 when first loaded useEffect(() => { setActiveUris(photoUris.slice(-4)); const count = Math.min(photoUris.length, 4); if (count === 1) setLayout('single'); else if (count === 2) setLayout('duo'); else if (count === 3) setLayout('strip'); else if (count === 4) setLayout('grid'); }, [photoUris]); const togglePhoto = (uri: string) => { logger.log(`Action: Toggled photo ${uri}`); setActiveUris(prev => { const isSelected = prev.includes(uri); let nextUris: string[]; if (isSelected) { if (prev.length <= 1) return prev; // Don't allow 0 photos nextUris = prev.filter(u => u !== uri); } else { if (prev.length >= 4) { nextUris = [...prev.slice(1), uri]; // Keep max 4, replace oldest } else { nextUris = [...prev, uri]; } } // Automatically update layout based on selection count const count = nextUris.length; if (count === 1) setLayout('single'); else if (count === 2) setLayout('duo'); else if (count === 3) setLayout('strip'); else if (count === 4) setLayout('grid'); return nextUris; }); }; // 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); const [showFrameTray, setShowFrameTray] = 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) }; const subscription = BackHandler.addEventListener('hardwareBackPress', onBackPress); return () => { subscription.remove(); }; }, [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 = () => { return { position: 'absolute' as const, left: `${dateOverlayTransform.x}%` as any, top: `${dateOverlayTransform.y}%` as any, transform: [ { rotate: `${dateOverlayTransform.rotation}deg` }, { scale: dateOverlayTransform.scale } ] }; }; // ── 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, 'Collage', eventText); 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); } }; const handleExitRef = useRef<(() => void) | null>(null); handleExitRef.current = async () => { logger.log('Idle timer triggered or BEENDEN clicked. Exiting PreviewScreen...'); if (idleTimerRef.current) clearTimeout(idleTimerRef.current); if (!mountedRef.current) { logger.log('PreviewScreen already unmounted, aborting exit.'); return; } if (isProcessing) { logger.log('Currently processing, aborting auto-exit.'); 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, 'Collage', eventText); 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(); }, 120000); }, []); 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, 'Collage', eventText); 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 ( {activeUris.map((uri, idx) => ( logger.log(`Image loaded successfully: ${uri}`)} onError={(e) => logger.error(`Image load failed: ${uri}`, e.nativeEvent?.error || e)} /> ))} ); case 'grid': return ( {activeUris.map((uri, idx) => ( logger.log(`Grid Image loaded successfully: ${uri}`)} onError={(e) => logger.error(`Grid Image load failed: ${uri}`, e.nativeEvent?.error || e)} /> ))} ); case 'duo': return ( {activeUris.map((uri, idx) => ( logger.log(`Duo Image loaded successfully: ${uri}`)} onError={(e) => logger.error(`Duo Image load failed: ${uri}`, e.nativeEvent?.error || e)} /> ))} ); default: return ( {activeUris.length > 0 && ( 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} /> )} {/* Global Trash Can for Stickers */} {selectedStickerId !== null && ( handleStickerDelete(selectedStickerId)} activeOpacity={0.7} > )} {/* Right panel: Controls */} {eventText} {/* Thumbnail Reel for Photo Selection */} {photoUris.length > 1 && ( Tippe auf bis zu 4 Bilder für eine Collage: {photoUris.map((uri, idx) => { const isActive = activeUris.includes(uri); const indexInSelection = activeUris.indexOf(uri) + 1; return ( togglePhoto(uri)} activeOpacity={0.7} > {isActive && ( {indexInSelection} )} ); })} )} Aufgenommene Fotos: {photoUris.length} {/* Editing Tools Row */} { logger.log(`Action: Toggled sticker tray ${!showStickerTray}`); setShowStickerTray(!showStickerTray); if (!showStickerTray) setShowFrameTray(false); }} > Sticker {frameMode === 'available' && ( { logger.log(`Action: Toggled frame tray ${!showFrameTray}`); setShowFrameTray(!showFrameTray); if (!showFrameTray) setShowStickerTray(false); }} > Rahmen )} {/* Frame Picker (when mode = available) */} {frameMode === 'available' && showFrameTray && ( { logger.log(`Action: Selected frame ID: ${id}`); setSelectedFrameId(id); }} availableFrameIds={availableFrameIds} /> )} {/* Action Buttons */} {!!printerIp?.trim() && ( JETZT DRUCKEN )} {photoUris.length < 4 && ( Weiteres Foto aufnehmen )} Wiederholen Beenden {/* Loading Overlay */} {isProcessing && ( {statusMessage} )} ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: THEME.colors.background, }, previewPanel: { justifyContent: 'flex-start', alignItems: 'center', padding: THEME.spacing.md, paddingTop: 60, backgroundColor: '#030308', }, viewShotContainer: { width: '100%', aspectRatio: 1.5, justifyContent: 'center', alignItems: 'center', backgroundColor: THEME.colors.background, overflow: 'hidden', }, controlPanel: { backgroundColor: THEME.colors.surface, padding: THEME.spacing.xl, 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, }, globalTrashButton: { position: 'absolute', top: 40, alignSelf: 'center', width: 60, height: 60, borderRadius: 30, backgroundColor: THEME.colors.error, justifyContent: 'center', alignItems: 'center', zIndex: 9999, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.5, shadowRadius: 6, elevation: 8, }, thumbnailInstruction: { color: THEME.colors.textMuted, fontSize: 12, marginBottom: THEME.spacing.xs, textAlign: 'center', }, thumbnailReelContainer: { marginBottom: THEME.spacing.md, }, thumbnailReel: { marginBottom: THEME.spacing.md, height: 60, }, thumbnailReelContent: { alignItems: 'center', gap: 10, }, thumbnailWrapper: { width: 45, height: 45, borderRadius: 4, borderWidth: 2, borderColor: 'transparent', overflow: 'hidden', position: 'relative', opacity: 0.5, }, thumbnailActive: { borderColor: THEME.colors.primary, opacity: 1, }, thumbnailImage: { width: '100%', height: '100%', resizeMode: 'cover', }, thumbnailCheck: { position: 'absolute', bottom: 2, right: 2, backgroundColor: THEME.colors.primary, borderRadius: 10, width: 14, height: 14, justifyContent: 'center', alignItems: 'center', }, 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: 8, paddingVertical: 12, paddingHorizontal: 20, 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: 16, 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: '100%', height: '100%', 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: '100%', height: '100%', backgroundColor: '#ffffff', padding: 6, alignItems: 'center', justifyContent: 'space-between', }, collageFooter: { height: 35, justifyContent: 'center', alignItems: 'center', width: '100%', borderTopWidth: 1, borderTopColor: '#e0e0e0', marginTop: 4, }, collageFooterText: { fontSize: 16, fontWeight: 'bold', color: '#333333', letterSpacing: 3, }, collageFooterDate: { fontSize: 12, color: '#666666', }, stripCanvas: {}, stripContent: { flex: 1, flexDirection: 'row', width: '100%', justifyContent: 'space-between', }, stripImage: { width: '32.5%', height: '100%', 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, flexDirection: 'row', width: '100%', justifyContent: 'space-between', }, duoImage: { width: '49%', height: '100%', resizeMode: 'cover', borderRadius: 2, }, // ── Date Overlay ── dateOverlay: { position: 'absolute', elevation: 22, zIndex: 12, }, dateOverlayText: { fontSize: 16, 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: 4, paddingHorizontal: 8, borderRadius: 6, }, // ── 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, }, });