Files
Schnappix/src/screens/AdminScreen.tsx
T

762 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (15)</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, 330)</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,
},
});