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/locales/en/translation.json b/src/locales/en/translation.json
index b24509d3..e2726b79 100644
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -174,12 +174,9 @@
"validate_download_source": "Validate",
"remove_download_source": "Remove",
"add_download_source": "Add source",
- "download_count_zero": "No downloads in list",
- "download_count_one": "{{countFormatted}} download in list",
- "download_count_other": "{{countFormatted}} downloads in list",
- "download_options_zero": "No download available",
- "download_options_one": "{{countFormatted}} download available",
- "download_options_other": "{{countFormatted}} downloads available",
+ "download_count_zero": "No download options",
+ "download_count_one": "{{countFormatted}} download option",
+ "download_count_other": "{{countFormatted}} download options",
"download_source_url": "Download source URL",
"add_download_source_description": "Insert the URL containing the .json file",
"download_source_up_to_date": "Up-to-date",
@@ -261,6 +258,18 @@
"undo_friendship": "Undo friendship",
"request_accepted": "Request accepted",
"user_blocked_successfully": "User blocked successfully",
- "user_block_modal_text": "This will block {{displayName}}"
+ "user_block_modal_text": "This will block {{displayName}}",
+ "settings": "Settings",
+ "public": "Public",
+ "private": "Private",
+ "friends_only": "Friends only",
+ "privacy": "Privacy",
+ "blocked_users": "Blocked users",
+ "unblock": "Unblock",
+ "no_friends_added": "You still don't have added friends",
+ "pending": "Pending",
+ "no_pending_invites": "You have no pending invites",
+ "no_blocked_users": "You have no blocked users",
+ "friend_code_copied": "Friend code copied"
}
}
diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json
index fcb2b099..0800b0c9 100644
--- a/src/locales/es/translation.json
+++ b/src/locales/es/translation.json
@@ -250,6 +250,17 @@
"friend_request_sent": "Solicitud de amistad enviada",
"friends": "Amigos",
"friends_list": "Lista de amigos",
- "user_not_found": "Usuario no encontrado"
+ "user_not_found": "Usuario no encontrado",
+ "block_user": "Bloquear usuario",
+ "add_friend": "Añadir amigo",
+ "request_sent": "Solicitud enviada",
+ "request_received": "Solicitud recibida",
+ "accept_request": "Aceptar solicitud",
+ "ignore_request": "Ignorar solicitud",
+ "cancel_request": "Cancelar solicitud",
+ "undo_friendship": "Eliminar amistad",
+ "request_accepted": "Solicitud aceptada",
+ "user_blocked_successfully": "Usuario bloqueado exitosamente",
+ "user_block_modal_text": "Esto va a bloquear a {{displayName}}"
}
}
diff --git a/src/locales/pt/translation.json b/src/locales/pt/translation.json
index ef94c31f..36d38c96 100644
--- a/src/locales/pt/translation.json
+++ b/src/locales/pt/translation.json
@@ -261,6 +261,18 @@
"undo_friendship": "Desfazer amizade",
"request_accepted": "Pedido de amizade aceito",
"user_blocked_successfully": "Usuário bloqueado com sucesso",
- "user_block_modal_text": "Bloquear {{displayName}}"
+ "user_block_modal_text": "Bloquear {{displayName}}",
+ "settings": "Configurações",
+ "privacy": "Privacidade",
+ "private": "Privado",
+ "friends_only": "Apenas amigos",
+ "public": "Público",
+ "blocked_users": "Usuários bloqueados",
+ "unblock": "Desbloquear",
+ "no_friends_added": "Você ainda não possui amigos adicionados",
+ "pending": "Pendentes",
+ "no_pending_invites": "Você não possui convites de amizade pendentes",
+ "no_blocked_users": "Você não tem nenhum usuário bloqueado",
+ "friend_code_copied": "Código de amigo copiado"
}
}
diff --git a/src/main/data-source.ts b/src/main/data-source.ts
index b47ce2c0..a88a8883 100644
--- a/src/main/data-source.ts
+++ b/src/main/data-source.ts
@@ -6,12 +6,12 @@ import {
GameShopCache,
Repack,
UserPreferences,
+ UserAuth,
} from "@main/entity";
import type { BetterSqlite3ConnectionOptions } from "typeorm/driver/better-sqlite3/BetterSqlite3ConnectionOptions";
import { databasePath } from "./constants";
import migrations from "./migrations";
-import { UserAuth } from "./entity/user-auth";
export const createDataSource = (
options: Partial
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/entity/repack.entity.ts b/src/main/entity/repack.entity.ts
index 1d5259fd..ff3f16cb 100644
--- a/src/main/entity/repack.entity.ts
+++ b/src/main/entity/repack.entity.ts
@@ -16,11 +16,14 @@ export class Repack {
@Column("text", { unique: true })
title: string;
+ /**
+ * @deprecated Use uris instead
+ */
@Column("text", { unique: true })
magnet: string;
/**
- * @deprecated
+ * @deprecated Direct scraping capability has been removed
*/
@Column("int", { nullable: true })
page: number;
@@ -37,6 +40,9 @@ export class Repack {
@ManyToOne(() => DownloadSource, { nullable: true, onDelete: "CASCADE" })
downloadSource: DownloadSource;
+ @Column("text", { default: "[]" })
+ uris: string;
+
@CreateDateColumn()
createdAt: Date;
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/events/download-sources/get-download-sources.ts b/src/main/events/download-sources/get-download-sources.ts
index 8f24caad..b8565645 100644
--- a/src/main/events/download-sources/get-download-sources.ts
+++ b/src/main/events/download-sources/get-download-sources.ts
@@ -1,16 +1,11 @@
import { downloadSourceRepository } from "@main/repository";
import { registerEvent } from "../register-event";
-const getDownloadSources = async (_event: Electron.IpcMainInvokeEvent) => {
- return downloadSourceRepository
- .createQueryBuilder("downloadSource")
- .leftJoin("downloadSource.repacks", "repacks")
- .orderBy("downloadSource.createdAt", "DESC")
- .loadRelationCountAndMap(
- "downloadSource.repackCount",
- "downloadSource.repacks"
- )
- .getMany();
-};
+const getDownloadSources = async (_event: Electron.IpcMainInvokeEvent) =>
+ downloadSourceRepository.find({
+ order: {
+ createdAt: "DESC",
+ },
+ });
registerEvent("getDownloadSources", getDownloadSources);
diff --git a/src/main/events/index.ts b/src/main/events/index.ts
index 57daf51c..3963e4b0 100644
--- a/src/main/events/index.ts
+++ b/src/main/events/index.ts
@@ -43,6 +43,7 @@ import "./auth/sign-out";
import "./auth/open-auth-window";
import "./auth/get-session-hash";
import "./user/get-user";
+import "./user/get-user-blocks";
import "./user/block-user";
import "./user/unblock-user";
import "./user/get-user-friends";
@@ -52,11 +53,9 @@ import "./profile/undo-friendship";
import "./profile/update-friend-request";
import "./profile/update-profile";
import "./profile/send-friend-request";
+import { isPortableVersion } from "@main/helpers";
ipcMain.handle("ping", () => "pong");
ipcMain.handle("getVersion", () => app.getVersion());
-ipcMain.handle(
- "isPortableVersion",
- () => process.env.PORTABLE_EXECUTABLE_FILE != null
-);
+ipcMain.handle("isPortableVersion", () => isPortableVersion());
ipcMain.handle("getDefaultDownloadsPath", () => defaultDownloadsPath);
diff --git a/src/main/events/profile/update-profile.ts b/src/main/events/profile/update-profile.ts
index 8620eaa1..50d2ab66 100644
--- a/src/main/events/profile/update-profile.ts
+++ b/src/main/events/profile/update-profile.ts
@@ -4,33 +4,22 @@ import axios from "axios";
import fs from "node:fs";
import path from "node:path";
import { fileTypeFromFile } from "file-type";
-import { UserProfile } from "@types";
+import { UpdateProfileProps, UserProfile } from "@types";
-const patchUserProfile = async (
- displayName: string,
- profileImageUrl?: string
-) => {
- if (profileImageUrl) {
- return HydraApi.patch("/profile", {
- displayName,
- profileImageUrl,
- });
- } else {
- return HydraApi.patch("/profile", {
- displayName,
- });
- }
+const patchUserProfile = async (updateProfile: UpdateProfileProps) => {
+ return HydraApi.patch("/profile", updateProfile);
};
const updateProfile = async (
_event: Electron.IpcMainInvokeEvent,
- displayName: string,
- newProfileImagePath: string | null
+ updateProfile: UpdateProfileProps
): Promise => {
- if (!newProfileImagePath) {
- return patchUserProfile(displayName);
+ if (!updateProfile.profileImageUrl) {
+ return patchUserProfile(updateProfile);
}
+ const newProfileImagePath = updateProfile.profileImageUrl;
+
const stats = fs.statSync(newProfileImagePath);
const fileBuffer = fs.readFileSync(newProfileImagePath);
const fileSizeInBytes = stats.size;
@@ -53,7 +42,7 @@ const updateProfile = async (
})
.catch(() => undefined);
- return patchUserProfile(displayName, profileImageUrl);
+ return patchUserProfile({ ...updateProfile, profileImageUrl });
};
registerEvent("updateProfile", updateProfile);
diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts
index cea41596..f4db999f 100644
--- a/src/main/events/torrenting/start-game-download.ts
+++ b/src/main/events/torrenting/start-game-download.ts
@@ -18,7 +18,8 @@ const startGameDownload = async (
_event: Electron.IpcMainInvokeEvent,
payload: StartGameDownloadPayload
) => {
- const { repackId, objectID, title, shop, downloadPath, downloader } = payload;
+ const { repackId, objectID, title, shop, downloadPath, downloader, uri } =
+ payload;
const [game, repack] = await Promise.all([
gameRepository.findOne({
@@ -54,7 +55,7 @@ const startGameDownload = async (
bytesDownloaded: 0,
downloadPath,
downloader,
- uri: repack.magnet,
+ uri,
isDeleted: false,
}
);
@@ -76,7 +77,7 @@ const startGameDownload = async (
shop,
status: "active",
downloadPath,
- uri: repack.magnet,
+ uri,
})
.then((result) => {
if (iconUrl) {
@@ -100,6 +101,7 @@ const startGameDownload = async (
await downloadQueueRepository.delete({ game: { id: updatedGame!.id } });
await downloadQueueRepository.insert({ game: { id: updatedGame!.id } });
+ await DownloadManager.cancelDownload(updatedGame!.id);
await DownloadManager.startDownload(updatedGame!);
};
diff --git a/src/main/events/user/get-user-blocks.ts b/src/main/events/user/get-user-blocks.ts
new file mode 100644
index 00000000..65bb3eb4
--- /dev/null
+++ b/src/main/events/user/get-user-blocks.ts
@@ -0,0 +1,13 @@
+import { registerEvent } from "../register-event";
+import { HydraApi } from "@main/services";
+import { UserBlocks } from "@types";
+
+export const getUserBlocks = async (
+ _event: Electron.IpcMainInvokeEvent,
+ take: number,
+ skip: number
+): Promise => {
+ return HydraApi.get(`/profile/blocks`, { take, skip });
+};
+
+registerEvent("getUserBlocks", getUserBlocks);
diff --git a/src/main/helpers/download-source.ts b/src/main/helpers/download-source.ts
index 012a4d24..c216212a 100644
--- a/src/main/helpers/download-source.ts
+++ b/src/main/helpers/download-source.ts
@@ -17,7 +17,8 @@ export const insertDownloadsFromSource = async (
const repacks: QueryDeepPartialEntity[] = downloads.map(
(download) => ({
title: download.title,
- magnet: download.uris[0],
+ uris: JSON.stringify(download.uris),
+ magnet: download.uris[0]!,
fileSize: download.fileSize,
repacker: downloadSource.name,
uploadDate: download.uploadDate,
diff --git a/src/main/helpers/index.ts b/src/main/helpers/index.ts
index 902b927d..b0ff391f 100644
--- a/src/main/helpers/index.ts
+++ b/src/main/helpers/index.ts
@@ -1,4 +1,5 @@
import axios from "axios";
+import { JSDOM } from "jsdom";
import UserAgent from "user-agents";
export const getSteamAppAsset = (
@@ -48,13 +49,19 @@ export const sleep = (ms: number) =>
export const requestWebPage = async (url: string) => {
const userAgent = new UserAgent();
- return axios
+ const data = await axios
.get(url, {
headers: {
"User-Agent": userAgent.toString(),
},
})
.then((response) => response.data);
+
+ const { window } = new JSDOM(data);
+ return window.document;
};
+export const isPortableVersion = () =>
+ process.env.PORTABLE_EXECUTABLE_FILE != null;
+
export * from "./download-source";
diff --git a/src/main/index.ts b/src/main/index.ts
index 9ff74bf6..e288302b 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -20,8 +20,6 @@ autoUpdater.setFeedURL({
autoUpdater.logger = logger;
-logger.log("Init Hydra");
-
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) app.quit();
@@ -123,7 +121,6 @@ app.on("window-all-closed", () => {
app.on("before-quit", () => {
/* Disconnects libtorrent */
PythonInstance.kill();
- logger.log("Quit Hydra");
});
app.on("activate", () => {
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..d4733a32 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, QiwiApi } 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,40 @@ export class DownloadManager {
}
static async startDownload(game: Game) {
- if (game.downloader === Downloader.RealDebrid) {
- RealDebridDownloader.startDownload(game);
- this.currentDownloader = Downloader.RealDebrid;
- } else {
- PythonInstance.startDownload(game);
- this.currentDownloader = Downloader.Torrent;
+ switch (game.downloader) {
+ case 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}`,
+ });
+ break;
+ }
+ case Downloader.PixelDrain: {
+ const id = game!.uri!.split("/").pop();
+
+ await GenericHttpDownloader.startDownload(
+ game,
+ `https://pixeldrain.com/api/file/${id}?download`
+ );
+ break;
+ }
+ case Downloader.Qiwi: {
+ const downloadUrl = await QiwiApi.getDownloadUrl(game.uri!);
+
+ await GenericHttpDownloader.startDownload(game, downloadUrl);
+ break;
+ }
+ case Downloader.Torrent:
+ PythonInstance.startDownload(game);
+ break;
+ case 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..055c8561
--- /dev/null
+++ b/src/main/services/download/generic-http-downloader.ts
@@ -0,0 +1,109 @@
+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 {
+ public static downloads = new Map();
+ public static downloadingGame: Game | null = null;
+
+ public static async getStatus() {
+ if (this.downloadingGame) {
+ const download = this.downloads.get(this.downloadingGame.id)!;
+ const status = download.getStatus();
+
+ 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 httpDownload = this.downloads.get(this.downloadingGame!.id!);
+
+ if (httpDownload) {
+ await httpDownload.pauseDownload();
+ }
+
+ 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 httpDownload = new HttpDownload(
+ game.downloadPath!,
+ downloadUrl,
+ headers
+ );
+
+ httpDownload.startDownload();
+
+ this.downloads.set(game.id!, httpDownload);
+ }
+
+ static async cancelDownload(gameId: number) {
+ const httpDownload = this.downloads.get(gameId);
+
+ if (httpDownload) {
+ await httpDownload.cancelDownload();
+ this.downloads.delete(gameId);
+ }
+ }
+
+ static async resumeDownload(gameId: number) {
+ const httpDownload = this.downloads.get(gameId);
+
+ if (httpDownload) {
+ await httpDownload.resumeDownload();
+ }
+ }
+}
diff --git a/src/main/services/download/http-download.ts b/src/main/services/download/http-download.ts
index 4553a6cb..4f6c31a9 100644
--- a/src/main/services/download/http-download.ts
+++ b/src/main/services/download/http-download.ts
@@ -1,68 +1,54 @@
-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 { WindowManager } from "../window-manager";
+import path from "node:path";
export class HttpDownload {
- private static connected = false;
- private static aria2c: ChildProcess | null = null;
+ private downloadItem: Electron.DownloadItem;
- private static aria2 = new Aria2({});
+ constructor(
+ private downloadPath: string,
+ private downloadUrl: string,
+ private headers?: 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);
- }
-
- 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);
- }
-
- static async pauseDownload(gid: string) {
- await this.aria2.call("forcePause", gid);
- }
-
- static async resumeDownload(gid: string) {
- await this.aria2.call("unpause", gid);
- }
-
- static async startDownload(downloadPath: string, downloadUrl: string) {
- if (!this.connected) await this.connect();
-
- const options = {
- dir: downloadPath,
+ public getStatus() {
+ return {
+ completedLength: this.downloadItem.getReceivedBytes(),
+ totalLength: this.downloadItem.getTotalBytes(),
+ downloadSpeed: this.downloadItem.getCurrentBytesPerSecond(),
+ folderName: this.downloadItem.getFilename(),
};
+ }
- return this.aria2.call("addUri", [downloadUrl], options);
+ async cancelDownload() {
+ this.downloadItem.cancel();
+ }
+
+ async pauseDownload() {
+ this.downloadItem.pause();
+ }
+
+ async resumeDownload() {
+ this.downloadItem.resume();
+ }
+
+ async startDownload() {
+ return new Promise((resolve) => {
+ const options = this.headers ? { headers: this.headers } : {};
+ WindowManager.mainWindow?.webContents.downloadURL(
+ this.downloadUrl,
+ options
+ );
+
+ WindowManager.mainWindow?.webContents.session.once(
+ "will-download",
+ (_event, item, _webContents) => {
+ this.downloadItem = item;
+
+ item.setSavePath(path.join(this.downloadPath, item.getFilename()));
+
+ resolve(null);
+ }
+ );
+ });
}
}
diff --git a/src/main/services/download/real-debrid-downloader.ts b/src/main/services/download/real-debrid-downloader.ts
index 8ead0067..2818644a 100644
--- a/src/main/services/download/real-debrid-downloader.ts
+++ b/src/main/services/download/real-debrid-downloader.ts
@@ -1,162 +1,72 @@
import { Game } from "@main/entity";
import { RealDebridClient } from "../real-debrid";
-import { gameRepository } from "@main/repository";
-import { calculateETA } from "./helpers";
-import { DownloadProgress } from "@types";
import { HttpDownload } from "./http-download";
+import { GenericHttpDownloader } from "./generic-http-downloader";
-export class RealDebridDownloader {
- private static downloads = new Map();
- private static downloadingGame: Game | null = null;
-
+export class RealDebridDownloader extends GenericHttpDownloader {
private static realDebridTorrentId: string | null = null;
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;
}
- return null;
- }
-
- public static async getStatus() {
- if (this.downloadingGame) {
- const gid = this.downloads.get(this.downloadingGame.id)!;
- const status = await 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",
- }
- );
-
- const result = {
- numPeers: 0,
- numSeeds: 0,
- downloadSpeed: Number(status.downloadSpeed),
- timeRemaining: calculateETA(
- Number(status.totalLength),
- Number(status.completedLength),
- Number(status.downloadSpeed)
- ),
- isDownloadingMetadata: false,
- isCheckingFiles: false,
- progress,
- gameId: this.downloadingGame!.id,
- } as DownloadProgress;
-
- if (progress === 1) {
- this.downloads.delete(this.downloadingGame.id);
- this.realDebridTorrentId = null;
- this.downloadingGame = null;
- }
-
- return result;
- }
- }
-
- if (this.realDebridTorrentId && this.downloadingGame) {
- const torrentInfo = await RealDebridClient.getTorrentInfo(
- this.realDebridTorrentId
+ if (this.downloadingGame?.uri) {
+ const { download } = await RealDebridClient.unrestrictLink(
+ this.downloadingGame?.uri
);
- 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 decodeURIComponent(download);
}
return null;
}
- static async pauseDownload() {
- const gid = this.downloads.get(this.downloadingGame!.id!);
- if (gid) {
- await HttpDownload.pauseDownload(gid);
- }
-
- this.realDebridTorrentId = null;
- this.downloadingGame = null;
- }
-
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();
if (downloadUrl) {
this.realDebridTorrentId = null;
- const gid = await HttpDownload.startDownload(
- game.downloadPath!,
- downloadUrl
- );
+ const httpDownload = new HttpDownload(game.downloadPath!, downloadUrl);
+ httpDownload.startDownload();
- 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);
+ this.downloads.set(game.id!, httpDownload);
}
}
}
diff --git a/src/main/services/hosters/gofile.ts b/src/main/services/hosters/gofile.ts
new file mode 100644
index 00000000..2c23556f
--- /dev/null
+++ b/src/main/services/hosters/gofile.ts
@@ -0,0 +1,63 @@
+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 const WT = "4fd6sg89d7s6";
+
+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: WT,
+ });
+
+ 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..4c5b1803
--- /dev/null
+++ b/src/main/services/hosters/index.ts
@@ -0,0 +1,2 @@
+export * from "./gofile";
+export * from "./qiwi";
diff --git a/src/main/services/hosters/qiwi.ts b/src/main/services/hosters/qiwi.ts
new file mode 100644
index 00000000..e18b011c
--- /dev/null
+++ b/src/main/services/hosters/qiwi.ts
@@ -0,0 +1,15 @@
+import { requestWebPage } from "@main/helpers";
+
+export class QiwiApi {
+ public static async getDownloadUrl(url: string) {
+ const document = await requestWebPage(url);
+ const fileName = document.querySelector("h1")?.textContent;
+
+ const slug = url.split("/").pop();
+ const extension = fileName?.split(".").pop();
+
+ const downloadUrl = `https://spyderrock.com/${slug}.${extension}`;
+
+ return downloadUrl;
+ }
+}
diff --git a/src/main/services/how-long-to-beat.ts b/src/main/services/how-long-to-beat.ts
index 39b938c5..67e96942 100644
--- a/src/main/services/how-long-to-beat.ts
+++ b/src/main/services/how-long-to-beat.ts
@@ -1,5 +1,4 @@
import axios from "axios";
-import { JSDOM } from "jsdom";
import { requestWebPage } from "@main/helpers";
import { HowLongToBeatCategory } from "@types";
import { formatName } from "@shared";
@@ -52,10 +51,7 @@ const parseListItems = ($lis: Element[]) => {
export const getHowLongToBeatGame = async (
id: string
): Promise => {
- const response = await requestWebPage(`https://howlongtobeat.com/game/${id}`);
-
- const { window } = new JSDOM(response);
- const { document } = window;
+ const document = await requestWebPage(`https://howlongtobeat.com/game/${id}`);
const $ul = document.querySelector(".shadow_shadow ul");
if (!$ul) return [];
diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts
index 5365bd9e..6f0e1905 100644
--- a/src/main/services/hydra-api.ts
+++ b/src/main/services/hydra-api.ts
@@ -64,59 +64,67 @@ 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,
});
- this.instance.interceptors.request.use(
- (request) => {
- logger.log(" ---- REQUEST -----");
- logger.log(request.method, request.url, request.params, request.data);
- return request;
- },
- (error) => {
- logger.error("request error", error);
- return Promise.reject(error);
- }
- );
+ // this.instance.interceptors.request.use(
+ // (request) => {
+ // logger.log(" ---- REQUEST -----");
+ // logger.log(request.method, request.url, request.params, request.data);
+ // return request;
+ // },
+ // (error) => {
+ // logger.error("request error", error);
+ // return Promise.reject(error);
+ // }
+ // );
- this.instance.interceptors.response.use(
- (response) => {
- logger.log(" ---- RESPONSE -----");
- logger.log(
- response.status,
- response.config.method,
- response.config.url,
- response.data
- );
- return response;
- },
- (error) => {
- logger.error(" ---- RESPONSE ERROR -----");
+ // this.instance.interceptors.response.use(
+ // (response) => {
+ // logger.log(" ---- RESPONSE -----");
+ // logger.log(
+ // response.status,
+ // response.config.method,
+ // response.config.url,
+ // response.data
+ // );
+ // return response;
+ // },
+ // (error) => {
+ // logger.error(" ---- RESPONSE ERROR -----");
- const { config } = error;
+ // const { config } = error;
- logger.error(
- config.method,
- config.baseURL,
- config.url,
- config.headers,
- config.data
- );
+ // logger.error(
+ // config.method,
+ // config.baseURL,
+ // config.url,
+ // config.headers,
+ // config.data
+ // );
- if (error.response) {
- logger.error("Response", error.response.status, error.response.data);
- } else if (error.request) {
- logger.error("Request", error.request);
- } else {
- logger.error("Error", error.message);
- }
+ // if (error.response) {
+ // logger.error("Response", error.response.status, error.response.data);
+ // } else if (error.request) {
+ // logger.error("Request", error.request);
+ // } else {
+ // logger.error("Error", error.message);
+ // }
- logger.error(" ----- END RESPONSE ERROR -------");
- return Promise.reject(error);
- }
- );
+ // logger.error(" ----- END RESPONSE ERROR -------");
+ // return Promise.reject(error);
+ // }
+ // );
const userAuth = await userAuthRepository.findOne({
where: { id: 1 },
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/main/services/repacks-manager.ts b/src/main/services/repacks-manager.ts
index 02821127..93157d6c 100644
--- a/src/main/services/repacks-manager.ts
+++ b/src/main/services/repacks-manager.ts
@@ -8,11 +8,25 @@ export class RepacksManager {
private static repacksIndex = new flexSearch.Index();
public static async updateRepacks() {
- this.repacks = await repackRepository.find({
- order: {
- createdAt: "DESC",
- },
- });
+ this.repacks = await repackRepository
+ .find({
+ order: {
+ createdAt: "DESC",
+ },
+ })
+ .then((repacks) =>
+ repacks.map((repack) => {
+ const uris: string[] = [];
+ const magnet = repack?.magnet;
+
+ if (magnet) uris.push(magnet);
+
+ return {
+ ...repack,
+ uris: [...uris, ...JSON.parse(repack.uris)],
+ };
+ })
+ );
for (let i = 0; i < this.repacks.length; i++) {
this.repacksIndex.remove(i);
diff --git a/src/preload/index.ts b/src/preload/index.ts
index 3350a340..087d573a 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -10,6 +10,7 @@ import type {
StartGameDownloadPayload,
GameRunning,
FriendRequestAction,
+ UpdateProfileProps,
} from "@types";
contextBridge.exposeInMainWorld("electron", {
@@ -137,8 +138,8 @@ contextBridge.exposeInMainWorld("electron", {
getMe: () => ipcRenderer.invoke("getMe"),
undoFriendship: (userId: string) =>
ipcRenderer.invoke("undoFriendship", userId),
- updateProfile: (displayName: string, newProfileImagePath: string | null) =>
- ipcRenderer.invoke("updateProfile", displayName, newProfileImagePath),
+ updateProfile: (updateProfile: UpdateProfileProps) =>
+ ipcRenderer.invoke("updateProfile", updateProfile),
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
updateFriendRequest: (userId: string, action: FriendRequestAction) =>
ipcRenderer.invoke("updateFriendRequest", userId, action),
@@ -151,6 +152,8 @@ contextBridge.exposeInMainWorld("electron", {
unblockUser: (userId: string) => ipcRenderer.invoke("unblockUser", userId),
getUserFriends: (userId: string, take: number, skip: number) =>
ipcRenderer.invoke("getUserFriends", userId, take, skip),
+ getUserBlocks: (take: number, skip: number) =>
+ ipcRenderer.invoke("getUserBlocks", take, skip),
/* Auth */
signOut: () => ipcRenderer.invoke("signOut"),
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/app.tsx b/src/renderer/src/app.tsx
index 8c6f7604..2b9ac187 100644
--- a/src/renderer/src/app.tsx
+++ b/src/renderer/src/app.tsx
@@ -108,7 +108,7 @@ export function App() {
fetchFriendRequests();
}
});
- }, [fetchUserDetails, updateUserDetails, dispatch]);
+ }, [fetchUserDetails, fetchFriendRequests, updateUserDetails, dispatch]);
const onSignIn = useCallback(() => {
fetchUserDetails().then((response) => {
@@ -118,7 +118,13 @@ export function App() {
showSuccessToast(t("successfully_signed_in"));
}
});
- }, [fetchUserDetails, t, showSuccessToast, updateUserDetails]);
+ }, [
+ fetchUserDetails,
+ fetchFriendRequests,
+ t,
+ showSuccessToast,
+ updateUserDetails,
+ ]);
useEffect(() => {
const unsubscribe = window.electron.onGamesRunning((gamesRunning) => {
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..63368c88 100644
--- a/src/renderer/src/constants.ts
+++ b/src/renderer/src/constants.ts
@@ -5,4 +5,7 @@ export const VERSION_CODENAME = "Leviticus";
export const DOWNLOADER_NAME = {
[Downloader.RealDebrid]: "Real-Debrid",
[Downloader.Torrent]: "Torrent",
+ [Downloader.Gofile]: "Gofile",
+ [Downloader.PixelDrain]: "PixelDrain",
+ [Downloader.Qiwi]: "Qiwi",
};
diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts
index e022cffe..29e4dcbb 100644
--- a/src/renderer/src/declaration.d.ts
+++ b/src/renderer/src/declaration.d.ts
@@ -17,6 +17,7 @@ import type {
FriendRequest,
FriendRequestAction,
UserFriends,
+ UserBlocks,
} from "@types";
import type { DiskSpace } from "check-disk-space";
@@ -135,14 +136,12 @@ declare global {
take: number,
skip: number
) => Promise;
+ getUserBlocks: (take: number, skip: number) => Promise;
/* Profile */
getMe: () => Promise;
undoFriendship: (userId: string) => Promise;
- updateProfile: (
- displayName: string,
- newProfileImagePath: string | null
- ) => Promise;
+ updateProfile: (updateProfile: UpdateProfileProps) => Promise;
getFriendRequests: () => Promise;
updateFriendRequest: (
userId: string,
diff --git a/src/renderer/src/hooks/use-download.ts b/src/renderer/src/hooks/use-download.ts
index f58a8765..07c885cf 100644
--- a/src/renderer/src/hooks/use-download.ts
+++ b/src/renderer/src/hooks/use-download.ts
@@ -22,9 +22,10 @@ export function useDownload() {
);
const dispatch = useAppDispatch();
- const startDownload = (payload: StartGameDownloadPayload) => {
+ const startDownload = async (payload: StartGameDownloadPayload) => {
dispatch(clearDownload());
- window.electron.startGameDownload(payload).then((game) => {
+
+ return window.electron.startGameDownload(payload).then((game) => {
updateLibrary();
return game;
diff --git a/src/renderer/src/hooks/use-user-details.ts b/src/renderer/src/hooks/use-user-details.ts
index 21690e7e..0cf2a381 100644
--- a/src/renderer/src/hooks/use-user-details.ts
+++ b/src/renderer/src/hooks/use-user-details.ts
@@ -8,8 +8,9 @@ import {
setFriendsModalHidden,
} from "@renderer/features";
import { profileBackgroundFromProfileImage } from "@renderer/helpers";
-import { FriendRequestAction, UserDetails } from "@types";
+import { FriendRequestAction, UpdateProfileProps, UserDetails } from "@types";
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
+import { logger } from "@renderer/logger";
export function useUserDetails() {
const dispatch = useAppDispatch();
@@ -43,7 +44,10 @@ export function useUserDetails() {
if (userDetails.profileImageUrl) {
const profileBackground = await profileBackgroundFromProfileImage(
userDetails.profileImageUrl
- );
+ ).catch((err) => {
+ logger.error("profileBackgroundFromProfileImage", err);
+ return `#151515B3`;
+ });
dispatch(setProfileBackground(profileBackground));
window.localStorage.setItem(
@@ -74,12 +78,8 @@ export function useUserDetails() {
}, [clearUserDetails]);
const patchUser = useCallback(
- async (displayName: string, imageProfileUrl: string | null) => {
- const response = await window.electron.updateProfile(
- displayName,
- imageProfileUrl
- );
-
+ async (props: UpdateProfileProps) => {
+ const response = await window.electron.updateProfile(props);
return updateUserDetails(response);
},
[updateUserDetails]
@@ -99,7 +99,7 @@ export function useUserDetails() {
dispatch(setFriendsModalVisible({ initialTab, userId }));
fetchFriendRequests();
},
- [dispatch]
+ [dispatch, fetchFriendRequests]
);
const hideFriendsModal = useCallback(() => {
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/game-details.tsx b/src/renderer/src/pages/game-details/game-details.tsx
index 5f32965a..5ac9673f 100644
--- a/src/renderer/src/pages/game-details/game-details.tsx
+++ b/src/renderer/src/pages/game-details/game-details.tsx
@@ -23,7 +23,7 @@ import {
} from "@renderer/context";
import { useDownload } from "@renderer/hooks";
import { GameOptionsModal, RepacksModal } from "./modals";
-import { Downloader } from "@shared";
+import { Downloader, getDownloadersForUri } from "@shared";
export function GameDetails() {
const [randomGame, setRandomGame] = useState(null);
@@ -70,6 +70,9 @@ export function GameDetails() {
}
};
+ const selectRepackUri = (repack: GameRepack, downloader: Downloader) =>
+ repack.uris.find((uri) => getDownloadersForUri(uri).includes(downloader))!;
+
return (
@@ -96,6 +99,7 @@ export function GameDetails() {
downloader,
shop: shop as GameShop,
downloadPath,
+ uri: selectRepackUri(repack, downloader),
});
await updateGame();
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.css.ts b/src/renderer/src/pages/game-details/modals/download-settings-modal.css.ts
index d5655d94..5450378c 100644
--- a/src/renderer/src/pages/game-details/modals/download-settings-modal.css.ts
+++ b/src/renderer/src/pages/game-details/modals/download-settings-modal.css.ts
@@ -20,13 +20,16 @@ export const hintText = style({
});
export const downloaders = style({
- display: "flex",
+ display: "grid",
gap: `${SPACING_UNIT}px`,
+ gridTemplateColumns: "repeat(2, 1fr)",
});
export const downloaderOption = style({
- flex: "1",
position: "relative",
+ ":only-child": {
+ gridColumn: "1 / -1",
+ },
});
export const downloaderIcon = style({
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..3450af24 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, getDownloadersForUris } 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 getDownloadersForUris(repack?.uris ?? []);
+ }, [repack?.uris]);
+
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({
-