From 4941709296dc42f8c0e84123229358ab802ba877 Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Mon, 20 May 2024 02:21:11 +0100
Subject: [PATCH 01/27] feat: adding aria2
---
.github/workflows/build.yml | 11 -
.github/workflows/release.yml | 11 -
.gitignore | 2 +-
electron-builder.yml | 1 -
hydra.db | Bin 54386688 -> 54386688 bytes
package.json | 1 +
src/main/declaration.d.ts | 80 +++++
src/main/entity/game.entity.ts | 8 +-
src/main/events/library/delete-game-folder.ts | 3 +-
src/main/events/library/get-library.ts | 3 +-
src/main/events/library/remove-game.ts | 3 +-
.../events/torrenting/cancel-game-download.ts | 48 +--
.../events/torrenting/pause-game-download.ts | 23 +-
.../events/torrenting/resume-game-download.ts | 30 +-
.../events/torrenting/start-game-download.ts | 29 +-
src/main/main.ts | 21 +-
src/main/services/download-manager.ts | 254 +++++++++++---
src/main/services/downloaders/downloader.ts | 85 -----
src/main/services/downloaders/index.ts | 2 -
.../downloaders/real-debrid.downloader.ts | 115 ------
.../downloaders/torrent.downloader.ts | 156 ---------
src/main/services/fifo.ts | 38 --
src/main/services/index.ts | 2 -
src/preload/index.ts | 6 +-
src/renderer/src/app.tsx | 3 +-
.../src/components/backdrop/backdrop.css.ts | 6 +
.../src/components/backdrop/backdrop.tsx | 9 +-
.../components/bottom-panel/bottom-panel.tsx | 18 +-
.../src/components/sidebar/sidebar.tsx | 24 +-
src/renderer/src/declaration.d.ts | 4 +-
src/renderer/src/features/download-slice.ts | 6 +-
src/renderer/src/hooks/use-download.ts | 51 +--
.../src/pages/downloads/downloads.tsx | 3 -
.../pages/game-details/description-header.tsx | 16 +-
.../src/pages/game-details/gallery-slider.tsx | 55 ++-
.../game-details/game-details.context.tsx | 206 +++++++++++
.../src/pages/game-details/game-details.tsx | 328 ++++++------------
.../game-details/hero/hero-panel-actions.tsx | 147 ++++----
.../game-details/hero/hero-panel-playtime.tsx | 22 +-
.../pages/game-details/hero/hero-panel.tsx | 110 ++----
.../src/pages/game-details/modals/index.ts | 3 +
.../installation-guides/constants.ts | 0
.../dodi-installation-guide.css.ts | 2 +-
.../dodi-installation-guide.tsx | 9 +-
.../{ => modals}/installation-guides/index.ts | 0
.../online-fix-installation-guide.css.ts | 2 +-
.../online-fix-installation-guide.tsx | 0
.../{ => modals}/repacks-modal.css.ts | 2 +-
.../{ => modals}/repacks-modal.tsx | 9 +-
.../{ => modals}/select-folder-modal.css.tsx | 2 +-
.../{ => modals}/select-folder-modal.tsx | 5 +-
.../pages/game-details/sidebar/sidebar.tsx | 45 ++-
src/shared/index.ts | 19 +-
src/types/index.ts | 13 +-
torrent-client/fifo.py | 35 --
torrent-client/main.py | 103 ------
torrent-client/setup.py | 20 --
yarn.lock | 15 +-
58 files changed, 895 insertions(+), 1329 deletions(-)
create mode 100644 src/main/declaration.d.ts
delete mode 100644 src/main/services/downloaders/downloader.ts
delete mode 100644 src/main/services/downloaders/index.ts
delete mode 100644 src/main/services/downloaders/real-debrid.downloader.ts
delete mode 100644 src/main/services/downloaders/torrent.downloader.ts
delete mode 100644 src/main/services/fifo.ts
create mode 100644 src/renderer/src/pages/game-details/game-details.context.tsx
create mode 100644 src/renderer/src/pages/game-details/modals/index.ts
rename src/renderer/src/pages/game-details/{ => modals}/installation-guides/constants.ts (100%)
rename src/renderer/src/pages/game-details/{ => modals}/installation-guides/dodi-installation-guide.css.ts (94%)
rename src/renderer/src/pages/game-details/{ => modals}/installation-guides/dodi-installation-guide.tsx (89%)
rename src/renderer/src/pages/game-details/{ => modals}/installation-guides/index.ts (100%)
rename src/renderer/src/pages/game-details/{ => modals}/installation-guides/online-fix-installation-guide.css.ts (71%)
rename src/renderer/src/pages/game-details/{ => modals}/installation-guides/online-fix-installation-guide.tsx (100%)
rename src/renderer/src/pages/game-details/{ => modals}/repacks-modal.css.ts (87%)
rename src/renderer/src/pages/game-details/{ => modals}/repacks-modal.tsx (92%)
rename src/renderer/src/pages/game-details/{ => modals}/select-folder-modal.css.tsx (86%)
rename src/renderer/src/pages/game-details/{ => modals}/select-folder-modal.tsx (99%)
delete mode 100644 torrent-client/fifo.py
delete mode 100644 torrent-client/main.py
delete mode 100644 torrent-client/setup.py
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 4522e1ae55d98964219de3dcaeabdc3dc830b1bc..0015965fc284f7c4e1f7ffca4f00bdd5642785e1 100644
GIT binary patch
delta 4374
zcmZ|SbyQS+9>?)1ks3u5P*DLz0RsseR1~oly8~40e(ZQ`Q8B?T>~8D=yRf^vu>;%v
zyxrZiyMOG=dB4uRb7$_IGiS~{zwhvr^c=(0RQD|L21D>2gTbby!C<;=Fc^wYD;(?>
zZ}9IImqB|5C-A9k-Dz!aztgU#U4ZQb%R);ZOLeQK#nIxpdE98TDKt1C+3cQ>ZuYSF
zMEC|IZtw^-7f9-D8Dwj*wlWk;cH8A*wjd)GAXmNs0_+RWmLAxPT4C5<*2MGlgg}|R2Jo|vZ`##MY$?Bm0jgf
zIhDK0rE;r0%0qc7FO^s2Q{F1SDxiFnukup`RUuVa6;b}Gs0vVlDo6#ZVyd_*p-QS!
zs6VlUoYnE+FOdfkbtHDq(yK>tRkP=hQ
z!9M7z!4ULBJ&ql4Dn_+e9aKlvNp)6TR9Dqabyq!9Pt{BHR)48Js;}y&`l|tIpcE&O;%IXR5eY-s{}P&%}_JdEHzur
zQFGNiHD4`I3ss_8q!z0sYN=YLma7$NrCOy{t2Jt^TBp{l4Qiv>q&BN9DoJfs+thZo
zL+w<%)NZv$?N$5KewD0J)B$x+9a4wY5p`4@Q^(Z_byA&Dsp_;kqt2>x>b$z3E~-oF
zvbv(Ks%z@Hx}k2WTk5vDqwcDE>b`oQ9;!#`v3jDOs%PrCdZAvbH1$foR&Uf>^-jH4
zAJj+nNqtu7>WliSzNzo(hx)00so!w}P8p{tFo78?kO6EUBiMo+*nA
zhX&9P8bM=d0!^VAG=~<@5?VoPh=OQn18t!l#6WxK03D$dbcQa_6}mxp=m9;U7xad|
zpbzwge$XEVz(5!TgJB2^g}-4K42Kag5=Oyj7z1M=7RJGNm;iAw5hlT8m;zH_8pJ~a
zOotgT6K26|m;-ZR9?XXYun-bq5iEu!uoRZTa##T?VHK=~HLw=e!Ft#L8(|Y{hAof;
zTVWe)haIpJcEN7g1AAc~?1yAXfdg<54#8nK0!QH(9ETHd5>7!XoQ5-S7S6$WxBwU7
z5?qEWa22k>b+`dH;TGJ6J8&27!F_lD58)9!h9~e8p22f?0WTp9Ucqa418?CSyoV3)
z5kA3ZNQW=*6~4iD_yIrR7yLHr_Gi+D5lmnP3uFKr$OyJz2ln6qj$nmMkQtmH3phho
z$ObOp3T}`cazIXShg^^w@_+|;f*0h4eBcfFp#b=RFZe-0C3%$2!KEcf?y~H
z#i0b0gi=r%%0LK&LRlyWmF(zW=xuDDq>7rnI2*;o46rQsJTL7fQEu0|1~Pu_(bTk
z(TSV%y0=ZxQ^z!m>z^GD*T4?Z-C}~CIHsA?++6hc3+@x$#y9BCSmEXQ=b%re*dSe6
z2Pa49VAmKvK|bNrZjMQ8l`GWz_Wyi^qzSe$7Gs_N3?(^DzQO+-Sxi0=nnmWC7KS;S
zEK@8|eN3V+^n?B|00zP!7z{&TDEtk>U^t9`kuVBI!x$I~u`mwC!vu(fi7*K!!xWeb
z(;yxaU^>iznJ^1x!yK3k^I$$KfQ669@q=}U_T^73LJoga0m{=5jYCR;5eLslW+=B;WV6qvv3a1
z!v(kqm*6s7fva#0uEPzu3Af-j+=07r5AMSQcnFW+F+72%@C=^A3wQ}>@CshT8+Z%v
z;5~eRkMIdTLppqcuka1N!w>igzu>ni&JtzTh7n9)1`A{W8^{Q@U%e};0kV#9dbZUaEDxw8}fh$c!C$?g?!)*`Jn*#fG_w#K_~=;p$PaxQ3!xQ
e2!dcJ2F0NSl!Q`H8p=Qjgqo8rQDx0PhW!KGt1*xO
delta 14627
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 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 (
diff --git a/src/renderer/src/pages/game-details/modals/select-folder-modal.css.tsx b/src/renderer/src/pages/game-details/modals/select-folder-modal.css.tsx
index b5bd4b56..65ca2f19 100644
--- a/src/renderer/src/pages/game-details/modals/select-folder-modal.css.tsx
+++ b/src/renderer/src/pages/game-details/modals/select-folder-modal.css.tsx
@@ -25,4 +25,10 @@ export const downloaders = style({
export const downloaderOption = style({
flex: "1",
+ position: "relative",
+});
+
+export const downloaderIcon = style({
+ position: "absolute",
+ left: `${SPACING_UNIT * 2}px`,
});
diff --git a/src/renderer/src/pages/game-details/modals/select-folder-modal.tsx b/src/renderer/src/pages/game-details/modals/select-folder-modal.tsx
index d0a0c513..bd04eff8 100644
--- a/src/renderer/src/pages/game-details/modals/select-folder-modal.tsx
+++ b/src/renderer/src/pages/game-details/modals/select-folder-modal.tsx
@@ -7,8 +7,9 @@ import { Button, Link, Modal, TextField } from "@renderer/components";
import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react";
import { Downloader, formatBytes } from "@shared";
-import type { GameRepack, UserPreferences } from "@types";
+import type { GameRepack } from "@types";
import { SPACING_UNIT } from "@renderer/theme.css";
+import { DOWNLOADER_NAME } from "@renderer/constants";
export interface SelectFolderModalProps {
visible: boolean;
@@ -21,6 +22,8 @@ export interface SelectFolderModalProps {
repack: GameRepack | null;
}
+const downloaders = [Downloader.Torrent, Downloader.RealDebrid];
+
export function SelectFolderModal({
visible,
onClose,
@@ -32,14 +35,14 @@ export function SelectFolderModal({
const [diskFreeSpace, setDiskFreeSpace] = useState
(null);
const [selectedPath, setSelectedPath] = useState("");
const [downloadStarting, setDownloadStarting] = useState(false);
- const [userPreferences, setUserPreferences] =
- useState(null);
const [selectedDownloader, setSelectedDownloader] = useState(
Downloader.Torrent
);
useEffect(() => {
- visible && getDiskFreeSpace(selectedPath);
+ if (visible) {
+ getDiskFreeSpace(selectedPath);
+ }
}, [visible, selectedPath]);
useEffect(() => {
@@ -48,7 +51,6 @@ export function SelectFolderModal({
window.electron.getUserPreferences(),
]).then(([path, userPreferences]) => {
setSelectedPath(userPreferences?.downloadsPath || path);
- setUserPreferences(userPreferences);
if (userPreferences?.realDebridApiToken) {
setSelectedDownloader(Downloader.RealDebrid);
@@ -106,35 +108,21 @@ export function SelectFolderModal({
-
-
+ {downloaders.map((downloader) => (
+
+ ))}
diff --git a/src/renderer/src/pages/settings/settings-real-debrid.tsx b/src/renderer/src/pages/settings/settings-real-debrid.tsx
index 3f894070..0285663d 100644
--- a/src/renderer/src/pages/settings/settings-real-debrid.tsx
+++ b/src/renderer/src/pages/settings/settings-real-debrid.tsx
@@ -5,8 +5,7 @@ import { Button, CheckboxField, Link, TextField } from "@renderer/components";
import * as styles from "./settings-real-debrid.css";
import type { UserPreferences } from "@types";
import { SPACING_UNIT } from "@renderer/theme.css";
-import { showToast } from "@renderer/features";
-import { useAppDispatch } from "@renderer/hooks";
+import { useToast } from "@renderer/hooks";
const REAL_DEBRID_API_TOKEN_URL = "https://real-debrid.com/apitoken";
@@ -19,12 +18,13 @@ export function SettingsRealDebrid({
userPreferences,
updateUserPreferences,
}: SettingsRealDebridProps) {
+ const [isLoading, setIsLoading] = useState(false);
const [form, setForm] = useState({
useRealDebrid: false,
realDebridApiToken: null as string | null,
});
- const dispatch = useAppDispatch();
+ const { showSuccessToast, showErrorToast } = useToast();
const { t } = useTranslation("settings");
@@ -40,38 +40,40 @@ export function SettingsRealDebrid({
const handleFormSubmit: React.FormEventHandler = async (
event
) => {
+ setIsLoading(true);
event.preventDefault();
- if (form.useRealDebrid) {
- const user = await window.electron.authenticateRealDebrid(
- form.realDebridApiToken!
- );
-
- if (user.type === "premium") {
- dispatch(
- showToast({
- message: t("real_debrid_free_account", { username: user.username }),
- type: "error",
- })
+ try {
+ if (form.useRealDebrid) {
+ const user = await window.electron.authenticateRealDebrid(
+ form.realDebridApiToken!
);
- return;
+ if (user.type === "free") {
+ showErrorToast(
+ t("real_debrid_free_account_error", { username: user.username })
+ );
+
+ return;
+ } else {
+ showSuccessToast(
+ t("real_debrid_linked_message", { username: user.username })
+ );
+ }
}
+
+ updateUserPreferences({
+ realDebridApiToken: form.useRealDebrid ? form.realDebridApiToken : null,
+ });
+ } catch (err) {
+ showErrorToast(t("real_debrid_invalid_token"));
+ } finally {
+ setIsLoading(false);
}
-
- // dispatch(
- // showToast({
- // message: t("real_debrid_free_account", { username: "doctorp" }),
- // type: "error",
- // })
- // );
-
- updateUserPreferences({
- realDebridApiToken: form.useRealDebrid ? form.realDebridApiToken : null,
- });
};
- const isButtonDisabled = form.useRealDebrid && !form.realDebridApiToken;
+ const isButtonDisabled =
+ (form.useRealDebrid && !form.realDebridApiToken) || isLoading;
return (