Files
NetMesh/electron/bridges/windowManager/settingsWindow.cjs

348 lines
11 KiB
JavaScript
Raw Normal View History

/* eslint-disable no-undef */
const {
windowsFramelessContentChromeOptions,
} = require("./windowsWindowChrome.cjs");
function createSettingsWindowApi(ctx) {
// The extracted window helpers intentionally share the parent window-manager state.
with (ctx) {
function restoreWindowInputFocus(win, options = {}) {
if (!win || win.isDestroyed()) return false;
const shouldShow = options.show === true;
const platform = options.platform || process.platform;
if (shouldShow) {
try {
win.show();
} catch {
// ignore
}
}
if (platform === "win32") {
try {
win.setAlwaysOnTop(true);
} catch {
// ignore
}
try {
win.focus();
} catch {
// ignore
} finally {
try {
win.setAlwaysOnTop(false);
} catch {
// ignore
}
}
} else {
try {
win.focus();
} catch {
// ignore
}
}
try {
if (win.webContents && !win.webContents.isDestroyed()) {
win.webContents.focus();
}
} catch {
// ignore
}
return true;
}
function showAndFocusWindow(win) {
restoreWindowInputFocus(win, { show: true });
}
function isLiveWindow(win) {
return Boolean(win && typeof win.isDestroyed === "function" && !win.isDestroyed());
}
function resolveSettingsWindowBounds(
electronModule,
{ sourceWindow, settingsWidth, settingsHeight } = {},
) {
const { screen } = electronModule || {};
if (!screen || !isLiveWindow(sourceWindow)) return {};
try {
const sourceBounds = sourceWindow.getBounds();
const display = screen.getDisplayMatching(sourceBounds);
const { x: dx, y: dy, width: dw, height: dh } = display.workArea;
return {
x: Math.round(dx + (dw - settingsWidth) / 2),
y: Math.round(dy + (dh - settingsHeight) / 2),
};
} catch {
return {};
}
}
function centerSettingsWindowOnSourceDisplay(win, electronModule, sourceWindow) {
if (!isLiveWindow(win)) return;
let bounds = { width: 980, height: 720 };
try {
bounds = win.getBounds();
} catch {
// keep defaults
}
const nextPosition = resolveSettingsWindowBounds(electronModule, {
sourceWindow,
settingsWidth: bounds.width,
settingsHeight: bounds.height,
});
if (nextPosition.x === undefined || nextPosition.y === undefined) return;
try {
win.setPosition(nextPosition.x, nextPosition.y);
} catch {
// ignore
}
}
async function openSettingsWindow(electronModule, options, { showOnLoad = true } = {}) {
const { BrowserWindow, shell } = electronModule;
const { preload, devServerUrl, isDev, appIcon, isMac, electronDir, sourceWindow } = options;
// If settings window already exists, show and focus it
if (settingsWindow && !settingsWindow.isDestroyed()) {
centerSettingsWindowOnSourceDisplay(settingsWindow, electronModule, sourceWindow || mainWindow);
showAndFocusWindow(settingsWindow);
return settingsWindow;
}
const osTheme = electronModule?.nativeTheme?.shouldUseDarkColors ? "dark" : "light";
const effectiveTheme = currentTheme === "dark" || currentTheme === "light" ? currentTheme : osTheme;
const frontendBackground = resolveFrontendBackgroundColor(electronDir || __dirname, effectiveTheme);
const backgroundColor = frontendBackground || "#1a1a1a";
const themeConfig = THEME_COLORS[effectiveTheme] || THEME_COLORS.light;
// Center the settings window on the same display as the main window
const settingsWidth = 980;
const settingsHeight = 720;
const { x: settingsX, y: settingsY } = resolveSettingsWindowBounds(electronModule, {
sourceWindow: sourceWindow || mainWindow,
settingsWidth,
settingsHeight,
});
const windowsChrome = !isMac ? windowsFramelessContentChromeOptions() : {};
const win = new BrowserWindow({
title: "netcatty Settings",
width: settingsWidth,
height: settingsHeight,
...(settingsX !== undefined && settingsY !== undefined ? { x: settingsX, y: settingsY } : {}),
minWidth: 820,
minHeight: 600,
backgroundColor,
icon: appIcon,
fullscreenable: !isMac,
// NOTE: Do NOT set parent - on macOS this causes rendering issues when dragging
// the window to a different screen (the window becomes invisible while still
// appearing in "Show All Windows" in the Dock). On Windows it can cause the
// main window to close when the settings window is closed.
modal: false,
show: false,
frame: isMac,
titleBarStyle: isMac ? "hiddenInset" : undefined,
trafficLightPosition: isMac ? { x: 12, y: 12 } : undefined,
...windowsChrome,
webPreferences: {
preload,
contextIsolation: true,
nodeIntegration: false,
sandbox: false,
spellcheck: false,
v8CacheOptions: V8_CACHE_OPTIONS,
},
});
settingsWindow = win;
// Open external links in system browser by default, and allow only known OAuth hosts in-app.
try {
win.webContents?.setWindowOpenHandler?.(
createAppWindowOpenHandler(shell, {
backgroundColor,
appIcon,
getAppIcon: () => resolveLiveAppIcon(appIcon),
})
);
} catch {
// ignore
}
// Never allow chained popups from remote content windows spawned from settings.
win.webContents?.on?.("did-create-window", (childWindow) => {
try {
childWindow.webContents?.setWindowOpenHandler?.(createExternalOnlyWindowOpenHandler(shell));
} catch {
// ignore
}
});
// Same navigation hardening as the main window (settings has preload access too).
const allowedOrigins = new Set(["app://netcatty"]);
if (isDev && devServerUrl) {
try {
allowedOrigins.add(new URL(getDevRendererBaseUrl(devServerUrl)).origin);
} catch {
// ignore invalid dev server URL
}
}
const isAllowedTopLevelUrl = (targetUrl) => {
try {
return allowedOrigins.has(new URL(String(targetUrl)).origin);
} catch {
return false;
}
};
const blockUntrustedNavigation = (event, targetUrl) => {
if (isAllowedTopLevelUrl(targetUrl)) return;
try {
event.preventDefault();
} catch {
// ignore
}
debugLog("Blocked navigation to untrusted origin (settings)", { targetUrl });
};
win.webContents.on("will-navigate", blockUntrustedNavigation);
win.webContents.on("will-redirect", blockUntrustedNavigation);
if (isMac) {
try {
win.setWindowButtonVisibility(true);
} catch {
// ignore
}
}
const safeSend = (channel, ...args) => {
try {
if (!win.isDestroyed() && win.webContents && !win.webContents.isDestroyed()) {
win.webContents.send(channel, ...args);
}
} catch {
// Render frame disposed during HMR / reload safe to ignore
}
};
win.on("enter-full-screen", () => {
safeSend("netcatty:window:fullscreen-changed", true);
});
win.on("leave-full-screen", () => {
safeSend("netcatty:window:fullscreen-changed", false);
});
// Ensure native background matches frontend background, even before first paint.
try {
win.setBackgroundColor(backgroundColor);
} catch {
// ignore
}
applyWindowOpacityToWindow(win);
// Hide instead of close so the window can be reused instantly.
// When the app is quitting, allow normal close/destroy.
win.on('close', (event) => {
if (!isQuitting) {
event.preventDefault();
try {
win.hide();
} catch {
// ignore
}
}
});
// Clean up reference when actually destroyed
win.on('closed', () => {
settingsWindow = null;
});
// Prevent HTML <title> from overriding the window title
win.on('page-title-updated', (e) => { e.preventDefault(); });
// Load the settings page
const settingsPath = '/#/settings';
if (isDev) {
try {
const baseUrl = getDevRendererBaseUrl(devServerUrl);
await win.loadURL(`${baseUrl}${settingsPath}`);
if (showOnLoad) { showAndFocusWindow(win); }
return win;
} catch (e) {
console.warn("Dev server not reachable for settings window", e);
}
}
// Production mode - load via custom protocol.
await win.loadURL("app://netcatty/index.html#/settings");
if (showOnLoad) { showAndFocusWindow(win); }
return win;
}
/**
* Destroy the settings window (used when the app is quitting).
*/
function closeSettingsWindow() {
if (settingsWindow && !settingsWindow.isDestroyed()) {
try {
settingsWindow.destroy();
} catch {
// ignore
}
settingsWindow = null;
}
}
/**
* Hide the settings window without destroying it (used when main window hides to tray).
*/
function hideSettingsWindow() {
if (settingsWindow && !settingsWindow.isDestroyed()) {
try {
settingsWindow.hide();
} catch {
// ignore
}
}
}
/**
* Pre-warm the settings window in the background so that opening it later is instant.
* The window is created hidden and fully loaded; `openSettingsWindow` will simply show it.
*/
async function prewarmSettingsWindow(electronModule, options) {
if (settingsWindow && !settingsWindow.isDestroyed()) return;
try {
await openSettingsWindow(electronModule, options, { showOnLoad: false });
} catch (err) {
debugLog("Failed to pre-warm settings window", { error: String(err) });
}
}
return {
restoreWindowInputFocus,
showAndFocusWindow,
isLiveWindow,
resolveSettingsWindowBounds,
centerSettingsWindowOnSourceDisplay,
openSettingsWindow,
closeSettingsWindow,
hideSettingsWindow,
prewarmSettingsWindow,
};
}
}
module.exports = { createSettingsWindowApi };