diff --git a/.github/workflows/flatpak-node.yml b/.github/workflows/flatpak-node.yml index bb472ea..5593947 100644 --- a/.github/workflows/flatpak-node.yml +++ b/.github/workflows/flatpak-node.yml @@ -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 diff --git a/README.md b/README.md index e0c052c..4c5083b 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/common/config.ts b/src/common/config.ts index e23da14..c54def6 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -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(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(["off", "minimized", "tray"]); @@ -148,33 +147,17 @@ function migrateStartMinimized(settingsObject: Record): boolean return false; } export function setConfig(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; + const settingsObject = ensureConfigCache() 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)[]; @@ -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); diff --git a/src/common/lang.ts b/src/common/lang.ts index d39c80f..5e372d9 100644 --- a/src/common/lang.ts +++ b/src/common/lang.ts @@ -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(); } diff --git a/src/common/themes.ts b/src/common/themes.ts index 8a79dce..8d9acdc 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,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(); } diff --git a/src/common/windowState.ts b/src/common/windowState.ts index 7350856..254ea85 100644 --- a/src/common/windowState.ts +++ b/src/common/windowState.ts @@ -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 - -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`; +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(object: K): WindowState[K] { + return ensureWindowStateCache()[object]; } diff --git a/src/main.ts b/src/main.ts index 7bdc368..00f8bee 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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) {