import React, { useState, useEffect } from 'react'; import { StyleSheet, Text, View, TextInput, TouchableOpacity, ScrollView, Switch, Alert, Modal, } from 'react-native'; import { LinearGradient } from 'expo-linear-gradient'; import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; import { faUserGear, faArrowLeft, faPrint, faCameraRetro, faLock, faBorderAll, faCalendarDay, faImages, faFloppyDisk, faFileLines, } from '@fortawesome/free-solid-svg-icons'; import { THEME } from '../styles/theme'; import { logger } from '../services/logger'; import KioskMode from '../../modules/kiosk-mode'; import { AppSettings, saveSettings } from '../services/settings'; import { FRAMES } from '../data/frames'; interface AdminScreenProps { currentSettings: AppSettings; onSave: (settings: AppSettings) => void; onClose: () => void; } type FrameMode = 'off' | 'always' | 'available'; type DateOverlayMode = 'off' | 'date' | 'datetime'; type DatePosition = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' | 'bottom-center'; const FRAME_MODE_OPTIONS: { value: FrameMode; label: string }[] = [ { value: 'off', label: 'Aus' }, { value: 'always', label: 'Immer an' }, { value: 'available', label: 'Verfügbar' }, ]; const DATE_OVERLAY_OPTIONS: { value: DateOverlayMode; label: string }[] = [ { value: 'off', label: 'Aus' }, { value: 'date', label: 'Nur Datum' }, { 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)); const [printerIp, setPrinterIp] = useState(currentSettings.printerIp); const [password, setPassword] = useState(currentSettings.adminPassword); const [logs, setLogs] = useState(null); const [kioskActive, setKioskActive] = useState(false); const [isDeviceOwner, setIsDeviceOwner] = useState(false); const [frameMode, setFrameMode] = useState(currentSettings.frameMode); 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 [eventText, setEventText] = useState(currentSettings.eventText); const [burstCount, setBurstCount] = useState(String(currentSettings.burstCount)); const [burstInterval, setBurstInterval] = useState(String(currentSettings.burstIntervalSec)); const [welcomeText, setWelcomeText] = useState(currentSettings.welcomeText); useEffect(() => { const checkKioskStatus = () => { try { const active = KioskMode.isKioskActive(); const owner = KioskMode.isDeviceOwner(); setKioskActive(active); setIsDeviceOwner(owner); } catch (e) { console.warn('Failed to query native KioskMode module:', e); } }; checkKioskStatus(); const interval = setInterval(checkKioskStatus, 1000); return () => clearInterval(interval); }, []); const handleKioskToggle = (enable: boolean) => { try { if (enable) { const success = KioskMode.startKiosk(); if (success) { setKioskActive(true); Alert.alert( 'Kiosk-Modus gestartet', isDeviceOwner ? 'Echter Kiosk-Modus aktiv. Die System-Navigationsleiste ist vollständig gesperrt.' : 'Bildschirmheftung (Screen Pinning) gestartet. Bitte bestätige das Systemfenster, um den Bildschirm zu sperren.' ); } else { Alert.alert('Fehler', 'Kiosk-Modus konnte nicht gestartet werden.'); } } else { const success = KioskMode.stopKiosk(); if (success) { setKioskActive(false); Alert.alert('Kiosk-Modus beendet', 'Die Navigationstasten des Systems sind wieder freigegeben.'); } else { Alert.alert('Fehler', 'Kiosk-Modus konnte nicht beendet werden.'); } } } catch (e: any) { Alert.alert('Nativer Fehler', e.message || 'Fehler im Kiosk-Modul'); } }; const handleSave = async () => { const duration = parseInt(countdown, 10); if (isNaN(duration) || duration < 1 || duration > 30) { Alert.alert('Ungültige Eingabe', 'Die Countdown-Dauer muss eine Zahl zwischen 1 und 30 Sekunden sein.'); return; } const ipTrimmed = printerIp.trim(); const ipRegex = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/; if (!ipTrimmed) { Alert.alert('Ungültige Eingabe', 'Die Drucker-IP-Adresse darf nicht leer sein.'); return; } if (!ipRegex.test(ipTrimmed)) { Alert.alert('Ungültige Eingabe', 'Bitte gib eine gültige IP-Adresse ein (z.B. 192.168.1.100).'); return; } if (!password.trim() || password.length < 4) { Alert.alert('Ungültige Eingabe', 'Das Admin-Passwort muss mindestens 4 Ziffern lang sein.'); 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; } if (frameMode === 'always' && selectedFrameId === null && FRAMES.length > 0) { Alert.alert('Hinweis', 'Bitte wähle einen Rahmen aus, wenn der Modus "Immer an" aktiviert ist.'); return; } if (frameMode === 'available' && availableFrameIds.length === 0 && FRAMES.length > 0) { Alert.alert('Hinweis', 'Bitte wähle mindestens einen Rahmen aus, der verfügbar sein soll.'); return; } const updated: AppSettings = { countdownDuration: duration, printerIp: ipTrimmed, adminPassword: password.trim(), kioskModeEnabled: kioskActive, frameMode, selectedFrameId: frameMode === 'always' ? selectedFrameId : null, availableFrameIds: frameMode === 'available' ? availableFrameIds : [], dateOverlay, dateOverlayPosition, eventText: eventText.trim(), burstCount: burst, burstIntervalSec: interval, welcomeText: welcomeText.trim() || 'Willkommen zu unserer Feier!', }; try { await saveSettings(updated); onSave(updated); Alert.alert('Erfolg', 'Einstellungen erfolgreich gespeichert!'); } catch (e) { Alert.alert('Fehler', 'Einstellungen konnten nicht gespeichert werden.'); } }; const renderSegment = ( options: { value: T; label: string }[], current: T, onChange: (v: T) => void, ) => ( {options.map((opt) => ( onChange(opt.value)} > {opt.label} ))} ); return ( EINSTELLUNGEN Zurück zur Fotobox setLogs(await logger.readLogs())} > Logs Drucker-Konfiguration Konfiguriere die lokale IP-Adresse deines über WLAN verbundenen Canon CP1300 Druckers. Drucker IP-Adresse Fotobox-Verhalten Countdown-Dauer (Sekunden) Admin-Passwort Begrüßungstext Event-Name (für Datum-Sticker) Serienbild / Burst-Modus Mehrere Fotos automatisch hintereinander aufnehmen. Bei Anzahl = 1 wird nur ein einzelnes Foto aufgenommen. Anzahl Fotos pro Durchgang (1–5) Abstand zwischen Fotos (Sekunden, 3–30) 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 && ( Ausgewählter Rahmen {FRAMES.map((frame) => ( setSelectedFrameId(frame.id)} > {frame.name} ))} )} {frameMode === 'available' && FRAMES.length > 0 && ( Verfügbare Rahmen auswählen {FRAMES.map((frame) => { const isSelected = availableFrameIds.includes(frame.id); return ( { if (isSelected) { setAvailableFrameIds((prev) => prev.filter((id) => id !== frame.id)); } else { setAvailableFrameIds((prev) => [...prev, frame.id]); } }} > {frame.name} ); })} )} {frameMode === 'always' && FRAMES.length === 0 && ( Noch keine Rahmen hinterlegt. Lege PNG-Dateien in assets/frames/ ab. )} 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)} )} 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) EINSTELLUNGEN SPEICHERN {/* Log Modal */} App Logs { try { const { Share } = require('react-native'); await Share.share({ message: logs || '' }); } catch (e) { console.log('Share error', e); } }} > Teilen / Kopieren { this.scrollView = ref }} onContentSizeChange={() => this.scrollView?.scrollToEnd({animated: true})} > {logs} setLogs(null)} > Schließen { await logger.clearLogs(); setLogs(await logger.readLogs()); }} > Leeren ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: THEME.colors.background, }, headerRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: THEME.spacing.lg, paddingHorizontal: THEME.spacing.xl, borderBottomWidth: 1, borderColor: THEME.colors.border, backgroundColor: THEME.colors.surface, }, headerLeft: { flexDirection: 'row', alignItems: 'center', gap: 12, }, title: { fontSize: 22, fontWeight: '900', color: THEME.colors.text, letterSpacing: 2, }, closeBtn: { flexDirection: 'row', alignItems: 'center', gap: 8, backgroundColor: THEME.colors.surfaceSecondary, borderWidth: 1, borderColor: THEME.colors.border, paddingVertical: THEME.spacing.sm, paddingHorizontal: THEME.spacing.lg, borderRadius: THEME.borderRadius.round, }, closeBtnText: { color: THEME.colors.text, fontSize: 14, fontWeight: 'bold', }, scrollContent: { padding: THEME.spacing.xl, alignItems: 'center', }, card: { width: '100%', maxWidth: 680, backgroundColor: THEME.colors.surface, borderRadius: THEME.borderRadius.md, borderWidth: 1, borderColor: THEME.colors.border, padding: THEME.spacing.lg, marginBottom: THEME.spacing.lg, }, cardHeader: { flexDirection: 'row', alignItems: 'center', gap: 10, marginBottom: THEME.spacing.xs, }, cardTitle: { fontSize: 18, fontWeight: 'bold', color: THEME.colors.text, }, cardDesc: { fontSize: 14, color: THEME.colors.textMuted, marginBottom: THEME.spacing.md, }, inputGroup: { marginBottom: THEME.spacing.md, }, label: { fontSize: 14, color: THEME.colors.text, marginBottom: THEME.spacing.xs, }, input: { height: 48, backgroundColor: THEME.colors.surfaceSecondary, borderWidth: 1, borderColor: THEME.colors.border, borderRadius: THEME.borderRadius.sm, color: THEME.colors.text, paddingHorizontal: THEME.spacing.md, fontSize: 16, }, segmentRow: { flexDirection: 'row', backgroundColor: THEME.colors.surfaceSecondary, borderRadius: THEME.borderRadius.sm, borderWidth: 1, borderColor: THEME.colors.border, marginBottom: THEME.spacing.md, overflow: 'hidden', }, segmentBtn: { flex: 1, paddingVertical: 10, alignItems: 'center', justifyContent: 'center', }, segmentBtnActive: { backgroundColor: THEME.colors.accent, }, segmentText: { fontSize: 13, fontWeight: '600', color: THEME.colors.textMuted, }, segmentTextActive: { color: THEME.colors.text, }, frameChipRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginTop: THEME.spacing.xs, }, frameChip: { paddingVertical: 6, paddingHorizontal: 14, borderRadius: THEME.borderRadius.round, borderWidth: 1, borderColor: THEME.colors.border, backgroundColor: THEME.colors.surfaceSecondary, }, frameChipActive: { borderColor: THEME.colors.accent, backgroundColor: 'rgba(40, 223, 255, 0.12)', }, frameChipText: { fontSize: 13, color: THEME.colors.textMuted, fontWeight: '600', }, frameChipTextActive: { color: THEME.colors.accent, }, warningText: { fontSize: 12, color: THEME.colors.error, fontStyle: 'italic', marginTop: THEME.spacing.xs, }, statusRow: { flexDirection: 'row', marginBottom: THEME.spacing.xs, }, statusLabel: { fontSize: 14, color: THEME.colors.textMuted, marginRight: THEME.spacing.sm, }, statusValue: { fontSize: 14, fontWeight: 'bold', }, statusActive: { color: THEME.colors.success, }, statusInactive: { color: THEME.colors.textMuted, }, statusWarning: { color: THEME.colors.error, }, kioskWarningText: { fontSize: 12, color: THEME.colors.textMuted, marginTop: THEME.spacing.xs, fontStyle: 'italic', }, toggleRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginTop: THEME.spacing.lg, paddingTop: THEME.spacing.md, borderTopWidth: 1, borderTopColor: THEME.colors.border, }, toggleLabel: { fontSize: 16, fontWeight: '600', color: THEME.colors.text, }, saveBtn: { width: '100%', maxWidth: 680, height: 52, overflow: 'hidden', borderRadius: THEME.borderRadius.md, marginTop: THEME.spacing.sm, marginBottom: THEME.spacing.xl, shadowColor: THEME.colors.gradientGlow, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.5, shadowRadius: 14, elevation: 8, }, saveGradient: { flex: 1, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', }, saveBtnText: { color: THEME.colors.text, fontSize: 16, fontWeight: 'bold', letterSpacing: 2, }, logBtn: { flexDirection: 'row', alignItems: 'center', padding: 10, backgroundColor: 'rgba(255,255,255,0.1)', borderRadius: 8, }, });