diff --git a/.cursorrules b/.cursorrules index 5015ab7e..23aa8113 100644 --- a/.cursorrules +++ b/.cursorrules @@ -28,6 +28,13 @@ - Use async/await instead of promises when possible - Prefer named exports over default exports for utilities and services +## TypeScript Array Syntax + +- **Always use `T[]` syntax instead of `Array`** for array types +- Prefer: `string[]`, `number[]`, `MyType[]` +- Avoid: `Array`, `Array`, `Array` +- This applies to all type annotations, type assertions, and generic type parameters + ## Comments - Keep comments concise and purposeful; avoid verbose explanations. diff --git a/README.md b/README.md index 1cdc0f72..c086cb2e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@
-[](https://help.hydralauncher.gg) +[](https://help.hydralauncher.gg)

Hydra Launcher

diff --git a/src/main/events/auth/index.ts b/src/main/events/auth/index.ts new file mode 100644 index 00000000..e94e9bc5 --- /dev/null +++ b/src/main/events/auth/index.ts @@ -0,0 +1,3 @@ +import "./get-session-hash"; +import "./open-auth-window"; +import "./sign-out"; diff --git a/src/main/events/autoupdater/index.ts b/src/main/events/autoupdater/index.ts new file mode 100644 index 00000000..f6b70367 --- /dev/null +++ b/src/main/events/autoupdater/index.ts @@ -0,0 +1,2 @@ +import "./check-for-updates"; +import "./restart-and-install-update"; diff --git a/src/main/events/catalogue/index.ts b/src/main/events/catalogue/index.ts new file mode 100644 index 00000000..383ba34c --- /dev/null +++ b/src/main/events/catalogue/index.ts @@ -0,0 +1,4 @@ +import "./get-game-assets"; +import "./get-game-shop-details"; +import "./get-game-stats"; +import "./get-random-game"; diff --git a/src/main/events/cloud-save/index.ts b/src/main/events/cloud-save/index.ts new file mode 100644 index 00000000..92e9f528 --- /dev/null +++ b/src/main/events/cloud-save/index.ts @@ -0,0 +1,4 @@ +import "./download-game-artifact"; +import "./get-game-backup-preview"; +import "./select-game-backup-path"; +import "./upload-save-game"; diff --git a/src/main/events/download-sources/index.ts b/src/main/events/download-sources/index.ts new file mode 100644 index 00000000..325d5570 --- /dev/null +++ b/src/main/events/download-sources/index.ts @@ -0,0 +1,6 @@ +import "./add-download-source"; +import "./get-download-sources-check-baseline"; +import "./get-download-sources-since-value"; +import "./get-download-sources"; +import "./remove-download-source"; +import "./sync-download-sources"; diff --git a/src/main/events/hardware/index.ts b/src/main/events/hardware/index.ts new file mode 100644 index 00000000..76823f51 --- /dev/null +++ b/src/main/events/hardware/index.ts @@ -0,0 +1,2 @@ +import "./check-folder-write-permission"; +import "./get-disk-free-space"; diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 2720d3ce..8efadf64 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -1,107 +1,22 @@ import { appVersion, defaultDownloadsPath, isStaging } from "@main/constants"; import { ipcMain } from "electron"; -import "./catalogue/get-game-shop-details"; -import "./catalogue/get-random-game"; -import "./catalogue/get-game-stats"; -import "./hardware/get-disk-free-space"; -import "./hardware/check-folder-write-permission"; -import "./library/add-game-to-library"; -import "./library/add-custom-game-to-library"; -import "./library/update-custom-game"; -import "./library/update-game-custom-assets"; -import "./library/add-game-to-favorites"; -import "./library/remove-game-from-favorites"; -import "./library/toggle-game-pin"; -import "./library/create-game-shortcut"; -import "./library/close-game"; -import "./library/delete-game-folder"; -import "./library/get-game-by-object-id"; -import "./library/get-library"; -import "./library/refresh-library-assets"; -import "./library/extract-game-download"; -import "./library/clear-new-download-options"; -import "./library/open-game"; -import "./library/open-game-executable-path"; -import "./library/open-game-installer"; -import "./library/open-game-installer-path"; -import "./library/update-executable-path"; -import "./library/update-launch-options"; -import "./library/verify-executable-path"; -import "./library/remove-game"; -import "./library/remove-game-from-library"; -import "./library/select-game-wine-prefix"; -import "./library/reset-game-achievements"; -import "./library/change-game-playtime"; -import "./library/toggle-automatic-cloud-sync"; -import "./library/get-default-wine-prefix-selection-path"; -import "./library/cleanup-unused-assets"; -import "./library/create-steam-shortcut"; -import "./library/copy-custom-game-asset"; -import "./misc/open-checkout"; -import "./misc/open-external"; -import "./misc/show-open-dialog"; -import "./misc/show-item-in-folder"; -import "./misc/install-common-redist"; -import "./misc/can-install-common-redist"; -import "./misc/save-temp-file"; -import "./misc/delete-temp-file"; -import "./misc/install-hydra-decky-plugin"; -import "./misc/get-hydra-decky-plugin-info"; -import "./misc/check-homebrew-folder-exists"; -import "./misc/hydra-api-call"; -import "./torrenting/cancel-game-download"; -import "./torrenting/pause-game-download"; -import "./torrenting/resume-game-download"; -import "./torrenting/start-game-download"; -import "./torrenting/pause-game-seed"; -import "./torrenting/resume-game-seed"; -import "./torrenting/check-debrid-availability"; -import "./user-preferences/get-user-preferences"; -import "./user-preferences/update-user-preferences"; -import "./user-preferences/auto-launch"; -import "./autoupdater/check-for-updates"; -import "./autoupdater/restart-and-install-update"; -import "./user-preferences/authenticate-real-debrid"; -import "./user-preferences/authenticate-torbox"; -import "./download-sources/add-download-source"; -import "./download-sources/sync-download-sources"; -import "./download-sources/get-download-sources-check-baseline"; -import "./download-sources/get-download-sources-since-value"; -import "./auth/sign-out"; -import "./auth/open-auth-window"; -import "./auth/get-session-hash"; -import "./user/get-auth"; -import "./user/get-unlocked-achievements"; -import "./user/get-compared-unlocked-achievements"; -import "./profile/get-me"; -import "./profile/update-profile"; -import "./profile/process-profile-image"; -import "./profile/sync-friend-requests"; -import "./cloud-save/download-game-artifact"; -import "./cloud-save/get-game-backup-preview"; -import "./cloud-save/upload-save-game"; -import "./cloud-save/select-game-backup-path"; -import "./notifications/publish-new-repacks-notification"; -import "./notifications/update-achievement-notification-window"; -import "./notifications/show-achievement-test-notification"; -import "./themes/add-custom-theme"; -import "./themes/delete-custom-theme"; -import "./themes/get-all-custom-themes"; -import "./themes/delete-all-custom-themes"; -import "./themes/update-custom-theme"; -import "./themes/open-editor-window"; -import "./themes/get-custom-theme-by-id"; -import "./themes/get-active-custom-theme"; -import "./themes/close-editor-window"; -import "./themes/toggle-custom-theme"; -import "./themes/copy-theme-achievement-sound"; -import "./themes/remove-theme-achievement-sound"; -import "./themes/get-theme-sound-path"; -import "./themes/get-theme-sound-data-url"; -import "./themes/import-theme-sound-from-store"; -import "./download-sources/remove-download-source"; -import "./download-sources/get-download-sources"; +import "./auth"; +import "./autoupdater"; +import "./catalogue"; +import "./cloud-save"; +import "./download-sources"; +import "./hardware"; +import "./library"; +import "./leveldb"; +import "./misc"; +import "./notifications"; +import "./profile"; +import "./themes"; +import "./torrenting"; +import "./user"; +import "./user-preferences"; + import { isPortableVersion } from "@main/helpers"; ipcMain.handle("ping", () => "pong"); diff --git a/src/main/events/leveldb/helpers.ts b/src/main/events/leveldb/helpers.ts new file mode 100644 index 00000000..e171e65a --- /dev/null +++ b/src/main/events/leveldb/helpers.ts @@ -0,0 +1,27 @@ +import { db } from "@main/level"; + +const sublevelCache = new Map< + string, + ReturnType> +>(); + +/** + * Gets a sublevel by name, creating it if it doesn't exist. + * All sublevels use "json" encoding by default. + * @param sublevelName - The name of the sublevel to get or create + * @returns The sublevel instance + */ +export const getSublevelByName = ( + sublevelName: string +): ReturnType> => { + if (sublevelCache.has(sublevelName)) { + return sublevelCache.get(sublevelName)!; + } + + // All sublevels use "json" encoding - this cannot be changed per sublevel + const sublevel = db.sublevel(sublevelName, { + valueEncoding: "json", + }); + sublevelCache.set(sublevelName, sublevel); + return sublevel; +}; diff --git a/src/main/events/leveldb/index.ts b/src/main/events/leveldb/index.ts new file mode 100644 index 00000000..6007bd33 --- /dev/null +++ b/src/main/events/leveldb/index.ts @@ -0,0 +1,6 @@ +import "./leveldb-get"; +import "./leveldb-put"; +import "./leveldb-del"; +import "./leveldb-clear"; +import "./leveldb-values"; +import "./leveldb-iterator"; diff --git a/src/main/events/leveldb/leveldb-clear.ts b/src/main/events/leveldb/leveldb-clear.ts new file mode 100644 index 00000000..cbed1db0 --- /dev/null +++ b/src/main/events/leveldb/leveldb-clear.ts @@ -0,0 +1,18 @@ +import { registerEvent } from "../register-event"; +import { getSublevelByName } from "./helpers"; +import { logger } from "@main/services"; + +const leveldbClear = async ( + _event: Electron.IpcMainInvokeEvent, + sublevelName: string +) => { + try { + const sublevel = getSublevelByName(sublevelName); + await sublevel.clear(); + } catch (error) { + logger.error("Error in leveldbClear", error); + throw error; + } +}; + +registerEvent("leveldbClear", leveldbClear); diff --git a/src/main/events/leveldb/leveldb-del.ts b/src/main/events/leveldb/leveldb-del.ts new file mode 100644 index 00000000..5bcded1d --- /dev/null +++ b/src/main/events/leveldb/leveldb-del.ts @@ -0,0 +1,28 @@ +import { registerEvent } from "../register-event"; +import { db } from "@main/level"; +import { getSublevelByName } from "./helpers"; +import { logger } from "@main/services"; + +const leveldbDel = async ( + _event: Electron.IpcMainInvokeEvent, + key: string, + sublevelName?: string | null +) => { + try { + if (sublevelName) { + const sublevel = getSublevelByName(sublevelName); + await sublevel.del(key); + } else { + await db.del(key); + } + } catch (error) { + if (error instanceof Error && error.name === "NotFoundError") { + // NotFoundError on delete is not an error, just return + return; + } + logger.error("Error in leveldbDel", error); + throw error; + } +}; + +registerEvent("leveldbDel", leveldbDel); diff --git a/src/main/events/leveldb/leveldb-get.ts b/src/main/events/leveldb/leveldb-get.ts new file mode 100644 index 00000000..059f1b30 --- /dev/null +++ b/src/main/events/leveldb/leveldb-get.ts @@ -0,0 +1,28 @@ +import { registerEvent } from "../register-event"; +import { db } from "@main/level"; +import { getSublevelByName } from "./helpers"; +import { logger } from "@main/services"; + +const leveldbGet = async ( + _event: Electron.IpcMainInvokeEvent, + key: string, + sublevelName?: string | null, + valueEncoding: "json" | "utf8" = "json" +) => { + try { + if (sublevelName) { + // Note: sublevels always use "json" encoding, valueEncoding parameter is ignored + const sublevel = getSublevelByName(sublevelName); + return sublevel.get(key); + } + return db.get(key, { valueEncoding }); + } catch (error) { + if (error instanceof Error && error.name === "NotFoundError") { + return null; + } + logger.error("Error in leveldbGet", error); + throw error; + } +}; + +registerEvent("leveldbGet", leveldbGet); diff --git a/src/main/events/leveldb/leveldb-iterator.ts b/src/main/events/leveldb/leveldb-iterator.ts new file mode 100644 index 00000000..a1960c31 --- /dev/null +++ b/src/main/events/leveldb/leveldb-iterator.ts @@ -0,0 +1,18 @@ +import { registerEvent } from "../register-event"; +import { getSublevelByName } from "./helpers"; +import { logger } from "@main/services"; + +const leveldbIterator = async ( + _event: Electron.IpcMainInvokeEvent, + sublevelName: string +) => { + try { + const sublevel = getSublevelByName(sublevelName); + return sublevel.iterator().all(); + } catch (error) { + logger.error("Error in leveldbIterator", error); + throw error; + } +}; + +registerEvent("leveldbIterator", leveldbIterator); diff --git a/src/main/events/leveldb/leveldb-put.ts b/src/main/events/leveldb/leveldb-put.ts new file mode 100644 index 00000000..9c416722 --- /dev/null +++ b/src/main/events/leveldb/leveldb-put.ts @@ -0,0 +1,27 @@ +import { registerEvent } from "../register-event"; +import { db } from "@main/level"; +import { getSublevelByName } from "./helpers"; +import { logger } from "@main/services"; + +const leveldbPut = async ( + _event: Electron.IpcMainInvokeEvent, + key: string, + value: unknown, + sublevelName?: string | null, + valueEncoding: "json" | "utf8" = "json" +) => { + try { + if (sublevelName) { + // Note: sublevels always use "json" encoding, valueEncoding parameter is ignored + const sublevel = getSublevelByName(sublevelName); + await sublevel.put(key, value); + } else { + await db.put(key, value, { valueEncoding }); + } + } catch (error) { + logger.error("Error in leveldbPut", error); + throw error; + } +}; + +registerEvent("leveldbPut", leveldbPut); diff --git a/src/main/events/leveldb/leveldb-values.ts b/src/main/events/leveldb/leveldb-values.ts new file mode 100644 index 00000000..0e2c3c0f --- /dev/null +++ b/src/main/events/leveldb/leveldb-values.ts @@ -0,0 +1,18 @@ +import { registerEvent } from "../register-event"; +import { getSublevelByName } from "./helpers"; +import { logger } from "@main/services"; + +const leveldbValues = async ( + _event: Electron.IpcMainInvokeEvent, + sublevelName: string +) => { + try { + const sublevel = getSublevelByName(sublevelName); + return sublevel.values().all(); + } catch (error) { + logger.error("Error in leveldbValues", error); + throw error; + } +}; + +registerEvent("leveldbValues", leveldbValues); diff --git a/src/main/events/library/index.ts b/src/main/events/library/index.ts new file mode 100644 index 00000000..d9d628d0 --- /dev/null +++ b/src/main/events/library/index.ts @@ -0,0 +1,32 @@ +import "./add-custom-game-to-library"; +import "./add-game-to-favorites"; +import "./add-game-to-library"; +import "./change-game-playtime"; +import "./cleanup-unused-assets"; +import "./clear-new-download-options"; +import "./close-game"; +import "./copy-custom-game-asset"; +import "./create-game-shortcut"; +import "./create-steam-shortcut"; +import "./delete-game-folder"; +import "./extract-game-download"; +import "./get-default-wine-prefix-selection-path"; +import "./get-game-by-object-id"; +import "./get-library"; +import "./open-game-executable-path"; +import "./open-game-installer-path"; +import "./open-game-installer"; +import "./open-game"; +import "./refresh-library-assets"; +import "./remove-game-from-favorites"; +import "./remove-game-from-library"; +import "./remove-game"; +import "./reset-game-achievements"; +import "./select-game-wine-prefix"; +import "./toggle-automatic-cloud-sync"; +import "./toggle-game-pin"; +import "./update-custom-game"; +import "./update-executable-path"; +import "./update-game-custom-assets"; +import "./update-launch-options"; +import "./verify-executable-path"; diff --git a/src/main/events/misc/index.ts b/src/main/events/misc/index.ts new file mode 100644 index 00000000..354e6687 --- /dev/null +++ b/src/main/events/misc/index.ts @@ -0,0 +1,12 @@ +import "./can-install-common-redist"; +import "./check-homebrew-folder-exists"; +import "./delete-temp-file"; +import "./get-hydra-decky-plugin-info"; +import "./hydra-api-call"; +import "./install-common-redist"; +import "./install-hydra-decky-plugin"; +import "./open-checkout"; +import "./open-external"; +import "./save-temp-file"; +import "./show-item-in-folder"; +import "./show-open-dialog"; diff --git a/src/main/events/notifications/index.ts b/src/main/events/notifications/index.ts new file mode 100644 index 00000000..c6e681e8 --- /dev/null +++ b/src/main/events/notifications/index.ts @@ -0,0 +1,3 @@ +import "./publish-new-repacks-notification"; +import "./show-achievement-test-notification"; +import "./update-achievement-notification-window"; diff --git a/src/main/events/profile/index.ts b/src/main/events/profile/index.ts new file mode 100644 index 00000000..1548249f --- /dev/null +++ b/src/main/events/profile/index.ts @@ -0,0 +1,4 @@ +import "./get-me"; +import "./process-profile-image"; +import "./sync-friend-requests"; +import "./update-profile"; diff --git a/src/main/events/themes/index.ts b/src/main/events/themes/index.ts new file mode 100644 index 00000000..5f4d4a02 --- /dev/null +++ b/src/main/events/themes/index.ts @@ -0,0 +1,15 @@ +import "./add-custom-theme"; +import "./close-editor-window"; +import "./copy-theme-achievement-sound"; +import "./delete-all-custom-themes"; +import "./delete-custom-theme"; +import "./get-active-custom-theme"; +import "./get-all-custom-themes"; +import "./get-custom-theme-by-id"; +import "./get-theme-sound-data-url"; +import "./get-theme-sound-path"; +import "./import-theme-sound-from-store"; +import "./open-editor-window"; +import "./remove-theme-achievement-sound"; +import "./toggle-custom-theme"; +import "./update-custom-theme"; diff --git a/src/main/events/torrenting/index.ts b/src/main/events/torrenting/index.ts new file mode 100644 index 00000000..408ecf17 --- /dev/null +++ b/src/main/events/torrenting/index.ts @@ -0,0 +1,7 @@ +import "./cancel-game-download"; +import "./check-debrid-availability"; +import "./pause-game-download"; +import "./pause-game-seed"; +import "./resume-game-download"; +import "./resume-game-seed"; +import "./start-game-download"; diff --git a/src/main/events/user-preferences/index.ts b/src/main/events/user-preferences/index.ts new file mode 100644 index 00000000..aab898e6 --- /dev/null +++ b/src/main/events/user-preferences/index.ts @@ -0,0 +1,5 @@ +import "./authenticate-real-debrid"; +import "./authenticate-torbox"; +import "./auto-launch"; +import "./get-user-preferences"; +import "./update-user-preferences"; diff --git a/src/main/events/user/index.ts b/src/main/events/user/index.ts new file mode 100644 index 00000000..cf63116f --- /dev/null +++ b/src/main/events/user/index.ts @@ -0,0 +1,3 @@ +import "./get-auth"; +import "./get-compared-unlocked-achievements"; +import "./get-unlocked-achievements"; diff --git a/src/main/level/sublevels/downloadSourcesCheckTimestamp.ts b/src/main/level/sublevels/downloadSourcesCheckTimestamp.ts index 4b60b962..36449b4d 100644 --- a/src/main/level/sublevels/downloadSourcesCheckTimestamp.ts +++ b/src/main/level/sublevels/downloadSourcesCheckTimestamp.ts @@ -7,7 +7,9 @@ export const getDownloadSourcesCheckBaseline = async (): Promise< string | null > => { try { - const timestamp = await db.get(levelKeys.downloadSourcesCheckBaseline); + const timestamp = await db.get(levelKeys.downloadSourcesCheckBaseline, { + valueEncoding: "utf8", + }); return timestamp; } catch (error) { if (error instanceof Error && error.name === "NotFoundError") { @@ -27,7 +29,9 @@ export const updateDownloadSourcesCheckBaseline = async ( timestamp: string ): Promise => { const utcTimestamp = new Date(timestamp).toISOString(); - await db.put(levelKeys.downloadSourcesCheckBaseline, utcTimestamp); + await db.put(levelKeys.downloadSourcesCheckBaseline, utcTimestamp, { + valueEncoding: "utf8", + }); }; // Gets the 'since' value the API used in the last check (for modal comparison) @@ -35,7 +39,9 @@ export const getDownloadSourcesSinceValue = async (): Promise< string | null > => { try { - const timestamp = await db.get(levelKeys.downloadSourcesSinceValue); + const timestamp = await db.get(levelKeys.downloadSourcesSinceValue, { + valueEncoding: "utf8", + }); return timestamp; } catch (error) { if (error instanceof Error && error.name === "NotFoundError") { @@ -55,5 +61,7 @@ export const updateDownloadSourcesSinceValue = async ( timestamp: string ): Promise => { const utcTimestamp = new Date(timestamp).toISOString(); - await db.put(levelKeys.downloadSourcesSinceValue, utcTimestamp); + await db.put(levelKeys.downloadSourcesSinceValue, utcTimestamp, { + valueEncoding: "utf8", + }); }; diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index a5a78e4a..7846571e 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -30,7 +30,7 @@ export class HydraApi { private static instance: AxiosInstance; private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5; // 5 minutes - private static readonly ADD_LOG_INTERCEPTOR = true; + private static readonly ADD_LOG_INTERCEPTOR = false; private static secondsToMilliseconds(seconds: number) { return seconds * 1000; diff --git a/src/main/services/system-path.ts b/src/main/services/system-path.ts index 32b34e11..0b42b0aa 100644 --- a/src/main/services/system-path.ts +++ b/src/main/services/system-path.ts @@ -13,9 +13,9 @@ export class SystemPath { }; static checkIfPathsAreAvailable() { - const paths = Object.keys(SystemPath.paths) as Array< - keyof typeof SystemPath.paths - >; + const paths = Object.keys( + SystemPath.paths + ) as (keyof typeof SystemPath.paths)[]; paths.forEach((pathName) => { try { diff --git a/src/preload/index.ts b/src/preload/index.ts index a2965532..f7c062cb 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -619,4 +619,28 @@ contextBridge.exposeInMainWorld("electron", { }, closeEditorWindow: (themeId?: string) => ipcRenderer.invoke("closeEditorWindow", themeId), + + /* LevelDB Generic CRUD */ + leveldb: { + get: ( + key: string, + sublevelName?: string | null, + valueEncoding?: "json" | "utf8" + ) => ipcRenderer.invoke("leveldbGet", key, sublevelName, valueEncoding), + put: ( + key: string, + value: unknown, + sublevelName?: string | null, + valueEncoding?: "json" | "utf8" + ) => + ipcRenderer.invoke("leveldbPut", key, value, sublevelName, valueEncoding), + del: (key: string, sublevelName?: string | null) => + ipcRenderer.invoke("leveldbDel", key, sublevelName), + clear: (sublevelName: string) => + ipcRenderer.invoke("leveldbClear", sublevelName), + values: (sublevelName: string) => + ipcRenderer.invoke("leveldbValues", sublevelName), + iterator: (sublevelName: string) => + ipcRenderer.invoke("leveldbIterator", sublevelName), + }, }); diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index 391e9c03..9badd12e 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -31,6 +31,8 @@ import { getAchievementSoundUrl, getAchievementSoundVolume, } from "./helpers"; +import { levelDBService } from "./services/leveldb.service"; +import type { UserPreferences } from "@types"; import "./app.scss"; export interface AppProps { @@ -77,11 +79,12 @@ export function App() { const { showSuccessToast } = useToast(); useEffect(() => { - Promise.all([window.electron.getUserPreferences(), updateLibrary()]).then( - ([preferences]) => { - dispatch(setUserPreferences(preferences)); - } - ); + Promise.all([ + levelDBService.get("userPreferences", null, "json"), + updateLibrary(), + ]).then(([preferences]) => { + dispatch(setUserPreferences(preferences as UserPreferences | null)); + }); }, [navigate, location.pathname, dispatch, updateLibrary]); useEffect(() => { @@ -204,7 +207,11 @@ export function App() { }, [dispatch, draggingDisabled]); const loadAndApplyTheme = useCallback(async () => { - const activeTheme = await window.electron.getActiveCustomTheme(); + const allThemes = (await levelDBService.values("themes")) as { + isActive?: boolean; + code?: string; + }[]; + const activeTheme = allThemes.find((theme) => theme.isActive); if (activeTheme?.code) { injectCustomCss(activeTheme.code); } else { diff --git a/src/renderer/src/components/search-dropdown/highlight-text.tsx b/src/renderer/src/components/search-dropdown/highlight-text.tsx index 9950c8a1..0d5f0fe4 100644 --- a/src/renderer/src/components/search-dropdown/highlight-text.tsx +++ b/src/renderer/src/components/search-dropdown/highlight-text.tsx @@ -1,11 +1,11 @@ import React from "react"; interface HighlightTextProps { - text: string; - query: string; + readonly text: string; + readonly query: string; } -export function HighlightText({ text, query }: HighlightTextProps) { +export function HighlightText({ text, query }: Readonly) { if (!query.trim()) { return <>{text}; } @@ -20,7 +20,7 @@ export function HighlightText({ text, query }: HighlightTextProps) { } const textWords = text.split(/\b/); - const matches: Array<{ start: number; end: number; text: string }> = []; + const matches: { start: number; end: number; text: string }[] = []; let currentIndex = 0; textWords.forEach((word) => { @@ -45,7 +45,7 @@ export function HighlightText({ text, query }: HighlightTextProps) { matches.sort((a, b) => a.start - b.start); - const mergedMatches: Array<{ start: number; end: number }> = []; + const mergedMatches: { start: number; end: number }[] = []; if (matches.length === 0) { return <>{text}; @@ -63,7 +63,7 @@ export function HighlightText({ text, query }: HighlightTextProps) { } mergedMatches.push(current); - const parts: Array<{ text: string; highlight: boolean }> = []; + const parts: { text: string; highlight: boolean; key: string }[] = []; let lastIndex = 0; mergedMatches.forEach((match) => { @@ -71,12 +71,14 @@ export function HighlightText({ text, query }: HighlightTextProps) { parts.push({ text: text.slice(lastIndex, match.start), highlight: false, + key: `${lastIndex}-${match.start}`, }); } parts.push({ text: text.slice(match.start, match.end), highlight: true, + key: `${match.start}-${match.end}`, }); lastIndex = match.end; @@ -86,18 +88,19 @@ export function HighlightText({ text, query }: HighlightTextProps) { parts.push({ text: text.slice(lastIndex), highlight: false, + key: `${lastIndex}-${text.length}`, }); } return ( <> - {parts.map((part, index) => + {parts.map((part) => part.highlight ? ( - + {part.text} ) : ( - {part.text} + {part.text} ) )} diff --git a/src/renderer/src/components/search-dropdown/search-dropdown.scss b/src/renderer/src/components/search-dropdown/search-dropdown.scss index 4b1983d1..54fb169a 100644 --- a/src/renderer/src/components/search-dropdown/search-dropdown.scss +++ b/src/renderer/src/components/search-dropdown/search-dropdown.scss @@ -44,7 +44,8 @@ transition: color ease 0.2s; &:hover { - color: #dadbe1; + color: #ffffff; + background-color: rgba(255, 255, 255, 0.15); } } @@ -76,6 +77,12 @@ display: flex; align-items: center; justify-content: center; + background-color: transparent; + + &:hover { + color: #ff3333; + background-color: rgba(255, 85, 85, 0.2); + } } &__item { @@ -134,8 +141,8 @@ } &__highlight { - background-color: rgba(255, 193, 7, 0.3); - color: #ffc107; + background-color: rgba(255, 193, 7, 0.4); + color: #ffa000; font-weight: 600; padding: 0 2px; border-radius: 2px; diff --git a/src/renderer/src/context/game-details/game-details.context.tsx b/src/renderer/src/context/game-details/game-details.context.tsx index bc1a6351..29feabf5 100644 --- a/src/renderer/src/context/game-details/game-details.context.tsx +++ b/src/renderer/src/context/game-details/game-details.context.tsx @@ -1,6 +1,8 @@ import { createContext, useCallback, useEffect, useRef, useState } from "react"; import { setHeaderTitle } from "@renderer/features"; +import { levelDBService } from "@renderer/services/leveldb.service"; +import { orderBy } from "lodash-es"; import { getSteamLanguage } from "@renderer/helpers"; import { useAppDispatch, @@ -10,6 +12,7 @@ import { } from "@renderer/hooks"; import type { + DownloadSource, GameRepack, GameShop, GameStats, @@ -297,7 +300,10 @@ export function GameDetailsContextProvider({ const fetchDownloadSources = async () => { try { - const sources = await window.electron.getDownloadSources(); + const sourcesRaw = (await levelDBService.values( + "downloadSources" + )) as DownloadSource[]; + const sources = orderBy(sourcesRaw, "createdAt", "desc"); const params = { take: 100, diff --git a/src/renderer/src/context/settings/settings.context.tsx b/src/renderer/src/context/settings/settings.context.tsx index 1160ca3e..338c4e45 100644 --- a/src/renderer/src/context/settings/settings.context.tsx +++ b/src/renderer/src/context/settings/settings.context.tsx @@ -2,6 +2,7 @@ import { createContext, useCallback, useEffect, useState } from "react"; import { setUserPreferences } from "@renderer/features"; import { useAppDispatch } from "@renderer/hooks"; +import { levelDBService } from "@renderer/services/leveldb.service"; import type { UserBlocks, UserPreferences } from "@types"; import { useSearchParams } from "react-router-dom"; @@ -134,9 +135,11 @@ export function SettingsContextProvider({ const updateUserPreferences = async (values: Partial) => { await window.electron.updateUserPreferences(values); - window.electron.getUserPreferences().then((userPreferences) => { - dispatch(setUserPreferences(userPreferences)); - }); + levelDBService + .get("userPreferences", null, "json") + .then((userPreferences) => { + dispatch(setUserPreferences(userPreferences as UserPreferences | null)); + }); }; return ( diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index e35ed57b..56205b2f 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -438,6 +438,25 @@ declare global { onNewDownloadOptions: ( cb: (gamesWithNewOptions: { gameId: string; count: number }[]) => void ) => () => Electron.IpcRenderer; + + /* LevelDB Generic CRUD */ + leveldb: { + get: ( + key: string, + sublevelName?: string | null, + valueEncoding?: "json" | "utf8" + ) => Promise; + put: ( + key: string, + value: unknown, + sublevelName?: string | null, + valueEncoding?: "json" | "utf8" + ) => Promise; + del: (key: string, sublevelName?: string | null) => Promise; + clear: (sublevelName: string) => Promise; + values: (sublevelName: string) => Promise; + iterator: (sublevelName: string) => Promise<[string, unknown][]>; + }; } interface Window { diff --git a/src/renderer/src/helpers.ts b/src/renderer/src/helpers.ts index e16aa7a4..0b057754 100644 --- a/src/renderer/src/helpers.ts +++ b/src/renderer/src/helpers.ts @@ -3,6 +3,7 @@ import type { GameShop } from "@types"; import Color from "color"; import { v4 as uuidv4 } from "uuid"; import { THEME_WEB_STORE_URL } from "./constants"; +import { levelDBService } from "./services/leveldb.service"; export const formatDownloadProgress = ( progress?: number, @@ -127,7 +128,12 @@ export const getAchievementSoundUrl = async (): Promise => { .default; try { - const activeTheme = await window.electron.getActiveCustomTheme(); + const allThemes = (await levelDBService.values("themes")) as { + id: string; + isActive?: boolean; + hasCustomSound?: boolean; + }[]; + const activeTheme = allThemes.find((theme) => theme.isActive); if (activeTheme?.hasCustomSound) { const soundDataUrl = await window.electron.getThemeSoundDataUrl( @@ -146,10 +152,18 @@ export const getAchievementSoundUrl = async (): Promise => { export const getAchievementSoundVolume = async (): Promise => { try { - const prefs = await window.electron.getUserPreferences(); + const prefs = (await levelDBService.get( + "userPreferences", + null, + "json" + )) as { achievementSoundVolume?: number } | null; return prefs?.achievementSoundVolume ?? 0.15; } catch (error) { console.error("Failed to get sound volume", error); return 0.15; } }; + +export const getGameKey = (shop: GameShop, objectId: string): string => { + return `${shop}:${objectId}`; +}; diff --git a/src/renderer/src/hooks/use-catalogue.ts b/src/renderer/src/hooks/use-catalogue.ts index 675f5013..ca2aaa4a 100644 --- a/src/renderer/src/hooks/use-catalogue.ts +++ b/src/renderer/src/hooks/use-catalogue.ts @@ -1,8 +1,9 @@ import axios from "axios"; import { useCallback, useEffect, useState } from "react"; +import { levelDBService } from "@renderer/services/leveldb.service"; +import type { DownloadSource } from "@types"; import { useAppDispatch } from "./redux"; import { setGenres, setTags } from "@renderer/features"; -import type { DownloadSource } from "@types"; export const externalResourcesInstance = axios.create({ baseURL: import.meta.env.RENDERER_VITE_EXTERNAL_RESOURCES_URL, @@ -40,8 +41,9 @@ export function useCatalogue() { }, []); const getDownloadSources = useCallback(() => { - window.electron.getDownloadSources().then((results) => { - setDownloadSources(results.filter((source) => !!source.fingerprint)); + levelDBService.values("downloadSources").then((results) => { + const sources = results as DownloadSource[]; + setDownloadSources(sources.filter((source) => !!source.fingerprint)); }); }, []); diff --git a/src/renderer/src/hooks/use-search-suggestions.ts b/src/renderer/src/hooks/use-search-suggestions.ts index b1f9ccac..d647cc5a 100644 --- a/src/renderer/src/hooks/use-search-suggestions.ts +++ b/src/renderer/src/hooks/use-search-suggestions.ts @@ -1,6 +1,7 @@ import { useState, useEffect, useCallback, useRef } from "react"; import { useAppSelector } from "./redux"; import { debounce } from "lodash-es"; +import { logger } from "@renderer/logger"; export interface SearchSuggestion { title: string; @@ -85,12 +86,12 @@ export function useSearchSuggestions( try { const response = await window.electron.hydraApi.get< - Array<{ + { title: string; objectId: string; shop: string; iconUrl: string | null; - }> + }[] >("/catalogue/search/suggestions", { params: { query: searchQuery, @@ -113,6 +114,7 @@ export function useSearchSuggestions( } catch (error) { if (!abortController.signal.aborted) { setSuggestions([]); + logger.error("Failed to fetch catalogue suggestions", error); } } finally { if (!abortController.signal.aborted) { diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index 84c7f815..92220a6e 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -21,6 +21,7 @@ import resources from "@locales"; import { logger } from "./logger"; import { addCookieInterceptor } from "./cookies"; +import { levelDBService } from "./services/leveldb.service"; import Catalogue from "./pages/catalogue/catalogue"; import Home from "./pages/home/home"; import Downloads from "./pages/downloads/downloads"; @@ -48,7 +49,11 @@ i18n }, }) .then(async () => { - const userPreferences = await window.electron.getUserPreferences(); + const userPreferences = (await levelDBService.get( + "userPreferences", + null, + "json" + )) as { language?: string } | null; if (userPreferences?.language) { i18n.changeLanguage(userPreferences.language); diff --git a/src/renderer/src/pages/achievements/notification/achievement-notification.tsx b/src/renderer/src/pages/achievements/notification/achievement-notification.tsx index 38b2443b..a362c545 100644 --- a/src/renderer/src/pages/achievements/notification/achievement-notification.tsx +++ b/src/renderer/src/pages/achievements/notification/achievement-notification.tsx @@ -11,6 +11,7 @@ import { getAchievementSoundVolume, } from "@renderer/helpers"; import { AchievementNotificationItem } from "@renderer/components/achievements/notification/achievement-notification"; +import { levelDBService } from "@renderer/services/leveldb.service"; import app from "../../../app.scss?inline"; import styles from "../../../components/achievements/notification/achievement-notification.scss?inline"; import root from "react-shadow"; @@ -144,7 +145,11 @@ export function AchievementNotification() { const loadAndApplyTheme = useCallback(async () => { if (!shadowRootRef) return; - const activeTheme = await window.electron.getActiveCustomTheme(); + const allThemes = (await levelDBService.values("themes")) as { + isActive?: boolean; + code?: string; + }[]; + const activeTheme = allThemes.find((theme) => theme.isActive); if (activeTheme?.code) { injectCustomCss(activeTheme.code, shadowRootRef); } else { diff --git a/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.tsx b/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.tsx index c9658636..56912fcc 100644 --- a/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.tsx +++ b/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.tsx @@ -100,20 +100,48 @@ export function GallerySlider() { src?: string; poster?: string; videoSrc?: string; + videoType?: string; alt: string; }> = []; if (shopDetails?.movies) { shopDetails.movies.forEach((video, index) => { - items.push({ - id: String(video.id), - type: "video", - poster: video.thumbnail, - videoSrc: video.mp4.max.startsWith("http://") - ? video.mp4.max.replace("http://", "https://") - : video.mp4.max, - alt: t("video", { number: String(index + 1) }), - }); + // Prefer new formats: HLS (best browser support), then DASH H264, then DASH AV1 + // Fallback to old format: mp4/webm if new formats are not available + let videoSrc: string | undefined; + let videoType: string | undefined; + + if (video.hls_h264) { + videoSrc = video.hls_h264; + videoType = "application/x-mpegURL"; + } else if (video.dash_h264) { + videoSrc = video.dash_h264; + videoType = "application/dash+xml"; + } else if (video.dash_av1) { + videoSrc = video.dash_av1; + videoType = "application/dash+xml"; + } else if (video.mp4?.max) { + // Fallback to old format + videoSrc = video.mp4.max; + videoType = "video/mp4"; + } else if (video.webm?.max) { + // Fallback to webm if mp4 is not available + videoSrc = video.webm.max; + videoType = "video/webm"; + } + + if (videoSrc) { + items.push({ + id: String(video.id), + type: "video", + poster: video.thumbnail, + videoSrc: videoSrc.startsWith("http://") + ? videoSrc.replace("http://", "https://") + : videoSrc, + videoType, + alt: video.name || t("video", { number: String(index + 1) }), + }); + } }); } @@ -172,7 +200,9 @@ export function GallerySlider() { autoPlay={autoplayEnabled} tabIndex={-1} > - + {item.videoSrc && ( + + )} ) : ( { - await window.electron.updateLaunchOptions( - game.shop, - game.objectId, - value - ); + const gameKey = getGameKey(game.shop, game.objectId); + const gameData = (await levelDBService.get( + gameKey, + "games" + )) as Game | null; + if (gameData) { + const trimmedValue = value.trim(); + const updated = { + ...gameData, + launchOptions: trimmedValue ? trimmedValue : null, + }; + await levelDBService.put(gameKey, updated, "games"); + } updateGame(); }, 1000) ).current; @@ -213,9 +223,16 @@ export function GameOptionsModal({ const handleClearLaunchOptions = async () => { setLaunchOptions(""); - window.electron - .updateLaunchOptions(game.shop, game.objectId, null) - .then(updateGame); + const gameKey = getGameKey(game.shop, game.objectId); + const gameData = (await levelDBService.get( + gameKey, + "games" + )) as Game | null; + if (gameData) { + const updated = { ...gameData, launchOptions: null }; + await levelDBService.put(gameKey, updated, "games"); + } + updateGame(); }; const shouldShowWinePrefixConfiguration = @@ -256,11 +273,15 @@ export function GameOptionsModal({ ) => { setAutomaticCloudSync(event.target.checked); - await window.electron.toggleAutomaticCloudSync( - game.shop, - game.objectId, - event.target.checked - ); + const gameKey = getGameKey(game.shop, game.objectId); + const gameData = (await levelDBService.get( + gameKey, + "games" + )) as Game | null; + if (gameData) { + const updated = { ...gameData, automaticCloudSync: event.target.checked }; + await levelDBService.put(gameKey, updated, "games"); + } updateGame(); }; diff --git a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx index 91013da0..51744fc5 100644 --- a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx @@ -15,7 +15,7 @@ import { TextField, CheckboxField, } from "@renderer/components"; -import type { DownloadSource, GameRepack } from "@types"; +import type { DownloadSource, Game, GameRepack } from "@types"; import { DownloadSettingsModal } from "./download-settings-modal"; import { gameDetailsContext } from "@renderer/context"; @@ -23,6 +23,8 @@ import { Downloader } from "@shared"; import { orderBy } from "lodash-es"; import { useDate, useFeature, useAppDispatch } from "@renderer/hooks"; import { clearNewDownloadOptions } from "@renderer/features"; +import { levelDBService } from "@renderer/services/leveldb.service"; +import { getGameKey } from "@renderer/helpers"; import "./repacks-modal.scss"; export interface RepacksModalProps { @@ -98,8 +100,11 @@ export function RepacksModal({ useEffect(() => { const fetchDownloadSources = async () => { - const sources = await window.electron.getDownloadSources(); - setDownloadSources(sources); + const sources = (await levelDBService.values( + "downloadSources" + )) as DownloadSource[]; + const sorted = orderBy(sources, "createdAt", "desc"); + setDownloadSources(sorted); }; fetchDownloadSources(); @@ -109,10 +114,19 @@ export function RepacksModal({ const fetchLastCheckTimestamp = async () => { setIsLoadingTimestamp(true); - const timestamp = await window.electron.getDownloadSourcesSinceValue(); + try { + const timestamp = (await levelDBService.get( + "downloadSourcesSinceValue", + null, + "utf8" + )) as string | null; - setLastCheckTimestamp(timestamp); - setIsLoadingTimestamp(false); + setLastCheckTimestamp(timestamp); + } catch { + setLastCheckTimestamp(null); + } finally { + setIsLoadingTimestamp(false); + } }; if (visible) { @@ -126,7 +140,20 @@ export function RepacksModal({ game?.newDownloadOptionsCount && game.newDownloadOptionsCount > 0 ) { - globalThis.electron.clearNewDownloadOptions(game.shop, game.objectId); + const gameKey = getGameKey(game.shop, game.objectId); + levelDBService + .get(gameKey, "games") + .then((gameData) => { + if (gameData) { + const updated = { + ...(gameData as Game), + newDownloadOptionsCount: undefined, + }; + return levelDBService.put(gameKey, updated, "games"); + } + return Promise.resolve(); + }) + .catch(() => {}); const gameId = `${game.shop}:${game.objectId}`; dispatch(clearNewDownloadOptions({ gameId })); diff --git a/src/renderer/src/pages/home/home.tsx b/src/renderer/src/pages/home/home.tsx index b8f632a6..91c9b2ff 100644 --- a/src/renderer/src/pages/home/home.tsx +++ b/src/renderer/src/pages/home/home.tsx @@ -1,11 +1,13 @@ import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; +import { levelDBService } from "@renderer/services/leveldb.service"; +import { orderBy } from "lodash-es"; import { useNavigate } from "react-router-dom"; import Skeleton, { SkeletonTheme } from "react-loading-skeleton"; import { Button, GameCard, Hero } from "@renderer/components"; -import type { ShopAssets, Steam250Game } from "@types"; +import type { DownloadSource, ShopAssets, Steam250Game } from "@types"; import flameIconStatic from "@renderer/assets/icons/flame-static.png"; import flameIconAnimated from "@renderer/assets/icons/flame-animated.gif"; @@ -40,7 +42,10 @@ export default function Home() { setCurrentCatalogueCategory(category); setIsLoading(true); - const downloadSources = await window.electron.getDownloadSources(); + const sources = (await levelDBService.values( + "downloadSources" + )) as DownloadSource[]; + const downloadSources = orderBy(sources, "createdAt", "desc"); const params = { take: 12, diff --git a/src/renderer/src/pages/settings/aparence/components/theme-card.tsx b/src/renderer/src/pages/settings/aparence/components/theme-card.tsx index 68eed4c3..eb0ca287 100644 --- a/src/renderer/src/pages/settings/aparence/components/theme-card.tsx +++ b/src/renderer/src/pages/settings/aparence/components/theme-card.tsx @@ -8,6 +8,7 @@ import { useState } from "react"; import { DeleteThemeModal } from "../modals/delete-theme-modal"; import { injectCustomCss, removeCustomCss } from "@renderer/helpers"; import { THEME_WEB_STORE_URL } from "@renderer/constants"; +import { levelDBService } from "@renderer/services/leveldb.service"; interface ThemeCardProps { theme: Theme; @@ -22,11 +23,18 @@ export const ThemeCard = ({ theme, onListUpdated }: ThemeCardProps) => { const handleSetTheme = async () => { try { - const currentTheme = await window.electron.getCustomThemeById(theme.id); + const currentTheme = (await levelDBService.get( + theme.id, + "themes" + )) as Theme | null; if (!currentTheme) return; - const activeTheme = await window.electron.getActiveCustomTheme(); + const allThemes = (await levelDBService.values("themes")) as { + id: string; + isActive?: boolean; + }[]; + const activeTheme = allThemes.find((t) => t.isActive); if (activeTheme) { removeCustomCss(); diff --git a/src/renderer/src/pages/settings/aparence/modals/add-theme-modal.tsx b/src/renderer/src/pages/settings/aparence/modals/add-theme-modal.tsx index 522d8546..c8a2c80d 100644 --- a/src/renderer/src/pages/settings/aparence/modals/add-theme-modal.tsx +++ b/src/renderer/src/pages/settings/aparence/modals/add-theme-modal.tsx @@ -10,6 +10,7 @@ import * as yup from "yup"; import { yupResolver } from "@hookform/resolvers/yup"; import { useCallback } from "react"; import { generateUUID } from "@renderer/helpers"; +import { levelDBService } from "@renderer/services/leveldb.service"; import "./modals.scss"; @@ -90,7 +91,7 @@ export function AddThemeModal({ updatedAt: new Date(), }; - await window.electron.addCustomTheme(theme); + await levelDBService.put(theme.id, theme, "themes"); onThemeAdded(); onClose(); reset(); diff --git a/src/renderer/src/pages/settings/aparence/modals/delete-all-themes-modal.tsx b/src/renderer/src/pages/settings/aparence/modals/delete-all-themes-modal.tsx index 9439d273..fa21bc2c 100644 --- a/src/renderer/src/pages/settings/aparence/modals/delete-all-themes-modal.tsx +++ b/src/renderer/src/pages/settings/aparence/modals/delete-all-themes-modal.tsx @@ -3,6 +3,7 @@ import { Modal } from "@renderer/components/modal/modal"; import { useTranslation } from "react-i18next"; import "./modals.scss"; import { removeCustomCss } from "@renderer/helpers"; +import { levelDBService } from "@renderer/services/leveldb.service"; interface DeleteAllThemesModalProps { visible: boolean; @@ -18,13 +19,16 @@ export const DeleteAllThemesModal = ({ const { t } = useTranslation("settings"); const handleDeleteAllThemes = async () => { - const activeTheme = await window.electron.getActiveCustomTheme(); + const allThemes = (await levelDBService.values("themes")) as { + isActive?: boolean; + }[]; + const activeTheme = allThemes.find((theme) => theme.isActive); if (activeTheme) { removeCustomCss(); } - await window.electron.deleteAllCustomThemes(); + await levelDBService.clear("themes"); await window.electron.closeEditorWindow(); onClose(); onThemesDeleted(); diff --git a/src/renderer/src/pages/settings/aparence/modals/delete-theme-modal.tsx b/src/renderer/src/pages/settings/aparence/modals/delete-theme-modal.tsx index c1a5a1e0..d2158f6f 100644 --- a/src/renderer/src/pages/settings/aparence/modals/delete-theme-modal.tsx +++ b/src/renderer/src/pages/settings/aparence/modals/delete-theme-modal.tsx @@ -3,6 +3,7 @@ import { Modal } from "@renderer/components/modal/modal"; import { useTranslation } from "react-i18next"; import "./modals.scss"; import { removeCustomCss } from "@renderer/helpers"; +import { levelDBService } from "@renderer/services/leveldb.service"; interface DeleteThemeModalProps { visible: boolean; @@ -28,7 +29,7 @@ export const DeleteThemeModal = ({ removeCustomCss(); } - await window.electron.deleteCustomTheme(themeId); + await levelDBService.del(themeId, "themes"); await window.electron.closeEditorWindow(themeId); onThemeDeleted(); }; diff --git a/src/renderer/src/pages/settings/aparence/modals/import-theme-modal.tsx b/src/renderer/src/pages/settings/aparence/modals/import-theme-modal.tsx index 93baf1cd..e729ae29 100644 --- a/src/renderer/src/pages/settings/aparence/modals/import-theme-modal.tsx +++ b/src/renderer/src/pages/settings/aparence/modals/import-theme-modal.tsx @@ -11,6 +11,7 @@ import { import { useToast } from "@renderer/hooks"; import { THEME_WEB_STORE_URL } from "@renderer/constants"; import { logger } from "@renderer/logger"; +import { levelDBService } from "@renderer/services/leveldb.service"; interface ImportThemeModalProps { visible: boolean; @@ -45,9 +46,12 @@ export const ImportThemeModal = ({ }; try { - await window.electron.addCustomTheme(theme); + await levelDBService.put(theme.id, theme, "themes"); - const currentTheme = await window.electron.getCustomThemeById(theme.id); + const currentTheme = (await levelDBService.get( + theme.id, + "themes" + )) as Theme | null; if (!currentTheme) return; @@ -61,7 +65,11 @@ export const ImportThemeModal = ({ logger.error("Failed to import theme sound", soundError); } - const activeTheme = await window.electron.getActiveCustomTheme(); + const allThemes = (await levelDBService.values("themes")) as { + id: string; + isActive?: boolean; + }[]; + const activeTheme = allThemes.find((t) => t.isActive); if (activeTheme) { removeCustomCss(); diff --git a/src/renderer/src/pages/settings/aparence/settings-appearance.tsx b/src/renderer/src/pages/settings/aparence/settings-appearance.tsx index 413e52e9..24247396 100644 --- a/src/renderer/src/pages/settings/aparence/settings-appearance.tsx +++ b/src/renderer/src/pages/settings/aparence/settings-appearance.tsx @@ -5,6 +5,7 @@ import type { Theme } from "@types"; import { ImportThemeModal } from "./modals/import-theme-modal"; import { settingsContext } from "@renderer/context"; import { useNavigate } from "react-router-dom"; +import { levelDBService } from "@renderer/services/leveldb.service"; interface SettingsAppearanceProps { appearance: { @@ -31,7 +32,7 @@ export function SettingsAppearance({ const navigate = useNavigate(); const loadThemes = useCallback(async () => { - const themesList = await window.electron.getAllCustomThemes(); + const themesList = (await levelDBService.values("themes")) as Theme[]; setThemes(themesList); }, []); diff --git a/src/renderer/src/pages/settings/settings-download-sources.tsx b/src/renderer/src/pages/settings/settings-download-sources.tsx index 675919e3..f597838e 100644 --- a/src/renderer/src/pages/settings/settings-download-sources.tsx +++ b/src/renderer/src/pages/settings/settings-download-sources.tsx @@ -21,6 +21,8 @@ import { DownloadSourceStatus } from "@shared"; import { settingsContext } from "@renderer/context"; import { useNavigate } from "react-router-dom"; import { setFilters, clearFilters } from "@renderer/features"; +import { levelDBService } from "@renderer/services/leveldb.service"; +import { orderBy } from "lodash-es"; import "./settings-download-sources.scss"; import { logger } from "@renderer/logger"; @@ -52,8 +54,11 @@ export function SettingsDownloadSources() { useEffect(() => { const fetchDownloadSources = async () => { - const sources = await window.electron.getDownloadSources(); - setDownloadSources(sources); + const sources = (await levelDBService.values( + "downloadSources" + )) as DownloadSource[]; + const sorted = orderBy(sources, "createdAt", "desc"); + setDownloadSources(sorted); }; fetchDownloadSources(); @@ -73,8 +78,11 @@ export function SettingsDownloadSources() { const intervalId = setInterval(async () => { try { await window.electron.syncDownloadSources(); - const sources = await window.electron.getDownloadSources(); - setDownloadSources(sources); + const sources = (await levelDBService.values( + "downloadSources" + )) as DownloadSource[]; + const sorted = orderBy(sources, "createdAt", "desc"); + setDownloadSources(sorted); } catch (error) { logger.error("Failed to fetch download sources:", error); } @@ -88,8 +96,11 @@ export function SettingsDownloadSources() { try { await window.electron.removeDownloadSource(false, downloadSource.id); - const sources = await window.electron.getDownloadSources(); - setDownloadSources(sources); + const sources = (await levelDBService.values( + "downloadSources" + )) as DownloadSource[]; + const sorted = orderBy(sources, "createdAt", "desc"); + setDownloadSources(sorted); showSuccessToast(t("removed_download_source")); } catch (error) { logger.error("Failed to remove download source:", error); @@ -103,8 +114,11 @@ export function SettingsDownloadSources() { try { await window.electron.removeDownloadSource(true); - const sources = await window.electron.getDownloadSources(); - setDownloadSources(sources); + const sources = (await levelDBService.values( + "downloadSources" + )) as DownloadSource[]; + const sorted = orderBy(sources, "createdAt", "desc"); + setDownloadSources(sorted); showSuccessToast(t("removed_all_download_sources")); } catch (error) { logger.error("Failed to remove all download sources:", error); @@ -116,8 +130,11 @@ export function SettingsDownloadSources() { const handleAddDownloadSource = async () => { try { - const sources = await window.electron.getDownloadSources(); - setDownloadSources(sources); + const sources = (await levelDBService.values( + "downloadSources" + )) as DownloadSource[]; + const sorted = orderBy(sources, "createdAt", "desc"); + setDownloadSources(sorted); } catch (error) { logger.error("Failed to refresh download sources:", error); } @@ -127,8 +144,11 @@ export function SettingsDownloadSources() { setIsSyncingDownloadSources(true); try { await window.electron.syncDownloadSources(); - const sources = await window.electron.getDownloadSources(); - setDownloadSources(sources); + const sources = (await levelDBService.values( + "downloadSources" + )) as DownloadSource[]; + const sorted = orderBy(sources, "createdAt", "desc"); + setDownloadSources(sorted); showSuccessToast(t("download_sources_synced_successfully")); } finally { diff --git a/src/renderer/src/pages/theme-editor/theme-editor.tsx b/src/renderer/src/pages/theme-editor/theme-editor.tsx index 3f0be9cf..41dc7a7f 100644 --- a/src/renderer/src/pages/theme-editor/theme-editor.tsx +++ b/src/renderer/src/pages/theme-editor/theme-editor.tsx @@ -16,6 +16,7 @@ import { injectCustomCss, getAchievementSoundVolume } from "@renderer/helpers"; import { AchievementNotificationItem } from "@renderer/components/achievements/notification/achievement-notification"; import { generateAchievementCustomNotificationTest } from "@shared"; import { CollapsedMenu } from "@renderer/components/collapsed-menu/collapsed-menu"; +import { levelDBService } from "@renderer/services/leveldb.service"; import app from "../../app.scss?inline"; import styles from "../../components/achievements/notification/achievement-notification.scss?inline"; import root from "react-shadow"; @@ -64,15 +65,16 @@ export default function ThemeEditor() { useEffect(() => { if (themeId) { - window.electron.getCustomThemeById(themeId).then((loadedTheme) => { - if (loadedTheme) { - setTheme(loadedTheme); - setCode(loadedTheme.code); - if (loadedTheme.originalSoundPath) { - setSoundPath(loadedTheme.originalSoundPath); + levelDBService.get(themeId, "themes").then((loadedTheme) => { + const theme = loadedTheme as Theme | null; + if (theme) { + setTheme(theme); + setCode(theme.code); + if (theme.originalSoundPath) { + setSoundPath(theme.originalSoundPath); } if (shadowRootRef) { - injectCustomCss(loadedTheme.code, shadowRootRef); + injectCustomCss(theme.code, shadowRootRef); } } }); @@ -132,7 +134,10 @@ export default function ThemeEditor() { if (filePaths && filePaths.length > 0) { const originalPath = filePaths[0]; await window.electron.copyThemeAchievementSound(theme.id, originalPath); - const updatedTheme = await window.electron.getCustomThemeById(theme.id); + const updatedTheme = (await levelDBService.get( + theme.id, + "themes" + )) as Theme | null; if (updatedTheme) { setTheme(updatedTheme); if (updatedTheme.originalSoundPath) { @@ -146,7 +151,10 @@ export default function ThemeEditor() { if (!theme) return; await window.electron.removeThemeAchievementSound(theme.id); - const updatedTheme = await window.electron.getCustomThemeById(theme.id); + const updatedTheme = (await levelDBService.get( + theme.id, + "themes" + )) as Theme | null; if (updatedTheme) { setTheme(updatedTheme); } diff --git a/src/renderer/src/services/leveldb.service.ts b/src/renderer/src/services/leveldb.service.ts new file mode 100644 index 00000000..68e5e9f1 --- /dev/null +++ b/src/renderer/src/services/leveldb.service.ts @@ -0,0 +1,36 @@ +class LevelDBService { + get( + key: string, + sublevelName?: string | null, + valueEncoding?: "json" | "utf8" + ): Promise { + return window.electron.leveldb.get(key, sublevelName, valueEncoding); + } + + put( + key: string, + value: unknown, + sublevelName?: string | null, + valueEncoding?: "json" | "utf8" + ): Promise { + return window.electron.leveldb.put(key, value, sublevelName, valueEncoding); + } + + del(key: string, sublevelName?: string | null): Promise { + return window.electron.leveldb.del(key, sublevelName); + } + + clear(sublevelName: string): Promise { + return window.electron.leveldb.clear(sublevelName); + } + + values(sublevelName: string): Promise { + return window.electron.leveldb.values(sublevelName); + } + + iterator(sublevelName: string): Promise<[string, unknown][]> { + return window.electron.leveldb.iterator(sublevelName); + } +} + +export const levelDBService = new LevelDBService(); diff --git a/src/types/steam.types.ts b/src/types/steam.types.ts index 4dcf460a..12d012ea 100644 --- a/src/types/steam.types.ts +++ b/src/types/steam.types.ts @@ -16,8 +16,11 @@ export interface SteamVideoSource { export interface SteamMovies { id: number; - mp4: SteamVideoSource; - webm: SteamVideoSource; + dash_av1?: string; + dash_h264?: string; + hls_h264?: string; + mp4?: SteamVideoSource; + webm?: SteamVideoSource; thumbnail: string; name: string; highlight: boolean;