UI refactor: Admin Screen redesign, Timers, Flash timing, Date overlay

This commit is contained in:
2026-06-12 00:06:12 +02:00
parent d1b7ca36d0
commit 3bed68f728
6 changed files with 282 additions and 205 deletions
+1 -1
View File
@@ -184,7 +184,7 @@ export default function App() {
initialFrameId={settings.selectedFrameId} initialFrameId={settings.selectedFrameId}
availableFrameIds={settings.availableFrameIds} availableFrameIds={settings.availableFrameIds}
dateOverlay={settings.dateOverlay} dateOverlay={settings.dateOverlay}
dateOverlayPosition={settings.dateOverlayPosition} dateOverlayTransform={settings.dateOverlayTransform}
eventText={settings.eventText} eventText={settings.eventText}
/> />
</View> </View>
+2 -1
View File
@@ -3,7 +3,7 @@
"newArchEnabled": true, "newArchEnabled": true,
"name": "Schnappix", "name": "Schnappix",
"slug": "Schnappix", "slug": "Schnappix",
"version": "1.0.0", "version": "1.0.1",
"orientation": "landscape", "orientation": "landscape",
"icon": "./Icon.png", "icon": "./Icon.png",
"userInterfaceStyle": "dark", "userInterfaceStyle": "dark",
@@ -13,6 +13,7 @@
}, },
"android": { "android": {
"package": "com.schnappix", "package": "com.schnappix",
"versionCode": 2,
"adaptiveIcon": { "adaptiveIcon": {
"backgroundColor": "#050510", "backgroundColor": "#050510",
"foregroundImage": "./Icon-padded.png" "foregroundImage": "./Icon-padded.png"
+253 -143
View File
@@ -10,6 +10,7 @@ import {
Alert, Alert,
Modal, Modal,
BackHandler, BackHandler,
PanResponder,
} from 'react-native'; } from 'react-native';
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';
@@ -54,13 +55,7 @@ const DATE_OVERLAY_OPTIONS: { value: DateOverlayMode; label: string }[] = [
{ value: 'datetime', label: 'Datum + Uhrzeit' }, { value: 'datetime', label: 'Datum + Uhrzeit' },
]; ];
const DATE_POSITION_OPTIONS: { value: DatePosition; label: string }[] = [
{ value: 'bottom-right', label: 'Unten rechts' },
{ value: 'bottom-left', label: 'Unten links' },
{ value: 'bottom-center', label: 'Unten Mitte' },
{ value: 'top-right', label: 'Oben rechts' },
{ value: 'top-left', label: 'Oben links' },
];
export default function AdminScreen({ currentSettings, onSave, onClose }: AdminScreenProps) { export default function AdminScreen({ currentSettings, onSave, onClose }: AdminScreenProps) {
const [countdown, setCountdown] = useState<string>(String(currentSettings.countdownDuration)); const [countdown, setCountdown] = useState<string>(String(currentSettings.countdownDuration));
@@ -77,15 +72,38 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
const [selectedFrameId, setSelectedFrameId] = useState<string | null>(currentSettings.selectedFrameId); const [selectedFrameId, setSelectedFrameId] = useState<string | null>(currentSettings.selectedFrameId);
const [availableFrameIds, setAvailableFrameIds] = useState<string[]>(currentSettings.availableFrameIds || []); const [availableFrameIds, setAvailableFrameIds] = useState<string[]>(currentSettings.availableFrameIds || []);
const [dateOverlay, setDateOverlay] = useState<DateOverlayMode>(currentSettings.dateOverlay); const [dateOverlay, setDateOverlay] = useState<DateOverlayMode>(currentSettings.dateOverlay);
const [dateOverlayPosition, setDateOverlayPosition] = useState<DatePosition>(currentSettings.dateOverlayPosition); const [dateOverlayTransform, setDateOverlayTransform] = useState(currentSettings.dateOverlayTransform);
const [eventText, setEventText] = useState<string>(currentSettings.eventText); const [eventText, setEventText] = useState<string>(currentSettings.eventText);
const [burstCount, setBurstCount] = useState<string>(String(currentSettings.burstCount)); const [burstCount, setBurstCount] = useState<number>(currentSettings.burstCount);
const [burstInterval, setBurstInterval] = useState<string>(String(currentSettings.burstIntervalSec)); const [burstIntervalSec, setBurstIntervalSec] = useState<number>(currentSettings.burstIntervalSec);
const [welcomeText, setWelcomeText] = useState<string>(currentSettings.welcomeText); const [welcomeText, setWelcomeText] = useState<string>(currentSettings.welcomeText);
const [footerText, setFooterText] = useState<string>(currentSettings.footerText); const [footerText, setFooterText] = useState<string>(currentSettings.footerText);
const [useFrontCamera, setUseFrontCamera] = useState<boolean>(currentSettings.useFrontCamera); const [useFrontCamera, setUseFrontCamera] = useState<boolean>(currentSettings.useFrontCamera);
const [enableLogs, setEnableLogs] = useState<boolean>(currentSettings.enableLogs); const [enableLogs, setEnableLogs] = useState<boolean>(currentSettings.enableLogs);
const transformRef = useRef(dateOverlayTransform);
useEffect(() => {
transformRef.current = dateOverlayTransform;
}, [dateOverlayTransform]);
const initialTransform = useRef(dateOverlayTransform);
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderGrant: () => {
initialTransform.current = transformRef.current;
},
onPanResponderMove: (e, gesture) => {
setDateOverlayTransform({
...transformRef.current,
x: Math.max(0, Math.min(100, initialTransform.current.x + (gesture.dx / 3))),
y: Math.max(0, Math.min(100, initialTransform.current.y + (gesture.dy / 2)))
});
},
})
).current;
const hasUnsavedChanges = () => { const hasUnsavedChanges = () => {
return ( return (
countdown !== String(currentSettings.countdownDuration) || countdown !== String(currentSettings.countdownDuration) ||
@@ -96,10 +114,10 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
selectedFrameId !== currentSettings.selectedFrameId || selectedFrameId !== currentSettings.selectedFrameId ||
availableFrameIds.join() !== (currentSettings.availableFrameIds || []).join() || availableFrameIds.join() !== (currentSettings.availableFrameIds || []).join() ||
dateOverlay !== currentSettings.dateOverlay || dateOverlay !== currentSettings.dateOverlay ||
dateOverlayPosition !== currentSettings.dateOverlayPosition || JSON.stringify(dateOverlayTransform) !== JSON.stringify(currentSettings.dateOverlayTransform) ||
eventText !== currentSettings.eventText || eventText !== currentSettings.eventText ||
burstCount !== String(currentSettings.burstCount) || burstCount !== currentSettings.burstCount ||
burstInterval !== String(currentSettings.burstIntervalSec) || burstIntervalSec !== currentSettings.burstIntervalSec ||
welcomeText !== currentSettings.welcomeText || welcomeText !== currentSettings.welcomeText ||
footerText !== currentSettings.footerText || footerText !== currentSettings.footerText ||
useFrontCamera !== currentSettings.useFrontCamera || useFrontCamera !== currentSettings.useFrontCamera ||
@@ -129,7 +147,7 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
return () => subscription.remove(); return () => subscription.remove();
}, [ }, [
countdown, printerIp, password, kioskActive, frameMode, selectedFrameId, availableFrameIds, countdown, printerIp, password, kioskActive, frameMode, selectedFrameId, availableFrameIds,
dateOverlay, dateOverlayPosition, eventText, burstCount, burstInterval, welcomeText, dateOverlay, dateOverlayTransform, eventText, burstCount, burstIntervalSec, welcomeText,
footerText, useFrontCamera, enableLogs, currentSettings footerText, useFrontCamera, enableLogs, currentSettings
]); ]);
@@ -206,17 +224,8 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
return; return;
} }
const burst = parseInt(burstCount, 10); const burst = burstCount;
if (isNaN(burst) || burst < 1 || burst > 5) { const interval = burstIntervalSec;
Alert.alert('Ungültige Eingabe', 'Die Serienbildanzahl muss zwischen 1 und 5 liegen.');
return;
}
const interval = parseInt(burstInterval, 10);
if (isNaN(interval) || interval < 3 || interval > 30) {
Alert.alert('Ungültige Eingabe', 'Der Abstand zwischen Serienbildern muss zwischen 3 und 30 Sekunden liegen.');
return;
}
if (frameMode === 'always' && selectedFrameId === null && FRAMES.length > 0) { if (frameMode === 'always' && selectedFrameId === null && FRAMES.length > 0) {
Alert.alert('Hinweis', 'Bitte wähle einen Rahmen aus, wenn der Modus "Immer an" aktiviert ist.'); Alert.alert('Hinweis', 'Bitte wähle einen Rahmen aus, wenn der Modus "Immer an" aktiviert ist.');
@@ -238,7 +247,7 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
selectedFrameId: frameMode === 'always' ? selectedFrameId : null, selectedFrameId: frameMode === 'always' ? selectedFrameId : null,
availableFrameIds: frameMode === 'available' ? availableFrameIds : [], availableFrameIds: frameMode === 'available' ? availableFrameIds : [],
dateOverlay, dateOverlay,
dateOverlayPosition, dateOverlayTransform,
eventText: eventText.trim(), eventText: eventText.trim(),
burstCount: burst, burstCount: burst,
burstIntervalSec: interval, burstIntervalSec: interval,
@@ -257,7 +266,7 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
} }
}; };
const renderSegment = <T extends string>( const renderSegment = <T extends string | number>(
options: { value: T; label: string }[], options: { value: T; label: string }[],
current: T, current: T,
onChange: (v: T) => void, onChange: (v: T) => void,
@@ -330,12 +339,9 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
<ScrollView contentContainerStyle={styles.scrollContent}> <ScrollView contentContainerStyle={styles.scrollContent}>
<View style={styles.card}> <View style={styles.card}>
<View style={styles.cardHeader}> <View style={styles.cardHeader}>
<FontAwesomeIcon icon={faPrint} size={16} color={THEME.colors.accent} /> <FontAwesomeIcon icon={faUserGear} size={16} color={THEME.colors.accent} />
<Text style={styles.cardTitle}>Drucker-Konfiguration</Text> <Text style={styles.cardTitle}>System & Hardware</Text>
</View> </View>
<Text style={styles.cardDesc}>
Konfiguriere die lokale IP-Adresse deines über WLAN verbundenen Canon CP1300 Druckers.
</Text>
<View style={styles.inputGroup}> <View style={styles.inputGroup}>
<Text style={styles.label}>Drucker IP-Adresse</Text> <Text style={styles.label}>Drucker IP-Adresse</Text>
<TextInput <TextInput
@@ -347,24 +353,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
keyboardType="numeric" keyboardType="numeric"
/> />
</View> </View>
</View>
<View style={styles.card}>
<View style={styles.cardHeader}>
<FontAwesomeIcon icon={faCameraRetro} size={16} color={THEME.colors.accent} />
<Text style={styles.cardTitle}>Fotobox-Verhalten</Text>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Countdown-Dauer (Sekunden)</Text>
<TextInput
style={styles.input}
placeholder="Standard: 3"
placeholderTextColor={THEME.colors.textMuted}
value={countdown}
onChangeText={setCountdown}
keyboardType="number-pad"
/>
</View>
<View style={styles.inputGroup}> <View style={styles.inputGroup}>
<Text style={styles.label}>Admin-Passwort</Text> <Text style={styles.label}>Admin-Passwort</Text>
<TextInput <TextInput
@@ -378,6 +366,57 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
keyboardType="number-pad" keyboardType="number-pad"
/> />
</View> </View>
<View style={styles.toggleRow}>
<Text style={styles.toggleLabel}>Tablet-Frontkamera als Fallback erzwingen</Text>
<Switch
value={useFrontCamera}
onValueChange={setUseFrontCamera}
trackColor={{ false: THEME.colors.surfaceSecondary, true: THEME.colors.accent }}
thumbColor={THEME.colors.text}
/>
</View>
<View style={styles.toggleRow}>
<Text style={styles.toggleLabel}>App-Protokollierung (Logs) aktivieren</Text>
<Switch
value={enableLogs}
onValueChange={setEnableLogs}
trackColor={{ false: THEME.colors.surfaceSecondary, true: THEME.colors.accent }}
thumbColor={THEME.colors.text}
/>
</View>
<View style={styles.statusRow}>
<Text style={styles.statusLabel}>Kiosk-Modus Status:</Text>
<Text style={[styles.statusValue, kioskActive ? styles.statusActive : styles.statusInactive]}>
{kioskActive ? 'GESPERRT 🔒' : 'ENTSPERRT 🔓'}
</Text>
</View>
<View style={styles.statusRow}>
<Text style={styles.statusLabel}>Device-Owner-Modus:</Text>
<Text style={[styles.statusValue, isDeviceOwner ? styles.statusActive : styles.statusWarning]}>
{isDeviceOwner ? 'AKTIV (Echte Sperre) ✅' : 'INAKTIV (Screen Pinning Fallback) ⚠️'}
</Text>
</View>
{!isDeviceOwner && (
<Text style={styles.kioskWarningText}>
Hinweis: Um den echten Lock-Task-Modus ohne Bestätigungsaufforderung zu aktivieren, mache die App mittels ADB zum Device Owner.
</Text>
)}
<View style={styles.toggleRow}>
<Text style={styles.toggleLabel}>Bildschirm sperren (Kiosk-Modus)</Text>
<Switch
value={kioskActive}
onValueChange={handleKioskToggle}
trackColor={{ false: THEME.colors.surfaceSecondary, true: THEME.colors.accent }}
thumbColor={THEME.colors.text}
/>
</View>
</View>
<View style={styles.card}>
<View style={styles.cardHeader}>
<FontAwesomeIcon icon={faBorderAll} size={16} color={THEME.colors.accent} />
<Text style={styles.cardTitle}>Startbildschirm</Text>
</View>
<View style={styles.inputGroup}> <View style={styles.inputGroup}>
<Text style={styles.label}>Begrüßungstext</Text> <Text style={styles.label}>Begrüßungstext</Text>
<TextInput <TextInput
@@ -399,7 +438,7 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
/> />
</View> </View>
<View style={styles.inputGroup}> <View style={styles.inputGroup}>
<Text style={styles.label}>Event-Name (für Datum-Sticker)</Text> <Text style={styles.label}>Event-Name (wird auch im Datum-Sticker & Collage verwendet)</Text>
<TextInput <TextInput
style={styles.input} style={styles.input}
placeholder="z.B. Jannik's Party" placeholder="z.B. Jannik's Party"
@@ -412,62 +451,59 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
<View style={styles.card}> <View style={styles.card}>
<View style={styles.cardHeader}> <View style={styles.cardHeader}>
<FontAwesomeIcon icon={faImages} size={16} color={THEME.colors.accent} /> <FontAwesomeIcon icon={faCameraRetro} size={16} color={THEME.colors.accent} />
<Text style={styles.cardTitle}>Serienbild / Burst-Modus</Text> <Text style={styles.cardTitle}>Foto-Ablauf</Text>
</View> </View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Countdown-Dauer (Sekunden)</Text>
<TextInput
style={styles.input}
placeholder="Standard: 3"
placeholderTextColor={THEME.colors.textMuted}
value={countdown}
onChangeText={setCountdown}
keyboardType="number-pad"
/>
</View>
<Text style={styles.label}>Serienbild</Text>
<Text style={styles.cardDesc}> <Text style={styles.cardDesc}>
Mehrere Fotos automatisch hintereinander aufnehmen. Bei Anzahl = 1 wird nur ein einzelnes Foto aufgenommen. Mehrere Fotos automatisch hintereinander aufnehmen. Bei 1 Bild wird nur ein einzelnes Foto aufgenommen.
</Text> </Text>
<View style={styles.inputGroup}> <View style={styles.inputGroup}>
<Text style={styles.label}>Anzahl Fotos pro Durchgang (15)</Text> <Text style={styles.label}>Anzahl Fotos pro Durchgang</Text>
<TextInput {renderSegment(
style={styles.input} [
placeholder="Standard: 1" { value: 1, label: '1 Bild' },
placeholderTextColor={THEME.colors.textMuted} { value: 2, label: '2 Bilder' },
value={burstCount} { value: 3, label: '3 Bilder' },
onChangeText={setBurstCount} { value: 4, label: '4 Bilder' },
keyboardType="number-pad" { value: 5, label: '5 Bilder' },
/> ],
burstCount,
setBurstCount
)}
</View> </View>
<View style={styles.inputGroup}> <View style={styles.inputGroup}>
<Text style={styles.label}>Abstand zwischen Fotos (Sekunden, 330)</Text> <Text style={styles.label}>Abstand zwischen Fotos (Sekunden)</Text>
<TextInput {renderSegment(
style={styles.input} [
placeholder="Standard: 5" { value: 3, label: '3s' },
placeholderTextColor={THEME.colors.textMuted} { value: 5, label: '5s' },
value={burstInterval} { value: 10, label: '10s' },
onChangeText={setBurstInterval} { value: 15, label: '15s' },
keyboardType="number-pad" { value: 20, label: '20s' },
/> ],
</View> burstIntervalSec,
<View style={styles.toggleRow}> setBurstIntervalSec
<Text style={styles.toggleLabel}>Tablet-Frontkamera als Fallback erzwingen</Text> )}
<Switch
value={useFrontCamera}
onValueChange={setUseFrontCamera}
trackColor={{ false: THEME.colors.surfaceSecondary, true: THEME.colors.accent }}
thumbColor={THEME.colors.text}
/>
</View>
<View style={styles.toggleRow}>
<Text style={styles.toggleLabel}>App-Protokollierung (Logs) aktivieren</Text>
<Switch
value={enableLogs}
onValueChange={setEnableLogs}
trackColor={{ false: THEME.colors.surfaceSecondary, true: THEME.colors.accent }}
thumbColor={THEME.colors.text}
/>
</View> </View>
</View> </View>
<View style={styles.card}> <View style={styles.card}>
<View style={styles.cardHeader}> <View style={styles.cardHeader}>
<FontAwesomeIcon icon={faBorderAll} size={16} color={THEME.colors.accent} /> <FontAwesomeIcon icon={faImages} size={16} color={THEME.colors.accent} />
<Text style={styles.cardTitle}>Fotorahmen</Text> <Text style={styles.cardTitle}>Gestaltung: Fotorahmen</Text>
</View> </View>
<Text style={styles.cardDesc}>
Rahmen über die Fotos legen. "Aus" deaktiviert Rahmen, "Immer an" erzwingt einen gewählten Rahmen, "Verfügbar" lässt Gäste einen Rahmen auswählen.
</Text>
{renderSegment(FRAME_MODE_OPTIONS, frameMode, setFrameMode)} {renderSegment(FRAME_MODE_OPTIONS, frameMode, setFrameMode)}
{frameMode === 'always' && FRAMES.length > 0 && ( {frameMode === 'always' && FRAMES.length > 0 && (
@@ -528,71 +564,79 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
</View> </View>
</View> </View>
)} )}
{frameMode === 'always' && FRAMES.length === 0 && (
<Text style={styles.warningText}>
Noch keine Rahmen hinterlegt. Lege PNG-Dateien in assets/frames/ ab.
</Text>
)}
</View> </View>
<View style={styles.card}> <View style={styles.card}>
<View style={styles.cardHeader}> <View style={styles.cardHeader}>
<FontAwesomeIcon icon={faCalendarDay} size={16} color={THEME.colors.accent} /> <FontAwesomeIcon icon={faCalendarDay} size={16} color={THEME.colors.accent} />
<Text style={styles.cardTitle}>Datum/Uhrzeit-Einblendung</Text> <Text style={styles.cardTitle}>Gestaltung: Datum/Uhrzeit-Einblendung</Text>
</View> </View>
<Text style={styles.cardDesc}>
Datum automatisch auf jedes Foto einblenden. Unabhängig davon ist der Datum-Sticker in der Bearbeitung immer verfügbar.
</Text>
{renderSegment(DATE_OVERLAY_OPTIONS, dateOverlay, setDateOverlay)} {renderSegment(DATE_OVERLAY_OPTIONS, dateOverlay, setDateOverlay)}
{dateOverlay !== 'off' && ( {dateOverlay !== 'off' && (
<View style={styles.inputGroup}> <View style={styles.inputGroup}>
<Text style={styles.label}>Position der Einblendung</Text> <Text style={styles.label}>Vorschau: Positionieren & Drehen</Text>
{renderSegment(DATE_POSITION_OPTIONS, dateOverlayPosition, setDateOverlayPosition)}
</View>
)}
</View>
<View style={styles.card}>
<View style={styles.cardHeader}>
<FontAwesomeIcon icon={faLock} size={16} color={THEME.colors.accent} />
<Text style={styles.cardTitle}>Kiosk-Modus & Sicherheit</Text>
</View>
<Text style={styles.cardDesc}> <Text style={styles.cardDesc}>
Sperre den Bildschirm, um zu verhindern, dass Gäste die App schließen oder auf die Systemeinstellungen zugreifen. Ziehe den Text in der Vorschau mit dem Finger an die gewünschte Position. Passe Rotation und Größe mit den +/- Tasten an.
</Text> </Text>
<View style={styles.statusRow}> <View style={styles.previewBoxContainer}>
<Text style={styles.statusLabel}>Kiosk-Modus Status:</Text> <View style={styles.previewBox}>
<Text style={[styles.statusValue, kioskActive ? styles.statusActive : styles.statusInactive]}> <View
{kioskActive ? 'GESPERRT 🔒' : 'ENTSPERRT 🔓'} style={[
</Text> styles.previewTextWrapper,
</View> {
left: `${dateOverlayTransform.x}%`,
<View style={styles.statusRow}> top: `${dateOverlayTransform.y}%`,
<Text style={styles.statusLabel}>Device-Owner-Modus:</Text> transform: [
<Text style={[styles.statusValue, isDeviceOwner ? styles.statusActive : styles.statusWarning]}> { rotate: `${dateOverlayTransform.rotation}deg` },
{isDeviceOwner ? 'AKTIV (Echte Sperre) ✅' : 'INAKTIV (Screen Pinning Fallback) ⚠️'} { scale: dateOverlayTransform.scale }
</Text> ]
</View> }
]}
{!isDeviceOwner && ( {...panResponder.panHandlers}
<Text style={styles.kioskWarningText}> >
Hinweis: Um den echten Lock-Task-Modus ohne Bestätigungsaufforderung zu aktivieren, mache die App mittels ADB zum Device Owner. <Text style={styles.previewTextContent}>
{dateOverlay === 'date' ? '01.01.2026' : '01.01.2026 14:00'}
</Text> </Text>
{eventText.length > 0 && (
<Text style={styles.previewTextEvent}>{eventText}</Text>
)} )}
<View style={styles.toggleRow}>
<Text style={styles.toggleLabel}>Bildschirm sperren (Kiosk-Modus)</Text>
<Switch
value={kioskActive}
onValueChange={handleKioskToggle}
trackColor={{ false: THEME.colors.surfaceSecondary, true: THEME.colors.accent }}
thumbColor={THEME.colors.text}
/>
</View> </View>
</View> </View>
</View>
<View style={styles.controlRow}>
<View style={styles.controlColumn}>
<Text style={styles.label}>Rotation</Text>
<View style={styles.buttonRow}>
<TouchableOpacity style={styles.controlBtn} onPress={() => setDateOverlayTransform(p => ({ ...p, rotation: p.rotation - 5 }))}>
<Text style={styles.controlBtnText}>-</Text>
</TouchableOpacity>
<Text style={styles.controlValueText}>{dateOverlayTransform.rotation}°</Text>
<TouchableOpacity style={styles.controlBtn} onPress={() => setDateOverlayTransform(p => ({ ...p, rotation: p.rotation + 5 }))}>
<Text style={styles.controlBtnText}>+</Text>
</TouchableOpacity>
</View>
</View>
<View style={styles.controlColumn}>
<Text style={styles.label}>Größe</Text>
<View style={styles.buttonRow}>
<TouchableOpacity style={styles.controlBtn} onPress={() => setDateOverlayTransform(p => ({ ...p, scale: Math.max(0.5, p.scale - 0.1) }))}>
<Text style={styles.controlBtnText}>-</Text>
</TouchableOpacity>
<Text style={styles.controlValueText}>{dateOverlayTransform.scale.toFixed(1)}</Text>
<TouchableOpacity style={styles.controlBtn} onPress={() => setDateOverlayTransform(p => ({ ...p, scale: Math.min(3.0, p.scale + 0.1) }))}>
<Text style={styles.controlBtnText}>+</Text>
</TouchableOpacity>
</View>
</View>
</View>
</View>
)}
</View>
<TouchableOpacity style={styles.saveBtn} onPress={handleSave} activeOpacity={0.8}> <TouchableOpacity style={styles.saveBtn} onPress={handleSave} activeOpacity={0.8}>
<LinearGradient <LinearGradient
@@ -878,4 +922,70 @@ const styles = StyleSheet.create({
backgroundColor: 'rgba(255,255,255,0.1)', backgroundColor: 'rgba(255,255,255,0.1)',
borderRadius: 8, borderRadius: 8,
}, },
previewBoxContainer: {
alignItems: 'center',
marginVertical: THEME.spacing.md,
},
previewBox: {
width: 300,
height: 200,
backgroundColor: '#333',
borderWidth: 2,
borderColor: THEME.colors.border,
borderRadius: THEME.borderRadius.md,
overflow: 'hidden',
position: 'relative',
},
previewTextWrapper: {
position: 'absolute',
padding: 10,
backgroundColor: 'rgba(0,0,0,0.5)',
borderRadius: 8,
},
previewTextContent: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
textAlign: 'center',
},
previewTextEvent: {
color: '#aaa',
fontSize: 12,
textAlign: 'center',
marginTop: 2,
},
controlRow: {
flexDirection: 'row',
justifyContent: 'space-around',
marginTop: THEME.spacing.md,
},
controlColumn: {
alignItems: 'center',
},
buttonRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
controlBtn: {
width: 40,
height: 40,
backgroundColor: THEME.colors.surfaceSecondary,
borderWidth: 1,
borderColor: THEME.colors.border,
borderRadius: 20,
justifyContent: 'center',
alignItems: 'center',
},
controlBtnText: {
color: THEME.colors.text,
fontSize: 20,
fontWeight: 'bold',
},
controlValueText: {
color: THEME.colors.text,
fontSize: 16,
width: 40,
textAlign: 'center',
},
}); });
+7 -6
View File
@@ -111,10 +111,11 @@ export default function CameraScreen({
useEffect(() => { useEffect(() => {
if (countdown !== null) { if (countdown !== null) {
scale.value = 0.5; scale.value = 1;
scale.value = withSpring(1.25, { damping: 10, stiffness: 120 }, () => { scale.value = withSequence(
scale.value = withTiming(1.0, { duration: 150 }); withTiming(1.5, { duration: 100 }),
}); withSpring(1, { damping: 10, stiffness: 100 })
);
} }
}, [countdown]); }, [countdown]);
@@ -133,7 +134,7 @@ export default function CameraScreen({
// Turn off white flash effect // Turn off white flash effect
const turnFlashOff = useCallback(() => { const turnFlashOff = useCallback(() => {
flashOpacity.value = withTiming(0, { duration: 400 }); flashOpacity.value = withTiming(0, { duration: 800 });
}, []); }, []);
// 1. Request built-in camera permission on mount (just in case we fallback) // 1. Request built-in camera permission on mount (just in case we fallback)
@@ -319,7 +320,7 @@ export default function CameraScreen({
turnFlashOn(); turnFlashOn();
// Wait for UI to render the flash before blocking main thread with capture // Wait for UI to render the flash before blocking main thread with capture
await new Promise(resolve => setTimeout(resolve, 50)); await new Promise(resolve => setTimeout(resolve, 150));
let capturedUri: string; let capturedUri: string;
+13 -48
View File
@@ -36,7 +36,7 @@ interface PreviewScreenProps {
availableFrameIds: string[]; availableFrameIds: string[];
// Date overlay settings // Date overlay settings
dateOverlay: 'off' | 'date' | 'datetime'; dateOverlay: 'off' | 'date' | 'datetime';
dateOverlayPosition: string; dateOverlayTransform: { x: number; y: number; rotation: number; scale: number };
// Event text // Event text
eventText: string; eventText: string;
} }
@@ -53,7 +53,7 @@ export default function PreviewScreen({
initialFrameId, initialFrameId,
availableFrameIds, availableFrameIds,
dateOverlay, dateOverlay,
dateOverlayPosition, dateOverlayTransform,
eventText, eventText,
}: PreviewScreenProps) { }: PreviewScreenProps) {
const [layout, setLayout] = useState<CollageLayout>('single'); const [layout, setLayout] = useState<CollageLayout>('single');
@@ -176,19 +176,15 @@ export default function PreviewScreen({
// Get date overlay position styles // Get date overlay position styles
const getDatePositionStyle = () => { const getDatePositionStyle = () => {
switch (dateOverlayPosition) { return {
case 'bottom-left': position: 'absolute' as const,
return { bottom: 12, left: 12 }; left: `${dateOverlayTransform.x}%` as any,
case 'top-right': top: `${dateOverlayTransform.y}%` as any,
return { top: 12, right: 12 }; transform: [
case 'top-left': { rotate: `${dateOverlayTransform.rotation}deg` },
return { top: 12, left: 12 }; { scale: dateOverlayTransform.scale }
case 'bottom-center': ]
return { bottom: 12, left: 0, right: 0, alignItems: 'center' as const }; };
case 'bottom-right':
default:
return { bottom: 12, right: 12 };
}
}; };
// ── Sticker handlers ── // ── Sticker handlers ──
@@ -310,35 +306,7 @@ export default function PreviewScreen({
} }
}; };
// Saves to gallery without printing
const handleSaveOnly = async () => {
logger.log('Action: Clicked Save button');
if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
if (!mountedRef.current) return;
if (isProcessing) return;
setIsProcessing(true);
try {
const saveUri = await captureComposite(currentPhoto);
if (!mountedRef.current) return;
setStatusMessage('Bild wird generiert...');
setStatusMessage('Wird in Galerie gespeichert...');
await saveToGallery(saveUri);
if (!mountedRef.current) return;
setStatusMessage('Erfolgreich gespeichert!');
setTimeout(() => {
if (!mountedRef.current) return;
setIsProcessing(false);
resetIdleTimer();
}, 1500);
} catch (error: any) {
if (!mountedRef.current) return;
console.error('Saving failed:', error);
alert('Fehler: ' + error.message);
setIsProcessing(false);
}
};
const handleExitRef = useRef<(() => void) | null>(null); const handleExitRef = useRef<(() => void) | null>(null);
handleExitRef.current = async () => { handleExitRef.current = async () => {
@@ -389,7 +357,7 @@ export default function PreviewScreen({
if (idleTimerRef.current) clearTimeout(idleTimerRef.current); if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
idleTimerRef.current = setTimeout(() => { idleTimerRef.current = setTimeout(() => {
if (handleExitRef.current) handleExitRef.current(); if (handleExitRef.current) handleExitRef.current();
}, 60000); }, 120000);
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -681,10 +649,7 @@ export default function PreviewScreen({
</LinearGradient> </LinearGradient>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity style={[styles.actionBtn, styles.saveBtn, isProcessing && styles.btnDisabled]} onPress={handleSaveOnly} disabled={isProcessing}>
<FontAwesomeIcon icon={faDownload} size={16} color={THEME.colors.accent} style={{ marginRight: 8 }} />
<Text style={[styles.actionBtnText, { color: THEME.colors.accent }]}>Nur auf Tablet speichern</Text>
</TouchableOpacity>
{photoUris.length < 4 && ( {photoUris.length < 4 && (
<TouchableOpacity style={[styles.actionBtn, styles.addBtn, isProcessing && styles.btnDisabled]} onPress={onAddAnother} disabled={isProcessing}> <TouchableOpacity style={[styles.actionBtn, styles.addBtn, isProcessing && styles.btnDisabled]} onPress={onAddAnother} disabled={isProcessing}>
+2 -2
View File
@@ -16,7 +16,7 @@ export interface AppSettings {
availableFrameIds: string[]; availableFrameIds: string[];
// Date/Time overlay // Date/Time overlay
dateOverlay: 'off' | 'date' | 'datetime'; dateOverlay: 'off' | 'date' | 'datetime';
dateOverlayPosition: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' | 'bottom-center'; dateOverlayTransform: { x: number; y: number; rotation: number; scale: number };
// Event text (shown in date sticker & welcome) // Event text (shown in date sticker & welcome)
eventText: string; eventText: string;
// Continuous shooting / burst // Continuous shooting / burst
@@ -42,7 +42,7 @@ const DEFAULT_SETTINGS: AppSettings = {
selectedFrameId: null, selectedFrameId: null,
availableFrameIds: FRAMES.map(f => f.id), availableFrameIds: FRAMES.map(f => f.id),
dateOverlay: 'off', dateOverlay: 'off',
dateOverlayPosition: 'bottom-right', dateOverlayTransform: { x: 0, y: 0, rotation: 0, scale: 1 },
eventText: '', eventText: '',
burstCount: 1, burstCount: 1,
burstIntervalSec: 5, burstIntervalSec: 5,