diff --git a/src/common/flags.ts b/src/common/flags.ts index d5337bb5..4de320e8 100644 --- a/src/common/flags.ts +++ b/src/common/flags.ts @@ -1,4 +1,7 @@ -import { powerMonitor } from "electron"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { app, powerMonitor } from "electron"; +import isDev from "electron-is-dev"; import { getConfig } from "./config.js"; interface Preset { @@ -7,6 +10,9 @@ interface Preset { disableFeatures: string[]; } +// Cache for custom flags to avoid repeated file reads +let customFlagsCache: Preset | null = null; + const performance: Preset = { switches: [ ["enable-gpu-rasterization"], @@ -81,6 +87,79 @@ const vaapi: Preset = { disableFeatures: ["UseChromeOSDirectVideoDecoder"], }; +/** + * Load custom flags from JSON file in user data directory (cached after first load) + * Path: + * - Windows: %APPDATA%\legcord\flags.json (typically C:\Users\{username}\AppData\Roaming\legcord\flags.json) + * - macOS: ~/Library/Application Support/legcord/flags.json + * - Linux: ~/.config/legcord/flags.json + * Returns an empty preset if file doesn't exist or is invalid + */ +function loadCustomFlags(): Preset { + // Return cached result to avoid repeated disk reads + if (customFlagsCache !== null) { + return customFlagsCache; + } + + const customPreset: Preset = { + switches: [], + enableFeatures: [], + disableFeatures: [], + }; + + try { + const userDataPath = app.getPath("userData"); + const customFlagsPath = join(userDataPath, "flags.json"); + + try { + const fileContent = readFileSync(customFlagsPath, "utf-8"); + const customFlags = JSON.parse(fileContent); + + // Merge switches + if (Array.isArray(customFlags.switches)) { + customPreset.switches = customFlags.switches; + } + + // Merge enableFeatures + if (Array.isArray(customFlags.enableFeatures)) { + customPreset.enableFeatures = customFlags.enableFeatures; + } + + // Merge disableFeatures + if (Array.isArray(customFlags.disableFeatures)) { + customPreset.disableFeatures = customFlags.disableFeatures; + } + + if (isDev) console.log(`Custom flags loaded from ${customFlagsPath}`); + } catch (fileError) { + if ((fileError as NodeJS.ErrnoException).code === "ENOENT") { + if (isDev) console.log(`Custom flags file not found at ${customFlagsPath}`); + } else if (isDev) { + console.error(`Error reading custom flags file: ${fileError}`); + } + } + } catch (error) { + if (isDev) console.error(`Error loading custom flags: ${error}`); + } + + customFlagsCache = customPreset; + return customPreset; +} + +/** + * Merge a preset with custom flags + * Custom flags will be appended to the preset's arrays + */ +function mergeWithCustomFlags(preset: Preset): Preset { + const customFlags = loadCustomFlags(); + + return { + switches: [...preset.switches, ...customFlags.switches], + enableFeatures: [...preset.enableFeatures, ...customFlags.enableFeatures], + disableFeatures: [...preset.disableFeatures, ...customFlags.disableFeatures], + }; +} + export function getPreset(): Preset | undefined { // MIT License @@ -107,24 +186,31 @@ export function getPreset(): Preset | undefined { case "dynamic": if (powerMonitor.isOnBatteryPower()) { console.log("Battery mode enabled"); - return battery; + return mergeWithCustomFlags(battery); } else { console.log("Performance mode enabled"); - return performance; + return mergeWithCustomFlags(performance); } case "performance": console.log("Performance mode enabled"); - return performance; + return mergeWithCustomFlags(performance); case "battery": console.log("Battery mode enabled"); - return battery; + return mergeWithCustomFlags(battery); case "vaapi": console.log("VAAPI mode enabled"); - return vaapi; + return mergeWithCustomFlags(vaapi); case "smoothScreenshare": console.log("Smooth screenshare mode enabled"); - return smoothExperiment; + return mergeWithCustomFlags(smoothExperiment); default: console.log("No performance modes set"); } } + +/** + * Get the currently applied preset for debugging purposes + */ +export function getCurrentPreset(): Preset | undefined { + return getPreset(); +} diff --git a/src/discord/ipc.ts b/src/discord/ipc.ts index 6da8a7d6..2fd38fce 100644 --- a/src/discord/ipc.ts +++ b/src/discord/ipc.ts @@ -13,6 +13,7 @@ import { getLang, getLangName, getRawLang, setLang } from "../common/lang.js"; import { installTheme, setThemeEnabled, uninstallTheme } from "../common/themes.js"; import { getDisplayVersion, getVersion } from "../common/version.js"; import { openCssEditor } from "../cssEditor/main.js"; +import { getAppliedFlags } from "../main.js"; import { isPowerSavingEnabled, setPowerSaving } from "../power.js"; import constPaths from "../shared/consts/paths.js"; import { splashWindow } from "../splash/main.js"; @@ -227,6 +228,11 @@ export function registerIpc(passedWindow: BrowserWindow): void { ipcMain.on("isDev", (event) => { event.returnValue = isDev; }); + ipcMain.on("dumpFlags", (event) => { + const flags = getAppliedFlags(); + console.log(`=== Chrome Flags === ${JSON.stringify(flags)}`); + event.returnValue = flags; + }); ipcMain.on("setConfig", (_event, key: keyof Settings, value: string) => { setConfig(key, value); }); diff --git a/src/discord/preload/bridge.ts b/src/discord/preload/bridge.ts index f7a0d92d..c6ae97e8 100644 --- a/src/discord/preload/bridge.ts +++ b/src/discord/preload/bridge.ts @@ -4,6 +4,7 @@ import type { Keybind } from "../../@types/keybind.js"; import type { LegcordWindow } from "../../@types/legcordWindow.d.ts"; import type { Settings } from "../../@types/settings.js"; import type { ThemeManifest } from "../../@types/themeManifest.js"; +import type { AppliedFlagsOutput } from "../../main.js"; import type { venmicListObject } from "../venmic.js"; let windowCallback: (arg0: object) => void; @@ -36,6 +37,7 @@ contextBridge.exposeInMainWorld("legcord", { openCustomIconDialog: () => ipcRenderer.send("openCustomIconDialog"), copyDebugInfo: () => ipcRenderer.send("copyDebugInfo"), copyGPUInfo: () => ipcRenderer.send("copyGPUInfo"), + dumpFlags: () => ipcRenderer.sendSync("dumpFlags") as AppliedFlagsOutput, }, touchbar: { setVoiceTouchbar: (state: boolean) => ipcRenderer.send("setVoiceTouchbar", state), diff --git a/src/discord/preload/patches.mts b/src/discord/preload/patches.mts index 449fe6dc..305f1933 100644 --- a/src/discord/preload/patches.mts +++ b/src/discord/preload/patches.mts @@ -63,21 +63,18 @@ async function load() { }); injectJS("legcord://assets/js/patchVencordQuickCSS.js"); // Settings info version injection - setInterval(() => { - const host = document.querySelector('[class*="sidebar"] [class*="info"]'); - if (!host || host.querySelector("#ac-ver")) { - return; - } + const observer = new MutationObserver(() => { + if (document.body.querySelector("#ac-ver")) return; - const discordVersionInfoPattern = /(stable|ptb|canary) \d+|Electron|Chromium/i; - if (!discordVersionInfoPattern.test(host.textContent || "")) { - return; - } + const info = document.body.querySelector('[class*="sidebar"] [class*="compactInfo"]'); + const host = info?.parentElement; + if (!host || !/(stable|ptb|canary) \d+|Electron|Chromium/i.test(host.textContent)) return; - const el = host.firstElementChild!.cloneNode() as HTMLSpanElement; + const el = host.querySelector("span")!.cloneNode() as HTMLSpanElement; el.id = "ac-ver"; el.textContent = `Legcord Version: ${version}`; - host.append(el); - }, 1000); + info.after(el); + }); + observer.observe(document.body, { childList: true, subtree: true }); } load(); diff --git a/src/discord/window.ts b/src/discord/window.ts index 91863815..ff2713ff 100644 --- a/src/discord/window.ts +++ b/src/discord/window.ts @@ -369,6 +369,7 @@ export function createWindow() { browserWindowOptions.titleBarStyle = "hidden"; browserWindowOptions.titleBarOverlay = false; } + break; case "native": browserWindowOptions.frame = true; break; diff --git a/src/main.ts b/src/main.ts index 5c8c80a4..93769ad6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,6 @@ // Modules to control application life and create native browser window import { BrowserWindow, app, crashReporter, session, systemPreferences } from "electron"; +import isDev from "electron-is-dev"; import "./discord/extensions/csp.js"; import "./protocol.js"; import { readFileSync } from "node:fs"; @@ -18,6 +19,40 @@ import { import "./updater.js"; import { getPreset } from "./common/flags.js"; import { setLang } from "./common/lang.js"; + +// Chrome flags tracking +export interface AppliedFlagsOutput { + switches: Record; + enableFeatures: string[]; + disableFeatures: string[]; + enableBlinkFeatures: string[]; + disableBlinkFeatures: string[]; +} + +const tracker = { + switches: new Map(), + enableFeatures: new Set(), + disableFeatures: new Set(), + enableBlinkFeatures: new Set(), + disableBlinkFeatures: new Set(), +}; + +const trackSwitch = (key: string, value?: string) => tracker.switches.set(key, value ?? true); +const trackEnableFeatures = (features: string[]) => features.forEach((f) => tracker.enableFeatures.add(f)); +const trackDisableFeatures = (features: string[]) => features.forEach((f) => tracker.disableFeatures.add(f)); +const trackEnableBlinkFeatures = (features: string[]) => features.forEach((f) => tracker.enableBlinkFeatures.add(f)); +const trackDisableBlinkFeatures = (features: string[]) => features.forEach((f) => tracker.disableBlinkFeatures.add(f)); + +export function getAppliedFlags(): AppliedFlagsOutput { + return { + switches: Object.fromEntries(tracker.switches), + enableFeatures: Array.from(tracker.enableFeatures), + disableFeatures: Array.from(tracker.disableFeatures), + enableBlinkFeatures: Array.from(tracker.enableBlinkFeatures), + disableBlinkFeatures: Array.from(tracker.disableBlinkFeatures), + }; +} + import { fetchMods } from "./discord/extensions/modloader.js"; import { createWindow } from "./discord/window.js"; import { createSetupWindow } from "./setup/main.js"; @@ -98,9 +133,11 @@ if (!app.requestSingleInstanceLock() && getConfig("multiInstance") === false) { // enable pulseaudio audio sharing on linux if (process.platform === "linux") { app.commandLine.appendSwitch("gtk-version", "3"); + trackSwitch("gtk-version", "3"); enableFeatures.add("PulseaudioLoopbackForScreenShare"); disableFeatures.add("WebRtcAllowInputVolumeAdjustment"); app.commandLine.appendSwitch("enable-speech-dispatcher"); + trackSwitch("enable-speech-dispatcher"); } // enable webrtc capturer for wayland if (process.platform === "linux" && process.env.XDG_SESSION_TYPE?.toLowerCase() === "wayland") { @@ -114,12 +151,17 @@ if (!app.requestSingleInstanceLock() && getConfig("multiInstance") === false) { } // work around chrome 66 disabling autoplay by default app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required"); + trackSwitch("autoplay-policy", "no-user-gesture-required"); app.commandLine.appendSwitch("enable-transparent-visuals"); + trackSwitch("enable-transparent-visuals"); checkIfConfigIsBroken(); const preset = getPreset(); if (preset) { - preset.switches.forEach(([key, val]) => app.commandLine.appendSwitch(key, val)); + preset.switches.forEach(([key, val]) => { + app.commandLine.appendSwitch(key, val); + trackSwitch(key, val); + }); preset.enableFeatures.forEach((val) => enableFeatures.add(val)); preset.disableFeatures.forEach((val) => disableFeatures.add(val)); } @@ -162,42 +204,69 @@ if (!app.requestSingleInstanceLock() && getConfig("multiInstance") === false) { if (getConfig("additionalArguments") !== undefined) { for (const arg of getConfig("additionalArguments").split(" ")) { if (arg.startsWith("--")) { - const [key, val] = arg.substring(2).split("=", 1) as [string, string?]; + const [key, ...rest] = arg.substring(2).split("="); + const val = rest.length > 0 ? rest.join("=") : undefined; if (val === undefined) { app.commandLine.appendSwitch(key); + trackSwitch(key); } else { if (key === "enable-features") { - val.split(",").forEach((flag) => enableFeatures.add(flag)); + const flags = val.split(","); + flags.forEach((flag) => enableFeatures.add(flag)); } else if (key === "disable-features") { - val.split(",").forEach((flag) => disableFeatures.add(flag)); + const flags = val.split(","); + flags.forEach((flag) => disableFeatures.add(flag)); } else if (key === "enable-blink-features") { - val.split(",").forEach((flag) => enableBlinkFeatures.add(flag)); + const flags = val.split(","); + flags.forEach((flag) => enableBlinkFeatures.add(flag)); } else if (key === "disable-blink-features") { - val.split(",").forEach((flag) => disableBlinkFeatures.add(flag)); + const flags = val.split(","); + flags.forEach((flag) => disableBlinkFeatures.add(flag)); } else { app.commandLine.appendSwitch(key, val); + trackSwitch(key, val); } } } } } - if (getConfig("smoothScroll") === false) app.commandLine.appendSwitch("disable-smooth-scrolling"); + if (getConfig("smoothScroll") === false) { + app.commandLine.appendSwitch("disable-smooth-scrolling"); + trackSwitch("disable-smooth-scrolling"); + } if (getConfig("autoScroll")) enableBlinkFeatures.add("MiddleClickAutoscroll"); - if (getConfig("disableHttpCache")) app.commandLine.appendSwitch("disable-http-cache"); + if (getConfig("disableHttpCache")) { + app.commandLine.appendSwitch("disable-http-cache"); + trackSwitch("disable-http-cache"); + } enableFeatures.delete(""); disableFeatures.delete(""); enableBlinkFeatures.delete(""); disableBlinkFeatures.delete(""); - if (enableFeatures.size > 0) app.commandLine.appendSwitch("enable-features", Array.from(enableFeatures).join(",")); - if (disableFeatures.size > 0) - app.commandLine.appendSwitch("disable-features", Array.from(disableFeatures).join(",")); - if (enableBlinkFeatures.size > 0) - app.commandLine.appendSwitch("enable-blink-features", Array.from(enableBlinkFeatures).join(",")); - if (disableBlinkFeatures.size > 0) - app.commandLine.appendSwitch("disable-blink-features", Array.from(disableBlinkFeatures).join(",")); + if (enableFeatures.size > 0) { + const featuresStr = Array.from(enableFeatures).join(","); + app.commandLine.appendSwitch("enable-features", featuresStr); + trackEnableFeatures(Array.from(enableFeatures)); + } + if (disableFeatures.size > 0) { + const featuresStr = Array.from(disableFeatures).join(","); + app.commandLine.appendSwitch("disable-features", featuresStr); + trackDisableFeatures(Array.from(disableFeatures)); + } + if (enableBlinkFeatures.size > 0) { + const featuresStr = Array.from(enableBlinkFeatures).join(","); + app.commandLine.appendSwitch("enable-blink-features", featuresStr); + trackEnableBlinkFeatures(Array.from(enableBlinkFeatures)); + } + if (disableBlinkFeatures.size > 0) { + const featuresStr = Array.from(disableBlinkFeatures).join(","); + app.commandLine.appendSwitch("disable-blink-features", featuresStr); + trackDisableBlinkFeatures(Array.from(disableBlinkFeatures)); + } void app.whenReady().then(async () => { + if (isDev) console.log(JSON.stringify(getAppliedFlags())); process.on("SIGINT", () => app.quit()); process.on("SIGTERM", () => app.quit()); // Patch for linux bug to ensure things are loaded before window creation (fixes transparency on some linux systems)