Compare commits

..

4 Commits

Author SHA1 Message Date
Chubby Granny Chaser
379c211472 feat: merge with v3.7.4 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
96 changed files with 765 additions and 2952 deletions

View File

@@ -28,26 +28,6 @@
- Use async/await instead of promises when possible
- 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
- Keep comments concise and purposeful; avoid verbose explanations.

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "hydralauncher",
"version": "3.7.6",
"version": "3.7.4",
"description": "Hydra",
"main": "./out/main/index.js",
"author": "Los Broxas",
@@ -63,14 +63,12 @@
"embla-carousel-react": "^8.6.0",
"file-type": "^20.5.0",
"framer-motion": "^12.15.0",
"hls.js": "^1.5.12",
"i18next": "^23.11.2",
"i18next-browser-languagedetector": "^7.2.1",
"jsdom": "^24.0.0",
"jsonwebtoken": "^9.0.2",
"lodash-es": "^4.17.21",
"lucide-react": "^0.544.0",
"node-7z": "^3.0.0",
"parse-torrent": "^11.0.18",
"rc-virtual-list": "^3.18.3",
"react-dnd": "^16.0.1",
@@ -86,7 +84,7 @@
"sound-play": "^1.1.0",
"steam-shortcut-editor": "https://github.com/hydralauncher/steam-shortcut-editor",
"sudo-prompt": "^9.2.1",
"tar": "^7.5.2",
"tar": "^7.4.3",
"tough-cookie": "^5.1.1",
"user-agents": "^1.1.387",
"uuid": "^13.0.0",

View File

@@ -96,7 +96,7 @@
"search_library": "Search library",
"recent_searches": "Recent Searches",
"suggestions": "Suggestions",
"clear_history": "Clear",
"clear_history": "Clear history",
"remove_from_history": "Remove from history",
"loading": "Loading...",
"no_results": "No results",
@@ -115,7 +115,6 @@
"downloading": "Downloading {{title}}… ({{percentage}} complete) - Completion {{eta}} - {{speed}}",
"calculating_eta": "Downloading {{title}}… ({{percentage}} complete) - Calculating remaining time…",
"checking_files": "Checking {{title}} files… ({{percentage}} complete)",
"extracting": "Extracting {{title}}… ({{percentage}} complete)",
"installing_common_redist": "{{log}}…",
"installation_complete": "Installation complete",
"installation_complete_message": "Common redistributables installed successfully"
@@ -203,7 +202,6 @@
"danger_zone_section_description": "Remove this game from your library or the files downloaded by Hydra",
"download_in_progress": "Download in progress",
"download_paused": "Download paused",
"extracting": "Extracting",
"last_downloaded_option": "Last downloaded option",
"new_download_option": "New",
"create_steam_shortcut": "Create Steam shortcut",
@@ -416,11 +414,7 @@
"resume_seeding": "Resume seeding",
"options": "Manage",
"extract": "Extract files",
"extracting": "Extracting files…",
"delete_archive_title": "Would you like to delete {{fileName}}?",
"delete_archive_description": "The file has been successfully extracted and it's no longer needed.",
"yes": "Yes",
"no": "No"
"extracting": "Extracting files…"
},
"settings": {
"downloads_path": "Downloads path",

View File

@@ -93,16 +93,8 @@
},
"header": {
"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",
"catalogue": "Catálogo",
"library": "Librería",
"downloads": "Descargas",
"search_results": "Resultados de búsqueda",
"settings": "Ajustes",
@@ -370,6 +362,7 @@
"write_review_placeholder": "Compartí tus pensamientos sobre este juego...",
"yes": "Si",
"you_seemed_to_enjoy_this_game": "Parece que has disfrutado de este juego",
"review_played_for": "Jugado por",
"language": "Idioma",
"caption": "Subtítulo",
"audio": "Audio",
@@ -458,7 +451,6 @@
"description_confirmation_delete_all_sources": "Vas a eliminar todas las fuentes de descargas",
"button_delete_all_sources": "Eliminar todo",
"added_download_source": "Añadir fuente de descarga",
"adding": "Añadiendo…",
"download_sources_synced": "Todas las fuentes de descarga están sincronizadas",
"insert_valid_json_url": "Introducí una URL de json válida",
"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.",
"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",
"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"
},
"notifications": {

View File

@@ -93,19 +93,11 @@
},
"header": {
"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",
"library": "Biblioteca",
"downloads": "Downloads",
"search_results": "Resultados da busca",
"settings": "Ajustes",
"home": "Início",
"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."
},
@@ -115,7 +107,6 @@
"downloading": "Baixando {{title}}… ({{percentage}} concluído) - Conclusão {{eta}} - {{speed}}",
"calculating_eta": "Baixando {{title}}… ({{percentage}} concluído) - Calculando tempo restante…",
"checking_files": "Verificando arquivos de {{title}}…",
"extracting": "Extraindo {{title}}… ({{percentage}} concluído)",
"installing_common_redist": "{{log}}…",
"installation_complete": "Instalação concluída",
"installation_complete_message": "Componentes recomendados instalados com sucesso"
@@ -191,7 +182,6 @@
"danger_zone_section_description": "Remova o jogo da sua biblioteca ou os arquivos que foram baixados pelo Hydra",
"download_in_progress": "Download em andamento",
"download_paused": "Download pausado",
"extracting": "Extraindo",
"last_downloaded_option": "Última opção baixada",
"new_download_option": "Novo",
"create_steam_shortcut": "Criar atalho na Steam",
@@ -321,6 +311,7 @@
"show_more": "Mostrar mais",
"show_less": "Mostrar menos",
"reviews": "Avaliações",
"review_played_for": "Jogou por",
"leave_a_review": "Deixar uma Avaliação",
"write_review_placeholder": "Compartilhe seus pensamentos sobre este jogo...",
"sort_newest": "Mais Recentes",
@@ -404,11 +395,7 @@
"resume_seeding": "Semear",
"options": "Gerenciar",
"extract": "Extrair arquivos",
"extracting": "Extraindo arquivos…",
"delete_archive_title": "Deseja deletar {{fileName}}?",
"delete_archive_description": "O arquivo foi extraído com sucesso e não é mais necessário.",
"yes": "Sim",
"no": "Não"
"extracting": "Extraindo arquivos…"
},
"settings": {
"downloads_path": "Diretório dos downloads",

View File

@@ -30,19 +30,11 @@
},
"header": {
"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",
"library": "Biblioteca",
"downloads": "Transferências",
"search_results": "Resultados da pesquisa",
"settings": "Definições",
"home": "Início",
"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."
},
@@ -190,6 +182,7 @@
"game_added_to_favorites": "Jogo adicionado aos favoritos",
"create_start_menu_shortcut": "Criar atalho no Menu Iniciar",
"review_from_blocked_user": "Avaliação de utilizador bloqueado",
"review_played_for": "Jogaste por",
"show": "Mostrar",
"hide": "Ocultar",
"review_played_for": "Jogado por"

View File

@@ -93,16 +93,8 @@
},
"header": {
"search": "Поиск",
"search_library": "Поиск в библиотеке",
"recent_searches": "Недавние поиски",
"suggestions": "Предложения",
"clear_history": "Очистить",
"remove_from_history": "Удалить из истории",
"loading": "Загрузка...",
"no_results": "Нет результатов",
"home": "Главная",
"catalogue": "Каталог",
"library": "Библиотека",
"downloads": "Загрузки",
"search_results": "Результаты поиска",
"settings": "Настройки",
@@ -233,6 +225,7 @@
"show_more": "Показать больше",
"show_less": "Показать меньше",
"reviews": "Отзывы",
"review_played_for": "Играно",
"leave_a_review": "Оставить отзыв",
"write_review_placeholder": "Поделитесь своими мыслями об этой игре...",
"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 { ipcMain } from "electron";
import "./auth";
import "./autoupdater";
import "./catalogue";
import "./cloud-save";
import "./download-sources";
import "./hardware";
import "./library";
import "./leveldb";
import "./misc";
import "./notifications";
import "./profile";
import "./themes";
import "./torrenting";
import "./user";
import "./user-preferences";
import "./catalogue/get-game-shop-details";
import "./catalogue/get-random-game";
import "./catalogue/get-game-stats";
import "./hardware/get-disk-free-space";
import "./hardware/check-folder-write-permission";
import "./library/add-game-to-library";
import "./library/add-custom-game-to-library";
import "./library/update-custom-game";
import "./library/update-game-custom-assets";
import "./library/add-game-to-favorites";
import "./library/remove-game-from-favorites";
import "./library/toggle-game-pin";
import "./library/create-game-shortcut";
import "./library/close-game";
import "./library/delete-game-folder";
import "./library/get-game-by-object-id";
import "./library/get-library";
import "./library/refresh-library-assets";
import "./library/extract-game-download";
import "./library/clear-new-download-options";
import "./library/open-game";
import "./library/open-game-executable-path";
import "./library/open-game-installer";
import "./library/open-game-installer-path";
import "./library/update-executable-path";
import "./library/update-launch-options";
import "./library/verify-executable-path";
import "./library/remove-game";
import "./library/remove-game-from-library";
import "./library/select-game-wine-prefix";
import "./library/reset-game-achievements";
import "./library/change-game-playtime";
import "./library/toggle-automatic-cloud-sync";
import "./library/get-default-wine-prefix-selection-path";
import "./library/cleanup-unused-assets";
import "./library/create-steam-shortcut";
import "./library/copy-custom-game-asset";
import "./misc/open-checkout";
import "./misc/open-external";
import "./misc/show-open-dialog";
import "./misc/show-item-in-folder";
import "./misc/install-common-redist";
import "./misc/can-install-common-redist";
import "./misc/save-temp-file";
import "./misc/delete-temp-file";
import "./misc/install-hydra-decky-plugin";
import "./misc/get-hydra-decky-plugin-info";
import "./misc/check-homebrew-folder-exists";
import "./misc/hydra-api-call";
import "./torrenting/cancel-game-download";
import "./torrenting/pause-game-download";
import "./torrenting/resume-game-download";
import "./torrenting/start-game-download";
import "./torrenting/pause-game-seed";
import "./torrenting/resume-game-seed";
import "./torrenting/check-debrid-availability";
import "./user-preferences/get-user-preferences";
import "./user-preferences/update-user-preferences";
import "./user-preferences/auto-launch";
import "./autoupdater/check-for-updates";
import "./autoupdater/restart-and-install-update";
import "./user-preferences/authenticate-real-debrid";
import "./user-preferences/authenticate-torbox";
import "./download-sources/add-download-source";
import "./download-sources/sync-download-sources";
import "./download-sources/get-download-sources-check-baseline";
import "./download-sources/get-download-sources-since-value";
import "./auth/sign-out";
import "./auth/open-auth-window";
import "./auth/get-session-hash";
import "./user/get-auth";
import "./user/get-unlocked-achievements";
import "./user/get-compared-unlocked-achievements";
import "./profile/get-me";
import "./profile/update-profile";
import "./profile/process-profile-image";
import "./profile/sync-friend-requests";
import "./cloud-save/download-game-artifact";
import "./cloud-save/get-game-backup-preview";
import "./cloud-save/upload-save-game";
import "./cloud-save/select-game-backup-path";
import "./notifications/publish-new-repacks-notification";
import "./notifications/update-achievement-notification-window";
import "./notifications/show-achievement-test-notification";
import "./themes/add-custom-theme";
import "./themes/delete-custom-theme";
import "./themes/get-all-custom-themes";
import "./themes/delete-all-custom-themes";
import "./themes/update-custom-theme";
import "./themes/open-editor-window";
import "./themes/get-custom-theme-by-id";
import "./themes/get-active-custom-theme";
import "./themes/close-editor-window";
import "./themes/toggle-custom-theme";
import "./themes/copy-theme-achievement-sound";
import "./themes/remove-theme-achievement-sound";
import "./themes/get-theme-sound-path";
import "./themes/get-theme-sound-data-url";
import "./themes/import-theme-sound-from-store";
import "./download-sources/remove-download-source";
import "./download-sources/get-download-sources";
import { isPortableVersion } from "@main/helpers";
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,23 +0,0 @@
import fs from "node:fs";
import { registerEvent } from "../register-event";
import { logger } from "@main/services";
const deleteArchive = async (
_event: Electron.IpcMainInvokeEvent,
filePath: string
) => {
try {
if (fs.existsSync(filePath)) {
await fs.promises.unlink(filePath);
logger.info(`Deleted archive: ${filePath}`);
return true;
}
return true;
} catch (err) {
logger.error(`Failed to delete archive: ${filePath}`, err);
return false;
}
};
registerEvent("deleteArchive", deleteArchive);

View File

@@ -22,7 +22,6 @@ const extractGameDownload = async (
await downloadsSublevel.put(gameKey, {
...download,
extracting: true,
extractionProgress: 0,
});
const gameFilesManager = new GameFilesManager(shop, objectId);

View File

@@ -1,33 +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-archive";
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);
if (
download &&
(download.status === "paused" || download.status === "active") &&
download.progress !== 1
) {
if (download?.status === "paused") {
await DownloadManager.pauseDownload();
for await (const [key, value] of downloadsSublevel.iterator()) {

View File

@@ -82,7 +82,6 @@ const startGameDownload = async (
queued: true,
extracting: false,
automaticallyExtract,
extractionProgress: 0,
};
try {

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
> => {
try {
const timestamp = await db.get(levelKeys.downloadSourcesCheckBaseline, {
valueEncoding: "utf8",
});
const timestamp = await db.get(levelKeys.downloadSourcesCheckBaseline);
return timestamp;
} catch (error) {
if (error instanceof Error && error.name === "NotFoundError") {
@@ -29,9 +27,7 @@ export const updateDownloadSourcesCheckBaseline = async (
timestamp: string
): Promise<void> => {
const utcTimestamp = new Date(timestamp).toISOString();
await db.put(levelKeys.downloadSourcesCheckBaseline, utcTimestamp, {
valueEncoding: "utf8",
});
await db.put(levelKeys.downloadSourcesCheckBaseline, utcTimestamp);
};
// 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
> => {
try {
const timestamp = await db.get(levelKeys.downloadSourcesSinceValue, {
valueEncoding: "utf8",
});
const timestamp = await db.get(levelKeys.downloadSourcesSinceValue);
return timestamp;
} catch (error) {
if (error instanceof Error && error.name === "NotFoundError") {
@@ -61,7 +55,5 @@ export const updateDownloadSourcesSinceValue = async (
timestamp: string
): Promise<void> => {
const utcTimestamp = new Date(timestamp).toISOString();
await db.put(levelKeys.downloadSourcesSinceValue, utcTimestamp, {
valueEncoding: "utf8",
});
await db.put(levelKeys.downloadSourcesSinceValue, utcTimestamp);
};

View File

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

View File

@@ -1,5 +1,5 @@
import { app } from "electron";
import Seven, { CommandLineSwitches } from "node-7z";
import cp from "node:child_process";
import path from "node:path";
import { logger } from "./logger";
@@ -9,17 +9,6 @@ export const binaryName = {
win32: "7z.exe",
};
export interface ExtractionProgress {
percent: number;
fileCount: number;
file: string;
}
export interface ExtractionResult {
success: boolean;
extractedFiles: string[];
}
export class SevenZip {
private static readonly binaryPath = app.isPackaged
? path.join(process.resourcesPath, binaryName[process.platform])
@@ -43,109 +32,43 @@ export class SevenZip {
cwd?: string;
passwords?: string[];
},
onProgress?: (progress: ExtractionProgress) => void
): Promise<ExtractionResult> {
return new Promise((resolve, reject) => {
const tryPassword = (index = -1) => {
const password = passwords[index] ?? "";
logger.info(
`Trying password "${password || "(empty)"}" on ${filePath}`
);
successCb: () => void,
errorCb: () => void
) {
const tryPassword = (index = -1) => {
const password = passwords[index] ?? "";
logger.info(`Trying password ${password} on ${filePath}`);
const extractedFiles: string[] = [];
let fileCount = 0;
const args = ["x", filePath, "-y", "-p" + password];
const options: CommandLineSwitches = {
$bin: this.binaryPath,
$progress: true,
yes: true,
password: password || undefined,
};
if (outputPath) {
args.push("-o" + outputPath);
}
if (outputPath) {
options.outputDir = outputPath;
const child = cp.execFile(this.binaryPath, args, {
cwd,
});
child.once("exit", (code) => {
if (code === 0) {
successCb();
return;
}
const stream = Seven.extractFull(filePath, outputPath || cwd || ".", {
...options,
$spawnOptions: cwd ? { cwd } : undefined,
});
stream.on("progress", (progress) => {
if (onProgress) {
onProgress({
percent: progress.percent,
fileCount: fileCount,
file: progress.fileCount?.toString() || "",
});
}
});
stream.on("data", (data) => {
if (data.file) {
extractedFiles.push(data.file);
fileCount++;
}
});
stream.on("end", () => {
if (index < passwords.length - 1) {
logger.info(
`Successfully extracted ${filePath} (${extractedFiles.length} files)`
`Failed to extract file: ${filePath} with password: ${password}. Trying next password...`
);
resolve({
success: true,
extractedFiles,
});
});
stream.on("error", (err) => {
logger.error(`Extraction error for ${filePath}:`, err);
tryPassword(index + 1);
} else {
logger.info(`Failed to extract file: ${filePath}`);
if (index < passwords.length - 1) {
logger.info(
`Failed to extract file: ${filePath} with password: "${password}". Trying next password...`
);
tryPassword(index + 1);
} else {
logger.error(
`Failed to extract file: ${filePath} after trying all passwords`
);
reject(new Error(`Failed to extract file: ${filePath}`));
}
});
};
tryPassword();
});
}
public static listFiles(
filePath: string,
password?: string
): Promise<string[]> {
return new Promise((resolve, reject) => {
const files: string[] = [];
const options: CommandLineSwitches = {
$bin: this.binaryPath,
password: password || undefined,
};
const stream = Seven.list(filePath, options);
stream.on("data", (data) => {
if (data.file) {
files.push(data.file);
errorCb();
}
});
};
stream.on("end", () => {
resolve(files);
});
stream.on("error", (err) => {
reject(err);
});
});
tryPassword();
}
}

View File

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

View File

@@ -74,16 +74,21 @@ export class DeckyPlugin {
await fs.promises.mkdir(extractPath, { recursive: true });
try {
await SevenZip.extractFile({
filePath: zipPath,
outputPath: extractPath,
});
logger.log(`Plugin extracted to: ${extractPath}`);
return extractPath;
} catch {
throw new Error("Failed to extract plugin");
}
return new Promise((resolve, reject) => {
SevenZip.extractFile(
{
filePath: zipPath,
outputPath: extractPath,
},
() => {
logger.log(`Plugin extracted to: ${extractPath}`);
resolve(extractPath);
},
() => {
reject(new Error("Failed to extract plugin"));
}
);
});
}
private static needsSudo(): boolean {

View File

@@ -20,7 +20,7 @@ import { RealDebridClient } from "./real-debrid";
import path from "path";
import { logger } from "../logger";
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
import { orderBy } from "lodash-es";
import { sortBy } from "lodash-es";
import { TorBoxClient } from "./torbox";
import { GameFilesManager } from "../game-files-manager";
import { HydraDebridClient } from "./hydra-debrid";
@@ -126,10 +126,21 @@ export class DownloadManager {
}
);
if (WindowManager.mainWindow && download) {
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
WindowManager.mainWindow.webContents.send(
"on-download-progress",
JSON.parse(
JSON.stringify({
...status,
game,
})
)
);
}
const shouldExtract = download.automaticallyExtract;
// Handle download completion BEFORE sending progress to renderer
// This ensures extraction starts and DB is updated before UI reacts
if (progress === 1 && download) {
publishDownloadCompleteNotification(game);
@@ -143,7 +154,6 @@ export class DownloadManager {
shouldSeed: true,
queued: false,
extracting: shouldExtract,
extractionProgress: shouldExtract ? 0 : download.extractionProgress,
});
} else {
await downloadsSublevel.put(gameId, {
@@ -152,22 +162,12 @@ export class DownloadManager {
shouldSeed: false,
queued: false,
extracting: shouldExtract,
extractionProgress: shouldExtract ? 0 : download.extractionProgress,
});
this.cancelDownload(gameId);
}
if (shouldExtract) {
// Send initial extraction progress BEFORE download progress
// This ensures the UI shows extraction immediately
WindowManager.mainWindow?.webContents.send(
"on-extraction-progress",
game.shop,
game.objectId,
0
);
const gameFilesManager = new GameFilesManager(
game.shop,
game.objectId
@@ -194,10 +194,10 @@ export class DownloadManager {
.values()
.all()
.then((games) => {
return orderBy(
return sortBy(
games.filter((game) => game.status === "paused" && game.queued),
"timestamp",
"desc"
"DESC"
);
});
@@ -209,18 +209,6 @@ export class DownloadManager {
this.downloadingGameId = null;
}
}
// Send progress to renderer after completion handling
if (WindowManager.mainWindow && download) {
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
WindowManager.mainWindow.webContents.send(
"on-download-progress",
structuredClone({
...status,
game,
})
);
}
}
}

View File

@@ -3,58 +3,24 @@ import fs from "node:fs";
import type { GameShop } from "@types";
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
import { FILE_EXTENSIONS_TO_EXTRACT } from "@shared";
import { SevenZip, ExtractionProgress } from "./7zip";
import { SevenZip } from "./7zip";
import { WindowManager } from "./window-manager";
import { publishExtractionCompleteNotification } from "./notifications";
import { logger } from "./logger";
const PROGRESS_THROTTLE_MS = 1000;
export class GameFilesManager {
private lastProgressUpdate = 0;
constructor(
private readonly shop: GameShop,
private readonly objectId: string
) {}
private get gameKey() {
return levelKeys.game(this.shop, this.objectId);
}
private async updateExtractionProgress(progress: number, force = false) {
const now = Date.now();
if (!force && now - this.lastProgressUpdate < PROGRESS_THROTTLE_MS) {
return;
}
this.lastProgressUpdate = now;
const download = await downloadsSublevel.get(this.gameKey);
if (!download) return;
await downloadsSublevel.put(this.gameKey, {
...download,
extractionProgress: progress,
});
WindowManager.mainWindow?.webContents.send(
"on-extraction-progress",
this.shop,
this.objectId,
progress
);
}
private async clearExtractionState() {
const download = await downloadsSublevel.get(this.gameKey);
if (!download) return;
const gameKey = levelKeys.game(this.shop, this.objectId);
const download = await downloadsSublevel.get(gameKey);
await downloadsSublevel.put(this.gameKey, {
...download,
await downloadsSublevel.put(gameKey, {
...download!,
extracting: false,
extractionProgress: 0,
});
WindowManager.mainWindow?.webContents.send(
@@ -64,10 +30,6 @@ export class GameFilesManager {
);
}
private readonly handleProgress = (progress: ExtractionProgress) => {
this.updateExtractionProgress(progress.percent / 100);
};
async extractFilesInDirectory(directoryPath: string) {
if (!fs.existsSync(directoryPath)) return;
const files = await fs.promises.readdir(directoryPath);
@@ -80,66 +42,53 @@ export class GameFilesManager {
(file) => /part1\.rar$/i.test(file) || !/part\d+\.rar$/i.test(file)
);
if (filesToExtract.length === 0) return;
await this.updateExtractionProgress(0, true);
const totalFiles = filesToExtract.length;
let completedFiles = 0;
for (const file of filesToExtract) {
try {
const result = await SevenZip.extractFile(
{
filePath: path.join(directoryPath, file),
cwd: directoryPath,
passwords: ["online-fix.me", "steamrip.com"],
},
(progress) => {
const overallProgress =
(completedFiles + progress.percent / 100) / totalFiles;
this.updateExtractionProgress(overallProgress);
}
);
if (result.success) {
completedFiles++;
await this.updateExtractionProgress(
completedFiles / totalFiles,
true
await Promise.all(
filesToExtract.map((file) => {
return new Promise((resolve, reject) => {
SevenZip.extractFile(
{
filePath: path.join(directoryPath, file),
cwd: directoryPath,
passwords: ["online-fix.me", "steamrip.com"],
},
() => {
resolve(true);
},
() => {
reject(new Error(`Failed to extract file: ${file}`));
this.clearExtractionState();
}
);
}
} catch (err) {
logger.error(`Failed to extract file: ${file}`, err);
await this.clearExtractionState();
return;
});
})
);
compressedFiles.forEach((file) => {
const extractionPath = path.join(directoryPath, file);
if (fs.existsSync(extractionPath)) {
fs.unlink(extractionPath, (err) => {
if (err) {
logger.error(`Failed to delete file: ${file}`, err);
this.clearExtractionState();
}
});
}
}
const archivePaths = compressedFiles
.map((file) => path.join(directoryPath, file))
.filter((archivePath) => fs.existsSync(archivePath));
if (archivePaths.length > 0) {
WindowManager.mainWindow?.webContents.send(
"on-archive-deletion-prompt",
archivePaths
);
}
});
}
async setExtractionComplete(publishNotification = true) {
const gameKey = levelKeys.game(this.shop, this.objectId);
const [download, game] = await Promise.all([
downloadsSublevel.get(this.gameKey),
gamesSublevel.get(this.gameKey),
downloadsSublevel.get(gameKey),
gamesSublevel.get(gameKey),
]);
if (!download) return;
await downloadsSublevel.put(this.gameKey, {
...download,
await downloadsSublevel.put(gameKey, {
...download!,
extracting: false,
extractionProgress: 0,
});
WindowManager.mainWindow?.webContents.send(
@@ -148,15 +97,17 @@ export class GameFilesManager {
this.objectId
);
if (publishNotification && game) {
publishExtractionCompleteNotification(game);
if (publishNotification) {
publishExtractionCompleteNotification(game!);
}
}
async extractDownloadedFile() {
const gameKey = levelKeys.game(this.shop, this.objectId);
const [download, game] = await Promise.all([
downloadsSublevel.get(this.gameKey),
gamesSublevel.get(this.gameKey),
downloadsSublevel.get(gameKey),
gamesSublevel.get(gameKey),
]);
if (!download || !game) return false;
@@ -168,39 +119,39 @@ export class GameFilesManager {
path.parse(download.folderName!).name
);
await this.updateExtractionProgress(0, true);
try {
const result = await SevenZip.extractFile(
{
filePath,
outputPath: extractionPath,
passwords: ["online-fix.me", "steamrip.com"],
},
this.handleProgress
);
if (result.success) {
SevenZip.extractFile(
{
filePath,
outputPath: extractionPath,
passwords: ["online-fix.me", "steamrip.com"],
},
async () => {
await this.extractFilesInDirectory(extractionPath);
if (fs.existsSync(extractionPath) && fs.existsSync(filePath)) {
WindowManager.mainWindow?.webContents.send(
"on-archive-deletion-prompt",
[filePath]
);
fs.unlink(filePath, (err) => {
if (err) {
logger.error(
`Failed to delete file: ${download.folderName}`,
err
);
this.clearExtractionState();
}
});
}
await downloadsSublevel.put(this.gameKey, {
...download,
await downloadsSublevel.put(gameKey, {
...download!,
folderName: path.parse(download.folderName!).name,
});
await this.setExtractionComplete();
this.setExtractionComplete();
},
() => {
this.clearExtractionState();
}
} catch (err) {
logger.error(`Failed to extract downloaded file: ${filePath}`, err);
await this.clearExtractionState();
}
);
return true;
}

View File

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

View File

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

View File

@@ -1,87 +0,0 @@
declare module "node-7z" {
import { ChildProcess } from "node:child_process";
import { EventEmitter } from "node:events";
export interface CommandLineSwitches {
$bin?: string;
$progress?: boolean;
$spawnOptions?: {
cwd?: string;
};
outputDir?: string;
yes?: boolean;
password?: string;
[key: string]: unknown;
}
export interface ProgressInfo {
percent: number;
fileCount?: number;
}
export interface FileInfo {
file?: string;
[key: string]: unknown;
}
export interface ZipStream extends EventEmitter {
on(event: "progress", listener: (progress: ProgressInfo) => void): this;
on(event: "data", listener: (data: FileInfo) => void): this;
on(event: "end", listener: () => void): this;
on(event: "error", listener: (err: Error) => void): this;
info: Map<string, unknown>;
_childProcess?: ChildProcess;
}
export function extractFull(
archive: string,
output: string,
options?: CommandLineSwitches
): ZipStream;
export function extract(
archive: string,
output: string,
options?: CommandLineSwitches
): ZipStream;
export function list(
archive: string,
options?: CommandLineSwitches
): ZipStream;
export function add(
archive: string,
files: string | string[],
options?: CommandLineSwitches
): ZipStream;
export function update(
archive: string,
files: string | string[],
options?: CommandLineSwitches
): ZipStream;
export function deleteFiles(
archive: string,
files: string | string[],
options?: CommandLineSwitches
): ZipStream;
export function test(
archive: string,
options?: CommandLineSwitches
): ZipStream;
const Seven: {
extractFull: typeof extractFull;
extract: typeof extract;
list: typeof list;
add: typeof add;
update: typeof update;
delete: typeof deleteFiles;
test: typeof test;
};
export default Seven;
}

View File

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

View File

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

View File

@@ -267,29 +267,6 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.on("on-extraction-complete", listener);
return () => ipcRenderer.removeListener("on-extraction-complete", listener);
},
onExtractionProgress: (
cb: (shop: GameShop, objectId: string, progress: number) => void
) => {
const listener = (
_event: Electron.IpcRendererEvent,
shop: GameShop,
objectId: string,
progress: number
) => cb(shop, objectId, progress);
ipcRenderer.on("on-extraction-progress", listener);
return () => ipcRenderer.removeListener("on-extraction-progress", listener);
},
onArchiveDeletionPrompt: (cb: (archivePaths: string[]) => void) => {
const listener = (
_event: Electron.IpcRendererEvent,
archivePaths: string[]
) => cb(archivePaths);
ipcRenderer.on("on-archive-deletion-prompt", listener);
return () =>
ipcRenderer.removeListener("on-archive-deletion-prompt", listener);
},
deleteArchive: (filePath: string) =>
ipcRenderer.invoke("deleteArchive", filePath),
/* Hardware */
getDiskFreeSpace: (path: string) =>
@@ -642,28 +619,4 @@ contextBridge.exposeInMainWorld("electron", {
},
closeEditorWindow: (themeId?: string) =>
ipcRenderer.invoke("closeEditorWindow", themeId),
/* LevelDB Generic CRUD */
leveldb: {
get: (
key: string,
sublevelName?: string | null,
valueEncoding?: "json" | "utf8"
) => ipcRenderer.invoke("leveldbGet", key, sublevelName, valueEncoding),
put: (
key: string,
value: unknown,
sublevelName?: string | null,
valueEncoding?: "json" | "utf8"
) =>
ipcRenderer.invoke("leveldbPut", key, value, sublevelName, valueEncoding),
del: (key: string, sublevelName?: string | null) =>
ipcRenderer.invoke("leveldbDel", key, sublevelName),
clear: (sublevelName: string) =>
ipcRenderer.invoke("leveldbClear", sublevelName),
values: (sublevelName: string) =>
ipcRenderer.invoke("leveldbValues", sublevelName),
iterator: (sublevelName: string) =>
ipcRenderer.invoke("leveldbIterator", sublevelName),
},
});

View File

@@ -6,7 +6,7 @@
<title>Hydra Launcher</title>
<meta
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>
<body>

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useRef } from "react";
import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
import {
@@ -19,14 +19,11 @@ import {
setUserDetails,
setProfileBackground,
setGameRunning,
setExtractionProgress,
clearExtraction,
} from "@renderer/features";
import { useTranslation } from "react-i18next";
import { UserFriendModal } from "./pages/shared-modals/user-friend-modal";
import { useSubscription } from "./hooks/use-subscription";
import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal";
import { ArchiveDeletionModal } from "./pages/downloads/archive-deletion-error-modal";
import {
injectCustomCss,
@@ -34,8 +31,6 @@ import {
getAchievementSoundUrl,
getAchievementSoundVolume,
} from "./helpers";
import { levelDBService } from "./services/leveldb.service";
import type { UserPreferences } from "@types";
import "./app.scss";
export interface AppProps {
@@ -81,17 +76,12 @@ export function App() {
const { showSuccessToast } = useToast();
const [showArchiveDeletionModal, setShowArchiveDeletionModal] =
useState(false);
const [archivePaths, setArchivePaths] = useState<string[]>([]);
useEffect(() => {
Promise.all([
levelDBService.get("userPreferences", null, "json"),
updateLibrary(),
]).then(([preferences]) => {
dispatch(setUserPreferences(preferences as UserPreferences | null));
});
Promise.all([window.electron.getUserPreferences(), updateLibrary()]).then(
([preferences]) => {
dispatch(setUserPreferences(preferences));
}
);
}, [navigate, location.pathname, dispatch, updateLibrary]);
useEffect(() => {
@@ -191,23 +181,12 @@ export function App() {
updateLibrary();
}),
window.electron.onSignOut(() => clearUserDetails()),
window.electron.onExtractionProgress((shop, objectId, progress) => {
dispatch(setExtractionProgress({ shop, objectId, progress }));
}),
window.electron.onExtractionComplete(() => {
dispatch(clearExtraction());
updateLibrary();
}),
window.electron.onArchiveDeletionPrompt((paths) => {
setArchivePaths(paths);
setShowArchiveDeletionModal(true);
}),
];
return () => {
listeners.forEach((unsubscribe) => unsubscribe());
};
}, [onSignIn, updateLibrary, clearUserDetails, dispatch]);
}, [onSignIn, updateLibrary, clearUserDetails]);
useEffect(() => {
if (contentRef.current) contentRef.current.scrollTop = 0;
@@ -225,11 +204,7 @@ export function App() {
}, [dispatch, draggingDisabled]);
const loadAndApplyTheme = useCallback(async () => {
const allThemes = (await levelDBService.values("themes")) as {
isActive?: boolean;
code?: string;
}[];
const activeTheme = allThemes.find((theme) => theme.isActive);
const activeTheme = await window.electron.getActiveCustomTheme();
if (activeTheme?.code) {
injectCustomCss(activeTheme.code);
} else {
@@ -299,12 +274,6 @@ export function App() {
feature={hydraCloudFeature}
/>
<ArchiveDeletionModal
visible={showArchiveDeletionModal}
archivePaths={archivePaths}
onClose={() => setShowArchiveDeletionModal(false)}
/>
{userDetails && (
<UserFriendModal
visible={isFriendsModalVisible}

View File

@@ -2,7 +2,6 @@ import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import {
useAppSelector,
useDownload,
useLibrary,
useToast,
@@ -27,8 +26,6 @@ export function BottomPanel() {
const { lastPacket, progress, downloadSpeed, eta } = useDownload();
const extraction = useAppSelector((state) => state.download.extraction);
const [version, setVersion] = useState("");
const [sessionHash, setSessionHash] = useState<null | string>("");
const [commonRedistStatus, setCommonRedistStatus] = useState<string | null>(
@@ -71,20 +68,6 @@ export function BottomPanel() {
return t("installing_common_redist", { log: commonRedistStatus });
}
if (extraction) {
const extractingGame = library.find(
(game) => game.id === extraction.visibleId
);
if (extractingGame) {
const extractionPercentage = Math.round(extraction.progress * 100);
return t("extracting", {
title: extractingGame.title,
percentage: `${extractionPercentage}%`,
});
}
}
const game = lastPacket
? library.find((game) => game.id === lastPacket?.gameId)
: undefined;
@@ -126,7 +109,6 @@ export function BottomPanel() {
eta,
downloadSpeed,
commonRedistStatus,
extraction,
]);
return (
@@ -140,10 +122,10 @@ export function BottomPanel() {
</button>
<button
data-open-workwonders-changelog-mini
data-featurebase-changelog
className="bottom-panel__version-button"
>
<small>
<small data-featurebase-changelog>
{sessionHash ? `${sessionHash} -` : ""} v{version} &quot;
{VERSION_CODENAME}&quot;
</small>

View File

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

View File

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

View File

@@ -1,11 +1,11 @@
import React from "react";
interface HighlightTextProps {
readonly text: string;
readonly query: string;
text: string;
query: string;
}
export function HighlightText({ text, query }: Readonly<HighlightTextProps>) {
export function HighlightText({ text, query }: HighlightTextProps) {
if (!query.trim()) {
return <>{text}</>;
}
@@ -19,25 +19,24 @@ export function HighlightText({ text, query }: Readonly<HighlightTextProps>) {
return <>{text}</>;
}
const matches: { start: number; end: number }[] = [];
const textLower = text.toLowerCase();
const textWords = text.split(/\b/);
const matches: Array<{ start: number; end: number; text: string }> = [];
queryWords.forEach((queryWord) => {
const escapedQuery = queryWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const regex = new RegExp(
`(?:^|[\\s])${escapedQuery}(?=[\\s]|$)|^${escapedQuery}$`,
"gi"
);
let currentIndex = 0;
textWords.forEach((word) => {
const wordLower = word.toLowerCase();
let match;
while ((match = regex.exec(textLower)) !== null) {
const matchedText = match[0];
const leadingSpace = matchedText.startsWith(" ") ? 1 : 0;
const start = match.index + leadingSpace;
const end = start + queryWord.length;
queryWords.forEach((queryWord) => {
if (wordLower === queryWord) {
matches.push({
start: currentIndex,
end: currentIndex + word.length,
text: word,
});
}
});
matches.push({ start, end });
}
currentIndex += word.length;
});
if (matches.length === 0) {
@@ -46,15 +45,17 @@ export function HighlightText({ text, query }: Readonly<HighlightTextProps>) {
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];
for (let i = 1; i < matches.length; i++) {
if (matches[i].start <= current.end) {
current = {
start: current.start,
end: Math.max(current.end, matches[i].end),
};
current.end = Math.max(current.end, matches[i].end);
} else {
mergedMatches.push(current);
current = matches[i];
@@ -62,7 +63,7 @@ export function HighlightText({ text, query }: Readonly<HighlightTextProps>) {
}
mergedMatches.push(current);
const parts: { text: string; highlight: boolean; key: string }[] = [];
const parts: Array<{ text: string; highlight: boolean }> = [];
let lastIndex = 0;
mergedMatches.forEach((match) => {
@@ -70,14 +71,12 @@ export function HighlightText({ text, query }: Readonly<HighlightTextProps>) {
parts.push({
text: text.slice(lastIndex, match.start),
highlight: false,
key: `${lastIndex}-${match.start}`,
});
}
parts.push({
text: text.slice(match.start, match.end),
highlight: true,
key: `${match.start}-${match.end}`,
});
lastIndex = match.end;
@@ -87,19 +86,18 @@ export function HighlightText({ text, query }: Readonly<HighlightTextProps>) {
parts.push({
text: text.slice(lastIndex),
highlight: false,
key: `${lastIndex}-${text.length}`,
});
}
return (
<>
{parts.map((part) =>
{parts.map((part, index) =>
part.highlight ? (
<mark key={part.key} className="search-dropdown__highlight">
<mark key={index} className="search-dropdown__highlight">
{part.text}
</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;
align-items: center;
justify-content: space-between;
padding: 8px 12px 8px;
margin-bottom: 4px;
padding: 8px 12px 4px;
}
&__section-title {
@@ -36,19 +35,19 @@
letter-spacing: 0.5px;
}
&__clear-text-button {
&__clear-button {
color: globals.$muted-color;
cursor: pointer;
padding: 0;
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
transition: color ease 0.2s;
background: transparent;
border: none;
padding: 4px;
border-radius: 4px;
transition: all ease 0.2s;
display: flex;
align-items: center;
justify-content: center;
&:hover {
color: #ffffff;
color: #dadbe1;
background-color: rgba(255, 255, 255, 0.1);
}
}
@@ -75,16 +74,17 @@
transform: translateY(-50%);
color: globals.$muted-color;
padding: 4px;
border-radius: 4px;
opacity: 0;
transition: opacity ease 0.15s;
transition: all ease 0.15s;
display: flex;
align-items: center;
justify-content: center;
background-color: transparent;
&:hover {
color: #ff3333;
background-color: rgba(255, 85, 85, 0.2);
color: #ff5555;
background-color: rgba(255, 85, 85, 0.1);
}
}
@@ -144,8 +144,8 @@
}
&__highlight {
background-color: rgba(255, 193, 7, 0.4);
color: #ffa000;
background-color: rgba(255, 193, 7, 0.3);
color: #ffc107;
font-weight: 600;
padding: 0 2px;
border-radius: 2px;

View File

@@ -1,6 +1,11 @@
import { useEffect, useRef, useCallback, useState } from "react";
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 { useTranslation } from "react-i18next";
import type { SearchHistoryEntry } from "@renderer/hooks/use-search-history";
@@ -139,10 +144,11 @@ export function SearchDropdown({
</span>
<button
type="button"
className="search-dropdown__clear-text-button"
className="search-dropdown__clear-button"
onClick={onClearHistory}
title={t("clear_history")}
>
{t("clear_history")}
<TrashIcon size={14} />
</button>
</div>
<ul className="search-dropdown__list">

View File

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

View File

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

View File

@@ -208,13 +208,6 @@ declare global {
onExtractionComplete: (
cb: (shop: GameShop, objectId: string) => void
) => () => Electron.IpcRenderer;
onExtractionProgress: (
cb: (shop: GameShop, objectId: string, progress: number) => void
) => () => Electron.IpcRenderer;
onArchiveDeletionPrompt: (
cb: (archivePaths: string[]) => void
) => () => Electron.IpcRenderer;
deleteArchive: (filePath: string) => Promise<boolean>;
getDefaultWinePrefixSelectionPath: () => Promise<string | null>;
createSteamShortcut: (shop: GameShop, objectId: string) => Promise<void>;
@@ -445,25 +438,6 @@ declare global {
onNewDownloadOptions: (
cb: (gamesWithNewOptions: { gameId: string; count: number }[]) => void
) => () => Electron.IpcRenderer;
/* LevelDB Generic CRUD */
leveldb: {
get: (
key: string,
sublevelName?: string | null,
valueEncoding?: "json" | "utf8"
) => Promise<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 {

View File

@@ -1,24 +1,17 @@
import { createSlice } from "@reduxjs/toolkit";
import type { PayloadAction } from "@reduxjs/toolkit";
import type { DownloadProgress, GameShop } from "@types";
export interface ExtractionInfo {
visibleId: string;
progress: number;
}
import type { DownloadProgress } from "@types";
export interface DownloadState {
lastPacket: DownloadProgress | null;
gameId: string | null;
gamesWithDeletionInProgress: string[];
extraction: ExtractionInfo | null;
}
const initialState: DownloadState = {
lastPacket: null,
gameId: null,
gamesWithDeletionInProgress: [],
extraction: null,
};
export const downloadSlice = createSlice({
@@ -45,23 +38,6 @@ export const downloadSlice = createSlice({
const index = state.gamesWithDeletionInProgress.indexOf(action.payload);
if (index >= 0) state.gamesWithDeletionInProgress.splice(index, 1);
},
setExtractionProgress: (
state,
action: PayloadAction<{
shop: GameShop;
objectId: string;
progress: number;
}>
) => {
const { shop, objectId, progress } = action.payload;
state.extraction = {
visibleId: `${shop}:${objectId}`,
progress,
};
},
clearExtraction: (state) => {
state.extraction = null;
},
},
});
@@ -70,6 +46,4 @@ export const {
clearDownload,
setGameDeleting,
removeGameFromDeleting,
setExtractionProgress,
clearExtraction,
} = downloadSlice.actions;

View File

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

View File

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

View File

@@ -1,9 +1,8 @@
import axios from "axios";
import { useCallback, useEffect, useState } from "react";
import { levelDBService } from "@renderer/services/leveldb.service";
import type { DownloadSource } from "@types";
import { useAppDispatch } from "./redux";
import { setGenres, setTags } from "@renderer/features";
import type { DownloadSource } from "@types";
export const externalResourcesInstance = axios.create({
baseURL: import.meta.env.RENDERER_VITE_EXTERNAL_RESOURCES_URL,
@@ -41,9 +40,8 @@ export function useCatalogue() {
}, []);
const getDownloadSources = useCallback(() => {
levelDBService.values("downloadSources").then((results) => {
const sources = results as DownloadSource[];
setDownloadSources(sources.filter((source) => !!source.fingerprint));
window.electron.getDownloadSources().then((results) => {
setDownloadSources(results.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 { levelDBService } from "@renderer/services/leveldb.service";
import { useState, useCallback, useEffect } from "react";
export interface SearchHistoryEntry {
query: string;
@@ -7,32 +6,22 @@ export interface SearchHistoryEntry {
context: "library" | "catalogue";
}
const LEVELDB_KEY = "searchHistory";
const STORAGE_KEY = "search-history";
const MAX_HISTORY_ENTRIES = 15;
export function useSearchHistory() {
const [history, setHistory] = useState<SearchHistoryEntry[]>([]);
const isInitialized = useRef(false);
useEffect(() => {
const loadHistory = async () => {
if (isInitialized.current) return;
isInitialized.current = true;
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
try {
const data = (await levelDBService.get(LEVELDB_KEY, null, "json")) as
| SearchHistoryEntry[]
| null;
if (data) {
setHistory(data);
}
const parsed = JSON.parse(stored) as SearchHistoryEntry[];
setHistory(parsed);
} catch {
setHistory([]);
localStorage.removeItem(STORAGE_KEY);
}
};
loadHistory();
}
}, []);
const addToHistory = useCallback(
@@ -50,7 +39,7 @@ export function useSearchHistory() {
(entry) => entry.query.toLowerCase() !== query.toLowerCase().trim()
);
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;
});
},
@@ -60,14 +49,14 @@ export function useSearchHistory() {
const removeFromHistory = useCallback((query: string) => {
setHistory((prev) => {
const updated = prev.filter((entry) => entry.query !== query);
levelDBService.put(LEVELDB_KEY, updated, null, "json");
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
return updated;
});
}, []);
const clearHistory = useCallback(() => {
setHistory([]);
levelDBService.del(LEVELDB_KEY, null);
localStorage.removeItem(STORAGE_KEY);
}, []);
const getRecentHistory = useCallback(

View File

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

View File

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

View File

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

View File

@@ -1,44 +0,0 @@
import { useTranslation } from "react-i18next";
import { ConfirmationModal } from "@renderer/components";
interface ArchiveDeletionModalProps {
visible: boolean;
archivePaths: string[];
onClose: () => void;
}
export function ArchiveDeletionModal({
visible,
archivePaths,
onClose,
}: Readonly<ArchiveDeletionModalProps>) {
const { t } = useTranslation("downloads");
const fullFileName =
archivePaths.length > 0 ? (archivePaths[0].split(/[/\\]/).pop() ?? "") : "";
const maxLength = 40;
const fileName =
fullFileName.length > maxLength
? `${fullFileName.slice(0, maxLength)}`
: fullFileName;
const handleConfirm = async () => {
for (const archivePath of archivePaths) {
await window.electron.deleteArchive(archivePath);
}
onClose();
};
return (
<ConfirmationModal
visible={visible}
title={t("delete_archive_title", { fileName })}
descriptionText={t("delete_archive_description")}
confirmButtonLabel={t("yes")}
cancelButtonLabel={t("no")}
onConfirm={handleConfirm}
onClose={onClose}
/>
);
}

View File

@@ -4,541 +4,158 @@
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit * 2);
margin-inline: calc(globals.$spacing-unit * 3);
padding-block: calc(globals.$spacing-unit * 3);
&--queued {
padding-bottom: 0;
}
&--completed {
padding-top: calc(globals.$spacing-unit * 3);
&__details-with-article {
display: flex;
align-items: center;
gap: calc(globals.$spacing-unit / 2);
align-self: flex-start;
cursor: pointer;
}
&__header {
display: flex;
align-items: center;
gap: calc(globals.$spacing-unit);
justify-content: space-between;
gap: calc(globals.$spacing-unit * 2);
&-title-group {
display: flex;
align-items: center;
gap: calc(globals.$spacing-unit);
&-divider {
flex: 1;
h2 {
margin: 0;
font-size: 20px;
font-weight: 700;
color: rgba(255, 255, 255, 0.95);
}
background-color: globals.$border-color;
height: 1px;
}
&-count {
background-color: rgba(255, 255, 255, 0.1);
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;
font-weight: 400;
}
}
&--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%;
position: relative;
overflow: hidden;
gap: calc(globals.$spacing-unit * 2);
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
padding-bottom: globals.$spacing-unit;
margin-top: globals.$spacing-unit;
}
&__hero-background {
position: absolute;
top: 0;
left: 0;
&__item {
width: 100%;
height: 120%;
z-index: 0;
background-color: globals.$background-color;
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 {
width: 100%;
height: 100%;
object-fit: cover;
object-position: 50% 20%;
&--hydra {
box-shadow: 0px 0px 16px 0px rgba(12, 241, 202, 0.15);
}
}
// PLEASE FIX THE COLORS
&__hero-overlay {
position: absolute;
top: 0;
left: 0;
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 {
&__cover {
width: 280px;
min-width: 280px;
height: auto;
border-right: solid 1px globals.$border-color;
position: relative;
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 {
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 {
&-content {
width: 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;
min-width: 0;
gap: globals.$spacing-unit;
background: linear-gradient(90deg, transparent 20%, rgb(0 0 0 / 20%) 100%);
}
&__details {
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit / 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;
flex: 1;
justify-content: center;
align-items: center;
gap: calc(globals.$spacing-unit);
gap: calc(globals.$spacing-unit / 2);
font-size: 14px;
}
&__simple-menu-btn {
padding: calc(globals.$spacing-unit);
&__actions {
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;
}
&__progress-wrapper {
flex: 1;
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit / 2);
}
&__progress-bar {
&__hydra-gradient {
background: linear-gradient(90deg, #01483c 0%, #0cf1ca 50%, #01483c 100%);
box-shadow: 0px 0px 8px 0px rgba(12, 241, 202, 0.15);
width: 100%;
height: 8px;
background-color: rgba(255, 255, 255, 0.08);
border-radius: 4px;
overflow: hidden;
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;
&--extraction {
background-color: #fff;
}
position: absolute;
bottom: 0;
height: 2px;
z-index: 1;
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,6 +1,6 @@
import { useTranslation } from "react-i18next";
import { useAppSelector, useDownload, useLibrary } from "@renderer/hooks";
import { useDownload, useLibrary } from "@renderer/hooks";
import { useEffect, useMemo, useRef, useState } from "react";
import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
@@ -13,7 +13,6 @@ import { ArrowDownIcon } from "@primer/octicons-react";
export default function Downloads() {
const { library, updateLibrary } = useLibrary();
const extraction = useAppSelector((state) => state.download.extraction);
const { t } = useTranslation("downloads");
@@ -40,13 +39,11 @@ export default function Downloads() {
useEffect(() => {
window.electron.onSeedingStatus((value) => setSeedingStatus(value));
const unsubscribeExtraction = window.electron.onExtractionComplete(() => {
const unsubscribe = window.electron.onExtractionComplete(() => {
updateLibrary();
});
return () => {
unsubscribeExtraction();
};
return () => unsubscribe();
}, [updateLibrary]);
const handleOpenGameInstaller = (shop: GameShop, objectId: string) =>
@@ -75,10 +72,8 @@ export default function Downloads() {
/* Game has been manually added to the library */
if (!next.download) return prev;
/* Is downloading or extracting */
const isExtracting =
next.download.extracting || extraction?.visibleId === next.id;
if (lastPacket?.gameId === next.id || isExtracting)
/* Is downloading */
if (lastPacket?.gameId === next.id || next.download.extracting)
return { ...prev, downloading: [...prev.downloading, next] };
/* Is either queued or paused */
@@ -101,7 +96,7 @@ export default function Downloads() {
queued,
complete,
};
}, [library, lastPacket?.gameId, extraction?.visibleId]);
}, [library, lastPacket?.gameId]);
const downloadGroups = [
{

View File

@@ -8,7 +8,6 @@ import {
import useEmblaCarousel from "embla-carousel-react";
import { gameDetailsContext } from "@renderer/context";
import { useAppSelector } from "@renderer/hooks";
import { VideoPlayer } from "./video-player";
import "./gallery-slider.scss";
export function GallerySlider() {
@@ -107,6 +106,8 @@ export function GallerySlider() {
if (shopDetails?.movies) {
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 videoType: string | undefined;
@@ -120,9 +121,11 @@ export function GallerySlider() {
videoSrc = video.dash_av1;
videoType = "application/dash+xml";
} else if (video.mp4?.max) {
// Fallback to old format
videoSrc = video.mp4.max;
videoType = "video/mp4";
} else if (video.webm?.max) {
// Fallback to webm if mp4 is not available
videoSrc = video.webm.max;
videoType = "video/webm";
}
@@ -188,17 +191,19 @@ export function GallerySlider() {
{mediaItems.map((item) => (
<div key={item.id} className="gallery-slider__slide">
{item.type === "video" ? (
<VideoPlayer
videoSrc={item.videoSrc}
videoType={item.videoType}
poster={item.poster}
autoplay={autoplayEnabled}
loop
muted
<video
controls
className="gallery-slider__media"
poster={item.poster}
loop
muted
autoPlay={autoplayEnabled}
tabIndex={-1}
/>
>
{item.videoSrc && (
<source src={item.videoSrc} type={item.videoType} />
)}
</video>
) : (
<img
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,12 +1,7 @@
import { useContext, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { formatDownloadProgress } from "@renderer/helpers";
import {
useAppSelector,
useDate,
useDownload,
useFormat,
} from "@renderer/hooks";
import { useDate, useDownload, useFormat } from "@renderer/hooks";
import { Link } from "@renderer/components";
import { gameDetailsContext } from "@renderer/context";
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
@@ -22,9 +17,6 @@ export function HeroPanelPlaytime() {
const { numberFormatter } = useFormat();
const { progress, lastPacket } = useDownload();
const { formatDistance } = useDate();
const extraction = useAppSelector((state) => state.download.extraction);
const isExtracting = extraction?.visibleId === game?.id;
useEffect(() => {
if (game?.lastTimePlayed) {
@@ -60,16 +52,6 @@ export function HeroPanelPlaytime() {
const isGameDownloading =
game.download?.status === "active" && lastPacket?.gameId === game.id;
const extractionInProgressInfo = (
<div className="hero-panel-playtime__download-details">
<Link to="/downloads" className="hero-panel-playtime__downloads-link">
{t("extracting")}
</Link>
<small>{formatDownloadProgress(extraction?.progress ?? 0)}</small>
</div>
);
const downloadInProgressInfo = (
<div className="hero-panel-playtime__download-details">
<Link to="/downloads" className="hero-panel-playtime__downloads-link">
@@ -90,8 +72,7 @@ export function HeroPanelPlaytime() {
return (
<>
<p>{t("not_played_yet", { title: game?.title })}</p>
{isExtracting && extractionInProgressInfo}
{!isExtracting && hasDownload && downloadInProgressInfo}
{hasDownload && downloadInProgressInfo}
</>
);
}
@@ -100,8 +81,7 @@ export function HeroPanelPlaytime() {
return (
<>
<p>{t("playing_now")}</p>
{isExtracting && extractionInProgressInfo}
{!isExtracting && hasDownload && downloadInProgressInfo}
{hasDownload && downloadInProgressInfo}
</>
);
}
@@ -133,9 +113,9 @@ export function HeroPanelPlaytime() {
})}
</p>
{isExtracting && extractionInProgressInfo}
{!isExtracting && hasDownload && downloadInProgressInfo}
{!isExtracting && !hasDownload && (
{hasDownload ? (
downloadInProgressInfo
) : (
<p>
{t("last_time_played", {
period: lastTimePlayed,

View File

@@ -80,11 +80,5 @@
&--disabled {
opacity: globals.$disabled-opacity;
}
&--extraction {
&::-webkit-progress-value {
background-color: #fff;
}
}
}
}

View File

@@ -1,7 +1,7 @@
import { useContext } from "react";
import { useTranslation } from "react-i18next";
import { useAppSelector, useDate, useDownload } from "@renderer/hooks";
import { useDate, useDownload } from "@renderer/hooks";
import { HeroPanelActions } from "./hero-panel-actions";
import { HeroPanelPlaytime } from "./hero-panel-playtime";
@@ -18,13 +18,9 @@ export function HeroPanel() {
const { lastPacket } = useDownload();
const extraction = useAppSelector((state) => state.download.extraction);
const isGameDownloading =
game?.download?.status === "active" && lastPacket?.gameId === game?.id;
const isExtracting = extraction?.visibleId === game?.id;
const getInfo = () => {
if (!game) {
const [latestRepack] = repacks;
@@ -53,8 +49,6 @@ export function HeroPanel() {
(game?.download?.status === "active" && game?.download?.progress < 1) ||
game?.download?.status === "paused";
const showExtractionProgressBar = isExtracting;
return (
<div className="hero-panel__container">
<div className="hero-panel">
@@ -78,14 +72,6 @@ export function HeroPanel() {
}`}
/>
)}
{showExtractionProgressBar && (
<progress
max={1}
value={extraction?.progress ?? 0}
className="hero-panel__progress-bar hero-panel__progress-bar--extraction"
/>
)}
</div>
</div>
);

View File

@@ -1,7 +1,7 @@
import { useContext, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
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 { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
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 SteamLogo from "@renderer/assets/steam-logo.svg?react";
import { debounce } from "lodash-es";
import { levelDBService } from "@renderer/services/leveldb.service";
import { getGameKey } from "@renderer/helpers";
import "./game-options-modal.scss";
import { logger } from "@renderer/logger";
@@ -77,19 +75,11 @@ export function GameOptionsModal({
const debounceUpdateLaunchOptions = useRef(
debounce(async (value: string) => {
const gameKey = getGameKey(game.shop, game.objectId);
const gameData = (await levelDBService.get(
gameKey,
"games"
)) as Game | null;
if (gameData) {
const trimmedValue = value.trim();
const updated = {
...gameData,
launchOptions: trimmedValue ? trimmedValue : null,
};
await levelDBService.put(gameKey, updated, "games");
}
await window.electron.updateLaunchOptions(
game.shop,
game.objectId,
value
);
updateGame();
}, 1000)
).current;
@@ -223,16 +213,9 @@ export function GameOptionsModal({
const handleClearLaunchOptions = async () => {
setLaunchOptions("");
const gameKey = getGameKey(game.shop, game.objectId);
const gameData = (await levelDBService.get(
gameKey,
"games"
)) as Game | null;
if (gameData) {
const updated = { ...gameData, launchOptions: null };
await levelDBService.put(gameKey, updated, "games");
}
updateGame();
window.electron
.updateLaunchOptions(game.shop, game.objectId, null)
.then(updateGame);
};
const shouldShowWinePrefixConfiguration =
@@ -273,15 +256,11 @@ export function GameOptionsModal({
) => {
setAutomaticCloudSync(event.target.checked);
const gameKey = getGameKey(game.shop, game.objectId);
const gameData = (await levelDBService.get(
gameKey,
"games"
)) as Game | null;
if (gameData) {
const updated = { ...gameData, automaticCloudSync: event.target.checked };
await levelDBService.put(gameKey, updated, "games");
}
await window.electron.toggleAutomaticCloudSync(
game.shop,
game.objectId,
event.target.checked
);
updateGame();
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5690,11 +5690,6 @@ hasown@^2.0.2:
dependencies:
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:
version "3.3.2"
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==
js-yaml@^4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
version "4.1.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
dependencies:
argparse "^2.0.1"
@@ -6330,7 +6325,7 @@ jsonwebtoken@^9.0.2:
object.assign "^4.1.4"
object.values "^1.1.6"
jwa@^1.4.2:
jwa@^1.4.1:
version "1.4.2"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.2.tgz#16011ac6db48de7b102777e57897901520eec7b9"
integrity sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==
@@ -6340,11 +6335,11 @@ jwa@^1.4.2:
safe-buffer "^5.0.1"
jws@^3.2.2:
version "3.2.3"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.3.tgz#5ac0690b460900a27265de24520526853c0b8ca1"
integrity sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==
version "3.2.2"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
dependencies:
jwa "^1.4.2"
jwa "^1.4.1"
safe-buffer "^5.0.1"
keyv@^4.0.0, keyv@^4.5.3:
@@ -6438,26 +6433,11 @@ lodash.clonedeep@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==
lodash.defaultsdeep@^4.6.1:
version "4.6.1"
resolved "https://registry.yarnpkg.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz#512e9bd721d272d94e3d3a63653fa17516741ca6"
integrity sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==
lodash.defaultto@^4.14.0:
version "4.14.0"
resolved "https://registry.yarnpkg.com/lodash.defaultto/-/lodash.defaultto-4.14.0.tgz#38bd3d425acee733e0e2bbbd4e4b29711cc2ee11"
integrity sha512-G6tizqH6rg4P5j32Wy4Z3ZIip7OfG8YWWlPFzUFGcYStH1Ld0l1tWs6NevEQNEDnO1M3NZYjuHuraaFSN5WqeQ==
lodash.escaperegexp@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347"
integrity sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==
lodash.flattendeep@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
integrity sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==
lodash.includes@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
@@ -6468,11 +6448,6 @@ lodash.isboolean@^3.0.3:
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==
lodash.isempty@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e"
integrity sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==
lodash.isequal@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
@@ -6513,11 +6488,6 @@ lodash.mergewith@^4.6.2:
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55"
integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==
lodash.negate@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/lodash.negate/-/lodash.negate-3.0.2.tgz#9c897b0bf610019e0b43b8ff3f0afef3d7b66f34"
integrity sha512-JGJYYVslKYC0tRMm/7igfdHulCjoXjoganRNWM8AgS+RXfOvFnPkOveDhPI65F9aAypCX9QEEQoBqWf7Q6uAeA==
lodash.once@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
@@ -6897,19 +6867,6 @@ no-case@^3.0.4:
lower-case "^2.0.2"
tslib "^2.0.3"
node-7z@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/node-7z/-/node-7z-3.0.0.tgz#42f71c5a43b00028749f7c88291a7abf2e2623e3"
integrity sha512-KIznWSxIkOYO/vOgKQfJEaXd7rgoFYKZbaurainCEdMhYc7V7mRHX+qdf2HgbpQFcdJL/Q6/XOPrDLoBeTfuZA==
dependencies:
debug "^4.3.2"
lodash.defaultsdeep "^4.6.1"
lodash.defaultto "^4.14.0"
lodash.flattendeep "^4.4.0"
lodash.isempty "^4.4.0"
lodash.negate "^3.0.2"
normalize-path "^3.0.0"
node-abi@^3.45.0:
version "3.78.0"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.78.0.tgz#fd0ecbd0aa89857b98da06bd3909194abb0821ba"
@@ -6965,11 +6922,6 @@ nopt@^6.0.0:
dependencies:
abbrev "^1.0.0"
normalize-path@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
normalize-url@^6.0.1:
version "6.1.0"
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
@@ -8566,10 +8518,10 @@ tar@^6.0.5, tar@^6.1.11, tar@^6.1.12, tar@^6.2.1:
mkdirp "^1.0.3"
yallist "^4.0.0"
tar@^7.5.2:
version "7.5.2"
resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.2.tgz#115c061495ec51ff3c6745ff8f6d0871c5b1dedc"
integrity sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==
tar@^7.4.3:
version "7.5.1"
resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.1.tgz#750a8bd63b7c44c1848e7bf982260a083cf747c9"
integrity sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==
dependencies:
"@isaacs/fs-minipass" "^4.0.0"
chownr "^3.0.0"