/** * VncTile — Live-Desktop der QEMU-VM (noVNC in einer WebView, RFB durch RVS). * * Nur aktiv, wenn das Desktop-Panel offen ist (focused): dann WebView mounten, * bei 'ready' den RVS-VNC-Tunnel oeffnen. Eine kleine Steuerungs-Leiste macht * die VM auf dem Handy bedienbar: Tastatur einblenden (tippt in die VM), * Strg-Alt-Entf, und Fit ↔ 1:1 umschalten. */ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { WebView, WebViewMessageEvent } from 'react-native-webview'; import desktop from '../../services/desktop'; import { NOVNC_HTML } from '../assets/novncHtml'; interface Props { projectId: string; focused: boolean; } const VncTile: React.FC = ({ projectId, focused }) => { const webRef = useRef(null); const [status, setStatus] = useState<'idle' | 'connecting' | 'connected' | 'disconnected'>('idle'); const unsubDataRef = useRef void)>(null); const teardown = useCallback(() => { if (unsubDataRef.current) { unsubDataRef.current(); unsubDataRef.current = null; } desktop.closeVnc(); }, []); useEffect(() => { if (!focused) { teardown(); setStatus('idle'); } return () => teardown(); }, [focused, teardown]); const ctl = useCallback((fn: string) => { webRef.current?.injectJavaScript(`window.ariaVncCtl && window.ariaVncCtl.${fn}(); true;`); }, []); const onMessage = useCallback((e: WebViewMessageEvent) => { let m: any; try { m = JSON.parse(e.nativeEvent.data); } catch { return; } if (m.event === 'ready') { setStatus('connecting'); unsubDataRef.current = desktop.onVncData((b64) => { const js = `window.ariaVnc && window.ariaVnc.onData(${JSON.stringify(b64)}); true;`; webRef.current?.injectJavaScript(js); }); desktop.openVnc(projectId); } else if (m.event === 'vnc_send') { desktop.sendInput(m.b64); } else if (m.event === 'vnc_close') { desktop.closeVnc(); } else if (m.event === 'vnc_state') { if (m.state === 'connected') setStatus('connected'); else if (m.state === 'disconnected') setStatus('disconnected'); } }, [projectId]); if (!focused) { return ( 🖥️ Desktop Panel öffnen zum Verbinden ); } const connected = status === 'connected'; return ( {/* Steuerungs-Leiste — nur wenn verbunden */} {connected && ( ctl('focusKeyboard')} activeOpacity={0.7}> ctl('cad')} activeOpacity={0.7}> Strg+Alt+Entf ctl('toggleFit')} activeOpacity={0.7}> )} {!connected && ( {status === 'connecting' ? 'Verbinde …' : status === 'disconnected' ? 'Getrennt' : ''} )} ); }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#000000' }, web: { flex: 1, backgroundColor: '#000000' }, placeholder: { flex: 1, backgroundColor: '#000000', alignItems: 'center', justifyContent: 'center' }, icon: { fontSize: 64, marginBottom: 16 }, text: { color: '#FFFFFF', fontSize: 18, fontWeight: '700' }, sub: { color: '#9090B0', fontSize: 14, marginTop: 8 }, ctlBar: { position: 'absolute', top: 8, right: 8, flexDirection: 'row', gap: 6, }, ctlBtn: { backgroundColor: 'rgba(18,18,42,0.9)', borderColor: '#2A2A3E', borderWidth: 1, borderRadius: 10, paddingHorizontal: 10, paddingVertical: 7, minWidth: 38, alignItems: 'center', justifyContent: 'center', }, ctlText: { color: '#E0E0F0', fontSize: 16, fontWeight: '700' }, ctlTextSmall: { color: '#E0E0F0', fontSize: 11, fontWeight: '700' }, overlay: { position: 'absolute', top: 12, left: 0, right: 0, alignItems: 'center' }, overlayText: { color: '#9090B0', fontSize: 12, backgroundColor: 'rgba(0,0,0,0.6)', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 10, overflow: 'hidden' }, }); export default VncTile;