import { app, BrowserWindow, ipcMain, Menu, nativeImage, screen, Tray } from "electron"; import path from "node:path"; const OVERLAY_WIDTH = 820; const OVERLAY_HEIGHT = 150; const SCREEN_EDGE_MARGIN = 24; const TASKBAR_GAP = 14; const APP_USER_MODEL_ID = "com.customstreamdeck.overlay"; const TRAY_ICON_DATA_URL = "data:image/png;base64," + "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAkUlEQVR4nGNgwAMEJdT/UwPjs4NmlpLlGFpbjtcR9LIcqyPobTmGI0a2AwbKcrgjiFGkdDSOLEwVB5BrObGOGPwOGPAoGBSJcNQBFnt/kIWp4gByLSfWEYPfAQMeBYMiEY464GexGF5MUwcQspxSRwy8A4hpFdHU8gFvEw4KBwx4x2RQdM3o5Qi8ltPKMfjsAABiL54YN/wECQAAAABJRU5ErkJggg=="; let overlayWindow: BrowserWindow | null = null; let tray: Tray | null = null; let isQuitting = false; const hasSingleInstanceLock = app.requestSingleInstanceLock(); if (!hasSingleInstanceLock) { app.quit(); } function createOverlayWindow(): void { const display = screen.getPrimaryDisplay(); const width = Math.min(OVERLAY_WIDTH, display.workArea.width - SCREEN_EDGE_MARGIN * 2); const height = OVERLAY_HEIGHT; const { x, y } = overlayPosition(display, width, height); overlayWindow = new BrowserWindow({ width, height, x, y, frame: false, transparent: true, resizable: false, movable: false, minimizable: false, maximizable: false, fullscreenable: false, alwaysOnTop: true, skipTaskbar: true, focusable: false, show: false, hasShadow: false, webPreferences: { preload: path.join(__dirname, "preload.js"), contextIsolation: true, nodeIntegration: false, backgroundThrottling: false } }); overlayWindow.setIgnoreMouseEvents(true); overlayWindow.setAlwaysOnTop(true, "screen-saver"); overlayWindow.loadFile(path.join(__dirname, "..", "index.html")); overlayWindow.once("ready-to-show", () => { console.log("[overlay:main] window ready; keeping transparent click-through overlay alive"); overlayWindow?.showInactive(); overlayWindow?.setIgnoreMouseEvents(true); }); overlayWindow.webContents.on("console-message", details => { console.log(`[overlay:renderer:${details.level}] ${details.message} (${details.sourceId}:${details.lineNumber})`); }); overlayWindow.on("closed", () => { console.log("[overlay:main] window closed"); overlayWindow = null; }); } function createTray(): void { if (tray) { return; } const icon = nativeImage.createFromDataURL(TRAY_ICON_DATA_URL); tray = new Tray(icon); tray.setToolTip("Custom Streamdeck Overlay"); tray.setContextMenu( Menu.buildFromTemplate([ { label: "Show overlay", click: revealOverlayFromTray }, { label: "Quit", click: () => { isQuitting = true; app.quit(); } } ]) ); tray.on("click", revealOverlayFromTray); } function showOverlay(): void { if (!overlayWindow) { console.log("[overlay:main] show requested before window exists"); return; } const display = screen.getDisplayNearestPoint(screen.getCursorScreenPoint()); const [width, height] = overlayWindow.getSize(); console.log(`[overlay:main] showing overlay at ${width}x${height} on display ${display.id}`); overlayWindow.setBounds({ width, height, ...overlayPosition(display, width, height) }); overlayWindow.showInactive(); overlayWindow.setAlwaysOnTop(true, "screen-saver"); overlayWindow.setIgnoreMouseEvents(true); } function hideOverlay(): void { // Keep the BrowserWindow alive so the hidden renderer continues receiving websocket events. console.log("[overlay:main] overlay faded; leaving window alive for websocket events"); overlayWindow?.setIgnoreMouseEvents(true); } function revealOverlayFromTray(): void { if (!overlayWindow) { createOverlayWindow(); } overlayWindow?.webContents.send("overlay:reveal"); showOverlay(); } if (hasSingleInstanceLock) { app.on("second-instance", revealOverlayFromTray); } app.whenReady().then(() => { app.setAppUserModelId(APP_USER_MODEL_ID); console.log("[overlay:main] app ready"); createTray(); createOverlayWindow(); app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) { createOverlayWindow(); } }); }); app.on("before-quit", () => { isQuitting = true; }); app.on("window-all-closed", () => { if (isQuitting) { app.quit(); } }); ipcMain.on("overlay:show", showOverlay); ipcMain.on("overlay:hide", hideOverlay); function overlayPosition(display: Electron.Display, width: number, height: number): { x: number; y: number } { const x = Math.round(display.workArea.x + (display.workArea.width - width) / 2); const bottomAlignedY = display.workArea.y + display.workArea.height - height - TASKBAR_GAP; const y = Math.round(Math.max(display.workArea.y + SCREEN_EDGE_MARGIN, bottomAlignedY)); return { x, y }; }