762 lines
26 KiB
TypeScript
762 lines
26 KiB
TypeScript
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>(String(currentSettings.countdownDuration));
|
||
const [printerIp, setPrinterIp] = useState<string>(currentSettings.printerIp);
|
||
const [password, setPassword] = useState<string>(currentSettings.adminPassword);
|
||
const [logs, setLogs] = useState<string | null>(null);
|
||
|
||
const [kioskActive, setKioskActive] = useState<boolean>(false);
|
||
const [isDeviceOwner, setIsDeviceOwner] = useState<boolean>(false);
|
||
|
||
const [frameMode, setFrameMode] = useState<FrameMode>(currentSettings.frameMode);
|
||
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 [eventText, setEventText] = useState<string>(currentSettings.eventText);
|
||
const [burstCount, setBurstCount] = useState<string>(String(currentSettings.burstCount));
|
||
const [burstInterval, setBurstInterval] = useState<string>(String(currentSettings.burstIntervalSec));
|
||
const [welcomeText, setWelcomeText] = useState<string>(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 = <T extends string>(
|
||
options: { value: T; label: string }[],
|
||
current: T,
|
||
onChange: (v: T) => void,
|
||
) => (
|
||
<View style={styles.segmentRow}>
|
||
{options.map((opt) => (
|
||
<TouchableOpacity
|
||
key={opt.value}
|
||
style={[styles.segmentBtn, current === opt.value && styles.segmentBtnActive]}
|
||
onPress={() => onChange(opt.value)}
|
||
>
|
||
<Text style={[styles.segmentText, current === opt.value && styles.segmentTextActive]}>
|
||
{opt.label}
|
||
</Text>
|
||
</TouchableOpacity>
|
||
))}
|
||
</View>
|
||
);
|
||
|
||
return (
|
||
<Animated.View entering={FadeIn.duration(400)} exiting={FadeOut.duration(300)} style={styles.container}>
|
||
<View style={styles.headerRow}>
|
||
<View style={styles.headerLeft}>
|
||
<FontAwesomeIcon icon={faUserGear} size={22} color={THEME.colors.accent} />
|
||
<Text style={styles.title}>EINSTELLUNGEN</Text>
|
||
</View>
|
||
<TouchableOpacity style={styles.closeBtn} onPress={onClose}>
|
||
<FontAwesomeIcon icon={faArrowLeft} size={14} color={THEME.colors.text} />
|
||
<Text style={styles.closeBtnText}>Zurück zur Fotobox</Text>
|
||
</TouchableOpacity>
|
||
<TouchableOpacity
|
||
style={styles.logBtn}
|
||
onPress={async () => setLogs(await logger.readLogs())}
|
||
>
|
||
<FontAwesomeIcon icon={faFileLines} size={16} color={THEME.colors.text} />
|
||
<Text style={styles.closeBtnText}>Logs</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
|
||
<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>
|
||
</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
|
||
style={styles.input}
|
||
placeholder="z.B. 192.168.1.100"
|
||
placeholderTextColor={THEME.colors.textMuted}
|
||
value={printerIp}
|
||
onChangeText={setPrinterIp}
|
||
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
|
||
style={styles.input}
|
||
placeholder="Standard: 1234"
|
||
placeholderTextColor={THEME.colors.textMuted}
|
||
secureTextEntry
|
||
value={password}
|
||
onChangeText={setPassword}
|
||
keyboardType="number-pad"
|
||
/>
|
||
</View>
|
||
<View style={styles.inputGroup}>
|
||
<Text style={styles.label}>Begrüßungstext</Text>
|
||
<TextInput
|
||
style={styles.input}
|
||
placeholder="Willkommen zu unserer Feier!"
|
||
placeholderTextColor={THEME.colors.textMuted}
|
||
value={welcomeText}
|
||
onChangeText={setWelcomeText}
|
||
/>
|
||
</View>
|
||
<View style={styles.inputGroup}>
|
||
<Text style={styles.label}>Event-Name (für Datum-Sticker)</Text>
|
||
<TextInput
|
||
style={styles.input}
|
||
placeholder="z.B. Jannik's Party"
|
||
placeholderTextColor={THEME.colors.textMuted}
|
||
value={eventText}
|
||
onChangeText={setEventText}
|
||
/>
|
||
</View>
|
||
</View>
|
||
|
||
<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>
|
||
</View>
|
||
<Text style={styles.cardDesc}>
|
||
Mehrere Fotos automatisch hintereinander aufnehmen. Bei Anzahl = 1 wird nur ein einzelnes Foto aufgenommen.
|
||
</Text>
|
||
<View style={styles.inputGroup}>
|
||
<Text style={styles.label}>Anzahl Fotos pro Durchgang (1–5)</Text>
|
||
<TextInput
|
||
style={styles.input}
|
||
placeholder="Standard: 1"
|
||
placeholderTextColor={THEME.colors.textMuted}
|
||
value={burstCount}
|
||
onChangeText={setBurstCount}
|
||
keyboardType="number-pad"
|
||
/>
|
||
</View>
|
||
<View style={styles.inputGroup}>
|
||
<Text style={styles.label}>Abstand zwischen Fotos (Sekunden, 3–30)</Text>
|
||
<TextInput
|
||
style={styles.input}
|
||
placeholder="Standard: 5"
|
||
placeholderTextColor={THEME.colors.textMuted}
|
||
value={burstInterval}
|
||
onChangeText={setBurstInterval}
|
||
keyboardType="number-pad"
|
||
/>
|
||
</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>
|
||
</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 && (
|
||
<View style={styles.inputGroup}>
|
||
<Text style={styles.label}>Ausgewählter Rahmen</Text>
|
||
<View style={styles.frameChipRow}>
|
||
{FRAMES.map((frame) => (
|
||
<TouchableOpacity
|
||
key={frame.id}
|
||
style={[
|
||
styles.frameChip,
|
||
selectedFrameId === frame.id && styles.frameChipActive,
|
||
]}
|
||
onPress={() => setSelectedFrameId(frame.id)}
|
||
>
|
||
<Text style={[
|
||
styles.frameChipText,
|
||
selectedFrameId === frame.id && styles.frameChipTextActive,
|
||
]}>
|
||
{frame.name}
|
||
</Text>
|
||
</TouchableOpacity>
|
||
))}
|
||
</View>
|
||
</View>
|
||
)}
|
||
|
||
{frameMode === 'available' && FRAMES.length > 0 && (
|
||
<View style={styles.inputGroup}>
|
||
<Text style={styles.label}>Verfügbare Rahmen auswählen</Text>
|
||
<View style={styles.frameChipRow}>
|
||
{FRAMES.map((frame) => {
|
||
const isSelected = availableFrameIds.includes(frame.id);
|
||
return (
|
||
<TouchableOpacity
|
||
key={frame.id}
|
||
style={[
|
||
styles.frameChip,
|
||
isSelected && styles.frameChipActive,
|
||
]}
|
||
onPress={() => {
|
||
if (isSelected) {
|
||
setAvailableFrameIds((prev) => prev.filter((id) => id !== frame.id));
|
||
} else {
|
||
setAvailableFrameIds((prev) => [...prev, frame.id]);
|
||
}
|
||
}}
|
||
>
|
||
<Text style={[
|
||
styles.frameChipText,
|
||
isSelected && styles.frameChipTextActive,
|
||
]}>
|
||
{frame.name}
|
||
</Text>
|
||
</TouchableOpacity>
|
||
);
|
||
})}
|
||
</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>
|
||
</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.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}
|
||
start={{ x: 0, y: 0.5 }}
|
||
end={{ x: 1, y: 0.5 }}
|
||
style={styles.saveGradient}
|
||
>
|
||
<FontAwesomeIcon icon={faFloppyDisk} size={18} color={THEME.colors.text} style={{ marginRight: 10 }} />
|
||
<Text style={styles.saveBtnText}>EINSTELLUNGEN SPEICHERN</Text>
|
||
</LinearGradient>
|
||
</TouchableOpacity>
|
||
</ScrollView>
|
||
|
||
{/* Log Modal */}
|
||
<Modal visible={logs !== null} transparent animationType="slide">
|
||
<View style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.8)', justifyContent: 'center', alignItems: 'center' }}>
|
||
<View style={{ width: '90%', height: '85%', backgroundColor: '#222', borderRadius: 10, padding: 20 }}>
|
||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
|
||
<Text style={{ fontSize: 24, fontWeight: 'bold', color: '#fff' }}>App Logs</Text>
|
||
<TouchableOpacity
|
||
style={{ backgroundColor: THEME.colors.accent, paddingVertical: 8, paddingHorizontal: 12, borderRadius: 8 }}
|
||
onPress={async () => {
|
||
try {
|
||
const { Share } = require('react-native');
|
||
await Share.share({ message: logs || '' });
|
||
} catch (e) {
|
||
console.log('Share error', e);
|
||
}
|
||
}}
|
||
>
|
||
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Teilen / Kopieren</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
<ScrollView
|
||
style={{ flex: 1, backgroundColor: '#000', padding: 10, borderRadius: 5 }}
|
||
ref={ref => { this.scrollView = ref }}
|
||
onContentSizeChange={() => this.scrollView?.scrollToEnd({animated: true})}
|
||
>
|
||
<Text style={{ color: '#0f0', fontFamily: 'monospace' }}>{logs}</Text>
|
||
</ScrollView>
|
||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 15 }}>
|
||
<TouchableOpacity
|
||
style={{ backgroundColor: THEME.colors.primary, padding: 15, borderRadius: 8, flex: 0.48, alignItems: 'center' }}
|
||
onPress={() => setLogs(null)}
|
||
>
|
||
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Schließen</Text>
|
||
</TouchableOpacity>
|
||
<TouchableOpacity
|
||
style={{ backgroundColor: THEME.colors.danger, padding: 15, borderRadius: 8, flex: 0.48, alignItems: 'center' }}
|
||
onPress={async () => { await logger.clearLogs(); setLogs(await logger.readLogs()); }}
|
||
>
|
||
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Leeren</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</Modal>
|
||
</Animated.View>
|
||
);
|
||
}
|
||
|
||
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,
|
||
},
|
||
});
|