diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 539f837c..1aef9a93 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -80,7 +80,6 @@ jobs:
BUILDS_URL: ${{ secrets.BUILDS_URL }}
BUILD_WEBHOOK_URL: ${{ secrets.BUILD_WEBHOOK_URL }}
GITHUB_ACTOR: ${{ github.actor }}
-
run: node scripts/upload-build.cjs
- name: Create artifact
diff --git a/binaries/7z.dll b/binaries/7z.dll
new file mode 100644
index 00000000..8ab081f5
Binary files /dev/null and b/binaries/7z.dll differ
diff --git a/binaries/7z.exe b/binaries/7z.exe
new file mode 100644
index 00000000..4774e2ee
Binary files /dev/null and b/binaries/7z.exe differ
diff --git a/binaries/7zz b/binaries/7zz
new file mode 100644
index 00000000..2cde7d8d
Binary files /dev/null and b/binaries/7zz differ
diff --git a/binaries/7zzs b/binaries/7zzs
new file mode 100644
index 00000000..69524c55
Binary files /dev/null and b/binaries/7zzs differ
diff --git a/electron-builder.yml b/electron-builder.yml
index 5ce6107b..dd10e81a 100644
--- a/electron-builder.yml
+++ b/electron-builder.yml
@@ -20,6 +20,9 @@ asarUnpack:
- resources/**
win:
executableName: Hydra
+ extraResources:
+ - from: binaries/7z.exe
+ - from: binaries/7z.dll
target:
- nsis
- portable
@@ -35,6 +38,8 @@ portable:
artifactName: ${name}-${version}-portable.${ext}
mac:
entitlementsInherit: build/entitlements.mac.plist
+ extraResources:
+ - from: binaries/7zz
extendInfo:
- NSCameraUsageDescription: Application requests access to the device's camera.
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
@@ -44,6 +49,8 @@ mac:
dmg:
artifactName: ${name}-${version}.${ext}
linux:
+ extraResources:
+ - from: binaries/7zzs
target:
- AppImage
- snap
diff --git a/electron.vite.config.ts b/electron.vite.config.ts
index e2af60ce..08bd00f9 100644
--- a/electron.vite.config.ts
+++ b/electron.vite.config.ts
@@ -16,9 +16,6 @@ export default defineConfig(({ mode }) => {
main: {
build: {
sourcemap: true,
- rollupOptions: {
- external: ["better-sqlite3"],
- },
},
resolve: {
alias: {
diff --git a/package.json b/package.json
index 71cc0ea6..803ca0ad 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "hydralauncher",
- "version": "3.3.1",
+ "version": "3.4.0",
"description": "Hydra",
"main": "./out/main/index.js",
"author": "Los Broxas",
@@ -28,8 +28,7 @@
"build:win": "electron-vite build && electron-builder --win",
"build:mac": "electron-vite build && electron-builder --mac",
"build:linux": "electron-vite build && electron-builder --linux",
- "prepare": "husky",
- "knex:migrate:make": "knex --knexfile src/main/knexfile.ts migrate:make --esm"
+ "prepare": "husky"
},
"dependencies": {
"@electron-toolkit/preload": "^3.0.0",
@@ -45,7 +44,6 @@
"auto-launch": "^5.0.6",
"axios": "^1.7.9",
"axios-cookiejar-support": "^5.0.5",
- "better-sqlite3": "^11.7.0",
"classic-level": "^2.0.0",
"classnames": "^2.5.1",
"color": "^4.2.3",
@@ -62,7 +60,6 @@
"jsdom": "^24.0.0",
"jsonwebtoken": "^9.0.2",
"kill-port": "^2.0.1",
- "knex": "^3.1.0",
"lodash-es": "^4.17.21",
"parse-torrent": "^11.0.17",
"piscina": "^4.7.0",
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index e4341a00..d6a1f687 100644
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -44,7 +44,10 @@
"downloading_metadata": "Downloading {{title}} metadata…",
"downloading": "Downloading {{title}}… ({{percentage}} complete) - Completion {{eta}} - {{speed}}",
"calculating_eta": "Downloading {{title}}… ({{percentage}} complete) - Calculating remaining time…",
- "checking_files": "Checking {{title}} files… ({{percentage}} complete)"
+ "checking_files": "Checking {{title}} files… ({{percentage}} complete)",
+ "installing_common_redist": "{{log}}…",
+ "installation_complete": "Installation complete",
+ "installation_complete_message": "Common redistributables installed successfully"
},
"catalogue": {
"search": "Filter…",
@@ -193,7 +196,8 @@
"download_error_not_cached_in_real_debrid": "This download is not available on Real-Debrid and polling download status from Real-Debrid is not yet available.",
"download_error_not_cached_in_torbox": "This download is not available on Torbox and polling download status from Torbox is not yet available.",
"game_removed_from_favorites": "Game removed from favorites",
- "game_added_to_favorites": "Game added to favorites"
+ "game_added_to_favorites": "Game added to favorites",
+ "automatically_extract_downloaded_files": "Automatically extract downloaded files"
},
"activation": {
"title": "Activate Hydra",
@@ -230,7 +234,9 @@
"seeding": "Seeding",
"stop_seeding": "Stop seeding",
"resume_seeding": "Resume seeding",
- "options": "Manage"
+ "options": "Manage",
+ "extract": "Extract files",
+ "extracting": "Extracting files…"
},
"settings": {
"downloads_path": "Downloads path",
@@ -344,7 +350,11 @@
"error_importing_theme": "Error importing theme",
"theme_imported": "Theme imported successfully",
"enable_friend_request_notifications": "When a friend request is received",
- "enable_auto_install": "Download updates automatically"
+ "enable_auto_install": "Download updates automatically",
+ "common_redist": "Common redistributables",
+ "common_redist_description": "Common redistributables are required to run some games. Installing them is recommended to avoid issues.",
+ "install_common_redist": "Install",
+ "installing_common_redist": "Installing…"
},
"notifications": {
"download_complete": "Download complete",
@@ -357,7 +367,9 @@
"notification_achievement_unlocked_title": "Achievement unlocked for {{game}}",
"notification_achievement_unlocked_body": "{{achievement}} and other {{count}} were unlocked",
"new_friend_request_description": "You have received a new friend request",
- "new_friend_request_title": "New friend request"
+ "new_friend_request_title": "New friend request",
+ "extraction_complete": "Extraction complete",
+ "game_extracted": "{{title}} extracted successfully"
},
"system_tray": {
"open": "Open Hydra",
diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json
index ff87ed13..5f2b62cc 100644
--- a/src/locales/pt-BR/translation.json
+++ b/src/locales/pt-BR/translation.json
@@ -44,7 +44,10 @@
"downloading_metadata": "Baixando metadados de {{title}}…",
"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}}…"
+ "checking_files": "Verificando arquivos de {{title}}…",
+ "installing_common_redist": "{{log}}…",
+ "installation_complete": "Instalação concluída",
+ "installation_complete_message": "Componentes recomendados instalados com sucesso"
},
"game_details": {
"open_download_options": "Ver opções de download",
@@ -182,7 +185,8 @@
"download_error_not_cached_in_real_debrid": "Este download não está disponível no Real-Debrid e a verificação do status do download não está disponível.",
"download_error_not_cached_in_torbox": "Este download não está disponível no Torbox e a verificação do status do download não está disponível.",
"game_removed_from_favorites": "Jogo removido dos favoritos",
- "game_added_to_favorites": "Jogo adicionado aos favoritos"
+ "game_added_to_favorites": "Jogo adicionado aos favoritos",
+ "automatically_extract_downloaded_files": "Extrair automaticamente os arquivos baixados"
},
"activation": {
"title": "Ativação",
@@ -219,7 +223,9 @@
"seeding": "Semeando",
"stop_seeding": "Parar de semear",
"resume_seeding": "Semear",
- "options": "Gerenciar"
+ "options": "Gerenciar",
+ "extract": "Extrair arquivos",
+ "extracting": "Extraindo arquivos…"
},
"settings": {
"downloads_path": "Diretório dos downloads",
@@ -331,7 +337,11 @@
"error_importing_theme": "Erro ao importar tema",
"theme_imported": "Tema importado com sucesso",
"enable_friend_request_notifications": "Quando um pedido de amizade é recebido",
- "enable_auto_install": "Baixar atualizações automaticamente"
+ "enable_auto_install": "Baixar atualizações automaticamente",
+ "common_redist": "Componentes recomendados",
+ "common_redist_description": "Componentes recomendados são necessários para executar alguns jogos. A instalação deles é recomendada para evitar problemas.",
+ "install_common_redist": "Instalar",
+ "installing_common_redist": "Instalando…"
},
"notifications": {
"download_complete": "Download concluído",
@@ -342,7 +352,9 @@
"new_update_available": "Versão {{version}} disponível",
"restart_to_install_update": "Reinicie o Hydra para instalar a nova versão",
"new_friend_request_title": "Novo pedido de amizade",
- "new_friend_request_description": "Você recebeu um novo pedido de amizade"
+ "new_friend_request_description": "Você recebeu um novo pedido de amizade",
+ "extraction_complete": "Extração concluída",
+ "game_extracted": "{{title}} extraído com sucesso"
},
"system_tray": {
"open": "Abrir Hydra",
diff --git a/src/main/constants.ts b/src/main/constants.ts
index 5e0b0409..48390250 100644
--- a/src/main/constants.ts
+++ b/src/main/constants.ts
@@ -12,10 +12,9 @@ export const levelDatabasePath = path.join(
`hydra-db${isStaging ? "-staging" : ""}`
);
-export const databaseDirectory = path.join(app.getPath("appData"), "hydra");
-export const databasePath = path.join(
- databaseDirectory,
- isStaging ? "hydra_test.db" : "hydra.db"
+export const commonRedistPath = path.join(
+ app.getPath("userData"),
+ "CommonRedist"
);
export const logsPath = path.join(app.getPath("userData"), "logs");
diff --git a/src/main/events/cloud-save/upload-save-game.ts b/src/main/events/cloud-save/upload-save-game.ts
index 2674e35a..891941a0 100644
--- a/src/main/events/cloud-save/upload-save-game.ts
+++ b/src/main/events/cloud-save/upload-save-game.ts
@@ -1,8 +1,8 @@
import { CloudSync } from "@main/services";
import { registerEvent } from "../register-event";
import type { GameShop } from "@types";
-import { t } from "i18next";
-import { format } from "date-fns";
+import i18next, { t } from "i18next";
+import { formatDate } from "date-fns";
const uploadSaveGame = async (
_event: Electron.IpcMainInvokeEvent,
@@ -10,13 +10,15 @@ const uploadSaveGame = async (
shop: GameShop,
downloadOptionTitle: string | null
) => {
+ const { language } = i18next;
+
return CloudSync.uploadSaveGame(
objectId,
shop,
downloadOptionTitle,
t("backup_from", {
ns: "game_details",
- date: format(new Date(), "dd/MM/yyyy"),
+ date: formatDate(new Date(), language),
})
);
};
diff --git a/src/main/events/download-sources/create-download-sources.ts b/src/main/events/download-sources/create-download-sources.ts
new file mode 100644
index 00000000..cf1f8f51
--- /dev/null
+++ b/src/main/events/download-sources/create-download-sources.ts
@@ -0,0 +1,13 @@
+import { HydraApi } from "@main/services";
+import { registerEvent } from "../register-event";
+
+const createDownloadSources = async (
+ _event: Electron.IpcMainInvokeEvent,
+ urls: string[]
+) => {
+ await HydraApi.post("/profile/download-sources", {
+ urls,
+ });
+};
+
+registerEvent("createDownloadSources", createDownloadSources);
diff --git a/src/main/events/download-sources/get-download-sources.ts b/src/main/events/download-sources/get-download-sources.ts
new file mode 100644
index 00000000..bbebd06c
--- /dev/null
+++ b/src/main/events/download-sources/get-download-sources.ts
@@ -0,0 +1,8 @@
+import { HydraApi } from "@main/services";
+import { registerEvent } from "../register-event";
+
+const getDownloadSources = async (_event: Electron.IpcMainInvokeEvent) => {
+ return HydraApi.get("/profile/download-sources");
+};
+
+registerEvent("getDownloadSources", getDownloadSources);
diff --git a/src/main/events/download-sources/remove-download-source.ts b/src/main/events/download-sources/remove-download-source.ts
new file mode 100644
index 00000000..bcc66998
--- /dev/null
+++ b/src/main/events/download-sources/remove-download-source.ts
@@ -0,0 +1,18 @@
+import { HydraApi } from "@main/services";
+import { registerEvent } from "../register-event";
+
+const removeDownloadSource = async (
+ _event: Electron.IpcMainInvokeEvent,
+ url?: string,
+ removeAll = false
+) => {
+ const params = new URLSearchParams({
+ all: removeAll.toString(),
+ });
+
+ if (url) params.set("url", url);
+
+ return HydraApi.delete(`/profile/download-sources?${params.toString()}`);
+};
+
+registerEvent("removeDownloadSource", removeDownloadSource);
diff --git a/src/main/events/index.ts b/src/main/events/index.ts
index bcbbb43d..8465843f 100644
--- a/src/main/events/index.ts
+++ b/src/main/events/index.ts
@@ -20,6 +20,7 @@ import "./library/close-game";
import "./library/delete-game-folder";
import "./library/get-game-by-object-id";
import "./library/get-library";
+import "./library/extract-game-download";
import "./library/open-game";
import "./library/open-game-executable-path";
import "./library/open-game-installer";
@@ -38,6 +39,8 @@ import "./misc/show-open-dialog";
import "./misc/get-features";
import "./misc/show-item-in-folder";
import "./misc/get-badges";
+import "./misc/install-common-redist";
+import "./misc/can-install-common-redist";
import "./torrenting/cancel-game-download";
import "./torrenting/pause-game-download";
import "./torrenting/resume-game-download";
@@ -90,6 +93,9 @@ import "./themes/get-custom-theme-by-id";
import "./themes/get-active-custom-theme";
import "./themes/close-editor-window";
import "./themes/toggle-custom-theme";
+import "./download-sources/create-download-sources";
+import "./download-sources/remove-download-source";
+import "./download-sources/get-download-sources";
import { isPortableVersion } from "@main/helpers";
ipcMain.handle("ping", () => "pong");
diff --git a/src/main/events/library/extract-game-download.ts b/src/main/events/library/extract-game-download.ts
new file mode 100644
index 00000000..8fb24b81
--- /dev/null
+++ b/src/main/events/library/extract-game-download.ts
@@ -0,0 +1,46 @@
+import { registerEvent } from "../register-event";
+import { GameShop } from "@types";
+import path from "node:path";
+import { GameFilesManager } from "@main/services";
+import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
+import { FILE_EXTENSIONS_TO_EXTRACT } from "@shared";
+
+const extractGameDownload = async (
+ _event: Electron.IpcMainInvokeEvent,
+ shop: GameShop,
+ objectId: string
+): Promise => {
+ const gameKey = levelKeys.game(shop, objectId);
+
+ const [download, game] = await Promise.all([
+ downloadsSublevel.get(gameKey),
+ gamesSublevel.get(gameKey),
+ ]);
+
+ if (!download || !game) return false;
+
+ await downloadsSublevel.put(gameKey, {
+ ...download,
+ extracting: true,
+ });
+
+ const gameFilesManager = new GameFilesManager(shop, objectId);
+
+ if (
+ FILE_EXTENSIONS_TO_EXTRACT.some((ext) => download.folderName?.endsWith(ext))
+ ) {
+ gameFilesManager.extractDownloadedFile();
+ } else {
+ gameFilesManager
+ .extractFilesInDirectory(
+ path.join(download.downloadPath, download.folderName!)
+ )
+ .then(() => {
+ gameFilesManager.setExtractionComplete(false);
+ });
+ }
+
+ return true;
+};
+
+registerEvent("extractGameDownload", extractGameDownload);
diff --git a/src/main/events/library/update-executable-path.ts b/src/main/events/library/update-executable-path.ts
index e753706b..c60638d7 100644
--- a/src/main/events/library/update-executable-path.ts
+++ b/src/main/events/library/update-executable-path.ts
@@ -21,6 +21,8 @@ const updateExecutablePath = async (
await gamesSublevel.put(gameKey, {
...game,
executablePath: parsedPath,
+ automaticCloudSync:
+ executablePath === null ? false : game.automaticCloudSync,
});
};
diff --git a/src/main/events/misc/can-install-common-redist.ts b/src/main/events/misc/can-install-common-redist.ts
new file mode 100644
index 00000000..e2303966
--- /dev/null
+++ b/src/main/events/misc/can-install-common-redist.ts
@@ -0,0 +1,7 @@
+import { registerEvent } from "../register-event";
+import { CommonRedistManager } from "@main/services/common-redist-manager";
+
+const canInstallCommonRedist = async (_event: Electron.IpcMainInvokeEvent) =>
+ CommonRedistManager.canInstallCommonRedist();
+
+registerEvent("canInstallCommonRedist", canInstallCommonRedist);
diff --git a/src/main/events/misc/install-common-redist.ts b/src/main/events/misc/install-common-redist.ts
new file mode 100644
index 00000000..34e609ec
--- /dev/null
+++ b/src/main/events/misc/install-common-redist.ts
@@ -0,0 +1,10 @@
+import { registerEvent } from "../register-event";
+import { CommonRedistManager } from "@main/services/common-redist-manager";
+
+const installCommonRedist = async (_event: Electron.IpcMainInvokeEvent) => {
+ if (await CommonRedistManager.canInstallCommonRedist()) {
+ CommonRedistManager.installCommonRedist();
+ }
+};
+
+registerEvent("installCommonRedist", installCommonRedist);
diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts
index 8b5f1918..59f117d3 100644
--- a/src/main/events/torrenting/start-game-download.ts
+++ b/src/main/events/torrenting/start-game-download.ts
@@ -12,7 +12,15 @@ const startGameDownload = async (
_event: Electron.IpcMainInvokeEvent,
payload: StartGameDownloadPayload
) => {
- const { objectId, title, shop, downloadPath, downloader, uri } = payload;
+ const {
+ objectId,
+ title,
+ shop,
+ downloadPath,
+ downloader,
+ uri,
+ automaticallyExtract,
+ } = payload;
const gameKey = levelKeys.game(shop, objectId);
@@ -74,6 +82,8 @@ const startGameDownload = async (
shouldSeed: false,
timestamp: Date.now(),
queued: true,
+ extracting: false,
+ automaticallyExtract,
};
try {
diff --git a/src/main/events/user-preferences/update-user-preferences.ts b/src/main/events/user-preferences/update-user-preferences.ts
index 09f39d2d..7a481837 100644
--- a/src/main/events/user-preferences/update-user-preferences.ts
+++ b/src/main/events/user-preferences/update-user-preferences.ts
@@ -23,10 +23,6 @@ const updateUserPreferences = async (
patchUserProfile({ language: preferences.language }).catch(() => {});
}
- if (!preferences.downloadsPath) {
- preferences.downloadsPath = null;
- }
-
await db.put(
levelKeys.userPreferences,
{
diff --git a/src/main/knex-client.ts b/src/main/knex-client.ts
deleted file mode 100644
index 57982332..00000000
--- a/src/main/knex-client.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import knex from "knex";
-import { databasePath } from "./constants";
-import { app } from "electron";
-
-export const knexClient = knex({
- debug: !app.isPackaged,
- client: "better-sqlite3",
- connection: {
- filename: databasePath,
- },
-});
diff --git a/src/main/knexfile.ts b/src/main/knexfile.ts
deleted file mode 100644
index df7972a9..00000000
--- a/src/main/knexfile.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-const config = {
- development: {
- migrations: {
- extension: "ts",
- stub: "migrations/migration.stub",
- },
- },
-};
-
-export default config;
diff --git a/src/main/level/sublevels/keys.ts b/src/main/level/sublevels/keys.ts
index 6559e460..12143917 100644
--- a/src/main/level/sublevels/keys.ts
+++ b/src/main/level/sublevels/keys.ts
@@ -13,6 +13,5 @@ export const levelKeys = {
downloads: "downloads",
userPreferences: "userPreferences",
language: "language",
- sqliteMigrationDone: "sqliteMigrationDone",
screenState: "screenState",
};
diff --git a/src/main/main.ts b/src/main/main.ts
index 84bd7732..93986ac2 100644
--- a/src/main/main.ts
+++ b/src/main/main.ts
@@ -1,4 +1,4 @@
-import { DownloadManager, logger, Ludusavi, startMainLoop } from "./services";
+import { DownloadManager, Ludusavi, startMainLoop } from "./services";
import { RealDebridClient } from "./services/download/real-debrid";
import { HydraApi } from "./services/hydra-api";
import { uploadGamesBatch } from "./services/library-sync";
@@ -6,26 +6,18 @@ import { Aria2 } from "./services/aria2";
import { downloadsSublevel } from "./level/sublevels/downloads";
import { sortBy } from "lodash-es";
import { Downloader } from "@shared";
-import {
- gameAchievementsSublevel,
- gamesSublevel,
- levelKeys,
- db,
-} from "./level";
-import { Auth, User, type UserPreferences } from "@types";
-import { knexClient } from "./knex-client";
+import { levelKeys, db } from "./level";
+import type { UserPreferences } from "@types";
import { TorBoxClient } from "./services/download/torbox";
+import { CommonRedistManager } from "./services/common-redist-manager";
export const loadState = async () => {
- const userPreferences = await migrateFromSqlite().then(async () => {
- await db.put(levelKeys.sqliteMigrationDone, true, {
+ const userPreferences = await db.get(
+ levelKeys.userPreferences,
+ {
valueEncoding: "json",
- });
-
- return db.get(levelKeys.userPreferences, {
- valueEncoding: "json",
- });
- });
+ }
+ );
await import("./events");
@@ -52,6 +44,15 @@ export const loadState = async () => {
return sortBy(games, "timestamp", "DESC");
});
+ downloads.forEach((download) => {
+ if (download.extracting) {
+ downloadsSublevel.put(levelKeys.game(download.shop, download.objectId), {
+ ...download,
+ extracting: false,
+ });
+ }
+ });
+
const [nextItemOnQueue] = downloads.filter((game) => game.queued);
const downloadsToSeed = downloads.filter(
@@ -65,138 +66,6 @@ export const loadState = async () => {
await DownloadManager.startRPC(nextItemOnQueue, downloadsToSeed);
startMainLoop();
-};
-
-const migrateFromSqlite = async () => {
- const sqliteMigrationDone = await db.get(levelKeys.sqliteMigrationDone);
-
- if (sqliteMigrationDone) {
- return;
- }
-
- const migrateGames = knexClient("game")
- .select("*")
- .then((games) => {
- return gamesSublevel.batch(
- games.map((game) => ({
- type: "put",
- key: levelKeys.game(game.shop, game.objectID),
- value: {
- objectId: game.objectID,
- shop: game.shop,
- title: game.title,
- iconUrl: game.iconUrl,
- playTimeInMilliseconds: game.playTimeInMilliseconds,
- lastTimePlayed: game.lastTimePlayed,
- remoteId: game.remoteId,
- winePrefixPath: game.winePrefixPath,
- launchOptions: game.launchOptions,
- executablePath: game.executablePath,
- isDeleted: game.isDeleted === 1,
- },
- }))
- );
- })
- .then(() => {
- logger.info("Games migrated successfully");
- });
-
- const migrateUserPreferences = knexClient("user_preferences")
- .select("*")
- .then(async (userPreferences) => {
- if (userPreferences.length > 0) {
- const { realDebridApiToken, ...rest } = userPreferences[0];
-
- await db.put(
- levelKeys.userPreferences,
- {
- ...rest,
- realDebridApiToken,
- preferQuitInsteadOfHiding: rest.preferQuitInsteadOfHiding === 1,
- runAtStartup: rest.runAtStartup === 1,
- startMinimized: rest.startMinimized === 1,
- disableNsfwAlert: rest.disableNsfwAlert === 1,
- seedAfterDownloadComplete: rest.seedAfterDownloadComplete === 1,
- showHiddenAchievementsDescription:
- rest.showHiddenAchievementsDescription === 1,
- downloadNotificationsEnabled:
- rest.downloadNotificationsEnabled === 1,
- repackUpdatesNotificationsEnabled:
- rest.repackUpdatesNotificationsEnabled === 1,
- achievementNotificationsEnabled:
- rest.achievementNotificationsEnabled === 1,
- },
- { valueEncoding: "json" }
- );
-
- if (rest.language) {
- await db.put(levelKeys.language, rest.language, {
- valueEncoding: "utf-8",
- });
- }
- }
- })
- .then(() => {
- logger.info("User preferences migrated successfully");
- });
-
- const migrateAchievements = knexClient("game_achievement")
- .select("*")
- .then((achievements) => {
- return gameAchievementsSublevel.batch(
- achievements.map((achievement) => ({
- type: "put",
- key: levelKeys.game(achievement.shop, achievement.objectId),
- value: {
- achievements: JSON.parse(achievement.achievements),
- unlockedAchievements: JSON.parse(achievement.unlockedAchievements),
- },
- }))
- );
- })
- .then(() => {
- logger.info("Achievements migrated successfully");
- });
-
- const migrateUser = knexClient("user_auth")
- .select("*")
- .then(async (users) => {
- if (users.length > 0) {
- await db.put(
- levelKeys.user,
- {
- id: users[0].userId,
- displayName: users[0].displayName,
- profileImageUrl: users[0].profileImageUrl,
- backgroundImageUrl: users[0].backgroundImageUrl,
- subscription: users[0].subscription,
- },
- {
- valueEncoding: "json",
- }
- );
-
- await db.put(
- levelKeys.auth,
- {
- accessToken: users[0].accessToken,
- refreshToken: users[0].refreshToken,
- tokenExpirationTimestamp: users[0].tokenExpirationTimestamp,
- },
- {
- valueEncoding: "json",
- }
- );
- }
- })
- .then(() => {
- logger.info("User data migrated successfully");
- });
-
- return Promise.allSettled([
- migrateGames,
- migrateUserPreferences,
- migrateAchievements,
- migrateUser,
- ]);
+
+ CommonRedistManager.downloadCommonRedist();
};
diff --git a/src/main/services/7zip.ts b/src/main/services/7zip.ts
new file mode 100644
index 00000000..08abf389
--- /dev/null
+++ b/src/main/services/7zip.ts
@@ -0,0 +1,76 @@
+import { app } from "electron";
+import cp from "node:child_process";
+import path from "node:path";
+import { logger } from "./logger";
+
+export const binaryName = {
+ linux: "7zzs",
+ darwin: "7zz",
+ win32: "7z.exe",
+};
+
+export class SevenZip {
+ private static readonly binaryPath = app.isPackaged
+ ? path.join(process.resourcesPath, binaryName[process.platform])
+ : path.join(
+ __dirname,
+ "..",
+ "..",
+ "binaries",
+ binaryName[process.platform]
+ );
+
+ public static extractFile(
+ {
+ filePath,
+ outputPath,
+ cwd,
+ passwords = [],
+ }: {
+ filePath: string;
+ outputPath?: string;
+ cwd?: string;
+ passwords?: string[];
+ },
+ successCb: () => void,
+ errorCb: () => void
+ ) {
+ const tryPassword = (index = -1) => {
+ const password = passwords[index] ?? "";
+ logger.info(`Trying password ${password} on ${filePath}`);
+
+ const args = ["x", filePath, "-y", "-p" + password];
+
+ if (outputPath) {
+ args.push("-o" + outputPath);
+ }
+
+ const child = cp.execFile(this.binaryPath, args, {
+ cwd,
+ });
+
+ child.once("exit", (code) => {
+ console.log("EXIT CALLED", code, filePath);
+
+ if (code === 0) {
+ successCb();
+ return;
+ }
+
+ if (index < passwords.length - 1) {
+ logger.info(
+ `Failed to extract file: ${filePath} with password: ${password}. Trying next password...`
+ );
+
+ tryPassword(index + 1);
+ } else {
+ logger.info(`Failed to extract file: ${filePath}`);
+
+ errorCb();
+ }
+ });
+ };
+
+ tryPassword();
+ }
+}
diff --git a/src/main/services/aria2.ts b/src/main/services/aria2.ts
index b7aa539c..a927a1bd 100644
--- a/src/main/services/aria2.ts
+++ b/src/main/services/aria2.ts
@@ -2,23 +2,26 @@ import path from "node:path";
import cp from "node:child_process";
import { app } from "electron";
-export const startAria2 = () => {};
-
export class Aria2 {
private static process: cp.ChildProcess | null = null;
+ private static readonly binaryPath = app.isPackaged
+ ? path.join(process.resourcesPath, "aria2", "aria2c")
+ : path.join(__dirname, "..", "..", "aria2", "aria2c");
public static spawn() {
- const binaryPath = app.isPackaged
- ? path.join(process.resourcesPath, "aria2", "aria2c")
- : path.join(__dirname, "..", "..", "aria2", "aria2c");
-
this.process = cp.spawn(
- binaryPath,
+ this.binaryPath,
[
"--enable-rpc",
"--rpc-listen-all",
"--file-allocation=none",
"--allow-overwrite=true",
+ "-s",
+ "16",
+ "-x",
+ "16",
+ "-k",
+ "1M",
],
{ stdio: "inherit", windowsHide: true }
);
diff --git a/src/main/services/common-redist-manager.ts b/src/main/services/common-redist-manager.ts
new file mode 100644
index 00000000..2a08bfab
--- /dev/null
+++ b/src/main/services/common-redist-manager.ts
@@ -0,0 +1,109 @@
+import { commonRedistPath } from "@main/constants";
+import axios from "axios";
+import fs from "node:fs";
+import cp from "node:child_process";
+import path from "node:path";
+import { logger } from "./logger";
+import { app } from "electron";
+import { WindowManager } from "./window-manager";
+
+export class CommonRedistManager {
+ private static readonly redistributables = [
+ "dotNetFx40_Full_setup.exe",
+ "dxwebsetup.exe",
+ "oalinst.exe",
+ "install.bat",
+ "vcredist_2015-2019_x64.exe",
+ "vcredist_2015-2019_x86.exe",
+ "vcredist_x64.exe",
+ "vcredist_x86.exe",
+ "xnafx40_redist.msi",
+ ];
+ private static readonly installationTimeout = 1000 * 60 * 5; // 5 minutes
+ private static readonly installationLog = path.join(
+ app.getPath("temp"),
+ "common_redist_install.log"
+ );
+
+ public static async installCommonRedist() {
+ const abortController = new AbortController();
+ const timeout = setTimeout(() => {
+ abortController.abort();
+ logger.error("Installation timed out");
+
+ WindowManager.mainWindow?.webContents.send("common-redist-progress", {
+ log: "Installation timed out",
+ complete: false,
+ });
+ }, this.installationTimeout);
+
+ const installationCompleteMessage = "Installation complete";
+
+ if (!fs.existsSync(this.installationLog)) {
+ await fs.promises.writeFile(this.installationLog, "");
+ }
+
+ fs.watch(this.installationLog, { signal: abortController.signal }, () => {
+ fs.readFile(this.installationLog, "utf-8", (err, data) => {
+ if (err) return logger.error("Error reading log file:", err);
+
+ const tail = data.split("\n").at(-2)?.trim();
+
+ if (tail?.includes(installationCompleteMessage)) {
+ clearTimeout(timeout);
+ if (!abortController.signal.aborted) {
+ abortController.abort();
+ }
+ }
+
+ WindowManager.mainWindow?.webContents.send("common-redist-progress", {
+ log: tail,
+ complete: tail?.includes(installationCompleteMessage),
+ });
+ });
+ });
+
+ cp.exec(
+ path.join(commonRedistPath, "install.bat"),
+ {
+ windowsHide: true,
+ },
+ (error) => {
+ if (error) {
+ logger.error("Failed to run install.bat", error);
+ }
+ }
+ );
+ }
+
+ public static async canInstallCommonRedist() {
+ return this.redistributables.every((redist) => {
+ const filePath = path.join(commonRedistPath, redist);
+
+ return fs.existsSync(filePath);
+ });
+ }
+
+ public static async downloadCommonRedist() {
+ if (!fs.existsSync(commonRedistPath)) {
+ await fs.promises.mkdir(commonRedistPath, { recursive: true });
+ }
+
+ for (const redist of this.redistributables) {
+ const filePath = path.join(commonRedistPath, redist);
+
+ if (fs.existsSync(filePath)) {
+ continue;
+ }
+
+ const response = await axios.get(
+ `https://github.com/hydralauncher/hydra-common-redist/raw/refs/heads/main/${redist}`,
+ {
+ responseType: "arraybuffer",
+ }
+ );
+
+ await fs.promises.writeFile(filePath, response.data);
+ }
+ }
+}
diff --git a/src/main/services/download/download-manager.ts b/src/main/services/download/download-manager.ts
index 860cf5cf..9eba39f3 100644
--- a/src/main/services/download/download-manager.ts
+++ b/src/main/services/download/download-manager.ts
@@ -1,4 +1,4 @@
-import { Downloader, DownloadError } from "@shared";
+import { Downloader, DownloadError, FILE_EXTENSIONS_TO_EXTRACT } from "@shared";
import { WindowManager } from "../window-manager";
import { publishDownloadCompleteNotification } from "../notifications";
import type { Download, DownloadProgress, UserPreferences } from "@types";
@@ -22,6 +22,7 @@ import { logger } from "../logger";
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
import { sortBy } from "lodash-es";
import { TorBoxClient } from "./torbox";
+import { GameFilesManager } from "../game-files-manager";
export class DownloadManager {
private static downloadingGameId: string | null = null;
@@ -136,6 +137,8 @@ export class DownloadManager {
);
}
+ const shouldExtract = download.automaticallyExtract;
+
if (progress === 1 && download) {
publishDownloadCompleteNotification(game);
@@ -143,23 +146,48 @@ export class DownloadManager {
userPreferences?.seedAfterDownloadComplete &&
download.downloader === Downloader.Torrent
) {
- downloadsSublevel.put(gameId, {
+ await downloadsSublevel.put(gameId, {
...download,
status: "seeding",
shouldSeed: true,
queued: false,
+ extracting: shouldExtract,
});
} else {
- downloadsSublevel.put(gameId, {
+ await downloadsSublevel.put(gameId, {
...download,
status: "complete",
shouldSeed: false,
queued: false,
+ extracting: shouldExtract,
});
this.cancelDownload(gameId);
}
+ if (shouldExtract) {
+ const gameFilesManager = new GameFilesManager(
+ game.shop,
+ game.objectId
+ );
+
+ if (
+ FILE_EXTENSIONS_TO_EXTRACT.some((ext) =>
+ download.folderName?.endsWith(ext)
+ )
+ ) {
+ gameFilesManager.extractDownloadedFile();
+ } else {
+ gameFilesManager
+ .extractFilesInDirectory(
+ path.join(download.downloadPath, download.folderName!)
+ )
+ .then(() => {
+ gameFilesManager.setExtractionComplete();
+ });
+ }
+ }
+
const downloads = await downloadsSublevel
.values()
.all()
diff --git a/src/main/services/game-files-manager.ts b/src/main/services/game-files-manager.ts
new file mode 100644
index 00000000..120b3e8f
--- /dev/null
+++ b/src/main/services/game-files-manager.ts
@@ -0,0 +1,158 @@
+import path from "node:path";
+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 } from "./7zip";
+import { WindowManager } from "./window-manager";
+import { publishExtractionCompleteNotification } from "./notifications";
+import { logger } from "./logger";
+
+export class GameFilesManager {
+ constructor(
+ private readonly shop: GameShop,
+ private readonly objectId: string
+ ) {}
+
+ private async clearExtractionState() {
+ const gameKey = levelKeys.game(this.shop, this.objectId);
+ const download = await downloadsSublevel.get(gameKey);
+
+ await downloadsSublevel.put(gameKey, {
+ ...download!,
+ extracting: false,
+ });
+
+ WindowManager.mainWindow?.webContents.send(
+ "on-extraction-complete",
+ this.shop,
+ this.objectId
+ );
+ }
+
+ async extractFilesInDirectory(directoryPath: string) {
+ if (!fs.existsSync(directoryPath)) return;
+ const files = await fs.promises.readdir(directoryPath);
+
+ const compressedFiles = files.filter((file) =>
+ FILE_EXTENSIONS_TO_EXTRACT.some((ext) => file.endsWith(ext))
+ );
+
+ const filesToExtract = compressedFiles.filter(
+ (file) => /part1\.rar$/i.test(file) || !/part\d+\.rar$/i.test(file)
+ );
+
+ 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();
+ }
+ );
+ });
+ })
+ );
+
+ 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();
+ }
+ });
+ }
+ });
+ }
+
+ async setExtractionComplete(publishNotification = true) {
+ const gameKey = levelKeys.game(this.shop, this.objectId);
+
+ const [download, game] = await Promise.all([
+ downloadsSublevel.get(gameKey),
+ gamesSublevel.get(gameKey),
+ ]);
+
+ await downloadsSublevel.put(gameKey, {
+ ...download!,
+ extracting: false,
+ });
+
+ WindowManager.mainWindow?.webContents.send(
+ "on-extraction-complete",
+ this.shop,
+ this.objectId
+ );
+
+ if (publishNotification) {
+ publishExtractionCompleteNotification(game!);
+ }
+ }
+
+ async extractDownloadedFile() {
+ const gameKey = levelKeys.game(this.shop, this.objectId);
+
+ const [download, game] = await Promise.all([
+ downloadsSublevel.get(gameKey),
+ gamesSublevel.get(gameKey),
+ ]);
+
+ if (!download || !game) return false;
+
+ const filePath = path.join(download.downloadPath, download.folderName!);
+
+ const extractionPath = path.join(
+ download.downloadPath,
+ path.parse(download.folderName!).name
+ );
+
+ SevenZip.extractFile(
+ {
+ filePath,
+ outputPath: extractionPath,
+ passwords: ["online-fix.me", "steamrip.com"],
+ },
+ async () => {
+ await this.extractFilesInDirectory(extractionPath);
+
+ if (fs.existsSync(extractionPath) && fs.existsSync(filePath)) {
+ fs.unlink(filePath, (err) => {
+ if (err) {
+ logger.error(
+ `Failed to delete file: ${download.folderName}`,
+ err
+ );
+
+ this.clearExtractionState();
+ }
+ });
+ }
+
+ await downloadsSublevel.put(gameKey, {
+ ...download!,
+ folderName: path.parse(download.folderName!).name,
+ });
+
+ this.setExtractionComplete();
+ },
+ () => {
+ this.clearExtractionState();
+ }
+ );
+
+ return true;
+ }
+}
diff --git a/src/main/services/index.ts b/src/main/services/index.ts
index f53dae31..30b502f5 100644
--- a/src/main/services/index.ts
+++ b/src/main/services/index.ts
@@ -8,3 +8,6 @@ export * from "./main-loop";
export * from "./hydra-api";
export * from "./ludusavi";
export * from "./cloud-sync";
+export * from "./7zip";
+export * from "./game-files-manager";
+export * from "./common-redist-manager";
diff --git a/src/main/services/notifications/index.ts b/src/main/services/notifications/index.ts
index 6ddb3200..79f200fe 100644
--- a/src/main/services/notifications/index.ts
+++ b/src/main/services/notifications/index.ts
@@ -128,6 +128,17 @@ export const publishCombinedNewAchievementNotification = async (
}
};
+export const publishExtractionCompleteNotification = async (game: Game) => {
+ new Notification({
+ title: t("extraction_complete", { ns: "notifications" }),
+ body: t("game_extracted", {
+ ns: "notifications",
+ title: game.title,
+ }),
+ icon: trayIcon,
+ }).show();
+};
+
export const publishNewAchievementNotification = async (info: {
achievements: { displayName: string; iconUrl: string }[];
unlockedAchievementCount: number;
diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts
index f7e30742..de0e88da 100644
--- a/src/main/services/process-watcher.ts
+++ b/src/main/services/process-watcher.ts
@@ -6,9 +6,9 @@ import axios from "axios";
import { exec } from "child_process";
import { ProcessPayload } from "./download/types";
import { gamesSublevel, levelKeys } from "@main/level";
-import { t } from "i18next";
+import i18next, { t } from "i18next";
import { CloudSync } from "./cloud-sync";
-import { format } from "date-fns";
+import { formatDate } from "date-fns";
const commands = {
findWineDir: `lsof -c wine 2>/dev/null | grep '/drive_c/windows$' | head -n 1 | awk '{for(i=9;i<=NF;i++) printf "%s ", $i; print ""}'`,
@@ -229,6 +229,8 @@ function onOpenGame(game: Game) {
if (game.remoteId) {
updateGamePlaytime(game, 0, new Date()).catch(() => {});
+ const { language } = i18next;
+
if (game.automaticCloudSync) {
CloudSync.uploadSaveGame(
game.objectId,
@@ -236,7 +238,7 @@ function onOpenGame(game: Game) {
null,
t("automatic_backup_from", {
ns: "game_details",
- date: format(new Date(), "dd/MM/yyyy"),
+ date: formatDate(new Date(), language),
})
);
}
@@ -296,6 +298,8 @@ const onCloseGame = (game: Game) => {
)!;
gamesPlaytime.delete(levelKeys.game(game.shop, game.objectId));
+ const { language } = i18next;
+
if (game.remoteId) {
updateGamePlaytime(
game,
@@ -310,7 +314,7 @@ const onCloseGame = (game: Game) => {
null,
t("automatic_backup_from", {
ns: "game_details",
- date: format(new Date(), "dd/MM/yyyy"),
+ date: formatDate(new Date(), language),
})
);
}
diff --git a/src/preload/index.ts b/src/preload/index.ts
index b49595a7..280c0cc4 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -100,6 +100,11 @@ contextBridge.exposeInMainWorld("electron", {
/* Download sources */
putDownloadSource: (objectIds: string[]) =>
ipcRenderer.invoke("putDownloadSource", objectIds),
+ createDownloadSources: (urls: string[]) =>
+ ipcRenderer.invoke("createDownloadSources", urls),
+ removeDownloadSource: (url: string, removeAll?: boolean) =>
+ ipcRenderer.invoke("removeDownloadSource", url, removeAll),
+ getDownloadSources: () => ipcRenderer.invoke("getDownloadSources"),
/* Library */
toggleAutomaticCloudSync: (
@@ -173,6 +178,8 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("getGameByObjectId", shop, objectId),
resetGameAchievements: (shop: GameShop, objectId: string) =>
ipcRenderer.invoke("resetGameAchievements", shop, objectId),
+ extractGameDownload: (shop: GameShop, objectId: string) =>
+ ipcRenderer.invoke("extractGameDownload", shop, objectId),
onGamesRunning: (
cb: (
gamesRunning: Pick[]
@@ -195,6 +202,15 @@ contextBridge.exposeInMainWorld("electron", {
return () =>
ipcRenderer.removeListener("on-achievement-unlocked", listener);
},
+ onExtractionComplete: (cb: (shop: GameShop, objectId: string) => void) => {
+ const listener = (
+ _event: Electron.IpcRendererEvent,
+ shop: GameShop,
+ objectId: string
+ ) => cb(shop, objectId);
+ ipcRenderer.on("on-extraction-complete", listener);
+ return () => ipcRenderer.removeListener("on-extraction-complete", listener);
+ },
/* Hardware */
getDiskFreeSpace: (path: string) =>
@@ -279,6 +295,8 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("showItemInFolder", path),
getFeatures: () => ipcRenderer.invoke("getFeatures"),
getBadges: () => ipcRenderer.invoke("getBadges"),
+ canInstallCommonRedist: () => ipcRenderer.invoke("canInstallCommonRedist"),
+ installCommonRedist: () => ipcRenderer.invoke("installCommonRedist"),
platform: process.platform,
/* Auto update */
@@ -294,6 +312,16 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.removeListener("autoUpdaterEvent", listener);
};
},
+ onCommonRedistProgress: (
+ cb: (value: { log: string; complete: boolean }) => void
+ ) => {
+ const listener = (
+ _event: Electron.IpcRendererEvent,
+ value: { log: string; complete: boolean }
+ ) => cb(value);
+ ipcRenderer.on("common-redist-progress", listener);
+ return () => ipcRenderer.removeListener("common-redist-progress", listener);
+ },
checkForUpdates: () => ipcRenderer.invoke("checkForUpdates"),
restartAndInstallUpdate: () => ipcRenderer.invoke("restartAndInstallUpdate"),
diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx
index f9bd645e..b1867279 100644
--- a/src/renderer/src/app.tsx
+++ b/src/renderer/src/app.tsx
@@ -31,6 +31,7 @@ import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-m
import { injectCustomCss } from "./helpers";
import "./app.scss";
+import { DownloadSource } from "@types";
export interface AppProps {
children: React.ReactNode;
@@ -136,6 +137,70 @@ export function App() {
});
}, [fetchUserDetails, updateUserDetails, dispatch]);
+ const syncDownloadSources = useCallback(async () => {
+ const downloadSources = await window.electron.getDownloadSources();
+
+ const existingDownloadSources: DownloadSource[] =
+ await downloadSourcesTable.toArray();
+
+ window.electron.createDownloadSources(
+ existingDownloadSources.map((source) => source.url)
+ );
+
+ await Promise.allSettled(
+ downloadSources.map(async (source) => {
+ return new Promise((resolve) => {
+ const existingDownloadSource = existingDownloadSources.find(
+ (downloadSource) => downloadSource.url === source.url
+ );
+
+ if (!existingDownloadSource) {
+ const channel = new BroadcastChannel(
+ `download_sources:import:${source.url}`
+ );
+
+ downloadSourcesWorker.postMessage([
+ "IMPORT_DOWNLOAD_SOURCE",
+ source.url,
+ ]);
+
+ channel.onmessage = () => {
+ resolve(true);
+ channel.close();
+ };
+ } else {
+ resolve(true);
+ }
+ });
+ })
+ );
+
+ updateRepacks();
+
+ const id = crypto.randomUUID();
+ const channel = new BroadcastChannel(`download_sources:sync:${id}`);
+
+ channel.onmessage = async (event: MessageEvent) => {
+ const newRepacksCount = event.data;
+ window.electron.publishNewRepacksNotification(newRepacksCount);
+ updateRepacks();
+
+ const downloadSources = await downloadSourcesTable.toArray();
+
+ downloadSources
+ .filter((source) => !source.fingerprint)
+ .forEach(async (downloadSource) => {
+ const { fingerprint } = await window.electron.putDownloadSource(
+ downloadSource.objectIds
+ );
+
+ downloadSourcesTable.update(downloadSource.id, { fingerprint });
+ });
+ };
+
+ downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]);
+ }, [updateRepacks]);
+
const onSignIn = useCallback(() => {
fetchUserDetails().then((response) => {
if (response) {
@@ -144,7 +209,15 @@ export function App() {
showSuccessToast(t("successfully_signed_in"));
}
});
- }, [fetchUserDetails, t, showSuccessToast, updateUserDetails]);
+
+ syncDownloadSources();
+ }, [
+ fetchUserDetails,
+ t,
+ showSuccessToast,
+ updateUserDetails,
+ syncDownloadSources,
+ ]);
useEffect(() => {
const unsubscribe = window.electron.onSyncFriendRequests((result) => {
@@ -212,31 +285,8 @@ export function App() {
}, [dispatch, draggingDisabled]);
useEffect(() => {
- updateRepacks();
-
- const id = crypto.randomUUID();
- const channel = new BroadcastChannel(`download_sources:sync:${id}`);
-
- channel.onmessage = async (event: MessageEvent) => {
- const newRepacksCount = event.data;
- window.electron.publishNewRepacksNotification(newRepacksCount);
- updateRepacks();
-
- const downloadSources = await downloadSourcesTable.toArray();
-
- downloadSources
- .filter((source) => !source.fingerprint)
- .forEach(async (downloadSource) => {
- const { fingerprint } = await window.electron.putDownloadSource(
- downloadSource.objectIds
- );
-
- downloadSourcesTable.update(downloadSource.id, { fingerprint });
- });
- };
-
- downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]);
- }, [updateRepacks]);
+ syncDownloadSources();
+ }, [syncDownloadSources]);
useEffect(() => {
const loadAndApplyTheme = async () => {
diff --git a/src/renderer/src/components/bottom-panel/bottom-panel.tsx b/src/renderer/src/components/bottom-panel/bottom-panel.tsx
index 16f1de06..2c32c5da 100644
--- a/src/renderer/src/components/bottom-panel/bottom-panel.tsx
+++ b/src/renderer/src/components/bottom-panel/bottom-panel.tsx
@@ -1,7 +1,12 @@
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
-import { useDownload, useLibrary, useUserDetails } from "@renderer/hooks";
+import {
+ useDownload,
+ useLibrary,
+ useToast,
+ useUserDetails,
+} from "@renderer/hooks";
import "./bottom-panel.scss";
@@ -17,20 +22,52 @@ export function BottomPanel() {
const { library } = useLibrary();
+ const { showSuccessToast } = useToast();
+
const { lastPacket, progress, downloadSpeed, eta } = useDownload();
const [version, setVersion] = useState("");
const [sessionHash, setSessionHash] = useState("");
+ const [commonRedistStatus, setCommonRedistStatus] = useState(
+ null
+ );
useEffect(() => {
window.electron.getVersion().then((result) => setVersion(result));
}, []);
+ useEffect(() => {
+ const unlisten = window.electron.onCommonRedistProgress(
+ ({ log, complete }) => {
+ if (log === "Installation timed out" || complete) {
+ setCommonRedistStatus(null);
+
+ if (complete) {
+ showSuccessToast(
+ t("installation_complete"),
+ t("installation_complete_message")
+ );
+ }
+
+ return;
+ }
+
+ setCommonRedistStatus(log);
+ }
+ );
+
+ return () => unlisten();
+ }, [t, showSuccessToast]);
+
useEffect(() => {
window.electron.getSessionHash().then((result) => setSessionHash(result));
}, [userDetails?.id]);
const status = useMemo(() => {
+ if (commonRedistStatus) {
+ return t("installing_common_redist", { log: commonRedistStatus });
+ }
+
const game = lastPacket
? library.find((game) => game.id === lastPacket?.gameId)
: undefined;
@@ -64,7 +101,15 @@ export function BottomPanel() {
}
return t("no_downloads_in_progress");
- }, [t, library, lastPacket, progress, eta, downloadSpeed]);
+ }, [
+ t,
+ library,
+ lastPacket,
+ progress,
+ eta,
+ downloadSpeed,
+ commonRedistStatus,
+ ]);
return (
+
+ setAutomaticExtractionEnabled(!automaticExtractionEnabled)
+ }
+ />
+