import { join } from "node:path"; import { BrowserWindow, Menu, MenuItem, app, ipcMain, nativeImage, } from "electron"; import windowIconAsset from "../../assets/desktop/icon.png?asset"; import { config } from "./config"; import { updateTrayMenu } from "./tray"; // global reference to main window export let mainWindow: BrowserWindow; // currently in-use build export const BUILD_URL = new URL( app.commandLine.hasSwitch("force-server") ? app.commandLine.getSwitchValue("force-server") : /*MAIN_WINDOW_VITE_DEV_SERVER_URL ??*/ "https://mithraic.space/app", ); // internal window state let shouldQuit = false; // load the window icon const windowIcon = nativeImage.createFromDataURL(windowIconAsset); // windowIcon.setTemplateImage(true); /** * Create the main application window */ export function createMainWindow() { // (CLI arg --hidden or config) const startHidden = app.commandLine.hasSwitch("hidden") || config.startMinimisedToTray; // create the window mainWindow = new BrowserWindow({ minWidth: 300, minHeight: 300, width: 1280, height: 720, backgroundColor: "#191919", frame: !config.customFrame, icon: windowIcon, show: !startHidden, webPreferences: { // relative to `.vite/build` preload: join(__dirname, "preload.js"), contextIsolation: true, nodeIntegration: false, spellcheck: true, }, }); // hide the options mainWindow.setMenu(null); // restore last position if it was moved previously if (config.windowState.x > 0 || config.windowState.y > 0) { mainWindow.setPosition( config.windowState.x ?? 0, config.windowState.y ?? 0, ); } // restore last size if it was resized previously if (config.windowState.width > 0 && config.windowState.height > 0) { mainWindow.setSize( config.windowState.width ?? 1280, config.windowState.height ?? 720, ); } // maximise the window if it was maximised before if (config.windowState.isMaximised) { mainWindow.maximize(); } // load the entrypoint mainWindow.loadURL(BUILD_URL.toString()); // minimise window to tray mainWindow.on("close", (event) => { if (!shouldQuit && config.minimiseToTray) { event.preventDefault(); mainWindow.hide(); } }); // update tray menu when window is shown/hidden mainWindow.on("show", updateTrayMenu); mainWindow.on("hide", updateTrayMenu); // keep track of window state function generateState() { config.windowState = { x: mainWindow.getPosition()[0], y: mainWindow.getPosition()[1], width: mainWindow.getSize()[0], height: mainWindow.getSize()[1], isMaximised: mainWindow.isMaximized(), }; } mainWindow.on("maximize", generateState); mainWindow.on("unmaximize", generateState); mainWindow.on("moved", generateState); mainWindow.on("resized", generateState); // rebind zoom controls to be more sensible mainWindow.webContents.on("before-input-event", (event, input) => { if (input.control && (input.key === "=" || input.key === "+")) { // zoom in (+) event.preventDefault(); mainWindow.webContents.setZoomLevel( mainWindow.webContents.getZoomLevel() + 1, ); } else if (input.control && input.key === "-") { // zoom out (-) event.preventDefault(); mainWindow.webContents.setZoomLevel( mainWindow.webContents.getZoomLevel() - 1, ); } else if (input.control && input.key === "0") { // reset zoom to default. event.preventDefault(); mainWindow.webContents.setZoomLevel(0); } else if ( input.key === "F5" || ((input.control || input.meta) && input.key.toLowerCase() === "r") ) { event.preventDefault(); mainWindow.webContents.reload(); } else if (input.key === "F12") { mainWindow.webContents.toggleDevTools(); } }); // send the config mainWindow.webContents.on("did-finish-load", () => { config.sync(); injectBranding(mainWindow.webContents); }); // configure spellchecker context menu mainWindow.webContents.on("context-menu", (_, params) => { const menu = new Menu(); // add all suggestions for (const suggestion of params.dictionarySuggestions) { menu.append( new MenuItem({ label: suggestion, click: () => mainWindow.webContents.replaceMisspelling(suggestion), }), ); } // allow users to add the misspelled word to the dictionary if (params.misspelledWord) { menu.append( new MenuItem({ label: "Add to dictionary", click: () => mainWindow.webContents.session.addWordToSpellCheckerDictionary( params.misspelledWord, ), }), ); } // add an option to toggle spellchecker menu.append( new MenuItem({ label: "Toggle spellcheck", click() { config.spellchecker = !config.spellchecker; }, }), ); // show menu if we've generated enough entries if (menu.items.length > 0) { menu.popup(); } }); // push world events to the window ipcMain.on("minimise", () => mainWindow.minimize()); ipcMain.on("maximise", () => mainWindow.isMaximized() ? mainWindow.unmaximize() : mainWindow.maximize(), ); ipcMain.on("close", () => mainWindow.close()); // mainWindow.webContents.openDevTools(); // let i = 0; // setInterval(() => setBadgeCount((++i % 30) + 1), 1000); } function injectBranding(wc: Electron.WebContents) { const logoUrl = windowIconAsset; wc.insertCSS(` [class*="wordmark"], [class*="Wordmark"], [data-app-name] { display: none !important; } `); wc.executeJavaScript(` (function() { const LOGO = ${JSON.stringify(logoUrl)}; const BRAND_RE = /\\b(Revolt|Stoat)\\b/g; const SKIP_TAGS = new Set(['SCRIPT','STYLE','TEXTAREA','INPUT','CODE','PRE']); function isLogoImg(img) { var src = img.getAttribute('src') || ''; var alt = (img.getAttribute('alt') || '').toLowerCase(); // explicit brand name in src or alt if (src.includes('revolt') || src.includes('stoat')) return true; if (alt === 'revolt' || alt === 'stoat') return true; // any asset image whose alt contains logo/brand keywords if (/revolt|stoat|logo/i.test(alt)) return true; // hashed asset paths with no alt — likely a logo if it's an svg or small png // and sits inside a known logo/brand container var parent = img.closest('[class*="logo"],[class*="Logo"],[class*="brand"],[class*="Brand"],[class*="wordmark"],[class*="Wordmark"],[class*="header"],[class*="auth"],[class*="login"],[class*="splash"]'); if (parent && /\\.(svg|png|webp)/.test(src)) return true; return false; } function patchImages() { document.querySelectorAll('img').forEach(function(img) { if (img.dataset.sanctumPatched) return; if (isLogoImg(img)) { img.src = LOGO; img.removeAttribute('srcset'); img.alt = 'Sanctum'; img.dataset.sanctumPatched = '1'; } }); } function replaceSvgWithLogo(svg) { var box = svg.getBoundingClientRect(); if (box.height > 0 && box.height < 48) { svg.parentNode.removeChild(svg); return; } var size = '96px'; var wrap = document.createElement('div'); wrap.style.cssText = 'display:flex;align-items:center;justify-content:center;width:100%;'; var img = document.createElement('img'); img.src = LOGO; img.alt = 'Sanctum'; img.style.cssText = 'width:' + size + ';height:' + size + ';object-fit:contain;flex-shrink:0;'; img.dataset.sanctumPatched = '1'; wrap.appendChild(img); svg.parentNode.replaceChild(wrap, svg); } function patchSVGs() { document.querySelectorAll('svg').forEach(function(svg) { if (svg.dataset.sanctumPatched) return; // Match the Revolt wordmark SVG by its unique path data fingerprint var paths = svg.querySelectorAll('path'); for (var i = 0; i < paths.length; i++) { var d = paths[i].getAttribute('d') || ''; if (d.includes('M478.909') || d.includes('M5.063') || d.includes('Revolt')) { replaceSvgWithLogo(svg); return; } } // Also catch any wide wordmark-style SVG in an auth/login container var parent = svg.closest('[class*="logo"],[class*="Logo"],[class*="brand"],[class*="Brand"],[class*="wordmark"],[class*="Wordmark"],[class*="auth"],[class*="login"],[class*="splash"],[class*="Landing"]'); if (parent) { var box = svg.getBoundingClientRect(); if (box.width > 80 && box.width / (box.height || 1) > 2) { replaceSvgWithLogo(svg); } } }); } function patchText(root) { var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); var node; while ((node = walker.nextNode())) { if (SKIP_TAGS.has(node.parentElement && node.parentElement.tagName)) continue; if (BRAND_RE.test(node.nodeValue)) { node.nodeValue = node.nodeValue.replace(BRAND_RE, 'Sanctum'); } BRAND_RE.lastIndex = 0; } } function patchTitle() { if (document.title && BRAND_RE.test(document.title)) { document.title = document.title.replace(BRAND_RE, 'Sanctum'); } BRAND_RE.lastIndex = 0; } function patch(root) { patchImages(); patchSVGs(); patchText(root || document.body); patchTitle(); } patch(document.documentElement); new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(n) { if (n.nodeType === 1) patch(n); else if (n.nodeType === 3 && !SKIP_TAGS.has(n.parentElement && n.parentElement.tagName)) { if (BRAND_RE.test(n.nodeValue)) n.nodeValue = n.nodeValue.replace(BRAND_RE, 'Sanctum'); BRAND_RE.lastIndex = 0; } }); }); patchTitle(); }).observe(document.documentElement, { childList: true, subtree: true }); })(); `, true).catch(function() {}); } /** * Quit the entire app */ export function quitApp() { shouldQuit = true; mainWindow.close(); } // Ensure global app quit works properly app.on("before-quit", () => { shouldQuit = true; });