Add Android app: phone UI for hosts, SSH terminal with extra key row, keys, snippets, LAN sync and app lock (password + fingerprint), reusing the desktop Node modules via capacitor-nodejs; APKs per ABI in releases for Obtainium
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
// Entwicklung ohne Handy: startet die gebaute Oberfläche (www/) in einem Fenster in Handygröße
|
||||
// und das Node-Backend im Electron-Hauptprozess, verbunden über eine nachgebaute Capacitor-Bridge.
|
||||
// cd mobile && node build.js && ../node_modules/.bin/electron dev/electron-dev.js [--user-data-dir=…]
|
||||
const { app, BrowserWindow, ipcMain, clipboard } = require('electron');
|
||||
const path = require('path');
|
||||
const Module = require('module');
|
||||
const EventEmitter = require('events');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const WWW = path.join(__dirname, '..', 'www');
|
||||
const toWeb = new EventEmitter();
|
||||
const fromWeb = new EventEmitter();
|
||||
let win;
|
||||
|
||||
// require('bridge') im Backend auf diese Nachbildung umleiten
|
||||
const bridge = {
|
||||
channel: { send: (name, ...args) => toWeb.emit('msg', name, args), addListener: (name, fn) => fromWeb.on(name, fn) },
|
||||
getDataPath: () => path.join(app.getPath('userData'), 'mobile-data'),
|
||||
};
|
||||
const resolve = Module._resolveFilename;
|
||||
Module._resolveFilename = function (req, ...rest) { return req === 'bridge' ? 'bridge' : resolve.call(this, req, ...rest); };
|
||||
require.cache.bridge = { id: 'bridge', filename: 'bridge', loaded: true, exports: bridge };
|
||||
|
||||
toWeb.on('msg', (name, args) => { if (win && !win.isDestroyed()) win.webContents.send('dev:node', name, args); });
|
||||
ipcMain.on('dev:send', (_e, name, args) => fromWeb.emit(name, ...args));
|
||||
ipcMain.handle('dev:start', (_e, env) => {
|
||||
Object.assign(process.env, env, { DATADIR: bridge.getDataPath() });
|
||||
require('fs').mkdirSync(process.env.DATADIR, { recursive: true });
|
||||
require(path.join(WWW, 'nodejs', 'index.js'));
|
||||
});
|
||||
ipcMain.handle('dev:native', (_e, method, arg) => {
|
||||
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 === 'bioAvailable') return { available: true, enrolled: false };
|
||||
if (method === 'bioEnroll' || method === 'bioUnlock') return { secret: Buffer.alloc(32, 1).toString('base64') };
|
||||
if (method === 'copy') return clipboard.writeText(arg.text);
|
||||
if (method === 'paste') return { text: clipboard.readText() };
|
||||
return {};
|
||||
});
|
||||
|
||||
app.whenReady().then(() => {
|
||||
win = new BrowserWindow({
|
||||
width: 412, height: 870, backgroundColor: '#14161d',
|
||||
webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true },
|
||||
});
|
||||
win.loadFile(path.join(WWW, 'index.html'));
|
||||
if (process.env.MRTERM_SHOT) {
|
||||
const steps = JSON.parse(require('fs').readFileSync(process.env.MRTERM_SHOT, 'utf8'));
|
||||
win.webContents.on('console-message', (e) => console.log('[r]', e.message));
|
||||
win.webContents.once('did-finish-load', async () => {
|
||||
await new Promise((r) => setTimeout(r, 2500));
|
||||
for (const s of steps) {
|
||||
if (s.js) await win.webContents.executeJavaScript(`(async()=>{${s.js}})();0`).catch((e) => console.log('js', e.message));
|
||||
await new Promise((r) => setTimeout(r, s.wait || 900));
|
||||
if (s.shot) require('fs').writeFileSync(path.join(process.env.MRTERM_SHOT_OUT, s.shot), (await win.webContents.capturePage()).toPNG());
|
||||
}
|
||||
app.exit(0);
|
||||
});
|
||||
}
|
||||
});
|
||||
app.on('window-all-closed', () => app.exit(0));
|
||||
@@ -0,0 +1,14 @@
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
const listeners = new Map();
|
||||
ipcRenderer.on('dev:node', (_e, name, args) => listeners.get(name)?.forEach((fn) => fn({ args })));
|
||||
const native = (m) => (arg) => ipcRenderer.invoke('dev:native', m, arg);
|
||||
|
||||
contextBridge.exposeInMainWorld('DevBridge', {
|
||||
NodeJS: {
|
||||
start: (o) => ipcRenderer.invoke('dev:start', o.env),
|
||||
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) }; },
|
||||
},
|
||||
Native: Object.fromEntries(['vaultKey', 'info', 'bioAvailable', 'bioEnroll', 'bioUnlock', 'bioDisable', 'multicast', 'copy', 'paste', 'background'].map((m) => [m, native(m)])),
|
||||
});
|
||||
Reference in New Issue
Block a user