diff options
Diffstat (limited to 'packages/meshbay-client/src/main.js')
| -rw-r--r-- | packages/meshbay-client/src/main.js | 68 |
1 files changed, 63 insertions, 5 deletions
diff --git a/packages/meshbay-client/src/main.js b/packages/meshbay-client/src/main.js index e996829..71b56be 100644 --- a/packages/meshbay-client/src/main.js +++ b/packages/meshbay-client/src/main.js @@ -20,7 +20,8 @@ 'use strict'; -const { app, BrowserWindow, dialog, ipcMain, protocol, safeStorage, shell } = +const { app, BrowserWindow, dialog, ipcMain, Menu, protocol, safeStorage, + shell, Tray } = require('electron'); const { execFile, spawn } = require('node:child_process'); const crypto = require('node:crypto'); @@ -326,6 +327,56 @@ const HUB_FETCH_TIMEOUT_MS = 30000; let mainWindow = null; +// ── System tray ────────────────────────────────────────────────────────────── +// Linux only for now; Windows is being done on that OS. +// +// The context menu is not optional. Under libappindicator -- which is how GNOME +// shows a tray at all, via the AppIndicator extension -- `tray.on('click')` +// never fires: the indicator only opens its menu. A tray whose only affordance +// was a click would be inert on the one desktop this targets. +// +// Labels arrive from the renderer rather than being translated here. The locale +// files are the interface's, the main process has no i18n, and a second string +// table is how two of them start disagreeing. +let tray = null; + +function showFromTray() { + if (!mainWindow) return; + if (!mainWindow.isVisible()) mainWindow.show(); + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.focus(); +} + +function ensureTray(labels) { + const text = { + show: (labels && labels.show) || 'Show MeshBay', + quit: (labels && labels.quit) || 'Quit', + }; + if (tray) { + // Relabel in place: the person may have changed language since it was made. + tray.setContextMenu(Menu.buildFromTemplate([ + { label: text.show, click: showFromTray }, + { type: 'separator' }, + { label: text.quit, click: () => app.quit() }, + ])); + return tray; + } + // In src/, not build/: electron-builder packages only `src/**` and `ui/**` + // (package.json `files`), so an icon under build/ exists in a dev run and is + // missing from every installed one. + tray = new Tray(path.join(__dirname, 'tray-icon.png')); + tray.setToolTip('MeshBay'); + tray.setContextMenu(Menu.buildFromTemplate([ + { label: text.show, click: showFromTray }, + { type: 'separator' }, + { label: text.quit, click: () => app.quit() }, + ])); + // Harmless where it does nothing (GNOME), useful on desktops that do send it. + tray.on('click', showFromTray); + return tray; +} + + function createWindow() { const bounds = (config.window && config.window.width) ? config.window : { width: 1200, height: 800, @@ -541,6 +592,16 @@ function registerBridge() { } catch { return null; } }); + // Hide, never close: `window-all-closed` quits the app, and closing here would + // make "minimise to tray" mean "exit". A hidden window keeps the session, the + // transfers and the node connection exactly as they were. + ipcMain.handle('window:minimize-to-tray', (_e, labels) => { + if (process.platform !== 'linux' || !mainWindow) return false; + ensureTray(labels); + mainWindow.hide(); + return true; + }); + ipcMain.handle('device:ensure', () => ensureDeviceKey()); ipcMain.handle('device:public', () => { const key = deviceKey(); @@ -1477,10 +1538,7 @@ if (!app.requestSingleInstanceLock()) { app.quit(); } else { app.on('second-instance', () => { - if (mainWindow) { - if (mainWindow.isMinimized()) mainWindow.restore(); - mainWindow.focus(); - } + showFromTray(); }); app.whenReady().then(() => { |