168 lines
4.8 KiB
TypeScript
168 lines
4.8 KiB
TypeScript
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 };
|
|
}
|