Compare commits

..

4 commits

Author SHA1 Message Date
smartfrigde
158f537e7d fix: building 2026-06-15 16:12:49 +02:00
smartfrigde
8aea77ba25 revert: sleepInBackground default to false 2026-06-15 16:09:35 +02:00
youtsuho
3ec1bc2d32
fix: Memory leaks and CPU fixes (performance audit) (#1102) 2026-06-15 16:07:03 +02:00
youtsuho
11d02104d9
fix: first launch blank settings and theme (#1101) 2026-06-15 16:04:53 +02:00
14 changed files with 74 additions and 66 deletions

View file

@ -5,7 +5,7 @@ import { applyAppImageSandboxFix } from "./scripts/build/sandboxFix.mjs";
export const config: Configuration = { export const config: Configuration = {
appId: "app.legcord.Legcord", appId: "app.legcord.Legcord",
productName: "Legcord", productName: "Legcord",
artifactName: `Legcord-${version}-${os}-${arch}.${ext}`, artifactName: "Legcord-${version}-${os}-${arch}.${ext}",
beforePack: applyAppImageSandboxFix, beforePack: applyAppImageSandboxFix,
protocols: [ protocols: [
{ {

View file

@ -10,7 +10,7 @@ export let firstRun: boolean;
// Performance optimization: Cache config to avoid reading file on every call // Performance optimization: Cache config to avoid reading file on every call
let configCache: Settings | null = null; let configCache: Settings | null = null;
let configCacheTime = 0; let configCacheTime = 0;
const CONFIG_CACHE_TTL = 1000; // Cache for 1 second const CONFIG_CACHE_TTL = 5000; // Cache for 5 seconds
const defaults: Settings = { const defaults: Settings = {
windowStyle: "default", windowStyle: "default",
channel: "stable", channel: "stable",

View file

@ -1,7 +1,12 @@
import type { BrowserWindow } from "electron"; import type { BrowserWindow } from "electron";
let scriptCounter = 0;
export function addStyle(styleUrl: string): void { export function addStyle(styleUrl: string): void {
const id = `legcord-style-${styleUrl.replace(/[^a-zA-Z0-9]/g, "-")}`;
if (document.getElementById(id)) return;
const style = document.createElement("link"); const style = document.createElement("link");
style.id = id;
style.rel = "stylesheet"; style.rel = "stylesheet";
style.type = "text/css"; style.type = "text/css";
style.href = styleUrl; style.href = styleUrl;
@ -9,6 +14,7 @@ export function addStyle(styleUrl: string): void {
} }
export function addTheme(id: string, styleString: string): void { export function addTheme(id: string, styleString: string): void {
if (document.getElementById(id)) return;
const style = document.createElement("style"); const style = document.createElement("style");
style.textContent = styleString; style.textContent = styleString;
style.id = id; style.id = id;
@ -16,18 +22,21 @@ export function addTheme(id: string, styleString: string): void {
} }
export function addScript(scriptString: string): void { export function addScript(scriptString: string): void {
const id = `legcord-script-${++scriptCounter}`;
if (document.getElementById(id)) return;
const script = document.createElement("script"); const script = document.createElement("script");
script.id = id;
script.appendChild(document.createTextNode(scriptString)); script.appendChild(document.createTextNode(scriptString));
document.body.append(script); document.body.append(script);
} }
export async function injectJS(inject: string): Promise<void> { export async function injectJS(inject: string): Promise<void> {
const id = `legcord-inject-${inject.replace(/[^a-zA-Z0-9]/g, "-")}`;
if (document.getElementById(id)) return;
const js = await (await fetch(`${inject}`)).text(); const js = await (await fetch(`${inject}`)).text();
const el = document.createElement("script"); const el = document.createElement("script");
el.id = id;
el.appendChild(document.createTextNode(js)); el.appendChild(document.createTextNode(js));
document.body.appendChild(el); document.body.appendChild(el);
} }

View file

@ -6,7 +6,7 @@ import type { WindowState } from "../@types/windowState.js";
// Performance optimization: Cache window state to avoid reading file on every call // Performance optimization: Cache window state to avoid reading file on every call
let windowStateCache: WindowState | null = null; let windowStateCache: WindowState | null = null;
let windowStateCacheTime = 0; let windowStateCacheTime = 0;
const WINDOW_STATE_CACHE_TTL = 1000; // Cache for 1 second const WINDOW_STATE_CACHE_TTL = 5000; // Cache for 5 seconds
export function getWindowStateLocation() { export function getWindowStateLocation() {
const userDataPath = app.getPath("userData"); const userDataPath = app.getPath("userData");

View file

@ -54,8 +54,7 @@ async function cacheCheck(mod: ValidMods) {
} }
try { try {
const latestRef = await getRef(modData[mod].repoData); const latestRef = await getRef(modData[mod].repoData);
// biome-ignore lint/correctness/noConstantCondition: https://github.com/Legcord/Legcord/issues/763 if (latestRef === modCache![mod]) {
if (/*latestRef === modCache![mod]*/ false) {
console.log(`[Mod Loader]: ${mod} Cache hit!`); console.log(`[Mod Loader]: ${mod} Cache hit!`);
return; return;
} else { } else {

View file

@ -60,7 +60,11 @@ function ifExistsRead(path: string): string | undefined {
if (existsSync(path)) return readFileSync(path, "utf-8"); if (existsSync(path)) return readFileSync(path, "utf-8");
} }
let ipcRegistered = false;
export function registerIpc(passedWindow: BrowserWindow): void { export function registerIpc(passedWindow: BrowserWindow): void {
if (ipcRegistered) return;
ipcRegistered = true;
ipcMain.handle("getShelterBundle", () => { ipcMain.handle("getShelterBundle", () => {
return { return {
js: ifExistsRead(path.join(app.getPath("userData"), "shelter.js")), js: ifExistsRead(path.join(app.getPath("userData"), "shelter.js")),

View file

@ -1,18 +1,10 @@
type OptimizableFunction<T extends Node> = (child: T) => T; type OptimizableFunction<T extends Node> = (child: T) => T;
const optimize = <T extends Node>(orig: OptimizableFunction<T>) => { const optimize = <T extends Node>(orig: OptimizableFunction<T>) => {
return function (this: Element, ...args: [Element]): T | number { return function (this: Element, ...args: [Element]): T {
if (typeof args[0]?.className === "string" && args[0].className.includes("activity")) {
// fix by xql.dev <@1356430365774053448>
setTimeout(() => orig.apply(this, args as unknown as [T]), 100);
return args[0] as unknown as T;
}
return orig.apply(this, args as unknown as [T]); return orig.apply(this, args as unknown as [T]);
} as unknown as OptimizableFunction<T>; } as unknown as OptimizableFunction<T>;
}; };
// We are taking in the function itself
// eslint-disable-next-line @typescript-eslint/unbound-method // eslint-disable-next-line @typescript-eslint/unbound-method
Element.prototype.removeChild = optimize(Element.prototype.removeChild); Element.prototype.removeChild = optimize(Element.prototype.removeChild);
// Thanks Ari - <@1249446413952225452>

View file

@ -205,6 +205,7 @@ async function load() {
el.id = "ac-ver"; el.id = "ac-ver";
el.textContent = `Legcord Version: ${version}`; el.textContent = `Legcord Version: ${version}`;
info.after(el); info.after(el);
observer.disconnect();
}); });
observer.observe(document.body, { childList: true, subtree: true }); observer.observe(document.body, { childList: true, subtree: true });
} }

View file

@ -9,6 +9,9 @@ let rpcWorker: Worker;
export let processList: GameList[] = []; export let processList: GameList[] = [];
export function startRPC(window: BrowserWindow) { export function startRPC(window: BrowserWindow) {
if (rpcWorker) {
rpcWorker.terminate();
}
const rpcPath = path.join(__dirname, "rpc.js"); const rpcPath = path.join(__dirname, "rpc.js");
rpcWorker = new Worker(rpcPath, { rpcWorker = new Worker(rpcPath, {

View file

@ -187,6 +187,14 @@ function doAfterDefiningTheWindow(passedWindow: BrowserWindow): void {
if (blockedPatterns.some((pattern) => pattern.test(details.url))) { if (blockedPatterns.some((pattern) => pattern.test(details.url))) {
return callback({ cancel: true }); return callback({ cancel: true });
} }
if (
details.url.includes("ws://127.0.0.1:") &&
!details.url.includes("127.0.0.1:1211") &&
!details.url.includes("127.0.0.1:1112") &&
!details.url.includes("127.0.0.1:6888")
) {
return callback({ cancel: true });
}
return callback({}); return callback({});
}); });
@ -268,9 +276,7 @@ function doAfterDefiningTheWindow(passedWindow: BrowserWindow): void {
// Update window title with Legcord suffix // Update window title with Legcord suffix
if (!title.endsWith(legcordSuffix)) { if (!title.endsWith(legcordSuffix)) {
e.preventDefault(); e.preventDefault();
// Security: Use JSON.stringify to prevent code injection via title passedWindow.setTitle(title.replace("Discord |", "") + legcordSuffix);
const safeTitle = JSON.stringify(title.replace("Discord |", "") + legcordSuffix);
void passedWindow.webContents.executeJavaScript(`document.title = ${safeTitle}`);
} }
}); });
injectThemesMain(passedWindow); injectThemesMain(passedWindow);
@ -303,18 +309,7 @@ function doAfterDefiningTheWindow(passedWindow: BrowserWindow): void {
}); });
setForceQuit(true); setForceQuit(true);
}); });
passedWindow.webContents.session.webRequest.onBeforeRequest((details, callback) => {
// Lune Dev exceptions, https://github.com/uwu/shelter/blob/8d4ca369bf01abf348df9d4e111d534800c7a38c/packages/shelter/src/devmode/index.tsx#L24
if (
details.url.includes("ws://127.0.0.1:") &&
!details.url.includes("127.0.0.1:1211") &&
!details.url.includes("127.0.0.1:1112") &&
!details.url.includes("127.0.0.1:6888")
) {
return callback({ cancel: true });
}
return callback({});
});
passedWindow.on("focus", () => { passedWindow.on("focus", () => {
void passedWindow.webContents.executeJavaScript(`document.body.removeAttribute("unFocused");`); void passedWindow.webContents.executeJavaScript(`document.body.removeAttribute("unFocused");`);
}); });

View file

@ -22,26 +22,30 @@ const {
flux: { dispatcher, storesFlat }, flux: { dispatcher, storesFlat },
} = shelter; } = shelter;
const settingsPages = [ let settingsCleanups: (() => void)[] = [];
registerSection("divider"),
registerSection("header", "Legcord"), function registerSections(): (() => void)[] {
registerSection("section", "legcord-settings", "Settings", SettingsPage, { icon: SettingsSidebarIcon }), return [
registerSection("section", "legcord-themes", "Themes", ThemesPage, { icon: ThemesSidebarIcon }), registerSection("divider"),
registerSection("section", "legcord-plugins", "Plugins", PluginsPage, { icon: PluginsSidebarIcon }), registerSection("header", "Legcord"),
registerSection("section", "legcord-keybinds", "Keybinds", KeybindsPage, { icon: KeybindsSidebarIcon }), registerSection("section", "legcord-settings", "Settings", SettingsPage, { icon: SettingsSidebarIcon }),
registerSection("section", "legcord-games", "Games", RegisteredGamesPage, { icon: GamesSidebarIcon }), registerSection("section", "legcord-themes", "Themes", ThemesPage, { icon: ThemesSidebarIcon }),
]; registerSection("section", "legcord-plugins", "Plugins", PluginsPage, { icon: PluginsSidebarIcon }),
registerSection("section", "legcord-keybinds", "Keybinds", KeybindsPage, { icon: KeybindsSidebarIcon }),
registerSection("section", "legcord-games", "Games", RegisteredGamesPage, { icon: GamesSidebarIcon }),
];
}
function restartRequired(payload: { event: string; properties: { origin_pane: string } }) { function restartRequired(payload: { event: string; properties: { origin_pane: string } }) {
if (payload.event === "settings_pane_viewed" && typeof payload.properties.origin_pane !== "undefined") { if (payload.event === "settings_pane_viewed" && typeof payload.properties.origin_pane !== "undefined") {
const pane = payload.properties.origin_pane; const pane = payload.properties.origin_pane;
if ((pane === "legcord-settings" || pane === "legcord-games") && isRestartRequired) { if ((pane === "legcord-settings" || pane === "legcord-games") && isRestartRequired) {
openConfirmationModal({ openConfirmationModal({
header: () => store.i18n["settings-restartRequired"], header: () => store.i18n?.["settings-restartRequired"] ?? "Restart required",
body: () => store.i18n["settings-restartRequiredBody"], body: () => store.i18n?.["settings-restartRequiredBody"] ?? "A restart is required to apply changes.",
type: "danger", type: "danger",
confirmText: store.i18n["settings-restart"], confirmText: store.i18n?.["settings-restart"] ?? "Restart",
cancelText: store.i18n["settings-restartLater"], cancelText: store.i18n?.["settings-restartLater"] ?? "Later",
}).then( }).then(
() => window.legcord.restart(), () => window.legcord.restart(),
() => console.log("restart skipped"), () => console.log("restart skipped"),
@ -57,11 +61,11 @@ export function onLoad() {
store.i18n = window.legcord.translations; store.i18n = window.legcord.translations;
log("Legcord Settings"); log("Legcord Settings");
window.legcord.settings.setLang(storesFlat.LocaleStore.locale); window.legcord.settings.setLang(storesFlat.LocaleStore.locale);
settingsPages; settingsCleanups = registerSections();
dispatcher.subscribe("TRACK", restartRequired); dispatcher.subscribe("TRACK", restartRequired);
} }
export function onUnload() { export function onUnload() {
settingsPages.forEach((e) => { settingsCleanups.forEach((e) => {
e(); e();
}); });
dispatcher.unsubscribe("TRACK", restartRequired); dispatcher.unsubscribe("TRACK", restartRequired);

View file

@ -12,21 +12,24 @@ const {
ui: { SwitchItem, Header, HeaderTags, Button, ButtonSizes }, ui: { SwitchItem, Header, HeaderTags, Button, ButtonSizes },
} = shelter; } = shelter;
const settings = store.settings as Settings; const noBundleUpdates = (settings: Settings) => {
const noBundleUpdates = () => {
const value = settings.noBundleUpdates; const value = settings.noBundleUpdates;
if (Array.isArray(value)) return value; if (Array.isArray(value)) return value;
return value ? ["shelter", "vencord", "equicord", "custom"] : []; return value ? ["shelter", "vencord", "equicord", "custom"] : [];
}; };
export function SettingsPage() { export function SettingsPage() {
const settings = store.settings as Settings;
if (!settings) { if (!settings) {
return ( return (
<> <>
<Header class={classes.category} tag={HeaderTags.HeadingXL}> <Header class={classes.category} tag={HeaderTags.HeadingXL}>
{store.i18n["settings-firstTimeCrash"]} {store.i18n?.["settings-firstTimeCrash"] ?? "Setting things up..."}
</Header> </Header>
<p>{store.i18n["settings-firstTimeCrash-desc"]}</p> <p>
{store.i18n?.["settings-firstTimeCrash-desc"] ??
"Settings are not available on a first-time launch. Please restart."}
</p>
<br /> <br />
<Button size={ButtonSizes.MAX} onClick={() => window.legcord.restart()}> <Button size={ButtonSizes.MAX} onClick={() => window.legcord.restart()}>
Restart Legcord Restart Legcord
@ -529,9 +532,9 @@ export function SettingsPage() {
</Header> </Header>
<SwitchItem <SwitchItem
note={store.i18n["settings-noBundleUpdates-desc"]} note={store.i18n["settings-noBundleUpdates-desc"]}
value={noBundleUpdates().includes("shelter")} value={noBundleUpdates(settings).includes("shelter")}
onChange={(e: boolean) => { onChange={(e: boolean) => {
const next = new Set(noBundleUpdates()); const next = new Set(noBundleUpdates(settings));
if (e) next.add("shelter"); if (e) next.add("shelter");
else next.delete("shelter"); else next.delete("shelter");
setConfig("noBundleUpdates", Array.from(next) as Settings["noBundleUpdates"], true); setConfig("noBundleUpdates", Array.from(next) as Settings["noBundleUpdates"], true);
@ -541,9 +544,9 @@ export function SettingsPage() {
</SwitchItem> </SwitchItem>
<Show when={settings.mods.includes("vencord")}> <Show when={settings.mods.includes("vencord")}>
<SwitchItem <SwitchItem
value={noBundleUpdates().includes("vencord")} value={noBundleUpdates(settings).includes("vencord")}
onChange={(e: boolean) => { onChange={(e: boolean) => {
const next = new Set(noBundleUpdates()); const next = new Set(noBundleUpdates(settings));
if (e) next.add("vencord"); if (e) next.add("vencord");
else next.delete("vencord"); else next.delete("vencord");
setConfig("noBundleUpdates", Array.from(next) as Settings["noBundleUpdates"], true); setConfig("noBundleUpdates", Array.from(next) as Settings["noBundleUpdates"], true);
@ -554,9 +557,9 @@ export function SettingsPage() {
</Show> </Show>
<Show when={settings.mods.includes("equicord")}> <Show when={settings.mods.includes("equicord")}>
<SwitchItem <SwitchItem
value={noBundleUpdates().includes("equicord")} value={noBundleUpdates(settings).includes("equicord")}
onChange={(e: boolean) => { onChange={(e: boolean) => {
const next = new Set(noBundleUpdates()); const next = new Set(noBundleUpdates(settings));
if (e) next.add("equicord"); if (e) next.add("equicord");
else next.delete("equicord"); else next.delete("equicord");
setConfig("noBundleUpdates", Array.from(next) as Settings["noBundleUpdates"], true); setConfig("noBundleUpdates", Array.from(next) as Settings["noBundleUpdates"], true);
@ -567,9 +570,9 @@ export function SettingsPage() {
</Show> </Show>
<Show when={settings.mods.includes("custom")}> <Show when={settings.mods.includes("custom")}>
<SwitchItem <SwitchItem
value={noBundleUpdates().includes("custom")} value={noBundleUpdates(settings).includes("custom")}
onChange={(e: boolean) => { onChange={(e: boolean) => {
const next = new Set(noBundleUpdates()); const next = new Set(noBundleUpdates(settings));
if (e) next.add("custom"); if (e) next.add("custom");
else next.delete("custom"); else next.delete("custom");
setConfig("noBundleUpdates", Array.from(next) as Settings["noBundleUpdates"], true); setConfig("noBundleUpdates", Array.from(next) as Settings["noBundleUpdates"], true);

View file

@ -9,7 +9,6 @@ const {
ui: { Button, Header, HeaderTags, ButtonSizes, TextBox, showToast, SwitchItem }, ui: { Button, Header, HeaderTags, ButtonSizes, TextBox, showToast, SwitchItem },
plugin: { store }, plugin: { store },
} = shelter; } = shelter;
const settings = store.settings as Settings;
export function ThemesPage() { export function ThemesPage() {
const [downloadUrl, setDownloadUrl] = createSignal(""); const [downloadUrl, setDownloadUrl] = createSignal("");
refreshThemes(); refreshThemes();
@ -27,13 +26,14 @@ export function ThemesPage() {
}); });
} }
const settings = () => store.settings as Settings;
const t = store.i18n; const t = store.i18n;
return ( return (
<> <>
<Header tag={HeaderTags.H1}>Themes</Header> <Header tag={HeaderTags.H1}>Themes</Header>
<SwitchItem <SwitchItem
note={store.i18n["settings-quickCss-desc"]} note={store.i18n["settings-quickCss-desc"]}
value={settings.quickCss} value={settings().quickCss}
onChange={(e: boolean) => { onChange={(e: boolean) => {
console.log("Toggled quick CSS", e); console.log("Toggled quick CSS", e);
if (e) { if (e) {
@ -50,7 +50,7 @@ export function ThemesPage() {
<Button <Button
size={ButtonSizes.LARGE} size={ButtonSizes.LARGE}
onClick={window.legcord.themes.openQuickCss} onClick={window.legcord.themes.openQuickCss}
disabled={!settings.quickCss} disabled={!settings().quickCss}
> >
{t["themes-openQuickCss"]} {t["themes-openQuickCss"]}
</Button> </Button>

View file

@ -4,8 +4,6 @@ const {
plugin: { store }, plugin: { store },
} = shelter; } = shelter;
const settings = store.settings as Settings;
export let isRestartRequired = false; export let isRestartRequired = false;
export function setRestartRequired() { export function setRestartRequired() {
@ -21,7 +19,7 @@ export function refreshThemes() {
} }
export function setConfig<K extends keyof Settings>(key: K, value: Settings[K], shouldRestart?: boolean) { export function setConfig<K extends keyof Settings>(key: K, value: Settings[K], shouldRestart?: boolean) {
settings[key] = value; store.settings[key] = value;
console.log(key, ":", store.settings[key]); console.log(key, ":", store.settings[key]);
if (shouldRestart) { if (shouldRestart) {
isRestartRequired = true; isRestartRequired = true;
@ -36,7 +34,7 @@ function removeMod(array: ValidMods[], filter: ValidMods) {
export function toggleMod(mod: ValidMods, enabled: boolean) { export function toggleMod(mod: ValidMods, enabled: boolean) {
isRestartRequired = true; isRestartRequired = true;
const currentMods = settings.mods; const currentMods = store.settings.mods;
if (enabled) { if (enabled) {
if (mod === "vencord") { if (mod === "vencord") {
currentMods.push("vencord"); currentMods.push("vencord");