ARIA-AGENT/android/src/screens/SettingsScreen.tsx

1055 lines
32 KiB
TypeScript

/**
* 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,
} 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 ModeSelector from '../components/ModeSelector';
import QRScanner from '../components/QRScanner';
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<string, string> = {
'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<ConnectionState>('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<LogTab>('live');
const [logs, setLogs] = useState<LogEntry[]>([]);
const [events, setEvents] = useState<EventEntry[]>([]);
const [connLog, setConnLog] = useState<ConnectionLogEntry[]>(rvs.getConnectionLog());
const [storagePath, setStoragePath] = useState(DEFAULT_STORAGE_PATH);
const [autoDownload, setAutoDownload] = useState(true);
const [storageSize, setStorageSize] = useState('...');
const [ttsEnabled, setTtsEnabled] = useState(true);
const [defaultVoice, setDefaultVoice] = useState('ramona');
const [highlightVoice, setHighlightVoice] = useState('thorsten');
const [speechSpeed, setSpeechSpeed] = useState(1.0);
const [editingPath, setEditingPath] = 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('aria_default_voice').then(saved => {
if (saved) setDefaultVoice(saved);
});
AsyncStorage.getItem('aria_highlight_voice').then(saved => {
if (saved) setHighlightVoice(saved);
});
AsyncStorage.getItem('aria_speech_speed').then(saved => {
if (saved) setSpeechSpeed(parseFloat(saved));
});
}, []);
// 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);
}
});
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
}, []);
// --- 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 (
<>
<QRScanner
visible={scannerVisible}
onScan={handleQRScan}
onClose={() => setScannerVisible(false)}
/>
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
{/* === Verbindung === */}
<Text style={styles.sectionTitle}>Verbindung</Text>
<View style={styles.card}>
{/* Status-Anzeige */}
<View style={styles.statusRow}>
<View style={[styles.statusDot, { backgroundColor: connectionDotColor }]} />
<Text style={styles.statusLabel}>{connectionLabel}</Text>
{connectionState === 'connected' && (
<TouchableOpacity style={styles.disconnectButton} onPress={disconnectRVS}>
<Text style={styles.disconnectText}>Trennen</Text>
</TouchableOpacity>
)}
</View>
{/* QR-Scanner */}
<TouchableOpacity style={styles.qrButton} onPress={openQRScanner}>
<Text style={styles.qrIcon}>{'\uD83D\uDCF1'}</Text>
<Text style={styles.qrText}>QR-Code scannen (Pairing)</Text>
</TouchableOpacity>
{/* Manuelle Eingabe */}
<Text style={styles.inputLabel}>Host</Text>
<TextInput
style={styles.input}
value={manualHost}
onChangeText={setManualHost}
placeholder="z.B. aria.example.com"
placeholderTextColor="#555570"
autoCapitalize="none"
/>
<Text style={styles.inputLabel}>Port</Text>
<TextInput
style={styles.input}
value={manualPort}
onChangeText={setManualPort}
placeholder="8765"
placeholderTextColor="#555570"
keyboardType="numeric"
/>
<Text style={styles.inputLabel}>Token</Text>
<TextInput
style={styles.input}
value={manualToken}
onChangeText={setManualToken}
placeholder="Verbindungs-Token"
placeholderTextColor="#555570"
autoCapitalize="none"
secureTextEntry
/>
<TouchableOpacity style={styles.connectButton} onPress={connectManually}>
<Text style={styles.connectButtonText}>Verbinden</Text>
</TouchableOpacity>
</View>
{/* === Verbindungslog === */}
<Text style={styles.sectionTitle}>Verbindungslog</Text>
<View style={styles.card}>
<ScrollView
style={styles.connLogScroll}
nestedScrollEnabled={true}
ref={(ref) => {
// 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) => (
<View key={`cl_${idx}`} style={styles.logEntry}>
<Text style={styles.logTime}>{formatTime(entry.timestamp)}</Text>
<Text
style={[
styles.logMessage,
entry.level === 'error' && styles.logError,
entry.level === 'warn' && styles.logWarn,
]}
numberOfLines={3}
>
{entry.message}
</Text>
</View>
))
) : (
<Text style={styles.emptyLog}>Noch keine Verbindungsversuche</Text>
)}
</ScrollView>
<TouchableOpacity
style={styles.clearButton}
onPress={() => setConnLog([])}
>
<Text style={styles.clearButtonText}>Log l{'\u00F6'}schen</Text>
</TouchableOpacity>
</View>
{/* === Modus === */}
<Text style={styles.sectionTitle}>Betriebsmodus</Text>
<View style={styles.card}>
<ModeSelector currentModeId={currentMode} onModeChange={handleModeChange} />
</View>
{/* === GPS === */}
<Text style={styles.sectionTitle}>Standort</Text>
<View style={styles.card}>
<View style={styles.toggleRow}>
<View style={styles.toggleInfo}>
<Text style={styles.toggleLabel}>GPS-Position mitsenden</Text>
<Text style={styles.toggleHint}>
Standort wird automatisch an Nachrichten angehaengt
</Text>
</View>
<Switch
value={gpsEnabled}
onValueChange={handleGPSToggle}
trackColor={{ false: '#2A2A3E', true: '#0096FF' }}
thumbColor={gpsEnabled ? '#FFFFFF' : '#666680'}
/>
</View>
</View>
{/* === Sprachausgabe === */}
<Text style={styles.sectionTitle}>Sprachausgabe</Text>
<View style={styles.card}>
{/* TTS An/Aus */}
<View style={styles.toggleRow}>
<View style={styles.toggleInfo}>
<Text style={styles.toggleLabel}>Sprachausgabe</Text>
<Text style={styles.toggleHint}>ARIA antwortet per Sprache (TTS)</Text>
</View>
<Switch
value={ttsEnabled}
onValueChange={(val) => {
setTtsEnabled(val);
AsyncStorage.setItem('aria_tts_enabled', String(val));
rvs.send('config' as any, { ttsEnabled: val });
}}
trackColor={{ false: '#2A2A3E', true: '#0096FF' }}
thumbColor={ttsEnabled ? '#FFFFFF' : '#666680'}
/>
</View>
{/* Standard-Stimme */}
<View style={{marginTop: 16}}>
<Text style={styles.toggleLabel}>Standard-Stimme</Text>
<Text style={styles.toggleHint}>Fuer normale Antworten und Gespraeche</Text>
<View style={{flexDirection: 'row', gap: 8, marginTop: 8}}>
<TouchableOpacity
style={[styles.voiceBtn, defaultVoice === 'ramona' && styles.voiceBtnActive]}
onPress={() => { setDefaultVoice('ramona'); AsyncStorage.setItem('aria_default_voice', 'ramona'); rvs.send('config' as any, { defaultVoice: 'ramona' }); }}
>
<Text style={styles.voiceBtnIcon}>{'\uD83D\uDE4E\u200D\u2640\uFE0F'}</Text>
<Text style={[styles.voiceBtnText, defaultVoice === 'ramona' && styles.voiceBtnTextActive]}>Ramona</Text>
<Text style={styles.voiceBtnHint}>Weiblich, warm</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.voiceBtn, defaultVoice === 'thorsten' && styles.voiceBtnActive]}
onPress={() => { setDefaultVoice('thorsten'); AsyncStorage.setItem('aria_default_voice', 'thorsten'); rvs.send('config' as any, { defaultVoice: 'thorsten' }); }}
>
<Text style={styles.voiceBtnIcon}>{'\uD83E\uDDD4'}</Text>
<Text style={[styles.voiceBtnText, defaultVoice === 'thorsten' && styles.voiceBtnTextActive]}>Thorsten</Text>
<Text style={styles.voiceBtnHint}>Maennlich, tief</Text>
</TouchableOpacity>
</View>
</View>
{/* Highlight-Stimme */}
<View style={{marginTop: 16}}>
<Text style={styles.toggleLabel}>Highlight-Stimme</Text>
<Text style={styles.toggleHint}>Fuer besondere Ereignisse (Deploy, Alarm, Erfolg)</Text>
<View style={{flexDirection: 'row', gap: 8, marginTop: 8}}>
<TouchableOpacity
style={[styles.voiceBtn, highlightVoice === 'thorsten' && styles.voiceBtnActive]}
onPress={() => { setHighlightVoice('thorsten'); AsyncStorage.setItem('aria_highlight_voice', 'thorsten'); rvs.send('config' as any, { highlightVoice: 'thorsten' }); }}
>
<Text style={styles.voiceBtnIcon}>{'\uD83E\uDDD4'}</Text>
<Text style={[styles.voiceBtnText, highlightVoice === 'thorsten' && styles.voiceBtnTextActive]}>Thorsten</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.voiceBtn, highlightVoice === 'ramona' && styles.voiceBtnActive]}
onPress={() => { setHighlightVoice('ramona'); AsyncStorage.setItem('aria_highlight_voice', 'ramona'); rvs.send('config' as any, { highlightVoice: 'ramona' }); }}
>
<Text style={styles.voiceBtnIcon}>{'\uD83D\uDE4E\u200D\u2640\uFE0F'}</Text>
<Text style={[styles.voiceBtnText, highlightVoice === 'ramona' && styles.voiceBtnTextActive]}>Ramona</Text>
</TouchableOpacity>
</View>
</View>
{/* Sprechgeschwindigkeit */}
<View style={{marginTop: 16}}>
<Text style={styles.toggleLabel}>Sprechgeschwindigkeit: {speechSpeed.toFixed(1)}x</Text>
<View style={{flexDirection: 'row', alignItems: 'center', gap: 8, marginTop: 8}}>
<Text style={{color: '#555570', fontSize: 11}}>0.5x</Text>
<View style={{flex: 1}}>
<TouchableOpacity
style={{height: 30, justifyContent: 'center'}}
onPress={(e) => {
const layout = e.nativeEvent;
// Einfacher Tap-basierter Slider
}}
>
<View style={{height: 4, backgroundColor: '#2A2A3E', borderRadius: 2}}>
<View style={{height: 4, backgroundColor: '#0096FF', borderRadius: 2, width: `${((speechSpeed - 0.5) / 1.5) * 100}%`}} />
</View>
</TouchableOpacity>
</View>
<Text style={{color: '#555570', fontSize: 11}}>2.0x</Text>
</View>
<View style={{flexDirection: 'row', justifyContent: 'space-around', marginTop: 8}}>
{[0.5, 0.75, 1.0, 1.25, 1.5, 2.0].map(speed => (
<TouchableOpacity
key={speed}
onPress={() => {
setSpeechSpeed(speed);
AsyncStorage.setItem('aria_speech_speed', String(speed));
rvs.send('config' as any, { speechSpeed: speed });
}}
style={{
paddingHorizontal: 10, paddingVertical: 6, borderRadius: 6,
backgroundColor: speechSpeed === speed ? '#0096FF' : '#1E1E2E',
}}
>
<Text style={{color: speechSpeed === speed ? '#fff' : '#8888AA', fontSize: 12, fontWeight: '600'}}>
{speed}x
</Text>
</TouchableOpacity>
))}
</View>
</View>
{/* Highlight-Trigger Info */}
<View style={{marginTop: 16, padding: 10, backgroundColor: '#1E1E2E', borderRadius: 8}}>
<Text style={styles.toggleLabel}>{'\u26A1'} Highlight-Trigger</Text>
<Text style={[styles.toggleHint, {marginTop: 4}]}>
Die Highlight-Stimme wird automatisch bei diesen Woertern verwendet:{'\n'}
deploy, erfolgreich, alarm, so soll es sein, kritisch, server down, sicherheitswarnung, ticket geloest, aufgabe abgeschlossen
</Text>
</View>
</View>
{/* === Speicher === */}
<Text style={styles.sectionTitle}>Anhang-Speicher</Text>
<View style={styles.card}>
<View style={styles.toggleRow}>
<View style={styles.toggleInfo}>
<Text style={styles.toggleLabel}>Auto-Download</Text>
<Text style={styles.toggleHint}>
Fehlende Anhaenge beim App-Start automatisch vom Server laden
</Text>
</View>
<Switch
value={autoDownload}
onValueChange={(val) => {
setAutoDownload(val);
AsyncStorage.setItem('aria_auto_download', String(val));
}}
trackColor={{ false: '#2A2A3E', true: '#0096FF' }}
thumbColor={autoDownload ? '#FFFFFF' : '#666680'}
/>
</View>
<View style={{height: 16}} />
<Text style={styles.toggleLabel}>Lokaler Speicherort</Text>
<Text style={styles.toggleHint}>
Hier werden Bilder und Dateien aus dem Chat gespeichert.
{autoDownload ? ' Fehlende Dateien werden automatisch nachgeladen.' : ' Fehlende Dateien koennen per Tippen geladen werden.'}
</Text>
{editingPath ? (
<View style={{marginTop: 10}}>
<TextInput
style={styles.input}
value={tempPath}
onChangeText={setTempPath}
placeholder="z.B. /storage/emulated/0/ARIA/attachments"
placeholderTextColor="#555570"
autoCapitalize="none"
/>
<View style={{flexDirection: 'row', gap: 8}}>
<TouchableOpacity
style={[styles.connectButton, {flex: 1}]}
onPress={() => saveStoragePath(tempPath)}
>
<Text style={styles.connectButtonText}>Speichern</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.clearButton, {flex: 1, marginTop: 0}]}
onPress={() => setEditingPath(false)}
>
<Text style={styles.clearButtonText}>Abbrechen</Text>
</TouchableOpacity>
</View>
</View>
) : (
<View style={{marginTop: 10}}>
<Text style={styles.storagePathText} numberOfLines={2}>{storagePath}</Text>
<Text style={styles.storageSizeText}>{storageSize}</Text>
<View style={{flexDirection: 'row', gap: 8, marginTop: 8}}>
<TouchableOpacity
style={[styles.clearButton, {flex: 1, marginTop: 0}]}
onPress={showPathPicker}
>
<Text style={styles.clearButtonText}>Pfad aendern</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.clearButton, {flex: 1, marginTop: 0, backgroundColor: 'rgba(255,59,48,0.15)'}]}
onPress={clearStorageCache}
>
<Text style={[styles.clearButtonText, {color: '#FF3B30'}]}>Cache leeren</Text>
</TouchableOpacity>
</View>
</View>
)}
</View>
{/* === Logs === */}
<Text style={styles.sectionTitle}>Protokoll</Text>
<View style={styles.card}>
{/* Tab-Umschalter */}
<View style={styles.tabRow}>
<TouchableOpacity
style={[styles.tab, logTab === 'live' && styles.tabActive]}
onPress={() => setLogTab('live')}
>
<Text style={[styles.tabText, logTab === 'live' && styles.tabTextActive]}>
Live Logs
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.tab, logTab === 'events' && styles.tabActive]}
onPress={() => setLogTab('events')}
>
<Text style={[styles.tabText, logTab === 'events' && styles.tabTextActive]}>
Events
</Text>
</TouchableOpacity>
</View>
{/* Log-Inhalt */}
<View style={styles.logContainer}>
{logTab === 'live' ? (
logs.length > 0 ? (
logs.slice(-50).map(log => (
<View key={log.id} style={styles.logEntry}>
<Text style={styles.logTime}>{formatTime(log.timestamp)}</Text>
<Text
style={[
styles.logSource,
{ color: SOURCE_COLORS[log.source] || SOURCE_COLORS.default },
]}
>
[{log.source}]
</Text>
<Text
style={[
styles.logMessage,
log.level === 'error' && styles.logError,
log.level === 'warn' && styles.logWarn,
]}
numberOfLines={2}
>
{log.message}
</Text>
</View>
))
) : (
<Text style={styles.emptyLog}>Noch keine Logs empfangen</Text>
)
) : (
events.length > 0 ? (
events.slice(-30).map(event => (
<View key={event.id} style={styles.eventEntry}>
<Text style={styles.eventTime}>{formatTime(event.timestamp)}</Text>
<Text style={styles.eventTitle}>{event.title}</Text>
<Text style={styles.eventDescription}>{event.description}</Text>
</View>
))
) : (
<Text style={styles.emptyLog}>Noch keine Events empfangen</Text>
)
)}
</View>
{/* Log-Aktionen */}
<TouchableOpacity
style={styles.clearButton}
onPress={() => {
if (logTab === 'live') setLogs([]);
else setEvents([]);
}}
>
<Text style={styles.clearButtonText}>Protokoll l\u00F6schen</Text>
</TouchableOpacity>
</View>
{/* === About === */}
<Text style={styles.sectionTitle}>{'\u00DC'}ber</Text>
<View style={styles.card}>
<Text style={styles.aboutTitle}>ARIA Cockpit</Text>
<Text style={styles.aboutVersion}>Version 0.0.2.1 </Text>
<Text style={styles.aboutInfo}>
Stefans Kommandozentrale f{'\u00FC'}r ARIA.{'\n'}
Gebaut mit React Native + TypeScript.
</Text>
</View>
{/* Platz am Ende */}
<View style={styles.bottomSpacer} />
</ScrollView>
</>
);
};
// --- 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,
},
// 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,
},
});
export default SettingsScreen;