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}
availableFrameIds={settings.availableFrameIds}
dateOverlay={settings.dateOverlay}
dateOverlayPosition={settings.dateOverlayPosition}
dateOverlayTransform={settings.dateOverlayTransform}
eventText={settings.eventText}
/>
</View>
+2 -1
View File
@@ -3,7 +3,7 @@
"newArchEnabled": true,
"name": "Schnappix",
"slug": "Schnappix",
"version": "1.0.0",
"version": "1.0.1",
"orientation": "landscape",
"icon": "./Icon.png",
"userInterfaceStyle": "dark",
@@ -13,6 +13,7 @@
},
"android": {
"package": "com.schnappix",
"versionCode": 2,
"adaptiveIcon": {
"backgroundColor": "#050510",
"foregroundImage": "./Icon-padded.png"
+257 -147
View File
@@ -10,6 +10,7 @@ import {
Alert,
Modal,
BackHandler,
PanResponder,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
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' },
];
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) {
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 [availableFrameIds, setAvailableFrameIds] = useState<string[]>(currentSettings.availableFrameIds || []);
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 [burstCount, setBurstCount] = useState<string>(String(currentSettings.burstCount));
const [burstInterval, setBurstInterval] = useState<string>(String(currentSettings.burstIntervalSec));
const [burstCount, setBurstCount] = useState<number>(currentSettings.burstCount);
const [burstIntervalSec, setBurstIntervalSec] = useState<number>(currentSettings.burstIntervalSec);
const [welcomeText, setWelcomeText] = useState<string>(currentSettings.welcomeText);
const [footerText, setFooterText] = useState<string>(currentSettings.footerText);
const [useFrontCamera, setUseFrontCamera] = useState<boolean>(currentSettings.useFrontCamera);
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 = () => {
return (
countdown !== String(currentSettings.countdownDuration) ||
@@ -96,10 +114,10 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
selectedFrameId !== currentSettings.selectedFrameId ||
availableFrameIds.join() !== (currentSettings.availableFrameIds || []).join() ||
dateOverlay !== currentSettings.dateOverlay ||
dateOverlayPosition !== currentSettings.dateOverlayPosition ||
JSON.stringify(dateOverlayTransform) !== JSON.stringify(currentSettings.dateOverlayTransform) ||
eventText !== currentSettings.eventText ||
burstCount !== String(currentSettings.burstCount) ||
burstInterval !== String(currentSettings.burstIntervalSec) ||
burstCount !== currentSettings.burstCount ||
burstIntervalSec !== currentSettings.burstIntervalSec ||
welcomeText !== currentSettings.welcomeText ||
footerText !== currentSettings.footerText ||
useFrontCamera !== currentSettings.useFrontCamera ||
@@ -129,7 +147,7 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
return () => subscription.remove();
}, [
countdown, printerIp, password, kioskActive, frameMode, selectedFrameId, availableFrameIds,
dateOverlay, dateOverlayPosition, eventText, burstCount, burstInterval, welcomeText,
dateOverlay, dateOverlayTransform, eventText, burstCount, burstIntervalSec, welcomeText,
footerText, useFrontCamera, enableLogs, currentSettings
]);
@@ -206,17 +224,8 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
return;
}
const burst = parseInt(burstCount, 10);
if (isNaN(burst) || burst < 1 || burst > 5) {
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;
}
const burst = burstCount;
const interval = burstIntervalSec;
if (frameMode === 'always' && selectedFrameId === null && FRAMES.length > 0) {
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,
availableFrameIds: frameMode === 'available' ? availableFrameIds : [],
dateOverlay,
dateOverlayPosition,
dateOverlayTransform,
eventText: eventText.trim(),
burstCount: burst,
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 }[],
current: T,
onChange: (v: T) => void,
@@ -330,12 +339,9 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
<ScrollView contentContainerStyle={styles.scrollContent}>
<View style={styles.card}>
<View style={styles.cardHeader}>
<FontAwesomeIcon icon={faPrint} size={16} color={THEME.colors.accent} />
<Text style={styles.cardTitle}>Drucker-Konfiguration</Text>
<FontAwesomeIcon icon={faUserGear} size={16} color={THEME.colors.accent} />
<Text style={styles.cardTitle}>System & Hardware</Text>
</View>
<Text style={styles.cardDesc}>
Konfiguriere die lokale IP-Adresse deines über WLAN verbundenen Canon CP1300 Druckers.
</Text>
<View style={styles.inputGroup}>
<Text style={styles.label}>Drucker IP-Adresse</Text>
<TextInput
@@ -347,24 +353,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
keyboardType="numeric"
/>
</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}>
<Text style={styles.label}>Admin-Passwort</Text>
<TextInput
@@ -378,6 +366,57 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
keyboardType="number-pad"
/>
</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}>
<Text style={styles.label}>Begrüßungstext</Text>
<TextInput
@@ -399,7 +438,7 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
/>
</View>
<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
style={styles.input}
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.cardHeader}>
<FontAwesomeIcon icon={faImages} size={16} color={THEME.colors.accent} />
<Text style={styles.cardTitle}>Serienbild / Burst-Modus</Text>
<FontAwesomeIcon icon={faCameraRetro} size={16} color={THEME.colors.accent} />
<Text style={styles.cardTitle}>Foto-Ablauf</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>
<Text style={styles.label}>Serienbild</Text>
<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>
<View style={styles.inputGroup}>
<Text style={styles.label}>Anzahl Fotos pro Durchgang (15)</Text>
<TextInput
style={styles.input}
placeholder="Standard: 1"
placeholderTextColor={THEME.colors.textMuted}
value={burstCount}
onChangeText={setBurstCount}
keyboardType="number-pad"
/>
<Text style={styles.label}>Anzahl Fotos pro Durchgang</Text>
{renderSegment(
[
{ value: 1, label: '1 Bild' },
{ value: 2, label: '2 Bilder' },
{ value: 3, label: '3 Bilder' },
{ value: 4, label: '4 Bilder' },
{ value: 5, label: '5 Bilder' },
],
burstCount,
setBurstCount
)}
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Abstand zwischen Fotos (Sekunden, 330)</Text>
<TextInput
style={styles.input}
placeholder="Standard: 5"
placeholderTextColor={THEME.colors.textMuted}
value={burstInterval}
onChangeText={setBurstInterval}
keyboardType="number-pad"
/>
</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}
/>
<Text style={styles.label}>Abstand zwischen Fotos (Sekunden)</Text>
{renderSegment(
[
{ value: 3, label: '3s' },
{ value: 5, label: '5s' },
{ value: 10, label: '10s' },
{ value: 15, label: '15s' },
{ value: 20, label: '20s' },
],
burstIntervalSec,
setBurstIntervalSec
)}
</View>
</View>
<View style={styles.card}>
<View style={styles.cardHeader}>
<FontAwesomeIcon icon={faBorderAll} size={16} color={THEME.colors.accent} />
<Text style={styles.cardTitle}>Fotorahmen</Text>
<FontAwesomeIcon icon={faImages} size={16} color={THEME.colors.accent} />
<Text style={styles.cardTitle}>Gestaltung: Fotorahmen</Text>
</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)}
{frameMode === 'always' && FRAMES.length > 0 && (
@@ -528,72 +564,80 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
</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 style={styles.card}>
<View style={styles.cardHeader}>
<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>
<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)}
{dateOverlay !== 'off' && (
<View style={styles.inputGroup}>
<Text style={styles.label}>Position der Einblendung</Text>
{renderSegment(DATE_POSITION_OPTIONS, dateOverlayPosition, setDateOverlayPosition)}
<Text style={styles.label}>Vorschau: Positionieren & Drehen</Text>
<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 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}>
<LinearGradient
colors={THEME.gradient.primary}
@@ -878,4 +922,70 @@ const styles = StyleSheet.create({
backgroundColor: 'rgba(255,255,255,0.1)',
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(() => {
if (countdown !== null) {
scale.value = 0.5;
scale.value = withSpring(1.25, { damping: 10, stiffness: 120 }, () => {
scale.value = withTiming(1.0, { duration: 150 });
});
scale.value = 1;
scale.value = withSequence(
withTiming(1.5, { duration: 100 }),
withSpring(1, { damping: 10, stiffness: 100 })
);
}
}, [countdown]);
@@ -133,7 +134,7 @@ export default function CameraScreen({
// Turn off white flash effect
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)
@@ -319,7 +320,7 @@ export default function CameraScreen({
turnFlashOn();
// 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;
+13 -48
View File
@@ -36,7 +36,7 @@ interface PreviewScreenProps {
availableFrameIds: string[];
// Date overlay settings
dateOverlay: 'off' | 'date' | 'datetime';
dateOverlayPosition: string;
dateOverlayTransform: { x: number; y: number; rotation: number; scale: number };
// Event text
eventText: string;
}
@@ -53,7 +53,7 @@ export default function PreviewScreen({
initialFrameId,
availableFrameIds,
dateOverlay,
dateOverlayPosition,
dateOverlayTransform,
eventText,
}: PreviewScreenProps) {
const [layout, setLayout] = useState<CollageLayout>('single');
@@ -176,19 +176,15 @@ export default function PreviewScreen({
// Get date overlay position styles
const getDatePositionStyle = () => {
switch (dateOverlayPosition) {
case 'bottom-left':
return { bottom: 12, left: 12 };
case 'top-right':
return { top: 12, right: 12 };
case 'top-left':
return { top: 12, left: 12 };
case 'bottom-center':
return { bottom: 12, left: 0, right: 0, alignItems: 'center' as const };
case 'bottom-right':
default:
return { bottom: 12, right: 12 };
}
return {
position: 'absolute' as const,
left: `${dateOverlayTransform.x}%` as any,
top: `${dateOverlayTransform.y}%` as any,
transform: [
{ rotate: `${dateOverlayTransform.rotation}deg` },
{ scale: dateOverlayTransform.scale }
]
};
};
// ── 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);
handleExitRef.current = async () => {
@@ -389,7 +357,7 @@ export default function PreviewScreen({
if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
idleTimerRef.current = setTimeout(() => {
if (handleExitRef.current) handleExitRef.current();
}, 60000);
}, 120000);
}, []);
useEffect(() => {
@@ -681,10 +649,7 @@ export default function PreviewScreen({
</LinearGradient>
</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 && (
<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[];
// Date/Time overlay
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)
eventText: string;
// Continuous shooting / burst
@@ -42,7 +42,7 @@ const DEFAULT_SETTINGS: AppSettings = {
selectedFrameId: null,
availableFrameIds: FRAMES.map(f => f.id),
dateOverlay: 'off',
dateOverlayPosition: 'bottom-right',
dateOverlayTransform: { x: 0, y: 0, rotation: 0, scale: 1 },
eventText: '',
burstCount: 1,
burstIntervalSec: 5,