first release 0.0.0.2

This commit is contained in:
2026-03-08 23:31:46 +01:00
parent ea52a4cec4
commit 5eb3ebf199
1432 changed files with 99065 additions and 60 deletions
+496
View File
@@ -0,0 +1,496 @@
/**
* ChatScreen - Hauptchat-Oberflaeche
*
* Zeigt die Konversation mit ARIA, Texteingabe, Sprach-Button,
* Datei- und Kamera-Upload.
*/
import React, { useState, useEffect, useRef, useCallback } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
FlatList,
KeyboardAvoidingView,
Platform,
StyleSheet,
Modal,
} from 'react-native';
import rvs, { RVSMessage, ConnectionState } from '../services/rvs';
import audioService from '../services/audio';
import VoiceButton from '../components/VoiceButton';
import FileUpload, { FileData } from '../components/FileUpload';
import CameraUpload, { PhotoData } from '../components/CameraUpload';
import { RecordingResult } from '../services/audio';
import Geolocation from '@react-native-community/geolocation';
// --- Typen ---
interface Attachment {
type: 'image' | 'file' | 'audio';
name: string;
size?: number;
}
interface ChatMessage {
id: string;
sender: 'user' | 'aria';
text: string;
timestamp: number;
attachments?: Attachment[];
}
// --- Komponente ---
const ChatScreen: React.FC = () => {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [inputText, setInputText] = useState('');
const [connectionState, setConnectionState] = useState<ConnectionState>('disconnected');
const [showFileUpload, setShowFileUpload] = useState(false);
const [showCameraUpload, setShowCameraUpload] = useState(false);
const [gpsEnabled, setGpsEnabled] = useState(false);
const flatListRef = useRef<FlatList>(null);
const messageIdCounter = useRef(0);
// Eindeutige Message-ID generieren
const nextId = (): string => {
messageIdCounter.current += 1;
return `msg_${Date.now()}_${messageIdCounter.current}`;
};
// GPS-Einstellung aus Settings laden (vereinfacht)
useEffect(() => {
// In Produktion: AsyncStorage oder Context verwenden
// Hier Platzhalter - GPS Toggle kommt aus SettingsScreen
}, []);
// RVS-Nachrichten abonnieren
useEffect(() => {
const unsubMessage = rvs.onMessage((message: RVSMessage) => {
if (message.type === 'chat') {
const ariaMsg: ChatMessage = {
id: nextId(),
sender: 'aria',
text: (message.payload.text as string) || '',
timestamp: message.timestamp,
attachments: message.payload.attachments as Attachment[] | undefined,
};
setMessages(prev => [...prev, ariaMsg]);
}
// TTS-Audio abspielen wenn vorhanden
if (message.type === 'audio' && message.payload.base64) {
audioService.playAudio(message.payload.base64 as string);
}
});
const unsubState = rvs.onStateChange((state) => {
setConnectionState(state);
});
// Initalen Status setzen
setConnectionState(rvs.getState());
return () => {
unsubMessage();
unsubState();
};
}, []);
// Auto-Scroll bei neuen Nachrichten
useEffect(() => {
if (messages.length > 0) {
setTimeout(() => {
flatListRef.current?.scrollToEnd({ animated: true });
}, 100);
}
}, [messages]);
// GPS-Position holen (optional)
const getCurrentLocation = useCallback((): Promise<{ lat: number; lon: number } | null> => {
if (!gpsEnabled) return Promise.resolve(null);
return new Promise((resolve) => {
Geolocation.getCurrentPosition(
(position) => {
resolve({
lat: position.coords.latitude,
lon: position.coords.longitude,
});
},
(_error) => {
resolve(null);
},
{ enableHighAccuracy: false, timeout: 5000 },
);
});
}, [gpsEnabled]);
// --- Nachricht senden ---
const sendTextMessage = useCallback(async () => {
const text = inputText.trim();
if (!text) return;
setInputText('');
const location = await getCurrentLocation();
const userMsg: ChatMessage = {
id: nextId(),
sender: 'user',
text,
timestamp: Date.now(),
};
setMessages(prev => [...prev, userMsg]);
// An RVS senden
rvs.send('chat', {
text,
...(location && { location }),
});
}, [inputText, getCurrentLocation]);
// Sprachaufnahme abgeschlossen
const handleVoiceRecording = useCallback(async (result: RecordingResult) => {
const location = await getCurrentLocation();
const userMsg: ChatMessage = {
id: nextId(),
sender: 'user',
text: '[Sprachnachricht]',
timestamp: Date.now(),
attachments: [{ type: 'audio', name: 'Sprachaufnahme' }],
};
setMessages(prev => [...prev, userMsg]);
rvs.send('audio', {
base64: result.base64,
durationMs: result.durationMs,
mimeType: result.mimeType,
...(location && { location }),
});
}, [getCurrentLocation]);
// Datei senden
const handleFileSelected = useCallback(async (file: FileData) => {
setShowFileUpload(false);
const location = await getCurrentLocation();
const userMsg: ChatMessage = {
id: nextId(),
sender: 'user',
text: `[Datei: ${file.name}]`,
timestamp: Date.now(),
attachments: [{ type: 'file', name: file.name, size: file.size }],
};
setMessages(prev => [...prev, userMsg]);
rvs.send('file', {
name: file.name,
type: file.type,
size: file.size,
base64: file.base64,
...(location && { location }),
});
}, [getCurrentLocation]);
// Foto senden
const handlePhotoSelected = useCallback(async (photo: PhotoData) => {
setShowCameraUpload(false);
const location = await getCurrentLocation();
const userMsg: ChatMessage = {
id: nextId(),
sender: 'user',
text: `[Foto: ${photo.fileName}]`,
timestamp: Date.now(),
attachments: [{ type: 'image', name: photo.fileName }],
};
setMessages(prev => [...prev, userMsg]);
rvs.send('file', {
name: photo.fileName,
type: photo.type,
base64: photo.base64,
width: photo.width,
height: photo.height,
...(location && { location }),
});
}, [getCurrentLocation]);
// --- Rendering ---
const renderMessage = ({ item }: { item: ChatMessage }) => {
const isUser = item.sender === 'user';
const time = new Date(item.timestamp).toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit',
});
return (
<View style={[styles.messageBubble, isUser ? styles.userBubble : styles.ariaBubble]}>
<Text style={[styles.messageText, isUser ? styles.userText : styles.ariaText]}>
{item.text}
</Text>
{item.attachments?.map((att, idx) => (
<View key={idx} style={styles.attachmentBadge}>
<Text style={styles.attachmentText}>
{att.type === 'image' ? '\uD83D\uDDBC\uFE0F' : att.type === 'audio' ? '\uD83C\uDFA4' : '\uD83D\uDCC4'} {att.name}
</Text>
</View>
))}
<Text style={styles.timestamp}>{time}</Text>
</View>
);
};
const connectionDotColor =
connectionState === 'connected' ? '#34C759' :
connectionState === 'connecting' ? '#FFD60A' : '#FF3B30';
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={90}
>
{/* Verbindungsstatus-Leiste */}
<View style={styles.statusBar}>
<View style={[styles.statusDot, { backgroundColor: connectionDotColor }]} />
<Text style={styles.statusText}>
{connectionState === 'connected' ? 'Verbunden' :
connectionState === 'connecting' ? 'Verbinde...' : 'Getrennt'}
</Text>
</View>
{/* Nachrichtenliste */}
<FlatList
ref={flatListRef}
data={messages}
keyExtractor={item => item.id}
renderItem={renderMessage}
contentContainerStyle={styles.messageList}
showsVerticalScrollIndicator={false}
ListEmptyComponent={
<View style={styles.emptyContainer}>
<Text style={styles.emptyIcon}>{'\uD83E\uDD16'}</Text>
<Text style={styles.emptyText}>ARIA Cockpit</Text>
<Text style={styles.emptyHint}>Starte eine Konversation mit ARIA</Text>
</View>
}
/>
{/* Eingabebereich */}
<View style={styles.inputContainer}>
{/* Datei-Buttons */}
<TouchableOpacity
style={styles.actionButton}
onPress={() => setShowFileUpload(true)}
>
<Text style={styles.actionIcon}>{'\uD83D\uDCCE'}</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.actionButton}
onPress={() => setShowCameraUpload(true)}
>
<Text style={styles.actionIcon}>{'\uD83D\uDCF7'}</Text>
</TouchableOpacity>
{/* Texteingabe */}
<TextInput
style={styles.textInput}
value={inputText}
onChangeText={setInputText}
placeholder="Nachricht an ARIA..."
placeholderTextColor="#555570"
multiline
maxLength={4000}
onSubmitEditing={sendTextMessage}
returnKeyType="send"
/>
{/* Senden oder Sprache */}
{inputText.trim() ? (
<TouchableOpacity style={styles.sendButton} onPress={sendTextMessage}>
<Text style={styles.sendIcon}>{'\u2B06\uFE0F'}</Text>
</TouchableOpacity>
) : (
<VoiceButton
onRecordingComplete={handleVoiceRecording}
disabled={connectionState !== 'connected'}
/>
)}
</View>
{/* Datei-Upload Modal */}
<Modal visible={showFileUpload} transparent animationType="slide">
<View style={styles.modalOverlay}>
<FileUpload
onFileSelected={handleFileSelected}
onCancel={() => setShowFileUpload(false)}
/>
</View>
</Modal>
{/* Kamera-Upload Modal */}
<Modal visible={showCameraUpload} transparent animationType="slide">
<View style={styles.modalOverlay}>
<CameraUpload
onPhotoSelected={handlePhotoSelected}
onCancel={() => setShowCameraUpload(false)}
/>
</View>
</Modal>
</KeyboardAvoidingView>
);
};
// --- Styles ---
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#0D0D1A',
},
statusBar: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 8,
backgroundColor: '#12122A',
borderBottomWidth: 1,
borderBottomColor: '#1E1E2E',
},
statusDot: {
width: 8,
height: 8,
borderRadius: 4,
marginRight: 8,
},
statusText: {
color: '#8888AA',
fontSize: 12,
},
messageList: {
padding: 12,
paddingBottom: 8,
flexGrow: 1,
},
messageBubble: {
maxWidth: '80%',
padding: 12,
borderRadius: 16,
marginBottom: 8,
},
userBubble: {
alignSelf: 'flex-end',
backgroundColor: '#0096FF',
borderBottomRightRadius: 4,
},
ariaBubble: {
alignSelf: 'flex-start',
backgroundColor: '#1E1E2E',
borderBottomLeftRadius: 4,
},
messageText: {
fontSize: 15,
lineHeight: 21,
},
userText: {
color: '#FFFFFF',
},
ariaText: {
color: '#E0E0F0',
},
attachmentBadge: {
backgroundColor: 'rgba(255,255,255,0.1)',
borderRadius: 6,
paddingHorizontal: 8,
paddingVertical: 4,
marginTop: 6,
alignSelf: 'flex-start',
},
attachmentText: {
color: '#CCCCDD',
fontSize: 12,
},
timestamp: {
color: 'rgba(255,255,255,0.4)',
fontSize: 10,
marginTop: 4,
alignSelf: 'flex-end',
},
emptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingTop: 120,
},
emptyIcon: {
fontSize: 48,
marginBottom: 12,
},
emptyText: {
color: '#FFFFFF',
fontSize: 22,
fontWeight: '700',
},
emptyHint: {
color: '#555570',
fontSize: 14,
marginTop: 4,
},
inputContainer: {
flexDirection: 'row',
alignItems: 'flex-end',
paddingHorizontal: 10,
paddingVertical: 8,
backgroundColor: '#12122A',
borderTopWidth: 1,
borderTopColor: '#1E1E2E',
},
actionButton: {
width: 38,
height: 38,
borderRadius: 19,
alignItems: 'center',
justifyContent: 'center',
marginRight: 4,
},
actionIcon: {
fontSize: 20,
},
textInput: {
flex: 1,
backgroundColor: '#1E1E2E',
borderRadius: 20,
paddingHorizontal: 16,
paddingVertical: 10,
color: '#FFFFFF',
fontSize: 15,
maxHeight: 100,
marginHorizontal: 6,
},
sendButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#0096FF',
alignItems: 'center',
justifyContent: 'center',
},
sendIcon: {
fontSize: 18,
},
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.6)',
justifyContent: 'center',
},
});
export default ChatScreen;
+605
View File
@@ -0,0 +1,605 @@
/**
* 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;