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 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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user