Files
ARIA-AGENT/android/src/workspace/assets/novncHtml.ts
T
duffyduckandClaude Opus 4.8 0110526622 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>
2026-07-17 21:44:32 +02:00

126 lines
5.7 KiB
TypeScript

/**
* novncHtml — noVNC-Client fuer die WebView, dessen WebSocket durch den
* RVS-Tunnel gebrueckt wird.
*
* Trick: window.WebSocket wird VOR dem Laden von noVNC durch einen Shim
* ersetzt. noVNC (RFB) glaubt, ein echtes WebSocket zu benutzen; tatsaechlich
* gehen die RFB-Bytes als Base64 per postMessage an RN → RVS → Bridge → QEMU
* (und zurueck). Da RFB "server-speaks-first" ist, ist die Reihenfolge robust.
*
* noVNC wird vom CDN geladen (das Telefon hat Internet, da es ohnehin am RVS
* haengt). Voll-offline-Bundling waere ein spaeterer Schritt.
*
* Protokoll:
* RN -> WebView window.ariaVnc.onData(b64) RFB-Bytes vom Server
* WebView -> RN {event:'ready'} RFB initialisiert → Tunnel oeffnen
* {event:'vnc_send', b64} RFB-Bytes an den Server
* {event:'vnc_close'} RFB hat geschlossen
* {event:'vnc_state', state} connected|disconnected
*/
export const NOVNC_HTML = `<!doctype html><html><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<style>
* { margin:0; padding:0; }
html, body { height:100%; background:#000; overflow:hidden; }
#screen { width:100%; height:100%; }
#msg { position:absolute; top:8px; left:0; right:0; text-align:center;
color:#9090B0; font-family:sans-serif; font-size:12px; pointer-events:none; }
</style></head><body>
<div id="screen"></div>
<div id="msg">Verbinde mit Desktop …</div>
<script>
(function(){
function post(o){ if(window.ReactNativeWebView) window.ReactNativeWebView.postMessage(JSON.stringify(o)); }
function b64FromBytes(bytes){
var CHUNK=0x8000, parts=[];
for(var i=0;i<bytes.length;i+=CHUNK){ parts.push(String.fromCharCode.apply(null, bytes.subarray(i,i+CHUNK))); }
return btoa(parts.join(''));
}
function bytesFromB64(b64){
var s=atob(b64), a=new Uint8Array(s.length);
for(var i=0;i<s.length;i++) a[i]=s.charCodeAt(i);
return a;
}
// --- WebSocket-Shim ---
function BridgeSocket(url, protocols){
this.url=url; this.protocol=''; this.readyState=0; this.binaryType='arraybuffer';
this.onopen=null; this.onclose=null; this.onerror=null; this.onmessage=null;
var self=this; window.__vncSocket=self;
setTimeout(function(){ self.readyState=1; if(self.onopen) self.onopen({type:'open'}); }, 0);
}
BridgeSocket.CONNECTING=0; BridgeSocket.OPEN=1; BridgeSocket.CLOSING=2; BridgeSocket.CLOSED=3;
BridgeSocket.prototype.send=function(data){
var bytes;
if(data instanceof ArrayBuffer) bytes=new Uint8Array(data);
else if(ArrayBuffer.isView(data)) bytes=new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
else bytes=new Uint8Array(0);
post({event:'vnc_send', b64:b64FromBytes(bytes)});
};
BridgeSocket.prototype.close=function(){
if(this.readyState===3) return;
this.readyState=3; if(this.onclose) this.onclose({type:'close'}); post({event:'vnc_close'});
};
BridgeSocket.prototype.addEventListener=function(t,fn){ this['on'+t]=fn; };
BridgeSocket.prototype.removeEventListener=function(t){ this['on'+t]=null; };
window.WebSocket = BridgeSocket;
// Eingehende Server-Bytes → in den Shim einspeisen.
window.ariaVnc = {
onData:function(b64){
var sock=window.__vncSocket;
if(!sock || !sock.onmessage) return;
sock.onmessage({ type:'message', data: bytesFromB64(b64).buffer });
}
};
var msg=document.getElementById('msg');
// Verstecktes Eingabefeld → Software-Tastatur des Handys tippt in die VM.
var kbd=document.createElement('input');
kbd.setAttribute('autocomplete','off'); kbd.setAttribute('autocorrect','off');
kbd.setAttribute('autocapitalize','off'); kbd.spellcheck=false;
kbd.style.cssText='position:absolute;left:-1000px;top:0;width:1px;height:1px;opacity:0;';
document.body.appendChild(kbd);
var SPECIAL={Enter:0xff0d,Backspace:0xff08,Tab:0xff09,Escape:0xff1b,Delete:0xffff,
ArrowLeft:0xff51,ArrowUp:0xff52,ArrowRight:0xff53,ArrowDown:0xff54,Home:0xff50,End:0xff57};
import('https://cdn.jsdelivr.net/npm/@novnc/novnc@1.4.0/core/rfb.js').then(function(mod){
var RFB = mod.default;
var rfb = new RFB(document.getElementById('screen'), 'ws://aria-vnc/', {});
var fit=true;
rfb.scaleViewport = true;
rfb.clipViewport = false;
rfb.addEventListener('connect', function(){ msg.style.display='none'; post({event:'vnc_state', state:'connected'}); });
rfb.addEventListener('disconnect', function(e){
msg.style.display='block'; msg.textContent='Desktop getrennt';
post({event:'vnc_state', state:'disconnected'});
});
window.__rfb = rfb;
// Tasten aus dem versteckten Feld an die VM schicken.
function tap(keysym, code){ try{ rfb.sendKey(keysym, code||null, true); rfb.sendKey(keysym, code||null, false); }catch(_){} }
kbd.addEventListener('keydown', function(e){
if(SPECIAL[e.key]!==undefined){ tap(SPECIAL[e.key], e.code); e.preventDefault(); }
});
kbd.addEventListener('input', function(){
var v=kbd.value; for(var i=0;i<v.length;i++){ tap(v.charCodeAt(i)); } kbd.value='';
});
// Steuerungs-API fuer die App (per injectJavaScript).
window.ariaVncCtl = {
focusKeyboard: function(){ try{ kbd.focus(); }catch(_){} },
blurKeyboard: function(){ try{ kbd.blur(); }catch(_){} },
cad: function(){ try{ rfb.sendCtrlAltDel(); }catch(_){} },
toggleFit: function(){ fit=!fit; rfb.scaleViewport=fit; rfb.clipViewport=!fit; post({event:'vnc_fit', fit:fit}); }
};
post({event:'ready'});
}).catch(function(err){
msg.textContent='noVNC konnte nicht geladen werden (Internet?)';
post({event:'vnc_state', state:'error', error:String(err)});
});
})();
</script></body></html>`;