diff --git a/.gitignore b/.gitignore index 7a6496a5..fb4badd7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ .vscode node_modules hydra-download-manager/ -aria2/ fastlist.exe __pycache__ dist diff --git a/electron-builder.yml b/electron-builder.yml index be300d36..cfdafe7d 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -3,7 +3,6 @@ productName: Hydra directories: buildResources: build extraResources: - - aria2 - hydra-download-manager - seeds - from: node_modules/create-desktop-shortcuts/src/windows.vbs diff --git a/package.json b/package.json index 1b99734d..aa77084e 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "start": "electron-vite preview", "dev": "electron-vite dev", "build": "npm run typecheck && electron-vite build", - "postinstall": "electron-builder install-app-deps && node ./postinstall.cjs", + "postinstall": "electron-builder install-app-deps", "build:unpack": "npm run build && electron-builder --dir", "build:win": "electron-vite build && electron-builder --win", "build:mac": "electron-vite build && electron-builder --mac", @@ -34,15 +34,13 @@ "dependencies": { "@electron-toolkit/preload": "^3.0.0", "@electron-toolkit/utils": "^3.0.0", - "@fontsource/fira-mono": "^5.0.13", - "@fontsource/fira-sans": "^5.0.20", + "@fontsource/noto-sans": "^5.0.22", "@primer/octicons-react": "^19.9.0", "@reduxjs/toolkit": "^2.2.3", "@sentry/electron": "^5.1.0", "@vanilla-extract/css": "^1.14.2", "@vanilla-extract/dynamic": "^2.1.1", "@vanilla-extract/recipes": "^0.5.2", - "aria2": "^4.1.2", "auto-launch": "^5.0.6", "axios": "^1.6.8", "better-sqlite3": "^9.5.0", @@ -97,7 +95,7 @@ "@types/user-agents": "^1.0.4", "@vanilla-extract/vite-plugin": "^4.0.7", "@vitejs/plugin-react": "^4.2.1", - "electron": "^30.0.9", + "electron": "^30.3.0", "electron-builder": "^24.9.1", "electron-vite": "^2.0.0", "eslint": "^8.56.0", diff --git a/postinstall.cjs b/postinstall.cjs deleted file mode 100644 index 547af988..00000000 --- a/postinstall.cjs +++ /dev/null @@ -1,50 +0,0 @@ -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-1-x86_64.pkg.tar.zst"; - - const downloadUrl = - process.platform === "win32" - ? `https://github.com/aria2/aria2/releases/download/release-1.37.0/${file}` - : "https://archlinux.org/packages/extra/x86_64/aria2/download/"; - - console.log(`Downloading ${file}...`); - - const response = await axios.get(downloadUrl, { responseType: "stream" }); - - const stream = response.data.pipe(fs.createWriteStream(file)); - - stream.on("finish", async () => { - console.log(`Downloaded ${file}, extracting...`); - - if (process.platform === "win32") { - await exec(`npx extract-zip ${file}`); - console.log("Extracted. Renaming folder..."); - - fs.renameSync(file.replace(".zip", ""), "aria2"); - } else { - await exec(`tar --zstd -xvf ${file} usr/bin/aria2c`); - console.log("Extracted. Copying binary file..."); - fs.mkdirSync("aria2"); - fs.copyFileSync("usr/bin/aria2c", "aria2/aria2c"); - fs.rmSync("usr", { recursive: true }); - } - - console.log(`Extracted ${file}, removing compressed downloaded file...`); - fs.rmSync(file); - }); -}; - -downloadAria2(); diff --git a/src/main/declaration.d.ts b/src/main/declaration.d.ts deleted file mode 100644 index ac2675a3..00000000 --- a/src/main/declaration.d.ts +++ /dev/null @@ -1,80 +0,0 @@ -declare module "aria2" { - export type Aria2Status = - | "active" - | "waiting" - | "paused" - | "error" - | "complete" - | "removed"; - - export interface StatusResponse { - gid: string; - status: Aria2Status; - totalLength: string; - completedLength: string; - uploadLength: string; - bitfield: string; - downloadSpeed: string; - uploadSpeed: string; - infoHash?: string; - numSeeders?: string; - seeder?: boolean; - pieceLength: string; - numPieces: string; - connections: string; - errorCode?: string; - errorMessage?: string; - followedBy?: string[]; - following: string; - belongsTo: string; - dir: string; - files: { - path: string; - length: string; - completedLength: string; - selected: string; - }[]; - bittorrent?: { - announceList: string[][]; - comment: string; - creationDate: string; - mode: "single" | "multi"; - info: { - name: string; - verifiedLength: string; - verifyIntegrityPending: string; - }; - }; - } - - export default class Aria2 { - constructor(options: any); - open: () => Promise; - call( - method: "addUri", - uris: string[], - options: { dir: string } - ): Promise; - call( - method: "tellStatus", - gid: string, - keys?: string[] - ): Promise; - call(method: "pause", gid: string): Promise; - call(method: "forcePause", gid: string): Promise; - call(method: "unpause", gid: string): Promise; - call(method: "remove", gid: string): Promise; - call(method: "forceRemove", gid: string): Promise; - call(method: "pauseAll"): Promise; - call(method: "forcePauseAll"): Promise; - listNotifications: () => [ - "onDownloadStart", - "onDownloadPause", - "onDownloadStop", - "onDownloadComplete", - "onDownloadError", - "onBtDownloadComplete", - ]; - on: (event: string, callback: (params: any) => void) => void; - } -} diff --git a/src/main/events/auth/sign-out.ts b/src/main/events/auth/sign-out.ts index fe640b9d..9998c733 100644 --- a/src/main/events/auth/sign-out.ts +++ b/src/main/events/auth/sign-out.ts @@ -26,6 +26,8 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => { /* Disconnects libtorrent */ PythonInstance.killTorrent(); + HydraApi.handleSignOut(); + await Promise.all([ databaseOperations, HydraApi.post("/auth/logout").catch(() => {}), diff --git a/src/main/main.ts b/src/main/main.ts index fbabc56c..af594e20 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -22,8 +22,9 @@ const loadState = async (userPreferences: UserPreferences | null) => { import("./events"); - if (userPreferences?.realDebridApiToken) + if (userPreferences?.realDebridApiToken) { RealDebridClient.authorize(userPreferences?.realDebridApiToken); + } HydraApi.setupApi().then(() => { uploadGamesBatch(); diff --git a/src/main/services/aria2c.ts b/src/main/services/aria2c.ts deleted file mode 100644 index b1b1da76..00000000 --- a/src/main/services/aria2c.ts +++ /dev/null @@ -1,20 +0,0 @@ -import path from "node:path"; -import { spawn } from "node:child_process"; -import { app } from "electron"; - -export const startAria2 = () => { - const binaryPath = app.isPackaged - ? path.join(process.resourcesPath, "aria2", "aria2c") - : path.join(__dirname, "..", "..", "aria2", "aria2c"); - - return spawn( - binaryPath, - [ - "--enable-rpc", - "--rpc-listen-all", - "--file-allocation=none", - "--allow-overwrite=true", - ], - { stdio: "inherit", windowsHide: true } - ); -}; diff --git a/src/main/services/download/download-manager.ts b/src/main/services/download/download-manager.ts index 31f28992..52a66693 100644 --- a/src/main/services/download/download-manager.ts +++ b/src/main/services/download/download-manager.ts @@ -6,6 +6,8 @@ import { downloadQueueRepository, gameRepository } from "@main/repository"; import { publishDownloadCompleteNotification } from "../notifications"; import { RealDebridDownloader } from "./real-debrid-downloader"; import type { DownloadProgress } from "@types"; +import { GofileApi } from "../hosters"; +import { GenericHTTPDownloader } from "./generic-http-downloader"; export class DownloadManager { private static currentDownloader: Downloader | null = null; @@ -13,10 +15,12 @@ export class DownloadManager { public static async watchDownloads() { let status: DownloadProgress | null = null; - if (this.currentDownloader === Downloader.RealDebrid) { + if (this.currentDownloader === Downloader.Torrent) { + status = await PythonInstance.getStatus(); + } else if (this.currentDownloader === Downloader.RealDebrid) { status = await RealDebridDownloader.getStatus(); } else { - status = await PythonInstance.getStatus(); + status = await GenericHTTPDownloader.getStatus(); } if (status) { @@ -62,10 +66,12 @@ export class DownloadManager { } static async pauseDownload() { - if (this.currentDownloader === Downloader.RealDebrid) { + if (this.currentDownloader === Downloader.Torrent) { + await PythonInstance.pauseDownload(); + } else if (this.currentDownloader === Downloader.RealDebrid) { await RealDebridDownloader.pauseDownload(); } else { - await PythonInstance.pauseDownload(); + await GenericHTTPDownloader.pauseDownload(); } WindowManager.mainWindow?.setProgressBar(-1); @@ -73,20 +79,16 @@ export class DownloadManager { } static async resumeDownload(game: Game) { - if (game.downloader === Downloader.RealDebrid) { - RealDebridDownloader.startDownload(game); - this.currentDownloader = Downloader.RealDebrid; - } else { - PythonInstance.startDownload(game); - this.currentDownloader = Downloader.Torrent; - } + return this.startDownload(game); } static async cancelDownload(gameId: number) { - if (this.currentDownloader === Downloader.RealDebrid) { + if (this.currentDownloader === Downloader.Torrent) { + PythonInstance.cancelDownload(gameId); + } else if (this.currentDownloader === Downloader.RealDebrid) { RealDebridDownloader.cancelDownload(gameId); } else { - PythonInstance.cancelDownload(gameId); + GenericHTTPDownloader.cancelDownload(gameId); } WindowManager.mainWindow?.setProgressBar(-1); @@ -94,12 +96,28 @@ export class DownloadManager { } static async startDownload(game: Game) { - if (game.downloader === Downloader.RealDebrid) { - RealDebridDownloader.startDownload(game); - this.currentDownloader = Downloader.RealDebrid; - } else { + if (game.downloader === Downloader.Gofile) { + const id = game!.uri!.split("/").pop(); + + const token = await GofileApi.authorize(); + const downloadLink = await GofileApi.getDownloadLink(id!); + + GenericHTTPDownloader.startDownload(game, downloadLink, { + Cookie: `accountToken=${token}`, + }); + } else if (game.downloader === Downloader.PixelDrain) { + const id = game!.uri!.split("/").pop(); + + await GenericHTTPDownloader.startDownload( + game, + `https://pixeldrain.com/api/file/${id}?download` + ); + } else if (game.downloader === Downloader.Torrent) { PythonInstance.startDownload(game); - this.currentDownloader = Downloader.Torrent; + } else if (game.downloader === Downloader.RealDebrid) { + RealDebridDownloader.startDownload(game); } + + this.currentDownloader = game.downloader; } } diff --git a/src/main/services/download/generic-http-downloader.ts b/src/main/services/download/generic-http-downloader.ts new file mode 100644 index 00000000..8384a5fd --- /dev/null +++ b/src/main/services/download/generic-http-downloader.ts @@ -0,0 +1,107 @@ +import { Game } from "@main/entity"; +import { gameRepository } from "@main/repository"; +import { calculateETA } from "./helpers"; +import { DownloadProgress } from "@types"; +import { HttpDownload } from "./http-download"; + +export class GenericHTTPDownloader { + private static downloads = new Map(); + private static downloadingGame: Game | null = null; + + public static async getStatus() { + if (this.downloadingGame) { + const gid = this.downloads.get(this.downloadingGame.id)!; + const status = HttpDownload.getStatus(gid); + + if (status) { + const progress = + Number(status.completedLength) / Number(status.totalLength); + + await gameRepository.update( + { id: this.downloadingGame!.id }, + { + bytesDownloaded: Number(status.completedLength), + fileSize: Number(status.totalLength), + progress, + status: "active", + folderName: status.folderName, + } + ); + + const result = { + numPeers: 0, + numSeeds: 0, + downloadSpeed: status.downloadSpeed, + timeRemaining: calculateETA( + status.totalLength, + status.completedLength, + status.downloadSpeed + ), + isDownloadingMetadata: false, + isCheckingFiles: false, + progress, + gameId: this.downloadingGame!.id, + } as DownloadProgress; + + if (progress === 1) { + this.downloads.delete(this.downloadingGame.id); + this.downloadingGame = null; + } + + return result; + } + } + + return null; + } + + static async pauseDownload() { + if (this.downloadingGame) { + const gid = this.downloads.get(this.downloadingGame!.id!); + + if (gid) { + await HttpDownload.pauseDownload(gid); + } + + this.downloadingGame = null; + } + } + + static async startDownload( + game: Game, + downloadUrl: string, + headers?: Record + ) { + this.downloadingGame = game; + + if (this.downloads.has(game.id)) { + await this.resumeDownload(game.id!); + return; + } + + const gid = await HttpDownload.startDownload( + game.downloadPath!, + downloadUrl, + headers + ); + + this.downloads.set(game.id!, gid); + } + + static async cancelDownload(gameId: number) { + const gid = this.downloads.get(gameId); + + if (gid) { + await HttpDownload.cancelDownload(gid); + this.downloads.delete(gameId); + } + } + + static async resumeDownload(gameId: number) { + const gid = this.downloads.get(gameId); + + if (gid) { + await HttpDownload.resumeDownload(gid); + } + } +} diff --git a/src/main/services/download/http-download.ts b/src/main/services/download/http-download.ts index 4553a6cb..cd8cbee5 100644 --- a/src/main/services/download/http-download.ts +++ b/src/main/services/download/http-download.ts @@ -1,68 +1,69 @@ -import type { ChildProcess } from "node:child_process"; -import { logger } from "../logger"; -import { sleep } from "@main/helpers"; -import { startAria2 } from "../aria2c"; -import Aria2 from "aria2"; +import { DownloadItem } from "electron"; +import { WindowManager } from "../window-manager"; +import path from "node:path"; export class HttpDownload { - private static connected = false; - private static aria2c: ChildProcess | null = null; + private static id = 0; - private static aria2 = new Aria2({}); + private static downloads: Record = {}; - private static async connect() { - this.aria2c = startAria2(); - - let retries = 0; - - while (retries < 4 && !this.connected) { - try { - await this.aria2.open(); - logger.log("Connected to aria2"); - - this.connected = true; - } catch (err) { - await sleep(100); - logger.log("Failed to connect to aria2, retrying..."); - retries++; - } - } - } - - public static getStatus(gid: string) { - if (this.connected) { - return this.aria2.call("tellStatus", gid); + public static getStatus(gid: string): { + completedLength: number; + totalLength: number; + downloadSpeed: number; + folderName: string; + } | null { + const downloadItem = this.downloads[gid]; + if (downloadItem) { + return { + completedLength: downloadItem.getReceivedBytes(), + totalLength: downloadItem.getTotalBytes(), + downloadSpeed: downloadItem.getCurrentBytesPerSecond(), + folderName: downloadItem.getFilename(), + }; } return null; } - public static disconnect() { - if (this.aria2c) { - this.aria2c.kill(); - this.connected = false; - } - } - static async cancelDownload(gid: string) { - await this.aria2.call("forceRemove", gid); + const downloadItem = this.downloads[gid]; + downloadItem?.cancel(); + delete this.downloads[gid]; } static async pauseDownload(gid: string) { - await this.aria2.call("forcePause", gid); + const downloadItem = this.downloads[gid]; + downloadItem?.pause(); } static async resumeDownload(gid: string) { - await this.aria2.call("unpause", gid); + const downloadItem = this.downloads[gid]; + downloadItem?.resume(); } - static async startDownload(downloadPath: string, downloadUrl: string) { - if (!this.connected) await this.connect(); + static async startDownload( + downloadPath: string, + downloadUrl: string, + headers?: Record + ) { + return new Promise((resolve) => { + const options = headers ? { headers } : {}; + WindowManager.mainWindow?.webContents.downloadURL(downloadUrl, options); - const options = { - dir: downloadPath, - }; + const gid = ++this.id; - return this.aria2.call("addUri", [downloadUrl], options); + WindowManager.mainWindow?.webContents.session.on( + "will-download", + (_event, item, _webContents) => { + this.downloads[gid.toString()] = item; + + // Set the save path, making Electron not to prompt a save dialog. + item.setSavePath(path.join(downloadPath, item.getFilename())); + + resolve(gid.toString()); + } + ); + }); } } diff --git a/src/main/services/download/real-debrid-downloader.ts b/src/main/services/download/real-debrid-downloader.ts index 8ead0067..c6925f57 100644 --- a/src/main/services/download/real-debrid-downloader.ts +++ b/src/main/services/download/real-debrid-downloader.ts @@ -13,22 +13,36 @@ export class RealDebridDownloader { private static async getRealDebridDownloadUrl() { if (this.realDebridTorrentId) { - const torrentInfo = await RealDebridClient.getTorrentInfo( + let torrentInfo = await RealDebridClient.getTorrentInfo( this.realDebridTorrentId ); - const { status, links } = torrentInfo; - - if (status === "waiting_files_selection") { + if (torrentInfo.status === "waiting_files_selection") { await RealDebridClient.selectAllFiles(this.realDebridTorrentId); - return null; + + torrentInfo = await RealDebridClient.getTorrentInfo( + this.realDebridTorrentId + ); } + const { links, status } = torrentInfo; + if (status === "downloaded") { const [link] = links; + const { download } = await RealDebridClient.unrestrictLink(link); return decodeURIComponent(download); } + + return null; + } + + if (this.downloadingGame?.uri) { + const { download } = await RealDebridClient.unrestrictLink( + this.downloadingGame?.uri + ); + + return decodeURIComponent(download); } return null; @@ -37,7 +51,7 @@ export class RealDebridDownloader { public static async getStatus() { if (this.downloadingGame) { const gid = this.downloads.get(this.downloadingGame.id)!; - const status = await HttpDownload.getStatus(gid); + const status = HttpDownload.getStatus(gid); if (status) { const progress = @@ -50,6 +64,7 @@ export class RealDebridDownloader { fileSize: Number(status.totalLength), progress, status: "active", + folderName: status.folderName, } ); @@ -78,40 +93,15 @@ export class RealDebridDownloader { } } - if (this.realDebridTorrentId && this.downloadingGame) { - const torrentInfo = await RealDebridClient.getTorrentInfo( - this.realDebridTorrentId - ); - - const { status } = torrentInfo; - - if (status === "downloaded") { - this.startDownload(this.downloadingGame); - } - - const progress = torrentInfo.progress / 100; - const totalDownloaded = progress * torrentInfo.bytes; - - return { - numPeers: 0, - numSeeds: torrentInfo.seeders, - downloadSpeed: torrentInfo.speed, - timeRemaining: calculateETA( - torrentInfo.bytes, - totalDownloaded, - torrentInfo.speed - ), - isDownloadingMetadata: status === "magnet_conversion", - } as DownloadProgress; - } - return null; } static async pauseDownload() { - const gid = this.downloads.get(this.downloadingGame!.id!); - if (gid) { - await HttpDownload.pauseDownload(gid); + if (this.downloadingGame) { + const gid = this.downloads.get(this.downloadingGame.id); + if (gid) { + await HttpDownload.pauseDownload(gid); + } } this.realDebridTorrentId = null; @@ -119,15 +109,19 @@ export class RealDebridDownloader { } static async startDownload(game: Game) { - this.downloadingGame = game; - if (this.downloads.has(game.id)) { await this.resumeDownload(game.id!); - + this.downloadingGame = game; return; } - this.realDebridTorrentId = await RealDebridClient.getTorrentId(game!.uri!); + if (game.uri?.startsWith("magnet:")) { + this.realDebridTorrentId = await RealDebridClient.getTorrentId( + game!.uri! + ); + } + + this.downloadingGame = game; const downloadUrl = await this.getRealDebridDownloadUrl(); @@ -150,6 +144,9 @@ export class RealDebridDownloader { await HttpDownload.cancelDownload(gid); this.downloads.delete(gameId); } + + this.realDebridTorrentId = null; + this.downloadingGame = null; } static async resumeDownload(gameId: number) { diff --git a/src/main/services/hosters/gofile.ts b/src/main/services/hosters/gofile.ts new file mode 100644 index 00000000..770bb15f --- /dev/null +++ b/src/main/services/hosters/gofile.ts @@ -0,0 +1,61 @@ +import axios from "axios"; + +export interface GofileAccountsReponse { + id: string; + token: string; +} + +export interface GofileContentChild { + id: string; + link: string; +} + +export interface GofileContentsResponse { + id: string; + type: string; + children: Record; +} + +export class GofileApi { + private static token: string; + + public static async authorize() { + const response = await axios.post<{ + status: string; + data: GofileAccountsReponse; + }>("https://api.gofile.io/accounts"); + + if (response.data.status === "ok") { + this.token = response.data.data.token; + return this.token; + } + + throw new Error("Failed to authorize"); + } + + public static async getDownloadLink(id: string) { + const searchParams = new URLSearchParams({ + wt: "4fd6sg89d7s6", + }); + + const response = await axios.get<{ + status: string; + data: GofileContentsResponse; + }>(`https://api.gofile.io/contents/${id}?${searchParams.toString()}`, { + headers: { + Authorization: `Bearer ${this.token}`, + }, + }); + + if (response.data.status === "ok") { + if (response.data.data.type !== "folder") { + throw new Error("Only folders are supported"); + } + + const [firstChild] = Object.values(response.data.data.children); + return firstChild.link; + } + + throw new Error("Failed to get download link"); + } +} diff --git a/src/main/services/hosters/index.ts b/src/main/services/hosters/index.ts new file mode 100644 index 00000000..921c45b1 --- /dev/null +++ b/src/main/services/hosters/index.ts @@ -0,0 +1 @@ +export * from "./gofile"; diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index 5365bd9e..120d27ac 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -64,6 +64,14 @@ export class HydraApi { } } + static handleSignOut() { + this.userAuth = { + authToken: "", + refreshToken: "", + expirationTimestamp: 0, + }; + } + static async setupApi() { this.instance = axios.create({ baseURL: import.meta.env.MAIN_VITE_API_URL, diff --git a/src/main/services/real-debrid.ts b/src/main/services/real-debrid.ts index 2e0debe6..26ba4c79 100644 --- a/src/main/services/real-debrid.ts +++ b/src/main/services/real-debrid.ts @@ -46,7 +46,7 @@ export class RealDebridClient { static async selectAllFiles(id: string) { const searchParams = new URLSearchParams({ files: "all" }); - await this.instance.post( + return this.instance.post( `/torrents/selectFiles/${id}`, searchParams.toString() ); diff --git a/src/renderer/src/app.css.ts b/src/renderer/src/app.css.ts index a5f9394b..c829021a 100644 --- a/src/renderer/src/app.css.ts +++ b/src/renderer/src/app.css.ts @@ -26,7 +26,7 @@ globalStyle("html, body, #root, main", { globalStyle("body", { overflow: "hidden", userSelect: "none", - fontFamily: "'Fira Mono', monospace", + fontFamily: "Noto Sans, sans-serif", fontSize: vars.size.body, background: vars.color.background, color: vars.color.body, diff --git a/src/renderer/src/components/hero/hero.css.ts b/src/renderer/src/components/hero/hero.css.ts index cdb36ee2..eaf0a101 100644 --- a/src/renderer/src/components/hero/hero.css.ts +++ b/src/renderer/src/components/hero/hero.css.ts @@ -45,7 +45,6 @@ export const description = style({ maxWidth: "700px", color: vars.color.muted, textAlign: "left", - fontFamily: "'Fira Sans', sans-serif", lineHeight: "20px", marginTop: `${SPACING_UNIT * 2}px`, }); diff --git a/src/renderer/src/components/modal/modal.css.ts b/src/renderer/src/components/modal/modal.css.ts index 45154015..d9d14fda 100644 --- a/src/renderer/src/components/modal/modal.css.ts +++ b/src/renderer/src/components/modal/modal.css.ts @@ -24,6 +24,7 @@ export const modal = recipe({ animation: `${scaleFadeIn} 0.2s cubic-bezier(0.33, 1, 0.68, 1) 0s 1 normal none running`, backgroundColor: vars.color.background, borderRadius: "4px", + minWidth: "400px", maxWidth: "600px", color: vars.color.body, maxHeight: "100%", diff --git a/src/renderer/src/constants.ts b/src/renderer/src/constants.ts index 6186bb85..7025df2a 100644 --- a/src/renderer/src/constants.ts +++ b/src/renderer/src/constants.ts @@ -5,4 +5,6 @@ export const VERSION_CODENAME = "Leviticus"; export const DOWNLOADER_NAME = { [Downloader.RealDebrid]: "Real-Debrid", [Downloader.Torrent]: "Torrent", + [Downloader.Gofile]: "Gofile", + [Downloader.PixelDrain]: "PixelDrain", }; diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index f87d66bf..b88348f0 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -8,12 +8,10 @@ import { HashRouter, Route, Routes } from "react-router-dom"; import * as Sentry from "@sentry/electron/renderer"; -import "@fontsource/fira-mono/400.css"; -import "@fontsource/fira-mono/500.css"; -import "@fontsource/fira-mono/700.css"; -import "@fontsource/fira-sans/400.css"; -import "@fontsource/fira-sans/500.css"; -import "@fontsource/fira-sans/700.css"; +import "@fontsource/noto-sans/400.css"; +import "@fontsource/noto-sans/500.css"; +import "@fontsource/noto-sans/700.css"; + import "react-loading-skeleton/dist/skeleton.css"; import { App } from "./app"; diff --git a/src/renderer/src/pages/downloads/downloads.tsx b/src/renderer/src/pages/downloads/downloads.tsx index 531bc526..5a9c228c 100644 --- a/src/renderer/src/pages/downloads/downloads.tsx +++ b/src/renderer/src/pages/downloads/downloads.tsx @@ -132,9 +132,7 @@ export function Downloads() {

{t("no_downloads_title")}

-

- {t("no_downloads_description")} -

+

{t("no_downloads_description")}

)} diff --git a/src/renderer/src/pages/game-details/description-header/description-header.tsx b/src/renderer/src/pages/game-details/description-header/description-header.tsx index e4272534..cd73c52a 100644 --- a/src/renderer/src/pages/game-details/description-header/description-header.tsx +++ b/src/renderer/src/pages/game-details/description-header/description-header.tsx @@ -19,7 +19,10 @@ export function DescriptionHeader() { date: shopDetails?.release_date.date, })}

-

{t("publisher", { publisher: shopDetails.publishers[0] })}

+ + {Array.isArray(shopDetails.publishers) && ( +

{t("publisher", { publisher: shopDetails.publishers[0] })}

+ )} ); diff --git a/src/renderer/src/pages/game-details/game-details.css.ts b/src/renderer/src/pages/game-details/game-details.css.ts index f0bbfd2e..3dc0ec94 100644 --- a/src/renderer/src/pages/game-details/game-details.css.ts +++ b/src/renderer/src/pages/game-details/game-details.css.ts @@ -101,7 +101,6 @@ export const descriptionContent = style({ export const description = style({ userSelect: "text", lineHeight: "22px", - fontFamily: "'Fira Sans', sans-serif", fontSize: "16px", padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 2}px`, "@media": { 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 e10d55a5..c379c1c3 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 @@ -9,6 +9,7 @@ export const panel = recipe({ height: "72px", minHeight: "72px", padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`, + backgroundColor: vars.color.darkBackground, display: "flex", alignItems: "center", justifyContent: "space-between", diff --git a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx index ef4ba040..dff73ea0 100644 --- a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx @@ -1,11 +1,11 @@ -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { DiskSpace } from "check-disk-space"; import * as styles from "./download-settings-modal.css"; import { Button, Link, Modal, TextField } from "@renderer/components"; import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react"; -import { Downloader, formatBytes } from "@shared"; +import { Downloader, formatBytes, getDownloadersForUri } from "@shared"; import type { GameRepack } from "@types"; import { SPACING_UNIT } from "@renderer/theme.css"; @@ -23,8 +23,6 @@ export interface DownloadSettingsModalProps { repack: GameRepack | null; } -const downloaders = [Downloader.Torrent, Downloader.RealDebrid]; - export function DownloadSettingsModal({ visible, onClose, @@ -36,9 +34,8 @@ export function DownloadSettingsModal({ const [diskFreeSpace, setDiskFreeSpace] = useState(null); const [selectedPath, setSelectedPath] = useState(""); const [downloadStarting, setDownloadStarting] = useState(false); - const [selectedDownloader, setSelectedDownloader] = useState( - Downloader.Torrent - ); + const [selectedDownloader, setSelectedDownloader] = + useState(null); const userPreferences = useAppSelector( (state) => state.userPreferences.value @@ -50,6 +47,10 @@ export function DownloadSettingsModal({ } }, [visible, selectedPath]); + const downloaders = useMemo(() => { + return getDownloadersForUri(repack?.magnet ?? ""); + }, [repack?.magnet]); + useEffect(() => { if (userPreferences?.downloadsPath) { setSelectedPath(userPreferences.downloadsPath); @@ -59,9 +60,27 @@ export function DownloadSettingsModal({ .then((defaultDownloadsPath) => setSelectedPath(defaultDownloadsPath)); } - if (userPreferences?.realDebridApiToken) - setSelectedDownloader(Downloader.RealDebrid); - }, [userPreferences?.downloadsPath, userPreferences?.realDebridApiToken]); + const filteredDownloaders = downloaders.filter((downloader) => { + if (downloader === Downloader.RealDebrid) + return userPreferences?.realDebridApiToken; + return true; + }); + + /* Gives preference to Real Debrid */ + const selectedDownloader = filteredDownloaders.includes( + Downloader.RealDebrid + ) + ? Downloader.RealDebrid + : filteredDownloaders[0]; + + setSelectedDownloader( + selectedDownloader === undefined ? null : selectedDownloader + ); + }, [ + userPreferences?.downloadsPath, + downloaders, + userPreferences?.realDebridApiToken, + ]); const getDiskFreeSpace = (path: string) => { window.electron.getDiskFreeSpace(path).then((result) => { @@ -85,7 +104,7 @@ export function DownloadSettingsModal({ if (repack) { setDownloadStarting(true); - startDownload(repack, selectedDownloader, selectedPath).finally(() => { + startDownload(repack, selectedDownloader!, selectedPath).finally(() => { setDownloadStarting(false); onClose(); }); @@ -167,7 +186,10 @@ export function DownloadSettingsModal({

- diff --git a/src/renderer/src/pages/game-details/modals/game-options-modal.css.ts b/src/renderer/src/pages/game-details/modals/game-options-modal.css.ts index 8bf0ae7f..f844a686 100644 --- a/src/renderer/src/pages/game-details/modals/game-options-modal.css.ts +++ b/src/renderer/src/pages/game-details/modals/game-options-modal.css.ts @@ -15,7 +15,6 @@ export const gameOptionHeader = style({ }); export const gameOptionHeaderDescription = style({ - fontFamily: "'Fira Sans', sans-serif", fontWeight: "400", }); 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 d2d8f5d9..f9d351af 100644 --- a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx @@ -44,8 +44,10 @@ export function RepacksModal({ }, [repacks]); const getInfoHash = useCallback(async () => { - const torrent = await parseTorrent(game?.uri ?? ""); - if (torrent.infoHash) setInfoHash(torrent.infoHash); + if (game?.uri?.startsWith("magnet:")) { + const torrent = await parseTorrent(game?.uri ?? ""); + if (torrent.infoHash) setInfoHash(torrent.infoHash); + } }, [game]); useEffect(() => { diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.css.ts b/src/renderer/src/pages/game-details/sidebar/sidebar.css.ts index e6d8b60a..734e6eb3 100644 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.css.ts +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.css.ts @@ -46,7 +46,6 @@ export const requirementButton = style({ export const requirementsDetails = style({ padding: `${SPACING_UNIT * 2}px`, lineHeight: "22px", - fontFamily: "'Fira Sans', sans-serif", fontSize: "16px", }); diff --git a/src/renderer/src/pages/settings/settings-download-sources.tsx b/src/renderer/src/pages/settings/settings-download-sources.tsx index 8214117a..64d653b0 100644 --- a/src/renderer/src/pages/settings/settings-download-sources.tsx +++ b/src/renderer/src/pages/settings/settings-download-sources.tsx @@ -82,9 +82,7 @@ export function SettingsDownloadSources() { onAddDownloadSource={handleAddDownloadSource} /> -

- {t("download_sources_description")} -

+

{t("download_sources_description")}

{t("no_recent_activity_title")}

- {isMe && ( -

- {t("no_recent_activity_description")} -

- )} + {isMe &&

{t("no_recent_activity_description")}

} ) : (
-

{t("sign_out_modal_text")}

+

{t("sign_out_modal_text")}