feat: add photo selection reel for collages and app version reset

This commit is contained in:
2026-06-11 15:21:47 +02:00
parent a382df71b5
commit d9b2c9cc52
2 changed files with 127 additions and 22 deletions
+116 -22
View File
@@ -1,13 +1,5 @@
import React, { useState, useRef, useCallback, useEffect } from 'react'; import React, { useState, useRef, useCallback, useEffect } from 'react';
import { import { ScrollView, StyleSheet, Text, View, Image, TouchableOpacity, ActivityIndicator, BackHandler } from 'react-native';
StyleSheet,
Text,
View,
Image,
TouchableOpacity,
ActivityIndicator,
BackHandler,
} from 'react-native';
import ViewShot from 'react-native-view-shot'; import ViewShot from 'react-native-view-shot';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
@@ -68,6 +60,42 @@ export default function PreviewScreen({
const [isCapturing, setIsCapturing] = useState<boolean>(false); const [isCapturing, setIsCapturing] = useState<boolean>(false);
const [statusMessage, setStatusMessage] = useState<string>(''); const [statusMessage, setStatusMessage] = useState<string>('');
const getRequiredPhotoCount = (l: CollageLayout) => {
if (l === 'single') return 1;
if (l === 'duo') return 2;
if (l === 'strip') return 3;
if (l === 'grid') return 4;
return 1;
};
const [activeUris, setActiveUris] = useState<string[]>([]);
useEffect(() => {
const req = getRequiredPhotoCount(layout);
setActiveUris(prev => {
if (prev.length === req) return prev;
if (prev.length > req) return prev.slice(prev.length - req);
const available = photoUris.filter(u => !prev.includes(u));
const needed = req - prev.length;
const toAdd = available.slice(-needed);
return [...prev, ...toAdd];
});
}, [layout, photoUris]);
const togglePhoto = (uri: string) => {
logger.log(`Action: Toggled photo ${uri}`);
setActiveUris(prev => {
const req = getRequiredPhotoCount(layout);
if (prev.includes(uri)) {
return prev; // Already selected, do nothing
}
if (prev.length >= req) {
return [...prev.slice(1), uri]; // FIFO: remove oldest, add new
}
return [...prev, uri];
});
};
// Sticker state // Sticker state
@@ -387,7 +415,7 @@ export default function PreviewScreen({
return ( return (
<View style={[styles.collageCanvas, styles.stripCanvas]}> <View style={[styles.collageCanvas, styles.stripCanvas]}>
<View style={styles.stripContent}> <View style={styles.stripContent}>
{photoUris.slice(-3).map((uri, idx) => ( {activeUris.map((uri, idx) => (
<Image <Image
key={idx} key={idx}
source={{ uri }} source={{ uri }}
@@ -409,7 +437,7 @@ export default function PreviewScreen({
return ( return (
<View style={[styles.collageCanvas, styles.gridCanvas]}> <View style={[styles.collageCanvas, styles.gridCanvas]}>
<View style={styles.gridContainer}> <View style={styles.gridContainer}>
{photoUris.slice(-4).map((uri, idx) => ( {activeUris.map((uri, idx) => (
<Image <Image
key={idx} key={idx}
source={{ uri }} source={{ uri }}
@@ -430,7 +458,7 @@ export default function PreviewScreen({
return ( return (
<View style={[styles.collageCanvas, styles.duoCanvas]}> <View style={[styles.collageCanvas, styles.duoCanvas]}>
<View style={styles.duoContainer}> <View style={styles.duoContainer}>
{photoUris.slice(-2).map((uri, idx) => ( {activeUris.map((uri, idx) => (
<Image <Image
key={idx} key={idx}
source={{ uri }} source={{ uri }}
@@ -449,15 +477,17 @@ export default function PreviewScreen({
); );
default: default:
return ( return (
<View style={styles.singleCanvas}> <View style={[styles.collageCanvas, styles.singleCanvas]}>
<Image {activeUris.length > 0 && (
source={{ uri: currentPhoto }} <Image
style={styles.singleImage} source={{ uri: activeUris[activeUris.length - 1] }}
resizeMode="cover" style={styles.singleImage}
resizeMethod="resize" resizeMode="cover"
onLoad={() => logger.log(`Single Image loaded successfully: ${currentPhoto}`)} resizeMethod="resize"
onError={(e) => logger.error(`Single Image load failed: ${currentPhoto}`, e.nativeEvent?.error || e)} onLoad={() => logger.log(`Single Image loaded successfully: ${currentPhoto}`)}
/> onError={(e) => logger.error(`Single Image load failed: ${currentPhoto}`, e.nativeEvent?.error || e)}
/>
)}
</View> </View>
); );
} }
@@ -570,8 +600,34 @@ export default function PreviewScreen({
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{/* Thumbnail Reel for Photo Selection */}
{photoUris.length > 1 && layout !== 'single' && (
<View style={styles.thumbnailReel}>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.thumbnailReelContent}>
{photoUris.map((uri, idx) => {
const isActive = activeUris.includes(uri);
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: 10, fontWeight: 'bold' }}></Text>
</View>
)}
</TouchableOpacity>
);
})}
</ScrollView>
</View>
)}
<Text style={styles.photosCountText}> <Text style={styles.photosCountText}>
Aufgenommene Fotos: {photoUris.length} / 4 Aufgenommene Fotos: {photoUris.length}
</Text> </Text>
{/* Editing Tools Row */} {/* Editing Tools Row */}
@@ -752,6 +808,44 @@ const styles = StyleSheet.create({
fontSize: 12, fontSize: 12,
fontWeight: 'bold', fontWeight: 'bold',
}, },
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: { photosCountText: {
fontSize: 14, fontSize: 14,
color: THEME.colors.textMuted, color: THEME.colors.textMuted,
+11
View File
@@ -2,7 +2,10 @@ import { File, Paths } from 'expo-file-system';
import * as FileSystem from 'expo-file-system/legacy'; import * as FileSystem from 'expo-file-system/legacy';
import { FRAMES } from '../data/frames'; import { FRAMES } from '../data/frames';
export const APP_VERSION = 2; // Increment this whenever you want settings to reset on update
export interface AppSettings { export interface AppSettings {
appVersion: number;
countdownDuration: number; // in seconds countdownDuration: number; // in seconds
printerIp: string; printerIp: string;
adminPassword: string; adminPassword: string;
@@ -30,6 +33,7 @@ export interface AppSettings {
const settingsFile = new File(Paths.document, 'settings.json'); const settingsFile = new File(Paths.document, 'settings.json');
const DEFAULT_SETTINGS: AppSettings = { const DEFAULT_SETTINGS: AppSettings = {
appVersion: APP_VERSION,
countdownDuration: 3, countdownDuration: 3,
printerIp: '192.168.1.100', printerIp: '192.168.1.100',
adminPassword: '1234', adminPassword: '1234',
@@ -55,6 +59,13 @@ export async function loadSettings(): Promise<AppSettings> {
try { try {
const content = await settingsFile.text(); const content = await settingsFile.text();
const parsed = JSON.parse(content); const parsed = JSON.parse(content);
// Reset to defaults if the app version has changed (app updated)
if (parsed.appVersion !== APP_VERSION) {
console.log(`Settings version mismatch (old: ${parsed.appVersion}, new: ${APP_VERSION}). Resetting to default.`);
return DEFAULT_SETTINGS;
}
return { ...DEFAULT_SETTINGS, ...parsed }; return { ...DEFAULT_SETTINGS, ...parsed };
} catch (e) { } catch (e) {
console.error('Failed to load settings, using defaults:', e); console.error('Failed to load settings, using defaults:', e);