/** * DesktopTile — das Desktop-Panel eines Code-Projekts. * * Zeigt die (pro Projekt gefuehrte) QEMU-VM-Liste: leer, bis ARIA per * vm_register eine VM eintraegt. Pro VM: Start / Stop / Verbinden. „Verbinden" * oeffnet die noVNC-Ansicht (VncTile) fuer den VNC-Port dieser VM. */ import React, { useCallback, useEffect, useState } from 'react'; import { ActivityIndicator, Image, Modal, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import brainApi, { ProjectVm } from '../../services/brainApi'; import VncTile from './VncTile'; interface Props { projectId: string; focused: boolean; } const DesktopTile: React.FC = ({ projectId, focused }) => { const [vms, setVms] = useState([]); const [loading, setLoading] = useState(false); const [err, setErr] = useState(''); const [busy, setBusy] = useState(''); // VM-Name, der gerade bootet/stoppt const [connected, setConnected] = useState(null); const [shotBusy, setShotBusy] = useState(''); const [shot, setShot] = useState<{ name: string; b64: string } | null>(null); const load = useCallback(() => { if (!projectId) { setVms([]); setErr(''); setLoading(false); return; } setLoading(true); setErr(''); brainApi.listProjectVms(projectId) .then(r => setVms(r.vms || [])) .catch(e => setErr(String(e?.message || e))) .finally(() => setLoading(false)); }, [projectId]); useEffect(() => { if (focused && !connected) load(); }, [focused, projectId, connected, load]); const boot = useCallback((vm: ProjectVm) => { setBusy(vm.name); brainApi.bootProjectVm(projectId, vm.name) .then(() => load()) .catch(e => setErr(String(e?.message || e))) .finally(() => setBusy('')); }, [projectId, load]); const stop = useCallback((vm: ProjectVm) => { setBusy(vm.name); brainApi.stopProjectVm(projectId, vm.name) .then(() => load()) .catch(e => setErr(String(e?.message || e))) .finally(() => setBusy('')); }, [projectId, load]); const screenshot = useCallback((vm: ProjectVm) => { setShotBusy(vm.name); setErr(''); brainApi.screenshotProjectVm(projectId, vm.name) .then(r => setShot({ name: vm.name, b64: r.base64 })) .catch(e => setErr(String(e?.message || e))) .finally(() => setShotBusy('')); }, [projectId]); if (!focused) { return ( 🖥️ Desktop Panel öffnen für VM-Liste ); } return ( Virtuelle Maschinen {loading && vms.length === 0 ? ( ) : err ? ( {err} ) : !projectId ? ( Kein aktives Projekt — wechsle in ein Projekt für dessen VMs. ) : vms.length === 0 ? ( Noch keine VM in diesem Projekt.{'\n'} Sag ARIA z.B. „bau eine QEMU-VM zum Testen" — sie registriert sie hier, dann kannst du sie starten und verbinden. ) : ( vms.map(vm => { const isBusy = busy === vm.name; return ( {vm.name} {vm.arch} · {vm.running ? 'läuft' : 'gestoppt'} {vm.boot_cmd || `aria-vm boot ${vm.name} --vnc-display ${vm.vnc_display}`} {isBusy ? ( ) : vm.running ? ( <> screenshot(vm)} style={[styles.vmBtn, { borderColor: '#8888AA' }]} disabled={shotBusy === vm.name}> {shotBusy === vm.name ? : 📷} setConnected(vm)} style={[styles.vmBtn, { borderColor: '#0096FF' }]}> Verbinden stop(vm)} style={[styles.vmBtn, { borderColor: '#E55C5C' }]}> Stop ) : ( boot(vm)} style={[styles.vmBtn, { borderColor: '#34C759' }]}> Start )} ); }) )} setShot(null)}> setShot(null)}> {shot?.name} — Screenshot {shot && ( )} Tippen zum Schließen {/* Vollbild-VNC — randlos ueber das ganze Display (Header + Dock weg). */} {connected && ( setConnected(null)} supportedOrientations={['portrait', 'landscape']}> setConnected(null)} activeOpacity={0.8}> ‹ VMs )} ); }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#0D0D1A' }, placeholder: { flex: 1, backgroundColor: '#000', alignItems: 'center', justifyContent: 'center' }, icon: { fontSize: 64, marginBottom: 16 }, text: { color: '#FFFFFF', fontSize: 18, fontWeight: '700' }, sub: { color: '#9090B0', fontSize: 14, marginTop: 8 }, bar: { height: 40, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 12, backgroundColor: '#12122A', borderBottomColor: '#1E1E2E', borderBottomWidth: 1 }, barTitle: { color: '#E0E0F0', fontSize: 14, fontWeight: '700', flex: 1 }, barBtn: { paddingHorizontal: 10, paddingVertical: 4 }, barBtnText: { color: '#0096FF', fontSize: 14, fontWeight: '700' }, empty: { color: '#8888AA', fontSize: 13, lineHeight: 20, textAlign: 'center', marginTop: 24 }, err: { color: '#FF6E6E', fontSize: 13, marginTop: 16 }, vmRow: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#12122A', borderRadius: 10, padding: 12, marginBottom: 8 }, vmName: { color: '#E0E0F0', fontSize: 15, fontWeight: '700' }, vmMeta: { color: '#8888AA', fontSize: 12, fontWeight: '400' }, vmCmd: { color: '#6A9BD0', fontSize: 11, fontFamily: 'monospace', marginTop: 4 }, vmBtns: { flexDirection: 'row', gap: 6, alignItems: 'center' }, vmBtn: { borderWidth: 1, borderRadius: 8, paddingHorizontal: 10, paddingVertical: 6, minWidth: 34, alignItems: 'center' }, vmBtnText: { fontSize: 12, fontWeight: '700' }, fs: { flex: 1, backgroundColor: '#000000' }, fsBack: { position: 'absolute', top: 34, left: 10, backgroundColor: 'rgba(18,18,42,0.9)', borderColor: '#2A2A3E', borderWidth: 1, borderRadius: 10, paddingHorizontal: 12, paddingVertical: 7 }, fsBackText: { color: '#0096FF', fontSize: 14, fontWeight: '700' }, shotOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.92)', alignItems: 'center', justifyContent: 'center', padding: 12 }, shotTitle: { color: '#E0E0F0', fontSize: 14, fontWeight: '700', marginBottom: 10 }, shotImg: { width: '100%', height: '78%', backgroundColor: '#000' }, shotHint: { color: '#8888AA', fontSize: 12, marginTop: 12 }, }); export default DesktopTile;