Tabs: reorder by dragging; detach tabs into their own window (drag out of the window or "Open in new window") and move them back (drag into another window's tab bar or "Back to main window"); terminal tabs keep their SSH connection and output, SFTP/Docker/firewall/network reconnect; events go to all windows, password prompts to the window showing the session; closing a detached window ends its sessions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+63
-11
@@ -43,8 +43,19 @@ process.on('uncaughtException', (e) => {
|
||||
});
|
||||
process.on('unhandledRejection', (e) => console.error('Unhandled rejection:', e));
|
||||
|
||||
// Abgelöste Tabs laufen in eigenen Fenstern. Ereignisse gehen an alle Fenster (die Oberfläche filtert nach Sitzungs-ID),
|
||||
// Passwortabfragen nur an das Fenster, das die Sitzung zeigt; Kopplung/Updates nur an das Hauptfenster.
|
||||
const tabWins = new Set();
|
||||
const sessionOwner = new Map(); // Sitzungs-ID -> BrowserWindow (nur für abgelöste Fenster)
|
||||
const MAIN_ONLY = new Set(['sync:pairPrompt', 'update:available']);
|
||||
const alive = (w) => w && !w.isDestroyed();
|
||||
function send(channel, ...args) {
|
||||
if (win && !win.isDestroyed()) win.webContents.send(channel, ...args);
|
||||
if (channel === 'secret:request') {
|
||||
const w = sessionOwner.get(args[0]?.sessionId);
|
||||
if (alive(w)) return w.webContents.send(channel, ...args);
|
||||
}
|
||||
if (alive(win)) win.webContents.send(channel, ...args);
|
||||
if (!MAIN_ONLY.has(channel)) for (const w of tabWins) if (alive(w)) w.webContents.send(channel, ...args);
|
||||
}
|
||||
|
||||
async function confirmHostKey(target, fingerprint, known) {
|
||||
@@ -91,12 +102,13 @@ const sync = new SyncService(store, send, (req) => new Promise((resolve) => {
|
||||
ipcMain.on('sync:pairReply', (_e, reqId, ok) => { const r = pairReplies.get(reqId); pairReplies.delete(reqId); r?.(!!ok); });
|
||||
store.onChange = () => sync.schedule();
|
||||
|
||||
function createWindow() {
|
||||
win = new BrowserWindow({
|
||||
function newWindow(opts = {}) {
|
||||
const w = new BrowserWindow({
|
||||
width: 1360,
|
||||
height: 860,
|
||||
minWidth: 900,
|
||||
minHeight: 560,
|
||||
minWidth: 600,
|
||||
minHeight: 400,
|
||||
...opts,
|
||||
backgroundColor: '#14161d',
|
||||
icon: path.join(__dirname, '..', '..', 'assets', 'icon.png'),
|
||||
frame: false,
|
||||
@@ -108,18 +120,57 @@ function createWindow() {
|
||||
sandbox: true,
|
||||
},
|
||||
});
|
||||
w.on('maximize', () => w.webContents.send('win:state', true));
|
||||
w.on('unmaximize', () => w.webContents.send('win:state', false));
|
||||
w.webContents.setWindowOpenHandler(({ url }) => { shell.openExternal(url); return { action: 'deny' }; });
|
||||
return w;
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
win = newWindow({ minWidth: 900, minHeight: 560 });
|
||||
Menu.setApplicationMenu(null);
|
||||
win.loadFile(path.join(__dirname, '..', 'renderer', 'index.html'));
|
||||
win.on('maximize', () => send('win:state', true));
|
||||
// Hauptfenster zu = MrTerm beenden, auch abgelöste Tab-Fenster schließen
|
||||
win.on('closed', () => { for (const w of tabWins) if (alive(w)) w.close(); });
|
||||
// Eingebettete RDP-Fenster (Windows: owned windows) beim Verschieben/Größenändern mitführen
|
||||
const followRdp = () => { for (const s of rdpSessions.values()) { try { s.emb.reposition(); } catch {} } };
|
||||
win.on('move', followRdp);
|
||||
win.on('resize', followRdp);
|
||||
win.on('restore', followRdp);
|
||||
win.on('unmaximize', () => send('win:state', false));
|
||||
win.webContents.setWindowOpenHandler(({ url }) => { shell.openExternal(url); return { action: 'deny' }; });
|
||||
}
|
||||
|
||||
// Tab in ein eigenes Fenster lösen: data = { kind, host, id, title, … } wird vom neuen Fenster per win:handoff abgeholt
|
||||
const handoffs = new Map();
|
||||
function openTabWindow(data, pos) {
|
||||
const token = crypto.randomUUID();
|
||||
handoffs.set(token, data);
|
||||
const w = newWindow({ width: 1000, height: 650, ...(pos ? { x: Math.round(pos.x - 120), y: Math.round(pos.y - 20) } : {}) });
|
||||
tabWins.add(w);
|
||||
w.loadFile(path.join(__dirname, '..', 'renderer', 'index.html'), { query: { detached: token } });
|
||||
// Fenster zu: dort noch offene Sitzungen beenden
|
||||
w.on('closed', () => {
|
||||
tabWins.delete(w);
|
||||
for (const [id, o] of sessionOwner) {
|
||||
if (o !== w) continue;
|
||||
sessionOwner.delete(id);
|
||||
for (const m of [ssh, docker, firewall, network]) { try { m.close(id); } catch {} }
|
||||
}
|
||||
});
|
||||
}
|
||||
ipcMain.handle('win:detach', (_e, data, pos) => { openTabWindow(data, pos); return true; });
|
||||
ipcMain.handle('win:handoff', (_e, token) => { const d = handoffs.get(token); handoffs.delete(token); return d || null; });
|
||||
// Sitzung gehört (jetzt) dem aufrufenden Fenster; im Hauptfenster gilt keine Zuordnung
|
||||
ipcMain.on('win:claim', (e, id) => {
|
||||
const w = BrowserWindow.fromWebContents(e.sender);
|
||||
if (w === win) sessionOwner.delete(id); else sessionOwner.set(id, w);
|
||||
});
|
||||
// Tab ins Hauptfenster zurück
|
||||
ipcMain.handle('win:attach', (_e, data) => { if (alive(win)) { win.webContents.send('win:adopt', data); win.focus(); } return true; });
|
||||
// Ein anderes Fenster hat den Tab übernommen (Drag & Drop): beim bisherigen Fenster ohne Trennen entfernen
|
||||
ipcMain.on('win:released', (e, id) => {
|
||||
for (const w of [win, ...tabWins]) if (alive(w) && w.webContents !== e.sender) w.webContents.send('win:released', id);
|
||||
});
|
||||
|
||||
// Solange MrTerm gesperrt ist, sind nur die Sperr-Kanäle erreichbar
|
||||
const OPEN_WHILE_LOCKED = new Set(['app:version']);
|
||||
function handle(channel, fn) {
|
||||
@@ -133,9 +184,10 @@ function handle(channel, fn) {
|
||||
}
|
||||
|
||||
// ---------- Fenster ----------
|
||||
ipcMain.on('win:min', () => win.minimize());
|
||||
ipcMain.on('win:max', () => (win.isMaximized() ? win.unmaximize() : win.maximize()));
|
||||
ipcMain.on('win:close', () => win.close());
|
||||
const senderWin = (e) => BrowserWindow.fromWebContents(e.sender);
|
||||
ipcMain.on('win:min', (e) => senderWin(e)?.minimize());
|
||||
ipcMain.on('win:max', (e) => { const w = senderWin(e); if (w) w.isMaximized() ? w.unmaximize() : w.maximize(); });
|
||||
ipcMain.on('win:close', (e) => senderWin(e)?.close());
|
||||
|
||||
// ---------- App-Sperre (Passwort / FIDO2) ----------
|
||||
const kdf = (pw, salt, N) => new Promise((resolve, reject) =>
|
||||
|
||||
Reference in New Issue
Block a user