Files
Schnappix/src/screens/PreviewScreen.tsx
T

1060 lines
32 KiB
TypeScript

import React, { useState, useRef, useCallback, useEffect } from 'react';
import { ScrollView, 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,
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';
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<CollageLayout>('single');
const [isProcessing, setIsProcessing] = useState<boolean>(false);
const [isCapturing, setIsCapturing] = useState<boolean>(false);
const [statusMessage, setStatusMessage] = useState<string>('');
const [activeUris, setActiveUris] = useState<string[]>([]);
// 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<StickerData[]>([]);
const [selectedStickerId, setSelectedStickerId] = useState<string | null>(null);
const mountedRef = useRef(true);
useEffect(() => {
return () => {
mountedRef.current = false;
};
}, []);
const [showStickerTray, setShowStickerTray] = useState<boolean>(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<any>(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<string | null>(
frameMode === 'always' ? initialFrameId : null
);
const viewShotRef = useRef<any>(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<StickerData>) => {
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);
}, 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 (
<View style={[styles.collageCanvas, styles.stripCanvas]}>
<View style={styles.stripContent}>
{activeUris.map((uri, idx) => (
<Image
key={idx}
source={{ uri }}
style={styles.stripImage}
resizeMode="cover"
resizeMethod="resize"
onLoad={() => logger.log(`Image loaded successfully: ${uri}`)}
onError={(e) => logger.error(`Image load failed: ${uri}`, e.nativeEvent?.error || e)}
/>
))}
</View>
<View style={styles.collageFooter}>
<Text style={styles.collageFooterText}>{eventText}</Text>
<Text style={styles.collageFooterDate}>{new Date().toLocaleDateString('de-DE')}</Text>
</View>
</View>
);
case 'grid':
return (
<View style={[styles.collageCanvas, styles.gridCanvas]}>
<View style={styles.gridContainer}>
{activeUris.map((uri, idx) => (
<Image
key={idx}
source={{ uri }}
style={styles.gridImage}
resizeMode="cover"
resizeMethod="resize"
onLoad={() => logger.log(`Grid Image loaded successfully: ${uri}`)}
onError={(e) => logger.error(`Grid Image load failed: ${uri}`, e.nativeEvent?.error || e)}
/>
))}
</View>
<View style={styles.collageFooter}>
<Text style={styles.collageFooterText}>{eventText}</Text>
</View>
</View>
);
case 'duo':
return (
<View style={[styles.collageCanvas, styles.duoCanvas]}>
<View style={styles.duoContainer}>
{activeUris.map((uri, idx) => (
<Image
key={idx}
source={{ uri }}
style={styles.duoImage}
resizeMode="cover"
resizeMethod="resize"
onLoad={() => logger.log(`Duo Image loaded successfully: ${uri}`)}
onError={(e) => logger.error(`Duo Image load failed: ${uri}`, e.nativeEvent?.error || e)}
/>
))}
</View>
<View style={styles.collageFooter}>
<Text style={styles.collageFooterText}>{eventText}</Text>
</View>
</View>
);
default:
return (
<View style={[styles.collageCanvas, styles.singleCanvas]}>
{activeUris.length > 0 && (
<Image
source={{ uri: activeUris[activeUris.length - 1] }}
style={styles.singleImage}
resizeMode="cover"
resizeMethod="resize"
onLoad={() => logger.log(`Single Image loaded successfully: ${currentPhoto}`)}
onError={(e) => logger.error(`Single Image load failed: ${currentPhoto}`, e.nativeEvent?.error || e)}
/>
)}
</View>
);
}
};
return (
<View
style={styles.container}
onTouchStart={resetIdleTimer}
>
{/* Left panel: Preview Canvas */}
<TouchableOpacity
style={styles.previewPanel}
activeOpacity={1}
onPress={handleCanvasPress}
>
<ViewShot
ref={viewShotRef}
options={{ format: 'jpg', quality: 0.95 }}
style={styles.viewShotContainer}
>
<View style={{ flex: 1, width: '100%', height: '100%' }} collapsable={false}>
{renderCollageView()}
{/* Frame Overlay */}
{activeFrame && <FrameOverlay frameAsset={activeFrame.asset} isCapturing={isCapturing} />}
{/* Date/Time Overlay */}
{dateOverlay !== 'off' && (
<View style={[styles.dateOverlay, getDatePositionStyle()]} pointerEvents={isCapturing ? "auto" : "none"}>
<Text style={styles.dateOverlayText}>
{formatDate(dateOverlay)}
</Text>
</View>
)}
{/* Stickers */}
{stickers.map((sticker) => (
<DraggableSticker
key={sticker.id}
sticker={sticker}
isSelected={selectedStickerId === sticker.id}
onSelect={handleStickerSelect}
onDelete={handleStickerDelete}
onUpdate={handleStickerUpdate}
/>
))}
</View>
</ViewShot>
{/* Sticker Tray (outside ViewShot — not captured) */}
{showStickerTray && (
<StickerTray
onAddEmoji={handleAddEmoji}
onAddDateSticker={handleAddDateSticker}
onAddEventSticker={handleAddEventSticker}
onClose={() => setShowStickerTray(false)}
eventText={eventText}
/>
)}
</TouchableOpacity>
{/* Global Trash Can for Stickers */}
{selectedStickerId !== null && (
<TouchableOpacity
style={styles.globalTrashButton}
onPress={() => handleStickerDelete(selectedStickerId)}
activeOpacity={0.7}
>
<FontAwesomeIcon icon={faTrash} color="#fff" size={32} />
</TouchableOpacity>
)}
{/* Right panel: Controls */}
<View style={styles.controlPanel}>
<Text style={styles.header}>{eventText}</Text>
{/* Thumbnail Reel for Photo Selection */}
{photoUris.length > 1 && (
<View style={styles.thumbnailReelContainer}>
<Text style={styles.thumbnailInstruction}>
Tippe auf bis zu 4 Bilder für eine Collage:
</Text>
<View style={styles.thumbnailReel}>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.thumbnailReelContent}>
{photoUris.map((uri, idx) => {
const isActive = activeUris.includes(uri);
const indexInSelection = activeUris.indexOf(uri) + 1;
return (
<TouchableOpacity
key={idx}
style={[styles.thumbnailWrapper, isActive && styles.thumbnailActive]}
onPress={() => togglePhoto(uri)}
activeOpacity={0.7}
>
<Image source={{ uri }} style={styles.thumbnailImage} />
{isActive && (
<View style={styles.thumbnailCheck}>
<Text style={{ color: '#fff', fontSize: 14, fontWeight: 'bold' }}>{indexInSelection}</Text>
</View>
)}
</TouchableOpacity>
);
})}
</ScrollView>
</View>
</View>
)}
<Text style={styles.photosCountText}>
Aufgenommene Fotos: {photoUris.length}
</Text>
{/* Editing Tools Row */}
<View style={styles.editToolsRow}>
<TouchableOpacity
style={[styles.editToolBtn, showStickerTray && styles.editToolBtnActive]}
onPress={() => { logger.log(`Action: Toggled sticker tray ${!showStickerTray}`); setShowStickerTray(!showStickerTray); }}
>
<FontAwesomeIcon
icon={faFaceGrinStars}
size={16}
color={showStickerTray ? THEME.colors.text : THEME.colors.accent}
/>
<Text style={[styles.editToolText, showStickerTray && styles.editToolTextActive]}>
Sticker
</Text>
</TouchableOpacity>
{frameMode === 'available' && (
<TouchableOpacity
style={[styles.editToolBtn, selectedFrameId !== null && styles.editToolBtnActive]}
onPress={() => {
if (selectedFrameId !== null) {
logger.log('Action: Removed frame');
setSelectedFrameId(null);
} else {
logger.log('Action: Toggled frame selection');
}
// FramePicker is shown below
}}
>
<FontAwesomeIcon
icon={faBorderAll}
size={16}
color={selectedFrameId !== null ? THEME.colors.text : THEME.colors.accent}
/>
<Text style={[styles.editToolText, selectedFrameId !== null && styles.editToolTextActive]}>
Rahmen
</Text>
</TouchableOpacity>
)}
</View>
{/* Frame Picker (when mode = available) */}
{frameMode === 'available' && (
<FramePicker
selectedFrameId={selectedFrameId}
onSelectFrame={(id) => {
logger.log(`Action: Selected frame ID: ${id}`);
setSelectedFrameId(id);
}}
availableFrameIds={availableFrameIds}
/>
)}
{/* Action Buttons */}
<View style={styles.actions}>
<TouchableOpacity style={[styles.actionBtn, styles.printBtn, isProcessing && styles.btnDisabled]} onPress={handlePrint} activeOpacity={0.8} disabled={isProcessing}>
<LinearGradient
colors={THEME.gradient.primary}
start={{ x: 0, y: 0.5 }}
end={{ x: 1, y: 0.5 }}
style={styles.printGradient}
>
<FontAwesomeIcon icon={faPrint} size={18} color={THEME.colors.text} style={{ marginRight: 8 }} />
<Text style={styles.actionBtnText}>JETZT DRUCKEN</Text>
</LinearGradient>
</TouchableOpacity>
<TouchableOpacity style={[styles.actionBtn, styles.saveBtn, isProcessing && styles.btnDisabled]} onPress={handleSaveOnly} disabled={isProcessing}>
<FontAwesomeIcon icon={faDownload} size={16} color={THEME.colors.accent} style={{ marginRight: 8 }} />
<Text style={[styles.actionBtnText, { color: THEME.colors.accent }]}>Nur auf Tablet speichern</Text>
</TouchableOpacity>
{photoUris.length < 4 && (
<TouchableOpacity style={[styles.actionBtn, styles.addBtn, isProcessing && styles.btnDisabled]} onPress={onAddAnother} disabled={isProcessing}>
<FontAwesomeIcon icon={faPlus} size={16} color={THEME.colors.text} style={{ marginRight: 8 }} />
<Text style={styles.actionBtnText}>Weiteres Foto aufnehmen</Text>
</TouchableOpacity>
)}
<View style={styles.rowActions}>
<TouchableOpacity style={[styles.smallBtn, styles.retakeBtn, isProcessing && styles.btnDisabled]} onPress={handleRetakeClick} disabled={isProcessing}>
<FontAwesomeIcon icon={faRotateLeft} size={14} color={THEME.colors.error} style={{ marginRight: 6 }} />
<Text style={[styles.smallBtnText, { color: THEME.colors.error }]}>Wiederholen</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.smallBtn, styles.resetBtn, isProcessing && styles.btnDisabled]} onPress={handleExit} disabled={isProcessing}>
<FontAwesomeIcon icon={faRightFromBracket} size={14} color={THEME.colors.textMuted} style={{ marginRight: 6 }} />
<Text style={styles.smallBtnText}>Beenden</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: '#030308',
},
viewShotContainer: {
width: 480,
height: 320,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: THEME.colors.background,
overflow: 'hidden',
},
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,
},
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: 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,
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: 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,
},
});