From 09662fc37e0d6ade4b605e38deaa1c0d9a755f5d Mon Sep 17 00:00:00 2001 From: Amelia Frost Date: Sat, 28 Mar 2026 17:22:40 -0700 Subject: [PATCH] Add the system's native menu to the titlebar, configurable --- avia_core/customFrameNativeMenu.js | 57 ++++++++++++++++++++++++++++++ src/config.d.ts | 1 + src/main.ts | 3 +- src/native/config.ts | 18 ++++++++++ src/native/window.ts | 33 +++++++++++++++-- 5 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 avia_core/customFrameNativeMenu.js diff --git a/avia_core/customFrameNativeMenu.js b/avia_core/customFrameNativeMenu.js new file mode 100644 index 0000000..f2f759f --- /dev/null +++ b/avia_core/customFrameNativeMenu.js @@ -0,0 +1,57 @@ +(function () { + if (window.__customFrameNativeMenu) return; + window.__customFrameNativeMenu = true; + + function toggleCheckbox(newElem, toggle) { + const tmp = newElem.querySelector("mdui-checkbox"); + if (tmp && tmp !== undefined) { + if (toggle) { + tmp.setAttribute('checked', ''); + tmp.setAttribute('value', 'on'); + } else { + tmp.removeAttribute('checked'); + tmp.setAttribute('value', 'off'); + } + } + } + + function initCFNM() { + let elem = document.querySelector("#floating div:not(:empty) div.will-change_transform.flex_1_1_800px div:has(> a) > a:last-child") + if (!elem) { return; } + + let title = elem.querySelector("div.flex-g_1 > div") + if (!title || title.textContent.trim() !== "Custom window frame") { return; } + + let desc = elem.querySelector("div.flex-g_1 > span") + if (!desc || desc.textContent.trim() !== "Let Stoat use its own custom titlebar.") { return; } + + var newElem = elem.cloneNode(true); + let nTitle = newElem.querySelector("div.flex-g_1 > div"); + let nDesc = newElem.querySelector("div.flex-g_1 > span"); + if (!nTitle || !nDesc) { newElem = null; return; } + + nTitle.textContent = "Native menu on custom window frame"; + nDesc.textContent = "Use the system's native menu on the custom window frame."; + + let config = window.desktopConfig.get(); + toggleCheckbox(newElem, config.customFrameNativeMenu); + + newElem.addEventListener("click", e => { + e.preventDefault(); + e.stopPropagation(); + + let config = window.desktopConfig.get(); + config.customFrameNativeMenu = !config.customFrameNativeMenu; + window.desktopConfig.set(config); + + toggleCheckbox(newElem, config.customFrameNativeMenu); + }); + + elem.parentNode.appendChild(newElem); + } + + initCFNM(); + + const observer = new MutationObserver(() => initCFNM()); + observer.observe(document.body, { childList: true, subtree: true }); +})(); diff --git a/src/config.d.ts b/src/config.d.ts index d5f0cde..2b4dc63 100644 --- a/src/config.d.ts +++ b/src/config.d.ts @@ -1,6 +1,7 @@ declare type DesktopConfig = { firstLaunch: boolean; customFrame: boolean; + customFrameNativeMenu: boolean; minimiseToTray: boolean; spellchecker: boolean; hardwareAcceleration: boolean; diff --git a/src/main.ts b/src/main.ts index 42d0832..7d3982b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -58,7 +58,8 @@ const loadInject = () => { "repofrontend.js", "ButtonFix.js", "headliner.js", - "aviadesktopversion.js" + "aviadesktopversion.js", + "customFrameNativeMenu.js" ]; for (const plugin of plugins) { diff --git a/src/native/config.ts b/src/native/config.ts index f7c2d37..6b2514d 100644 --- a/src/native/config.ts +++ b/src/native/config.ts @@ -13,6 +13,9 @@ const schema = { customFrame: { type: "boolean", } as JSONSchema.Boolean, + customFrameNativeMenu: { + type: "boolean", + } as JSONSchema.Boolean, minimiseToTray: { type: "boolean", } as JSONSchema.Boolean, @@ -55,6 +58,7 @@ const store = new Store({ defaults: { firstLaunch: true, customFrame: true, + customFrameNativeMenu: false, minimiseToTray: true, startMinimisedToTray: false, spellchecker: true, @@ -78,6 +82,7 @@ class Config { mainWindow.webContents.send("config", { firstLaunch: this.firstLaunch, customFrame: this.customFrame, + customFrameNativeMenu: this.customFrameNativeMenu, minimiseToTray: this.minimiseToTray, startMinimisedToTray: this.startMinimisedToTray, spellchecker: this.spellchecker, @@ -113,6 +118,19 @@ class Config { this.sync(); } + get customFrameNativeMenu() { + return (store as never as { get(k: string): boolean }).get("customFrameNativeMenu"); + } + + set customFrameNativeMenu(value: boolean) { + (store as never as { set(k: string, value: boolean): void }).set( + "customFrameNativeMenu", + value, + ); + + this.sync(); + } + get minimiseToTray() { return (store as never as { get(k: string): boolean }).get( "minimiseToTray", diff --git a/src/native/window.ts b/src/native/window.ts index 74db0ff..ca2dd9d 100644 --- a/src/native/window.ts +++ b/src/native/window.ts @@ -48,6 +48,14 @@ export function createMainWindow() { height: 720, backgroundColor: "#191919", frame: !config.customFrame, + ...(config.customFrame && config.customFrameNativeMenu ? { + // remove the default titlebar + titleBarStyle: 'hidden', + // expose window controls in Windows/Linux + ...(process.platform !== 'darwin' ? { + titleBarOverlay: true + } : {}) + } : {}), icon: windowIcon, show: !startHidden, webPreferences: { @@ -141,8 +149,29 @@ export function createMainWindow() { } }); - // send the config - mainWindow.webContents.on("did-finish-load", () => config.sync()); + const initialCustomFrame: boolean = config.customFrame; + const initialCFNM: boolean = config.customFrameNativeMenu; + + mainWindow.webContents.on("did-finish-load", () => { + // send the config + config.sync(); + + // on macOS add margin to the title, and hide custom controls + // We only use initial values other the menu can disappear + if (process.platform === 'darwin' && + initialCustomFrame && initialCFNM) { + mainWindow.webContents.insertCSS(` + #root > div[style="display: flex; flex-direction: column; height: 100%;"] > div > div.h_29px { + &> div.d_flex:first-child { + margin-left: 75px; + } + &> a.place-items_center { + display: none; + } + } + `); + } + }); // configure spellchecker context menu mainWindow.webContents.on("context-menu", (_, params) => {