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>
126 lines
5.7 KiB
TypeScript
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>`;
|