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 { THEME } from '../styles/theme';
import { logger } from '../services/logger'; import { logger } from '../services/logger';
import KioskMode from '../../modules/kiosk-mode'; 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'; import { FRAMES } from '../data/frames';
interface AdminScreenProps { interface AdminScreenProps {
@@ -229,6 +229,7 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
} }
const updated: AppSettings = { const updated: AppSettings = {
appVersion: APP_VERSION,
countdownDuration: duration, countdownDuration: duration,
printerIp: ipTrimmed, printerIp: ipTrimmed,
adminPassword: password.trim(), adminPassword: password.trim(),
+2 -2
View File
@@ -108,7 +108,7 @@ export default function CameraScreen({
const scale = useSharedValue(1); const scale = useSharedValue(1);
useEffect(() => { useEffect(() => {
if (countdown !== '') { if (countdown !== null) {
scale.value = 0.5; scale.value = 0.5;
scale.value = withSpring(1.25, { damping: 10, stiffness: 120 }, () => { scale.value = withSpring(1.25, { damping: 10, stiffness: 120 }, () => {
scale.value = withTiming(1.0, { duration: 150 }); scale.value = withTiming(1.0, { duration: 150 });
@@ -147,7 +147,7 @@ export default function CameraScreen({
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE, PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
]); ]);
logger.log('Android Permissions:', JSON.stringify(granted)); logger.log('Android Permissions: ' + JSON.stringify(granted));
} catch (err) { } catch (err) {
console.warn('Failed to request Android permissions', 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 [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[]>([]); const [activeUris, setActiveUris] = useState<string[]>([]);
// Initialize activeUris with all available photos up to 4 when first loaded
useEffect(() => { useEffect(() => {
const req = getRequiredPhotoCount(layout); setActiveUris(photoUris.slice(-4));
setActiveUris(prev => { const count = Math.min(photoUris.length, 4);
if (prev.length === req) return prev; if (count === 1) setLayout('single');
if (prev.length > req) return prev.slice(prev.length - req); else if (count === 2) setLayout('duo');
const available = photoUris.filter(u => !prev.includes(u)); else if (count === 3) setLayout('strip');
const needed = req - prev.length; else if (count === 4) setLayout('grid');
const toAdd = available.slice(-needed); }, [photoUris]);
return [...prev, ...toAdd];
});
}, [layout, photoUris]);
const togglePhoto = (uri: string) => { const togglePhoto = (uri: string) => {
logger.log(`Action: Toggled photo ${uri}`); logger.log(`Action: Toggled photo ${uri}`);
setActiveUris(prev => { setActiveUris(prev => {
const req = getRequiredPhotoCount(layout); const isSelected = prev.includes(uri);
if (prev.includes(uri)) { let nextUris: string[];
return prev; // Already selected, do nothing
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 // Automatically update layout based on selection count
} const count = nextUris.length;
return [...prev, uri]; 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 */} {/* Right panel: Controls */}
<View style={styles.controlPanel}> <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 */} {/* Thumbnail Reel for Photo Selection */}
{photoUris.length > 1 && layout !== 'single' && ( {photoUris.length > 1 && (
<View style={styles.thumbnailReel}> <View style={styles.thumbnailReelContainer}>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.thumbnailReelContent}> <Text style={styles.thumbnailInstruction}>
{photoUris.map((uri, idx) => { Tippe auf bis zu 4 Bilder für eine Collage:
const isActive = activeUris.includes(uri); </Text>
return ( <View style={styles.thumbnailReel}>
<TouchableOpacity <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.thumbnailReelContent}>
key={idx} {photoUris.map((uri, idx) => {
style={[styles.thumbnailWrapper, isActive && styles.thumbnailActive]} const isActive = activeUris.includes(uri);
onPress={() => togglePhoto(uri)} return (
activeOpacity={0.7} <TouchableOpacity
> key={idx}
<Image source={{ uri }} style={styles.thumbnailImage} /> style={[styles.thumbnailWrapper, isActive && styles.thumbnailActive]}
{isActive && ( onPress={() => togglePhoto(uri)}
<View style={styles.thumbnailCheck}> activeOpacity={0.7}
<Text style={{ color: '#fff', fontSize: 10, fontWeight: 'bold' }}></Text> >
</View> <Image source={{ uri }} style={styles.thumbnailImage} />
)} {isActive && (
</TouchableOpacity> <View style={styles.thumbnailCheck}>
); <Text style={{ color: '#fff', fontSize: 10, fontWeight: 'bold' }}></Text>
})} </View>
</ScrollView> )}
</TouchableOpacity>
);
})}
</ScrollView>
</View>
</View> </View>
)} )}
@@ -776,37 +739,14 @@ const styles = StyleSheet.create({
textShadowOffset: { width: 0, height: 0 }, textShadowOffset: { width: 0, height: 0 },
textShadowRadius: 12, textShadowRadius: 12,
}, },
layoutSelector: { thumbnailInstruction: {
flexDirection: 'row', color: THEME.colors.textMuted,
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, fontSize: 12,
fontWeight: 'bold', marginBottom: THEME.spacing.xs,
textAlign: 'center',
},
thumbnailReelContainer: {
marginBottom: THEME.spacing.md,
}, },
thumbnailReel: { thumbnailReel: {
marginBottom: THEME.spacing.md, marginBottom: THEME.spacing.md,