version 0.0.0.3
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* QRScanner - Vollbild QR-Code Scanner fuer ARIA Pairing
|
||||
*
|
||||
* Scannt QR-Codes im Format:
|
||||
* {"host": "rvs.hackersoft.de", "port": 443, "token": "a3f8b2c9..."}
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
Modal,
|
||||
Alert,
|
||||
Platform,
|
||||
PermissionsAndroid,
|
||||
} from 'react-native';
|
||||
import { CameraScreen } from 'react-native-camera-kit';
|
||||
import { ConnectionConfig } from '../services/rvs';
|
||||
|
||||
interface QRScannerProps {
|
||||
visible: boolean;
|
||||
onScan: (config: ConnectionConfig) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/** QR-Daten parsen und validieren */
|
||||
function parseQRData(data: string): ConnectionConfig | null {
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
|
||||
if (!parsed.host || !parsed.token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
host: String(parsed.host),
|
||||
port: Number(parsed.port) || 443,
|
||||
token: String(parsed.token),
|
||||
useTLS: parsed.tls !== false, // Standard: TLS an
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Kamera-Berechtigung anfordern (Android) */
|
||||
async function requestCameraPermission(): Promise<boolean> {
|
||||
if (Platform.OS !== 'android') return true;
|
||||
|
||||
try {
|
||||
const granted = await PermissionsAndroid.request(
|
||||
PermissionsAndroid.PERMISSIONS.CAMERA,
|
||||
{
|
||||
title: 'Kamera-Zugriff',
|
||||
message: 'ARIA Cockpit braucht Kamera-Zugriff um den QR-Code zu scannen.',
|
||||
buttonPositive: 'Erlauben',
|
||||
buttonNegative: 'Ablehnen',
|
||||
},
|
||||
);
|
||||
return granted === PermissionsAndroid.RESULTS.GRANTED;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const QRScanner: React.FC<QRScannerProps> = ({ visible, onScan, onClose }) => {
|
||||
const [hasPermission, setHasPermission] = useState<boolean | null>(null);
|
||||
const [scanned, setScanned] = useState(false);
|
||||
|
||||
// Berechtigung pruefen beim Oeffnen
|
||||
React.useEffect(() => {
|
||||
if (visible) {
|
||||
setScanned(false);
|
||||
requestCameraPermission().then(granted => {
|
||||
setHasPermission(granted);
|
||||
if (!granted) {
|
||||
Alert.alert(
|
||||
'Kamera blockiert',
|
||||
'Bitte erlaube den Kamera-Zugriff in den Einstellungen.',
|
||||
[{ text: 'OK', onPress: onClose }],
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [visible, onClose]);
|
||||
|
||||
const handleBarcodeScan = useCallback(
|
||||
(event: { nativeEvent: { codeStringValue: string } }) => {
|
||||
if (scanned) return;
|
||||
|
||||
const data = event.nativeEvent.codeStringValue;
|
||||
const config = parseQRData(data);
|
||||
|
||||
if (config) {
|
||||
setScanned(true);
|
||||
onScan(config);
|
||||
} else {
|
||||
// Ungueltig — einmal warnen, dann weiter scannen lassen
|
||||
setScanned(true);
|
||||
Alert.alert(
|
||||
'Ungueltiger QR-Code',
|
||||
'Der QR-Code hat nicht das erwartete ARIA-Format.\n\nErwartet: {"host": "...", "port": 443, "token": "..."}',
|
||||
[{ text: 'Nochmal', onPress: () => setScanned(false) }],
|
||||
);
|
||||
}
|
||||
},
|
||||
[scanned, onScan],
|
||||
);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
animationType="slide"
|
||||
presentationStyle="fullScreen"
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<View style={styles.container}>
|
||||
{hasPermission ? (
|
||||
<>
|
||||
<CameraScreen
|
||||
scanBarcode={true}
|
||||
onReadCode={handleBarcodeScan}
|
||||
showFrame={true}
|
||||
frameColor="#0096FF"
|
||||
laserColor="#0096FF"
|
||||
colorForScannerFrame="#0096FF"
|
||||
/>
|
||||
|
||||
{/* Overlay oben */}
|
||||
<View style={styles.topOverlay}>
|
||||
<Text style={styles.title}>QR-Code scannen</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
Richte die Kamera auf den QR-Code vom RVS
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Abbrechen-Button unten */}
|
||||
<View style={styles.bottomOverlay}>
|
||||
<TouchableOpacity style={styles.cancelButton} onPress={onClose}>
|
||||
<Text style={styles.cancelText}>Abbrechen</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<View style={styles.noPermission}>
|
||||
<Text style={styles.noPermissionText}>Kamera-Zugriff wird benoetigt</Text>
|
||||
<TouchableOpacity style={styles.cancelButton} onPress={onClose}>
|
||||
<Text style={styles.cancelText}>Zurueck</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#000000',
|
||||
},
|
||||
topOverlay: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
paddingTop: 60,
|
||||
paddingBottom: 20,
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
},
|
||||
subtitle: {
|
||||
color: '#AAAACC',
|
||||
fontSize: 14,
|
||||
marginTop: 6,
|
||||
textAlign: 'center',
|
||||
},
|
||||
bottomOverlay: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
paddingBottom: 40,
|
||||
paddingTop: 20,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
||||
alignItems: 'center',
|
||||
},
|
||||
cancelButton: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.15)',
|
||||
paddingHorizontal: 40,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 12,
|
||||
},
|
||||
cancelText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
noPermission: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
noPermissionText: {
|
||||
color: '#AAAACC',
|
||||
fontSize: 16,
|
||||
marginBottom: 20,
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
export default QRScanner;
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from 'react-native';
|
||||
import rvs, { ConnectionState, RVSMessage, ConnectionConfig } from '../services/rvs';
|
||||
import ModeSelector from '../components/ModeSelector';
|
||||
import QRScanner from '../components/QRScanner';
|
||||
|
||||
// --- Typen ---
|
||||
|
||||
@@ -56,6 +57,7 @@ const SettingsScreen: React.FC = () => {
|
||||
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[]>([]);
|
||||
@@ -105,11 +107,24 @@ const SettingsScreen: React.FC = () => {
|
||||
// --- QR-Code scannen ---
|
||||
|
||||
const openQRScanner = useCallback(() => {
|
||||
// In Produktion: QR-Scanner oeffnen (react-native-camera)
|
||||
// Format: aria://host:port?token=xxx&tls=1
|
||||
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(
|
||||
'QR-Scanner',
|
||||
'QR-Code Scanner wird in der naechsten Version implementiert.\n\nBitte Token manuell eingeben.',
|
||||
'Pairing erfolgreich',
|
||||
`Verbinde mit ${config.host}:${config.port}...`,
|
||||
);
|
||||
}, []);
|
||||
|
||||
@@ -170,6 +185,12 @@ const SettingsScreen: React.FC = () => {
|
||||
connectionState === 'connecting' ? 'Verbinde...' : 'Getrennt';
|
||||
|
||||
return (
|
||||
<>
|
||||
<QRScanner
|
||||
visible={scannerVisible}
|
||||
onScan={handleQRScan}
|
||||
onClose={() => setScannerVisible(false)}
|
||||
/>
|
||||
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
||||
|
||||
{/* === Verbindung === */}
|
||||
@@ -348,6 +369,7 @@ const SettingsScreen: React.FC = () => {
|
||||
{/* Platz am Ende */}
|
||||
<View style={styles.bottomSpacer} />
|
||||
</ScrollView>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user