Compare commits

..

3 commits

7 changed files with 139 additions and 270 deletions

View file

@ -16,8 +16,7 @@ jobs:
- uses: actions/checkout@v4
- name: Run flatpak-node-generator
run: pipx run git+https://github.com/flatpak/flatpak-builder-tools.git#subdirectory=node pnpm pnpm-lock.yaml --node-sdk-extension org.freedesktop.Sdk.Extension.node26 --electron-node-headers
run: pipx run git+https://github.com/flatpak/flatpak-builder-tools.git#subdirectory=node pnpm pnpm-lock.yaml --node-sdk-extension org.freedesktop.Sdk.Extension.node26 --electron-node-headers --pnpm-store-version v11
- name: Upload generated-sources.json to release
run: |
gh release upload ${{ github.event.release.tag_name }} generated-sources.json

View file

@ -54,7 +54,8 @@ winget install --id=smartfrigde.Legcord -e
### Flatpak
Not available yet.
You can find our **official** Legcord flatpak on [Flathub!](https://flathub.org/en/apps/app.legcord.Legcord)
Maintained by @imide, a contributor to Legcord and is officially sanctioned by us.
### Debian, Ubuntu and Raspbian

View file

@ -7,10 +7,19 @@ import { getLang } from "./lang.js";
import { getWindowStateLocation } from "./windowState.js";
export let firstRun: boolean;
// Performance optimization: Cache config to avoid reading file on every call
let configCache: Settings | null = null;
let configCacheTime = 0;
const CONFIG_CACHE_TTL = 5000; // Cache for 5 seconds
function ensureConfigCache(): Settings {
if (configCache) return configCache;
try {
const rawData = readFileSync(getConfigLocation(), "utf-8");
configCache = JSON.parse(rawData) as Settings;
} catch {
configCache = {} as Settings;
}
return configCache;
}
const defaults: Settings = {
windowStyle: "overlay",
channel: "stable",
@ -110,17 +119,7 @@ export function getConfig<K extends keyof Settings>(object: K): Settings[K] {
return safeMode[object];
}
// Performance optimization: Use cached config if available and fresh
const now = Date.now();
if (configCache && now - configCacheTime < CONFIG_CACHE_TTL) {
return configCache[object];
}
const rawData = readFileSync(getConfigLocation(), "utf-8");
const returnData = JSON.parse(rawData) as Settings;
configCache = returnData;
configCacheTime = now;
return returnData[object];
return ensureConfigCache()[object];
}
const START_MINIMIZED_MODES = new Set<Settings["startMinimized"]>(["off", "minimized", "tray"]);
@ -148,33 +147,17 @@ function migrateStartMinimized(settingsObject: Record<string, unknown>): boolean
return false;
}
export function setConfig<K extends keyof Settings>(object: K, toSet: Settings[K]): void {
const rawData = readFileSync(getConfigLocation(), "utf-8");
const parsed = JSON.parse(rawData) as Settings;
const parsed = ensureConfigCache();
parsed[object] = toSet;
const toSave = JSON.stringify(parsed, null, 4);
writeFileSync(getConfigLocation(), toSave, "utf-8");
// Performance optimization: Update cache immediately
configCache = parsed;
configCacheTime = Date.now();
}
export function setConfigBulk(object: Settings): void {
let existingData = {};
try {
const existingDataBuffer = readFileSync(getConfigLocation(), "utf-8");
existingData = JSON.parse(existingDataBuffer.toString()) as Settings;
} catch (_error) {
// Ignore errors when the file doesn't exist or parsing fails
}
// Merge the existing data with the new data
const existingData = configCache ?? ({} as Settings);
const mergedData = { ...existingData, ...object };
// Write the merged data back to the file
configCache = mergedData as Settings;
const toSave = JSON.stringify(mergedData, null, 4);
writeFileSync(getConfigLocation(), toSave, "utf-8");
// Performance optimization: Update cache immediately
configCache = mergedData as Settings;
configCacheTime = Date.now();
}
export function checkIfConfigExists(): void {
const userDataPath = app.getPath("userData");
@ -208,18 +191,12 @@ export function checkIfConfigExists(): void {
}
export function checkIfConfigIsBroken(): void {
try {
const settingsData = readFileSync(getConfigLocation(), "utf-8");
const settingsObject = JSON.parse(settingsData) as Settings & Record<string, unknown>;
const settingsObject = ensureConfigCache() as Settings & Record<string, unknown>;
// Migrate before typeof repair — boolean → "tray" | "off"
if (migrateStartMinimized(settingsObject)) {
writeFileSync(getConfigLocation(), JSON.stringify(settingsObject, null, 4), "utf-8");
}
// Performance optimization: Update cache after validation
configCache = settingsObject as Settings;
configCacheTime = Date.now();
let configWasFine = true;
const settingsKeys = Object.keys(settingsObject) as (keyof Settings)[];
const defaultKeys = Object.keys(defaults) as (keyof Settings)[];
@ -245,13 +222,6 @@ export function checkIfConfigIsBroken(): void {
setConfig(missingKey, defaults[missingKey]);
});
// Performance optimization: Ensure cache is updated after fixes
if (!configWasFine) {
const updatedData = readFileSync(getConfigLocation(), "utf-8");
configCache = JSON.parse(updatedData) as Settings;
configCacheTime = Date.now();
}
console.log(configWasFine ? "Config is fine" : "Config is now fine");
} catch (e) {
console.error(e);

View file

@ -3,158 +3,77 @@ import path from "node:path";
import { app } from "electron";
import type { i18nStrings } from "../@types/i18nStrings.js";
// Performance optimization: Cache language files to avoid reading on every call
let languageCache: i18nStrings | null = null;
let languageCacheTime = 0;
let languageConfigCache: string | null = null;
let languageConfigCacheTime = 0;
const LANGUAGE_CACHE_TTL = 5000; // Cache for 5 seconds
let currentLanguage: string | null = null;
function resolveLangFromConfig(): string {
if (currentLanguage) return currentLanguage;
try {
const langConfigFile = `${path.join(app.getPath("userData"), "/storage/")}lang.json`;
const rawData = fs.readFileSync(langConfigFile, "utf-8");
const parsed = JSON.parse(rawData) as i18nStrings;
currentLanguage = parsed.lang;
} catch {
currentLanguage = "en-US";
}
if (currentLanguage.length === 2) {
currentLanguage = `${currentLanguage}-${currentLanguage.toUpperCase()}`;
}
return currentLanguage;
}
function getLangFilePath(lang: string): string {
const langPath = path.join(import.meta.dirname, "../", `/assets/lang/${lang}.json`);
if (fs.existsSync(langPath)) return langPath;
return path.join(import.meta.dirname, "../", "/assets/lang/en-US.json");
}
function loadLanguage(): i18nStrings {
if (languageCache) return languageCache;
const lang = resolveLangFromConfig();
const langPath = getLangFilePath(lang);
const fallbackPath = path.join(import.meta.dirname, "../", "/assets/lang/en-US.json");
const rawData = fs.readFileSync(langPath, "utf-8");
const parsed = JSON.parse(rawData) as i18nStrings;
if (langPath !== fallbackPath) {
const fallbackData = fs.readFileSync(fallbackPath, "utf-8");
const fallbackParsed = JSON.parse(fallbackData) as i18nStrings;
for (const key in fallbackParsed) {
if (parsed[key] === undefined) {
parsed[key] = fallbackParsed[key];
}
}
}
languageCache = parsed;
return parsed;
}
export function setLang(language: string): void {
const langConfigFile = `${path.join(app.getPath("userData"), "/storage/")}lang.json`;
if (!fs.existsSync(langConfigFile)) {
fs.writeFileSync(langConfigFile, "{}", "utf-8");
const dir = path.dirname(langConfigFile);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const rawData = fs.readFileSync(langConfigFile, "utf-8");
const parsed = JSON.parse(rawData) as i18nStrings;
parsed.lang = language;
const toSave = JSON.stringify(parsed, null, 4);
console.log(`Setting language to ${language}`);
const toSave = JSON.stringify({ lang: language }, null, 4);
fs.writeFileSync(langConfigFile, toSave, "utf-8");
// Performance optimization: Invalidate cache when language changes
languageConfigCache = language;
languageConfigCacheTime = Date.now();
languageCache = null; // Invalidate language file cache
console.log(`Setting language to ${language}`);
currentLanguage = null;
languageCache = null;
}
let language: string;
export function getLang(object: string): string {
// Performance optimization: Use cached language config if available
const now = Date.now();
if (languageConfigCache && now - languageConfigCacheTime < LANGUAGE_CACHE_TTL) {
language = languageConfigCache;
} else if (language === undefined) {
try {
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
const langConfigFile = `${storagePath}lang.json`;
const rawData = fs.readFileSync(langConfigFile, "utf-8");
const parsed = JSON.parse(rawData) as i18nStrings;
language = parsed.lang;
languageConfigCache = language;
languageConfigCacheTime = now;
} catch (_e) {
console.log("Language config file doesn't exist. Fallback to English.");
language = "en-US";
languageConfigCache = language;
languageConfigCacheTime = now;
}
}
if (language.length === 2) {
language = `${language}-${language.toUpperCase()}`;
}
// Performance optimization: Use cached language file if available
const normalizedLang = language;
if (languageCache && now - languageCacheTime < LANGUAGE_CACHE_TTL) {
if (languageCache[object] !== undefined) {
return languageCache[object];
}
}
let langPath = path.join(import.meta.dirname, "../", `/assets/lang/${normalizedLang}.json`);
if (!fs.existsSync(langPath)) {
langPath = path.join(import.meta.dirname, "../", "/assets/lang/en-US.json");
}
let rawData = fs.readFileSync(langPath, "utf-8");
let parsed = JSON.parse(rawData) as i18nStrings;
if (parsed[object] === undefined) {
console.log(`${object} is undefined in ${normalizedLang}`);
langPath = path.join(import.meta.dirname, "../", "/assets/lang/en-US.json");
rawData = fs.readFileSync(langPath, "utf-8");
parsed = JSON.parse(rawData) as i18nStrings;
}
// Update cache
languageCache = parsed;
languageCacheTime = now;
return parsed[object];
const data = loadLanguage();
return data[object] ?? "";
}
export function getRawLang(): i18nStrings {
// Performance optimization: Use cached result if available
const now = Date.now();
if (languageCache && now - languageCacheTime < LANGUAGE_CACHE_TTL) {
return languageCache;
}
if (language === undefined) {
try {
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
const langConfigFile = `${storagePath}lang.json`;
const rawData = fs.readFileSync(langConfigFile, "utf-8");
const parsed = JSON.parse(rawData) as i18nStrings;
language = parsed.lang;
languageConfigCache = language;
languageConfigCacheTime = now;
} catch (_e) {
console.log("Language config file doesn't exist. Fallback to English.");
language = "en-US";
languageConfigCache = language;
languageConfigCacheTime = now;
}
}
if (language.length === 2) {
language = `${language}-${language.toUpperCase()}`;
}
let langPath = path.join(import.meta.dirname, "../", `/assets/lang/${language}.json`);
if (!fs.existsSync(langPath)) {
langPath = path.join(import.meta.dirname, "../", "/assets/lang/en-US.json");
}
const fallbackPath = path.join(import.meta.dirname, "../", "/assets/lang/en-US.json");
const rawData = fs.readFileSync(langPath, "utf-8");
const parsed = JSON.parse(rawData) as i18nStrings;
const fallbackData = fs.readFileSync(fallbackPath, "utf-8");
const fallbackParsed = JSON.parse(fallbackData) as i18nStrings;
for (const key in fallbackParsed) {
if (parsed[key] === undefined) {
parsed[key] = fallbackParsed[key];
}
}
// Update cache
languageCache = parsed;
languageCacheTime = now;
return parsed;
return loadLanguage();
}
export function getLangName(): string {
// Performance optimization: Use cached language config if available
const now = Date.now();
if (languageConfigCache && now - languageConfigCacheTime < LANGUAGE_CACHE_TTL) {
return languageConfigCache;
}
if (language === undefined) {
try {
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
const langConfigFile = `${storagePath}lang.json`;
const rawData = fs.readFileSync(langConfigFile, "utf-8");
const parsed = JSON.parse(rawData) as i18nStrings;
language = parsed.lang;
languageConfigCache = language;
languageConfigCacheTime = now;
} catch (_e) {
console.log("Language config file doesn't exist. Fallback to English.");
language = "en-US";
languageConfigCache = language;
languageConfigCacheTime = now;
}
}
if (language.length === 2) {
language = `${language}-${language.toUpperCase()}`;
}
return language;
return resolveLangFromConfig();
}

View file

@ -5,8 +5,8 @@ import type { ThemeManifest } from "../@types/themeManifest.js";
import { mainWindows } from "../discord/window.js";
import { getConfig } from "./config.js";
// Performance optimization: Cache theme manifests to avoid reading on every calll
const themeManifestCache = new Map<string, { manifest: ThemeManifest; mtime: number }>();
const themeCssCache = new Map<string, string>();
let quickCssWatcher: fs.FSWatcher | null = null;
const userDataPath = app.getPath("userData");
@ -112,10 +112,7 @@ function getThemeManifest(themeId: string): ThemeManifest | null {
}
}
// Performance optimization: Cached theme list with directory watcher
const THEME_LIST_CACHE_TTL = 2000;
let themeListCache: ThemeManifest[] | null = null;
let themeListCacheTime = 0;
let themeWatcher: fs.FSWatcher | null = null;
let themeListRefreshTimeout: NodeJS.Timeout | null = null;
@ -143,7 +140,6 @@ function refreshThemeListCache(): ThemeManifest[] {
try {
if (!fs.existsSync(themesFolder)) {
themeListCache = [];
themeListCacheTime = Date.now();
return themeListCache;
}
const themes: ThemeManifest[] = [];
@ -158,7 +154,6 @@ function refreshThemeListCache(): ThemeManifest[] {
}
}
themeListCache = themes;
themeListCacheTime = Date.now();
return themes;
} catch (err) {
console.error("[Theme Manager] Failed to refresh theme list cache:", err);
@ -167,8 +162,7 @@ function refreshThemeListCache(): ThemeManifest[] {
}
export function getCachedThemeList(): ThemeManifest[] {
const now = Date.now();
if (themeListCache && now - themeListCacheTime < THEME_LIST_CACHE_TTL) {
if (themeListCache) {
return themeListCache;
}
importLooseThemeFiles();
@ -217,51 +211,52 @@ export function stopThemeWatcher(): void {
}
}
function buildThemeCssCache(): void {
themeCssCache.clear();
if (!fs.existsSync(themesFolder)) return;
const files = fs.readdirSync(themesFolder);
for (const file of files) {
const themePath = path.join(themesFolder, file);
if (fs.statSync(themePath).isFile() && (file.endsWith(".css") || file.endsWith(".theme.css"))) {
try {
const code = fs.readFileSync(themePath, "utf8");
installThemeFromCode(code);
fs.unlinkSync(themePath);
} catch {}
} else {
try {
const themeFile = getThemeManifest(file);
if (!themeFile) continue;
if (themeFile.enabled === undefined) {
const disabledPath = `${userDataPath}/disabled.txt`;
if (fs.existsSync(disabledPath) && fs.readFileSync(disabledPath).toString().includes(file)) {
themeFile.enabled = false;
} else {
themeFile.enabled = true;
}
}
if (themeFile.enabled) {
themeCssCache.set(file, fs.readFileSync(`${themePath}/${themeFile.theme}`, "utf-8"));
}
} catch (err) {
console.error(err);
}
}
}
}
export function injectThemesMain(browserWindow: BrowserWindow): void {
if (process.argv.includes("--safe-mode")) return;
if (!fs.existsSync(themesFolder)) {
fs.mkdirSync(themesFolder);
console.log("Created missing theme folder");
}
buildThemeCssCache();
browserWindow.webContents.on("did-finish-load", () => {
if (getConfig("quickCss")) initQuickCss(browserWindow); // load quick CSS if enabled
const files = fs.readdirSync(themesFolder);
for (const file of files) {
const themePath = path.join(themesFolder, file);
if (fs.statSync(themePath).isFile() && (file.endsWith(".css") || file.endsWith(".theme.css"))) {
console.log(`[Theme Manager] Local theme detected: ${themePath}`);
const code = fs.readFileSync(themePath, "utf8");
installThemeFromCode(code);
try {
fs.unlinkSync(themePath);
} catch {}
} else {
try {
const themeFile = getThemeManifest(file);
if (!themeFile) continue;
if (themeFile.enabled === undefined) {
const disabledPath = `${userDataPath}/disabled.txt`;
if (fs.existsSync(disabledPath) && fs.readFileSync(disabledPath).toString().includes(file)) {
themeFile.enabled = false;
} else {
themeFile.enabled = true;
}
}
if (themeFile.enabled === false) {
console.log(`%cSkipped ${themeFile.name} made by ${themeFile.author}`, "color:red");
} else {
browserWindow.webContents.send(
"addTheme",
file,
fs.readFileSync(`${themePath}/${themeFile.theme}`, "utf-8"),
);
console.log(`%cLoaded ${themeFile.name} made by ${themeFile.author}`, "color:red");
}
} catch (err) {
console.error(err);
}
}
if (getConfig("quickCss")) initQuickCss(browserWindow);
for (const [id, css] of themeCssCache) {
browserWindow.webContents.send("addTheme", id, css);
}
});
}
@ -275,6 +270,7 @@ export function uninstallTheme(id: string) {
fs.rmdirSync(path.join(themesFolder, `${id}-BD`), { recursive: true });
console.log(`Removed ${id} folder`);
}
themeCssCache.delete(id);
themeManifestCache.delete(id);
invalidateThemeListCache();
}
@ -288,13 +284,12 @@ export function setThemeEnabled(id: string, enabled: boolean) {
if (enabled !== manifest.enabled) {
mainWindows.every((passedWindow) => {
if (enabled) {
passedWindow.webContents.send(
"addTheme",
id,
fs.readFileSync(path.join(themesFolder, id, manifest.theme), "utf-8"),
);
const css = fs.readFileSync(path.join(themesFolder, id, manifest.theme), "utf-8");
themeCssCache.set(id, css);
passedWindow.webContents.send("addTheme", id, css);
console.log(`[Theme Manager] Loaded ${manifest.name} made by ${manifest.author}`);
} else {
themeCssCache.delete(id);
passedWindow.webContents.send("removeTheme", id);
console.log(`[Theme Manager] Removing ${manifest.name} made by ${manifest.author}`);
}
@ -322,6 +317,7 @@ function installThemeFromCode(code: string, linkOrPath?: string): void {
else manifest.supportsLegcordTitlebar = false;
fs.writeFileSync(path.join(themePath, "manifest.json"), JSON.stringify(manifest));
fs.writeFileSync(path.join(themePath, "src.css"), code);
themeCssCache.clear();
invalidateThemeListCache();
}

View file

@ -3,50 +3,32 @@ import path from "node:path";
import { app } from "electron";
import type { WindowState } from "../@types/windowState.js";
// Performance optimization: Cache window state to avoid reading file on every call
let windowStateCache: WindowState | null = null;
let windowStateCacheTime = 0;
const WINDOW_STATE_CACHE_TTL = 5000; // Cache for 5 seconds
export function getWindowStateLocation() {
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
return `${storagePath}window.json`;
}
export function setWindowState(object: WindowState): void {
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
const saveFile = `${storagePath}window.json`;
const toSave = JSON.stringify(object, null, 4);
fs.writeFileSync(saveFile, toSave, "utf-8");
// Performance optimization: Update cache immediately
windowStateCache = object;
windowStateCacheTime = Date.now();
}
// NOTE - Similar to getConfig, this seems to return a promise when it has no async. Originally Promise<WindowState[K]>
export function getWindowState<K extends keyof WindowState>(object: K): WindowState[K] {
// Performance optimization: Use cached window state if available
const now = Date.now();
if (windowStateCache && now - windowStateCacheTime < WINDOW_STATE_CACHE_TTL) {
return windowStateCache[object];
}
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
const settingsFile = `${storagePath}window.json`;
function ensureWindowStateCache(): WindowState {
if (windowStateCache) return windowStateCache;
const settingsFile = getWindowStateLocation();
if (!fs.existsSync(settingsFile)) {
fs.writeFileSync(settingsFile, "{}", "utf-8");
}
const rawData = fs.readFileSync(settingsFile, "utf-8");
const returnData = JSON.parse(rawData) as WindowState;
console.log(`[Window state manager] ${JSON.stringify(returnData)}`);
// Update cache
windowStateCache = returnData;
windowStateCacheTime = now;
return returnData[object];
windowStateCache = JSON.parse(rawData) as WindowState;
return windowStateCache;
}
export function setWindowState(object: WindowState): void {
const saveFile = getWindowStateLocation();
const toSave = JSON.stringify(object, null, 4);
fs.writeFileSync(saveFile, toSave, "utf-8");
windowStateCache = object;
}
export function getWindowState<K extends keyof WindowState>(object: K): WindowState[K] {
return ensureWindowStateCache()[object];
}

View file

@ -224,7 +224,9 @@ if (!app.requestSingleInstanceLock() && getConfig("multiInstance") === false) {
}
await Promise.all([fetchMods(), initializePluginSystem()]);
void import("./discord/extensions/plugin.js"); // load chrome extensions
console.log(`[Config Manager] Current config: ${readFileSync(getConfigLocation(), "utf-8")}`);
if (isDev) {
console.log(`[Config Manager] Current config: ${readFileSync(getConfigLocation(), "utf-8")}`);
}
// OLD CONFIGS MIGRATION
if (getConfig("hardwareAcceleration") === false) {