981 lines
30 KiB
TypeScript
981 lines
30 KiB
TypeScript
import React, { useState, useRef, useCallback, useEffect } 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<CollageLayout>('single');
|
|
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
|
const [isCapturing, setIsCapturing] = useState<boolean>(false);
|
|
const [statusMessage, setStatusMessage] = useState<string>('');
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
// 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 && 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) => {
|
|
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);
|
|
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 (
|
|
<View style={[styles.collageCanvas, styles.stripCanvas]}>
|
|
<View style={styles.stripContent}>
|
|
{photoUris.slice(-3).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}>SCHNAPPIX FOTOBOX</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}>
|
|
{photoUris.slice(-4).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}>SCHNAPPIX FOTOBOX</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}
|
|
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}>SCHNAPPIX FOTOBOX</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
default:
|
|
return (
|
|
<View style={styles.singleCanvas}>
|
|
<Image
|
|
source={{ uri: currentPhoto }}
|
|
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>
|
|
|
|
{/* Right panel: Controls */}
|
|
<View style={styles.controlPanel}>
|
|
<Text style={styles.header}>WÄHLE DEINEN STIL</Text>
|
|
|
|
{/* Layout Selection */}
|
|
<View style={styles.layoutSelector}>
|
|
<TouchableOpacity
|
|
style={[styles.layoutBtn, layout === 'single' && styles.layoutBtnActive]}
|
|
onPress={() => { logger.log('Action: Selected layout single'); setLayout('single'); }}
|
|
>
|
|
<Text style={styles.layoutBtnText}>Einzelbild</Text>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.layoutBtn,
|
|
photoUris.length < 2 && styles.layoutBtnDisabled,
|
|
layout === 'duo' && styles.layoutBtnActive,
|
|
]}
|
|
disabled={photoUris.length < 2}
|
|
onPress={() => { logger.log('Action: Selected layout duo'); 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={() => { logger.log('Action: Selected layout strip'); setLayout('strip'); }}
|
|
>
|
|
<Text style={styles.layoutBtnText}>Streifen (3)</Text>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.layoutBtn,
|
|
photoUris.length < 4 && styles.layoutBtnDisabled,
|
|
layout === 'grid' && styles.layoutBtnActive,
|
|
]}
|
|
disabled={photoUris.length < 4}
|
|
onPress={() => { logger.log('Action: Selected layout grid'); setLayout('grid'); }}
|
|
>
|
|
<Text style={styles.layoutBtnText}>Raster (4)</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
<Text style={styles.photosCountText}>
|
|
Aufgenommene Fotos: {photoUris.length} / 4
|
|
</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' && photoUris.length === 1 && (
|
|
<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 and only 1 photo) */}
|
|
{frameMode === 'available' && photoUris.length === 1 && (
|
|
<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,
|
|
},
|
|
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,
|
|
},
|
|
});
|