diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 431df932..c9094117 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -22,17 +22,6 @@ jobs:
- name: Install dependencies
run: yarn
- - name: Install Python
- uses: actions/setup-python@v5
- with:
- python-version: 3.9
-
- - name: Install dependencies
- run: pip install -r requirements.txt
-
- - name: Build with cx_Freeze
- run: python torrent-client/setup.py build
-
- name: Build Linux
if: matrix.os == 'ubuntu-latest'
run: yarn build:linux
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index c51d9ea6..4eee0aad 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -24,17 +24,6 @@ jobs:
- name: Install dependencies
run: yarn
- - name: Install Python
- uses: actions/setup-python@v5
- with:
- python-version: 3.9
-
- - name: Install dependencies
- run: pip install -r requirements.txt
-
- - name: Build with cx_Freeze
- run: python torrent-client/setup.py build
-
- name: Build Linux
if: matrix.os == 'ubuntu-latest'
run: yarn build:linux
diff --git a/.gitignore b/.gitignore
index 69af659f..1cd10467 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,6 @@
.vscode
node_modules
-hydra-download-manager
+aria2*
fastlist.exe
__pycache__
dist
diff --git a/electron-builder.yml b/electron-builder.yml
index 1dbac52a..b9a4acc6 100644
--- a/electron-builder.yml
+++ b/electron-builder.yml
@@ -3,7 +3,6 @@ productName: Hydra
directories:
buildResources: build
extraResources:
- - hydra-download-manager
- hydra.db
- fastlist.exe
- seeds
diff --git a/hydra.db b/hydra.db
index 4522e1ae..0015965f 100644
Binary files a/hydra.db and b/hydra.db differ
diff --git a/package.json b/package.json
index 6cb748ac..97944d50 100644
--- a/package.json
+++ b/package.json
@@ -41,6 +41,7 @@
"@reduxjs/toolkit": "^2.2.3",
"@vanilla-extract/css": "^1.14.2",
"@vanilla-extract/recipes": "^0.5.2",
+ "aria2": "^4.1.2",
"auto-launch": "^5.0.6",
"axios": "^1.6.8",
"better-sqlite3": "^9.5.0",
diff --git a/src/main/declaration.d.ts b/src/main/declaration.d.ts
new file mode 100644
index 00000000..ac2675a3
--- /dev/null
+++ b/src/main/declaration.d.ts
@@ -0,0 +1,80 @@
+declare module "aria2" {
+ export type Aria2Status =
+ | "active"
+ | "waiting"
+ | "paused"
+ | "error"
+ | "complete"
+ | "removed";
+
+ export interface StatusResponse {
+ gid: string;
+ status: Aria2Status;
+ totalLength: string;
+ completedLength: string;
+ uploadLength: string;
+ bitfield: string;
+ downloadSpeed: string;
+ uploadSpeed: string;
+ infoHash?: string;
+ numSeeders?: string;
+ seeder?: boolean;
+ pieceLength: string;
+ numPieces: string;
+ connections: string;
+ errorCode?: string;
+ errorMessage?: string;
+ followedBy?: string[];
+ following: string;
+ belongsTo: string;
+ dir: string;
+ files: {
+ path: string;
+ length: string;
+ completedLength: string;
+ selected: string;
+ }[];
+ bittorrent?: {
+ announceList: string[][];
+ comment: string;
+ creationDate: string;
+ mode: "single" | "multi";
+ info: {
+ name: string;
+ verifiedLength: string;
+ verifyIntegrityPending: string;
+ };
+ };
+ }
+
+ export default class Aria2 {
+ constructor(options: any);
+ open: () => Promise;
+ call(
+ method: "addUri",
+ uris: string[],
+ options: { dir: string }
+ ): Promise;
+ call(
+ method: "tellStatus",
+ gid: string,
+ keys?: string[]
+ ): Promise;
+ call(method: "pause", gid: string): Promise;
+ call(method: "forcePause", gid: string): Promise;
+ call(method: "unpause", gid: string): Promise;
+ call(method: "remove", gid: string): Promise;
+ call(method: "forceRemove", gid: string): Promise;
+ call(method: "pauseAll"): Promise;
+ call(method: "forcePauseAll"): Promise;
+ listNotifications: () => [
+ "onDownloadStart",
+ "onDownloadPause",
+ "onDownloadStop",
+ "onDownloadComplete",
+ "onDownloadError",
+ "onBtDownloadComplete",
+ ];
+ on: (event: string, callback: (params: any) => void) => void;
+ }
+}
diff --git a/src/main/entity/game.entity.ts b/src/main/entity/game.entity.ts
index 91e19ea6..fd168f51 100644
--- a/src/main/entity/game.entity.ts
+++ b/src/main/entity/game.entity.ts
@@ -10,7 +10,8 @@ import {
import { Repack } from "./repack.entity";
import type { GameShop } from "@types";
-import { Downloader, GameStatus } from "@shared";
+import { Downloader } from "@shared";
+import type { Aria2Status } from "aria2";
@Entity("game")
export class Game {
@@ -42,7 +43,7 @@ export class Game {
shop: GameShop;
@Column("text", { nullable: true })
- status: GameStatus | null;
+ status: Aria2Status | null;
@Column("int", { default: Downloader.Torrent })
downloader: Downloader;
@@ -53,9 +54,6 @@ export class Game {
@Column("float", { default: 0 })
progress: number;
- @Column("float", { default: 0 })
- fileVerificationProgress: number;
-
@Column("int", { default: 0 })
bytesDownloaded: number;
diff --git a/src/main/events/library/delete-game-folder.ts b/src/main/events/library/delete-game-folder.ts
index 954367a0..adfafefb 100644
--- a/src/main/events/library/delete-game-folder.ts
+++ b/src/main/events/library/delete-game-folder.ts
@@ -1,7 +1,6 @@
import path from "node:path";
import fs from "node:fs";
-import { GameStatus } from "@shared";
import { gameRepository } from "@main/repository";
import { getDownloadsPath } from "../helpers/get-downloads-path";
@@ -15,7 +14,7 @@ const deleteGameFolder = async (
const game = await gameRepository.findOne({
where: {
id: gameId,
- status: GameStatus.Cancelled,
+ status: "removed",
isDeleted: false,
},
});
diff --git a/src/main/events/library/get-library.ts b/src/main/events/library/get-library.ts
index 2374c497..4fd4e254 100644
--- a/src/main/events/library/get-library.ts
+++ b/src/main/events/library/get-library.ts
@@ -2,7 +2,6 @@ import { gameRepository } from "@main/repository";
import { searchRepacks } from "../helpers/search-games";
import { registerEvent } from "../register-event";
-import { GameStatus } from "@shared";
import { sortBy } from "lodash-es";
const getLibrary = async () =>
@@ -24,7 +23,7 @@ const getLibrary = async () =>
...game,
repacks: searchRepacks(game.title),
})),
- (game) => (game.status !== GameStatus.Cancelled ? 0 : 1)
+ (game) => (game.status !== "removed" ? 0 : 1)
)
);
diff --git a/src/main/events/library/remove-game.ts b/src/main/events/library/remove-game.ts
index 57b10b37..54bf66b8 100644
--- a/src/main/events/library/remove-game.ts
+++ b/src/main/events/library/remove-game.ts
@@ -1,6 +1,5 @@
import { registerEvent } from "../register-event";
import { gameRepository } from "../../repository";
-import { GameStatus } from "@shared";
const removeGame = async (
_event: Electron.IpcMainInvokeEvent,
@@ -9,7 +8,7 @@ const removeGame = async (
await gameRepository.update(
{
id: gameId,
- status: GameStatus.Cancelled,
+ status: "removed",
},
{
status: null,
diff --git a/src/main/events/torrenting/cancel-game-download.ts b/src/main/events/torrenting/cancel-game-download.ts
index 18d29fde..3c9a0715 100644
--- a/src/main/events/torrenting/cancel-game-download.ts
+++ b/src/main/events/torrenting/cancel-game-download.ts
@@ -1,53 +1,25 @@
import { gameRepository } from "@main/repository";
import { registerEvent } from "../register-event";
-import { WindowManager } from "@main/services";
-import { In } from "typeorm";
import { DownloadManager } from "@main/services";
-import { GameStatus } from "@shared";
const cancelGameDownload = async (
_event: Electron.IpcMainInvokeEvent,
gameId: number
) => {
- const game = await gameRepository.findOne({
- where: {
+ await DownloadManager.cancelDownload(gameId);
+
+ await gameRepository.update(
+ {
id: gameId,
- isDeleted: false,
- status: In([
- GameStatus.Downloading,
- GameStatus.DownloadingMetadata,
- GameStatus.CheckingFiles,
- GameStatus.Paused,
- GameStatus.Seeding,
- GameStatus.Finished,
- ]),
},
- });
-
- if (!game) return;
- DownloadManager.cancelDownload();
-
- await gameRepository
- .update(
- {
- id: game.id,
- },
- {
- status: GameStatus.Cancelled,
- bytesDownloaded: 0,
- progress: 0,
- }
- )
- .then((result) => {
- if (
- game.status !== GameStatus.Paused &&
- game.status !== GameStatus.Seeding
- ) {
- if (result.affected) WindowManager.mainWindow?.setProgressBar(-1);
- }
- });
+ {
+ status: "removed",
+ bytesDownloaded: 0,
+ progress: 0,
+ }
+ );
};
registerEvent("cancelGameDownload", cancelGameDownload);
diff --git a/src/main/events/torrenting/pause-game-download.ts b/src/main/events/torrenting/pause-game-download.ts
index ceda70cc..f9ed1102 100644
--- a/src/main/events/torrenting/pause-game-download.ts
+++ b/src/main/events/torrenting/pause-game-download.ts
@@ -1,30 +1,13 @@
import { registerEvent } from "../register-event";
import { gameRepository } from "../../repository";
-import { In } from "typeorm";
-import { DownloadManager, WindowManager } from "@main/services";
-import { GameStatus } from "@shared";
+import { DownloadManager } from "@main/services";
const pauseGameDownload = async (
_event: Electron.IpcMainInvokeEvent,
gameId: number
) => {
- DownloadManager.pauseDownload();
-
- await gameRepository
- .update(
- {
- id: gameId,
- status: In([
- GameStatus.Downloading,
- GameStatus.DownloadingMetadata,
- GameStatus.CheckingFiles,
- ]),
- },
- { status: GameStatus.Paused }
- )
- .then((result) => {
- if (result.affected) WindowManager.mainWindow?.setProgressBar(-1);
- });
+ await DownloadManager.pauseDownload();
+ await gameRepository.update({ id: gameId }, { status: "paused" });
};
registerEvent("pauseGameDownload", pauseGameDownload);
diff --git a/src/main/events/torrenting/resume-game-download.ts b/src/main/events/torrenting/resume-game-download.ts
index 6982d895..51a81996 100644
--- a/src/main/events/torrenting/resume-game-download.ts
+++ b/src/main/events/torrenting/resume-game-download.ts
@@ -1,9 +1,7 @@
import { registerEvent } from "../register-event";
import { gameRepository } from "../../repository";
-import { getDownloadsPath } from "../helpers/get-downloads-path";
-import { In } from "typeorm";
+
import { DownloadManager } from "@main/services";
-import { GameStatus } from "@shared";
const resumeGameDownload = async (
_event: Electron.IpcMainInvokeEvent,
@@ -18,31 +16,13 @@ const resumeGameDownload = async (
});
if (!game) return;
- DownloadManager.pauseDownload();
- if (game.status === GameStatus.Paused) {
- const downloadsPath = game.downloadPath ?? (await getDownloadsPath());
+ if (game.status === "paused") {
+ await DownloadManager.pauseDownload();
- DownloadManager.resumeDownload(gameId);
+ await gameRepository.update({ status: "active" }, { status: "paused" });
- await gameRepository.update(
- {
- status: In([
- GameStatus.Downloading,
- GameStatus.DownloadingMetadata,
- GameStatus.CheckingFiles,
- ]),
- },
- { status: GameStatus.Paused }
- );
-
- await gameRepository.update(
- { id: game.id },
- {
- status: GameStatus.Downloading,
- downloadPath: downloadsPath,
- }
- );
+ await DownloadManager.resumeDownload(gameId);
}
};
diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts
index f94d0999..62bce369 100644
--- a/src/main/events/torrenting/start-game-download.ts
+++ b/src/main/events/torrenting/start-game-download.ts
@@ -8,9 +8,8 @@ import { registerEvent } from "../register-event";
import type { GameShop } from "@types";
import { getFileBase64, getSteamAppAsset } from "@main/helpers";
-import { In } from "typeorm";
import { DownloadManager } from "@main/services";
-import { Downloader, GameStatus } from "@shared";
+import { Downloader } from "@shared";
import { stateManager } from "@main/state-manager";
const startGameDownload = async (
@@ -42,19 +41,9 @@ const startGameDownload = async (
}),
]);
- if (!repack || game?.status === GameStatus.Downloading) return;
- DownloadManager.pauseDownload();
+ if (!repack || game?.status === "active") return;
- await gameRepository.update(
- {
- status: In([
- GameStatus.Downloading,
- GameStatus.DownloadingMetadata,
- GameStatus.CheckingFiles,
- ]),
- },
- { status: GameStatus.Paused }
- );
+ await gameRepository.update({ status: "active" }, { status: "paused" });
if (game) {
await gameRepository.update(
@@ -62,17 +51,17 @@ const startGameDownload = async (
id: game.id,
},
{
- status: GameStatus.DownloadingMetadata,
- downloadPath: downloadPath,
+ status: "active",
+ downloadPath,
downloader,
repack: { id: repackId },
isDeleted: false,
}
);
- DownloadManager.downloadGame(game.id);
+ await DownloadManager.startDownload(game.id);
- game.status = GameStatus.DownloadingMetadata;
+ game.status = "active";
return game;
} else {
@@ -91,7 +80,7 @@ const startGameDownload = async (
objectID,
downloader,
shop: gameShop,
- status: GameStatus.Downloading,
+ status: "active",
downloadPath,
repack: { id: repackId },
})
@@ -105,7 +94,7 @@ const startGameDownload = async (
return result;
});
- DownloadManager.downloadGame(createdGame.id);
+ DownloadManager.startDownload(createdGame.id);
const { repack: _, ...rest } = createdGame;
diff --git a/src/main/main.ts b/src/main/main.ts
index e03a6ab8..a9f0ed19 100644
--- a/src/main/main.ts
+++ b/src/main/main.ts
@@ -13,17 +13,15 @@ import {
repackRepository,
userPreferencesRepository,
} from "./repository";
-import { TorrentDownloader } from "./services";
import { Repack, UserPreferences } from "./entity";
import { Notification } from "electron";
import { t } from "i18next";
-import { GameStatus } from "@shared";
-import { In } from "typeorm";
import fs from "node:fs";
import path from "node:path";
import { RealDebridClient } from "./services/real-debrid";
import { orderBy } from "lodash-es";
import { SteamGame } from "@types";
+import { Not } from "typeorm";
startProcessWatcher();
@@ -72,7 +70,7 @@ const checkForNewRepacks = async (userPreferences: UserPreferences | null) => {
};
const loadState = async (userPreferences: UserPreferences | null) => {
- const repacks = await repackRepository.find({
+ const repacks = repackRepository.find({
order: {
createdAt: "desc",
},
@@ -82,7 +80,7 @@ const loadState = async (userPreferences: UserPreferences | null) => {
fs.readFileSync(path.join(seedsPath, "steam-games.json"), "utf-8")
) as SteamGame[];
- stateManager.setValue("repacks", repacks);
+ stateManager.setValue("repacks", await repacks);
stateManager.setValue("steamGames", orderBy(steamGames, ["name"], "asc"));
import("./events");
@@ -90,22 +88,19 @@ const loadState = async (userPreferences: UserPreferences | null) => {
if (userPreferences?.realDebridApiToken)
await RealDebridClient.authorize(userPreferences?.realDebridApiToken);
+ await DownloadManager.connect();
+
const game = await gameRepository.findOne({
where: {
- status: In([
- GameStatus.Downloading,
- GameStatus.DownloadingMetadata,
- GameStatus.CheckingFiles,
- ]),
+ status: "active",
+ progress: Not(1),
isDeleted: false,
},
relations: { repack: true },
});
- await TorrentDownloader.startClient();
-
if (game) {
- DownloadManager.resumeDownload(game.id);
+ DownloadManager.startDownload(game.id);
}
};
diff --git a/src/main/services/download-manager.ts b/src/main/services/download-manager.ts
index e345835a..94e19835 100644
--- a/src/main/services/download-manager.ts
+++ b/src/main/services/download-manager.ts
@@ -1,13 +1,156 @@
-import { gameRepository } from "@main/repository";
+import Aria2, { StatusResponse } from "aria2";
+import { spawn } from "node:child_process";
-import type { Game } from "@main/entity";
+import { gameRepository, userPreferencesRepository } from "@main/repository";
+
+import path from "node:path";
+import { WindowManager } from "./window-manager";
+import { RealDebridClient } from "./real-debrid";
+import { Notification } from "electron";
+import { t } from "i18next";
import { Downloader } from "@shared";
-
-import { writePipe } from "./fifo";
-import { RealDebridDownloader } from "./downloaders";
+import { DownloadProgress } from "@types";
export class DownloadManager {
- private static gameDownloading: Game;
+ private static downloads = new Map();
+
+ private static gid: string | null = null;
+ private static gameId: number | null = null;
+
+ private static aria2 = new Aria2({});
+
+ static async connect() {
+ const binary = path.join(
+ __dirname,
+ "..",
+ "..",
+ "aria2-1.37.0-win-64bit-build1",
+ "aria2c"
+ );
+
+ spawn(binary, ["--enable-rpc", "--rpc-listen-all"], { stdio: "inherit" });
+
+ await this.aria2.open();
+ this.attachListener();
+ }
+
+ private static getETA(status: StatusResponse) {
+ const remainingBytes =
+ Number(status.totalLength) - Number(status.completedLength);
+ const speed = Number(status.downloadSpeed);
+
+ if (remainingBytes >= 0 && speed > 0) {
+ return (remainingBytes / speed) * 1000;
+ }
+
+ return -1;
+ }
+
+ static async publishNotification() {
+ const userPreferences = await userPreferencesRepository.findOne({
+ where: { id: 1 },
+ });
+
+ if (userPreferences?.downloadNotificationsEnabled && this.gameId) {
+ const game = await this.getGame(this.gameId);
+
+ new Notification({
+ title: t("download_complete", {
+ ns: "notifications",
+ lng: userPreferences.language,
+ }),
+ body: t("game_ready_to_install", {
+ ns: "notifications",
+ lng: userPreferences.language,
+ title: game?.title,
+ }),
+ }).show();
+ }
+ }
+
+ private static getFolderName(status: StatusResponse) {
+ if (status.bittorrent?.info) return status.bittorrent.info.name;
+ return "";
+ }
+
+ private static async attachListener() {
+ while (true) {
+ try {
+ if (!this.gid || !this.gameId) {
+ continue;
+ }
+
+ const status = await this.aria2.call("tellStatus", this.gid);
+
+ const downloadingMetadata =
+ status.bittorrent && !status.bittorrent?.info;
+
+ if (status.followedBy?.length) {
+ this.gid = status.followedBy[0];
+ this.downloads.set(this.gameId, this.gid);
+ continue;
+ }
+
+ const progress =
+ Number(status.completedLength) / Number(status.totalLength);
+
+ await gameRepository.update(
+ { id: this.gameId },
+ {
+ progress:
+ isNaN(progress) || downloadingMetadata ? undefined : progress,
+ bytesDownloaded: Number(status.completedLength),
+ fileSize: Number(status.totalLength),
+ status: status.status,
+ folderName: this.getFolderName(status),
+ }
+ );
+
+ const game = await gameRepository.findOne({
+ where: { id: this.gameId, isDeleted: false },
+ relations: { repack: true },
+ });
+
+ if (progress === 1 && game && !downloadingMetadata) {
+ await this.publishNotification();
+ /*
+ Only cancel bittorrent downloads to stop seeding
+ */
+ if (status.bittorrent) {
+ await this.cancelDownload(game.id);
+ } else {
+ this.clearCurrentDownload();
+ }
+ }
+
+ if (WindowManager.mainWindow && game) {
+ WindowManager.mainWindow.setProgressBar(
+ progress === 1 || downloadingMetadata ? -1 : progress,
+ { mode: downloadingMetadata ? "indeterminate" : "normal" }
+ );
+
+ const payload = {
+ progress,
+ bytesDownloaded: Number(status.completedLength),
+ fileSize: Number(status.totalLength),
+ numPeers: Number(status.connections),
+ numSeeds: Number(status.numSeeders ?? 0),
+ downloadSpeed: Number(status.downloadSpeed),
+ timeRemaining: this.getETA(status),
+ downloadingMetadata: !!downloadingMetadata,
+ game,
+ } as DownloadProgress;
+
+ WindowManager.mainWindow.webContents.send(
+ "on-download-progress",
+ JSON.parse(JSON.stringify(payload))
+ );
+ }
+ } finally {
+ await new Promise((resolve) => setTimeout(resolve, 500));
+ }
+ }
+ }
static async getGame(gameId: number) {
return gameRepository.findOne({
@@ -18,59 +161,80 @@ export class DownloadManager {
});
}
- static async cancelDownload() {
- if (
- this.gameDownloading &&
- this.gameDownloading.downloader === Downloader.Torrent
- ) {
- writePipe.write({ action: "cancel" });
- } else {
- RealDebridDownloader.destroy();
+ private static clearCurrentDownload() {
+ if (this.gameId) {
+ this.downloads.delete(this.gameId);
+ this.gid = null;
+ this.gameId = null;
+ }
+ }
+
+ static async cancelDownload(gameId: number) {
+ const gid = this.downloads.get(gameId);
+
+ if (gid) {
+ await this.aria2.call("remove", gid);
+
+ if (this.gid === gid) {
+ this.clearCurrentDownload();
+
+ WindowManager.mainWindow?.setProgressBar(-1);
+ } else {
+ this.downloads.delete(gameId);
+ }
}
}
static async pauseDownload() {
- if (
- this.gameDownloading &&
- this.gameDownloading.downloader === Downloader.Torrent
- ) {
- writePipe.write({ action: "pause" });
- } else {
- RealDebridDownloader.destroy();
+ if (this.gid) {
+ await this.aria2.call("forcePause", this.gid);
+ this.gid = null;
+ this.gameId = null;
+
+ WindowManager.mainWindow?.setProgressBar(-1);
}
}
static async resumeDownload(gameId: number) {
- const game = await this.getGame(gameId);
+ await this.aria2.call("forcePauseAll");
- if (game!.downloader === Downloader.Torrent) {
- writePipe.write({
- action: "start",
- game_id: game!.id,
- magnet: game!.repack.magnet,
- save_path: game!.downloadPath,
- });
+ if (this.downloads.has(gameId)) {
+ const gid = this.downloads.get(gameId)!;
+ await this.aria2.call("unpause", gid);
+
+ this.gid = gid;
+ this.gameId = gameId;
} else {
- RealDebridDownloader.startDownload(game!);
+ return this.startDownload(gameId);
}
-
- this.gameDownloading = game!;
}
- static async downloadGame(gameId: number) {
- const game = await this.getGame(gameId);
+ static async startDownload(gameId: number) {
+ await this.aria2.call("forcePauseAll");
- if (game!.downloader === Downloader.Torrent) {
- writePipe.write({
- action: "start",
- game_id: game!.id,
- magnet: game!.repack.magnet,
- save_path: game!.downloadPath,
- });
- } else {
- RealDebridDownloader.startDownload(game!);
+ const game = await this.getGame(gameId)!;
+
+ if (game) {
+ const options = {
+ dir: game.downloadPath!,
+ };
+
+ if (game.downloader === Downloader.RealDebrid) {
+ const downloadUrl = decodeURIComponent(
+ await RealDebridClient.getDownloadUrl(game)
+ );
+
+ this.gid = await this.aria2.call("addUri", [downloadUrl], options);
+ } else {
+ this.gid = await this.aria2.call(
+ "addUri",
+ [game.repack.magnet],
+ options
+ );
+ }
+
+ this.gameId = gameId;
+ this.downloads.set(gameId, this.gid);
}
-
- this.gameDownloading = game!;
}
}
diff --git a/src/main/services/downloaders/downloader.ts b/src/main/services/downloaders/downloader.ts
deleted file mode 100644
index 14440676..00000000
--- a/src/main/services/downloaders/downloader.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-import { t } from "i18next";
-import { Notification } from "electron";
-
-import { Game } from "@main/entity";
-
-import type { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
-
-import { WindowManager } from "../window-manager";
-import type { TorrentUpdate } from "./torrent.downloader";
-
-import { GameStatus } from "@shared";
-import { gameRepository, userPreferencesRepository } from "@main/repository";
-
-interface DownloadStatus {
- numPeers?: number;
- numSeeds?: number;
- downloadSpeed?: number;
- timeRemaining?: number;
-}
-
-export class Downloader {
- static getGameProgress(game: Game) {
- if (game.status === GameStatus.CheckingFiles)
- return game.fileVerificationProgress;
-
- return game.progress;
- }
-
- static async updateGameProgress(
- gameId: number,
- gameUpdate: QueryDeepPartialEntity,
- downloadStatus: DownloadStatus
- ) {
- await gameRepository.update({ id: gameId }, gameUpdate);
-
- const game = await gameRepository.findOne({
- where: { id: gameId, isDeleted: false },
- relations: { repack: true },
- });
-
- if (game?.progress === 1) {
- const userPreferences = await userPreferencesRepository.findOne({
- where: { id: 1 },
- });
-
- if (userPreferences?.downloadNotificationsEnabled) {
- new Notification({
- title: t("download_complete", {
- ns: "notifications",
- lng: userPreferences.language,
- }),
- body: t("game_ready_to_install", {
- ns: "notifications",
- lng: userPreferences.language,
- title: game?.title,
- }),
- }).show();
- }
- }
-
- if (WindowManager.mainWindow && game) {
- const progress = this.getGameProgress(game);
- WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
-
- WindowManager.mainWindow.webContents.send(
- "on-download-progress",
- JSON.parse(
- JSON.stringify({
- ...({
- progress: gameUpdate.progress,
- bytesDownloaded: gameUpdate.bytesDownloaded,
- fileSize: gameUpdate.fileSize,
- gameId,
- numPeers: downloadStatus.numPeers,
- numSeeds: downloadStatus.numSeeds,
- downloadSpeed: downloadStatus.downloadSpeed,
- timeRemaining: downloadStatus.timeRemaining,
- } as TorrentUpdate),
- game,
- })
- )
- );
- }
- }
-}
diff --git a/src/main/services/downloaders/index.ts b/src/main/services/downloaders/index.ts
deleted file mode 100644
index cd742107..00000000
--- a/src/main/services/downloaders/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from "./real-debrid.downloader";
-export * from "./torrent.downloader";
diff --git a/src/main/services/downloaders/real-debrid.downloader.ts b/src/main/services/downloaders/real-debrid.downloader.ts
deleted file mode 100644
index 8a44f934..00000000
--- a/src/main/services/downloaders/real-debrid.downloader.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-import { Game } from "@main/entity";
-import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
-import path from "node:path";
-import fs from "node:fs";
-import EasyDL from "easydl";
-import { GameStatus } from "@shared";
-// import { fullArchive } from "node-7z-archive";
-
-import { Downloader } from "./downloader";
-import { RealDebridClient } from "../real-debrid";
-
-export class RealDebridDownloader extends Downloader {
- private static download: EasyDL;
- private static downloadSize = 0;
-
- private static getEta(bytesDownloaded: number, speed: number) {
- const remainingBytes = this.downloadSize - bytesDownloaded;
-
- if (remainingBytes >= 0 && speed > 0) {
- return (remainingBytes / speed) * 1000;
- }
-
- return 1;
- }
-
- private static createFolderIfNotExists(path: string) {
- if (!fs.existsSync(path)) {
- fs.mkdirSync(path);
- }
- }
-
- // private static async startDecompression(
- // rarFile: string,
- // dest: string,
- // game: Game
- // ) {
- // await fullArchive(rarFile, dest);
-
- // const updatePayload: QueryDeepPartialEntity = {
- // status: GameStatus.Finished,
- // };
-
- // await this.updateGameProgress(game.id, updatePayload, {});
- // }
-
- static destroy() {
- if (this.download) {
- this.download.destroy();
- }
- }
-
- static async startDownload(game: Game) {
- if (this.download) this.download.destroy();
- const downloadUrl = decodeURIComponent(
- await RealDebridClient.getDownloadUrl(game)
- );
-
- const filename = path.basename(downloadUrl);
- const folderName = path.basename(filename, path.extname(filename));
-
- const downloadPath = path.join(game.downloadPath!, folderName);
- this.createFolderIfNotExists(downloadPath);
-
- this.download = new EasyDL(downloadUrl, path.join(downloadPath, filename));
-
- const metadata = await this.download.metadata();
-
- this.downloadSize = metadata.size;
-
- const updatePayload: QueryDeepPartialEntity = {
- status: GameStatus.Downloading,
- fileSize: metadata.size,
- folderName,
- };
-
- const downloadStatus = {
- timeRemaining: Number.POSITIVE_INFINITY,
- };
-
- await this.updateGameProgress(game.id, updatePayload, downloadStatus);
-
- this.download.on("progress", async ({ total }) => {
- const updatePayload: QueryDeepPartialEntity = {
- status: GameStatus.Downloading,
- progress: Math.min(0.99, total.percentage / 100),
- bytesDownloaded: total.bytes,
- };
-
- const downloadStatus = {
- downloadSpeed: total.speed,
- timeRemaining: this.getEta(total.bytes ?? 0, total.speed ?? 0),
- };
-
- await this.updateGameProgress(game.id, updatePayload, downloadStatus);
- });
-
- this.download.on("end", async () => {
- const updatePayload: QueryDeepPartialEntity = {
- status: GameStatus.Finished,
- progress: 1,
- };
-
- await this.updateGameProgress(game.id, updatePayload, {
- timeRemaining: 0,
- });
-
- /* This has to be improved */
- // this.startDecompression(
- // path.join(downloadPath, filename),
- // downloadPath,
- // game
- // );
- });
- }
-}
diff --git a/src/main/services/downloaders/torrent.downloader.ts b/src/main/services/downloaders/torrent.downloader.ts
deleted file mode 100644
index d5e039a8..00000000
--- a/src/main/services/downloaders/torrent.downloader.ts
+++ /dev/null
@@ -1,156 +0,0 @@
-import path from "node:path";
-import cp from "node:child_process";
-import fs from "node:fs";
-import { app, dialog } from "electron";
-import type { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
-
-import { Game } from "@main/entity";
-import { GameStatus } from "@shared";
-import { Downloader } from "./downloader";
-import { readPipe, writePipe } from "../fifo";
-
-const binaryNameByPlatform: Partial> = {
- darwin: "hydra-download-manager",
- linux: "hydra-download-manager",
- win32: "hydra-download-manager.exe",
-};
-
-enum TorrentState {
- CheckingFiles = 1,
- DownloadingMetadata = 2,
- Downloading = 3,
- Finished = 4,
- Seeding = 5,
-}
-
-export interface TorrentUpdate {
- gameId: number;
- progress: number;
- downloadSpeed: number;
- timeRemaining: number;
- numPeers: number;
- numSeeds: number;
- status: TorrentState;
- folderName: string;
- fileSize: number;
- bytesDownloaded: number;
-}
-
-export const BITTORRENT_PORT = "5881";
-
-export class TorrentDownloader extends Downloader {
- private static messageLength = 1024 * 2;
-
- public static async attachListener() {
- // eslint-disable-next-line no-constant-condition
- while (true) {
- const buffer = readPipe.socket?.read(this.messageLength);
-
- if (buffer === null) {
- await new Promise((resolve) => setTimeout(resolve, 100));
- continue;
- }
-
- const message = Buffer.from(
- buffer.slice(0, buffer.indexOf(0x00))
- ).toString("utf-8");
-
- try {
- const payload = JSON.parse(message) as TorrentUpdate;
-
- const updatePayload: QueryDeepPartialEntity = {
- bytesDownloaded: payload.bytesDownloaded,
- status: this.getTorrentStateName(payload.status),
- };
-
- if (payload.status === TorrentState.CheckingFiles) {
- updatePayload.fileVerificationProgress = payload.progress;
- } else {
- if (payload.folderName) {
- updatePayload.folderName = payload.folderName;
- updatePayload.fileSize = payload.fileSize;
- }
- }
-
- if (
- [TorrentState.Downloading, TorrentState.Seeding].includes(
- payload.status
- )
- ) {
- updatePayload.progress = payload.progress;
- }
-
- this.updateGameProgress(payload.gameId, updatePayload, {
- numPeers: payload.numPeers,
- numSeeds: payload.numSeeds,
- downloadSpeed: payload.downloadSpeed,
- timeRemaining: payload.timeRemaining,
- });
- } finally {
- await new Promise((resolve) => setTimeout(resolve, 100));
- }
- }
- }
-
- public static startClient() {
- return new Promise((resolve) => {
- const commonArgs = [
- BITTORRENT_PORT,
- writePipe.socketPath,
- readPipe.socketPath,
- ];
-
- if (app.isPackaged) {
- const binaryName = binaryNameByPlatform[process.platform]!;
- const binaryPath = path.join(
- process.resourcesPath,
- "hydra-download-manager",
- binaryName
- );
-
- if (!fs.existsSync(binaryPath)) {
- dialog.showErrorBox(
- "Fatal",
- "Hydra download manager binary not found. Please check if it has been removed by Windows Defender."
- );
-
- app.quit();
- }
-
- cp.spawn(binaryPath, commonArgs, {
- stdio: "inherit",
- windowsHide: true,
- });
- } else {
- const scriptPath = path.join(
- __dirname,
- "..",
- "..",
- "torrent-client",
- "main.py"
- );
-
- cp.spawn("python3", [scriptPath, ...commonArgs], {
- stdio: "inherit",
- });
- }
-
- Promise.all([writePipe.createPipe(), readPipe.createPipe()]).then(
- async () => {
- this.attachListener();
- resolve(null);
- }
- );
- });
- }
-
- private static getTorrentStateName(state: TorrentState) {
- if (state === TorrentState.CheckingFiles) return GameStatus.CheckingFiles;
- if (state === TorrentState.Downloading) return GameStatus.Downloading;
- if (state === TorrentState.DownloadingMetadata)
- return GameStatus.DownloadingMetadata;
- if (state === TorrentState.Finished) return GameStatus.Finished;
- if (state === TorrentState.Seeding) return GameStatus.Seeding;
- return null;
- }
-}
diff --git a/src/main/services/fifo.ts b/src/main/services/fifo.ts
deleted file mode 100644
index 866232cc..00000000
--- a/src/main/services/fifo.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import path from "node:path";
-import net from "node:net";
-import crypto from "node:crypto";
-import os from "node:os";
-
-export class FIFO {
- public socket: null | net.Socket = null;
- public socketPath = this.generateSocketFilename();
-
- private generateSocketFilename() {
- const hash = crypto.randomBytes(16).toString("hex");
-
- if (process.platform === "win32") {
- return "\\\\.\\pipe\\" + hash;
- }
-
- return path.join(os.tmpdir(), hash);
- }
-
- public write(data: any) {
- if (!this.socket) return;
- this.socket.write(Buffer.from(JSON.stringify(data)));
- }
-
- public createPipe() {
- return new Promise((resolve) => {
- const server = net.createServer((socket) => {
- this.socket = socket;
- resolve(null);
- });
-
- server.listen(this.socketPath);
- });
- }
-}
-
-export const writePipe = new FIFO();
-export const readPipe = new FIFO();
diff --git a/src/main/services/index.ts b/src/main/services/index.ts
index 4b13d38d..4808736d 100644
--- a/src/main/services/index.ts
+++ b/src/main/services/index.ts
@@ -5,8 +5,6 @@ export * from "./steam-250";
export * from "./steam-grid";
export * from "./update-resolver";
export * from "./window-manager";
-export * from "./fifo";
-export * from "./downloaders";
export * from "./download-manager";
export * from "./how-long-to-beat";
export * from "./process-watcher";
diff --git a/src/preload/index.ts b/src/preload/index.ts
index 6a209787..0e397a4a 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -5,7 +5,7 @@ import { contextBridge, ipcRenderer } from "electron";
import type {
CatalogueCategory,
GameShop,
- TorrentProgress,
+ DownloadProgress,
UserPreferences,
} from "@types";
@@ -32,10 +32,10 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("pauseGameDownload", gameId),
resumeGameDownload: (gameId: number) =>
ipcRenderer.invoke("resumeGameDownload", gameId),
- onDownloadProgress: (cb: (value: TorrentProgress) => void) => {
+ onDownloadProgress: (cb: (value: DownloadProgress) => void) => {
const listener = (
_event: Electron.IpcRendererEvent,
- value: TorrentProgress
+ value: DownloadProgress
) => cb(value);
ipcRenderer.on("on-download-progress", listener);
return () => ipcRenderer.removeListener("on-download-progress", listener);
diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx
index da95f292..adb2a613 100644
--- a/src/renderer/src/app.tsx
+++ b/src/renderer/src/app.tsx
@@ -19,7 +19,6 @@ import {
setUserPreferences,
toggleDraggingDisabled,
} from "@renderer/features";
-import { GameStatusHelper } from "@shared";
document.body.classList.add(themeClass);
@@ -54,7 +53,7 @@ export function App({ children }: AppProps) {
useEffect(() => {
const unsubscribe = window.electron.onDownloadProgress(
(downloadProgress) => {
- if (GameStatusHelper.isReady(downloadProgress.game.status)) {
+ if (downloadProgress.game.progress === 1) {
clearDownload();
updateLibrary();
return;
diff --git a/src/renderer/src/components/backdrop/backdrop.css.ts b/src/renderer/src/components/backdrop/backdrop.css.ts
index 0a7b61bb..3b8cc4e2 100644
--- a/src/renderer/src/components/backdrop/backdrop.css.ts
+++ b/src/renderer/src/components/backdrop/backdrop.css.ts
@@ -43,5 +43,11 @@ export const backdrop = recipe({
backgroundColor: "rgba(0, 0, 0, 0)",
},
},
+ windows: {
+ true: {
+ // SPACING_UNIT * 3 + title bar spacing
+ paddingTop: `${SPACING_UNIT * 3 + 35}px`,
+ },
+ },
},
});
diff --git a/src/renderer/src/components/backdrop/backdrop.tsx b/src/renderer/src/components/backdrop/backdrop.tsx
index 5852d59d..f498e664 100644
--- a/src/renderer/src/components/backdrop/backdrop.tsx
+++ b/src/renderer/src/components/backdrop/backdrop.tsx
@@ -7,6 +7,13 @@ export interface BackdropProps {
export function Backdrop({ isClosing = false, children }: BackdropProps) {
return (
- {children}
+
+ {children}
+
);
}
diff --git a/src/renderer/src/components/bottom-panel/bottom-panel.tsx b/src/renderer/src/components/bottom-panel/bottom-panel.tsx
index 44d125cd..310f31b4 100644
--- a/src/renderer/src/components/bottom-panel/bottom-panel.tsx
+++ b/src/renderer/src/components/bottom-panel/bottom-panel.tsx
@@ -7,17 +7,16 @@ import { vars } from "../../theme.css";
import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { VERSION_CODENAME } from "@renderer/constants";
-import { GameStatus, GameStatusHelper } from "@shared";
export function BottomPanel() {
const { t } = useTranslation("bottom_panel");
const navigate = useNavigate();
- const { game, progress, downloadSpeed, eta } = useDownload();
+ const { lastPacket, progress, downloadSpeed, eta } = useDownload();
const isGameDownloading =
- game && GameStatusHelper.isDownloading(game.status ?? null);
+ lastPacket?.game && lastPacket?.game.status === "active";
const [version, setVersion] = useState("");
@@ -27,17 +26,8 @@ export function BottomPanel() {
const status = useMemo(() => {
if (isGameDownloading) {
- if (game.status === GameStatus.DownloadingMetadata)
- return t("downloading_metadata", { title: game.title });
-
- if (game.status === GameStatus.CheckingFiles)
- return t("checking_files", {
- title: game.title,
- percentage: progress,
- });
-
return t("downloading", {
- title: game?.title,
+ title: lastPacket?.game.title,
percentage: progress,
eta,
speed: downloadSpeed,
@@ -45,7 +35,7 @@ export function BottomPanel() {
}
return t("no_downloads_in_progress");
- }, [t, isGameDownloading, game, progress, eta, downloadSpeed]);
+ }, [t, isGameDownloading, lastPacket?.game, progress, eta, downloadSpeed]);
return (
- {isGamePlaying ? (
+ {isGameRunning ? (
{t("playing_now")}
) : (
diff --git a/src/renderer/src/pages/game-details/hero/hero-panel.tsx b/src/renderer/src/pages/game-details/hero/hero-panel.tsx
index 87a4b0ee..5f4ba9d1 100644
--- a/src/renderer/src/pages/game-details/hero/hero-panel.tsx
+++ b/src/renderer/src/pages/game-details/hero/hero-panel.tsx
@@ -1,72 +1,48 @@
import { format } from "date-fns";
-import { useMemo, useState } from "react";
+import { useContext, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
+import Color from "color";
import { useDownload } from "@renderer/hooks";
-import type { Game, GameRepack } from "@types";
import { formatDownloadProgress } from "@renderer/helpers";
import { HeroPanelActions } from "./hero-panel-actions";
-import { Downloader, GameStatus, GameStatusHelper, formatBytes } from "@shared";
+import { Downloader, formatBytes } from "@shared";
import { BinaryNotFoundModal } from "../../shared-modals/binary-not-found-modal";
import * as styles from "./hero-panel.css";
import { HeroPanelPlaytime } from "./hero-panel-playtime";
+import { gameDetailsContext } from "../game-details.context";
-export interface HeroPanelProps {
- game: Game | null;
- color: string;
- isGamePlaying: boolean;
- objectID: string;
- title: string;
- repacks: GameRepack[];
- openRepacksModal: () => void;
- getGame: () => void;
-}
-
-export function HeroPanel({
- game,
- color,
- repacks,
- objectID,
- title,
- isGamePlaying,
- openRepacksModal,
- getGame,
-}: HeroPanelProps) {
+export function HeroPanel() {
const { t } = useTranslation("game_details");
+ const { game, repacks, gameColor } = useContext(gameDetailsContext);
+
const [showBinaryNotFoundModal, setShowBinaryNotFoundModal] = useState(false);
- const {
- game: gameDownloading,
- progress,
- eta,
- numPeers,
- numSeeds,
- isGameDeleting,
- } = useDownload();
-
- const isGameDownloading =
- gameDownloading?.id === game?.id &&
- GameStatusHelper.isDownloading(game?.status ?? null);
+ const { progress, eta, lastPacket, isGameDeleting } = useDownload();
const finalDownloadSize = useMemo(() => {
if (!game) return "N/A";
if (game.fileSize) return formatBytes(game.fileSize);
- if (gameDownloading?.fileSize && isGameDownloading)
- return formatBytes(gameDownloading.fileSize);
+ if (lastPacket?.game.fileSize && game?.status === "active")
+ return formatBytes(lastPacket?.game.fileSize);
return game.repack?.fileSize ?? "N/A";
- }, [game, isGameDownloading, gameDownloading]);
+ }, [game, lastPacket?.game]);
const getInfo = () => {
- if (isGameDeleting(game?.id ?? -1)) {
- return
{t("deleting")}
;
- }
+ if (isGameDeleting(game?.id ?? -1)) return {t("deleting")}
;
+
+ if (game?.progress === 1) return ;
+
+ if (game?.status === "active") {
+ if (lastPacket?.downloadingMetadata) {
+ return {t("downloading_metadata")}
;
+ }
- if (isGameDownloading && gameDownloading?.status) {
return (
<>
@@ -74,33 +50,25 @@ export function HeroPanel({
{eta && {t("eta", { eta })}}
- {gameDownloading.status !== GameStatus.Downloading ? (
- <>
- {t(gameDownloading.status)}
- {eta && {t("eta", { eta })}}
- >
- ) : (
-
- {formatBytes(gameDownloading.bytesDownloaded)} /{" "}
- {finalDownloadSize}
+
+ {formatBytes(lastPacket?.game?.bytesDownloaded ?? 0)} /{" "}
+ {finalDownloadSize}
+ {game?.downloader === Downloader.Torrent && (
- {game?.downloader === Downloader.Torrent &&
- `${numPeers} peers / ${numSeeds} seeds`}
+ {lastPacket?.numPeers} peers / {lastPacket?.numSeeds} seeds
-
- )}
+ )}
+
>
);
}
- if (game?.status === GameStatus.Paused) {
+ if (game?.status === "paused") {
+ const formattedProgress = formatDownloadProgress(game.progress);
+
return (
<>
-
- {t("paused_progress", {
- progress: formatDownloadProgress(game.progress),
- })}
-
+ {t("paused_progress", { progress: formattedProgress })}
{formatBytes(game.bytesDownloaded)} / {finalDownloadSize}
@@ -108,10 +76,6 @@ export function HeroPanel({
);
}
- if (game && GameStatusHelper.isReady(game?.status ?? GameStatus.Finished)) {
- return ;
- }
-
const [latestRepack] = repacks;
if (latestRepack) {
@@ -129,6 +93,10 @@ export function HeroPanel({
return {t("no_downloads")}
;
};
+ const backgroundColor = gameColor
+ ? (new Color(gameColor).darken(0.6).toString() as string)
+ : "";
+
return (
<>
setShowBinaryNotFoundModal(false)}
/>
-
+
{getInfo()}
setShowBinaryNotFoundModal(true)}
- isGamePlaying={isGamePlaying}
- isGameDownloading={isGameDownloading}
/>
diff --git a/src/renderer/src/pages/game-details/modals/index.ts b/src/renderer/src/pages/game-details/modals/index.ts
new file mode 100644
index 00000000..7934029b
--- /dev/null
+++ b/src/renderer/src/pages/game-details/modals/index.ts
@@ -0,0 +1,3 @@
+export * from "./installation-guides";
+export * from "./repacks-modal";
+export * from "./select-folder-modal";
diff --git a/src/renderer/src/pages/game-details/installation-guides/constants.ts b/src/renderer/src/pages/game-details/modals/installation-guides/constants.ts
similarity index 100%
rename from src/renderer/src/pages/game-details/installation-guides/constants.ts
rename to src/renderer/src/pages/game-details/modals/installation-guides/constants.ts
diff --git a/src/renderer/src/pages/game-details/installation-guides/dodi-installation-guide.css.ts b/src/renderer/src/pages/game-details/modals/installation-guides/dodi-installation-guide.css.ts
similarity index 94%
rename from src/renderer/src/pages/game-details/installation-guides/dodi-installation-guide.css.ts
rename to src/renderer/src/pages/game-details/modals/installation-guides/dodi-installation-guide.css.ts
index d95add53..27e6d6e8 100644
--- a/src/renderer/src/pages/game-details/installation-guides/dodi-installation-guide.css.ts
+++ b/src/renderer/src/pages/game-details/modals/installation-guides/dodi-installation-guide.css.ts
@@ -1,4 +1,4 @@
-import { vars } from "../../../theme.css";
+import { vars } from "../../../../theme.css";
import { keyframes, style } from "@vanilla-extract/css";
export const slideIn = keyframes({
diff --git a/src/renderer/src/pages/game-details/installation-guides/dodi-installation-guide.tsx b/src/renderer/src/pages/game-details/modals/installation-guides/dodi-installation-guide.tsx
similarity index 89%
rename from src/renderer/src/pages/game-details/installation-guides/dodi-installation-guide.tsx
rename to src/renderer/src/pages/game-details/modals/installation-guides/dodi-installation-guide.tsx
index 007568fb..548579c6 100644
--- a/src/renderer/src/pages/game-details/installation-guides/dodi-installation-guide.tsx
+++ b/src/renderer/src/pages/game-details/modals/installation-guides/dodi-installation-guide.tsx
@@ -1,4 +1,4 @@
-import { useState } from "react";
+import { useContext, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import { Button, CheckboxField, Modal } from "@renderer/components";
@@ -7,18 +7,19 @@ import { SPACING_UNIT } from "@renderer/theme.css";
import * as styles from "./dodi-installation-guide.css";
import { ArrowUpIcon } from "@primer/octicons-react";
import { DONT_SHOW_DODI_INSTRUCTIONS_KEY } from "./constants";
+import { gameDetailsContext } from "../../game-details.context";
export interface DODIInstallationGuideProps {
- windowColor: string;
visible: boolean;
onClose: () => void;
}
export function DODIInstallationGuide({
- windowColor,
visible,
onClose,
}: DODIInstallationGuideProps) {
+ const { gameColor } = useContext(gameDetailsContext);
+
const { t } = useTranslation("game_details");
const [dontShowAgain, setDontShowAgain] = useState(false);
@@ -53,7 +54,7 @@ export function DODIInstallationGuide({
diff --git a/src/renderer/src/pages/game-details/installation-guides/index.ts b/src/renderer/src/pages/game-details/modals/installation-guides/index.ts
similarity index 100%
rename from src/renderer/src/pages/game-details/installation-guides/index.ts
rename to src/renderer/src/pages/game-details/modals/installation-guides/index.ts
diff --git a/src/renderer/src/pages/game-details/installation-guides/online-fix-installation-guide.css.ts b/src/renderer/src/pages/game-details/modals/installation-guides/online-fix-installation-guide.css.ts
similarity index 71%
rename from src/renderer/src/pages/game-details/installation-guides/online-fix-installation-guide.css.ts
rename to src/renderer/src/pages/game-details/modals/installation-guides/online-fix-installation-guide.css.ts
index 891f11be..b7665d7d 100644
--- a/src/renderer/src/pages/game-details/installation-guides/online-fix-installation-guide.css.ts
+++ b/src/renderer/src/pages/game-details/modals/installation-guides/online-fix-installation-guide.css.ts
@@ -1,4 +1,4 @@
-import { SPACING_UNIT } from "../../../theme.css";
+import { SPACING_UNIT } from "../../../../theme.css";
import { style } from "@vanilla-extract/css";
export const passwordField = style({
diff --git a/src/renderer/src/pages/game-details/installation-guides/online-fix-installation-guide.tsx b/src/renderer/src/pages/game-details/modals/installation-guides/online-fix-installation-guide.tsx
similarity index 100%
rename from src/renderer/src/pages/game-details/installation-guides/online-fix-installation-guide.tsx
rename to src/renderer/src/pages/game-details/modals/installation-guides/online-fix-installation-guide.tsx
diff --git a/src/renderer/src/pages/game-details/repacks-modal.css.ts b/src/renderer/src/pages/game-details/modals/repacks-modal.css.ts
similarity index 87%
rename from src/renderer/src/pages/game-details/repacks-modal.css.ts
rename to src/renderer/src/pages/game-details/modals/repacks-modal.css.ts
index 4e15a63a..11fc71f6 100644
--- a/src/renderer/src/pages/game-details/repacks-modal.css.ts
+++ b/src/renderer/src/pages/game-details/modals/repacks-modal.css.ts
@@ -1,5 +1,5 @@
import { style } from "@vanilla-extract/css";
-import { SPACING_UNIT, vars } from "../../theme.css";
+import { SPACING_UNIT, vars } from "../../../theme.css";
export const repacks = style({
display: "flex",
diff --git a/src/renderer/src/pages/game-details/repacks-modal.tsx b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx
similarity index 92%
rename from src/renderer/src/pages/game-details/repacks-modal.tsx
rename to src/renderer/src/pages/game-details/modals/repacks-modal.tsx
index 4bb92408..1aea6c0d 100644
--- a/src/renderer/src/pages/game-details/repacks-modal.tsx
+++ b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useState } from "react";
+import { useContext, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button, Modal, TextField } from "@renderer/components";
@@ -6,20 +6,19 @@ import type { GameRepack } from "@types";
import * as styles from "./repacks-modal.css";
-import { SPACING_UNIT } from "../../theme.css";
+import { SPACING_UNIT } from "../../../theme.css";
import { format } from "date-fns";
import { SelectFolderModal } from "./select-folder-modal";
+import { gameDetailsContext } from "../game-details.context";
export interface RepacksModalProps {
visible: boolean;
- repacks: GameRepack[];
startDownload: (repack: GameRepack, downloadPath: string) => Promise
;
onClose: () => void;
}
export function RepacksModal({
visible,
- repacks,
startDownload,
onClose,
}: RepacksModalProps) {
@@ -27,6 +26,8 @@ export function RepacksModal({
const [repack, setRepack] = useState(null);
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
+ const { repacks } = useContext(gameDetailsContext);
+
const { t } = useTranslation("game_details");
useEffect(() => {
diff --git a/src/renderer/src/pages/game-details/select-folder-modal.css.tsx b/src/renderer/src/pages/game-details/modals/select-folder-modal.css.tsx
similarity index 86%
rename from src/renderer/src/pages/game-details/select-folder-modal.css.tsx
rename to src/renderer/src/pages/game-details/modals/select-folder-modal.css.tsx
index 21bbdfea..fe369301 100644
--- a/src/renderer/src/pages/game-details/select-folder-modal.css.tsx
+++ b/src/renderer/src/pages/game-details/modals/select-folder-modal.css.tsx
@@ -1,5 +1,5 @@
import { style } from "@vanilla-extract/css";
-import { SPACING_UNIT, vars } from "../../theme.css";
+import { SPACING_UNIT, vars } from "../../../theme.css";
export const container = style({
display: "flex",
diff --git a/src/renderer/src/pages/game-details/select-folder-modal.tsx b/src/renderer/src/pages/game-details/modals/select-folder-modal.tsx
similarity index 99%
rename from src/renderer/src/pages/game-details/select-folder-modal.tsx
rename to src/renderer/src/pages/game-details/modals/select-folder-modal.tsx
index d43990dc..9c1d18e1 100644
--- a/src/renderer/src/pages/game-details/select-folder-modal.tsx
+++ b/src/renderer/src/pages/game-details/modals/select-folder-modal.tsx
@@ -1,13 +1,14 @@
-import { Button, Link, Modal, TextField } from "@renderer/components";
-import type { GameRepack } from "@types";
import { useEffect, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import { DiskSpace } from "check-disk-space";
import * as styles from "./select-folder-modal.css";
+import { Button, Link, Modal, TextField } from "@renderer/components";
import { DownloadIcon } from "@primer/octicons-react";
import { formatBytes } from "@shared";
+import type { GameRepack } from "@types";
+
export interface SelectFolderModalProps {
visible: boolean;
onClose: () => void;
diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx
index ec9e12c7..780f5964 100644
--- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx
+++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx
@@ -1,22 +1,13 @@
-import { useEffect, useState } from "react";
+import { useContext, useEffect, useState } from "react";
import { HowLongToBeatSection } from "./how-long-to-beat-section";
-import type {
- HowLongToBeatCategory,
- ShopDetails,
- SteamAppDetails,
-} from "@types";
+import type { HowLongToBeatCategory, SteamAppDetails } from "@types";
import { useTranslation } from "react-i18next";
import { Button } from "@renderer/components";
import * as styles from "./sidebar.css";
+import { gameDetailsContext } from "../game-details.context";
-export interface SidebarProps {
- objectID: string;
- title: string;
- gameDetails: ShopDetails | null;
-}
-
-export function Sidebar({ objectID, title, gameDetails }: SidebarProps) {
+export function Sidebar() {
const [howLongToBeat, setHowLongToBeat] = useState<{
isLoading: boolean;
data: HowLongToBeatCategory[] | null;
@@ -25,20 +16,24 @@ export function Sidebar({ objectID, title, gameDetails }: SidebarProps) {
const [activeRequirement, setActiveRequirement] =
useState("minimum");
+ const { gameTitle, shopDetails, objectID } = useContext(gameDetailsContext);
+
const { t } = useTranslation("game_details");
useEffect(() => {
- setHowLongToBeat({ isLoading: true, data: null });
+ if (objectID) {
+ setHowLongToBeat({ isLoading: true, data: null });
- window.electron
- .getHowLongToBeat(objectID, "steam", title)
- .then((howLongToBeat) => {
- setHowLongToBeat({ isLoading: false, data: howLongToBeat });
- })
- .catch(() => {
- setHowLongToBeat({ isLoading: false, data: null });
- });
- }, [objectID, title]);
+ window.electron
+ .getHowLongToBeat(objectID, "steam", gameTitle)
+ .then((howLongToBeat) => {
+ setHowLongToBeat({ isLoading: false, data: howLongToBeat });
+ })
+ .catch(() => {
+ setHowLongToBeat({ isLoading: false, data: null });
+ });
+ }
+ }, [objectID, gameTitle]);
return (