// System tray + Windows taskbar overlay badge. Renderer pushes the // current aggregate unread count via CHANNELS.TRAY_UNREAD; we update the // tooltip and (on Windows) set an overlay icon on the main window's // taskbar button. // // Overlay image is a pre-rendered 16x16 red dot embedded as base64 so // the module is self-contained — no runtime canvas dependency. import { app, BrowserWindow, ipcMain, Menu, nativeImage, type NativeImage, Tray, } from 'electron'; import path from 'node:path'; import { CHANNELS } from '../ipc-types'; let trayRef: Tray | null = null; let overlayImage: NativeImage | null = null; function resolveIconPath(): string { return path.join(process.resourcesPath || app.getAppPath(), 'icon.ico'); } function resolveIconPathDev(): string { return path.join(app.getAppPath(), 'resources', 'icon.ico'); } function loadTrayIcon(): NativeImage { for (const p of [resolveIconPath(), resolveIconPathDev()]) { try { const img = nativeImage.createFromPath(p); if (!img.isEmpty()) return img; } catch { /* try next */ } } return nativeImage.createEmpty(); } function buildOverlay(): NativeImage { if (overlayImage) return overlayImage; // 16x16 PNG, solid red circle. Inline base64 so no external file lookup. const base64 = 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAPElEQVR42mNkYGD4z0AGYBxVSF2F//' + '//Z2BgYGD4/58kBYz4FDAxMDAwMDIwMDD8//+foArGUYWjCoc1AABTgwUBf3lZtAAAAABJRU5ErkJggg=='; overlayImage = nativeImage.createFromBuffer(Buffer.from(base64, 'base64')); return overlayImage; } export function register(mainWindow: BrowserWindow): void { const icon = loadTrayIcon(); trayRef = new Tray(icon.isEmpty() ? nativeImage.createEmpty() : icon); trayRef.setToolTip('Netralax'); const menu = Menu.buildFromTemplate([ { label: 'Open', click: (): void => { if (mainWindow.isDestroyed()) return; if (mainWindow.isMinimized()) mainWindow.restore(); mainWindow.show(); mainWindow.focus(); }, }, { type: 'separator' }, { label: 'Quit', click: (): void => { app.quit(); }, }, ]); trayRef.setContextMenu(menu); trayRef.on('click', (): void => { if (mainWindow.isDestroyed()) return; if (mainWindow.isMinimized()) mainWindow.restore(); if (mainWindow.isVisible()) mainWindow.focus(); else mainWindow.show(); }); ipcMain.handle( CHANNELS.TRAY_UNREAD, async (_evt, count: number, badgeDataUrl?: string | null): Promise => { const n = Math.max(0, Math.floor(Number(count) || 0)); if (trayRef && !trayRef.isDestroyed()) { trayRef.setToolTip(n > 0 ? `Netralax — ${n} ungelesen` : 'Netralax'); } if (mainWindow.isDestroyed()) return; if (process.platform !== 'win32') return; if (n <= 0) { mainWindow.setOverlayIcon(null, ''); return; } // Discord-style: prefer the renderer-painted badge (red circle with // the actual unread number). Fall back to the static red dot only if // the renderer didn't supply one or decoding failed — keeps the // visual indicator alive even when the canvas pipeline is unavailable. let overlay: NativeImage | null = null; if (typeof badgeDataUrl === 'string' && badgeDataUrl.startsWith('data:image/')) { const decoded = nativeImage.createFromDataURL(badgeDataUrl); if (!decoded.isEmpty()) overlay = decoded; } mainWindow.setOverlayIcon(overlay ?? buildOverlay(), `${n} ungelesen`); }, ); app.on('before-quit', () => { try { trayRef?.destroy(); } catch { /* already gone */ } trayRef = null; }); }