feat: dynamic collage layout based on photo selection count

This commit is contained in:
2026-06-11 15:25:22 +02:00
parent d9b2c9cc52
commit fc7b2c7769
3 changed files with 68 additions and 127 deletions
+2 -1
View File
@@ -29,7 +29,7 @@ import {
import { THEME } from '../styles/theme';
import { logger } from '../services/logger';
import KioskMode from '../../modules/kiosk-mode';
import { AppSettings, saveSettings } from '../services/settings';
import { AppSettings, loadSettings, saveSettings, APP_VERSION } from '../services/settings';
import { FRAMES } from '../data/frames';
interface AdminScreenProps {
@@ -229,6 +229,7 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
}
const updated: AppSettings = {
appVersion: APP_VERSION,
countdownDuration: duration,
printerIp: ipTrimmed,
adminPassword: password.trim(),
+2 -2
View File
@@ -108,7 +108,7 @@ export default function CameraScreen({
const scale = useSharedValue(1);
useEffect(() => {
if (countdown !== '') {
if (countdown !== null) {
scale.value = 0.5;
scale.value = withSpring(1.25, { damping: 10, stiffness: 120 }, () => {
scale.value = withTiming(1.0, { duration: 150 });
@@ -147,7 +147,7 @@ export default function CameraScreen({
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
]);
logger.log('Android Permissions:', JSON.stringify(granted));
logger.log('Android Permissions: ' + JSON.stringify(granted));
} catch (err) {
console.warn('Failed to request Android permissions', err);
}
+64 -124
View File
@@ -60,39 +60,43 @@ 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[]>([]);
// Initialize activeUris with all available photos up to 4 when first loaded
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]);
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 req = getRequiredPhotoCount(layout);
if (prev.includes(uri)) {
return prev; // Already selected, do nothing
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];
}
}
if (prev.length >= req) {
return [...prev.slice(1), uri]; // FIFO: remove oldest, add new
}
return [...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;
});
};
@@ -552,77 +556,36 @@ export default function PreviewScreen({
{/* Right panel: Controls */}
<View style={styles.controlPanel}>
<Text style={styles.header}>WÄHLE DEINEN STIL</Text>
<Text style={styles.header}>DEIN FOTO</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>
{/* 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>
{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);
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>
</View>
)}
@@ -776,37 +739,14 @@ const styles = StyleSheet.create({
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,
thumbnailInstruction: {
color: THEME.colors.textMuted,
fontSize: 12,
fontWeight: 'bold',
marginBottom: THEME.spacing.xs,
textAlign: 'center',
},
thumbnailReelContainer: {
marginBottom: THEME.spacing.md,
},
thumbnailReel: {
marginBottom: THEME.spacing.md,