/** * SettingsScreen - Einstellungen und Verbindungsverwaltung * * QR-Scanner fuer Pairing, Moduswahl, GPS-Toggle, Log-Viewer. */ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { View, Text, TextInput, TouchableOpacity, ScrollView, Switch, StyleSheet, Alert, Platform, ToastAndroid, ActivityIndicator, } from 'react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; import RNFS from 'react-native-fs'; import DocumentPicker from 'react-native-document-picker'; import rvs, { ConnectionState, RVSMessage, ConnectionConfig, ConnectionLogEntry } from '../services/rvs'; import { TTS_PREROLL_DEFAULT_SEC, TTS_PREROLL_MIN_SEC, TTS_PREROLL_MAX_SEC, TTS_PREROLL_STORAGE_KEY, VAD_SILENCE_DEFAULT_SEC, VAD_SILENCE_MIN_SEC, VAD_SILENCE_MAX_SEC, VAD_SILENCE_STORAGE_KEY, CONV_WINDOW_DEFAULT_SEC, CONV_WINDOW_MIN_SEC, CONV_WINDOW_MAX_SEC, CONV_WINDOW_STORAGE_KEY, } from '../services/audio'; import wakeWordService, { BUILTIN_KEYWORDS, DEFAULT_KEYWORD, WAKE_ACCESS_KEY_STORAGE, WAKE_KEYWORD_STORAGE, } from '../services/wakeword'; import ModeSelector from '../components/ModeSelector'; import QRScanner from '../components/QRScanner'; import VoiceCloneModal from '../components/VoiceCloneModal'; const STORAGE_PATH_KEY = 'aria_attachment_storage_path'; const DEFAULT_STORAGE_PATH = `${RNFS.DocumentDirectoryPath}/chat_attachments`; // --- Typen --- interface LogEntry { id: string; timestamp: number; source: string; message: string; level: 'info' | 'warn' | 'error'; } interface EventEntry { id: string; timestamp: number; title: string; description: string; } type LogTab = 'live' | 'events'; // Container-Farben fuer Live-Logs const SOURCE_COLORS: Record = { 'aria-core': '#4A9EFF', // Blau bridge: '#FFD60A', // Gelb proxy: '#FFFFFF', // Weiss rvs: '#34C759', // Gruen default: '#8888AA', // Grau }; // --- Komponente --- const SettingsScreen: React.FC = () => { const [connectionState, setConnectionState] = useState('disconnected'); const [manualToken, setManualToken] = useState(''); const [manualHost, setManualHost] = useState(''); const [manualPort, setManualPort] = useState('8765'); const [currentMode, setCurrentMode] = useState('normal'); const [gpsEnabled, setGpsEnabled] = useState(false); const [scannerVisible, setScannerVisible] = useState(false); const [logTab, setLogTab] = useState('live'); const [logs, setLogs] = useState([]); const [events, setEvents] = useState([]); const [connLog, setConnLog] = useState(rvs.getConnectionLog()); const [storagePath, setStoragePath] = useState(DEFAULT_STORAGE_PATH); const [autoDownload, setAutoDownload] = useState(true); const [storageSize, setStorageSize] = useState('...'); const [ttsEnabled, setTtsEnabled] = useState(true); const [ttsPrerollSec, setTtsPrerollSec] = useState(TTS_PREROLL_DEFAULT_SEC); const [vadSilenceSec, setVadSilenceSec] = useState(VAD_SILENCE_DEFAULT_SEC); const [convWindowSec, setConvWindowSec] = useState(CONV_WINDOW_DEFAULT_SEC); const [wakeAccessKey, setWakeAccessKey] = useState(''); const [wakeAccessKeyVisible, setWakeAccessKeyVisible] = useState(false); const [wakeKeyword, setWakeKeyword] = useState(DEFAULT_KEYWORD); const [wakeStatus, setWakeStatus] = useState(''); const [editingPath, setEditingPath] = useState(false); const [xttsVoice, setXttsVoice] = useState(''); const [loadingVoice, setLoadingVoice] = useState(null); const [availableVoices, setAvailableVoices] = useState>([]); const [voiceCloneVisible, setVoiceCloneVisible] = useState(false); const [tempPath, setTempPath] = useState(''); let logIdCounter = 0; // Gespeicherte Config in die Felder laden useEffect(() => { const config = rvs.getConfig(); if (config) { setManualHost(config.host); setManualPort(String(config.port)); setManualToken(config.token); } // Speicherpfad + Auto-Download laden AsyncStorage.getItem(STORAGE_PATH_KEY).then(saved => { if (saved) setStoragePath(saved); }); AsyncStorage.getItem('aria_auto_download').then(saved => { if (saved !== null) setAutoDownload(saved === 'true'); }); AsyncStorage.getItem('aria_tts_enabled').then(saved => { if (saved !== null) setTtsEnabled(saved === 'true'); }); AsyncStorage.getItem(TTS_PREROLL_STORAGE_KEY).then(saved => { if (saved != null) { const n = parseFloat(saved); if (isFinite(n) && n >= TTS_PREROLL_MIN_SEC && n <= TTS_PREROLL_MAX_SEC) { setTtsPrerollSec(n); } } }); AsyncStorage.getItem(VAD_SILENCE_STORAGE_KEY).then(saved => { if (saved != null) { const n = parseFloat(saved); if (isFinite(n) && n >= VAD_SILENCE_MIN_SEC && n <= VAD_SILENCE_MAX_SEC) { setVadSilenceSec(n); } } }); AsyncStorage.getItem(CONV_WINDOW_STORAGE_KEY).then(saved => { if (saved != null) { const n = parseFloat(saved); if (isFinite(n) && n >= CONV_WINDOW_MIN_SEC && n <= CONV_WINDOW_MAX_SEC) { setConvWindowSec(n); } } }); AsyncStorage.getItem(WAKE_ACCESS_KEY_STORAGE).then(saved => { if (saved) setWakeAccessKey(saved); }); AsyncStorage.getItem(WAKE_KEYWORD_STORAGE).then(saved => { if (saved) setWakeKeyword(saved); }); AsyncStorage.getItem('aria_xtts_voice').then(saved => { if (saved) setXttsVoice(saved); }); // Voice-Liste vom XTTS-Server holen (via RVS) rvs.send('xtts_list_voices' as any, {}); }, []); // Speichergroesse berechnen useEffect(() => { const calcSize = async () => { try { const exists = await RNFS.exists(storagePath); if (!exists) { setStorageSize('0 KB'); return; } const items = await RNFS.readDir(storagePath); const totalBytes = items.reduce((sum, f) => sum + (f.size || 0), 0); if (totalBytes > 1024 * 1024) { setStorageSize(`${(totalBytes / 1024 / 1024).toFixed(1)} MB (${items.length} Dateien)`); } else { setStorageSize(`${Math.round(totalBytes / 1024)} KB (${items.length} Dateien)`); } } catch { setStorageSize('nicht verfuegbar'); } }; calcSize(); }, [storagePath]); const saveStoragePath = useCallback(async (newPath: string) => { const clean = newPath.trim(); if (!clean) return; await AsyncStorage.setItem(STORAGE_PATH_KEY, clean); setStoragePath(clean); setEditingPath(false); Alert.alert('Gespeichert', `Neuer Speicherort:\n${clean}\n\nWird ab der naechsten Nachricht verwendet.`); }, []); const showPathPicker = useCallback(() => { Alert.alert( 'Speicherort waehlen', 'Wo sollen Anhaenge gespeichert werden?', [ { text: 'Ordner auswaehlen...', onPress: async () => { try { const result = await DocumentPicker.pickDirectory(); if (result?.uri) { // SAF URI decodieren (content://com.android.externalstorage...) const decoded = decodeURIComponent(result.uri); // Versuche einen lesbaren Pfad zu extrahieren const match = decoded.match(/primary[:%]3A(.+)/); const readablePath = match ? `/storage/emulated/0/${match[1].replace(/%2F|%3A/g, '/')}` : decoded; saveStoragePath(readablePath); } } catch (e: any) { if (!DocumentPicker.isCancel(e)) { Alert.alert('Fehler', 'Ordnerauswahl fehlgeschlagen'); } } }, }, { text: 'App-intern (Standard)', onPress: () => saveStoragePath(DEFAULT_STORAGE_PATH), }, { text: 'Pfad manuell eingeben', onPress: () => { setTempPath(storagePath); setEditingPath(true); }, }, { text: 'Abbrechen', style: 'cancel' as const }, ], ); }, [storagePath]); const clearStorageCache = useCallback(async () => { Alert.alert( 'Cache loeschen', `Alle lokalen Anhaenge in\n${storagePath}\nloeschen?\n\nDateien koennen ueber RVS erneut heruntergeladen werden.`, [ { text: 'Abbrechen', style: 'cancel' }, { text: 'Loeschen', style: 'destructive', onPress: async () => { try { const exists = await RNFS.exists(storagePath); if (exists) await RNFS.unlink(storagePath); await RNFS.mkdir(storagePath); setStorageSize('0 KB (0 Dateien)'); Alert.alert('Erledigt', 'Cache geleert. Anhaenge werden bei Bedarf neu geladen.'); } catch (e: any) { Alert.alert('Fehler', e.message); } }, }, ], ); }, [storagePath]); // RVS-Nachrichten und Verbindungslog abonnieren useEffect(() => { const unsubState = rvs.onStateChange(setConnectionState); setConnectionState(rvs.getState()); const unsubLog = rvs.onLog((entry) => { setConnLog(prev => [...prev.slice(-99), entry]); }); const unsubMessage = rvs.onMessage((message: RVSMessage) => { if (message.type === 'log') { const entry: LogEntry = { id: `log_${Date.now()}_${logIdCounter++}`, timestamp: message.timestamp, source: (message.payload.source as string) || 'default', message: (message.payload.message as string) || '', level: (message.payload.level as 'info' | 'warn' | 'error') || 'info', }; setLogs(prev => [...prev.slice(-200), entry]); // Max 200 Eintraege behalten } if (message.type === 'event') { const entry: EventEntry = { id: `evt_${Date.now()}_${logIdCounter++}`, timestamp: message.timestamp, title: (message.payload.title as string) || '', description: (message.payload.description as string) || '', }; setEvents(prev => [...prev.slice(-100), entry]); } // Modus-Bestaetigung if (message.type === 'mode') { const mode = message.payload.mode as string; if (mode) setCurrentMode(mode); } // XTTS-Voice-Liste if (message.type === ('xtts_voices_list' as any)) { const voices = ((message.payload as any).voices || []) as Array<{name: string, size: number}>; setAvailableVoices(voices); } // Voice wurde gespeichert → Liste neu laden + ggf. auswaehlen if (message.type === ('xtts_voice_saved' as any)) { const name = (message.payload as any).name as string; if (name) { setXttsVoice(name); AsyncStorage.setItem('aria_xtts_voice', name); } rvs.send('xtts_list_voices' as any, {}); } // Diagnostic-Voice-Wechsel → lokale App-Stimme auf den neuen Default zuruecksetzen. // Zusaetzlich Preload triggern, damit der User weiss wann's geladen ist. if (message.type === ('config' as any)) { const newVoice = ((message.payload as any).xttsVoice as string) ?? ''; setXttsVoice(newVoice); AsyncStorage.setItem('aria_xtts_voice', newVoice); if (newVoice) { setLoadingVoice(newVoice); } } // XTTS-Bridge meldet: Stimme fertig geladen if (message.type === ('voice_ready' as any)) { const v = ((message.payload as any).voice as string) ?? ''; const err = (message.payload as any).error as string | undefined; const ms = (message.payload as any).loadMs as number | undefined; setLoadingVoice(null); if (err) { ToastAndroid.show(`Stimme "${v}" konnte nicht geladen werden: ${err}`, ToastAndroid.LONG); } else { const suffix = ms ? ` (${(ms / 1000).toFixed(1)}s)` : ''; ToastAndroid.show(`Stimme "${v || 'Standard'}" bereit${suffix}`, ToastAndroid.SHORT); } } }); return () => { unsubState(); unsubMessage(); unsubLog(); }; }, []); // --- QR-Code scannen --- const openQRScanner = useCallback(() => { setScannerVisible(true); }, []); const handleQRScan = useCallback((config: ConnectionConfig) => { setScannerVisible(false); // Felder im UI aktualisieren setManualHost(config.host); setManualPort(String(config.port)); setManualToken(config.token); // Direkt verbinden rvs.setConfig(config); rvs.connect(); Alert.alert( 'Pairing erfolgreich', `Verbinde mit ${config.host}:${config.port}...`, ); }, []); // --- Manuelle Verbindung --- const connectManually = useCallback(() => { if (!manualHost.trim() || !manualToken.trim()) { Alert.alert('Fehler', 'Host und Token muessen angegeben werden.'); return; } const config: ConnectionConfig = { host: manualHost.trim(), port: parseInt(manualPort, 10) || 8765, token: manualToken.trim(), useTLS: true, }; rvs.setConfig(config); rvs.connect(); }, [manualHost, manualPort, manualToken]); const disconnectRVS = useCallback(() => { rvs.disconnect(); }, []); // --- GPS Toggle --- const handleGPSToggle = useCallback((value: boolean) => { setGpsEnabled(value); // In Produktion: Wert in AsyncStorage persistieren }, []); // --- XTTS Voice --- const selectVoice = useCallback((voiceName: string) => { setXttsVoice(voiceName); AsyncStorage.setItem('aria_xtts_voice', voiceName); // Preload nur fuer Custom-Voices — "Standard" braucht keinen Ladevorgang if (voiceName) { setLoadingVoice(voiceName); rvs.send('voice_preload' as any, { voice: voiceName, source: 'app' }); } else { setLoadingVoice(null); } }, []); const deleteVoice = useCallback((name: string) => { Alert.alert( 'Stimme loeschen', `Stimme "${name}" vom Server endgueltig loeschen?\nAlle Apps verlieren sie.`, [ { text: 'Abbrechen', style: 'cancel' }, { text: 'Loeschen', style: 'destructive', onPress: () => { rvs.send('xtts_delete_voice' as any, { name }); if (xttsVoice === name) { setXttsVoice(''); AsyncStorage.setItem('aria_xtts_voice', ''); } // Liste nach kurzer Wartezeit neu laden (XTTS-Bridge schickt eh neue Liste) setTimeout(() => rvs.send('xtts_list_voices' as any, {}), 500); }, }, ], ); }, [xttsVoice]); // --- Modus aendern --- const handleModeChange = useCallback((modeId: string) => { setCurrentMode(modeId); }, []); // --- Zeitformat --- const formatTime = (ts: number): string => { return new Date(ts).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit', }); }; // --- Verbindungsstatus --- const connectionDotColor = connectionState === 'connected' ? '#34C759' : connectionState === 'connecting' ? '#FFD60A' : '#FF3B30'; const connectionLabel = connectionState === 'connected' ? 'Verbunden' : connectionState === 'connecting' ? 'Verbinde...' : 'Getrennt'; return ( <> setScannerVisible(false)} /> setVoiceCloneVisible(false)} /> {/* === Verbindung === */} Verbindung {/* Status-Anzeige */} {connectionLabel} {connectionState === 'connected' && ( Trennen )} {/* QR-Scanner */} {'\uD83D\uDCF1'} QR-Code scannen (Pairing) {/* Manuelle Eingabe */} Host Port Token Verbinden {/* === Verbindungslog === */} Verbindungslog { // Auto-Scroll nach unten bei neuen Eintraegen if (ref && connLog.length > 0) { setTimeout(() => ref.scrollToEnd({ animated: false }), 50); } }} > {connLog.length > 0 ? ( connLog.slice(-50).map((entry, idx) => ( {formatTime(entry.timestamp)} {entry.message} )) ) : ( Noch keine Verbindungsversuche )} setConnLog([])} > Log l{'\u00F6'}schen {/* === Modus === */} Betriebsmodus {/* === GPS === */} Standort GPS-Position mitsenden Standort wird automatisch an Nachrichten angehaengt {/* === Spracheingabe (geraetelokal) === */} Spracheingabe Stille-Toleranz Wie lange du eine Sprechpause machen darfst, bevor die Aufnahme automatisch beendet und gesendet wird. Hoeher = mehr Zeit zum Nachdenken; niedriger = schnelleres Senden. Default: {VAD_SILENCE_DEFAULT_SEC.toFixed(1)}s. { const next = Math.max(VAD_SILENCE_MIN_SEC, Math.round((vadSilenceSec - 0.5) * 10) / 10); setVadSilenceSec(next); AsyncStorage.setItem(VAD_SILENCE_STORAGE_KEY, String(next)); }} disabled={vadSilenceSec <= VAD_SILENCE_MIN_SEC} > −0.5 {vadSilenceSec.toFixed(1)} s { const next = Math.min(VAD_SILENCE_MAX_SEC, Math.round((vadSilenceSec + 0.5) * 10) / 10); setVadSilenceSec(next); AsyncStorage.setItem(VAD_SILENCE_STORAGE_KEY, String(next)); }} disabled={vadSilenceSec >= VAD_SILENCE_MAX_SEC} > +0.5 Konversations-Fenster Im Gespraechsmodus (Ohr-Button): nach ARIA's Antwort hast du so lange Zeit, weiter zu sprechen, bevor die Konversation automatisch beendet wird. Sprichst du nichts → Mikrofon zu. Default: {CONV_WINDOW_DEFAULT_SEC.toFixed(1)}s. { const next = Math.max(CONV_WINDOW_MIN_SEC, Math.round((convWindowSec - 1) * 10) / 10); setConvWindowSec(next); AsyncStorage.setItem(CONV_WINDOW_STORAGE_KEY, String(next)); }} disabled={convWindowSec <= CONV_WINDOW_MIN_SEC} > −1 {convWindowSec.toFixed(0)} s { const next = Math.min(CONV_WINDOW_MAX_SEC, Math.round((convWindowSec + 1) * 10) / 10); setConvWindowSec(next); AsyncStorage.setItem(CONV_WINDOW_STORAGE_KEY, String(next)); }} disabled={convWindowSec >= CONV_WINDOW_MAX_SEC} > +1 {/* === Wake-Word (geraetelokal) === */} Wake-Word Wenn ein Picovoice-Access-Key eingetragen ist, hoert die App passiv auf das gewaehlte Wake-Word — du kannst dich mit anderen unterhalten, Musik laufen lassen und mit "{wakeKeyword}" eine Konversation mit ARIA starten. Ohne Key oder bei Fehlschlag startet das Ohr direkt eine Konversation (klassischer Modus). Picovoice Access Key setWakeAccessKeyVisible(v => !v)} style={{padding: 8}} > {wakeAccessKeyVisible ? '🙈' : '👁'} Wake-Word Built-In: sofort verwendbar. "ARIA" als Custom-Keyword kommt spaeter ueber Diagnostic-Upload. {BUILTIN_KEYWORDS.map(kw => ( setWakeKeyword(kw)} > {kw} ))} { setWakeStatus('Initialisiere...'); try { const ok = await wakeWordService.configure(wakeAccessKey, wakeKeyword); setWakeStatus(ok ? `✅ "${wakeKeyword}" bereit` : '❌ Fehlgeschlagen — Access Key pruefen'); } catch (err: any) { setWakeStatus('❌ ' + String(err?.message || err).slice(0, 80)); } setTimeout(() => setWakeStatus(''), 5000); }} > Speichern + Aktivieren {!!wakeStatus && ( {wakeStatus} )} {/* === Sprachausgabe (geraetelokal) === */} Sprachausgabe Sprachausgabe auf diesem Geraet Nur lokal — andere Geraete sind unabhaengig. Wenn aus, erscheint im Chat auch kein Mund-Button. { setTtsEnabled(val); AsyncStorage.setItem('aria_tts_enabled', String(val)); }} trackColor={{ false: '#2A2A3E', true: '#0096FF' }} thumbColor={ttsEnabled ? '#FFFFFF' : '#666680'} /> {ttsEnabled && ( Puffer vor Wiedergabestart Wie viel Audio gesammelt wird bevor die Wiedergabe startet. Hoeher = robuster gegen Render-Pausen, aber mehr Startverzoegerung. Default: {TTS_PREROLL_DEFAULT_SEC.toFixed(1)}s. { const next = Math.max(TTS_PREROLL_MIN_SEC, Math.round((ttsPrerollSec - 0.5) * 10) / 10); setTtsPrerollSec(next); AsyncStorage.setItem(TTS_PREROLL_STORAGE_KEY, String(next)); }} disabled={ttsPrerollSec <= TTS_PREROLL_MIN_SEC} > −0.5 {ttsPrerollSec.toFixed(1)} s { const next = Math.min(TTS_PREROLL_MAX_SEC, Math.round((ttsPrerollSec + 0.5) * 10) / 10); setTtsPrerollSec(next); AsyncStorage.setItem(TTS_PREROLL_STORAGE_KEY, String(next)); }} disabled={ttsPrerollSec >= TTS_PREROLL_MAX_SEC} > +0.5 )} {ttsEnabled && ( Stimme (geraetelokal) Eigene Wahl fuer dieses Geraet. Ohne Auswahl gilt der Diagnostic-Default. {/* Default-Option */} selectVoice('')} > Standard (Diagnostic-Default) {xttsVoice === '' && {'\u2713'}} {availableVoices.length === 0 ? ( Keine eigenen Stimmen auf dem XTTS-Server. ) : ( availableVoices.map(v => ( selectVoice(v.name)} > {v.name} {(v.size / 1024).toFixed(0)} KB {loadingVoice === v.name && ( )} {xttsVoice === v.name && loadingVoice !== v.name && {'\u2713'}} deleteVoice(v.name)} style={styles.voiceRowDelete}> X )) )} setVoiceCloneVisible(true)} > {'\uD83C\uDFA4'} Eigene Stimme aufnehmen rvs.send('xtts_list_voices' as any, {})} > Aktualisieren )} {/* === Speicher === */} Anhang-Speicher Auto-Download Fehlende Anhaenge beim App-Start automatisch vom Server laden { setAutoDownload(val); AsyncStorage.setItem('aria_auto_download', String(val)); }} trackColor={{ false: '#2A2A3E', true: '#0096FF' }} thumbColor={autoDownload ? '#FFFFFF' : '#666680'} /> Lokaler Speicherort Hier werden Bilder und Dateien aus dem Chat gespeichert. {autoDownload ? ' Fehlende Dateien werden automatisch nachgeladen.' : ' Fehlende Dateien koennen per Tippen geladen werden.'} {editingPath ? ( saveStoragePath(tempPath)} > Speichern setEditingPath(false)} > Abbrechen ) : ( {storagePath} {storageSize} Pfad aendern Cache leeren )} {/* === Logs === */} Protokoll {/* Tab-Umschalter */} setLogTab('live')} > Live Logs setLogTab('events')} > Events {/* Log-Inhalt */} {logTab === 'live' ? ( logs.length > 0 ? ( logs.slice(-50).map(log => ( {formatTime(log.timestamp)} [{log.source}] {log.message} )) ) : ( Noch keine Logs empfangen ) ) : ( events.length > 0 ? ( events.slice(-30).map(event => ( {formatTime(event.timestamp)} {event.title} {event.description} )) ) : ( Noch keine Events empfangen ) )} {/* Log-Aktionen */} { if (logTab === 'live') setLogs([]); else setEvents([]); }} > Protokoll l\u00F6schen {/* === About === */} {'\u00DC'}ber ARIA Cockpit Version {require('../../package.json').version} Stefans Kommandozentrale f{'\u00FC'}r ARIA.{'\n'} Gebaut mit React Native + TypeScript. { const updateService = require('../services/updater').default; updateService.checkForUpdate(); Alert.alert('Update-Check', 'Pruefe auf neue Version...'); }} > Auf Updates pr{'\u00FC'}fen {/* Platz am Ende */} ); }; // --- Styles --- const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#0D0D1A', }, content: { padding: 16, }, sectionTitle: { color: '#8888AA', fontSize: 13, fontWeight: '700', textTransform: 'uppercase', letterSpacing: 1, marginTop: 20, marginBottom: 8, marginLeft: 4, }, card: { backgroundColor: '#12122A', borderRadius: 14, padding: 16, borderWidth: 1, borderColor: '#1E1E2E', }, // Verbindungsstatus statusRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 16, }, statusDot: { width: 10, height: 10, borderRadius: 5, marginRight: 10, }, statusLabel: { color: '#FFFFFF', fontSize: 16, fontWeight: '600', flex: 1, }, disconnectButton: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 6, backgroundColor: 'rgba(255, 59, 48, 0.2)', }, disconnectText: { color: '#FF3B30', fontSize: 13, fontWeight: '600', }, // QR-Button qrButton: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#1E1E2E', borderRadius: 10, padding: 14, marginBottom: 16, }, qrIcon: { fontSize: 22, marginRight: 10, }, qrText: { color: '#FFFFFF', fontSize: 15, fontWeight: '500', }, // Eingabefelder inputLabel: { color: '#8888AA', fontSize: 12, marginBottom: 4, marginLeft: 2, }, input: { backgroundColor: '#1E1E2E', borderRadius: 8, paddingHorizontal: 14, paddingVertical: 10, color: '#FFFFFF', fontSize: 15, marginBottom: 12, borderWidth: 1, borderColor: '#2A2A3E', }, connectButton: { backgroundColor: '#0096FF', borderRadius: 10, padding: 14, alignItems: 'center', marginTop: 4, }, connectButtonText: { color: '#FFFFFF', fontSize: 16, fontWeight: '700', }, // Toggle toggleRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, toggleInfo: { flex: 1, marginRight: 12, }, toggleLabel: { color: '#FFFFFF', fontSize: 15, fontWeight: '500', }, toggleHint: { color: '#666680', fontSize: 12, marginTop: 2, }, // XTTS Voice List voiceRow: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#1E1E2E', borderRadius: 8, padding: 10, marginTop: 6, borderWidth: 1, borderColor: 'transparent', }, voiceRowActive: { borderColor: '#0096FF', backgroundColor: '#0D1A2E', }, voiceRowName: { color: '#CCCCDD', fontSize: 14, fontWeight: '500', }, voiceRowNameActive: { color: '#FFFFFF', }, voiceRowMeta: { color: '#666680', fontSize: 11, marginTop: 2, }, voiceRowCheck: { color: '#34C759', fontSize: 16, fontWeight: '700', marginHorizontal: 6, }, voiceRowDelete: { width: 28, height: 28, borderRadius: 14, backgroundColor: 'rgba(255,59,48,0.2)', alignItems: 'center', justifyContent: 'center', marginLeft: 4, }, voiceRowDeleteIcon: { color: '#FF3B30', fontSize: 12, fontWeight: '700', }, // Stimmen voiceBtn: { flex: 1, padding: 12, borderRadius: 10, backgroundColor: '#1E1E2E', alignItems: 'center', borderWidth: 2, borderColor: 'transparent', }, voiceBtnActive: { borderColor: '#0096FF', backgroundColor: '#0D1A2E', }, voiceBtnIcon: { fontSize: 28, marginBottom: 4, }, voiceBtnText: { color: '#8888AA', fontSize: 14, fontWeight: '600', }, voiceBtnTextActive: { color: '#FFFFFF', }, voiceBtnHint: { color: '#555570', fontSize: 11, marginTop: 2, }, // Speicher storagePathText: { color: '#0096FF', fontSize: 12, fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', }, storageSizeText: { color: '#8888AA', fontSize: 12, marginTop: 4, }, // Logs tabRow: { flexDirection: 'row', marginBottom: 12, borderRadius: 8, backgroundColor: '#1E1E2E', overflow: 'hidden', }, tab: { flex: 1, paddingVertical: 10, alignItems: 'center', }, tabActive: { backgroundColor: '#0096FF', }, tabText: { color: '#666680', fontSize: 13, fontWeight: '600', }, tabTextActive: { color: '#FFFFFF', }, connLogScroll: { maxHeight: 200, backgroundColor: '#0A0A18', borderRadius: 8, padding: 10, }, logContainer: { maxHeight: 300, backgroundColor: '#0A0A18', borderRadius: 8, padding: 10, }, logEntry: { flexDirection: 'row', flexWrap: 'wrap', marginBottom: 4, }, logTime: { color: '#555570', fontSize: 11, fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', marginRight: 6, }, logSource: { fontSize: 11, fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', fontWeight: '700', marginRight: 6, }, logMessage: { color: '#CCCCDD', fontSize: 11, fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', flex: 1, }, logError: { color: '#FF3B30', }, logWarn: { color: '#FFD60A', }, emptyLog: { color: '#555570', fontSize: 13, textAlign: 'center', padding: 20, }, eventEntry: { marginBottom: 10, paddingBottom: 10, borderBottomWidth: 1, borderBottomColor: '#1E1E2E', }, eventTime: { color: '#555570', fontSize: 11, }, eventTitle: { color: '#FFFFFF', fontSize: 14, fontWeight: '600', marginTop: 2, }, eventDescription: { color: '#8888AA', fontSize: 13, marginTop: 2, }, clearButton: { marginTop: 10, padding: 10, alignItems: 'center', borderRadius: 8, backgroundColor: '#1E1E2E', }, clearButtonText: { color: '#666680', fontSize: 13, fontWeight: '500', }, // About aboutTitle: { color: '#FFFFFF', fontSize: 18, fontWeight: '700', }, aboutVersion: { color: '#0096FF', fontSize: 13, marginTop: 2, }, aboutInfo: { color: '#666680', fontSize: 13, marginTop: 8, lineHeight: 20, }, bottomSpacer: { height: 40, }, prerollRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', marginTop: 12, gap: 16, }, prerollButton: { backgroundColor: '#2A2A3E', paddingHorizontal: 18, paddingVertical: 10, borderRadius: 8, minWidth: 72, alignItems: 'center', }, prerollButtonText: { color: '#FFFFFF', fontSize: 16, fontWeight: '600', }, prerollValue: { color: '#FFFFFF', fontSize: 20, fontWeight: '700', minWidth: 80, textAlign: 'center', }, keywordChip: { backgroundColor: '#1E1E2E', borderWidth: 1, borderColor: '#2A2A3E', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 14, }, keywordChipActive: { backgroundColor: '#0096FF', borderColor: '#0096FF', }, keywordChipText: { color: '#8888AA', fontSize: 13, fontWeight: '500', }, keywordChipTextActive: { color: '#FFFFFF', fontWeight: '700', }, }); export default SettingsScreen;