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"
+253 -143
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,71 +564,79 @@ 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)}
</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.label}>Vorschau: Positionieren & Drehen</Text>
<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>
<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.
<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 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 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}>
<LinearGradient
@@ -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,