fix(desktop): tray badge — IPC handler died when icon.ico missing from packaged resources

This commit is contained in:
byGalax
2026-05-16 16:47:19 +02:00
parent c52c65faa9
commit 472665b980
+52 -27
View File
@@ -30,6 +30,17 @@ function resolveIconPathDev(): string {
return path.join(app.getAppPath(), 'resources', 'icon.ico'); return path.join(app.getAppPath(), 'resources', 'icon.ico');
} }
// 16×16 grey square — last-ditch fallback when neither the packaged
// nor dev icon file resolves. Tray constructor on Windows throws when
// handed an empty NativeImage, which would tear down the whole
// registrar before `ipcMain.handle(TRAY_UNREAD)` runs — leaving the
// taskbar overlay badge wired but the renderer's invoke rejecting
// with "No handler registered". A non-empty placeholder keeps the
// constructor happy so the IPC handler always gets registered.
const FALLBACK_TRAY_PNG_BASE64 =
'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAH0lEQVR42mNk' +
'YGD4z0ABYBxVOKpwVOGowlGFwwoBAEnYAR9XlIldAAAAAElFTkSuQmCC';
function loadTrayIcon(): NativeImage { function loadTrayIcon(): NativeImage {
for (const p of [resolveIconPath(), resolveIconPathDev()]) { for (const p of [resolveIconPath(), resolveIconPathDev()]) {
try { try {
@@ -39,7 +50,7 @@ function loadTrayIcon(): NativeImage {
/* try next */ /* try next */
} }
} }
return nativeImage.createEmpty(); return nativeImage.createFromBuffer(Buffer.from(FALLBACK_TRAY_PNG_BASE64, 'base64'));
} }
function buildOverlay(): NativeImage { function buildOverlay(): NativeImage {
@@ -53,35 +64,49 @@ function buildOverlay(): NativeImage {
} }
export function register(mainWindow: BrowserWindow): void { export function register(mainWindow: BrowserWindow): void {
const icon = loadTrayIcon(); // Tray icon is best-effort: if neither the packaged resource nor the
trayRef = new Tray(icon.isEmpty() ? nativeImage.createEmpty() : icon); // dev path resolves (e.g. icon.ico isn't shipped under resourcesPath
trayRef.setToolTip('Netralax'); // in packaged builds — only the app icon goes into the .exe metadata),
// we still want the taskbar overlay badge to work. setOverlayIcon is
// a BrowserWindow method, so it functions even when the systray icon
// creation fails.
try {
const icon = loadTrayIcon();
if (!icon.isEmpty()) {
trayRef = new Tray(icon);
trayRef.setToolTip('Netralax');
const menu = Menu.buildFromTemplate([ const menu = Menu.buildFromTemplate([
{ {
label: 'Open', label: 'Open',
click: (): void => { 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.isDestroyed()) return;
if (mainWindow.isMinimized()) mainWindow.restore(); if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.show(); if (mainWindow.isVisible()) mainWindow.focus();
mainWindow.focus(); else mainWindow.show();
}, });
}, }
{ type: 'separator' }, } catch (err: unknown) {
{ // Swallow: the overlay badge below is the user-visible bit. A missing
label: 'Quit', // systray icon is cosmetic and shouldn't take the unread handler with it.
click: (): void => { console.warn('[tray] systray init failed, overlay badge still active', err);
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( ipcMain.handle(
CHANNELS.TRAY_UNREAD, CHANNELS.TRAY_UNREAD,