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"
+257 -147
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,72 +564,80 @@ 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)} <Text style={styles.cardDesc}>
Ziehe den Text in der Vorschau mit dem Finger an die gewünschte Position. Passe Rotation und Größe mit den +/- Tasten an.
</Text>
<View style={styles.previewBoxContainer}>
<View style={styles.previewBox}>
<View
style={[
styles.previewTextWrapper,
{
left: `${dateOverlayTransform.x}%`,
top: `${dateOverlayTransform.y}%`,
transform: [
{ rotate: `${dateOverlayTransform.rotation}deg` },
{ scale: dateOverlayTransform.scale }
]
}
]}
{...panResponder.panHandlers}
>
<Text style={styles.previewTextContent}>
{dateOverlay === 'date' ? '01.01.2026' : '01.01.2026 14:00'}
</Text>
{eventText.length > 0 && (
<Text style={styles.previewTextEvent}>{eventText}</Text>
)}
</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>
)} )}
</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}>
Sperre den Bildschirm, um zu verhindern, dass Gäste die App schließen oder auf die Systemeinstellungen zugreifen.
</Text>
<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>
<TouchableOpacity style={styles.saveBtn} onPress={handleSave} activeOpacity={0.8}> <TouchableOpacity style={styles.saveBtn} onPress={handleSave} activeOpacity={0.8}>
<LinearGradient <LinearGradient
colors={THEME.gradient.primary} colors={THEME.gradient.primary}
@@ -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,