Compare commits

..

4 Commits

Author SHA1 Message Date
Chubby Granny Chaser
379c211472 feat: merge with v3.7.4
Some checks failed
Build Renderer / build (push) Has been cancelled
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-2022) (push) Has been cancelled
2025-11-29 01:33:23 +00:00
Chubby Granny Chaser
e2f30a28e4 fix: fixing labels 2025-11-02 04:14:39 +00:00
Chubby Granny Chaser
1d6acab5da Merge branch 'main' of https://github.com/hydralauncher/hydra into release/v3.7.2 2025-11-02 04:03:18 +00:00
Chubby Granny Chaser
9046bec7da fix: fixing review item 2025-11-02 04:03:06 +00:00
83 changed files with 619 additions and 2289 deletions

View File

@@ -28,26 +28,6 @@
- Use async/await instead of promises when possible - Use async/await instead of promises when possible
- Prefer named exports over default exports for utilities and services - Prefer named exports over default exports for utilities and services
## ESLint Issues
- **Always try to fix ESLint errors properly before disabling rules**
- When encountering ESLint errors, explore these solutions in order:
1. **Fix the code to comply with the rule** (e.g., add missing required elements, fix accessibility issues)
2. **Use minimal markup to satisfy the rule** (e.g., add empty `<track>` elements for videos without captions, add `role` attributes)
3. **Only disable the rule as a last resort** when no reasonable solution exists
- When disabling a rule, always include a comment explaining why it's necessary
- Examples of proper fixes:
- For `jsx-a11y/media-has-caption`: Add `<track kind="captions" />` even if no captions are available
- For `jsx-a11y/alt-text`: Add meaningful alt text or `alt=""` for decorative images
- For accessibility rules: Add appropriate ARIA attributes rather than disabling
## TypeScript Array Syntax
- **Always use `T[]` syntax instead of `Array<T>`** for array types
- Prefer: `string[]`, `number[]`, `MyType[]`
- Avoid: `Array<string>`, `Array<number>`, `Array<MyType>`
- This applies to all type annotations, type assertions, and generic type parameters
## Comments ## Comments
- Keep comments concise and purposeful; avoid verbose explanations. - Keep comments concise and purposeful; avoid verbose explanations.

View File

@@ -1,6 +1,6 @@
<div align="center"> <div align="center">
[<img src="https://raw.githubusercontent.com/hydralauncher/hydra/refs/heads/main/resources/icon.png" width="144"/>](https://help.hydralauncher.gg) [<img src="./resources/icon.png" width="144"/>](https://help.hydralauncher.gg)
<h1 align="center">Hydra Launcher</h1> <h1 align="center">Hydra Launcher</h1>

View File

@@ -1,6 +1,6 @@
{ {
"name": "hydralauncher", "name": "hydralauncher",
"version": "3.7.6", "version": "3.7.4",
"description": "Hydra", "description": "Hydra",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"author": "Los Broxas", "author": "Los Broxas",
@@ -63,7 +63,6 @@
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"file-type": "^20.5.0", "file-type": "^20.5.0",
"framer-motion": "^12.15.0", "framer-motion": "^12.15.0",
"hls.js": "^1.5.12",
"i18next": "^23.11.2", "i18next": "^23.11.2",
"i18next-browser-languagedetector": "^7.2.1", "i18next-browser-languagedetector": "^7.2.1",
"jsdom": "^24.0.0", "jsdom": "^24.0.0",
@@ -85,7 +84,7 @@
"sound-play": "^1.1.0", "sound-play": "^1.1.0",
"steam-shortcut-editor": "https://github.com/hydralauncher/steam-shortcut-editor", "steam-shortcut-editor": "https://github.com/hydralauncher/steam-shortcut-editor",
"sudo-prompt": "^9.2.1", "sudo-prompt": "^9.2.1",
"tar": "^7.5.2", "tar": "^7.4.3",
"tough-cookie": "^5.1.1", "tough-cookie": "^5.1.1",
"user-agents": "^1.1.387", "user-agents": "^1.1.387",
"uuid": "^13.0.0", "uuid": "^13.0.0",

View File

@@ -96,7 +96,7 @@
"search_library": "Search library", "search_library": "Search library",
"recent_searches": "Recent Searches", "recent_searches": "Recent Searches",
"suggestions": "Suggestions", "suggestions": "Suggestions",
"clear_history": "Clear", "clear_history": "Clear history",
"remove_from_history": "Remove from history", "remove_from_history": "Remove from history",
"loading": "Loading...", "loading": "Loading...",
"no_results": "No results", "no_results": "No results",

View File

@@ -93,16 +93,8 @@
}, },
"header": { "header": {
"search": "Buscar juegos", "search": "Buscar juegos",
"search_library": "Buscar en la librería",
"recent_searches": "Búsquedas Recientes",
"suggestions": "Sugerencias",
"clear_history": "Limpiar",
"remove_from_history": "Eliminar del historial",
"loading": "Cargando...",
"no_results": "Sin resultados",
"home": "Inicio", "home": "Inicio",
"catalogue": "Catálogo", "catalogue": "Catálogo",
"library": "Librería",
"downloads": "Descargas", "downloads": "Descargas",
"search_results": "Resultados de búsqueda", "search_results": "Resultados de búsqueda",
"settings": "Ajustes", "settings": "Ajustes",
@@ -370,6 +362,7 @@
"write_review_placeholder": "Compartí tus pensamientos sobre este juego...", "write_review_placeholder": "Compartí tus pensamientos sobre este juego...",
"yes": "Si", "yes": "Si",
"you_seemed_to_enjoy_this_game": "Parece que has disfrutado de este juego", "you_seemed_to_enjoy_this_game": "Parece que has disfrutado de este juego",
"review_played_for": "Jugado por",
"language": "Idioma", "language": "Idioma",
"caption": "Subtítulo", "caption": "Subtítulo",
"audio": "Audio", "audio": "Audio",
@@ -458,7 +451,6 @@
"description_confirmation_delete_all_sources": "Vas a eliminar todas las fuentes de descargas", "description_confirmation_delete_all_sources": "Vas a eliminar todas las fuentes de descargas",
"button_delete_all_sources": "Eliminar todo", "button_delete_all_sources": "Eliminar todo",
"added_download_source": "Añadir fuente de descarga", "added_download_source": "Añadir fuente de descarga",
"adding": "Añadiendo…",
"download_sources_synced": "Todas las fuentes de descarga están sincronizadas", "download_sources_synced": "Todas las fuentes de descarga están sincronizadas",
"insert_valid_json_url": "Introducí una URL de json válida", "insert_valid_json_url": "Introducí una URL de json válida",
"found_download_option_zero": "Sin opciones de descargas encontrada", "found_download_option_zero": "Sin opciones de descargas encontrada",
@@ -564,19 +556,6 @@
"debrid_description": "Los servicios Debrid son descargadores premium sin restricciones que te dejan descargar más rápido archivos alojados en servicios de alojamiento siendo que la única limitación es tu velocidad de internet.", "debrid_description": "Los servicios Debrid son descargadores premium sin restricciones que te dejan descargar más rápido archivos alojados en servicios de alojamiento siendo que la única limitación es tu velocidad de internet.",
"enable_friend_start_game_notifications": "Cuando un amigo está jugando un juego", "enable_friend_start_game_notifications": "Cuando un amigo está jugando un juego",
"autoplay_trailers_on_game_page": "Reproducir trailers automáticamente en la página del juego", "autoplay_trailers_on_game_page": "Reproducir trailers automáticamente en la página del juego",
"change_achievement_sound": "Cambiar sonido de logro",
"download_source_already_exists": "Esta fuente de descarga URL ya existe.",
"download_source_failed": "Error",
"download_source_matched": "Actualizado",
"download_source_matching": "Actualizando",
"download_source_no_information": "Sin información disponible",
"download_source_pending_matching": "Actualizando pronto",
"download_sources_synced_successfully": "Todas las fuentes de descarga están sincronizadas",
"failed_add_download_source": "Error al añadir la fuente de descarga. Por favor intentá de nuevo.",
"hydra_cloud": "Hydra Cloud",
"preview_sound": "Vista previa de sonido",
"remove_achievement_sound": "Eliminar sonido de logros",
"removed_all_download_sources": "Todas las fuentes de descarga eliminadas",
"hide_to_tray_on_game_start": "Ocultar Hydra en la bandeja al iniciar un juego" "hide_to_tray_on_game_start": "Ocultar Hydra en la bandeja al iniciar un juego"
}, },
"notifications": { "notifications": {

View File

@@ -93,19 +93,11 @@
}, },
"header": { "header": {
"search": "Buscar jogos", "search": "Buscar jogos",
"search_library": "Buscar na biblioteca",
"recent_searches": "Buscas Recentes",
"suggestions": "Sugestões",
"clear_history": "Limpar",
"remove_from_history": "Remover do histórico",
"loading": "Carregando...",
"no_results": "Sem resultados",
"home": "Início",
"catalogue": "Catálogo", "catalogue": "Catálogo",
"library": "Biblioteca",
"downloads": "Downloads", "downloads": "Downloads",
"search_results": "Resultados da busca", "search_results": "Resultados da busca",
"settings": "Ajustes", "settings": "Ajustes",
"home": "Início",
"version_available_install": "Versão {{version}} disponível. Clique aqui para reiniciar e instalar.", "version_available_install": "Versão {{version}} disponível. Clique aqui para reiniciar e instalar.",
"version_available_download": "Versão {{version}} disponível. Clique aqui para fazer o download." "version_available_download": "Versão {{version}} disponível. Clique aqui para fazer o download."
}, },
@@ -319,6 +311,7 @@
"show_more": "Mostrar mais", "show_more": "Mostrar mais",
"show_less": "Mostrar menos", "show_less": "Mostrar menos",
"reviews": "Avaliações", "reviews": "Avaliações",
"review_played_for": "Jogou por",
"leave_a_review": "Deixar uma Avaliação", "leave_a_review": "Deixar uma Avaliação",
"write_review_placeholder": "Compartilhe seus pensamentos sobre este jogo...", "write_review_placeholder": "Compartilhe seus pensamentos sobre este jogo...",
"sort_newest": "Mais Recentes", "sort_newest": "Mais Recentes",

View File

@@ -30,19 +30,11 @@
}, },
"header": { "header": {
"search": "Procurar jogos", "search": "Procurar jogos",
"search_library": "Procurar na biblioteca",
"recent_searches": "Pesquisas Recentes",
"suggestions": "Sugestões",
"clear_history": "Limpar",
"remove_from_history": "Remover do histórico",
"loading": "A carregar...",
"no_results": "Sem resultados",
"home": "Início",
"catalogue": "Catálogo", "catalogue": "Catálogo",
"library": "Biblioteca",
"downloads": "Transferências", "downloads": "Transferências",
"search_results": "Resultados da pesquisa", "search_results": "Resultados da pesquisa",
"settings": "Definições", "settings": "Definições",
"home": "Início",
"version_available_install": "Versão {{version}} disponível. Clica aqui para reiniciar e instalar.", "version_available_install": "Versão {{version}} disponível. Clica aqui para reiniciar e instalar.",
"version_available_download": "Versão {{version}} disponível. Clica aqui para fazer o download." "version_available_download": "Versão {{version}} disponível. Clica aqui para fazer o download."
}, },
@@ -190,6 +182,7 @@
"game_added_to_favorites": "Jogo adicionado aos favoritos", "game_added_to_favorites": "Jogo adicionado aos favoritos",
"create_start_menu_shortcut": "Criar atalho no Menu Iniciar", "create_start_menu_shortcut": "Criar atalho no Menu Iniciar",
"review_from_blocked_user": "Avaliação de utilizador bloqueado", "review_from_blocked_user": "Avaliação de utilizador bloqueado",
"review_played_for": "Jogaste por",
"show": "Mostrar", "show": "Mostrar",
"hide": "Ocultar", "hide": "Ocultar",
"review_played_for": "Jogado por" "review_played_for": "Jogado por"

View File

@@ -93,16 +93,8 @@
}, },
"header": { "header": {
"search": "Поиск", "search": "Поиск",
"search_library": "Поиск в библиотеке",
"recent_searches": "Недавние поиски",
"suggestions": "Предложения",
"clear_history": "Очистить",
"remove_from_history": "Удалить из истории",
"loading": "Загрузка...",
"no_results": "Нет результатов",
"home": "Главная", "home": "Главная",
"catalogue": "Каталог", "catalogue": "Каталог",
"library": "Библиотека",
"downloads": "Загрузки", "downloads": "Загрузки",
"search_results": "Результаты поиска", "search_results": "Результаты поиска",
"settings": "Настройки", "settings": "Настройки",
@@ -233,6 +225,7 @@
"show_more": "Показать больше", "show_more": "Показать больше",
"show_less": "Показать меньше", "show_less": "Показать меньше",
"reviews": "Отзывы", "reviews": "Отзывы",
"review_played_for": "Играно",
"leave_a_review": "Оставить отзыв", "leave_a_review": "Оставить отзыв",
"write_review_placeholder": "Поделитесь своими мыслями об этой игре...", "write_review_placeholder": "Поделитесь своими мыслями об этой игре...",
"sort_newest": "Сначала новые", "sort_newest": "Сначала новые",

View File

@@ -1,3 +0,0 @@
import "./get-session-hash";
import "./open-auth-window";
import "./sign-out";

View File

@@ -1,2 +0,0 @@
import "./check-for-updates";
import "./restart-and-install-update";

View File

@@ -1,4 +0,0 @@
import "./get-game-assets";
import "./get-game-shop-details";
import "./get-game-stats";
import "./get-random-game";

View File

@@ -1,4 +0,0 @@
import "./download-game-artifact";
import "./get-game-backup-preview";
import "./select-game-backup-path";
import "./upload-save-game";

View File

@@ -1,6 +0,0 @@
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";

View File

@@ -1,2 +0,0 @@
import "./check-folder-write-permission";
import "./get-disk-free-space";

View File

@@ -1,22 +1,107 @@
import { appVersion, defaultDownloadsPath, isStaging } from "@main/constants"; import { appVersion, defaultDownloadsPath, isStaging } from "@main/constants";
import { ipcMain } from "electron"; import { ipcMain } from "electron";
import "./auth"; import "./catalogue/get-game-shop-details";
import "./autoupdater"; import "./catalogue/get-random-game";
import "./catalogue"; import "./catalogue/get-game-stats";
import "./cloud-save"; import "./hardware/get-disk-free-space";
import "./download-sources"; import "./hardware/check-folder-write-permission";
import "./hardware"; import "./library/add-game-to-library";
import "./library"; import "./library/add-custom-game-to-library";
import "./leveldb"; import "./library/update-custom-game";
import "./misc"; import "./library/update-game-custom-assets";
import "./notifications"; import "./library/add-game-to-favorites";
import "./profile"; import "./library/remove-game-from-favorites";
import "./themes"; import "./library/toggle-game-pin";
import "./torrenting"; import "./library/create-game-shortcut";
import "./user"; import "./library/close-game";
import "./user-preferences"; 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 { isPortableVersion } from "@main/helpers"; import { isPortableVersion } from "@main/helpers";
ipcMain.handle("ping", () => "pong"); ipcMain.handle("ping", () => "pong");

View File

@@ -1,27 +0,0 @@
import { db } from "@main/level";
const sublevelCache = new Map<
string,
ReturnType<typeof db.sublevel<string, unknown>>
>();
/**
* 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<typeof db.sublevel<string, unknown>> => {
if (sublevelCache.has(sublevelName)) {
return sublevelCache.get(sublevelName)!;
}
// All sublevels use "json" encoding - this cannot be changed per sublevel
const sublevel = db.sublevel<string, unknown>(sublevelName, {
valueEncoding: "json",
});
sublevelCache.set(sublevelName, sublevel);
return sublevel;
};

View File

@@ -1,6 +0,0 @@
import "./leveldb-get";
import "./leveldb-put";
import "./leveldb-del";
import "./leveldb-clear";
import "./leveldb-values";
import "./leveldb-iterator";

View File

@@ -1,18 +0,0 @@
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);

View File

@@ -1,28 +0,0 @@
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);

View File

@@ -1,28 +0,0 @@
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<string, unknown>(key, { valueEncoding });
} catch (error) {
if (error instanceof Error && error.name === "NotFoundError") {
return null;
}
logger.error("Error in leveldbGet", error);
throw error;
}
};
registerEvent("leveldbGet", leveldbGet);

View File

@@ -1,18 +0,0 @@
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);

View File

@@ -1,27 +0,0 @@
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<string, unknown>(key, value, { valueEncoding });
}
} catch (error) {
logger.error("Error in leveldbPut", error);
throw error;
}
};
registerEvent("leveldbPut", leveldbPut);

View File

@@ -1,18 +0,0 @@
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);

View File

@@ -1,32 +0,0 @@
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";

View File

@@ -1,12 +0,0 @@
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";

View File

@@ -1,3 +0,0 @@
import "./publish-new-repacks-notification";
import "./show-achievement-test-notification";
import "./update-achievement-notification-window";

View File

@@ -1,4 +0,0 @@
import "./get-me";
import "./process-profile-image";
import "./sync-friend-requests";
import "./update-profile";

View File

@@ -1,15 +0,0 @@
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";

View File

@@ -1,7 +0,0 @@
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";

View File

@@ -13,11 +13,7 @@ const resumeGameDownload = async (
const download = await downloadsSublevel.get(gameKey); const download = await downloadsSublevel.get(gameKey);
if ( if (download?.status === "paused") {
download &&
(download.status === "paused" || download.status === "active") &&
download.progress !== 1
) {
await DownloadManager.pauseDownload(); await DownloadManager.pauseDownload();
for await (const [key, value] of downloadsSublevel.iterator()) { for await (const [key, value] of downloadsSublevel.iterator()) {

View File

@@ -1,5 +0,0 @@
import "./authenticate-real-debrid";
import "./authenticate-torbox";
import "./auto-launch";
import "./get-user-preferences";
import "./update-user-preferences";

View File

@@ -1,3 +0,0 @@
import "./get-auth";
import "./get-compared-unlocked-achievements";
import "./get-unlocked-achievements";

View File

@@ -7,9 +7,7 @@ export const getDownloadSourcesCheckBaseline = async (): Promise<
string | null string | null
> => { > => {
try { try {
const timestamp = await db.get(levelKeys.downloadSourcesCheckBaseline, { const timestamp = await db.get(levelKeys.downloadSourcesCheckBaseline);
valueEncoding: "utf8",
});
return timestamp; return timestamp;
} catch (error) { } catch (error) {
if (error instanceof Error && error.name === "NotFoundError") { if (error instanceof Error && error.name === "NotFoundError") {
@@ -29,9 +27,7 @@ export const updateDownloadSourcesCheckBaseline = async (
timestamp: string timestamp: string
): Promise<void> => { ): Promise<void> => {
const utcTimestamp = new Date(timestamp).toISOString(); 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) // Gets the 'since' value the API used in the last check (for modal comparison)
@@ -39,9 +35,7 @@ export const getDownloadSourcesSinceValue = async (): Promise<
string | null string | null
> => { > => {
try { try {
const timestamp = await db.get(levelKeys.downloadSourcesSinceValue, { const timestamp = await db.get(levelKeys.downloadSourcesSinceValue);
valueEncoding: "utf8",
});
return timestamp; return timestamp;
} catch (error) { } catch (error) {
if (error instanceof Error && error.name === "NotFoundError") { if (error instanceof Error && error.name === "NotFoundError") {
@@ -61,7 +55,5 @@ export const updateDownloadSourcesSinceValue = async (
timestamp: string timestamp: string
): Promise<void> => { ): Promise<void> => {
const utcTimestamp = new Date(timestamp).toISOString(); const utcTimestamp = new Date(timestamp).toISOString();
await db.put(levelKeys.downloadSourcesSinceValue, utcTimestamp, { await db.put(levelKeys.downloadSourcesSinceValue, utcTimestamp);
valueEncoding: "utf8",
});
}; };

View File

@@ -1,5 +1,5 @@
import { downloadsSublevel } from "./level/sublevels/downloads"; import { downloadsSublevel } from "./level/sublevels/downloads";
import { orderBy } from "lodash-es"; import { sortBy } from "lodash-es";
import { Downloader } from "@shared"; import { Downloader } from "@shared";
import { levelKeys, db } from "./level"; import { levelKeys, db } from "./level";
import type { UserPreferences } from "@types"; import type { UserPreferences } from "@types";
@@ -33,7 +33,9 @@ export const loadState = async () => {
await import("./events"); await import("./events");
Aria2.spawn(); if (process.platform !== "darwin") {
Aria2.spawn();
}
if (userPreferences?.realDebridApiToken) { if (userPreferences?.realDebridApiToken) {
RealDebridClient.authorize(userPreferences.realDebridApiToken); RealDebridClient.authorize(userPreferences.realDebridApiToken);
@@ -66,7 +68,7 @@ export const loadState = async () => {
.values() .values()
.all() .all()
.then((games) => { .then((games) => {
return orderBy(games, "timestamp", "desc"); return sortBy(games, "timestamp", "DESC");
}); });
downloads.forEach((download) => { downloads.forEach((download) => {

View File

@@ -7,12 +7,9 @@ export class Aria2 {
private static process: cp.ChildProcess | null = null; private static process: cp.ChildProcess | null = null;
public static spawn() { public static spawn() {
const binaryPath = const binaryPath = app.isPackaged
process.platform === "darwin" ? path.join(process.resourcesPath, "aria2c")
? "aria2c" : path.join(__dirname, "..", "..", "binaries", "aria2c");
: app.isPackaged
? path.join(process.resourcesPath, "aria2c")
: path.join(__dirname, "..", "..", "binaries", "aria2c");
this.process = cp.spawn( this.process = cp.spawn(
binaryPath, binaryPath,

View File

@@ -20,7 +20,7 @@ import { RealDebridClient } from "./real-debrid";
import path from "path"; import path from "path";
import { logger } from "../logger"; import { logger } from "../logger";
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level"; import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
import { orderBy } from "lodash-es"; import { sortBy } from "lodash-es";
import { TorBoxClient } from "./torbox"; import { TorBoxClient } from "./torbox";
import { GameFilesManager } from "../game-files-manager"; import { GameFilesManager } from "../game-files-manager";
import { HydraDebridClient } from "./hydra-debrid"; import { HydraDebridClient } from "./hydra-debrid";
@@ -194,10 +194,10 @@ export class DownloadManager {
.values() .values()
.all() .all()
.then((games) => { .then((games) => {
return orderBy( return sortBy(
games.filter((game) => game.status === "paused" && game.queued), games.filter((game) => game.status === "paused" && game.queued),
"timestamp", "timestamp",
"desc" "DESC"
); );
}); });

View File

@@ -36,13 +36,16 @@ export class GofileApi {
} }
public static async getDownloadLink(id: string) { public static async getDownloadLink(id: string) {
const searchParams = new URLSearchParams({
wt: WT,
});
const response = await axios.get<{ const response = await axios.get<{
status: string; status: string;
data: GofileContentsResponse; data: GofileContentsResponse;
}>(`https://api.gofile.io/contents/${id}`, { }>(`https://api.gofile.io/contents/${id}?${searchParams.toString()}`, {
headers: { headers: {
Authorization: `Bearer ${this.token}`, Authorization: `Bearer ${this.token}`,
"X-Website-Token": WT,
}, },
}); });

View File

@@ -58,13 +58,7 @@ export class HydraApi {
const decodedBase64 = atob(payload as string); const decodedBase64 = atob(payload as string);
const jsonData = JSON.parse(decodedBase64); const jsonData = JSON.parse(decodedBase64);
const { const { accessToken, expiresIn, refreshToken } = jsonData;
accessToken,
expiresIn,
refreshToken,
featurebaseJwt,
workwondersJwt,
} = jsonData;
const now = new Date(); const now = new Date();
@@ -91,8 +85,6 @@ export class HydraApi {
accessToken, accessToken,
refreshToken, refreshToken,
tokenExpirationTimestamp, tokenExpirationTimestamp,
featurebaseJwt,
workwondersJwt,
}, },
{ valueEncoding: "json" } { valueEncoding: "json" }
); );

View File

@@ -13,9 +13,9 @@ export class SystemPath {
}; };
static checkIfPathsAreAvailable() { static checkIfPathsAreAvailable() {
const paths = Object.keys( const paths = Object.keys(SystemPath.paths) as Array<
SystemPath.paths keyof typeof SystemPath.paths
) as (keyof typeof SystemPath.paths)[]; >;
paths.forEach((pathName) => { paths.forEach((pathName) => {
try { try {

View File

@@ -138,8 +138,7 @@ export class WindowManager {
(details, callback) => { (details, callback) => {
if ( if (
details.webContentsId !== this.mainWindow?.webContents.id || details.webContentsId !== this.mainWindow?.webContents.id ||
details.url.includes("chatwoot") || details.url.includes("chatwoot")
details.url.includes("workwonders")
) { ) {
return callback(details); return callback(details);
} }
@@ -160,8 +159,7 @@ export class WindowManager {
if ( if (
details.webContentsId !== this.mainWindow?.webContents.id || details.webContentsId !== this.mainWindow?.webContents.id ||
details.url.includes("featurebase") || details.url.includes("featurebase") ||
details.url.includes("chatwoot") || details.url.includes("chatwoot")
details.url.includes("workwonders")
) { ) {
return callback(details); return callback(details);
} }

View File

@@ -619,28 +619,4 @@ contextBridge.exposeInMainWorld("electron", {
}, },
closeEditorWindow: (themeId?: string) => closeEditorWindow: (themeId?: string) =>
ipcRenderer.invoke("closeEditorWindow", themeId), 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),
},
}); });

View File

@@ -6,7 +6,7 @@
<title>Hydra Launcher</title> <title>Hydra Launcher</title>
<meta <meta
http-equiv="Content-Security-Policy" http-equiv="Content-Security-Policy"
content="default-src 'self' 'unsafe-inline' * data: local:; media-src 'self' 'unsafe-inline' * data: local: blob:;" content="default-src 'self' 'unsafe-inline' * data: local:;"
/> />
</head> </head>
<body> <body>

View File

@@ -31,8 +31,6 @@ import {
getAchievementSoundUrl, getAchievementSoundUrl,
getAchievementSoundVolume, getAchievementSoundVolume,
} from "./helpers"; } from "./helpers";
import { levelDBService } from "./services/leveldb.service";
import type { UserPreferences } from "@types";
import "./app.scss"; import "./app.scss";
export interface AppProps { export interface AppProps {
@@ -79,12 +77,11 @@ export function App() {
const { showSuccessToast } = useToast(); const { showSuccessToast } = useToast();
useEffect(() => { useEffect(() => {
Promise.all([ Promise.all([window.electron.getUserPreferences(), updateLibrary()]).then(
levelDBService.get("userPreferences", null, "json"), ([preferences]) => {
updateLibrary(), dispatch(setUserPreferences(preferences));
]).then(([preferences]) => { }
dispatch(setUserPreferences(preferences as UserPreferences | null)); );
});
}, [navigate, location.pathname, dispatch, updateLibrary]); }, [navigate, location.pathname, dispatch, updateLibrary]);
useEffect(() => { useEffect(() => {
@@ -207,11 +204,7 @@ export function App() {
}, [dispatch, draggingDisabled]); }, [dispatch, draggingDisabled]);
const loadAndApplyTheme = useCallback(async () => { const loadAndApplyTheme = useCallback(async () => {
const allThemes = (await levelDBService.values("themes")) as { const activeTheme = await window.electron.getActiveCustomTheme();
isActive?: boolean;
code?: string;
}[];
const activeTheme = allThemes.find((theme) => theme.isActive);
if (activeTheme?.code) { if (activeTheme?.code) {
injectCustomCss(activeTheme.code); injectCustomCss(activeTheme.code);
} else { } else {

View File

@@ -122,10 +122,10 @@ export function BottomPanel() {
</button> </button>
<button <button
data-open-workwonders-changelog-mini data-featurebase-changelog
className="bottom-panel__version-button" className="bottom-panel__version-button"
> >
<small> <small data-featurebase-changelog>
{sessionHash ? `${sessionHash} -` : ""} v{version} &quot; {sessionHash ? `${sessionHash} -` : ""} v{version} &quot;
{VERSION_CODENAME}&quot; {VERSION_CODENAME}&quot;
</small> </small>

View File

@@ -18,7 +18,6 @@ interface DropdownMenuProps {
side?: "top" | "bottom" | "left" | "right"; side?: "top" | "bottom" | "left" | "right";
align?: "start" | "center" | "end"; align?: "start" | "center" | "end";
alignOffset?: number; alignOffset?: number;
collisionPadding?: number;
} }
export function DropdownMenu({ export function DropdownMenu({
@@ -30,7 +29,6 @@ export function DropdownMenu({
loop = true, loop = true,
align = "center", align = "center",
alignOffset = 0, alignOffset = 0,
collisionPadding = 16,
}: Readonly<DropdownMenuProps>) { }: Readonly<DropdownMenuProps>) {
return ( return (
<DropdownMenuPrimitive.Root> <DropdownMenuPrimitive.Root>
@@ -45,7 +43,6 @@ export function DropdownMenu({
loop={loop} loop={loop}
align={align} align={align}
alignOffset={alignOffset} alignOffset={alignOffset}
collisionPadding={collisionPadding}
className="dropdown-menu__content" className="dropdown-menu__content"
> >
{title && ( {title && (

View File

@@ -15,8 +15,6 @@ import { AutoUpdateSubHeader } from "./auto-update-sub-header";
import { setFilters, setLibrarySearchQuery } from "@renderer/features"; import { setFilters, setLibrarySearchQuery } from "@renderer/features";
import cn from "classnames"; import cn from "classnames";
import { SearchDropdown } from "@renderer/components"; import { SearchDropdown } from "@renderer/components";
import { buildGameDetailsPath } from "@renderer/helpers";
import type { GameShop } from "@types";
const pathTitle: Record<string, string> = { const pathTitle: Record<string, string> = {
"/": "home", "/": "home",
@@ -163,11 +161,11 @@ export function Header() {
const handleSelectSuggestion = (suggestion: { const handleSelectSuggestion = (suggestion: {
title: string; title: string;
objectId: string; objectId: string;
shop: GameShop; shop: string;
}) => { }) => {
setIsDropdownVisible(false); setIsDropdownVisible(false);
inputRef.current?.blur(); inputRef.current?.blur();
navigate(buildGameDetailsPath(suggestion)); navigate(`/game/${suggestion.shop}/${suggestion.objectId}`);
}; };
const handleClearSearch = () => { const handleClearSearch = () => {

View File

@@ -1,11 +1,11 @@
import React from "react"; import React from "react";
interface HighlightTextProps { interface HighlightTextProps {
readonly text: string; text: string;
readonly query: string; query: string;
} }
export function HighlightText({ text, query }: Readonly<HighlightTextProps>) { export function HighlightText({ text, query }: HighlightTextProps) {
if (!query.trim()) { if (!query.trim()) {
return <>{text}</>; return <>{text}</>;
} }
@@ -19,25 +19,24 @@ export function HighlightText({ text, query }: Readonly<HighlightTextProps>) {
return <>{text}</>; return <>{text}</>;
} }
const matches: { start: number; end: number }[] = []; const textWords = text.split(/\b/);
const textLower = text.toLowerCase(); const matches: Array<{ start: number; end: number; text: string }> = [];
queryWords.forEach((queryWord) => { let currentIndex = 0;
const escapedQuery = queryWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); textWords.forEach((word) => {
const regex = new RegExp( const wordLower = word.toLowerCase();
`(?:^|[\\s])${escapedQuery}(?=[\\s]|$)|^${escapedQuery}$`,
"gi"
);
let match; queryWords.forEach((queryWord) => {
while ((match = regex.exec(textLower)) !== null) { if (wordLower === queryWord) {
const matchedText = match[0]; matches.push({
const leadingSpace = matchedText.startsWith(" ") ? 1 : 0; start: currentIndex,
const start = match.index + leadingSpace; end: currentIndex + word.length,
const end = start + queryWord.length; text: word,
});
}
});
matches.push({ start, end }); currentIndex += word.length;
}
}); });
if (matches.length === 0) { if (matches.length === 0) {
@@ -46,15 +45,17 @@ export function HighlightText({ text, query }: Readonly<HighlightTextProps>) {
matches.sort((a, b) => a.start - b.start); matches.sort((a, b) => a.start - b.start);
const mergedMatches: { start: number; end: number }[] = []; const mergedMatches: Array<{ start: number; end: number }> = [];
if (matches.length === 0) {
return <>{text}</>;
}
let current = matches[0]; let current = matches[0];
for (let i = 1; i < matches.length; i++) { for (let i = 1; i < matches.length; i++) {
if (matches[i].start <= current.end) { if (matches[i].start <= current.end) {
current = { current.end = Math.max(current.end, matches[i].end);
start: current.start,
end: Math.max(current.end, matches[i].end),
};
} else { } else {
mergedMatches.push(current); mergedMatches.push(current);
current = matches[i]; current = matches[i];
@@ -62,7 +63,7 @@ export function HighlightText({ text, query }: Readonly<HighlightTextProps>) {
} }
mergedMatches.push(current); mergedMatches.push(current);
const parts: { text: string; highlight: boolean; key: string }[] = []; const parts: Array<{ text: string; highlight: boolean }> = [];
let lastIndex = 0; let lastIndex = 0;
mergedMatches.forEach((match) => { mergedMatches.forEach((match) => {
@@ -70,14 +71,12 @@ export function HighlightText({ text, query }: Readonly<HighlightTextProps>) {
parts.push({ parts.push({
text: text.slice(lastIndex, match.start), text: text.slice(lastIndex, match.start),
highlight: false, highlight: false,
key: `${lastIndex}-${match.start}`,
}); });
} }
parts.push({ parts.push({
text: text.slice(match.start, match.end), text: text.slice(match.start, match.end),
highlight: true, highlight: true,
key: `${match.start}-${match.end}`,
}); });
lastIndex = match.end; lastIndex = match.end;
@@ -87,19 +86,18 @@ export function HighlightText({ text, query }: Readonly<HighlightTextProps>) {
parts.push({ parts.push({
text: text.slice(lastIndex), text: text.slice(lastIndex),
highlight: false, highlight: false,
key: `${lastIndex}-${text.length}`,
}); });
} }
return ( return (
<> <>
{parts.map((part) => {parts.map((part, index) =>
part.highlight ? ( part.highlight ? (
<mark key={part.key} className="search-dropdown__highlight"> <mark key={index} className="search-dropdown__highlight">
{part.text} {part.text}
</mark> </mark>
) : ( ) : (
<React.Fragment key={part.key}>{part.text}</React.Fragment> <React.Fragment key={index}>{part.text}</React.Fragment>
) )
)} )}
</> </>

View File

@@ -24,8 +24,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 8px 12px 8px; padding: 8px 12px 4px;
margin-bottom: 4px;
} }
&__section-title { &__section-title {
@@ -36,19 +35,19 @@
letter-spacing: 0.5px; letter-spacing: 0.5px;
} }
&__clear-text-button { &__clear-button {
color: globals.$muted-color; color: globals.$muted-color;
cursor: pointer; cursor: pointer;
padding: 0; padding: 4px;
font-size: 11px; border-radius: 4px;
font-weight: bold; transition: all ease 0.2s;
text-transform: uppercase; display: flex;
transition: color ease 0.2s; align-items: center;
background: transparent; justify-content: center;
border: none;
&:hover { &:hover {
color: #ffffff; color: #dadbe1;
background-color: rgba(255, 255, 255, 0.1);
} }
} }
@@ -75,16 +74,17 @@
transform: translateY(-50%); transform: translateY(-50%);
color: globals.$muted-color; color: globals.$muted-color;
padding: 4px; padding: 4px;
border-radius: 4px;
opacity: 0; opacity: 0;
transition: opacity ease 0.15s; transition: all ease 0.15s;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-color: transparent; background-color: transparent;
&:hover { &:hover {
color: #ff3333; color: #ff5555;
background-color: rgba(255, 85, 85, 0.2); background-color: rgba(255, 85, 85, 0.1);
} }
} }
@@ -144,8 +144,8 @@
} }
&__highlight { &__highlight {
background-color: rgba(255, 193, 7, 0.4); background-color: rgba(255, 193, 7, 0.3);
color: #ffa000; color: #ffc107;
font-weight: 600; font-weight: 600;
padding: 0 2px; padding: 0 2px;
border-radius: 2px; border-radius: 2px;

View File

@@ -1,6 +1,11 @@
import { useEffect, useRef, useCallback, useState } from "react"; import { useEffect, useRef, useCallback, useState } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { ClockIcon, SearchIcon, XIcon } from "@primer/octicons-react"; import {
ClockIcon,
SearchIcon,
TrashIcon,
XIcon,
} from "@primer/octicons-react";
import cn from "classnames"; import cn from "classnames";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import type { SearchHistoryEntry } from "@renderer/hooks/use-search-history"; import type { SearchHistoryEntry } from "@renderer/hooks/use-search-history";
@@ -139,10 +144,11 @@ export function SearchDropdown({
</span> </span>
<button <button
type="button" type="button"
className="search-dropdown__clear-text-button" className="search-dropdown__clear-button"
onClick={onClearHistory} onClick={onClearHistory}
title={t("clear_history")}
> >
{t("clear_history")} <TrashIcon size={14} />
</button> </button>
</div> </div>
<ul className="search-dropdown__list"> <ul className="search-dropdown__list">

View File

@@ -1,8 +1,6 @@
import { createContext, useCallback, useEffect, useRef, useState } from "react"; import { createContext, useCallback, useEffect, useRef, useState } from "react";
import { setHeaderTitle } from "@renderer/features"; import { setHeaderTitle } from "@renderer/features";
import { levelDBService } from "@renderer/services/leveldb.service";
import { orderBy } from "lodash-es";
import { getSteamLanguage } from "@renderer/helpers"; import { getSteamLanguage } from "@renderer/helpers";
import { import {
useAppDispatch, useAppDispatch,
@@ -12,7 +10,6 @@ import {
} from "@renderer/hooks"; } from "@renderer/hooks";
import type { import type {
DownloadSource,
GameRepack, GameRepack,
GameShop, GameShop,
GameStats, GameStats,
@@ -300,10 +297,7 @@ export function GameDetailsContextProvider({
const fetchDownloadSources = async () => { const fetchDownloadSources = async () => {
try { try {
const sourcesRaw = (await levelDBService.values( const sources = await window.electron.getDownloadSources();
"downloadSources"
)) as DownloadSource[];
const sources = orderBy(sourcesRaw, "createdAt", "desc");
const params = { const params = {
take: 100, take: 100,

View File

@@ -2,7 +2,6 @@ import { createContext, useCallback, useEffect, useState } from "react";
import { setUserPreferences } from "@renderer/features"; import { setUserPreferences } from "@renderer/features";
import { useAppDispatch } from "@renderer/hooks"; import { useAppDispatch } from "@renderer/hooks";
import { levelDBService } from "@renderer/services/leveldb.service";
import type { UserBlocks, UserPreferences } from "@types"; import type { UserBlocks, UserPreferences } from "@types";
import { useSearchParams } from "react-router-dom"; import { useSearchParams } from "react-router-dom";
@@ -135,11 +134,9 @@ export function SettingsContextProvider({
const updateUserPreferences = async (values: Partial<UserPreferences>) => { const updateUserPreferences = async (values: Partial<UserPreferences>) => {
await window.electron.updateUserPreferences(values); await window.electron.updateUserPreferences(values);
levelDBService window.electron.getUserPreferences().then((userPreferences) => {
.get("userPreferences", null, "json") dispatch(setUserPreferences(userPreferences));
.then((userPreferences) => { });
dispatch(setUserPreferences(userPreferences as UserPreferences | null));
});
}; };
return ( return (

View File

@@ -438,25 +438,6 @@ declare global {
onNewDownloadOptions: ( onNewDownloadOptions: (
cb: (gamesWithNewOptions: { gameId: string; count: number }[]) => void cb: (gamesWithNewOptions: { gameId: string; count: number }[]) => void
) => () => Electron.IpcRenderer; ) => () => Electron.IpcRenderer;
/* LevelDB Generic CRUD */
leveldb: {
get: (
key: string,
sublevelName?: string | null,
valueEncoding?: "json" | "utf8"
) => Promise<unknown>;
put: (
key: string,
value: unknown,
sublevelName?: string | null,
valueEncoding?: "json" | "utf8"
) => Promise<void>;
del: (key: string, sublevelName?: string | null) => Promise<void>;
clear: (sublevelName: string) => Promise<void>;
values: (sublevelName: string) => Promise<unknown[]>;
iterator: (sublevelName: string) => Promise<[string, unknown][]>;
};
} }
interface Window { interface Window {

View File

@@ -3,7 +3,6 @@ import type { GameShop } from "@types";
import Color from "color"; import Color from "color";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { THEME_WEB_STORE_URL } from "./constants"; import { THEME_WEB_STORE_URL } from "./constants";
import { levelDBService } from "./services/leveldb.service";
export const formatDownloadProgress = ( export const formatDownloadProgress = (
progress?: number, progress?: number,
@@ -128,12 +127,7 @@ export const getAchievementSoundUrl = async (): Promise<string> => {
.default; .default;
try { try {
const allThemes = (await levelDBService.values("themes")) as { const activeTheme = await window.electron.getActiveCustomTheme();
id: string;
isActive?: boolean;
hasCustomSound?: boolean;
}[];
const activeTheme = allThemes.find((theme) => theme.isActive);
if (activeTheme?.hasCustomSound) { if (activeTheme?.hasCustomSound) {
const soundDataUrl = await window.electron.getThemeSoundDataUrl( const soundDataUrl = await window.electron.getThemeSoundDataUrl(
@@ -152,18 +146,10 @@ export const getAchievementSoundUrl = async (): Promise<string> => {
export const getAchievementSoundVolume = async (): Promise<number> => { export const getAchievementSoundVolume = async (): Promise<number> => {
try { try {
const prefs = (await levelDBService.get( const prefs = await window.electron.getUserPreferences();
"userPreferences",
null,
"json"
)) as { achievementSoundVolume?: number } | null;
return prefs?.achievementSoundVolume ?? 0.15; return prefs?.achievementSoundVolume ?? 0.15;
} catch (error) { } catch (error) {
console.error("Failed to get sound volume", error); console.error("Failed to get sound volume", error);
return 0.15; return 0.15;
} }
}; };
export const getGameKey = (shop: GameShop, objectId: string): string => {
return `${shop}:${objectId}`;
};

View File

@@ -10,4 +10,3 @@ export * from "./use-download-options-listener";
export * from "./use-game-card"; export * from "./use-game-card";
export * from "./use-search-history"; export * from "./use-search-history";
export * from "./use-search-suggestions"; export * from "./use-search-suggestions";
export * from "./use-hls-video";

View File

@@ -1,9 +1,8 @@
import axios from "axios"; import axios from "axios";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { levelDBService } from "@renderer/services/leveldb.service";
import type { DownloadSource } from "@types";
import { useAppDispatch } from "./redux"; import { useAppDispatch } from "./redux";
import { setGenres, setTags } from "@renderer/features"; import { setGenres, setTags } from "@renderer/features";
import type { DownloadSource } from "@types";
export const externalResourcesInstance = axios.create({ export const externalResourcesInstance = axios.create({
baseURL: import.meta.env.RENDERER_VITE_EXTERNAL_RESOURCES_URL, baseURL: import.meta.env.RENDERER_VITE_EXTERNAL_RESOURCES_URL,
@@ -41,9 +40,8 @@ export function useCatalogue() {
}, []); }, []);
const getDownloadSources = useCallback(() => { const getDownloadSources = useCallback(() => {
levelDBService.values("downloadSources").then((results) => { window.electron.getDownloadSources().then((results) => {
const sources = results as DownloadSource[]; setDownloadSources(results.filter((source) => !!source.fingerprint));
setDownloadSources(sources.filter((source) => !!source.fingerprint));
}); });
}, []); }, []);

View File

@@ -1,102 +0,0 @@
import { useEffect, useRef } from "react";
import Hls from "hls.js";
import { logger } from "@renderer/logger";
interface UseHlsVideoOptions {
videoSrc: string | undefined;
videoType: string | undefined;
autoplay?: boolean;
muted?: boolean;
loop?: boolean;
}
export function useHlsVideo(
videoRef: React.RefObject<HTMLVideoElement>,
{ videoSrc, videoType, autoplay, muted, loop }: UseHlsVideoOptions
) {
const hlsRef = useRef<Hls | null>(null);
useEffect(() => {
const video = videoRef.current;
if (!video || !videoSrc) return;
const isHls = videoType === "application/x-mpegURL";
if (!isHls) {
return undefined;
}
if (Hls.isSupported()) {
const hls = new Hls({
enableWorker: true,
lowLatencyMode: false,
});
hlsRef.current = hls;
hls.loadSource(videoSrc);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
if (autoplay) {
video.play().catch((err) => {
logger.warn("Failed to autoplay HLS video:", err);
});
}
});
hls.on(Hls.Events.ERROR, (_event, data) => {
if (data.fatal) {
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
logger.error("HLS network error, trying to recover");
hls.startLoad();
break;
case Hls.ErrorTypes.MEDIA_ERROR:
logger.error("HLS media error, trying to recover");
hls.recoverMediaError();
break;
default:
logger.error("HLS fatal error, destroying instance");
hls.destroy();
break;
}
}
});
return () => {
hls.destroy();
hlsRef.current = null;
};
} else if (video.canPlayType("application/vnd.apple.mpegurl")) {
video.src = videoSrc;
video.load();
if (autoplay) {
video.play().catch((err) => {
logger.warn("Failed to autoplay HLS video:", err);
});
}
return () => {
video.src = "";
};
} else {
logger.warn("HLS playback is not supported in this browser");
return undefined;
}
}, [videoRef, videoSrc, videoType, autoplay, muted, loop]);
useEffect(() => {
const video = videoRef.current;
if (!video) return;
if (muted !== undefined) {
video.muted = muted;
}
if (loop !== undefined) {
video.loop = loop;
}
}, [videoRef, muted, loop]);
return hlsRef.current;
}

View File

@@ -1,5 +1,4 @@
import { useState, useCallback, useEffect, useRef } from "react"; import { useState, useCallback, useEffect } from "react";
import { levelDBService } from "@renderer/services/leveldb.service";
export interface SearchHistoryEntry { export interface SearchHistoryEntry {
query: string; query: string;
@@ -7,32 +6,22 @@ export interface SearchHistoryEntry {
context: "library" | "catalogue"; context: "library" | "catalogue";
} }
const LEVELDB_KEY = "searchHistory"; const STORAGE_KEY = "search-history";
const MAX_HISTORY_ENTRIES = 15; const MAX_HISTORY_ENTRIES = 15;
export function useSearchHistory() { export function useSearchHistory() {
const [history, setHistory] = useState<SearchHistoryEntry[]>([]); const [history, setHistory] = useState<SearchHistoryEntry[]>([]);
const isInitialized = useRef(false);
useEffect(() => { useEffect(() => {
const loadHistory = async () => { const stored = localStorage.getItem(STORAGE_KEY);
if (isInitialized.current) return; if (stored) {
isInitialized.current = true;
try { try {
const data = (await levelDBService.get(LEVELDB_KEY, null, "json")) as const parsed = JSON.parse(stored) as SearchHistoryEntry[];
| SearchHistoryEntry[] setHistory(parsed);
| null;
if (data) {
setHistory(data);
}
} catch { } catch {
setHistory([]); localStorage.removeItem(STORAGE_KEY);
} }
}; }
loadHistory();
}, []); }, []);
const addToHistory = useCallback( const addToHistory = useCallback(
@@ -50,7 +39,7 @@ export function useSearchHistory() {
(entry) => entry.query.toLowerCase() !== query.toLowerCase().trim() (entry) => entry.query.toLowerCase() !== query.toLowerCase().trim()
); );
const updated = [newEntry, ...filtered].slice(0, MAX_HISTORY_ENTRIES); const updated = [newEntry, ...filtered].slice(0, MAX_HISTORY_ENTRIES);
levelDBService.put(LEVELDB_KEY, updated, null, "json"); localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
return updated; return updated;
}); });
}, },
@@ -60,14 +49,14 @@ export function useSearchHistory() {
const removeFromHistory = useCallback((query: string) => { const removeFromHistory = useCallback((query: string) => {
setHistory((prev) => { setHistory((prev) => {
const updated = prev.filter((entry) => entry.query !== query); const updated = prev.filter((entry) => entry.query !== query);
levelDBService.put(LEVELDB_KEY, updated, null, "json"); localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
return updated; return updated;
}); });
}, []); }, []);
const clearHistory = useCallback(() => { const clearHistory = useCallback(() => {
setHistory([]); setHistory([]);
levelDBService.del(LEVELDB_KEY, null); localStorage.removeItem(STORAGE_KEY);
}, []); }, []);
const getRecentHistory = useCallback( const getRecentHistory = useCallback(

View File

@@ -1,13 +1,11 @@
import { useState, useEffect, useCallback, useRef } from "react"; import { useState, useEffect, useCallback, useRef } from "react";
import { useAppSelector } from "./redux"; import { useAppSelector } from "./redux";
import { debounce } from "lodash-es"; import { debounce } from "lodash-es";
import { logger } from "@renderer/logger";
import type { GameShop } from "@types";
export interface SearchSuggestion { export interface SearchSuggestion {
title: string; title: string;
objectId: string; objectId: string;
shop: GameShop; shop: string;
iconUrl: string | null; iconUrl: string | null;
source: "library" | "catalogue"; source: "library" | "catalogue";
} }
@@ -21,7 +19,6 @@ export function useSearchSuggestions(
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const library = useAppSelector((state) => state.library.value); const library = useAppSelector((state) => state.library.value);
const abortControllerRef = useRef<AbortController | null>(null); const abortControllerRef = useRef<AbortController | null>(null);
const cacheRef = useRef<Map<string, SearchSuggestion[]>>(new Map());
const getLibrarySuggestions = useCallback( const getLibrarySuggestions = useCallback(
(searchQuery: string, limit: number = 3): SearchSuggestion[] => { (searchQuery: string, limit: number = 3): SearchSuggestion[] => {
@@ -70,15 +67,6 @@ export function useSearchSuggestions(
return; return;
} }
const cacheKey = `${searchQuery.toLowerCase()}_${limit}`;
const cachedResults = cacheRef.current.get(cacheKey);
if (cachedResults) {
setSuggestions(cachedResults);
setIsLoading(false);
return;
}
abortControllerRef.current?.abort(); abortControllerRef.current?.abort();
const abortController = new AbortController(); const abortController = new AbortController();
abortControllerRef.current = abortController; abortControllerRef.current = abortController;
@@ -87,12 +75,12 @@ export function useSearchSuggestions(
try { try {
const response = await window.electron.hydraApi.get< const response = await window.electron.hydraApi.get<
{ Array<{
title: string; title: string;
objectId: string; objectId: string;
shop: GameShop; shop: string;
iconUrl: string | null; iconUrl: string | null;
}[] }>
>("/catalogue/search/suggestions", { >("/catalogue/search/suggestions", {
params: { params: {
query: searchQuery, query: searchQuery,
@@ -110,12 +98,10 @@ export function useSearchSuggestions(
}) })
); );
cacheRef.current.set(cacheKey, catalogueSuggestions);
setSuggestions(catalogueSuggestions); setSuggestions(catalogueSuggestions);
} catch (error) { } catch (error) {
if (!abortController.signal.aborted) { if (!abortController.signal.aborted) {
setSuggestions([]); setSuggestions([]);
logger.error("Failed to fetch catalogue suggestions", error);
} }
} finally { } finally {
if (!abortController.signal.aborted) { if (!abortController.signal.aborted) {

View File

@@ -21,7 +21,6 @@ import resources from "@locales";
import { logger } from "./logger"; import { logger } from "./logger";
import { addCookieInterceptor } from "./cookies"; import { addCookieInterceptor } from "./cookies";
import { levelDBService } from "./services/leveldb.service";
import Catalogue from "./pages/catalogue/catalogue"; import Catalogue from "./pages/catalogue/catalogue";
import Home from "./pages/home/home"; import Home from "./pages/home/home";
import Downloads from "./pages/downloads/downloads"; import Downloads from "./pages/downloads/downloads";
@@ -49,11 +48,7 @@ i18n
}, },
}) })
.then(async () => { .then(async () => {
const userPreferences = (await levelDBService.get( const userPreferences = await window.electron.getUserPreferences();
"userPreferences",
null,
"json"
)) as { language?: string } | null;
if (userPreferences?.language) { if (userPreferences?.language) {
i18n.changeLanguage(userPreferences.language); i18n.changeLanguage(userPreferences.language);

View File

@@ -11,7 +11,6 @@ import {
getAchievementSoundVolume, getAchievementSoundVolume,
} from "@renderer/helpers"; } from "@renderer/helpers";
import { AchievementNotificationItem } from "@renderer/components/achievements/notification/achievement-notification"; import { AchievementNotificationItem } from "@renderer/components/achievements/notification/achievement-notification";
import { levelDBService } from "@renderer/services/leveldb.service";
import app from "../../../app.scss?inline"; import app from "../../../app.scss?inline";
import styles from "../../../components/achievements/notification/achievement-notification.scss?inline"; import styles from "../../../components/achievements/notification/achievement-notification.scss?inline";
import root from "react-shadow"; import root from "react-shadow";
@@ -145,11 +144,7 @@ export function AchievementNotification() {
const loadAndApplyTheme = useCallback(async () => { const loadAndApplyTheme = useCallback(async () => {
if (!shadowRootRef) return; if (!shadowRootRef) return;
const allThemes = (await levelDBService.values("themes")) as { const activeTheme = await window.electron.getActiveCustomTheme();
isActive?: boolean;
code?: string;
}[];
const activeTheme = allThemes.find((theme) => theme.isActive);
if (activeTheme?.code) { if (activeTheme?.code) {
injectCustomCss(activeTheme.code, shadowRootRef); injectCustomCss(activeTheme.code, shadowRootRef);
} else { } else {

View File

@@ -4,537 +4,158 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: calc(globals.$spacing-unit * 2); gap: calc(globals.$spacing-unit * 2);
margin-inline: calc(globals.$spacing-unit * 3);
padding-block: calc(globals.$spacing-unit * 3);
&--queued { &__details-with-article {
padding-bottom: 0; display: flex;
} align-items: center;
gap: calc(globals.$spacing-unit / 2);
&--completed { align-self: flex-start;
padding-top: calc(globals.$spacing-unit * 3); cursor: pointer;
} }
&__header { &__header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: calc(globals.$spacing-unit); justify-content: space-between;
gap: calc(globals.$spacing-unit * 2);
&-title-group { &-divider {
display: flex;
align-items: center;
gap: calc(globals.$spacing-unit);
flex: 1; flex: 1;
background-color: globals.$border-color;
h2 { height: 1px;
margin: 0;
font-size: 20px;
font-weight: 700;
color: rgba(255, 255, 255, 0.95);
}
} }
&-count { &-count {
background-color: rgba(255, 255, 255, 0.1); font-weight: 400;
color: rgba(255, 255, 255, 0.7);
padding: 4px 8px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
min-width: 24px;
text-align: center;
flex-shrink: 0;
} }
} }
&--hero {
&__title-wrapper {
display: flex;
align-items: center;
margin-bottom: globals.$spacing-unit;
gap: globals.$spacing-unit;
}
&__title {
font-weight: bold;
cursor: pointer;
color: globals.$body-color;
text-align: left;
font-size: 16px;
display: block;
&:hover {
text-decoration: underline;
}
}
&__downloads {
width: 100%; width: 100%;
position: relative; gap: calc(globals.$spacing-unit * 2);
overflow: hidden; display: flex;
flex-direction: column;
margin: 0; margin: 0;
padding: 0; padding: 0;
padding-bottom: globals.$spacing-unit; margin-top: globals.$spacing-unit;
} }
&__hero-background { &__item {
position: absolute;
top: 0;
left: 0;
width: 100%; width: 100%;
height: 120%; background-color: globals.$background-color;
z-index: 0; display: flex;
border-radius: 8px;
border: solid 1px globals.$border-color;
overflow: hidden;
box-shadow: 0px 0px 5px 0px #000000;
transition: all ease 0.2s;
height: 140px;
min-height: 140px;
max-height: 140px;
position: relative;
img { &--hydra {
width: 100%; box-shadow: 0px 0px 16px 0px rgba(12, 241, 202, 0.15);
height: 100%;
object-fit: cover;
object-position: 50% 20%;
} }
} }
// PLEASE FIX THE COLORS &__cover {
&__hero-overlay { width: 280px;
position: absolute; min-width: 280px;
top: 0; height: auto;
left: 0; border-right: solid 1px globals.$border-color;
width: 100%;
height: 100%;
background: linear-gradient(
to bottom,
rgba(0, 0, 0, 0.3) 0%,
rgb(5, 5, 5) 70%,
rgb(26, 26, 26) 100%
);
}
&__hero-content {
position: relative; position: relative;
z-index: 1; z-index: 1;
padding: calc(globals.$spacing-unit * 4);
padding-bottom: 0;
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit * 2);
}
&__hero-logo { &-content {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
&-button {
background: none;
border: none;
padding: 0;
cursor: pointer;
display: flex;
align-items: center;
transition: scale 0.2s ease;
outline: none;
&:hover {
scale: 1.05;
}
}
img {
max-width: 180px;
max-height: 60px;
object-fit: contain;
cursor: pointer;
transition: opacity 0.2s ease;
&:hover {
opacity: 0.8;
}
&:focus {
outline: 2px solid rgba(255, 255, 255, 0.5);
outline-offset: 4px;
border-radius: 4px;
}
@container #{globals.$app-container} (min-width: 700px) {
max-width: 220px;
max-height: 75px;
}
@container #{globals.$app-container} (min-width: 900px) {
max-width: 280px;
max-height: 95px;
}
@container #{globals.$app-container} (min-width: 1200px) {
max-width: 340px;
max-height: 115px;
}
@container #{globals.$app-container} (min-width: 1500px) {
max-width: 400px;
max-height: 130px;
}
}
h1 {
font-size: 20px;
font-weight: 700;
color: #ffffff;
text-shadow: 2px 2px 12px rgba(0, 0, 0, 0.9);
margin: 0;
cursor: pointer;
transition: opacity 0.2s ease;
&:hover {
opacity: 0.8;
}
&:focus {
outline: 2px solid rgba(255, 255, 255, 0.5);
outline-offset: 4px;
border-radius: 4px;
}
@container #{globals.$app-container} (min-width: 700px) {
font-size: 26px;
}
@container #{globals.$app-container} (min-width: 900px) {
font-size: 32px;
}
@container #{globals.$app-container} (min-width: 1200px) {
font-size: 38px;
}
@container #{globals.$app-container} (min-width: 1500px) {
font-size: 44px;
}
}
}
&__hero-action-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: calc(globals.$spacing-unit * 3);
margin-top: calc(globals.$spacing-unit * 4);
margin-bottom: calc(globals.$spacing-unit * 2);
}
&__hero-buttons {
display: flex;
gap: calc(globals.$spacing-unit);
align-items: center;
flex-shrink: 0;
}
&__glass-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -4px rgba(0, 0, 0, 0.1);
color: #fff;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
&:hover {
background: rgba(255, 255, 255, 0.2);
}
}
&__hero-progress {
display: flex;
flex-direction: column;
}
&__progress-info-row {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
&__progress-row {
display: flex;
align-items: flex-end;
gap: calc(globals.$spacing-unit * 2);
&--bar {
margin-top: calc(globals.$spacing-unit);
}
}
&__progress-status {
font-size: 13px;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
text-transform: uppercase;
letter-spacing: 0.5px;
}
&__progress-percentage {
font-size: 14px;
font-weight: 700;
color: #ffffff;
align-self: flex-end;
display: inline-block;
overflow: hidden;
line-height: 1.2;
span {
display: inline-block;
}
}
&__progress-size {
display: flex;
align-items: center;
gap: calc(globals.$spacing-unit);
font-size: 13px;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
}
&__progress-status {
font-size: 13px;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
}
&__progress-time {
display: flex;
align-items: center;
gap: calc(globals.$spacing-unit);
font-size: 13px;
color: globals.$muted-color;
}
&__hero-stats {
display: flex;
gap: calc(globals.$spacing-unit * 4);
padding: calc(globals.$spacing-unit * 2);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(26, 26, 26, 0.1);
backdrop-filter: blur(8px);
margin-top: calc(globals.$spacing-unit * 2);
}
&__stats-column {
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit * 2);
min-width: 200px;
padding-right: calc(globals.$spacing-unit * 2);
border-right: 1px solid rgba(255, 255, 255, 0.1);
align-self: flex-start;
}
&__speed-chart {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
&__speed-chart-canvas {
width: 100%;
height: 80px;
image-rendering: crisp-edges;
}
&__stat-item {
display: flex;
align-items: flex-end;
gap: calc(globals.$spacing-unit);
svg {
opacity: 0.8;
flex-shrink: 0;
}
}
&__stat-content {
display: flex;
justify-content: space-between;
gap: calc(globals.$spacing-unit / 2);
width: 100%;
}
&__stat-label {
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
font-size: 10px;
color: rgba(255, 255, 255, 0.6);
}
&__stat-value {
color: #ffffff;
font-weight: 700;
font-size: 11px;
line-height: 1.2;
}
&__simple-list {
width: 100%;
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit * 2);
margin: 0;
padding: 0;
list-style: none;
}
&__simple-card {
display: flex;
align-items: center;
gap: calc(globals.$spacing-unit * 2);
border-radius: 8px;
}
&__simple-thumbnail {
width: 200px;
height: 100px;
border-radius: 6px;
overflow: hidden;
flex-shrink: 0;
background-color: rgba(0, 0, 0, 0.3);
border: 1px solid globals.$border-color;
padding: 0;
cursor: pointer;
transition:
opacity 0.2s ease,
transform 0.2s ease;
&:hover {
opacity: 0.9;
}
&:focus,
&:focus-visible {
outline: 2px solid rgba(255, 255, 255, 0.5);
outline-offset: 2px;
}
img {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; padding: globals.$spacing-unit;
display: flex;
align-items: flex-end;
justify-content: flex-end;
}
&-backdrop {
width: 100%;
height: 100%;
background: linear-gradient(
0deg,
rgba(0, 0, 0, 0.8) 5%,
transparent 100%
);
display: flex;
overflow: hidden;
z-index: 1;
}
&-image {
width: 100%;
height: 100%;
position: absolute;
z-index: -1;
} }
} }
&__simple-info { &__right-content {
display: flex;
padding: calc(globals.$spacing-unit * 2);
flex: 1; flex: 1;
min-width: 0; gap: globals.$spacing-unit;
background: linear-gradient(90deg, transparent 20%, rgb(0 0 0 / 20%) 100%);
}
&__details {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: calc(globals.$spacing-unit / 1); flex: 1;
}
&__simple-title-button {
background: none;
border: none;
padding: 0;
cursor: pointer;
text-align: left;
width: 100%;
transition: opacity 0.2s ease;
&:focus,
&:focus-visible {
outline: none;
}
}
&__simple-title {
font-size: 16px;
font-weight: 600;
color: #ffffff;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__simple-meta {
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit * 1.5);
}
&__simple-meta-row {
display: flex;
align-items: center;
gap: calc(globals.$spacing-unit * 2);
font-size: 13px;
color: globals.$muted-color;
}
&__simple-size {
display: flex;
align-items: center;
gap: calc(globals.$spacing-unit / 2);
font-weight: 500;
}
&__simple-extracting {
display: flex;
align-items: center;
gap: calc(globals.$spacing-unit / 2);
font-weight: 500;
color: globals.$muted-color;
}
&__simple-seeding {
color: #4ade80;
font-weight: 600;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
&__simple-progress {
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit / 2);
width: 200px;
flex-shrink: 0;
}
&__simple-progress-text {
font-size: 12px;
font-weight: 600;
color: rgba(255, 255, 255, 0.8);
text-align: right;
}
&__simple-actions {
flex-shrink: 0;
display: flex;
justify-content: center; justify-content: center;
align-items: center; gap: calc(globals.$spacing-unit / 2);
gap: calc(globals.$spacing-unit); font-size: 14px;
} }
&__simple-menu-btn { &__actions {
padding: calc(globals.$spacing-unit); display: flex;
align-items: center;
gap: globals.$spacing-unit;
}
&__menu-button {
position: absolute;
top: 12px;
right: 12px;
border-radius: 50%;
border: none;
padding: 8px;
min-height: unset; min-height: unset;
} }
&__progress-wrapper { &__hydra-gradient {
flex: 1; background: linear-gradient(90deg, #01483c 0%, #0cf1ca 50%, #01483c 100%);
display: flex; box-shadow: 0px 0px 8px 0px rgba(12, 241, 202, 0.15);
flex-direction: column;
gap: calc(globals.$spacing-unit / 2);
}
&__progress-bar {
width: 100%; width: 100%;
height: 8px; position: absolute;
background-color: rgba(255, 255, 255, 0.08); bottom: 0;
border-radius: 4px; height: 2px;
overflow: hidden; z-index: 1;
margin-top: calc(globals.$spacing-unit / 2);
&--small {
height: 6px;
}
}
&__progress-fill {
height: 100%;
background-color: #fff;
transition: width 0.3s ease;
border-radius: 4px;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@
.downloads { .downloads {
&__container { &__container {
display: flex; display: flex;
padding: calc(globals.$spacing-unit * 3);
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
} }

View File

@@ -8,7 +8,6 @@ import {
import useEmblaCarousel from "embla-carousel-react"; import useEmblaCarousel from "embla-carousel-react";
import { gameDetailsContext } from "@renderer/context"; import { gameDetailsContext } from "@renderer/context";
import { useAppSelector } from "@renderer/hooks"; import { useAppSelector } from "@renderer/hooks";
import { VideoPlayer } from "./video-player";
import "./gallery-slider.scss"; import "./gallery-slider.scss";
export function GallerySlider() { export function GallerySlider() {
@@ -107,6 +106,8 @@ export function GallerySlider() {
if (shopDetails?.movies) { if (shopDetails?.movies) {
shopDetails.movies.forEach((video, index) => { shopDetails.movies.forEach((video, index) => {
// 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 videoSrc: string | undefined;
let videoType: string | undefined; let videoType: string | undefined;
@@ -120,9 +121,11 @@ export function GallerySlider() {
videoSrc = video.dash_av1; videoSrc = video.dash_av1;
videoType = "application/dash+xml"; videoType = "application/dash+xml";
} else if (video.mp4?.max) { } else if (video.mp4?.max) {
// Fallback to old format
videoSrc = video.mp4.max; videoSrc = video.mp4.max;
videoType = "video/mp4"; videoType = "video/mp4";
} else if (video.webm?.max) { } else if (video.webm?.max) {
// Fallback to webm if mp4 is not available
videoSrc = video.webm.max; videoSrc = video.webm.max;
videoType = "video/webm"; videoType = "video/webm";
} }
@@ -188,17 +191,19 @@ export function GallerySlider() {
{mediaItems.map((item) => ( {mediaItems.map((item) => (
<div key={item.id} className="gallery-slider__slide"> <div key={item.id} className="gallery-slider__slide">
{item.type === "video" ? ( {item.type === "video" ? (
<VideoPlayer <video
videoSrc={item.videoSrc}
videoType={item.videoType}
poster={item.poster}
autoplay={autoplayEnabled}
loop
muted
controls controls
className="gallery-slider__media" className="gallery-slider__media"
poster={item.poster}
loop
muted
autoPlay={autoplayEnabled}
tabIndex={-1} tabIndex={-1}
/> >
{item.videoSrc && (
<source src={item.videoSrc} type={item.videoType} />
)}
</video>
) : ( ) : (
<img <img
className="gallery-slider__media" className="gallery-slider__media"

View File

@@ -1,70 +0,0 @@
import { useRef } from "react";
import { useHlsVideo } from "@renderer/hooks";
interface VideoPlayerProps {
videoSrc?: string;
videoType?: string;
poster?: string;
autoplay?: boolean;
muted?: boolean;
loop?: boolean;
controls?: boolean;
tabIndex?: number;
className?: string;
}
export function VideoPlayer({
videoSrc,
videoType,
poster,
autoplay = false,
muted = true,
loop = false,
controls = true,
tabIndex = -1,
className,
}: VideoPlayerProps) {
const videoRef = useRef<HTMLVideoElement>(null);
const isHls = videoType === "application/x-mpegURL";
useHlsVideo(videoRef, {
videoSrc,
videoType,
autoplay,
muted,
loop,
});
if (isHls) {
return (
<video
ref={videoRef}
controls={controls}
className={className}
poster={poster}
loop={loop}
muted={muted}
autoPlay={autoplay}
tabIndex={tabIndex}
>
<track kind="captions" />
</video>
);
}
return (
<video
ref={videoRef}
controls={controls}
className={className}
poster={poster}
loop={loop}
muted={muted}
autoPlay={autoplay}
tabIndex={tabIndex}
>
{videoSrc && <source src={videoSrc} type={videoType} />}
<track kind="captions" />
</video>
);
}

View File

@@ -1,7 +1,7 @@
import { useContext, useRef, useState } from "react"; import { useContext, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Button, CheckboxField, Modal, TextField } from "@renderer/components"; import { Button, CheckboxField, Modal, TextField } from "@renderer/components";
import type { Game, LibraryGame, ShortcutLocation } from "@types"; import type { LibraryGame, ShortcutLocation } from "@types";
import { gameDetailsContext } from "@renderer/context"; import { gameDetailsContext } from "@renderer/context";
import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal"; import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
import { useDownload, useToast, useUserDetails } from "@renderer/hooks"; import { useDownload, useToast, useUserDetails } from "@renderer/hooks";
@@ -11,8 +11,6 @@ import { ChangeGamePlaytimeModal } from "./change-game-playtime-modal";
import { FileDirectoryIcon, FileIcon } from "@primer/octicons-react"; import { FileDirectoryIcon, FileIcon } from "@primer/octicons-react";
import SteamLogo from "@renderer/assets/steam-logo.svg?react"; import SteamLogo from "@renderer/assets/steam-logo.svg?react";
import { debounce } from "lodash-es"; import { debounce } from "lodash-es";
import { levelDBService } from "@renderer/services/leveldb.service";
import { getGameKey } from "@renderer/helpers";
import "./game-options-modal.scss"; import "./game-options-modal.scss";
import { logger } from "@renderer/logger"; import { logger } from "@renderer/logger";
@@ -77,19 +75,11 @@ export function GameOptionsModal({
const debounceUpdateLaunchOptions = useRef( const debounceUpdateLaunchOptions = useRef(
debounce(async (value: string) => { debounce(async (value: string) => {
const gameKey = getGameKey(game.shop, game.objectId); await window.electron.updateLaunchOptions(
const gameData = (await levelDBService.get( game.shop,
gameKey, game.objectId,
"games" value
)) as Game | null; );
if (gameData) {
const trimmedValue = value.trim();
const updated = {
...gameData,
launchOptions: trimmedValue ? trimmedValue : null,
};
await levelDBService.put(gameKey, updated, "games");
}
updateGame(); updateGame();
}, 1000) }, 1000)
).current; ).current;
@@ -223,16 +213,9 @@ export function GameOptionsModal({
const handleClearLaunchOptions = async () => { const handleClearLaunchOptions = async () => {
setLaunchOptions(""); setLaunchOptions("");
const gameKey = getGameKey(game.shop, game.objectId); window.electron
const gameData = (await levelDBService.get( .updateLaunchOptions(game.shop, game.objectId, null)
gameKey, .then(updateGame);
"games"
)) as Game | null;
if (gameData) {
const updated = { ...gameData, launchOptions: null };
await levelDBService.put(gameKey, updated, "games");
}
updateGame();
}; };
const shouldShowWinePrefixConfiguration = const shouldShowWinePrefixConfiguration =
@@ -273,15 +256,11 @@ export function GameOptionsModal({
) => { ) => {
setAutomaticCloudSync(event.target.checked); setAutomaticCloudSync(event.target.checked);
const gameKey = getGameKey(game.shop, game.objectId); await window.electron.toggleAutomaticCloudSync(
const gameData = (await levelDBService.get( game.shop,
gameKey, game.objectId,
"games" event.target.checked
)) as Game | null; );
if (gameData) {
const updated = { ...gameData, automaticCloudSync: event.target.checked };
await levelDBService.put(gameKey, updated, "games");
}
updateGame(); updateGame();
}; };

View File

@@ -15,7 +15,7 @@ import {
TextField, TextField,
CheckboxField, CheckboxField,
} from "@renderer/components"; } from "@renderer/components";
import type { DownloadSource, Game, GameRepack } from "@types"; import type { DownloadSource, GameRepack } from "@types";
import { DownloadSettingsModal } from "./download-settings-modal"; import { DownloadSettingsModal } from "./download-settings-modal";
import { gameDetailsContext } from "@renderer/context"; import { gameDetailsContext } from "@renderer/context";
@@ -23,8 +23,6 @@ import { Downloader } from "@shared";
import { orderBy } from "lodash-es"; import { orderBy } from "lodash-es";
import { useDate, useFeature, useAppDispatch } from "@renderer/hooks"; import { useDate, useFeature, useAppDispatch } from "@renderer/hooks";
import { clearNewDownloadOptions } from "@renderer/features"; import { clearNewDownloadOptions } from "@renderer/features";
import { levelDBService } from "@renderer/services/leveldb.service";
import { getGameKey } from "@renderer/helpers";
import "./repacks-modal.scss"; import "./repacks-modal.scss";
export interface RepacksModalProps { export interface RepacksModalProps {
@@ -100,11 +98,8 @@ export function RepacksModal({
useEffect(() => { useEffect(() => {
const fetchDownloadSources = async () => { const fetchDownloadSources = async () => {
const sources = (await levelDBService.values( const sources = await window.electron.getDownloadSources();
"downloadSources" setDownloadSources(sources);
)) as DownloadSource[];
const sorted = orderBy(sources, "createdAt", "desc");
setDownloadSources(sorted);
}; };
fetchDownloadSources(); fetchDownloadSources();
@@ -114,19 +109,10 @@ export function RepacksModal({
const fetchLastCheckTimestamp = async () => { const fetchLastCheckTimestamp = async () => {
setIsLoadingTimestamp(true); setIsLoadingTimestamp(true);
try { const timestamp = await window.electron.getDownloadSourcesSinceValue();
const timestamp = (await levelDBService.get(
"downloadSourcesSinceValue",
null,
"utf8"
)) as string | null;
setLastCheckTimestamp(timestamp); setLastCheckTimestamp(timestamp);
} catch { setIsLoadingTimestamp(false);
setLastCheckTimestamp(null);
} finally {
setIsLoadingTimestamp(false);
}
}; };
if (visible) { if (visible) {
@@ -140,20 +126,7 @@ export function RepacksModal({
game?.newDownloadOptionsCount && game?.newDownloadOptionsCount &&
game.newDownloadOptionsCount > 0 game.newDownloadOptionsCount > 0
) { ) {
const gameKey = getGameKey(game.shop, game.objectId); globalThis.electron.clearNewDownloadOptions(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}`; const gameId = `${game.shop}:${game.objectId}`;
dispatch(clearNewDownloadOptions({ gameId })); dispatch(clearNewDownloadOptions({ gameId }));
@@ -231,19 +204,9 @@ export function RepacksModal({
return false; return false;
} }
try { const lastCheckUtc = new Date(lastCheckTimestamp).toISOString();
const lastCheckDate = new Date(lastCheckTimestamp);
if (isNaN(lastCheckDate.getTime())) { return repack.createdAt > lastCheckUtc;
return false;
}
const lastCheckUtc = lastCheckDate.toISOString();
return repack.createdAt > lastCheckUtc;
} catch {
return false;
}
}; };
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false); const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false);

View File

@@ -175,7 +175,7 @@
&__review-score-stars { &__review-score-stars {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 6px;
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
border-radius: 8px; border-radius: 8px;
padding: 2px 6px; padding: 2px 6px;
@@ -188,6 +188,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-end; align-items: flex-end;
align-self: flex-start;
} }
&__review-star { &__review-star {
@@ -219,7 +220,7 @@
&__review-playtime { &__review-playtime {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 6px;
color: rgba(255, 255, 255, 0.7); color: rgba(255, 255, 255, 0.7);
font-size: 11px; font-size: 11px;
font-weight: 500; font-weight: 500;

View File

@@ -70,11 +70,13 @@ export function ReviewItem({
const getBaseLanguage = (lang: string | null) => lang?.split("-")[0] || ""; const getBaseLanguage = (lang: string | null) => lang?.split("-")[0] || "";
const userLanguage = getBaseLanguage(i18n.language);
const isDifferentLanguage = const isDifferentLanguage =
getBaseLanguage(review.detectedLanguage) !== getBaseLanguage(i18n.language); getBaseLanguage(review.detectedLanguage) !== userLanguage;
const needsTranslation = const needsTranslation =
!isOwnReview && isDifferentLanguage && review.translations[i18n.language]; !isOwnReview && isDifferentLanguage && review.translations[userLanguage];
const getLanguageName = (languageCode: string | null) => { const getLanguageName = (languageCode: string | null) => {
if (!languageCode) return ""; if (!languageCode) return "";
@@ -104,7 +106,7 @@ export function ReviewItem({
// Determine which content to show - always show original for own reviews // Determine which content to show - always show original for own reviews
const displayContent = needsTranslation const displayContent = needsTranslation
? review.translations[i18n.language] ? review.translations[userLanguage]
: review.reviewHtml; : review.reviewHtml;
if (isBlocked && !isVisible) { if (isBlocked && !isVisible) {
@@ -123,6 +125,8 @@ export function ReviewItem({
); );
} }
console.log(review);
return ( return (
<div className="game-details__review-item"> <div className="game-details__review-item">
<div className="game-details__review-header"> <div className="game-details__review-header">

View File

@@ -1,13 +1,11 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { levelDBService } from "@renderer/services/leveldb.service";
import { orderBy } from "lodash-es";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import Skeleton, { SkeletonTheme } from "react-loading-skeleton"; import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
import { Button, GameCard, Hero } from "@renderer/components"; import { Button, GameCard, Hero } from "@renderer/components";
import type { DownloadSource, ShopAssets, Steam250Game } from "@types"; import type { ShopAssets, Steam250Game } from "@types";
import flameIconStatic from "@renderer/assets/icons/flame-static.png"; import flameIconStatic from "@renderer/assets/icons/flame-static.png";
import flameIconAnimated from "@renderer/assets/icons/flame-animated.gif"; import flameIconAnimated from "@renderer/assets/icons/flame-animated.gif";
@@ -42,10 +40,7 @@ export default function Home() {
setCurrentCatalogueCategory(category); setCurrentCatalogueCategory(category);
setIsLoading(true); setIsLoading(true);
const sources = (await levelDBService.values( const downloadSources = await window.electron.getDownloadSources();
"downloadSources"
)) as DownloadSource[];
const downloadSources = orderBy(sources, "createdAt", "desc");
const params = { const params = {
take: 12, take: 12,

View File

@@ -76,13 +76,7 @@ export default function Library() {
switch (filterBy) { switch (filterBy) {
case "recently_played": case "recently_played":
filtered = library filtered = library.filter((game) => game.lastTimePlayed !== null);
.filter((game) => game.lastTimePlayed !== null)
.sort(
(a: any, b: any) =>
new Date(b.lastTimePlayed).getTime() -
new Date(a.lastTimePlayed).getTime()
);
break; break;
case "favorites": case "favorites":
filtered = library.filter((game) => game.favorite); filtered = library.filter((game) => game.favorite);

View File

@@ -8,7 +8,6 @@ import { useState } from "react";
import { DeleteThemeModal } from "../modals/delete-theme-modal"; import { DeleteThemeModal } from "../modals/delete-theme-modal";
import { injectCustomCss, removeCustomCss } from "@renderer/helpers"; import { injectCustomCss, removeCustomCss } from "@renderer/helpers";
import { THEME_WEB_STORE_URL } from "@renderer/constants"; import { THEME_WEB_STORE_URL } from "@renderer/constants";
import { levelDBService } from "@renderer/services/leveldb.service";
interface ThemeCardProps { interface ThemeCardProps {
theme: Theme; theme: Theme;
@@ -23,18 +22,11 @@ export const ThemeCard = ({ theme, onListUpdated }: ThemeCardProps) => {
const handleSetTheme = async () => { const handleSetTheme = async () => {
try { try {
const currentTheme = (await levelDBService.get( const currentTheme = await window.electron.getCustomThemeById(theme.id);
theme.id,
"themes"
)) as Theme | null;
if (!currentTheme) return; if (!currentTheme) return;
const allThemes = (await levelDBService.values("themes")) as { const activeTheme = await window.electron.getActiveCustomTheme();
id: string;
isActive?: boolean;
}[];
const activeTheme = allThemes.find((t) => t.isActive);
if (activeTheme) { if (activeTheme) {
removeCustomCss(); removeCustomCss();

View File

@@ -10,7 +10,6 @@ import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import { useCallback } from "react"; import { useCallback } from "react";
import { generateUUID } from "@renderer/helpers"; import { generateUUID } from "@renderer/helpers";
import { levelDBService } from "@renderer/services/leveldb.service";
import "./modals.scss"; import "./modals.scss";
@@ -91,7 +90,7 @@ export function AddThemeModal({
updatedAt: new Date(), updatedAt: new Date(),
}; };
await levelDBService.put(theme.id, theme, "themes"); await window.electron.addCustomTheme(theme);
onThemeAdded(); onThemeAdded();
onClose(); onClose();
reset(); reset();

View File

@@ -3,7 +3,6 @@ import { Modal } from "@renderer/components/modal/modal";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import "./modals.scss"; import "./modals.scss";
import { removeCustomCss } from "@renderer/helpers"; import { removeCustomCss } from "@renderer/helpers";
import { levelDBService } from "@renderer/services/leveldb.service";
interface DeleteAllThemesModalProps { interface DeleteAllThemesModalProps {
visible: boolean; visible: boolean;
@@ -19,16 +18,13 @@ export const DeleteAllThemesModal = ({
const { t } = useTranslation("settings"); const { t } = useTranslation("settings");
const handleDeleteAllThemes = async () => { const handleDeleteAllThemes = async () => {
const allThemes = (await levelDBService.values("themes")) as { const activeTheme = await window.electron.getActiveCustomTheme();
isActive?: boolean;
}[];
const activeTheme = allThemes.find((theme) => theme.isActive);
if (activeTheme) { if (activeTheme) {
removeCustomCss(); removeCustomCss();
} }
await levelDBService.clear("themes"); await window.electron.deleteAllCustomThemes();
await window.electron.closeEditorWindow(); await window.electron.closeEditorWindow();
onClose(); onClose();
onThemesDeleted(); onThemesDeleted();

View File

@@ -3,7 +3,6 @@ import { Modal } from "@renderer/components/modal/modal";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import "./modals.scss"; import "./modals.scss";
import { removeCustomCss } from "@renderer/helpers"; import { removeCustomCss } from "@renderer/helpers";
import { levelDBService } from "@renderer/services/leveldb.service";
interface DeleteThemeModalProps { interface DeleteThemeModalProps {
visible: boolean; visible: boolean;
@@ -29,7 +28,7 @@ export const DeleteThemeModal = ({
removeCustomCss(); removeCustomCss();
} }
await levelDBService.del(themeId, "themes"); await window.electron.deleteCustomTheme(themeId);
await window.electron.closeEditorWindow(themeId); await window.electron.closeEditorWindow(themeId);
onThemeDeleted(); onThemeDeleted();
}; };

View File

@@ -11,7 +11,6 @@ import {
import { useToast } from "@renderer/hooks"; import { useToast } from "@renderer/hooks";
import { THEME_WEB_STORE_URL } from "@renderer/constants"; import { THEME_WEB_STORE_URL } from "@renderer/constants";
import { logger } from "@renderer/logger"; import { logger } from "@renderer/logger";
import { levelDBService } from "@renderer/services/leveldb.service";
interface ImportThemeModalProps { interface ImportThemeModalProps {
visible: boolean; visible: boolean;
@@ -46,12 +45,9 @@ export const ImportThemeModal = ({
}; };
try { try {
await levelDBService.put(theme.id, theme, "themes"); await window.electron.addCustomTheme(theme);
const currentTheme = (await levelDBService.get( const currentTheme = await window.electron.getCustomThemeById(theme.id);
theme.id,
"themes"
)) as Theme | null;
if (!currentTheme) return; if (!currentTheme) return;
@@ -65,11 +61,7 @@ export const ImportThemeModal = ({
logger.error("Failed to import theme sound", soundError); logger.error("Failed to import theme sound", soundError);
} }
const allThemes = (await levelDBService.values("themes")) as { const activeTheme = await window.electron.getActiveCustomTheme();
id: string;
isActive?: boolean;
}[];
const activeTheme = allThemes.find((t) => t.isActive);
if (activeTheme) { if (activeTheme) {
removeCustomCss(); removeCustomCss();

View File

@@ -5,7 +5,6 @@ import type { Theme } from "@types";
import { ImportThemeModal } from "./modals/import-theme-modal"; import { ImportThemeModal } from "./modals/import-theme-modal";
import { settingsContext } from "@renderer/context"; import { settingsContext } from "@renderer/context";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { levelDBService } from "@renderer/services/leveldb.service";
interface SettingsAppearanceProps { interface SettingsAppearanceProps {
appearance: { appearance: {
@@ -32,7 +31,7 @@ export function SettingsAppearance({
const navigate = useNavigate(); const navigate = useNavigate();
const loadThemes = useCallback(async () => { const loadThemes = useCallback(async () => {
const themesList = (await levelDBService.values("themes")) as Theme[]; const themesList = await window.electron.getAllCustomThemes();
setThemes(themesList); setThemes(themesList);
}, []); }, []);

View File

@@ -21,8 +21,6 @@ import { DownloadSourceStatus } from "@shared";
import { settingsContext } from "@renderer/context"; import { settingsContext } from "@renderer/context";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { setFilters, clearFilters } from "@renderer/features"; import { setFilters, clearFilters } from "@renderer/features";
import { levelDBService } from "@renderer/services/leveldb.service";
import { orderBy } from "lodash-es";
import "./settings-download-sources.scss"; import "./settings-download-sources.scss";
import { logger } from "@renderer/logger"; import { logger } from "@renderer/logger";
@@ -54,11 +52,8 @@ export function SettingsDownloadSources() {
useEffect(() => { useEffect(() => {
const fetchDownloadSources = async () => { const fetchDownloadSources = async () => {
const sources = (await levelDBService.values( const sources = await window.electron.getDownloadSources();
"downloadSources" setDownloadSources(sources);
)) as DownloadSource[];
const sorted = orderBy(sources, "createdAt", "desc");
setDownloadSources(sorted);
}; };
fetchDownloadSources(); fetchDownloadSources();
@@ -78,11 +73,8 @@ export function SettingsDownloadSources() {
const intervalId = setInterval(async () => { const intervalId = setInterval(async () => {
try { try {
await window.electron.syncDownloadSources(); await window.electron.syncDownloadSources();
const sources = (await levelDBService.values( const sources = await window.electron.getDownloadSources();
"downloadSources" setDownloadSources(sources);
)) as DownloadSource[];
const sorted = orderBy(sources, "createdAt", "desc");
setDownloadSources(sorted);
} catch (error) { } catch (error) {
logger.error("Failed to fetch download sources:", error); logger.error("Failed to fetch download sources:", error);
} }
@@ -96,11 +88,8 @@ export function SettingsDownloadSources() {
try { try {
await window.electron.removeDownloadSource(false, downloadSource.id); await window.electron.removeDownloadSource(false, downloadSource.id);
const sources = (await levelDBService.values( const sources = await window.electron.getDownloadSources();
"downloadSources" setDownloadSources(sources);
)) as DownloadSource[];
const sorted = orderBy(sources, "createdAt", "desc");
setDownloadSources(sorted);
showSuccessToast(t("removed_download_source")); showSuccessToast(t("removed_download_source"));
} catch (error) { } catch (error) {
logger.error("Failed to remove download source:", error); logger.error("Failed to remove download source:", error);
@@ -114,11 +103,8 @@ export function SettingsDownloadSources() {
try { try {
await window.electron.removeDownloadSource(true); await window.electron.removeDownloadSource(true);
const sources = (await levelDBService.values( const sources = await window.electron.getDownloadSources();
"downloadSources" setDownloadSources(sources);
)) as DownloadSource[];
const sorted = orderBy(sources, "createdAt", "desc");
setDownloadSources(sorted);
showSuccessToast(t("removed_all_download_sources")); showSuccessToast(t("removed_all_download_sources"));
} catch (error) { } catch (error) {
logger.error("Failed to remove all download sources:", error); logger.error("Failed to remove all download sources:", error);
@@ -130,11 +116,8 @@ export function SettingsDownloadSources() {
const handleAddDownloadSource = async () => { const handleAddDownloadSource = async () => {
try { try {
const sources = (await levelDBService.values( const sources = await window.electron.getDownloadSources();
"downloadSources" setDownloadSources(sources);
)) as DownloadSource[];
const sorted = orderBy(sources, "createdAt", "desc");
setDownloadSources(sorted);
} catch (error) { } catch (error) {
logger.error("Failed to refresh download sources:", error); logger.error("Failed to refresh download sources:", error);
} }
@@ -144,11 +127,8 @@ export function SettingsDownloadSources() {
setIsSyncingDownloadSources(true); setIsSyncingDownloadSources(true);
try { try {
await window.electron.syncDownloadSources(); await window.electron.syncDownloadSources();
const sources = (await levelDBService.values( const sources = await window.electron.getDownloadSources();
"downloadSources" setDownloadSources(sources);
)) as DownloadSource[];
const sorted = orderBy(sources, "createdAt", "desc");
setDownloadSources(sorted);
showSuccessToast(t("download_sources_synced_successfully")); showSuccessToast(t("download_sources_synced_successfully"));
} finally { } finally {

View File

@@ -16,7 +16,6 @@ import { injectCustomCss, getAchievementSoundVolume } from "@renderer/helpers";
import { AchievementNotificationItem } from "@renderer/components/achievements/notification/achievement-notification"; import { AchievementNotificationItem } from "@renderer/components/achievements/notification/achievement-notification";
import { generateAchievementCustomNotificationTest } from "@shared"; import { generateAchievementCustomNotificationTest } from "@shared";
import { CollapsedMenu } from "@renderer/components/collapsed-menu/collapsed-menu"; import { CollapsedMenu } from "@renderer/components/collapsed-menu/collapsed-menu";
import { levelDBService } from "@renderer/services/leveldb.service";
import app from "../../app.scss?inline"; import app from "../../app.scss?inline";
import styles from "../../components/achievements/notification/achievement-notification.scss?inline"; import styles from "../../components/achievements/notification/achievement-notification.scss?inline";
import root from "react-shadow"; import root from "react-shadow";
@@ -65,16 +64,15 @@ export default function ThemeEditor() {
useEffect(() => { useEffect(() => {
if (themeId) { if (themeId) {
levelDBService.get(themeId, "themes").then((loadedTheme) => { window.electron.getCustomThemeById(themeId).then((loadedTheme) => {
const theme = loadedTheme as Theme | null; if (loadedTheme) {
if (theme) { setTheme(loadedTheme);
setTheme(theme); setCode(loadedTheme.code);
setCode(theme.code); if (loadedTheme.originalSoundPath) {
if (theme.originalSoundPath) { setSoundPath(loadedTheme.originalSoundPath);
setSoundPath(theme.originalSoundPath);
} }
if (shadowRootRef) { if (shadowRootRef) {
injectCustomCss(theme.code, shadowRootRef); injectCustomCss(loadedTheme.code, shadowRootRef);
} }
} }
}); });
@@ -134,10 +132,7 @@ export default function ThemeEditor() {
if (filePaths && filePaths.length > 0) { if (filePaths && filePaths.length > 0) {
const originalPath = filePaths[0]; const originalPath = filePaths[0];
await window.electron.copyThemeAchievementSound(theme.id, originalPath); await window.electron.copyThemeAchievementSound(theme.id, originalPath);
const updatedTheme = (await levelDBService.get( const updatedTheme = await window.electron.getCustomThemeById(theme.id);
theme.id,
"themes"
)) as Theme | null;
if (updatedTheme) { if (updatedTheme) {
setTheme(updatedTheme); setTheme(updatedTheme);
if (updatedTheme.originalSoundPath) { if (updatedTheme.originalSoundPath) {
@@ -151,10 +146,7 @@ export default function ThemeEditor() {
if (!theme) return; if (!theme) return;
await window.electron.removeThemeAchievementSound(theme.id); await window.electron.removeThemeAchievementSound(theme.id);
const updatedTheme = (await levelDBService.get( const updatedTheme = await window.electron.getCustomThemeById(theme.id);
theme.id,
"themes"
)) as Theme | null;
if (updatedTheme) { if (updatedTheme) {
setTheme(updatedTheme); setTheme(updatedTheme);
} }

View File

@@ -1,36 +0,0 @@
class LevelDBService {
get(
key: string,
sublevelName?: string | null,
valueEncoding?: "json" | "utf8"
): Promise<unknown> {
return window.electron.leveldb.get(key, sublevelName, valueEncoding);
}
put(
key: string,
value: unknown,
sublevelName?: string | null,
valueEncoding?: "json" | "utf8"
): Promise<void> {
return window.electron.leveldb.put(key, value, sublevelName, valueEncoding);
}
del(key: string, sublevelName?: string | null): Promise<void> {
return window.electron.leveldb.del(key, sublevelName);
}
clear(sublevelName: string): Promise<void> {
return window.electron.leveldb.clear(sublevelName);
}
values(sublevelName: string): Promise<unknown[]> {
return window.electron.leveldb.values(sublevelName);
}
iterator(sublevelName: string): Promise<[string, unknown][]> {
return window.electron.leveldb.iterator(sublevelName);
}
}
export const levelDBService = new LevelDBService();

View File

@@ -20,8 +20,6 @@ export interface Auth {
accessToken: string; accessToken: string;
refreshToken: string; refreshToken: string;
tokenExpirationTimestamp: number; tokenExpirationTimestamp: number;
featurebaseJwt: string;
workwondersJwt: string;
} }
export interface User { export interface User {

View File

@@ -14,7 +14,7 @@ export interface SteamVideoSource {
"480": string; "480": string;
} }
export interface SteamMovie { export interface SteamMovies {
id: number; id: number;
dash_av1?: string; dash_av1?: string;
dash_h264?: string; dash_h264?: string;
@@ -34,7 +34,7 @@ export interface SteamAppDetails {
short_description: string; short_description: string;
publishers: string[]; publishers: string[];
genres: SteamGenre[]; genres: SteamGenre[];
movies?: SteamMovie[]; movies?: SteamMovies[];
supported_languages: string; supported_languages: string;
screenshots?: SteamScreenshot[]; screenshots?: SteamScreenshot[];
pc_requirements: { pc_requirements: {

View File

@@ -5690,11 +5690,6 @@ hasown@^2.0.2:
dependencies: dependencies:
function-bind "^1.1.2" function-bind "^1.1.2"
hls.js@^1.5.12:
version "1.6.15"
resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-1.6.15.tgz#9ce13080d143a9bc9b903fb43f081e335b8321e5"
integrity sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==
hoist-non-react-statics@^3.3.2: hoist-non-react-statics@^3.3.2:
version "3.3.2" version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
@@ -6210,9 +6205,9 @@ jiti@^2.6.1:
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
js-yaml@^4.1.0: js-yaml@^4.1.0:
version "4.1.1" version "4.1.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
dependencies: dependencies:
argparse "^2.0.1" argparse "^2.0.1"
@@ -6330,7 +6325,7 @@ jsonwebtoken@^9.0.2:
object.assign "^4.1.4" object.assign "^4.1.4"
object.values "^1.1.6" object.values "^1.1.6"
jwa@^1.4.2: jwa@^1.4.1:
version "1.4.2" version "1.4.2"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.2.tgz#16011ac6db48de7b102777e57897901520eec7b9" resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.2.tgz#16011ac6db48de7b102777e57897901520eec7b9"
integrity sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw== integrity sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==
@@ -6340,11 +6335,11 @@ jwa@^1.4.2:
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
jws@^3.2.2: jws@^3.2.2:
version "3.2.3" version "3.2.2"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.3.tgz#5ac0690b460900a27265de24520526853c0b8ca1" resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
integrity sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g== integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
dependencies: dependencies:
jwa "^1.4.2" jwa "^1.4.1"
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
keyv@^4.0.0, keyv@^4.5.3: keyv@^4.0.0, keyv@^4.5.3:
@@ -8523,10 +8518,10 @@ tar@^6.0.5, tar@^6.1.11, tar@^6.1.12, tar@^6.2.1:
mkdirp "^1.0.3" mkdirp "^1.0.3"
yallist "^4.0.0" yallist "^4.0.0"
tar@^7.5.2: tar@^7.4.3:
version "7.5.2" version "7.5.1"
resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.2.tgz#115c061495ec51ff3c6745ff8f6d0871c5b1dedc" resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.1.tgz#750a8bd63b7c44c1848e7bf982260a083cf747c9"
integrity sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg== integrity sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==
dependencies: dependencies:
"@isaacs/fs-minipass" "^4.0.0" "@isaacs/fs-minipass" "^4.0.0"
chownr "^3.0.0" chownr "^3.0.0"