Compare commits

...
8 Commits
9 changed files with 67 additions and 20 deletions
@@ -5,6 +5,7 @@ import android.view.View;
import android.webkit.WebView; import android.webkit.WebView;
import androidx.activity.OnBackPressedCallback; import androidx.activity.OnBackPressedCallback;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat; import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat; import androidx.core.view.WindowInsetsCompat;
@@ -25,13 +26,19 @@ public class MainActivity extends BridgeActivity {
} }
}); });
// Edge-to-Edge: Bei offener Tastatur den WebView um die Tastaturhöhe verkleinern, // Edge-to-Edge: Ränder für Status- und Navigationsleiste setzt diese Ansicht. Den Platz für die Tastatur
// damit Terminal und Zusatztasten sichtbar bleiben // schafft Android/WebView selbst – hier nur melden, ob sie offen ist (sonst würde doppelt verkleinert).
View parent = (View) web.getParent(); View parent = (View) web.getParent();
parent.setBackgroundColor(0xFF14161D);
final boolean[] imeOpen = { false };
ViewCompat.setOnApplyWindowInsetsListener(parent, (v, insets) -> { ViewCompat.setOnApplyWindowInsetsListener(parent, (v, insets) -> {
int ime = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom; Insets bars = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
v.setPadding(0, 0, 0, ime); boolean open = insets.isVisible(WindowInsetsCompat.Type.ime());
web.evaluateJavascript("window.mrtermIme && window.mrtermIme(" + (ime > 0) + ")", null); v.setPadding(bars.left, bars.top, bars.right, open ? 0 : bars.bottom);
if (open != imeOpen[0]) {
imeOpen[0] = open;
web.evaluateJavascript("window.mrtermIme && window.mrtermIme(" + open + ")", null);
}
return insets; return insets;
}); });
} }
+10
View File
@@ -37,13 +37,23 @@ function copy(from, to) {
fs.cpSync(from, to, { recursive: true }); fs.cpSync(from, to, { recursive: true });
} }
// capacitor-nodejs baut seine Brückenbibliothek mit 4-KB-Seitenausrichtung; neuere Geräte (z. B. Pixel mit
// 16-KB-Speicherseiten) laden sie dann nicht. Linker-Option einmalig ergänzen.
function patchPlugin() {
const f = path.join(__dirname, 'node_modules', 'capacitor-nodejs', 'android', 'CMakeLists.txt');
const s = fs.readFileSync(f, 'utf8');
if (!s.includes('max-page-size=16384')) fs.writeFileSync(f, `${s}\ntarget_link_options(native-lib PRIVATE "-Wl,-z,max-page-size=16384")\n`);
}
function buildWeb() { function buildWeb() {
patchPlugin();
fs.rmSync(WWW, { recursive: true, force: true }); fs.rmSync(WWW, { recursive: true, force: true });
copy(path.join(__dirname, 'web'), WWW); copy(path.join(__dirname, 'web'), WWW);
const nm = path.join(ROOT, 'node_modules', '@xterm'); const nm = path.join(ROOT, 'node_modules', '@xterm');
copy(path.join(nm, 'xterm', 'lib', 'xterm.js'), path.join(WWW, 'lib', 'xterm.js')); copy(path.join(nm, 'xterm', 'lib', 'xterm.js'), path.join(WWW, 'lib', 'xterm.js'));
copy(path.join(nm, 'xterm', 'css', 'xterm.css'), path.join(WWW, 'lib', 'xterm.css')); copy(path.join(nm, 'xterm', 'css', 'xterm.css'), path.join(WWW, 'lib', 'xterm.css'));
copy(path.join(nm, 'addon-fit', 'lib', 'addon-fit.js'), path.join(WWW, 'lib', 'addon-fit.js')); copy(path.join(nm, 'addon-fit', 'lib', 'addon-fit.js'), path.join(WWW, 'lib', 'addon-fit.js'));
copy(path.join(__dirname, 'node_modules', '@capacitor', 'core', 'dist', 'capacitor.js'), path.join(WWW, 'lib', 'capacitor.js'));
copy(path.join(ROOT, 'src', 'i18n.js'), path.join(WWW, 'lib', 'i18n.js')); copy(path.join(ROOT, 'src', 'i18n.js'), path.join(WWW, 'lib', 'i18n.js'));
copy(path.join(ROOT, 'src', 'renderer', 'themes.js'), path.join(WWW, 'lib', 'themes.js')); copy(path.join(ROOT, 'src', 'renderer', 'themes.js'), path.join(WWW, 'lib', 'themes.js'));
copy(path.join(ROOT, 'src', 'renderer', 'logo.png'), path.join(WWW, 'logo.png')); copy(path.join(ROOT, 'src', 'renderer', 'logo.png'), path.join(WWW, 'logo.png'));
+3 -1
View File
@@ -26,8 +26,10 @@ ipcMain.on('dev:send', (_e, name, args) => fromWeb.emit(name, ...args));
ipcMain.handle('dev:start', (_e, env) => { ipcMain.handle('dev:start', (_e, env) => {
Object.assign(process.env, env, { DATADIR: bridge.getDataPath() }); Object.assign(process.env, env, { DATADIR: bridge.getDataPath() });
require('fs').mkdirSync(process.env.DATADIR, { recursive: true }); require('fs').mkdirSync(process.env.DATADIR, { recursive: true });
require(path.join(WWW, 'nodejs', 'index.js')); setTimeout(() => { started = true; require(path.join(WWW, 'nodejs', 'index.js')); }, 500);
}); });
let started;
ipcMain.handle('dev:ready', async () => { while (!started) await new Promise((r) => setTimeout(r, 100)); });
ipcMain.handle('dev:native', (_e, method, arg) => { ipcMain.handle('dev:native', (_e, method, arg) => {
if (method === 'vaultKey') return { key: crypto.createHash('sha256').update(app.getPath('userData')).digest('base64') }; if (method === 'vaultKey') return { key: crypto.createHash('sha256').update(app.getPath('userData')).digest('base64') };
if (method === 'info') return { device: 'Dev Phone', locale: process.env.MRTERM_DEV_LOCALE || 'en-US', version: require('../../package.json').version }; if (method === 'info') return { device: 'Dev Phone', locale: process.env.MRTERM_DEV_LOCALE || 'en-US', version: require('../../package.json').version };
+3 -1
View File
@@ -6,7 +6,9 @@ const native = (m) => (arg) => ipcRenderer.invoke('dev:native', m, arg);
contextBridge.exposeInMainWorld('DevBridge', { contextBridge.exposeInMainWorld('DevBridge', {
NodeJS: { NodeJS: {
start: (o) => ipcRenderer.invoke('dev:start', o.env), // Wie auf Android: start() kehrt erst zurück, wenn Node beendet wird
start: (o) => { ipcRenderer.invoke('dev:start', o.env); return new Promise(() => {}); },
whenReady: () => ipcRenderer.invoke('dev:ready'),
send: ({ eventName, args }) => ipcRenderer.send('dev:send', eventName, args), send: ({ eventName, args }) => ipcRenderer.send('dev:send', eventName, args),
addListener: (name, fn) => { if (!listeners.has(name)) listeners.set(name, new Set()); listeners.get(name).add(fn); return { remove: () => listeners.get(name).delete(fn) }; }, addListener: (name, fn) => { if (!listeners.has(name)) listeners.set(name, new Set()); listeners.get(name).add(fn); return { remove: () => listeners.get(name).delete(fn) }; },
}, },
+25 -8
View File
@@ -22,10 +22,10 @@ const api = {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const id = ++seq; const id = ++seq;
waits.set(id, { resolve, reject }); waits.set(id, { resolve, reject });
NodeJS.send({ eventName: 'call', args: [id, ch, args] }); Promise.resolve(NodeJS.send({ eventName: 'call', args: [id, ch, args] })).catch((e) => { waits.delete(id); reject(e); });
}); });
}, },
send(ch, ...args) { NodeJS.send({ eventName: 'send', args: [ch, args] }); }, send(ch, ...args) { NodeJS.send({ eventName: 'send', args: [ch, args] }).catch?.((e) => console.error(e)); },
on(ch, fn) { if (!events.has(ch)) events.set(ch, new Set()); events.get(ch).add(fn); return () => events.get(ch).delete(fn); }, on(ch, fn) { if (!events.has(ch)) events.set(ch, new Set()); events.get(ch).add(fn); return () => events.get(ch).delete(fn); },
}; };
NodeJS.addListener('reply', ({ args: [id, ok, value] }) => { NodeJS.addListener('reply', ({ args: [id, ok, value] }) => {
@@ -174,10 +174,15 @@ async function boot() {
const info = await Native.info().catch(() => ({ device: 'Android', locale: navigator.language, version: '0.0.0' })); const info = await Native.info().catch(() => ({ device: 'Android', locale: navigator.language, version: '0.0.0' }));
S.info = info; S.info = info;
I18N.setLanguage('auto', info.locale); I18N.setLanguage('auto', info.locale);
const ready = new Promise((r) => { const off = NodeJS.addListener('ready', () => r()); void off; }); const ready = new Promise((r) => NodeJS.addListener('ready', () => r(true)));
const { key } = await Native.vaultKey(); const { key } = await Native.vaultKey();
await NodeJS.start({ env: { MRTERM_VAULT_KEY: key, MRTERM_DEVICE: info.device, MRTERM_LOCALE: info.locale, MRTERM_VERSION: info.version } }); // start() kehrt erst zurück, wenn Node beendet wird – daher nicht darauf warten, sondern auf whenReady()
await Promise.race([ready, new Promise((r) => setTimeout(r, 8000))]); NodeJS.start({ env: { MRTERM_VAULT_KEY: key, MRTERM_DEVICE: info.device, MRTERM_LOCALE: info.locale, MRTERM_VERSION: info.version } })
.catch((e) => { if (!/already been started/.test(e.message)) bootError(`Node.js: ${e.message}`); });
const timeout = (ms, what) => new Promise((_, rej) => setTimeout(() => rej(new Error(what)), ms));
await Promise.race([whenReady(), timeout(30000, 'The Node.js engine did not start.')]);
// Backend meldet sich mit 'ready'; nach einem Neuladen der Oberfläche läuft es schon → Ping genügt
await Promise.race([ready, api.call('app:version'), timeout(20000, 'The MrTerm backend did not respond.')]);
try { Object.assign(S.bio, await Native.bioAvailable()); } catch {} try { Object.assign(S.bio, await Native.bioAvailable()); } catch {}
Native.multicast({ on: true }).catch(() => {}); Native.multicast({ on: true }).catch(() => {});
$('#boot').hidden = true; $('#boot').hidden = true;
@@ -1015,6 +1020,18 @@ window.mrtermBack = onBack; // wird von MainActivity bei der Zurück-Taste aufge
// MainActivity meldet die Bildschirmtastatur (WebView wird dann um ihre Höhe verkleinert) // MainActivity meldet die Bildschirmtastatur (WebView wird dann um ihre Höhe verkleinert)
window.mrtermIme = (open) => { document.body.classList.toggle('kbd-open', open); setTimeout(fitViewport, 50); }; window.mrtermIme = (open) => { document.body.classList.toggle('kbd-open', open); setTimeout(fitViewport, 50); };
boot().catch((e) => { // whenReady() wartet, bis die Node-Engine Nachrichten annimmt (vorher lehnt send() ab)
$('#boot').innerHTML = `<div style="color:var(--red);padding:24px;text-align:center">${esc(e.message || e)}</div>`; async function whenReady() {
}); for (;;) {
try { await NodeJS.whenReady(); return; } catch (e) {
if (!/not been started/.test(e.message)) throw e;
await new Promise((r) => setTimeout(r, 200));
}
}
}
function bootError(msg) {
const b = $('#boot');
if (b.hidden) return toast(msg, 'error');
b.innerHTML = `<img src="logo.png" alt=""/><div style="color:var(--red);padding:0 24px;text-align:center;max-width:420px">${esc(msg)}</div><div style="color:var(--muted);padding:0 24px;text-align:center;font-size:13px">Android ${esc(navigator.userAgent.match(/Android ([\d.]+)/)?.[1] || '')} · ${esc(S.info?.device || '')} · ${esc(S.info?.version || '')}</div>`;
}
boot().catch((e) => bootError(e.message || String(e)));
+8
View File
@@ -38,6 +38,14 @@
<div id="layer"></div> <div id="layer"></div>
<div id="toasts"></div> <div id="toasts"></div>
<script>
// Fehler beim Laden sichtbar machen statt endlos den Ladekreis zu zeigen
window.addEventListener('error', (e) => {
const b = document.getElementById('boot');
if (b && !b.hidden) b.innerHTML = '<div style="color:#ff5f6d;padding:24px;text-align:center;font:14px system-ui">' + String(e.message || e.error).replace(/</g, '&lt;') + '</div>';
});
</script>
<script src="lib/capacitor.js"></script>
<script src="lib/i18n.js"></script> <script src="lib/i18n.js"></script>
<script src="lib/themes.js"></script> <script src="lib/themes.js"></script>
<script src="lib/xterm.js"></script> <script src="lib/xterm.js"></script>
+3 -2
View File
@@ -13,8 +13,9 @@
--orange: #ffb347; --orange: #ffb347;
--radius: 14px; --radius: 14px;
/* Capacitor setzt --safe-area-inset-* bei Edge-to-Edge; env() als Rückfall */ /* Capacitor setzt --safe-area-inset-* bei Edge-to-Edge; env() als Rückfall */
--top: max(env(safe-area-inset-top, 0px), var(--safe-area-inset-top, 0px)); /* Status-/Navigationsleiste und Tastatur rechnet MainActivity als Rand um den WebView heraus */
--bottom: max(env(safe-area-inset-bottom, 0px), var(--safe-area-inset-bottom, 0px)); --top: 0px;
--bottom: 0px;
color-scheme: dark; color-scheme: dark;
} }
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; } * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "mrterm", "name": "mrterm",
"version": "0.11.0", "version": "0.11.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mrterm", "name": "mrterm",
"version": "0.11.0", "version": "0.11.4",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@xterm/addon-fit": "^0.11.0", "@xterm/addon-fit": "^0.11.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "mrterm", "name": "mrterm",
"productName": "MrTerm", "productName": "MrTerm",
"version": "0.11.0", "version": "0.11.4",
"description": "Moderner SSH-, SFTP- und RDP-Client", "description": "Moderner SSH-, SFTP- und RDP-Client",
"main": "src/main/main.js", "main": "src/main/main.js",
"author": "MrBlake", "author": "MrBlake",