606 lines
15 KiB
TypeScript
606 lines
15 KiB
TypeScript
/**
|
|
* SettingsScreen - Einstellungen und Verbindungsverwaltung
|
|
*
|
|
* QR-Scanner fuer Pairing, Moduswahl, GPS-Toggle, Log-Viewer.
|
|
*/
|
|
|
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
TextInput,
|
|
TouchableOpacity,
|
|
ScrollView,
|
|
Switch,
|
|
StyleSheet,
|
|
Alert,
|
|
Platform,
|
|
} from 'react-native';
|
|
import rvs, { ConnectionState, RVSMessage, ConnectionConfig } from '../services/rvs';
|
|
import ModeSelector from '../components/ModeSelector';
|
|
|
|
// --- 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 [logTab, setLogTab] = useState<LogTab>('live');
|
|
const [logs, setLogs] = useState<LogEntry[]>([]);
|
|
const [events, setEvents] = useState<EventEntry[]>([]);
|
|
|
|
let logIdCounter = 0;
|
|
|
|
// RVS-Nachrichten abonnieren (Logs und Events)
|
|
useEffect(() => {
|
|
const unsubState = rvs.onStateChange(setConnectionState);
|
|
setConnectionState(rvs.getState());
|
|
|
|
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();
|
|
};
|
|
}, []);
|
|
|
|
// --- QR-Code scannen ---
|
|
|
|
const openQRScanner = useCallback(() => {
|
|
// In Produktion: QR-Scanner oeffnen (react-native-camera)
|
|
// Format: aria://host:port?token=xxx&tls=1
|
|
Alert.alert(
|
|
'QR-Scanner',
|
|
'QR-Code Scanner wird in der naechsten Version implementiert.\n\nBitte Token manuell eingeben.',
|
|
);
|
|
}, []);
|
|
|
|
// --- 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 (
|
|
<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>
|
|
|
|
{/* === 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>
|
|
|
|
{/* === 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.1.0 (Alpha)</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,
|
|
},
|
|
|
|
// 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',
|
|
},
|
|
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;
|