diff --git a/.github/workflows/flatpak-node.yml b/.github/workflows/flatpak-node.yml index 5593947..bb472ea 100644 --- a/.github/workflows/flatpak-node.yml +++ b/.github/workflows/flatpak-node.yml @@ -16,7 +16,8 @@ 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 --pnpm-store-version v11 + 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 + - name: Upload generated-sources.json to release run: | gh release upload ${{ github.event.release.tag_name }} generated-sources.json diff --git a/README.md b/README.md index 4c5083b..e0c052c 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,7 @@ winget install --id=smartfrigde.Legcord -e ### Flatpak -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. +Not available yet. ### Debian, Ubuntu and Raspbian diff --git a/src/common/config.ts b/src/common/config.ts index c54def6..e23da14 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -7,19 +7,10 @@ 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; - -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; -} - +let configCacheTime = 0; +const CONFIG_CACHE_TTL = 5000; // Cache for 5 seconds const defaults: Settings = { windowStyle: "overlay", channel: "stable", @@ -119,7 +110,17 @@ export function getConfig(object: K): Settings[K] { return safeMode[object]; } - return ensureConfigCache()[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]; } const START_MINIMIZED_MODES = new Set(["off", "minimized", "tray"]); @@ -147,17 +148,33 @@ function migrateStartMinimized(settingsObject: Record): boolean return false; } export function setConfig(object: K, toSet: Settings[K]): void { - const parsed = ensureConfigCache(); + const rawData = readFileSync(getConfigLocation(), "utf-8"); + const parsed = JSON.parse(rawData) as Settings; 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 { - const existingData = configCache ?? ({} as Settings); + 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 mergedData = { ...existingData, ...object }; - configCache = mergedData as Settings; + // Write the merged data back to the file 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"); @@ -191,12 +208,18 @@ export function checkIfConfigExists(): void { } export function checkIfConfigIsBroken(): void { try { - const settingsObject = ensureConfigCache() as Settings & Record; + const settingsData = readFileSync(getConfigLocation(), "utf-8"); + const settingsObject = JSON.parse(settingsData) as Settings & Record; + // 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)[]; @@ -222,6 +245,13 @@ 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); diff --git a/src/common/lang.ts b/src/common/lang.ts index 5e372d9..d39c80f 100644 --- a/src/common/lang.ts +++ b/src/common/lang.ts @@ -3,77 +3,158 @@ 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 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; -} +let languageCacheTime = 0; +let languageConfigCache: string | null = null; +let languageConfigCacheTime = 0; +const LANGUAGE_CACHE_TTL = 5000; // Cache for 5 seconds export function setLang(language: string): void { const langConfigFile = `${path.join(app.getPath("userData"), "/storage/")}lang.json`; - const dir = path.dirname(langConfigFile); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); + if (!fs.existsSync(langConfigFile)) { + fs.writeFileSync(langConfigFile, "{}", "utf-8"); } - const toSave = JSON.stringify({ lang: language }, null, 4); - fs.writeFileSync(langConfigFile, toSave, "utf-8"); + 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}`); - currentLanguage = null; - languageCache = null; -} + fs.writeFileSync(langConfigFile, toSave, "utf-8"); + // Performance optimization: Invalidate cache when language changes + languageConfigCache = language; + languageConfigCacheTime = Date.now(); + languageCache = null; // Invalidate language file cache +} +let language: string; export function getLang(object: string): string { - const data = loadLanguage(); - return data[object] ?? ""; -} + // 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]; +} export function getRawLang(): i18nStrings { - return loadLanguage(); -} + // Performance optimization: Use cached result if available + const now = Date.now(); + if (languageCache && now - languageCacheTime < LANGUAGE_CACHE_TTL) { + return languageCache; + } -export function getLangName(): string { - return resolveLangFromConfig(); + 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; +} +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; } diff --git a/src/common/themes.ts b/src/common/themes.ts index 8d9acdc..8a79dce 100644 --- a/src/common/themes.ts +++ b/src/common/themes.ts @@ -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(); -const themeCssCache = new Map(); let quickCssWatcher: fs.FSWatcher | null = null; const userDataPath = app.getPath("userData"); @@ -112,7 +112,10 @@ 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; @@ -140,6 +143,7 @@ function refreshThemeListCache(): ThemeManifest[] { try { if (!fs.existsSync(themesFolder)) { themeListCache = []; + themeListCacheTime = Date.now(); return themeListCache; } const themes: ThemeManifest[] = []; @@ -154,6 +158,7 @@ function refreshThemeListCache(): ThemeManifest[] { } } themeListCache = themes; + themeListCacheTime = Date.now(); return themes; } catch (err) { console.error("[Theme Manager] Failed to refresh theme list cache:", err); @@ -162,7 +167,8 @@ function refreshThemeListCache(): ThemeManifest[] { } export function getCachedThemeList(): ThemeManifest[] { - if (themeListCache) { + const now = Date.now(); + if (themeListCache && now - themeListCacheTime < THEME_LIST_CACHE_TTL) { return themeListCache; } importLooseThemeFiles(); @@ -211,52 +217,51 @@ 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); - for (const [id, css] of themeCssCache) { - browserWindow.webContents.send("addTheme", id, css); + 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); + } + } } }); } @@ -270,7 +275,6 @@ 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(); } @@ -284,12 +288,13 @@ export function setThemeEnabled(id: string, enabled: boolean) { if (enabled !== manifest.enabled) { mainWindows.every((passedWindow) => { if (enabled) { - const css = fs.readFileSync(path.join(themesFolder, id, manifest.theme), "utf-8"); - themeCssCache.set(id, css); - passedWindow.webContents.send("addTheme", id, css); + passedWindow.webContents.send( + "addTheme", + id, + fs.readFileSync(path.join(themesFolder, id, manifest.theme), "utf-8"), + ); 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}`); } @@ -317,7 +322,6 @@ 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(); } diff --git a/src/common/windowState.ts b/src/common/windowState.ts index 254ea85..7350856 100644 --- a/src/common/windowState.ts +++ b/src/common/windowState.ts @@ -3,32 +3,50 @@ 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"); -function ensureWindowStateCache(): WindowState { - if (windowStateCache) return windowStateCache; - const settingsFile = getWindowStateLocation(); + // 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 + +export function getWindowState(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`; if (!fs.existsSync(settingsFile)) { fs.writeFileSync(settingsFile, "{}", "utf-8"); } const rawData = fs.readFileSync(settingsFile, "utf-8"); - windowStateCache = JSON.parse(rawData) as WindowState; - return windowStateCache; -} + const returnData = JSON.parse(rawData) as WindowState; + console.log(`[Window state manager] ${JSON.stringify(returnData)}`); -export function setWindowState(object: WindowState): void { - const saveFile = getWindowStateLocation(); - const toSave = JSON.stringify(object, null, 4); - fs.writeFileSync(saveFile, toSave, "utf-8"); - windowStateCache = object; -} + // Update cache + windowStateCache = returnData; + windowStateCacheTime = now; -export function getWindowState(object: K): WindowState[K] { - return ensureWindowStateCache()[object]; + return returnData[object]; } diff --git a/src/main.ts b/src/main.ts index 00f8bee..7bdc368 100644 --- a/src/main.ts +++ b/src/main.ts @@ -224,9 +224,7 @@ if (!app.requestSingleInstanceLock() && getConfig("multiInstance") === false) { } await Promise.all([fetchMods(), initializePluginSystem()]); void import("./discord/extensions/plugin.js"); // load chrome extensions - if (isDev) { - console.log(`[Config Manager] Current config: ${readFileSync(getConfigLocation(), "utf-8")}`); - } + console.log(`[Config Manager] Current config: ${readFileSync(getConfigLocation(), "utf-8")}`); // OLD CONFIGS MIGRATION if (getConfig("hardwareAcceleration") === false) {