feat: add photo selection reel for collages and app version reset
This commit is contained in:
+116
-22
@@ -1,13 +1,5 @@
|
||||
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
Image,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
BackHandler,
|
||||
} from 'react-native';
|
||||
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';
|
||||
@@ -68,6 +60,42 @@ export default function PreviewScreen({
|
||||
const [isCapturing, setIsCapturing] = useState<boolean>(false);
|
||||
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
|
||||
@@ -387,7 +415,7 @@ export default function PreviewScreen({
|
||||
return (
|
||||
<View style={[styles.collageCanvas, styles.stripCanvas]}>
|
||||
<View style={styles.stripContent}>
|
||||
{photoUris.slice(-3).map((uri, idx) => (
|
||||
{activeUris.map((uri, idx) => (
|
||||
<Image
|
||||
key={idx}
|
||||
source={{ uri }}
|
||||
@@ -409,7 +437,7 @@ export default function PreviewScreen({
|
||||
return (
|
||||
<View style={[styles.collageCanvas, styles.gridCanvas]}>
|
||||
<View style={styles.gridContainer}>
|
||||
{photoUris.slice(-4).map((uri, idx) => (
|
||||
{activeUris.map((uri, idx) => (
|
||||
<Image
|
||||
key={idx}
|
||||
source={{ uri }}
|
||||
@@ -430,7 +458,7 @@ export default function PreviewScreen({
|
||||
return (
|
||||
<View style={[styles.collageCanvas, styles.duoCanvas]}>
|
||||
<View style={styles.duoContainer}>
|
||||
{photoUris.slice(-2).map((uri, idx) => (
|
||||
{activeUris.map((uri, idx) => (
|
||||
<Image
|
||||
key={idx}
|
||||
source={{ uri }}
|
||||
@@ -449,15 +477,17 @@ export default function PreviewScreen({
|
||||
);
|
||||
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 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>
|
||||
);
|
||||
}
|
||||
@@ -570,8 +600,34 @@ export default function PreviewScreen({
|
||||
</TouchableOpacity>
|
||||
</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}>
|
||||
Aufgenommene Fotos: {photoUris.length} / 4
|
||||
Aufgenommene Fotos: {photoUris.length}
|
||||
</Text>
|
||||
|
||||
{/* Editing Tools Row */}
|
||||
@@ -752,6 +808,44 @@ const styles = StyleSheet.create({
|
||||
fontSize: 12,
|
||||
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: {
|
||||
fontSize: 14,
|
||||
color: THEME.colors.textMuted,
|
||||
|
||||
@@ -2,7 +2,10 @@ import { File, Paths } from 'expo-file-system';
|
||||
import * as FileSystem from 'expo-file-system/legacy';
|
||||
import { FRAMES } from '../data/frames';
|
||||
|
||||
export const APP_VERSION = 2; // Increment this whenever you want settings to reset on update
|
||||
|
||||
export interface AppSettings {
|
||||
appVersion: number;
|
||||
countdownDuration: number; // in seconds
|
||||
printerIp: string;
|
||||
adminPassword: string;
|
||||
@@ -30,6 +33,7 @@ export interface AppSettings {
|
||||
const settingsFile = new File(Paths.document, 'settings.json');
|
||||
|
||||
const DEFAULT_SETTINGS: AppSettings = {
|
||||
appVersion: APP_VERSION,
|
||||
countdownDuration: 3,
|
||||
printerIp: '192.168.1.100',
|
||||
adminPassword: '1234',
|
||||
@@ -55,6 +59,13 @@ export async function loadSettings(): Promise<AppSettings> {
|
||||
try {
|
||||
const content = await settingsFile.text();
|
||||
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 };
|
||||
} catch (e) {
|
||||
console.error('Failed to load settings, using defaults:', e);
|
||||
|
||||
Reference in New Issue
Block a user