diff --git a/electron-builder.yml b/electron-builder.yml index b9a4acc6..de64dbcb 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -3,9 +3,10 @@ productName: Hydra directories: buildResources: build extraResources: + - aria2 + - seeds - hydra.db - fastlist.exe - - seeds files: - "!**/.vscode/*" - "!src/*" diff --git a/package.json b/package.json index 3c787f4d..9faff757 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,6 @@ "color": "^4.2.3", "color.js": "^1.2.0", "date-fns": "^3.6.0", - "easydl": "^1.1.1", "electron-updater": "^6.1.8", "fetch-cookie": "^3.0.1", "flexsearch": "^0.7.43", @@ -59,7 +58,6 @@ "jsdom": "^24.0.0", "lodash-es": "^4.17.21", "lottie-react": "^2.4.0", - "node-7z-archive": "^1.1.7", "parse-torrent": "^11.0.16", "ps-list": "^8.1.1", "react-i18next": "^14.1.0", diff --git a/postinstall.cjs b/postinstall.cjs index 8ca8f101..c975e17a 100644 --- a/postinstall.cjs +++ b/postinstall.cjs @@ -1,4 +1,41 @@ -const fs = require("fs"); +const { default: axios } = require("axios"); +const util = require("node:util"); +const fs = require("node:fs"); + +const exec = util.promisify(require("node:child_process").exec); + +const downloadAria2 = async () => { + if (fs.existsSync("aria2")) { + console.log("Aria2 already exists, skipping download..."); + return; + } + + const file = + process.platform === "win32" + ? "aria2-1.37.0-win-64bit-build1.zip" + : "aria2-1.37.0-aarch64-linux-android-build1.zip"; + + console.log(`Downloading ${file}...`); + + const response = await axios.get( + `https://github.com/aria2/aria2/releases/download/release-1.37.0/${file}`, + { responseType: "stream" } + ); + + const stream = response.data.pipe(fs.createWriteStream(file)); + + stream.on("finish", async () => { + console.log(`Downloaded ${file}, extracting...`); + + await exec(`npx extract-zip ${file}`); + console.log("Extracted. Renaming folder..."); + + fs.renameSync(file.replace(".zip", ""), "aria2"); + + console.log(`Extracted ${file}, removing zip file...`); + fs.rmSync(file); + }); +}; if (process.platform === "win32") { fs.copyFileSync( @@ -6,3 +43,5 @@ if (process.platform === "win32") { "fastlist.exe" ); } + +downloadAria2(); diff --git a/src/main/events/library/add-game-to-library.ts b/src/main/events/library/add-game-to-library.ts index 35ffa65a..1db812fa 100644 --- a/src/main/events/library/add-game-to-library.ts +++ b/src/main/events/library/add-game-to-library.ts @@ -10,7 +10,7 @@ const addGameToLibrary = async ( _event: Electron.IpcMainInvokeEvent, objectID: string, title: string, - gameShop: GameShop, + shop: GameShop, executablePath: string | null ) => { return gameRepository @@ -19,7 +19,7 @@ const addGameToLibrary = async ( objectID, }, { - shop: gameShop, + shop, status: null, executablePath, isDeleted: false, @@ -40,7 +40,7 @@ const addGameToLibrary = async ( title, iconUrl, objectID, - shop: gameShop, + shop, executablePath, }) .then(() => { diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts index 82b7be3b..02b36f4a 100644 --- a/src/main/events/torrenting/start-game-download.ts +++ b/src/main/events/torrenting/start-game-download.ts @@ -1,33 +1,18 @@ -import { - gameRepository, - repackRepository, - userPreferencesRepository, -} from "@main/repository"; +import { gameRepository, repackRepository } from "@main/repository"; import { registerEvent } from "../register-event"; -import type { GameShop } from "@types"; +import type { StartGameDownloadPayload } from "@types"; import { getFileBase64, getSteamAppAsset } from "@main/helpers"; import { DownloadManager } from "@main/services"; -import { Downloader } from "@shared"; import { stateManager } from "@main/state-manager"; import { Not } from "typeorm"; const startGameDownload = async ( _event: Electron.IpcMainInvokeEvent, - repackId: number, - objectID: string, - title: string, - gameShop: GameShop, - downloadPath: string + payload: StartGameDownloadPayload ) => { - const userPreferences = await userPreferencesRepository.findOne({ - where: { id: 1 }, - }); - - const downloader = userPreferences?.realDebridApiToken - ? Downloader.RealDebrid - : Downloader.Torrent; + const { repackId, objectID, title, shop, downloadPath, downloader } = payload; const [game, repack] = await Promise.all([ gameRepository.findOne({ @@ -83,7 +68,7 @@ const startGameDownload = async ( iconUrl, objectID, downloader, - shop: gameShop, + shop, status: "active", downloadPath, repack: { id: repackId }, diff --git a/src/main/main.ts b/src/main/main.ts index a9f0ed19..50f18202 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -5,8 +5,8 @@ import { getNewRepacksFromUser, getNewRepacksFromXatab, getNewRepacksFromOnlineFix, - startProcessWatcher, DownloadManager, + startMainLoop, } from "./services"; import { gameRepository, @@ -23,7 +23,7 @@ import { orderBy } from "lodash-es"; import { SteamGame } from "@types"; import { Not } from "typeorm"; -startProcessWatcher(); +startMainLoop(); const track1337xUsers = async (existingRepacks: Repack[]) => { for (const repacker of repackersOn1337x) { @@ -88,8 +88,6 @@ const loadState = async (userPreferences: UserPreferences | null) => { if (userPreferences?.realDebridApiToken) await RealDebridClient.authorize(userPreferences?.realDebridApiToken); - await DownloadManager.connect(); - const game = await gameRepository.findOne({ where: { status: "active", @@ -99,9 +97,7 @@ const loadState = async (userPreferences: UserPreferences | null) => { relations: { repack: true }, }); - if (game) { - DownloadManager.startDownload(game.id); - } + if (game) DownloadManager.startDownload(game.id); }; userPreferencesRepository diff --git a/src/main/services/download-manager.ts b/src/main/services/download-manager.ts index 225d80cb..3511dde5 100644 --- a/src/main/services/download-manager.ts +++ b/src/main/services/download-manager.ts @@ -6,7 +6,7 @@ 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 { Notification, app } from "electron"; import { t } from "i18next"; import { Downloader } from "@shared"; import { DownloadProgress } from "@types"; @@ -16,33 +16,36 @@ import { Game } from "@main/entity"; export class DownloadManager { private static downloads = new Map(); + private static connected = false; 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" - ); + private static connect(): Promise { + return new Promise((resolve) => { + const binaryPath = app.isPackaged + ? path.join(process.resourcesPath, "aria2", "aria2c") + : path.join(__dirname, "..", "..", "aria2", "aria2c"); - spawn( - binary, - [ + const cp = spawn(binaryPath, [ "--enable-rpc", "--rpc-listen-all", "--file-allocation=none", "--allow-overwrite=true", - ], - { stdio: "inherit" } - ); + ]); - await this.aria2.open(); - this.attachListener(); + cp.stdout.on("data", async (data) => { + const msg = Buffer.from(data).toString("utf-8"); + + if (msg.includes("IPv6 RPC: listening on TCP")) { + await this.aria2.open(); + this.connected = true; + + resolve(true); + } + }); + }); } private static getETA(status: StatusResponse) { @@ -84,91 +87,78 @@ export class DownloadManager { return ""; } - private static async attachListener() { - // eslint-disable-next-line no-constant-condition - while (true) { - try { - if (!this.gid || !this.gameId) { - continue; + public static async watchDownloads() { + if (!this.gid || !this.gameId) return; + + 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); + return; + } + + const progress = + Number(status.completedLength) / Number(status.totalLength); + + if (!downloadingMetadata) { + const update: QueryDeepPartialEntity = { + bytesDownloaded: Number(status.completedLength), + fileSize: Number(status.totalLength), + status: status.status, + }; + + if (!isNaN(progress)) update.progress = progress; + + await gameRepository.update( + { id: this.gameId }, + { + ...update, + status: status.status, + folderName: this.getFolderName(status), } + ); + } - const status = await this.aria2.call("tellStatus", this.gid); + const game = await gameRepository.findOne({ + where: { id: this.gameId, isDeleted: false }, + relations: { repack: true }, + }); - 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); - - if (!downloadingMetadata) { - const update: QueryDeepPartialEntity = { - bytesDownloaded: Number(status.completedLength), - fileSize: Number(status.totalLength), - status: status.status, - }; - - if (!isNaN(progress)) update.progress = progress; - - await gameRepository.update( - { id: this.gameId }, - { - ...update, - 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)); + 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 ? -1 : progress); + + 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)) + ); + } } static async getGame(gameId: number) { @@ -227,6 +217,8 @@ export class DownloadManager { } static async startDownload(gameId: number) { + if (!this.connected) await this.connect(); + const game = await this.getGame(gameId)!; if (game) { diff --git a/src/main/services/index.ts b/src/main/services/index.ts index 4808736d..776fd7f6 100644 --- a/src/main/services/index.ts +++ b/src/main/services/index.ts @@ -8,3 +8,4 @@ export * from "./window-manager"; export * from "./download-manager"; export * from "./how-long-to-beat"; export * from "./process-watcher"; +export * from "./main-loop"; diff --git a/src/main/services/main-loop.ts b/src/main/services/main-loop.ts new file mode 100644 index 00000000..9a55ebda --- /dev/null +++ b/src/main/services/main-loop.ts @@ -0,0 +1,16 @@ +import { DownloadManager } from "./download-manager"; +import { watchProcesses } from "./process-watcher"; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export const startMainLoop = async () => { + // eslint-disable-next-line no-constant-condition + while (true) { + await Promise.allSettled([ + watchProcesses(), + DownloadManager.watchDownloads(), + ]); + + await sleep(500); + } +}; diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts index 16646934..882aeea8 100644 --- a/src/main/services/process-watcher.ts +++ b/src/main/services/process-watcher.ts @@ -5,73 +5,58 @@ import { gameRepository } from "@main/repository"; import { getProcesses } from "@main/helpers"; import { WindowManager } from "./window-manager"; -const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); +const gamesPlaytime = new Map(); -export const startProcessWatcher = async () => { - const sleepTime = 500; - const gamesPlaytime = new Map(); +export const watchProcesses = async () => { + const games = await gameRepository.find({ + where: { + executablePath: Not(IsNull()), + isDeleted: false, + }, + }); - // eslint-disable-next-line no-constant-condition - while (true) { - const games = await gameRepository.find({ - where: { - executablePath: Not(IsNull()), - isDeleted: false, - }, + if (games.length === 0) return; + + const processes = await getProcesses(); + + for (const game of games) { + const executablePath = game.executablePath!; + const basename = path.win32.basename(executablePath); + const basenameWithoutExtension = path.win32.basename( + executablePath, + path.extname(executablePath) + ); + + const gameProcess = processes.find((runningProcess) => { + if (process.platform === "win32") { + return runningProcess.name === basename; + } + + return [basename, basenameWithoutExtension].includes(runningProcess.name); }); - if (games.length === 0) { - await sleep(sleepTime); - continue; - } + if (gameProcess) { + if (gamesPlaytime.has(game.id)) { + const zero = gamesPlaytime.get(game.id) ?? 0; + const delta = performance.now() - zero; - const processes = await getProcesses(); - - for (const game of games) { - const executablePath = game.executablePath!; - const basename = path.win32.basename(executablePath); - const basenameWithoutExtension = path.win32.basename( - executablePath, - path.extname(executablePath) - ); - - const gameProcess = processes.find((runningProcess) => { - if (process.platform === "win32") { - return runningProcess.name === basename; - } - - return [basename, basenameWithoutExtension].includes( - runningProcess.name - ); - }); - - if (gameProcess) { - if (gamesPlaytime.has(game.id)) { - const zero = gamesPlaytime.get(game.id) ?? 0; - const delta = performance.now() - zero; - - if (WindowManager.mainWindow) { - WindowManager.mainWindow.webContents.send("on-playtime", game.id); - } - - await gameRepository.update(game.id, { - playTimeInMilliseconds: game.playTimeInMilliseconds + delta, - }); - - gameRepository.update(game.id, { - lastTimePlayed: new Date().toUTCString(), - }); - } - - gamesPlaytime.set(game.id, performance.now()); - } else if (gamesPlaytime.has(game.id)) { - gamesPlaytime.delete(game.id); if (WindowManager.mainWindow) { - WindowManager.mainWindow.webContents.send("on-game-close", game.id); + WindowManager.mainWindow.webContents.send("on-playtime", game.id); } + + await gameRepository.update(game.id, { + playTimeInMilliseconds: game.playTimeInMilliseconds + delta, + lastTimePlayed: new Date().toUTCString(), + }); + } + + gamesPlaytime.set(game.id, performance.now()); + } else if (gamesPlaytime.has(game.id)) { + gamesPlaytime.delete(game.id); + + if (WindowManager.mainWindow) { + WindowManager.mainWindow.webContents.send("on-game-close", game.id); } } - - await sleep(sleepTime); } }; diff --git a/src/preload/index.ts b/src/preload/index.ts index c808d8fc..ecd328df 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -7,26 +7,14 @@ import type { GameShop, DownloadProgress, UserPreferences, - AppUpdaterEvents, + AppUpdaterEvent, + StartGameDownloadPayload, } from "@types"; contextBridge.exposeInMainWorld("electron", { /* Torrenting */ - startGameDownload: ( - repackId: number, - objectID: string, - title: string, - shop: GameShop, - downloadPath: string - ) => - ipcRenderer.invoke( - "startGameDownload", - repackId, - objectID, - title, - shop, - downloadPath - ), + startGameDownload: (payload: StartGameDownloadPayload) => + ipcRenderer.invoke("startGameDownload", payload), cancelGameDownload: (gameId: number) => ipcRenderer.invoke("cancelGameDownload", gameId), pauseGameDownload: (gameId: number) => @@ -115,10 +103,10 @@ contextBridge.exposeInMainWorld("electron", { platform: process.platform, /* Splash */ - onAutoUpdaterEvent: (cb: (value: AppUpdaterEvents) => void) => { + onAutoUpdaterEvent: (cb: (value: AppUpdaterEvent) => void) => { const listener = ( _event: Electron.IpcRendererEvent, - value: AppUpdaterEvents + value: AppUpdaterEvent ) => cb(value); ipcRenderer.on("autoUpdaterEvent", listener); diff --git a/src/renderer/src/components/bottom-panel/bottom-panel.tsx b/src/renderer/src/components/bottom-panel/bottom-panel.tsx index 310f31b4..e52cc3b0 100644 --- a/src/renderer/src/components/bottom-panel/bottom-panel.tsx +++ b/src/renderer/src/components/bottom-panel/bottom-panel.tsx @@ -15,8 +15,7 @@ export function BottomPanel() { const { lastPacket, progress, downloadSpeed, eta } = useDownload(); - const isGameDownloading = - lastPacket?.game && lastPacket?.game.status === "active"; + const isGameDownloading = !!lastPacket?.game; const [version, setVersion] = useState(""); diff --git a/src/renderer/src/components/button/button.css.ts b/src/renderer/src/components/button/button.css.ts index de808ad8..e342b2e9 100644 --- a/src/renderer/src/components/button/button.css.ts +++ b/src/renderer/src/components/button/button.css.ts @@ -18,7 +18,6 @@ const base = style({ }, ":disabled": { opacity: vars.opacity.disabled, - pointerEvents: "none", cursor: "not-allowed", }, }); @@ -30,6 +29,9 @@ export const button = styleVariants({ ":hover": { backgroundColor: "#DADBE1", }, + ":disabled": { + backgroundColor: vars.color.muted, + }, }, ], outline: [ @@ -41,6 +43,9 @@ export const button = styleVariants({ ":hover": { backgroundColor: "rgba(255, 255, 255, 0.1)", }, + ":disabled": { + backgroundColor: "transparent", + }, }, ], dark: [ diff --git a/src/renderer/src/components/sidebar/sidebar.css.ts b/src/renderer/src/components/sidebar/sidebar.css.ts index e91ef4a7..929241fa 100644 --- a/src/renderer/src/components/sidebar/sidebar.css.ts +++ b/src/renderer/src/components/sidebar/sidebar.css.ts @@ -28,7 +28,6 @@ export const content = recipe({ flexDirection: "column", padding: `${SPACING_UNIT * 2}px`, gap: `${SPACING_UNIT * 2}px`, - paddingBottom: "0", width: "100%", overflow: "auto", }, diff --git a/src/renderer/src/components/toast/toast.css.ts b/src/renderer/src/components/toast/toast.css.ts index bc8d5377..eebe7bda 100644 --- a/src/renderer/src/components/toast/toast.css.ts +++ b/src/renderer/src/components/toast/toast.css.ts @@ -48,10 +48,8 @@ export const toast = recipe({ export const toastContent = style({ display: "flex", - position: "relative", - gap: `${SPACING_UNIT}px`, - padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 5}px`, - paddingLeft: `${SPACING_UNIT * 2}px`, + gap: `${SPACING_UNIT * 2}px`, + padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`, justifyContent: "center", alignItems: "center", }); @@ -63,13 +61,11 @@ export const progress = style({ backgroundColor: vars.color.darkBackground, }, "::-webkit-progress-value": { - backgroundColor: "#1c9749", + backgroundColor: vars.color.muted, }, }); export const closeButton = style({ - position: "absolute", - right: `${SPACING_UNIT}px`, color: vars.color.bodyText, cursor: "pointer", padding: "0", @@ -77,5 +73,9 @@ export const closeButton = style({ }); export const successIcon = style({ - color: "#1c9749", + color: vars.color.success, +}); + +export const errorIcon = style({ + color: vars.color.danger, }); diff --git a/src/renderer/src/components/toast/toast.tsx b/src/renderer/src/components/toast/toast.tsx index 1228653e..0bded31d 100644 --- a/src/renderer/src/components/toast/toast.tsx +++ b/src/renderer/src/components/toast/toast.tsx @@ -1,12 +1,12 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { CheckCircleFillIcon, - CheckCircleIcon, - XCircleIcon, + XCircleFillIcon, XIcon, } from "@primer/octicons-react"; import * as styles from "./toast.css"; +import { SPACING_UNIT } from "@renderer/theme.css"; export interface ToastProps { visible: boolean; @@ -78,8 +78,13 @@ export function Toast({ visible, message, type, onClose }: ToastProps) { return (
- - {message} +
+ {type === "success" && ( + + )} + {type === "error" && } + {message} +
diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 8a4e1c93..26cde84b 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -10,6 +10,7 @@ import type { Steam250Game, DownloadProgress, UserPreferences, + StartGameDownloadPayload, } from "@types"; import type { DiskSpace } from "check-disk-space"; @@ -21,13 +22,7 @@ declare global { interface Electron { /* Torrenting */ - startGameDownload: ( - repackId: number, - objectID: string, - title: string, - shop: GameShop, - downloadPath: string - ) => Promise; + startGameDownload: (payload: StartGameDownloadPayload) => Promise; cancelGameDownload: (gameId: number) => Promise; pauseGameDownload: (gameId: number) => Promise; resumeGameDownload: (gameId: number) => Promise; diff --git a/src/renderer/src/hooks/use-download.ts b/src/renderer/src/hooks/use-download.ts index 43ed65b6..05a44afd 100644 --- a/src/renderer/src/hooks/use-download.ts +++ b/src/renderer/src/hooks/use-download.ts @@ -9,7 +9,7 @@ import { setGameDeleting, removeGameFromDeleting, } from "@renderer/features"; -import type { DownloadProgress, GameShop } from "@types"; +import type { DownloadProgress, StartGameDownloadPayload } from "@types"; import { useDate } from "./use-date"; import { formatBytes } from "@shared"; @@ -22,21 +22,13 @@ export function useDownload() { ); const dispatch = useAppDispatch(); - const startDownload = ( - repackId: number, - objectID: string, - title: string, - shop: GameShop, - downloadPath: string - ) => - window.electron - .startGameDownload(repackId, objectID, title, shop, downloadPath) - .then((game) => { - dispatch(clearDownload()); - updateLibrary(); + const startDownload = (payload: StartGameDownloadPayload) => + window.electron.startGameDownload(payload).then((game) => { + dispatch(clearDownload()); + updateLibrary(); - return game; - }); + return game; + }); const pauseDownload = async (gameId: number) => { await window.electron.pauseGameDownload(gameId); @@ -62,11 +54,11 @@ export function useDownload() { }); const getETA = () => { - if (lastPacket && lastPacket.timeRemaining < 0) return ""; + if (!lastPacket || lastPacket.timeRemaining < 0) return ""; try { return formatDistance( - addMilliseconds(new Date(), lastPacket?.timeRemaining ?? 1), + addMilliseconds(new Date(), lastPacket.timeRemaining), new Date(), { addSuffix: true } ); @@ -94,7 +86,7 @@ export function useDownload() { return { downloadSpeed: `${formatBytes(lastPacket?.downloadSpeed ?? 0)}/s`, - progress: formatDownloadProgress(lastPacket?.game.progress ?? 0), + progress: formatDownloadProgress(lastPacket?.game.progress), lastPacket, eta: getETA(), startDownload, diff --git a/src/renderer/src/pages/downloads/downloads.tsx b/src/renderer/src/pages/downloads/downloads.tsx index b2d2b0f3..2059d029 100644 --- a/src/renderer/src/pages/downloads/downloads.tsx +++ b/src/renderer/src/pages/downloads/downloads.tsx @@ -254,9 +254,7 @@ export function Downloads() { diff --git a/src/renderer/src/pages/game-details/gallery-slider.css.ts b/src/renderer/src/pages/game-details/gallery-slider.css.ts index 801c3a5b..f6995312 100644 --- a/src/renderer/src/pages/game-details/gallery-slider.css.ts +++ b/src/renderer/src/pages/game-details/gallery-slider.css.ts @@ -67,6 +67,7 @@ export const mediaPreviewButton = recipe({ transition: "translate 0.3s ease-in-out, opacity 0.2s ease", borderRadius: "4px", border: `solid 1px ${vars.color.border}`, + overflow: "hidden", ":hover": { opacity: "0.8", }, @@ -84,7 +85,6 @@ export const mediaPreview = style({ width: "100%", height: "100%", display: "flex", - flex: "1", }); export const gallerySliderButton = recipe({ diff --git a/src/renderer/src/pages/game-details/game-details.context.tsx b/src/renderer/src/pages/game-details/game-details.context.tsx index 7f197020..87cd97a6 100644 --- a/src/renderer/src/pages/game-details/game-details.context.tsx +++ b/src/renderer/src/pages/game-details/game-details.context.tsx @@ -15,6 +15,7 @@ import { OnlineFixInstallationGuide, RepacksModal, } from "./modals"; +import { Downloader } from "@shared"; export interface GameDetailsContext { game: Game | null; @@ -138,15 +139,17 @@ export function GameDetailsContextProvider({ const handleStartDownload = async ( repack: GameRepack, + downloader: Downloader, downloadPath: string ) => { - await startDownload( - repack.id, - objectID!, - gameTitle, - shop as GameShop, - downloadPath - ); + await startDownload({ + repackId: repack.id, + objectID: objectID!, + title: gameTitle, + downloader, + shop: shop as GameShop, + downloadPath, + }); await updateGame(); setShowRepacksModal(false); diff --git a/src/renderer/src/pages/game-details/hero/hero-panel.css.ts b/src/renderer/src/pages/game-details/hero/hero-panel.css.ts index 640196e6..20cb73a4 100644 --- a/src/renderer/src/pages/game-details/hero/hero-panel.css.ts +++ b/src/renderer/src/pages/game-details/hero/hero-panel.css.ts @@ -1,5 +1,6 @@ import { style } from "@vanilla-extract/css"; import { SPACING_UNIT, vars } from "../../../theme.css"; +import { recipe } from "@vanilla-extract/recipes"; export const panel = style({ width: "100%", @@ -11,6 +12,8 @@ export const panel = style({ justifyContent: "space-between", transition: "all ease 0.2s", borderBottom: `solid 1px ${vars.color.border}`, + position: "relative", + overflow: "hidden", }); export const content = style({ @@ -29,3 +32,27 @@ export const downloadDetailsRow = style({ display: "flex", alignItems: "flex-end", }); + +export const progressBar = recipe({ + base: { + position: "absolute", + bottom: "0", + left: "0", + width: "100%", + height: "3px", + transition: "all ease 0.2s", + "::-webkit-progress-bar": { + backgroundColor: "transparent", + }, + "::-webkit-progress-value": { + backgroundColor: vars.color.muted, + }, + }, + variants: { + disabled: { + true: { + opacity: vars.opacity.disabled, + }, + }, + }, +}); 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 23a14f58..a736d133 100644 --- a/src/renderer/src/pages/game-details/hero/hero-panel.tsx +++ b/src/renderer/src/pages/game-details/hero/hero-panel.tsx @@ -33,29 +33,46 @@ export function HeroPanel() { return game.repack?.fileSize ?? "N/A"; }, [game, lastPacket?.game]); + const isGameDownloading = + game?.status === "active" && lastPacket?.game.id === game?.id; + const getInfo = () => { if (isGameDeleting(game?.id ?? -1)) return

{t("deleting")}

; if (game?.progress === 1) return ; - console.log(lastPacket?.game.id, game?.id); - - if (game?.status === "active" && lastPacket?.game.id === game?.id) { - if (lastPacket?.downloadingMetadata) { - return

{t("downloading_metadata")}

; + if (game?.status === "active") { + if (lastPacket?.downloadingMetadata && isGameDownloading) { + return ( + <> +

{progress}

+

{t("downloading_metadata")}

+ + ); } + const sizeDownloaded = formatBytes( + lastPacket?.game?.bytesDownloaded ?? game?.bytesDownloaded + ); + + const showPeers = + game?.downloader === Downloader.Torrent && + lastPacket?.numPeers !== undefined; + return ( <>

- {progress} + {isGameDownloading + ? progress + : formatDownloadProgress(game?.progress)} {eta && {t("eta", { eta })}}

- {formatBytes(lastPacket?.game?.bytesDownloaded ?? 0)} /{" "} - {finalDownloadSize} - {game?.downloader === Downloader.Torrent && ( + + {sizeDownloaded} / {finalDownloadSize} + + {showPeers && ( {lastPacket?.numPeers} peers / {lastPacket?.numSeeds} seeds @@ -99,6 +116,10 @@ export function HeroPanel() { ? (new Color(gameColor).darken(0.6).toString() as string) : ""; + const showProgressBar = + (game?.status === "active" && game?.progress < 1) || + game?.status === "paused"; + return ( <> setShowBinaryNotFoundModal(true)} />

+ + {showProgressBar && ( + + )} ); diff --git a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx index 1aea6c0d..8b52721e 100644 --- a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx @@ -10,10 +10,15 @@ import { SPACING_UNIT } from "../../../theme.css"; import { format } from "date-fns"; import { SelectFolderModal } from "./select-folder-modal"; import { gameDetailsContext } from "../game-details.context"; +import { Downloader } from "@shared"; export interface RepacksModalProps { visible: boolean; - startDownload: (repack: GameRepack, downloadPath: string) => Promise; + startDownload: ( + repack: GameRepack, + downloader: Downloader, + downloadPath: string + ) => Promise; onClose: () => void; } 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 4b43c74d..6825f32b 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 @@ -4,15 +4,19 @@ 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 { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react"; +import { Downloader, formatBytes } from "@shared"; -import type { GameRepack } from "@types"; +import type { GameRepack, UserPreferences } from "@types"; export interface SelectFolderModalProps { visible: boolean; onClose: () => void; - startDownload: (repack: GameRepack, downloadPath: string) => Promise; + startDownload: ( + repack: GameRepack, + downloader: Downloader, + downloadPath: string + ) => Promise; repack: GameRepack | null; } @@ -27,6 +31,11 @@ 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); @@ -38,6 +47,11 @@ export function SelectFolderModal({ window.electron.getUserPreferences(), ]).then(([path, userPreferences]) => { setSelectedPath(userPreferences?.downloadsPath || path); + setUserPreferences(userPreferences); + + if (userPreferences?.realDebridApiToken) { + setSelectedDownloader(Downloader.RealDebrid); + } }); }, []); @@ -63,7 +77,7 @@ export function SelectFolderModal({ if (repack) { setDownloadStarting(true); - startDownload(repack, selectedPath).finally(() => { + startDownload(repack, selectedDownloader, selectedPath).finally(() => { setDownloadStarting(false); onClose(); }); @@ -73,7 +87,7 @@ export function SelectFolderModal({ return (
- +
- - +
- +