diff --git a/App.tsx b/App.tsx index a3037ee4..19daf74d 100644 --- a/App.tsx +++ b/App.tsx @@ -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} /> diff --git a/app.json b/app.json index b28f61ca..5c4c581a 100644 --- a/app.json +++ b/app.json @@ -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" diff --git a/src/screens/AdminScreen.tsx b/src/screens/AdminScreen.tsx index 2607ad84..b6d5742c 100644 --- a/src/screens/AdminScreen.tsx +++ b/src/screens/AdminScreen.tsx @@ -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(currentSettings.countdownDuration)); @@ -77,15 +72,38 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS const [selectedFrameId, setSelectedFrameId] = useState(currentSettings.selectedFrameId); const [availableFrameIds, setAvailableFrameIds] = useState(currentSettings.availableFrameIds || []); const [dateOverlay, setDateOverlay] = useState(currentSettings.dateOverlay); - const [dateOverlayPosition, setDateOverlayPosition] = useState(currentSettings.dateOverlayPosition); + const [dateOverlayTransform, setDateOverlayTransform] = useState(currentSettings.dateOverlayTransform); const [eventText, setEventText] = useState(currentSettings.eventText); - const [burstCount, setBurstCount] = useState(String(currentSettings.burstCount)); - const [burstInterval, setBurstInterval] = useState(String(currentSettings.burstIntervalSec)); + const [burstCount, setBurstCount] = useState(currentSettings.burstCount); + const [burstIntervalSec, setBurstIntervalSec] = useState(currentSettings.burstIntervalSec); const [welcomeText, setWelcomeText] = useState(currentSettings.welcomeText); const [footerText, setFooterText] = useState(currentSettings.footerText); const [useFrontCamera, setUseFrontCamera] = useState(currentSettings.useFrontCamera); const [enableLogs, setEnableLogs] = useState(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 = ( + const renderSegment = ( options: { value: T; label: string }[], current: T, onChange: (v: T) => void, @@ -330,12 +339,9 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS - - Drucker-Konfiguration + + System & Hardware - - Konfiguriere die lokale IP-Adresse deines über WLAN verbundenen Canon CP1300 Druckers. - Drucker IP-Adresse - - - - - - Fotobox-Verhalten - - - Countdown-Dauer (Sekunden) - - Admin-Passwort + + Tablet-Frontkamera als Fallback erzwingen + + + + App-Protokollierung (Logs) aktivieren + + + + Kiosk-Modus Status: + + {kioskActive ? 'GESPERRT 🔒' : 'ENTSPERRT 🔓'} + + + + Device-Owner-Modus: + + {isDeviceOwner ? 'AKTIV (Echte Sperre) ✅' : 'INAKTIV (Screen Pinning Fallback) ⚠️'} + + + {!isDeviceOwner && ( + + Hinweis: Um den echten Lock-Task-Modus ohne Bestätigungsaufforderung zu aktivieren, mache die App mittels ADB zum Device Owner. + + )} + + Bildschirm sperren (Kiosk-Modus) + + + + + + + + Startbildschirm + Begrüßungstext - Event-Name (für Datum-Sticker) + Event-Name (wird auch im Datum-Sticker & Collage verwendet) - - Serienbild / Burst-Modus + + Foto-Ablauf + + Countdown-Dauer (Sekunden) + + + Serienbild - 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. - Anzahl Fotos pro Durchgang (1–5) - + Anzahl Fotos pro Durchgang + {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 + )} - Abstand zwischen Fotos (Sekunden, 3–30) - - - - Tablet-Frontkamera als Fallback erzwingen - - - - App-Protokollierung (Logs) aktivieren - + Abstand zwischen Fotos (Sekunden) + {renderSegment( + [ + { value: 3, label: '3s' }, + { value: 5, label: '5s' }, + { value: 10, label: '10s' }, + { value: 15, label: '15s' }, + { value: 20, label: '20s' }, + ], + burstIntervalSec, + setBurstIntervalSec + )} - - Fotorahmen + + Gestaltung: Fotorahmen - - 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. - {renderSegment(FRAME_MODE_OPTIONS, frameMode, setFrameMode)} {frameMode === 'always' && FRAMES.length > 0 && ( @@ -528,72 +564,80 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS )} - - {frameMode === 'always' && FRAMES.length === 0 && ( - - Noch keine Rahmen hinterlegt. Lege PNG-Dateien in assets/frames/ ab. - - )} - Datum/Uhrzeit-Einblendung + Gestaltung: Datum/Uhrzeit-Einblendung - - Datum automatisch auf jedes Foto einblenden. Unabhängig davon ist der Datum-Sticker in der Bearbeitung immer verfügbar. - {renderSegment(DATE_OVERLAY_OPTIONS, dateOverlay, setDateOverlay)} {dateOverlay !== 'off' && ( - Position der Einblendung - {renderSegment(DATE_POSITION_OPTIONS, dateOverlayPosition, setDateOverlayPosition)} + Vorschau: Positionieren & Drehen + + Ziehe den Text in der Vorschau mit dem Finger an die gewünschte Position. Passe Rotation und Größe mit den +/- Tasten an. + + + + + + + {dateOverlay === 'date' ? '01.01.2026' : '01.01.2026 14:00'} + + {eventText.length > 0 && ( + {eventText} + )} + + + + + + + Rotation + + setDateOverlayTransform(p => ({ ...p, rotation: p.rotation - 5 }))}> + - + + {dateOverlayTransform.rotation}° + setDateOverlayTransform(p => ({ ...p, rotation: p.rotation + 5 }))}> + + + + + + + + Größe + + setDateOverlayTransform(p => ({ ...p, scale: Math.max(0.5, p.scale - 0.1) }))}> + - + + {dateOverlayTransform.scale.toFixed(1)} + setDateOverlayTransform(p => ({ ...p, scale: Math.min(3.0, p.scale + 0.1) }))}> + + + + + + + )} - - - - Kiosk-Modus & Sicherheit - - - Sperre den Bildschirm, um zu verhindern, dass Gäste die App schließen oder auf die Systemeinstellungen zugreifen. - - - - Kiosk-Modus Status: - - {kioskActive ? 'GESPERRT 🔒' : 'ENTSPERRT 🔓'} - - - - - Device-Owner-Modus: - - {isDeviceOwner ? 'AKTIV (Echte Sperre) ✅' : 'INAKTIV (Screen Pinning Fallback) ⚠️'} - - - - {!isDeviceOwner && ( - - Hinweis: Um den echten Lock-Task-Modus ohne Bestätigungsaufforderung zu aktivieren, mache die App mittels ADB zum Device Owner. - - )} - - - Bildschirm sperren (Kiosk-Modus) - - - - { 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; diff --git a/src/screens/PreviewScreen.tsx b/src/screens/PreviewScreen.tsx index e23e01c3..3399af7e 100644 --- a/src/screens/PreviewScreen.tsx +++ b/src/screens/PreviewScreen.tsx @@ -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('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({ - - - Nur auf Tablet speichern - + {photoUris.length < 4 && ( diff --git a/src/services/settings.ts b/src/services/settings.ts index 180f1a83..d614cd83 100644 --- a/src/services/settings.ts +++ b/src/services/settings.ts @@ -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,