/** * 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, 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 load = useCallback(() => { 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]); if (!focused) { return ( 🖥️ Desktop Panel öffnen für VM-Liste ); } // Verbunden → noVNC-Ansicht der VM + Zurück-Leiste. if (connected) { return ( setConnected(null)} style={styles.barBtn}> ‹ VMs {connected.name} · :{connected.vnc_display} ); } return ( Virtuelle Maschinen {loading && vms.length === 0 ? ( ) : err ? ( {err} ) : 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 ? ( <> setConnected(vm)} style={[styles.vmBtn, { borderColor: '#0096FF' }]}> Verbinden stop(vm)} style={[styles.vmBtn, { borderColor: '#E55C5C' }]}> Stop ) : ( boot(vm)} style={[styles.vmBtn, { borderColor: '#34C759' }]}> Start )} ); }) )} ); }; 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 }, vmBtnText: { fontSize: 12, fontWeight: '700' }, }); export default DesktopTile;