diff options
Diffstat (limited to 'packages')
18 files changed, 152 insertions, 7 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(() => { diff --git a/packages/meshbay-client/src/preload.js b/packages/meshbay-client/src/preload.js index 97a5063..1aa16e5 100644 --- a/packages/meshbay-client/src/preload.js +++ b/packages/meshbay-client/src/preload.js @@ -41,8 +41,15 @@ contextBridge.exposeInMainWorld('meshbay', { localFolders: true, nativeSave: true, lanCast: true, + // Linux only for now -- Windows is being done on that OS. Declared per + // platform rather than always: a button that hides the window somewhere the + // desktop shows no indicator would hide it for good. + tray: process.platform === 'linux', }, + // Labels are passed in because the main process has no i18n; see main.js. + minimizeToTray: (labels) => ipcRenderer.invoke('window:minimize-to-tray', labels), + // Ask the main process to call the hub. The renderer has an `app://` origin, // which CORS refuses and which is not a credential anyway. fetch: (url, init) => ipcRenderer.invoke('hub:fetch', url, init), diff --git a/packages/meshbay-client/src/tray-icon.png b/packages/meshbay-client/src/tray-icon.png Binary files differnew file mode 100644 index 0000000..394604d --- /dev/null +++ b/packages/meshbay-client/src/tray-icon.png diff --git a/packages/meshbay-client/src/tray-icon@2x.png b/packages/meshbay-client/src/tray-icon@2x.png Binary files differnew file mode 100644 index 0000000..3e533fb --- /dev/null +++ b/packages/meshbay-client/src/tray-icon@2x.png diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index 5311403..5857821 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -243,6 +243,13 @@ function Nav({ user, theme, onThemeChange, onLogout, onMenuToggle, unreadCount, </div> <div class="nav-right"> ${user && html`<${TransferWidget} />`} + ${platform.capabilities.tray && html` + <button class="nav-tray" onClick=${() => platform.minimizeToTray( + { show: t('tray.show'), quit: t('tray.quit') })} + title=${t('nav.minimize_tray')} aria-label=${t('nav.minimize_tray')}> + <${Icon} name="tray" /> + </button> + `} ${user && html` <a class="nav-notif" href="#/" title=${t('notif.title')}> <${Icon} name="bell" />${unreadCount > 0 diff --git a/packages/meshbay-hub/src/meshbay_hub/static/icon.js b/packages/meshbay-hub/src/meshbay_hub/static/icon.js index cc28110..79a3a08 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/icon.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/icon.js @@ -13,6 +13,8 @@ import { html } from './vendor/htm-preact.js'; const ICON_PATHS = { menu: ['M4 7h16M4 12h16M4 17h16'], + tray: ['M12 3.5v9', 'M8.5 9l3.5 3.5L15.5 9', + 'M4 15v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3'], bell: ['M18 9a6 6 0 1 0-12 0c0 6-2.5 7.5-2.5 7.5h17S18 15 18 9', 'M10.3 20a2 2 0 0 0 3.4 0'], shield: ['M12 3l7.5 3v5.2c0 4.6-3.1 8.6-7.5 10.3-4.4-1.7-7.5-5.7-7.5-10.3V6z'], diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js index 49b5f9e..24ff315 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -14,6 +14,9 @@ export default { 'nav.dark_mode': 'Dunkles Design', 'nav.logout': 'Abmelden', 'nav.login': 'Anmelden', + 'nav.minimize_tray': 'In den Infobereich minimieren', + 'tray.show': 'MeshBay anzeigen', + 'tray.quit': 'Beenden', // Sidebar 'sidebar.my_groups': 'Meine Gruppen', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js index 7ea8989..ce08f1f 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -17,6 +17,9 @@ export default { 'nav.dark_mode': 'Dark mode', 'nav.logout': 'Logout', 'nav.login': 'Login', + 'nav.minimize_tray': 'Minimize to tray', + 'tray.show': 'Show MeshBay', + 'tray.quit': 'Quit', // Sidebar 'sidebar.my_groups': 'My Groups', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js index 6109d4f..7aa1d98 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -13,6 +13,9 @@ export default { 'nav.dark_mode': 'Tema oscuro', 'nav.logout': 'Cerrar sesión', 'nav.login': 'Iniciar sesión', + 'nav.minimize_tray': 'Minimizar en la bandeja', + 'tray.show': 'Mostrar MeshBay', + 'tray.quit': 'Salir', // Sidebar 'sidebar.my_groups': 'Mis grupos', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js index 926f64a..b87cb81 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -13,6 +13,9 @@ export default { 'nav.dark_mode': 'Thème sombre', 'nav.logout': 'Déconnexion', 'nav.login': 'Connexion', + 'nav.minimize_tray': 'Réduire dans la zone de notification', + 'tray.show': 'Afficher MeshBay', + 'tray.quit': 'Quitter', // Sidebar 'sidebar.my_groups': 'Mes groupes', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js index f7011ec..242bdb4 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -14,6 +14,9 @@ export default { 'nav.dark_mode': 'Tema scuro', 'nav.logout': 'Esci', 'nav.login': 'Accedi', + 'nav.minimize_tray': 'Riduci nella barra di sistema', + 'tray.show': 'Mostra MeshBay', + 'tray.quit': 'Esci', // Sidebar 'sidebar.my_groups': 'I miei gruppi', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js index 592f939..8c8f654 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -14,6 +14,9 @@ export default { 'nav.dark_mode': 'ダークテーマ', 'nav.logout': 'ログアウト', 'nav.login': 'ログイン', + 'nav.minimize_tray': 'トレイに最小化', + 'tray.show': 'MeshBay を表示', + 'tray.quit': '終了', // Sidebar 'sidebar.my_groups': 'マイグループ', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js index eba234e..566677d 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -14,6 +14,9 @@ export default { 'nav.dark_mode': 'Donker thema', 'nav.logout': 'Afmelden', 'nav.login': 'Aanmelden', + 'nav.minimize_tray': 'Minimaliseren naar systeemvak', + 'tray.show': 'MeshBay tonen', + 'tray.quit': 'Afsluiten', // Sidebar 'sidebar.my_groups': 'Mijn groepen', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js index 999e689..4e75670 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -17,6 +17,9 @@ export default { 'nav.dark_mode': 'Motyw ciemny', 'nav.logout': 'Wyloguj', 'nav.login': 'Zaloguj', + 'nav.minimize_tray': 'Zminimalizuj do zasobnika', + 'tray.show': 'Pokaż MeshBay', + 'tray.quit': 'Zakończ', // Sidebar 'sidebar.my_groups': 'Moje grupy', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js index 79d2079..a9fd638 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js @@ -15,6 +15,9 @@ export default { 'nav.dark_mode': 'Tema escuro', 'nav.logout': 'Sair', 'nav.login': 'Entrar', + 'nav.minimize_tray': 'Minimizar na bandeja', + 'tray.show': 'Mostrar o MeshBay', + 'tray.quit': 'Sair', // Sidebar 'sidebar.my_groups': 'Meus grupos', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js index d67c34b..ca39837 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js @@ -14,6 +14,9 @@ export default { 'nav.dark_mode': '深色主题', 'nav.logout': '退出登录', 'nav.login': '登录', + 'nav.minimize_tray': '最小化到托盘', + 'tray.show': '显示 MeshBay', + 'tray.quit': '退出', // Sidebar 'sidebar.my_groups': '我的群组', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/platform.js b/packages/meshbay-hub/src/meshbay_hub/static/platform.js index 8f5d484..b009ebe 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/platform.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/platform.js @@ -48,6 +48,10 @@ export const capabilities = { nativeSave: Boolean(bridge && bridge.capabilities && bridge.capabilities.nativeSave), // Cast decrypted video to a device on the same LAN via a local HTTP relay. lanCast: Boolean(bridge && bridge.capabilities && bridge.capabilities.lanCast), + // Hide the window to a system tray indicator. Declared per-OS by the app, not + // by this file: a tray is only worth offering where the desktop actually shows + // one, so the browser and any platform without support simply never see it. + tray: Boolean(bridge && bridge.capabilities && bridge.capabilities.tray), }; /** @@ -454,9 +458,21 @@ export const cast = { }, }; +/** + * Hide the window to the system tray indicator. + * + * A no-op without the bridge, so the same nav renders in a browser without a + * guard at the call site -- though `capabilities.tray` keeps the button itself + * out of a browser, where there is no tray to minimise into. + */ +export async function minimizeToTray(labels) { + if (!bridge || !bridge.minimizeToTray) return false; + return bridge.minimizeToTray(labels); +} + export default { isNative, hubBase, capabilities, secrets, nativeSave, apiFetch, device, bridgeMessage, folder, rootPicker, node, - cast }; + cast, minimizeToTray }; // Also a global, because `transport.js` is loaded as a classic script — it // predates the module graph and exposes `MeshBayTransport` the same way. The @@ -466,5 +482,5 @@ if (typeof window !== 'undefined') { window.MeshBayPlatform = { isNative, hubBase, capabilities, secrets, nativeSave, apiFetch, device, bridgeMessage, folder, rootPicker, node, - cast }; + cast, minimizeToTray }; } diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css index 3d9206d..2770dd7 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/style.css +++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css @@ -2075,6 +2075,31 @@ a.transfer-name { } } +/* ── Minimise to tray ────────────────────────────────────────────────────── */ + +/* Monochrome on purpose: it sits next to the bell, and the bell's badge is the + only thing in this corner allowed to pull the eye. Inherits --nav-text like + every other nav control, so it follows the theme rather than carrying its own + colour, and the glyph is a stroked `currentColor` icon with nothing to tint. */ +.nav-tray { + display: inline-flex; + align-items: center; + justify-content: center; + background: none; + border: 0; + padding: 4px; + margin-right: 2px; + color: var(--nav-text); + cursor: pointer; + border-radius: 6px; + opacity: 0.75; +} +.nav-tray:hover, +.nav-tray:focus-visible { + opacity: 1; + background: rgba(255, 255, 255, 0.1); +} + /* ── Notification bell ───────────────────────────────────────────────────── */ .nav-notif { |