feat(app): Workbench-Dock statt Zoom-Landkarte + bedienbare VNC-Konsole (0.2.2.0)

Cockpit neu gedacht — das Pinch-Zoom-Landkarten-Konzept war auf 5 Zoll fummelig:
- WorkspaceDeck + WorkspaceDock: Taskleiste unten (Chat · Code · Desktop),
  Ein-Tap-Panelwechsel im Daumenbereich, animierter Indikator, Aktivitaets-
  Badges (Code blau / Desktop gruen), Safe-Area, blendet bei offener Tastatur
  aus. Panels bildschirmfuellend + immer gemountet (kein Remount).
- noVNC bedienbar: novncHtml bekommt Tastatur-Bridge (verstecktes Input-Feld →
  rfb.sendKey inkl. Enter/Backspace/Pfeile), Strg-Alt-Entf, Fit↔1:1. VncTile
  zeigt eine Steuerungs-Leiste (⌨ / Strg+Alt+Entf / ⤢).
- Entfernt: WorkspaceCanvas, Tile, PreviewTile, CockpitOverviewButton,
  cockpitNav (Pinch-Canvas + Uebersichts-Button obsolet). layout.ts auf
  Panel-Metadaten geschlankt. Header nur noch mit Kompakt/Cockpit-Umschalter.

tsc clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 21:44:32 +02:00
co-authored by Claude Opus 4.8
parent 7d46d8e531
commit 0110526622
14 changed files with 317 additions and 551 deletions
@@ -1,40 +0,0 @@
/**
* PreviewTile — zeigt den letzten Screenshot/Vorschau-Frame eines Code-Projekts
* (z. B. aria-vm screenshot). Fuellt sich, sobald ARIA ein Bild in den
* Vorschau-Kanal legt; bis dahin ein ruhiger Platzhalter.
*
* (Screenshot-Anbindung folgt mit dem QEMU-Track; hier zunaechst die Kachel.)
*/
import React from 'react';
import { Image, StyleSheet, Text, View } from 'react-native';
interface Props {
imageUri?: string;
}
const PreviewTile: React.FC<Props> = ({ imageUri }) => {
return (
<View style={styles.container}>
{imageUri ? (
<Image source={{ uri: imageUri }} style={styles.image} resizeMode="contain" />
) : (
<>
<Text style={styles.icon}>🖼</Text>
<Text style={styles.text}>Noch keine Vorschau</Text>
<Text style={styles.sub}>Screenshots der VM erscheinen hier.</Text>
</>
)}
</View>
);
};
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#0D0D1A', alignItems: 'center', justifyContent: 'center' },
image: { width: '100%', height: '100%' },
icon: { fontSize: 64, marginBottom: 16 },
text: { color: '#FFFFFF', fontSize: 18, fontWeight: '700' },
sub: { color: '#9090B0', fontSize: 14, marginTop: 8 },
});
export default PreviewTile;
+50 -12
View File
@@ -1,15 +1,14 @@
/**
* VncTile — Live-Desktop der QEMU-VM (noVNC in einer WebView, RFB durch RVS).
*
* Nur im Fokus aktiv: dann wird die WebView gemountet, bei 'ready' der
* RVS-VNC-Tunnel geoeffnet (desktop.openVnc). Server-Bytes (vnc_data) werden in
* die WebView injiziert, RFB-Bytes der WebView (vnc_send) gehen als vnc_input
* zurueck. Verlaesst man die Kachel, wird der Tunnel geschlossen (die VM laeuft
* auf dem Host weiter).
* 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, View } from 'react-native';
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';
@@ -24,23 +23,24 @@ const VncTile: React.FC<Props> = ({ projectId, focused }) => {
const [status, setStatus] = useState<'idle' | 'connecting' | 'connected' | 'disconnected'>('idle');
const unsubDataRef = useRef<null | (() => void)>(null);
// Aufraeumen: Tunnel zu + Daten-Abo weg.
const teardown = useCallback(() => {
if (unsubDataRef.current) { unsubDataRef.current(); unsubDataRef.current = null; }
desktop.closeVnc();
}, []);
// Fokus verloren / Unmount → Tunnel schliessen.
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') {
// WebView + RFB bereit → Tunnel oeffnen und Server-Bytes einspeisen.
setStatus('connecting');
unsubDataRef.current = desktop.onVncData((b64) => {
const js = `window.ariaVnc && window.ariaVnc.onData(${JSON.stringify(b64)}); true;`;
@@ -62,11 +62,12 @@ const VncTile: React.FC<Props> = ({ projectId, focused }) => {
<View style={styles.placeholder}>
<Text style={styles.icon}>🖥</Text>
<Text style={styles.text}>Desktop</Text>
<Text style={styles.sub}>Antippen zum Verbinden</Text>
<Text style={styles.sub}>Panel öffnen zum Verbinden</Text>
</View>
);
}
const connected = status === 'connected';
return (
<View style={styles.container}>
<WebView
@@ -79,8 +80,25 @@ const VncTile: React.FC<Props> = ({ projectId, focused }) => {
domStorageEnabled
mixedContentMode="always"
androidLayerType="hardware"
keyboardDisplayRequiresUserAction={false}
/>
{status !== 'connected' && (
{/* Steuerungs-Leiste — nur wenn verbunden */}
{connected && (
<View style={styles.ctlBar}>
<TouchableOpacity style={styles.ctlBtn} onPress={() => ctl('focusKeyboard')} activeOpacity={0.7}>
<Text style={styles.ctlText}></Text>
</TouchableOpacity>
<TouchableOpacity style={styles.ctlBtn} onPress={() => ctl('cad')} activeOpacity={0.7}>
<Text style={styles.ctlTextSmall}>Strg+Alt+Entf</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.ctlBtn} onPress={() => ctl('toggleFit')} activeOpacity={0.7}>
<Text style={styles.ctlText}></Text>
</TouchableOpacity>
</View>
)}
{!connected && (
<View style={styles.overlay} pointerEvents="none">
<Text style={styles.overlayText}>
{status === 'connecting' ? 'Verbinde …' : status === 'disconnected' ? 'Getrennt' : ''}
@@ -98,7 +116,27 @@ const styles = StyleSheet.create({
icon: { fontSize: 64, marginBottom: 16 },
text: { color: '#FFFFFF', fontSize: 18, fontWeight: '700' },
sub: { color: '#9090B0', fontSize: 14, marginTop: 8 },
overlay: { position: 'absolute', top: 10, left: 0, right: 0, alignItems: 'center' },
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' },
});