feat: dynamic collage layout based on photo selection count
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user