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