diff --git a/src/discord/ipc.ts b/src/discord/ipc.ts index 6fc93fe..1f524e7 100644 --- a/src/discord/ipc.ts +++ b/src/discord/ipc.ts @@ -1,10 +1,11 @@ //ipc stuff import {app, clipboard, desktopCapturer, ipcMain, nativeImage, shell, SourcesOptions} from "electron"; -import {mainWindow} from "./window.js"; +import {BrowserWindow} from "electron"; import os from "os"; import fs from "fs"; import path from "path"; +import {mainWindows} from "./window.js"; import {getConfig, setConfigBulk, getConfigLocation} from "../common/config.js"; import {setLang, getLang, getLangName} from "../common/lang.js"; import {getVersion, getDisplayVersion} from "../common/version.js"; @@ -19,7 +20,20 @@ const userDataPath = app.getPath("userData"); const storagePath = path.join(userDataPath, "/storage/"); const themesPath = path.join(userDataPath, "/themes/"); const pluginsPath = path.join(userDataPath, "/plugins/"); -export function registerIpc(): void { +export function registerIpc(passedWindow: BrowserWindow): void { + ipcMain.on("splashEnd", () => { + splashWindow.close(); + if (getConfig("startMinimized")) { + passedWindow.hide(); + } else { + passedWindow.show(); + } + }); + + if (mainWindows.length !== 1) { + return; + } + ipcMain.on("get-app-path", (event) => { event.reply("app-path", app.getAppPath()); }); @@ -40,33 +54,33 @@ export function registerIpc(): void { case "win32": if (pingCount > 0) { const image = nativeImage.createFromPath(path.join(import.meta.dirname, "../", `/assets/ping.png`)); - mainWindow.setOverlayIcon(image, "badgeCount"); + passedWindow.setOverlayIcon(image, "badgeCount"); } else { - mainWindow.setOverlayIcon(null, "badgeCount"); + passedWindow.setOverlayIcon(null, "badgeCount"); } break; } }); ipcMain.on("win-maximize", () => { - mainWindow.maximize(); + passedWindow.maximize(); }); ipcMain.on("win-isMaximized", (event) => { - event.returnValue = mainWindow.isMaximized(); + event.returnValue = passedWindow.isMaximized(); }); ipcMain.on("win-isNormal", (event) => { - event.returnValue = mainWindow.isNormal(); + event.returnValue = passedWindow.isNormal(); }); ipcMain.on("win-minimize", () => { - mainWindow.minimize(); + passedWindow.minimize(); }); ipcMain.on("win-unmaximize", () => { - mainWindow.unmaximize(); + passedWindow.unmaximize(); }); ipcMain.on("win-show", () => { - mainWindow.show(); + passedWindow.show(); }); ipcMain.on("win-hide", () => { - mainWindow.hide(); + passedWindow.hide(); }); ipcMain.on("win-quit", () => { app.exit(); @@ -80,14 +94,6 @@ export function registerIpc(): void { ipcMain.on("modInstallState", (event) => { event.returnValue = modInstallState; }); - ipcMain.on("splashEnd", () => { - splashWindow.close(); - if (getConfig("startMinimized")) { - mainWindow.hide(); - } else { - mainWindow.show(); - } - }); ipcMain.on("restart", () => { app.relaunch(); app.exit(); diff --git a/src/discord/menu.ts b/src/discord/menu.ts index 6b8aeef..40c2054 100644 --- a/src/discord/menu.ts +++ b/src/discord/menu.ts @@ -1,5 +1,5 @@ import {BrowserWindow, Menu, app} from "electron"; -import {mainWindow} from "./window.js"; +import {mainWindows} from "./window.js"; import {createSettingsWindow} from "../settings/main.js"; export function setMenu(): void { @@ -31,7 +31,9 @@ export function setMenu(): void { label: "Reload", accelerator: "CmdOrCtrl+R", click() { - mainWindow.reload(); + mainWindows.forEach((mainWindow) => { + mainWindow.reload(); + }); } }, { diff --git a/src/discord/window.ts b/src/discord/window.ts index eb4823d..8b90040 100644 --- a/src/discord/window.ts +++ b/src/discord/window.ts @@ -13,10 +13,10 @@ import contextMenu from "electron-context-menu"; import os from "os"; import RPCServer from "arrpc"; import {tray} from "../tray.js"; -import {iconPath} from "../main.js"; +import {iconPath, init} from "../main.js"; import {getConfig, setConfig, firstRun} from "../common/config.js"; import {getWindowState, setWindowState} from "../common/windowState.js"; -export let mainWindow: BrowserWindow; +export let mainWindows: BrowserWindow[] = []; export let inviteWindow: BrowserWindow; let forceQuit = false; let osType = os.type(); @@ -43,49 +43,57 @@ contextMenu({ } ] }); -function doAfterDefiningTheWindow(): void { +function doAfterDefiningTheWindow(passedWindow: BrowserWindow): void { if (getWindowState("isMaximized") ?? false) { - mainWindow.setSize(835, 600); //just so the whole thing doesn't cover whole screen - mainWindow.maximize(); - void mainWindow.webContents.executeJavaScript(`document.body.setAttribute("isMaximized", "");`); - mainWindow.hide(); // please don't flashbang the user + passedWindow.setSize(835, 600); //just so the whole thing doesn't cover whole screen + passedWindow.maximize(); + void passedWindow.webContents.executeJavaScript(`document.body.setAttribute("isMaximized", "");`); + passedWindow.hide(); // please don't flashbang the user } if (getConfig("windowStyle") == "transparency" && process.platform === "win32") { - mainWindow.setBackgroundMaterial("mica"); + passedWindow.setBackgroundMaterial("mica"); if (getConfig("startMinimized") == false) { - mainWindow.show(); + passedWindow.show(); } } // REVIEW - Test the protocol warning. I was not sure how to get it to pop up. For now I've voided the promises. const ignoreProtocolWarning = getConfig("ignoreProtocolWarning"); - registerIpc(); + registerIpc(passedWindow); if (getConfig("mobileMode")) { - mainWindow.webContents.userAgent = + passedWindow.webContents.userAgent = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.149 Mobile Safari/537.36"; } else { // A little sloppy but it works :p if (osType == "Windows_NT") { osType = `Windows ${os.release().split(".")[0]} (${os.release()})`; } - mainWindow.webContents.userAgent = `Mozilla/5.0 (X11; ${osType} ${os.arch()}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36`; //fake useragent for screenshare to work + passedWindow.webContents.userAgent = `Mozilla/5.0 (X11; ${osType} ${os.arch()}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36`; //fake useragent for screenshare to work } - app.on("second-instance", (_event, _commandLine, _workingDirectory, additionalData) => { - // Print out data received from the second instance. - console.log(additionalData); + if (mainWindows.length === 1) { + app.on("second-instance", (_event, _commandLine, _workingDirectory, additionalData) => { + void (async () => { + // Print out data received from the second instance. + console.log(additionalData); - // Someone tried to run a second instance, we should focus our window. - if (mainWindow) { - if (mainWindow.isMinimized()) mainWindow.restore(); - mainWindow.show(); - mainWindow.focus(); - } - }); + if (getConfig("multiInstance") == (false ?? undefined)) { + // Someone tried to run a second instance, we should focus our window. + if (passedWindow) { + if (passedWindow.isMinimized()) passedWindow.restore(); + passedWindow.show(); + passedWindow.focus(); + } + } else { + await init(); + } + })(); + }); + } app.on("activate", function () { app.show(); }); - mainWindow.webContents.setWindowOpenHandler(({url}) => { + passedWindow.webContents.setWindowOpenHandler(({url}) => { // Allow about:blank (used by Vencord QuickCss popup) if (url === "about:blank") return {action: "allow"}; // Allow Discord stream popout @@ -116,7 +124,7 @@ function doAfterDefiningTheWindow(): void { checkboxChecked: false }; - void dialog.showMessageBox(mainWindow, options).then(({response, checkboxChecked}) => { + void dialog.showMessageBox(passedWindow, options).then(({response, checkboxChecked}) => { console.log(response, checkboxChecked); if (checkboxChecked) { if (response == 0) { @@ -137,15 +145,15 @@ function doAfterDefiningTheWindow(): void { import("./screenshare/main.js"); } - mainWindow.webContents.session.webRequest.onBeforeRequest( + passedWindow.webContents.session.webRequest.onBeforeRequest( {urls: ["https://*/api/v*/science", "https://sentry.io/*", "https://*.nel.cloudflare.com/*"]}, (_, callback) => callback({cancel: true}) ); if (getConfig("trayIcon") == "default" || getConfig("dynamicIcon")) { - mainWindow.webContents.on("page-favicon-updated", () => { + passedWindow.webContents.on("page-favicon-updated", () => { // REVIEW - no need to await if we just .then() - This works! - void mainWindow.webContents + void passedWindow.webContents .executeJavaScript( ` var getFavicon = function(){ @@ -177,17 +185,17 @@ function doAfterDefiningTheWindow(): void { } } if (getConfig("dynamicIcon")) { - mainWindow.setIcon(trayPath); + passedWindow.setIcon(trayPath); } }); }); } - mainWindow.webContents.on("page-title-updated", (e, title) => { + passedWindow.webContents.on("page-title-updated", (e, title) => { const armCordSuffix = " - ArmCord"; /* identify */ if (!title.endsWith(armCordSuffix)) { e.preventDefault(); // REVIEW - I don't see a reason to wait for the titlebar to update - void mainWindow.webContents.executeJavaScript( + void passedWindow.webContents.executeJavaScript( `document.title = '${title.replace("Discord |", "") + armCordSuffix}'` ); } @@ -201,7 +209,7 @@ function doAfterDefiningTheWindow(): void { if (!fs.existsSync(`${userDataPath}/disabled.txt`)) { fs.writeFileSync(path.join(userDataPath, "/disabled.txt"), ""); } - mainWindow.webContents.on("did-finish-load", () => { + passedWindow.webContents.on("did-finish-load", () => { fs.readdirSync(themesFolder).forEach((file) => { try { const manifest = fs.readFileSync(`${themesFolder}/${file}/manifest.json`, "utf8"); @@ -214,7 +222,7 @@ function doAfterDefiningTheWindow(): void { ) { console.log(`%cSkipped ${themeFile.name} made by ${themeFile.author}`, "color:red"); } else { - mainWindow.webContents.send( + passedWindow.webContents.send( "themeLoader", fs.readFileSync(`${themesFolder}/${file}/${themeFile.theme}`, "utf-8") ); @@ -226,21 +234,24 @@ function doAfterDefiningTheWindow(): void { }); }); setMenu(); - mainWindow.on("close", (e) => { + passedWindow.on("close", (e) => { if (process.platform === "darwin" && forceQuit) { - mainWindow.close(); + passedWindow.close(); + } else if (mainWindows.length > 1) { + mainWindows = mainWindows.filter((mainWindow) => mainWindow.id != passedWindow.id); + passedWindow.destroy(); } else { - const [width, height] = mainWindow.getSize(); + const [width, height] = passedWindow.getSize(); setWindowState({ width, height, - isMaximized: mainWindow.isMaximized(), - x: mainWindow.getPosition()[0], - y: mainWindow.getPosition()[1] + isMaximized: passedWindow.isMaximized(), + x: passedWindow.getPosition()[0], + y: passedWindow.getPosition()[1] }); if (getConfig("minimizeToTray")) { e.preventDefault(); - mainWindow.hide(); + passedWindow.hide(); } else if (!getConfig("minimizeToTray")) { e.preventDefault(); app.quit(); @@ -258,25 +269,25 @@ function doAfterDefiningTheWindow(): void { } // REVIEW - Awaiting javascript execution is silly - mainWindow.on("focus", () => { - void mainWindow.webContents.executeJavaScript(`document.body.removeAttribute("unFocused");`); + passedWindow.on("focus", () => { + void passedWindow.webContents.executeJavaScript(`document.body.removeAttribute("unFocused");`); }); - mainWindow.on("blur", () => { - void mainWindow.webContents.executeJavaScript(`document.body.setAttribute("unFocused", "");`); + passedWindow.on("blur", () => { + void passedWindow.webContents.executeJavaScript(`document.body.setAttribute("unFocused", "");`); }); - mainWindow.on("maximize", () => { - void mainWindow.webContents.executeJavaScript(`document.body.setAttribute("isMaximized", "");`); + passedWindow.on("maximize", () => { + void passedWindow.webContents.executeJavaScript(`document.body.setAttribute("isMaximized", "");`); }); - mainWindow.on("unmaximize", () => { - void mainWindow.webContents.executeJavaScript(`document.body.removeAttribute("isMaximized");`); + passedWindow.on("unmaximize", () => { + void passedWindow.webContents.executeJavaScript(`document.body.removeAttribute("isMaximized");`); }); - if (getConfig("inviteWebsocket")) { + if (getConfig("inviteWebsocket") && mainWindows.length === 1) { // NOTE - RPCServer appears to be untyped. cool. // REVIEW - Whatever Ducko has done here to make an async constructor is awful. // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access new RPCServer().then((server: EventEmitter) => { - server.on("activity", (data: string) => mainWindow.webContents.send("rpc", data)); + server.on("activity", (data: string) => passedWindow.webContents.send("rpc", data)); server.on("invite", (code: string) => { console.log(code); createInviteWindow(code); @@ -284,17 +295,17 @@ function doAfterDefiningTheWindow(): void { }); } if (firstRun) { - mainWindow.close(); + passedWindow.close(); } //loadURL broke for no good reason after E28 - void mainWindow.loadFile(`${import.meta.dirname}/../splash/redirect.html`); + void passedWindow.loadFile(`${import.meta.dirname}/../splash/redirect.html`); if (getConfig("skipSplash")) { - mainWindow.show(); + passedWindow.show(); } } export function createCustomWindow(): void { - mainWindow = new BrowserWindow({ + const mainWindow = new BrowserWindow({ width: getWindowState("width") ?? 835, height: getWindowState("height") ?? 600, x: getWindowState("x"), @@ -313,10 +324,11 @@ export function createCustomWindow(): void { spellcheck: getConfig("spellcheck") } }); - doAfterDefiningTheWindow(); + mainWindows.push(mainWindow); + doAfterDefiningTheWindow(mainWindow); } export function createNativeWindow(): void { - mainWindow = new BrowserWindow({ + const mainWindow = new BrowserWindow({ width: getWindowState("width") ?? 835, height: getWindowState("height") ?? 600, x: getWindowState("x"), @@ -335,10 +347,11 @@ export function createNativeWindow(): void { spellcheck: getConfig("spellcheck") } }); - doAfterDefiningTheWindow(); + mainWindows.push(mainWindow); + doAfterDefiningTheWindow(mainWindow); } export function createTransparentWindow(): void { - mainWindow = new BrowserWindow({ + const mainWindow = new BrowserWindow({ width: getWindowState("width") ?? 835, height: getWindowState("height") ?? 600, x: getWindowState("x"), @@ -357,7 +370,8 @@ export function createTransparentWindow(): void { spellcheck: getConfig("spellcheck") } }); - doAfterDefiningTheWindow(); + mainWindows.push(mainWindow); + doAfterDefiningTheWindow(mainWindow); } export function createInviteWindow(code: string): void { inviteWindow = new BrowserWindow({ @@ -381,7 +395,7 @@ export function createInviteWindow(code: string): void { // REVIEW - This shouldn't matter, since below we have an event on it void inviteWindow.loadURL(formInviteURL); inviteWindow.webContents.once("did-finish-load", () => { - if (!mainWindow.webContents.isLoading()) { + if (!mainWindows[0].webContents.isLoading()) { inviteWindow.show(); inviteWindow.webContents.once("will-navigate", () => { inviteWindow.close(); diff --git a/src/main.ts b/src/main.ts index 1cc7315..26b0d2b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -49,8 +49,33 @@ async function args(): Promise { }); } } +export async function init(): Promise { + if (getConfig("skipSplash") == false) { + void createSplashWindow(); // REVIEW - Awaiting will hang at start + } + if (firstRun == true) { + setLang(new Intl.DateTimeFormat().resolvedOptions().locale); + await createSetupWindow(); + } + switch (getConfig("windowStyle")) { + case "default": + createCustomWindow(); + customTitlebar = true; + break; + case "native": + createNativeWindow(); + break; + case "transparent": + createTransparentWindow(); + break; + default: + createCustomWindow(); + customTitlebar = true; + break; + } +} await args(); // i want my top level awaits - IMPLEMENTED :) -if (!app.requestSingleInstanceLock() && getConfig("multiInstance") == (false ?? undefined)) { +if (!app.requestSingleInstanceLock()) { // if value isn't set after 3.2.4 // kill if 2nd instance app.quit(); @@ -88,31 +113,6 @@ if (!app.requestSingleInstanceLock() && getConfig("multiInstance") == (false ?? } else { iconPath = path.join(import.meta.dirname, "../", "/assets/desktop.png"); } - async function init(): Promise { - if (getConfig("skipSplash") == false) { - void createSplashWindow(); // REVIEW - Awaiting will hang at start - } - if (firstRun == true) { - setLang(new Intl.DateTimeFormat().resolvedOptions().locale); - await createSetupWindow(); - } - switch (getConfig("windowStyle")) { - case "default": - createCustomWindow(); - customTitlebar = true; - break; - case "native": - createNativeWindow(); - break; - case "transparent": - createTransparentWindow(); - break; - default: - createCustomWindow(); - customTitlebar = true; - break; - } - } await init(); await installModLoader(); session.fromPartition("some-partition").setPermissionRequestHandler((_webContents, permission, callback) => { diff --git a/src/themeManager/main.ts b/src/themeManager/main.ts index acb47a2..c7c8c84 100644 --- a/src/themeManager/main.ts +++ b/src/themeManager/main.ts @@ -1,7 +1,7 @@ import {BrowserWindow, app, dialog, ipcMain, shell} from "electron"; import path from "path"; import fs from "fs"; -import {createInviteWindow, mainWindow} from "../discord/window.js"; +import {createInviteWindow, mainWindows} from "../discord/window.js"; import type {ThemeManifest} from "../types/themeManifest.d.js"; let themeWindow: BrowserWindow; let instance = 0; @@ -125,7 +125,9 @@ export async function createTManagerWindow(): Promise { shell.showItemInFolder(themesPath); }); ipcMain.on("reloadMain", () => { - mainWindow.webContents.reload(); + mainWindows.forEach((mainWindow) => { + mainWindow.webContents.reload(); + }); }); ipcMain.on("addToDisabled", (_event, name: string) => { fs.appendFileSync(path.join(userDataPath, "/disabled.txt"), `${name}\n`); @@ -147,7 +149,9 @@ export async function createTManagerWindow(): Promise { console.log(`Removed ${id} folder`); } themeWindow.webContents.reload(); - mainWindow.webContents.reload(); + mainWindows.forEach((mainWindow) => { + mainWindow.webContents.reload(); + }); }); ipcMain.on("installBDTheme", (_event, link: string) => { return async () => { @@ -170,7 +174,9 @@ export async function createTManagerWindow(): Promise { message: "Successfully imported theme from link." }); themeWindow.webContents.reload(); - mainWindow.webContents.reload(); + mainWindows.forEach((mainWindow) => { + mainWindow.webContents.reload(); + }); } catch (e) { dialog.showErrorBox( "BD Theme import fail", diff --git a/src/tray.ts b/src/tray.ts index d076aac..2424fd2 100644 --- a/src/tray.ts +++ b/src/tray.ts @@ -1,6 +1,6 @@ import fs from "fs"; import {Menu, MessageBoxOptions, Tray, app, dialog, nativeImage} from "electron"; -import {createInviteWindow, mainWindow} from "./discord/window.js"; +import {createInviteWindow, mainWindows} from "./discord/window.js"; import path from "path"; import {createSettingsWindow} from "./settings/main.js"; import {getConfig, getConfigLocation, setConfig} from "./common/config.js"; @@ -62,7 +62,9 @@ void app.whenReady().then(async () => { { label: `Open ${clientName}`, click() { - mainWindow.show(); + mainWindows.forEach((mainWindow) => { + mainWindow.show(); + }); } }, { @@ -92,7 +94,9 @@ void app.whenReady().then(async () => { } tray.setToolTip(clientName); tray.on("click", function () { - mainWindow.show(); + mainWindows.forEach((mainWindow) => { + mainWindow.show(); + }); }); } else { if (getConfig("tray") == undefined) { @@ -106,7 +110,7 @@ void app.whenReady().then(async () => { detail: "Linux may not work well with tray icons. Depending on your system configuration, you may not be able to see the tray icon. Enable at your own risk. Can be changed later." }; - await dialog.showMessageBox(mainWindow, options).then(({response}) => { + await dialog.showMessageBox(mainWindows[0], options).then(({response}) => { if (response == 0) { setConfig("tray", true); } else {