From b32d51992c0a310e4762c38b0e28220e9867922b Mon Sep 17 00:00:00 2001 From: Aditya Patil Date: Fri, 22 May 2026 17:53:00 +0530 Subject: [PATCH 1/3] Feat: Multi-Monitor Support (#11) and Auto-Start (#14) --- main.js | 169 ++++++++++++++++++++++++++++++++++------------- settingsStore.js | 10 +++ 2 files changed, 132 insertions(+), 47 deletions(-) diff --git a/main.js b/main.js index c27c55c..5bd39b6 100644 --- a/main.js +++ b/main.js @@ -3,7 +3,7 @@ const path = require("path"); const { createAudioBridge } = require("./audioBridge"); const { createDefaultSettings, createSettingsStore, createThemeDefaults } = require("./settingsStore"); -let overlayWindow; +const overlayWindows = new Map(); let lastBridgeMode = null; let lastBridgeReason = null; let audioBridge; @@ -14,6 +14,15 @@ let settingsStore; let visualizerSettings; const APP_VERSION = app.getVersion(); + +function applyStartupSettings(launchOnStartup) { + app.setLoginItemSettings({ + openAtLogin: launchOnStartup, + path: process.execPath, + args: app.isPackaged ? [] : [app.getAppPath()] + }); +} + const PROJECT_URL = "https://github.com/SamXop123/Paraline"; const LANDING_URL = "https://paraline.vercel.app"; const singleInstanceLock = app.requestSingleInstanceLock(); @@ -34,11 +43,19 @@ const THEME_LABELS = { edgeCrystals: "Edge Crystals" }; -function createOverlayWindow() { - const primaryDisplay = screen.getPrimaryDisplay(); - const { bounds } = primaryDisplay; +function createOverlayForDisplay(display) { + const { bounds, id } = display; - overlayWindow = new BrowserWindow({ + const isEnabled = visualizerSettings.monitors && visualizerSettings.monitors[id] ? visualizerSettings.monitors[id].enabled !== false : true; + if (!isEnabled) { + return; + } + + if (overlayWindows.has(id)) { + return; + } + + const win = new BrowserWindow({ x: bounds.x, y: bounds.y, width: bounds.width, @@ -60,21 +77,42 @@ function createOverlayWindow() { } }); - overlayWindow.setAlwaysOnTop(true, "screen-saver"); - overlayWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); - overlayWindow.setIgnoreMouseEvents(true, { forward: true }); - overlayWindow.setBounds(bounds); - overlayWindow.moveTop(); - overlayWindow.loadFile("index.html"); - overlayWindow.webContents.on("did-finish-load", () => { + win.setAlwaysOnTop(true, "screen-saver"); + win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); + win.setIgnoreMouseEvents(true, { forward: true }); + win.setBounds(bounds); + win.moveTop(); + win.loadFile("index.html"); + win.webContents.on("did-finish-load", () => { setTimeout(() => { sendVisualizerSettings(); }, 100); }); - overlayWindow.on("closed", () => { - overlayWindow = null; + win.on("closed", () => { + overlayWindows.delete(id); }); + + overlayWindows.set(id, win); +} + +function syncOverlayWindows() { + const displays = screen.getAllDisplays(); + const currentDisplayIds = new Set(displays.map(d => d.id)); + + for (const [id, win] of overlayWindows.entries()) { + const isEnabled = visualizerSettings.monitors && visualizerSettings.monitors[id] ? visualizerSettings.monitors[id].enabled !== false : true; + if (!currentDisplayIds.has(id) || !isEnabled) { + if (!win.isDestroyed()) { + win.close(); + } + overlayWindows.delete(id); + } + } + + for (const display of displays) { + createOverlayForDisplay(display); + } } function sendAudioLevel(value, source) { @@ -82,14 +120,11 @@ function sendAudioLevel(value, source) { return; } - if (!overlayWindow || overlayWindow.isDestroyed()) { - return; + for (const win of overlayWindows.values()) { + if (!win.isDestroyed()) { + win.webContents.send("audio-level", { value, source }); + } } - - overlayWindow.webContents.send("audio-level", { - value, - source - }); } function getRendererSettings() { @@ -100,11 +135,11 @@ function getRendererSettings() { } function sendVisualizerSettings() { - if (!overlayWindow || overlayWindow.isDestroyed()) { - return; + for (const win of overlayWindows.values()) { + if (!win.isDestroyed()) { + win.webContents.send("visualizer-settings", getRendererSettings()); + } } - - overlayWindow.webContents.send("visualizer-settings", getRendererSettings()); } function mergeSettingsPatch(currentSettings, patch) { @@ -128,6 +163,14 @@ function mergeSettingsPatch(currentSettings, patch) { function updateSettings(nextSettings) { visualizerSettings = settingsStore.save(mergeSettingsPatch(visualizerSettings, nextSettings)); + if (nextSettings.launchOnStartup !== undefined) { + applyStartupSettings(visualizerSettings.launchOnStartup); + } + + if (nextSettings.monitors !== undefined) { + syncOverlayWindows(); + } + sendVisualizerSettings(); refreshTrayMenu(); } @@ -139,11 +182,11 @@ function togglePaused() { } function reloadVisualizer() { - if (!overlayWindow || overlayWindow.isDestroyed()) { - return; + for (const win of overlayWindows.values()) { + if (!win.isDestroyed()) { + win.webContents.reloadIgnoringCache(); + } } - - overlayWindow.webContents.reloadIgnoringCache(); } function startSimulatedAudioFallback() { @@ -163,16 +206,17 @@ function stopSimulatedAudioFallback() { } } -function resizeOverlayToPrimaryDisplay() { - if (!overlayWindow) { - return; +function resizeAllOverlays() { + const displays = screen.getAllDisplays(); + for (const display of displays) { + const win = overlayWindows.get(display.id); + if (win && !win.isDestroyed()) { + win.setBounds(display.bounds); + win.setAlwaysOnTop(true, "screen-saver"); + win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); + win.moveTop(); + } } - - const { bounds } = screen.getPrimaryDisplay(); - overlayWindow.setBounds(bounds); - overlayWindow.setAlwaysOnTop(true, "screen-saver"); - overlayWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); - overlayWindow.moveTop(); } function handleAudioBridgeStatusChange(status) { @@ -909,8 +953,35 @@ function refreshTrayMenu() { label: "Reload Visualizer", click: () => reloadVisualizer() }, + { type: "separator" }, + { + label: "Displays", + submenu: screen.getAllDisplays().map((display, index) => { + const isEnabled = visualizerSettings.monitors && visualizerSettings.monitors[display.id] ? visualizerSettings.monitors[display.id].enabled !== false : true; + return { + label: `Display ${index + 1} (${display.bounds.width}x${display.bounds.height})`, + type: "checkbox", + checked: isEnabled, + click: () => { + updateSettings({ + monitors: { + ...visualizerSettings.monitors, + [display.id]: { enabled: !isEnabled } + } + }); + } + }; + }) + }, { - label: "Visualizer Mode", + label: "Launch on Startup", + type: "checkbox", + checked: !!visualizerSettings.launchOnStartup, + click: () => updateSettings({ launchOnStartup: !visualizerSettings.launchOnStartup }) + }, + { type: "separator" }, + { + label: "Visualizer Mode", submenu: buildMainThemeMenuItems() }, { type: "separator" }, @@ -968,7 +1039,8 @@ app.whenReady().then(() => { return getRendererSettings(); }); - createOverlayWindow(); + applyStartupSettings(visualizerSettings.launchOnStartup); + syncOverlayWindows(); createTray(); sendVisualizerSettings(); @@ -982,20 +1054,23 @@ app.whenReady().then(() => { audioBridge.start(); refreshTrayMenu(); - screen.on("display-metrics-changed", resizeOverlayToPrimaryDisplay); - screen.on("display-added", resizeOverlayToPrimaryDisplay); - screen.on("display-removed", resizeOverlayToPrimaryDisplay); + screen.on("display-metrics-changed", () => { + syncOverlayWindows(); + resizeAllOverlays(); + }); + screen.on("display-added", syncOverlayWindows); + screen.on("display-removed", syncOverlayWindows); app.on("activate", () => { - if (BrowserWindow.getAllWindows().length === 0) { - createOverlayWindow(); + if (overlayWindows.size === 0) { + syncOverlayWindows(); } }); }); app.on("second-instance", () => { - if (overlayWindow && !overlayWindow.isDestroyed()) { - resizeOverlayToPrimaryDisplay(); + if (overlayWindows.size > 0) { + resizeAllOverlays(); sendVisualizerSettings(); } }); diff --git a/settingsStore.js b/settingsStore.js index 41feba8..807feeb 100644 --- a/settingsStore.js +++ b/settingsStore.js @@ -2,6 +2,8 @@ const fs = require("fs"); const path = require("path"); const DEFAULT_SETTINGS = Object.freeze({ + launchOnStartup: false, + monitors: Object.freeze({}), selectedTheme: "ambientWave", ambientWave: Object.freeze({ tone: "blue", @@ -91,6 +93,8 @@ const VALID_EDGE_FLUTTER_MODES = new Set(["left", "right", "both"]); function createDefaultSettings() { return { + launchOnStartup: DEFAULT_SETTINGS.launchOnStartup, + monitors: { ...DEFAULT_SETTINGS.monitors }, selectedTheme: DEFAULT_SETTINGS.selectedTheme, ambientWave: { ...DEFAULT_SETTINGS.ambientWave }, reactiveBorder: { ...DEFAULT_SETTINGS.reactiveBorder }, @@ -273,7 +277,13 @@ function migrateLegacySettings(input = {}) { function sanitizeSettings(input = {}) { const source = migrateLegacySettings(input); + const monitors = (source.monitors && typeof source.monitors === "object" && !Array.isArray(source.monitors)) + ? source.monitors + : { ...DEFAULT_SETTINGS.monitors }; + return { + launchOnStartup: typeof source.launchOnStartup === "boolean" ? source.launchOnStartup : DEFAULT_SETTINGS.launchOnStartup, + monitors, selectedTheme: pick(source.selectedTheme, VALID_MAIN_THEMES, DEFAULT_SETTINGS.selectedTheme), ambientWave: sanitizeAmbientWave(source.ambientWave), reactiveBorder: sanitizeReactiveBorder(source.reactiveBorder), From 84149e9ca04552f040a27732011d81071304366f Mon Sep 17 00:00:00 2001 From: Aditya Patil Date: Thu, 18 Jun 2026 15:58:24 +0530 Subject: [PATCH 2/3] feat(architecture): refine motion systems --- renderer.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/renderer.js b/renderer.js index da2aa3a..12847fc 100644 --- a/renderer.js +++ b/renderer.js @@ -314,7 +314,9 @@ function updateAudioLevel(now) { const helperDriven = latestSource === "helper"; const breathing = helperDriven ? 0.003 : 0.028 * (Math.sin(now * 0.00023) + 1); const response = helperDriven ? 0.2 : 0.018; - smoothedLevel += ((incomingLevel + breathing) - smoothedLevel) * response; + const deltaTime = lastFrameAt ? now - lastFrameAt : 16.6; + const timeScale = deltaTime / 16.6; + smoothedLevel += ((incomingLevel + breathing) - smoothedLevel) * response * timeScale; } function renderFrame(now) { From f9ea0f32e0516361afc9fe8344dbe405e12b10c1 Mon Sep 17 00:00:00 2001 From: Aditya Patil Date: Fri, 19 Jun 2026 18:59:24 +0530 Subject: [PATCH 3/3] solved requested review --- renderer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renderer.js b/renderer.js index 94c8f24..379f066 100644 --- a/renderer.js +++ b/renderer.js @@ -595,7 +595,7 @@ function updateAudioLevel(now) { const response = helperDriven ? 0.2 : 0.018; const deltaTime = lastFrameAt ? now - lastFrameAt : 16.6; const timeScale = deltaTime / 16.6; - smoothedLevel += ((incomingLevel + breathing) - smoothedLevel) * response * timeScale; + smoothedLevel += ((incomingLevel + breathing) - smoothedLevel) * Math.min(1, response * timeScale); } function renderFrame(now) {