Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2aef0347ae | ||
|
|
a49c022308 | ||
|
|
2d3ba024a4 | ||
|
|
1c7157327c | ||
|
|
6c27097c96 | ||
|
|
a91325a04f |
@@ -79,8 +79,8 @@ android {
|
|||||||
applicationId "com.ariacockpit"
|
applicationId "com.ariacockpit"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 20207
|
versionCode 20209
|
||||||
versionName "0.2.2.7"
|
versionName "0.2.2.9"
|
||||||
// Fallback fuer Libraries mit Product Flavors
|
// Fallback fuer Libraries mit Product Flavors
|
||||||
missingDimensionStrategy 'react-native-camera', 'general'
|
missingDimensionStrategy 'react-native-camera', 'general'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aria-cockpit",
|
"name": "aria-cockpit",
|
||||||
"version": "0.2.2.7",
|
"version": "0.2.2.9",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
|||||||
@@ -5,13 +5,13 @@
|
|||||||
* Cockpit-Modus → Workbench mit Taskleisten-Dock: Chat · Code · Desktop.
|
* Cockpit-Modus → Workbench mit Taskleisten-Dock: Chat · Code · Desktop.
|
||||||
*
|
*
|
||||||
* Aktivitaets-Badges am Dock: Editor blau, wenn schon Code-Dateien da sind;
|
* Aktivitaets-Badges am Dock: Editor blau, wenn schon Code-Dateien da sind;
|
||||||
* Desktop gruen, wenn eine VM verbunden ist.
|
* Desktop gruen NUR, wenn im aktiven Projekt eine VM laeuft.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import projectFocus, { FocusSnapshot } from '../services/projectFocus';
|
import projectFocus, { FocusSnapshot } from '../services/projectFocus';
|
||||||
import codeFile from '../services/codeFile';
|
import codeFile from '../services/codeFile';
|
||||||
import desktop from '../services/desktop';
|
import brainApi from '../services/brainApi';
|
||||||
import viewMode, { ViewModeValue } from '../services/viewMode';
|
import viewMode, { ViewModeValue } from '../services/viewMode';
|
||||||
import ChatScreen from '../screens/ChatScreen';
|
import ChatScreen from '../screens/ChatScreen';
|
||||||
import { TileId } from './layout';
|
import { TileId } from './layout';
|
||||||
@@ -29,7 +29,6 @@ const WorkspaceScreen: React.FC = () => {
|
|||||||
useEffect(() => projectFocus.subscribe(setFocus), []);
|
useEffect(() => projectFocus.subscribe(setFocus), []);
|
||||||
|
|
||||||
const pid = focus.focusedProjectId;
|
const pid = focus.focusedProjectId;
|
||||||
const kind = projectFocus.getProjectKind(pid);
|
|
||||||
|
|
||||||
// Code-Signal: hat der Spiegel schon Dateien fuer dieses Projekt?
|
// Code-Signal: hat der Spiegel schon Dateien fuer dieses Projekt?
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -39,13 +38,21 @@ const WorkspaceScreen: React.FC = () => {
|
|||||||
});
|
});
|
||||||
}, [pid]);
|
}, [pid]);
|
||||||
|
|
||||||
// Desktop-Signal + einmaliger Check beim Betreten eines Code-Projekts.
|
// Desktop-Signal: gruener Punkt NUR, wenn im AKTIVEN Projekt wirklich eine VM
|
||||||
|
// laeuft (nicht generell irgendwo). Quelle ist die projektbezogene VM-Liste;
|
||||||
|
// leichtes Nachfassen, damit Start/Stop sich zeitnah zeigt.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHasDesktop(desktop.getStatus().available);
|
if (!pid) { setHasDesktop(false); return; }
|
||||||
const unsub = desktop.subscribeStatus((s) => setHasDesktop(s.available));
|
let alive = true;
|
||||||
if (kind === 'code') desktop.requestCheck(pid);
|
const check = () => {
|
||||||
return unsub;
|
brainApi.listProjectVms(pid)
|
||||||
}, [pid, kind]);
|
.then(r => { if (alive) setHasDesktop((r.vms || []).some(v => v.running)); })
|
||||||
|
.catch(() => { if (alive) setHasDesktop(false); });
|
||||||
|
};
|
||||||
|
check();
|
||||||
|
const t = setInterval(check, 6000);
|
||||||
|
return () => { alive = false; clearInterval(t); };
|
||||||
|
}, [pid]);
|
||||||
|
|
||||||
const badges = useMemo(() => ({
|
const badges = useMemo(() => ({
|
||||||
editor: hasCode ? '#0096FF' : undefined,
|
editor: hasCode ? '#0096FF' : undefined,
|
||||||
|
|||||||
@@ -77,20 +77,13 @@ export const NOVNC_HTML = `<!doctype html><html><head><meta charset="utf-8">
|
|||||||
|
|
||||||
var msg=document.getElementById('msg');
|
var msg=document.getElementById('msg');
|
||||||
|
|
||||||
// (Fast) unsichtbares Eingabefeld → Software-Tastatur des Handys tippt in die
|
// Tastatur laeuft NICHT mehr ueber ein verstecktes WebView-Feld (Android
|
||||||
// VM. WICHTIG: on-screen (nicht off-screen), sonst oeffnet Android die Tastatur
|
// oeffnet die Software-Tastatur dafuer unzuverlaessig). Stattdessen haelt die
|
||||||
// oft nicht bzw. liefert keine Events. opacity:0 + pointer-events:none = sichtbar
|
// App ein echtes RN-<TextInput> und ruft window.ariaVncKey.* per
|
||||||
// fuer den Fokus, aber unsichtbar und klaut keine VM-Touches. font-size:16px
|
// injectJavaScript auf → wird unten (nach RFB-Init) definiert.
|
||||||
// verhindert Auto-Zoom.
|
// cp<0x100 → Keysym == Codepoint (Latin-1)
|
||||||
var kbd=document.createElement('input');
|
// sonst → X11-Unicode-Keysym 0x01000000+cp
|
||||||
kbd.setAttribute('autocomplete','off'); kbd.setAttribute('autocorrect','off');
|
function cpToKeysym(cp){ return cp < 0x100 ? cp : 0x01000000 + cp; }
|
||||||
kbd.setAttribute('autocapitalize','off'); kbd.spellcheck=false;
|
|
||||||
kbd.style.cssText='position:fixed;bottom:0;left:0;width:100%;height:1px;opacity:0;'
|
|
||||||
+'border:0;padding:0;margin:0;background:transparent;color:transparent;'
|
|
||||||
+'caret-color:transparent;font-size:16px;pointer-events:none;';
|
|
||||||
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){
|
import('https://cdn.jsdelivr.net/npm/@novnc/novnc@1.4.0/core/rfb.js').then(function(mod){
|
||||||
var RFB = mod.default;
|
var RFB = mod.default;
|
||||||
@@ -105,30 +98,29 @@ export const NOVNC_HTML = `<!doctype html><html><head><meta charset="utf-8">
|
|||||||
});
|
});
|
||||||
window.__rfb = rfb;
|
window.__rfb = rfb;
|
||||||
|
|
||||||
// Tasten aus dem versteckten Feld an die VM schicken.
|
// Down+Up einer Taste an die VM schicken.
|
||||||
function tap(keysym, code){ try{ rfb.sendKey(keysym, code||null, true); rfb.sendKey(keysym, code||null, false); }catch(_){} }
|
function tap(keysym, code){ try{ rfb.sendKey(keysym, code||null, true); rfb.sendKey(keysym, code||null, false); }catch(_){} }
|
||||||
// Sondertasten (feuern zuverlaessig als keydown).
|
|
||||||
kbd.addEventListener('keydown', function(e){
|
// Empfaenger-API: die App (RN-<TextInput> + Sondertasten-Leiste) ruft das
|
||||||
if(SPECIAL[e.key]!==undefined){ tap(SPECIAL[e.key], e.code); e.preventDefault(); }
|
// per injectJavaScript.
|
||||||
});
|
// char(cp) druckbares Zeichen (Codepoint)
|
||||||
// Druckbare Zeichen + Editieren: beforeinput ist auf Android robuster als
|
// keysym(ks) Sondertaste als fertiges X11-Keysym (Enter/Esc/F1/…)
|
||||||
// ein input-Diff. preventDefault haelt das Feld leer → kein Doppel-Senden.
|
// combo(mods,ks) Modifier(-Keysyms) halten → Taste → wieder loslassen
|
||||||
kbd.addEventListener('beforeinput', function(e){
|
// (Strg+C, Strg+Alt+Entf, …). mods = Array von Keysyms.
|
||||||
var t=e.inputType||'';
|
window.ariaVncKey = {
|
||||||
if(t==='insertText' && e.data){ for(var i=0;i<e.data.length;i++) tap(e.data.charCodeAt(i)); e.preventDefault(); }
|
char: function(cp){ tap(cpToKeysym(cp)); },
|
||||||
else if(t.indexOf('insertLineBreak')>=0 || t.indexOf('insertParagraph')>=0){ tap(0xff0d); e.preventDefault(); }
|
keysym: function(ks){ tap(ks); },
|
||||||
else if(t.indexOf('deleteContentBackward')>=0){ tap(0xff08); e.preventDefault(); }
|
combo: function(mods, ks){
|
||||||
kbd.value='';
|
try{
|
||||||
});
|
for(var i=0;i<mods.length;i++) rfb.sendKey(mods[i], null, true);
|
||||||
// Fallback, falls beforeinput nicht unterstuetzt wird.
|
rfb.sendKey(ks, null, true); rfb.sendKey(ks, null, false);
|
||||||
kbd.addEventListener('input', function(){
|
for(var j=mods.length-1;j>=0;j--) rfb.sendKey(mods[j], null, false);
|
||||||
if(kbd.value){ for(var i=0;i<kbd.value.length;i++){ tap(kbd.value.charCodeAt(i)); } kbd.value=''; }
|
}catch(_){}
|
||||||
});
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Steuerungs-API fuer die App (per injectJavaScript).
|
// Steuerungs-API fuer die App (per injectJavaScript).
|
||||||
window.ariaVncCtl = {
|
window.ariaVncCtl = {
|
||||||
focusKeyboard: function(){ try{ kbd.value=''; kbd.focus(); }catch(_){} },
|
|
||||||
blurKeyboard: function(){ try{ kbd.blur(); }catch(_){} },
|
|
||||||
cad: function(){ try{ rfb.sendCtrlAltDel(); }catch(_){} },
|
cad: function(){ try{ rfb.sendCtrlAltDel(); }catch(_){} },
|
||||||
toggleFit: function(){ fit=!fit; rfb.scaleViewport=fit; rfb.clipViewport=!fit; post({event:'vnc_fit', fit:fit}); }
|
toggleFit: function(){ fit=!fit; rfb.scaleViewport=fit; rfb.clipViewport=!fit; post({event:'vnc_fit', fit:fit}); }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,13 +2,16 @@
|
|||||||
* VncTile — Live-Desktop der QEMU-VM (noVNC in einer WebView, RFB durch RVS).
|
* VncTile — Live-Desktop der QEMU-VM (noVNC in einer WebView, RFB durch RVS).
|
||||||
*
|
*
|
||||||
* Nur aktiv, wenn das Desktop-Panel offen ist (focused): dann WebView mounten,
|
* Nur aktiv, wenn das Desktop-Panel offen ist (focused): dann WebView mounten,
|
||||||
* bei 'ready' den RVS-VNC-Tunnel oeffnen. Eine kleine Steuerungs-Leiste macht
|
* bei 'ready' den RVS-VNC-Tunnel oeffnen. Zwei Bedien-Leisten machen die VM auf
|
||||||
* die VM auf dem Handy bedienbar: Tastatur einblenden (tippt in die VM),
|
* dem Handy voll bedienbar:
|
||||||
* Strg-Alt-Entf, und Fit ↔ 1:1 umschalten.
|
* - ctlBar (oben rechts): Fn-Leiste ein/aus, Software-Tastatur, Fit ↔ 1:1.
|
||||||
|
* - keyBar (oben, Fn): echte Steuertasten, die keine Software-Tastatur
|
||||||
|
* liefert — Esc, Tab, Pfeile, Pos1/Ende/Bild, Einfg/Entf, Enter, F1–F12 und
|
||||||
|
* Sticky-Modifier Strg/Alt/Shift (fuer Strg+C, Strg+Alt+Entf, …).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
import { Keyboard, NativeSyntheticEvent, ScrollView, StyleSheet, Text, TextInput, TextInputChangeEventData, TextInputKeyPressEventData, TouchableOpacity, View } from 'react-native';
|
||||||
import { WebView, WebViewMessageEvent } from 'react-native-webview';
|
import { WebView, WebViewMessageEvent } from 'react-native-webview';
|
||||||
import desktop from '../../services/desktop';
|
import desktop from '../../services/desktop';
|
||||||
import { NOVNC_HTML } from '../assets/novncHtml';
|
import { NOVNC_HTML } from '../assets/novncHtml';
|
||||||
@@ -19,9 +22,30 @@ interface Props {
|
|||||||
port?: number; // VNC-Port der zu verbindenden VM (Default 5901 = Display :1)
|
port?: number; // VNC-Port der zu verbindenden VM (Default 5901 = Display :1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// X11-Keysyms fuer Sondertasten, die kein druckbares Zeichen liefern.
|
||||||
|
const KEYSYM = { Backspace: 0xff08, Enter: 0xff0d, Tab: 0xff09 };
|
||||||
|
const MOD = { ctrl: 0xffe3, alt: 0xffe9, shift: 0xffe1 };
|
||||||
|
const cpToKeysym = (cp: number) => (cp < 0x100 ? cp : 0x01000000 + cp);
|
||||||
|
|
||||||
|
// Sondertasten fuer die Fn-Leiste (Label → Keysym).
|
||||||
|
const NAV_KEYS: { label: string; ks: number }[] = [
|
||||||
|
{ label: 'Esc', ks: 0xff1b }, { label: 'Tab', ks: 0xff09 },
|
||||||
|
{ label: '←', ks: 0xff51 }, { label: '↑', ks: 0xff52 }, { label: '↓', ks: 0xff54 }, { label: '→', ks: 0xff53 },
|
||||||
|
{ label: 'Pos1', ks: 0xff50 }, { label: 'Ende', ks: 0xff57 },
|
||||||
|
{ label: 'Bild↑', ks: 0xff55 }, { label: 'Bild↓', ks: 0xff56 },
|
||||||
|
{ label: 'Einfg', ks: 0xff63 }, { label: 'Entf', ks: 0xffff }, { label: '⏎', ks: 0xff0d },
|
||||||
|
];
|
||||||
|
const F_KEYS: { label: string; ks: number }[] = Array.from({ length: 12 }, (_, i) => ({ label: 'F' + (i + 1), ks: 0xffbe + i }));
|
||||||
|
|
||||||
const VncTile: React.FC<Props> = ({ projectId, focused, port = 5901 }) => {
|
const VncTile: React.FC<Props> = ({ projectId, focused, port = 5901 }) => {
|
||||||
const webRef = useRef<WebView>(null);
|
const webRef = useRef<WebView>(null);
|
||||||
|
const kbdRef = useRef<TextInput>(null);
|
||||||
|
const bufRef = useRef(''); // Spiegel des TextInput-Textes
|
||||||
const [status, setStatus] = useState<'idle' | 'connecting' | 'connected' | 'disconnected'>('idle');
|
const [status, setStatus] = useState<'idle' | 'connecting' | 'connected' | 'disconnected'>('idle');
|
||||||
|
const [kbdOn, setKbdOn] = useState(false);
|
||||||
|
const [keyBar, setKeyBar] = useState(false); // Fn-Leiste sichtbar?
|
||||||
|
const [mods, setMods] = useState({ ctrl: false, alt: false, shift: false });
|
||||||
|
const modRef = useRef({ ctrl: false, alt: false, shift: false }); // Spiegel fuer Closures
|
||||||
const unsubDataRef = useRef<null | (() => void)>(null);
|
const unsubDataRef = useRef<null | (() => void)>(null);
|
||||||
|
|
||||||
const teardown = useCallback(() => {
|
const teardown = useCallback(() => {
|
||||||
@@ -34,10 +58,87 @@ const VncTile: React.FC<Props> = ({ projectId, focused, port = 5901 }) => {
|
|||||||
return () => teardown();
|
return () => teardown();
|
||||||
}, [focused, teardown]);
|
}, [focused, teardown]);
|
||||||
|
|
||||||
|
// Button-Zustand an die ECHTE Tastatur-Sichtbarkeit koppeln: Androids
|
||||||
|
// Zurueck-Taste blendet die Tastatur aus, ohne den TextInput zu blurren —
|
||||||
|
// ueber keyboardDidHide setzen wir das ⌨-Symbol trotzdem zurueck.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!focused) return;
|
||||||
|
const show = Keyboard.addListener('keyboardDidShow', () => setKbdOn(true));
|
||||||
|
const hide = Keyboard.addListener('keyboardDidHide', () => setKbdOn(false));
|
||||||
|
return () => { show.remove(); hide.remove(); };
|
||||||
|
}, [focused]);
|
||||||
|
|
||||||
const ctl = useCallback((fn: string) => {
|
const ctl = useCallback((fn: string) => {
|
||||||
webRef.current?.injectJavaScript(`window.ariaVncCtl && window.ariaVncCtl.${fn}(); true;`);
|
webRef.current?.injectJavaScript(`window.ariaVncCtl && window.ariaVncCtl.${fn}(); true;`);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const sendKeysym = useCallback((ks: number) => {
|
||||||
|
webRef.current?.injectJavaScript(`window.ariaVncKey && window.ariaVncKey.keysym(${ks}); true;`);
|
||||||
|
}, []);
|
||||||
|
const sendCombo = useCallback((modKeysyms: number[], ks: number) => {
|
||||||
|
webRef.current?.injectJavaScript(`window.ariaVncKey && window.ariaVncKey.combo(${JSON.stringify(modKeysyms)}, ${ks}); true;`);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Aktive Sticky-Modifier als Keysym-Liste; nach dem Anwenden one-shot zuruecksetzen.
|
||||||
|
const activeMods = useCallback(() => {
|
||||||
|
const m = modRef.current; const a: number[] = [];
|
||||||
|
if (m.ctrl) a.push(MOD.ctrl); if (m.alt) a.push(MOD.alt); if (m.shift) a.push(MOD.shift);
|
||||||
|
return a;
|
||||||
|
}, []);
|
||||||
|
const clearMods = useCallback(() => {
|
||||||
|
if (modRef.current.ctrl || modRef.current.alt || modRef.current.shift) {
|
||||||
|
modRef.current = { ctrl: false, alt: false, shift: false };
|
||||||
|
setMods(modRef.current);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
const toggleMod = useCallback((k: 'ctrl' | 'alt' | 'shift') => {
|
||||||
|
modRef.current = { ...modRef.current, [k]: !modRef.current[k] };
|
||||||
|
setMods(modRef.current);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Eine Taste (fertiges Keysym) senden — mit ggf. aktiven Modifiern.
|
||||||
|
const pressKey = useCallback((ks: number) => {
|
||||||
|
const m = activeMods();
|
||||||
|
if (m.length) { sendCombo(m, ks); clearMods(); } else sendKeysym(ks);
|
||||||
|
}, [activeMods, sendCombo, clearMods, sendKeysym]);
|
||||||
|
|
||||||
|
// Ein druckbares Zeichen senden — mit ggf. aktiven Modifiern (Strg+C etc.).
|
||||||
|
const pressChar = useCallback((cp: number) => {
|
||||||
|
const m = activeMods();
|
||||||
|
if (m.length) { sendCombo(m, cpToKeysym(cp)); clearMods(); }
|
||||||
|
else webRef.current?.injectJavaScript(`window.ariaVncKey && window.ariaVncKey.char(${cp}); true;`);
|
||||||
|
}, [activeMods, sendCombo, clearMods]);
|
||||||
|
|
||||||
|
// Tastatur ein-/ausblenden. Oeffnen: blur→focus erzwingt das Aufklappen auch
|
||||||
|
// dann, wenn der TextInput noch fokussiert ist (Tastatur per Zurueck-Taste
|
||||||
|
// versteckt). Schliessen: Keyboard.dismiss(); den Button-Zustand setzt der
|
||||||
|
// keyboardDidShow/Hide-Listener — nicht hier —, damit er nie „haengen" bleibt.
|
||||||
|
const toggleKbd = useCallback(() => {
|
||||||
|
if (kbdOn) { Keyboard.dismiss(); }
|
||||||
|
else { kbdRef.current?.blur(); setTimeout(() => kbdRef.current?.focus(), 30); }
|
||||||
|
}, [kbdOn]);
|
||||||
|
|
||||||
|
// Druckbare Zeichen: Prefix-Diff des (wachsenden) Feldes → nur neu Getipptes an
|
||||||
|
// die VM. Loeschungen kommen ueber onKeyPress(Backspace), daher hier nur Inserts.
|
||||||
|
const onKbdChange = useCallback((e: NativeSyntheticEvent<TextInputChangeEventData>) => {
|
||||||
|
const text = e.nativeEvent.text || '';
|
||||||
|
const prev = bufRef.current;
|
||||||
|
let i = 0;
|
||||||
|
const min = Math.min(prev.length, text.length);
|
||||||
|
while (i < min && prev.charCodeAt(i) === text.charCodeAt(i)) i++;
|
||||||
|
for (const ch of text.slice(i)) { const cp = ch.codePointAt(0); if (cp) pressChar(cp); }
|
||||||
|
bufRef.current = text;
|
||||||
|
if (text.length > 200) { bufRef.current = ''; kbdRef.current?.setNativeProps({ text: '' }); }
|
||||||
|
}, [pressChar]);
|
||||||
|
|
||||||
|
// Sondertasten der Software-Tastatur: Backspace feuert auf Android zuverlaessig
|
||||||
|
// als keyPress; die Return-Taste (Haken) kommt als onSubmitEditing (s.u.).
|
||||||
|
const onKbdKeyPress = useCallback((e: NativeSyntheticEvent<TextInputKeyPressEventData>) => {
|
||||||
|
const k = e.nativeEvent.key;
|
||||||
|
if (k === 'Backspace') pressKey(KEYSYM.Backspace);
|
||||||
|
else if (k === 'Enter') pressKey(KEYSYM.Enter);
|
||||||
|
}, [pressKey]);
|
||||||
|
|
||||||
const onMessage = useCallback((e: WebViewMessageEvent) => {
|
const onMessage = useCallback((e: WebViewMessageEvent) => {
|
||||||
let m: any;
|
let m: any;
|
||||||
try { m = JSON.parse(e.nativeEvent.data); } catch { return; }
|
try { m = JSON.parse(e.nativeEvent.data); } catch { return; }
|
||||||
@@ -84,14 +185,34 @@ const VncTile: React.FC<Props> = ({ projectId, focused, port = 5901 }) => {
|
|||||||
keyboardDisplayRequiresUserAction={false}
|
keyboardDisplayRequiresUserAction={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Verstecktes Eingabefeld: fokussiert → Android-Tastatur tippt in die VM.
|
||||||
|
keyboardType=visible-password schaltet Autokorrektur/Vorschlaege ab und
|
||||||
|
liefert saubere Einzelzeichen. Offscreen, aber fokussierbar. */}
|
||||||
|
<TextInput
|
||||||
|
ref={kbdRef}
|
||||||
|
style={styles.hiddenInput}
|
||||||
|
onChange={onKbdChange}
|
||||||
|
onKeyPress={onKbdKeyPress}
|
||||||
|
onSubmitEditing={() => pressKey(KEYSYM.Enter)}
|
||||||
|
keyboardType="visible-password"
|
||||||
|
returnKeyType="send"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
spellCheck={false}
|
||||||
|
blurOnSubmit={false}
|
||||||
|
caretHidden
|
||||||
|
contextMenuHidden
|
||||||
|
multiline={false}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Steuerungs-Leiste — nur wenn verbunden */}
|
{/* Steuerungs-Leiste — nur wenn verbunden */}
|
||||||
{connected && (
|
{connected && (
|
||||||
<View style={styles.ctlBar}>
|
<View style={styles.ctlBar}>
|
||||||
<TouchableOpacity style={styles.ctlBtn} onPress={() => ctl('focusKeyboard')} activeOpacity={0.7}>
|
<TouchableOpacity style={[styles.ctlBtn, keyBar && styles.ctlBtnOn]} onPress={() => setKeyBar(v => !v)} activeOpacity={0.7}>
|
||||||
<Text style={styles.ctlText}>⌨</Text>
|
<Text style={styles.ctlText}>Fn</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity style={styles.ctlBtn} onPress={() => ctl('cad')} activeOpacity={0.7}>
|
<TouchableOpacity style={[styles.ctlBtn, kbdOn && styles.ctlBtnOn]} onPress={toggleKbd} activeOpacity={0.7}>
|
||||||
<Text style={styles.ctlTextSmall}>Strg+Alt+Entf</Text>
|
<Text style={styles.ctlText}>⌨</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity style={styles.ctlBtn} onPress={() => ctl('toggleFit')} activeOpacity={0.7}>
|
<TouchableOpacity style={styles.ctlBtn} onPress={() => ctl('toggleFit')} activeOpacity={0.7}>
|
||||||
<Text style={styles.ctlText}>⤢</Text>
|
<Text style={styles.ctlText}>⤢</Text>
|
||||||
@@ -99,6 +220,26 @@ const VncTile: React.FC<Props> = ({ projectId, focused, port = 5901 }) => {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Fn-Leiste — echte Steuertasten (oben, ueber der Software-Tastatur). */}
|
||||||
|
{connected && keyBar && (
|
||||||
|
<View style={styles.keyBar} pointerEvents="box-none">
|
||||||
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} keyboardShouldPersistTaps="always" contentContainerStyle={styles.keyRow}>
|
||||||
|
<TouchableOpacity style={[styles.key, mods.ctrl && styles.keyOn]} onPress={() => toggleMod('ctrl')} activeOpacity={0.7}><Text style={styles.keyText}>Strg</Text></TouchableOpacity>
|
||||||
|
<TouchableOpacity style={[styles.key, mods.alt && styles.keyOn]} onPress={() => toggleMod('alt')} activeOpacity={0.7}><Text style={styles.keyText}>Alt</Text></TouchableOpacity>
|
||||||
|
<TouchableOpacity style={[styles.key, mods.shift && styles.keyOn]} onPress={() => toggleMod('shift')} activeOpacity={0.7}><Text style={styles.keyText}>Shift</Text></TouchableOpacity>
|
||||||
|
{NAV_KEYS.map(k => (
|
||||||
|
<TouchableOpacity key={k.label} style={styles.key} onPress={() => pressKey(k.ks)} activeOpacity={0.7}><Text style={styles.keyText}>{k.label}</Text></TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} keyboardShouldPersistTaps="always" contentContainerStyle={styles.keyRow}>
|
||||||
|
{F_KEYS.map(k => (
|
||||||
|
<TouchableOpacity key={k.label} style={styles.key} onPress={() => pressKey(k.ks)} activeOpacity={0.7}><Text style={styles.keyText}>{k.label}</Text></TouchableOpacity>
|
||||||
|
))}
|
||||||
|
<TouchableOpacity style={styles.key} onPress={() => ctl('cad')} activeOpacity={0.7}><Text style={styles.keyTextSm}>Strg+Alt+Entf</Text></TouchableOpacity>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
{!connected && (
|
{!connected && (
|
||||||
<View style={styles.overlay} pointerEvents="none">
|
<View style={styles.overlay} pointerEvents="none">
|
||||||
<Text style={styles.overlayText}>
|
<Text style={styles.overlayText}>
|
||||||
@@ -135,8 +276,21 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
|
ctlBtnOn: { backgroundColor: 'rgba(0,150,255,0.85)', borderColor: '#0096FF' },
|
||||||
ctlText: { color: '#E0E0F0', fontSize: 16, fontWeight: '700' },
|
ctlText: { color: '#E0E0F0', fontSize: 16, fontWeight: '700' },
|
||||||
ctlTextSmall: { color: '#E0E0F0', fontSize: 11, fontWeight: '700' },
|
ctlTextSmall: { color: '#E0E0F0', fontSize: 11, fontWeight: '700' },
|
||||||
|
// Fokussierbar (nicht display:none), aber aus dem Sichtfeld geschoben.
|
||||||
|
hiddenInput: { position: 'absolute', width: 1, height: 1, top: -100, left: -100, opacity: 0, padding: 0 },
|
||||||
|
keyBar: { position: 'absolute', top: 74, left: 0, right: 0, gap: 5 },
|
||||||
|
keyRow: { paddingHorizontal: 6, gap: 5, alignItems: 'center' },
|
||||||
|
key: {
|
||||||
|
backgroundColor: 'rgba(18,18,42,0.92)', borderColor: '#2A2A3E', borderWidth: 1,
|
||||||
|
borderRadius: 8, paddingHorizontal: 9, paddingVertical: 7, minWidth: 34,
|
||||||
|
alignItems: 'center', justifyContent: 'center',
|
||||||
|
},
|
||||||
|
keyOn: { backgroundColor: 'rgba(0,150,255,0.85)', borderColor: '#0096FF' },
|
||||||
|
keyText: { color: '#E0E0F0', fontSize: 13, fontWeight: '700' },
|
||||||
|
keyTextSm: { color: '#E0E0F0', fontSize: 10, fontWeight: '700' },
|
||||||
overlay: { position: 'absolute', top: 12, left: 0, right: 0, alignItems: 'center' },
|
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' },
|
overlayText: { color: '#9090B0', fontSize: 12, backgroundColor: 'rgba(0,0,0,0.6)', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 10, overflow: 'hidden' },
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user