mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 21:44:55 +03:00
Merge branch 'main' into feat/logs-python-process-errors
This commit is contained in:
commit
24689cad5a
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,7 +1,6 @@
|
|||||||
.vscode
|
.vscode
|
||||||
node_modules
|
node_modules
|
||||||
hydra-download-manager/
|
hydra-download-manager/
|
||||||
aria2/
|
|
||||||
fastlist.exe
|
fastlist.exe
|
||||||
__pycache__
|
__pycache__
|
||||||
dist
|
dist
|
||||||
|
@ -3,7 +3,6 @@ productName: Hydra
|
|||||||
directories:
|
directories:
|
||||||
buildResources: build
|
buildResources: build
|
||||||
extraResources:
|
extraResources:
|
||||||
- aria2
|
|
||||||
- hydra-download-manager
|
- hydra-download-manager
|
||||||
- seeds
|
- seeds
|
||||||
- from: node_modules/create-desktop-shortcuts/src/windows.vbs
|
- from: node_modules/create-desktop-shortcuts/src/windows.vbs
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
"start": "electron-vite preview",
|
"start": "electron-vite preview",
|
||||||
"dev": "electron-vite dev",
|
"dev": "electron-vite dev",
|
||||||
"build": "npm run typecheck && electron-vite build",
|
"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:unpack": "npm run build && electron-builder --dir",
|
||||||
"build:win": "electron-vite build && electron-builder --win",
|
"build:win": "electron-vite build && electron-builder --win",
|
||||||
"build:mac": "electron-vite build && electron-builder --mac",
|
"build:mac": "electron-vite build && electron-builder --mac",
|
||||||
@ -34,15 +34,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron-toolkit/preload": "^3.0.0",
|
"@electron-toolkit/preload": "^3.0.0",
|
||||||
"@electron-toolkit/utils": "^3.0.0",
|
"@electron-toolkit/utils": "^3.0.0",
|
||||||
"@fontsource/fira-mono": "^5.0.13",
|
"@fontsource/noto-sans": "^5.0.22",
|
||||||
"@fontsource/fira-sans": "^5.0.20",
|
|
||||||
"@primer/octicons-react": "^19.9.0",
|
"@primer/octicons-react": "^19.9.0",
|
||||||
"@reduxjs/toolkit": "^2.2.3",
|
"@reduxjs/toolkit": "^2.2.3",
|
||||||
"@sentry/electron": "^5.1.0",
|
"@sentry/electron": "^5.1.0",
|
||||||
"@vanilla-extract/css": "^1.14.2",
|
"@vanilla-extract/css": "^1.14.2",
|
||||||
"@vanilla-extract/dynamic": "^2.1.1",
|
"@vanilla-extract/dynamic": "^2.1.1",
|
||||||
"@vanilla-extract/recipes": "^0.5.2",
|
"@vanilla-extract/recipes": "^0.5.2",
|
||||||
"aria2": "^4.1.2",
|
|
||||||
"auto-launch": "^5.0.6",
|
"auto-launch": "^5.0.6",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
"better-sqlite3": "^9.5.0",
|
"better-sqlite3": "^9.5.0",
|
||||||
@ -97,7 +95,7 @@
|
|||||||
"@types/user-agents": "^1.0.4",
|
"@types/user-agents": "^1.0.4",
|
||||||
"@vanilla-extract/vite-plugin": "^4.0.7",
|
"@vanilla-extract/vite-plugin": "^4.0.7",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"electron": "^30.0.9",
|
"electron": "^30.3.0",
|
||||||
"electron-builder": "^24.9.1",
|
"electron-builder": "^24.9.1",
|
||||||
"electron-vite": "^2.0.0",
|
"electron-vite": "^2.0.0",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
|
@ -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();
|
|
@ -174,12 +174,9 @@
|
|||||||
"validate_download_source": "Validate",
|
"validate_download_source": "Validate",
|
||||||
"remove_download_source": "Remove",
|
"remove_download_source": "Remove",
|
||||||
"add_download_source": "Add source",
|
"add_download_source": "Add source",
|
||||||
"download_count_zero": "No downloads in list",
|
"download_count_zero": "No download options",
|
||||||
"download_count_one": "{{countFormatted}} download in list",
|
"download_count_one": "{{countFormatted}} download option",
|
||||||
"download_count_other": "{{countFormatted}} downloads in list",
|
"download_count_other": "{{countFormatted}} download options",
|
||||||
"download_options_zero": "No download available",
|
|
||||||
"download_options_one": "{{countFormatted}} download available",
|
|
||||||
"download_options_other": "{{countFormatted}} downloads available",
|
|
||||||
"download_source_url": "Download source URL",
|
"download_source_url": "Download source URL",
|
||||||
"add_download_source_description": "Insert the URL containing the .json file",
|
"add_download_source_description": "Insert the URL containing the .json file",
|
||||||
"download_source_up_to_date": "Up-to-date",
|
"download_source_up_to_date": "Up-to-date",
|
||||||
@ -261,6 +258,18 @@
|
|||||||
"undo_friendship": "Undo friendship",
|
"undo_friendship": "Undo friendship",
|
||||||
"request_accepted": "Request accepted",
|
"request_accepted": "Request accepted",
|
||||||
"user_blocked_successfully": "User blocked successfully",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -250,6 +250,17 @@
|
|||||||
"friend_request_sent": "Solicitud de amistad enviada",
|
"friend_request_sent": "Solicitud de amistad enviada",
|
||||||
"friends": "Amigos",
|
"friends": "Amigos",
|
||||||
"friends_list": "Lista de 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}}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -261,6 +261,18 @@
|
|||||||
"undo_friendship": "Desfazer amizade",
|
"undo_friendship": "Desfazer amizade",
|
||||||
"request_accepted": "Pedido de amizade aceito",
|
"request_accepted": "Pedido de amizade aceito",
|
||||||
"user_blocked_successfully": "Usuário bloqueado com sucesso",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,12 +6,12 @@ import {
|
|||||||
GameShopCache,
|
GameShopCache,
|
||||||
Repack,
|
Repack,
|
||||||
UserPreferences,
|
UserPreferences,
|
||||||
|
UserAuth,
|
||||||
} from "@main/entity";
|
} from "@main/entity";
|
||||||
import type { BetterSqlite3ConnectionOptions } from "typeorm/driver/better-sqlite3/BetterSqlite3ConnectionOptions";
|
import type { BetterSqlite3ConnectionOptions } from "typeorm/driver/better-sqlite3/BetterSqlite3ConnectionOptions";
|
||||||
|
|
||||||
import { databasePath } from "./constants";
|
import { databasePath } from "./constants";
|
||||||
import migrations from "./migrations";
|
import migrations from "./migrations";
|
||||||
import { UserAuth } from "./entity/user-auth";
|
|
||||||
|
|
||||||
export const createDataSource = (
|
export const createDataSource = (
|
||||||
options: Partial<BetterSqlite3ConnectionOptions>
|
options: Partial<BetterSqlite3ConnectionOptions>
|
||||||
|
80
src/main/declaration.d.ts
vendored
80
src/main/declaration.d.ts
vendored
@ -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<void>;
|
|
||||||
call(
|
|
||||||
method: "addUri",
|
|
||||||
uris: string[],
|
|
||||||
options: { dir: string }
|
|
||||||
): Promise<string>;
|
|
||||||
call(
|
|
||||||
method: "tellStatus",
|
|
||||||
gid: string,
|
|
||||||
keys?: string[]
|
|
||||||
): Promise<StatusResponse>;
|
|
||||||
call(method: "pause", gid: string): Promise<string>;
|
|
||||||
call(method: "forcePause", gid: string): Promise<string>;
|
|
||||||
call(method: "unpause", gid: string): Promise<string>;
|
|
||||||
call(method: "remove", gid: string): Promise<string>;
|
|
||||||
call(method: "forceRemove", gid: string): Promise<string>;
|
|
||||||
call(method: "pauseAll"): Promise<string>;
|
|
||||||
call(method: "forcePauseAll"): Promise<string>;
|
|
||||||
listNotifications: () => [
|
|
||||||
"onDownloadStart",
|
|
||||||
"onDownloadPause",
|
|
||||||
"onDownloadStop",
|
|
||||||
"onDownloadComplete",
|
|
||||||
"onDownloadError",
|
|
||||||
"onBtDownloadComplete",
|
|
||||||
];
|
|
||||||
on: (event: string, callback: (params: any) => void) => void;
|
|
||||||
}
|
|
||||||
}
|
|
@ -16,11 +16,14 @@ export class Repack {
|
|||||||
@Column("text", { unique: true })
|
@Column("text", { unique: true })
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use uris instead
|
||||||
|
*/
|
||||||
@Column("text", { unique: true })
|
@Column("text", { unique: true })
|
||||||
magnet: string;
|
magnet: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated
|
* @deprecated Direct scraping capability has been removed
|
||||||
*/
|
*/
|
||||||
@Column("int", { nullable: true })
|
@Column("int", { nullable: true })
|
||||||
page: number;
|
page: number;
|
||||||
@ -37,6 +40,9 @@ export class Repack {
|
|||||||
@ManyToOne(() => DownloadSource, { nullable: true, onDelete: "CASCADE" })
|
@ManyToOne(() => DownloadSource, { nullable: true, onDelete: "CASCADE" })
|
||||||
downloadSource: DownloadSource;
|
downloadSource: DownloadSource;
|
||||||
|
|
||||||
|
@Column("text", { default: "[]" })
|
||||||
|
uris: string;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
|
@ -26,6 +26,8 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
|
|||||||
/* Disconnects libtorrent */
|
/* Disconnects libtorrent */
|
||||||
PythonInstance.killTorrent();
|
PythonInstance.killTorrent();
|
||||||
|
|
||||||
|
HydraApi.handleSignOut();
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
databaseOperations,
|
databaseOperations,
|
||||||
HydraApi.post("/auth/logout").catch(() => {}),
|
HydraApi.post("/auth/logout").catch(() => {}),
|
||||||
|
@ -1,16 +1,11 @@
|
|||||||
import { downloadSourceRepository } from "@main/repository";
|
import { downloadSourceRepository } from "@main/repository";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
const getDownloadSources = async (_event: Electron.IpcMainInvokeEvent) => {
|
const getDownloadSources = async (_event: Electron.IpcMainInvokeEvent) =>
|
||||||
return downloadSourceRepository
|
downloadSourceRepository.find({
|
||||||
.createQueryBuilder("downloadSource")
|
order: {
|
||||||
.leftJoin("downloadSource.repacks", "repacks")
|
createdAt: "DESC",
|
||||||
.orderBy("downloadSource.createdAt", "DESC")
|
},
|
||||||
.loadRelationCountAndMap(
|
});
|
||||||
"downloadSource.repackCount",
|
|
||||||
"downloadSource.repacks"
|
|
||||||
)
|
|
||||||
.getMany();
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("getDownloadSources", getDownloadSources);
|
registerEvent("getDownloadSources", getDownloadSources);
|
||||||
|
@ -43,6 +43,7 @@ import "./auth/sign-out";
|
|||||||
import "./auth/open-auth-window";
|
import "./auth/open-auth-window";
|
||||||
import "./auth/get-session-hash";
|
import "./auth/get-session-hash";
|
||||||
import "./user/get-user";
|
import "./user/get-user";
|
||||||
|
import "./user/get-user-blocks";
|
||||||
import "./user/block-user";
|
import "./user/block-user";
|
||||||
import "./user/unblock-user";
|
import "./user/unblock-user";
|
||||||
import "./user/get-user-friends";
|
import "./user/get-user-friends";
|
||||||
@ -52,11 +53,9 @@ import "./profile/undo-friendship";
|
|||||||
import "./profile/update-friend-request";
|
import "./profile/update-friend-request";
|
||||||
import "./profile/update-profile";
|
import "./profile/update-profile";
|
||||||
import "./profile/send-friend-request";
|
import "./profile/send-friend-request";
|
||||||
|
import { isPortableVersion } from "@main/helpers";
|
||||||
|
|
||||||
ipcMain.handle("ping", () => "pong");
|
ipcMain.handle("ping", () => "pong");
|
||||||
ipcMain.handle("getVersion", () => app.getVersion());
|
ipcMain.handle("getVersion", () => app.getVersion());
|
||||||
ipcMain.handle(
|
ipcMain.handle("isPortableVersion", () => isPortableVersion());
|
||||||
"isPortableVersion",
|
|
||||||
() => process.env.PORTABLE_EXECUTABLE_FILE != null
|
|
||||||
);
|
|
||||||
ipcMain.handle("getDefaultDownloadsPath", () => defaultDownloadsPath);
|
ipcMain.handle("getDefaultDownloadsPath", () => defaultDownloadsPath);
|
||||||
|
@ -4,33 +4,22 @@ import axios from "axios";
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileTypeFromFile } from "file-type";
|
import { fileTypeFromFile } from "file-type";
|
||||||
import { UserProfile } from "@types";
|
import { UpdateProfileProps, UserProfile } from "@types";
|
||||||
|
|
||||||
const patchUserProfile = async (
|
const patchUserProfile = async (updateProfile: UpdateProfileProps) => {
|
||||||
displayName: string,
|
return HydraApi.patch("/profile", updateProfile);
|
||||||
profileImageUrl?: string
|
|
||||||
) => {
|
|
||||||
if (profileImageUrl) {
|
|
||||||
return HydraApi.patch("/profile", {
|
|
||||||
displayName,
|
|
||||||
profileImageUrl,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return HydraApi.patch("/profile", {
|
|
||||||
displayName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateProfile = async (
|
const updateProfile = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
displayName: string,
|
updateProfile: UpdateProfileProps
|
||||||
newProfileImagePath: string | null
|
|
||||||
): Promise<UserProfile> => {
|
): Promise<UserProfile> => {
|
||||||
if (!newProfileImagePath) {
|
if (!updateProfile.profileImageUrl) {
|
||||||
return patchUserProfile(displayName);
|
return patchUserProfile(updateProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newProfileImagePath = updateProfile.profileImageUrl;
|
||||||
|
|
||||||
const stats = fs.statSync(newProfileImagePath);
|
const stats = fs.statSync(newProfileImagePath);
|
||||||
const fileBuffer = fs.readFileSync(newProfileImagePath);
|
const fileBuffer = fs.readFileSync(newProfileImagePath);
|
||||||
const fileSizeInBytes = stats.size;
|
const fileSizeInBytes = stats.size;
|
||||||
@ -53,7 +42,7 @@ const updateProfile = async (
|
|||||||
})
|
})
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
|
|
||||||
return patchUserProfile(displayName, profileImageUrl);
|
return patchUserProfile({ ...updateProfile, profileImageUrl });
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("updateProfile", updateProfile);
|
registerEvent("updateProfile", updateProfile);
|
||||||
|
@ -18,7 +18,8 @@ const startGameDownload = async (
|
|||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
payload: StartGameDownloadPayload
|
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([
|
const [game, repack] = await Promise.all([
|
||||||
gameRepository.findOne({
|
gameRepository.findOne({
|
||||||
@ -54,7 +55,7 @@ const startGameDownload = async (
|
|||||||
bytesDownloaded: 0,
|
bytesDownloaded: 0,
|
||||||
downloadPath,
|
downloadPath,
|
||||||
downloader,
|
downloader,
|
||||||
uri: repack.magnet,
|
uri,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -76,7 +77,7 @@ const startGameDownload = async (
|
|||||||
shop,
|
shop,
|
||||||
status: "active",
|
status: "active",
|
||||||
downloadPath,
|
downloadPath,
|
||||||
uri: repack.magnet,
|
uri,
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (iconUrl) {
|
if (iconUrl) {
|
||||||
@ -100,6 +101,7 @@ const startGameDownload = async (
|
|||||||
await downloadQueueRepository.delete({ game: { id: updatedGame!.id } });
|
await downloadQueueRepository.delete({ game: { id: updatedGame!.id } });
|
||||||
await downloadQueueRepository.insert({ game: { id: updatedGame!.id } });
|
await downloadQueueRepository.insert({ game: { id: updatedGame!.id } });
|
||||||
|
|
||||||
|
await DownloadManager.cancelDownload(updatedGame!.id);
|
||||||
await DownloadManager.startDownload(updatedGame!);
|
await DownloadManager.startDownload(updatedGame!);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
13
src/main/events/user/get-user-blocks.ts
Normal file
13
src/main/events/user/get-user-blocks.ts
Normal file
@ -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<UserBlocks> => {
|
||||||
|
return HydraApi.get(`/profile/blocks`, { take, skip });
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("getUserBlocks", getUserBlocks);
|
@ -17,7 +17,8 @@ export const insertDownloadsFromSource = async (
|
|||||||
const repacks: QueryDeepPartialEntity<Repack>[] = downloads.map(
|
const repacks: QueryDeepPartialEntity<Repack>[] = downloads.map(
|
||||||
(download) => ({
|
(download) => ({
|
||||||
title: download.title,
|
title: download.title,
|
||||||
magnet: download.uris[0],
|
uris: JSON.stringify(download.uris),
|
||||||
|
magnet: download.uris[0]!,
|
||||||
fileSize: download.fileSize,
|
fileSize: download.fileSize,
|
||||||
repacker: downloadSource.name,
|
repacker: downloadSource.name,
|
||||||
uploadDate: download.uploadDate,
|
uploadDate: download.uploadDate,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { JSDOM } from "jsdom";
|
||||||
import UserAgent from "user-agents";
|
import UserAgent from "user-agents";
|
||||||
|
|
||||||
export const getSteamAppAsset = (
|
export const getSteamAppAsset = (
|
||||||
@ -48,13 +49,19 @@ export const sleep = (ms: number) =>
|
|||||||
export const requestWebPage = async (url: string) => {
|
export const requestWebPage = async (url: string) => {
|
||||||
const userAgent = new UserAgent();
|
const userAgent = new UserAgent();
|
||||||
|
|
||||||
return axios
|
const data = await axios
|
||||||
.get(url, {
|
.get(url, {
|
||||||
headers: {
|
headers: {
|
||||||
"User-Agent": userAgent.toString(),
|
"User-Agent": userAgent.toString(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((response) => response.data);
|
.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";
|
export * from "./download-source";
|
||||||
|
@ -20,8 +20,6 @@ autoUpdater.setFeedURL({
|
|||||||
|
|
||||||
autoUpdater.logger = logger;
|
autoUpdater.logger = logger;
|
||||||
|
|
||||||
logger.log("Init Hydra");
|
|
||||||
|
|
||||||
const gotTheLock = app.requestSingleInstanceLock();
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
if (!gotTheLock) app.quit();
|
if (!gotTheLock) app.quit();
|
||||||
|
|
||||||
@ -123,7 +121,6 @@ app.on("window-all-closed", () => {
|
|||||||
app.on("before-quit", () => {
|
app.on("before-quit", () => {
|
||||||
/* Disconnects libtorrent */
|
/* Disconnects libtorrent */
|
||||||
PythonInstance.kill();
|
PythonInstance.kill();
|
||||||
logger.log("Quit Hydra");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("activate", () => {
|
app.on("activate", () => {
|
||||||
|
@ -22,8 +22,9 @@ const loadState = async (userPreferences: UserPreferences | null) => {
|
|||||||
|
|
||||||
import("./events");
|
import("./events");
|
||||||
|
|
||||||
if (userPreferences?.realDebridApiToken)
|
if (userPreferences?.realDebridApiToken) {
|
||||||
RealDebridClient.authorize(userPreferences?.realDebridApiToken);
|
RealDebridClient.authorize(userPreferences?.realDebridApiToken);
|
||||||
|
}
|
||||||
|
|
||||||
HydraApi.setupApi().then(() => {
|
HydraApi.setupApi().then(() => {
|
||||||
uploadGamesBatch();
|
uploadGamesBatch();
|
||||||
|
@ -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 }
|
|
||||||
);
|
|
||||||
};
|
|
@ -6,6 +6,8 @@ import { downloadQueueRepository, gameRepository } from "@main/repository";
|
|||||||
import { publishDownloadCompleteNotification } from "../notifications";
|
import { publishDownloadCompleteNotification } from "../notifications";
|
||||||
import { RealDebridDownloader } from "./real-debrid-downloader";
|
import { RealDebridDownloader } from "./real-debrid-downloader";
|
||||||
import type { DownloadProgress } from "@types";
|
import type { DownloadProgress } from "@types";
|
||||||
|
import { GofileApi, QiwiApi } from "../hosters";
|
||||||
|
import { GenericHttpDownloader } from "./generic-http-downloader";
|
||||||
|
|
||||||
export class DownloadManager {
|
export class DownloadManager {
|
||||||
private static currentDownloader: Downloader | null = null;
|
private static currentDownloader: Downloader | null = null;
|
||||||
@ -13,10 +15,12 @@ export class DownloadManager {
|
|||||||
public static async watchDownloads() {
|
public static async watchDownloads() {
|
||||||
let status: DownloadProgress | null = null;
|
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();
|
status = await RealDebridDownloader.getStatus();
|
||||||
} else {
|
} else {
|
||||||
status = await PythonInstance.getStatus();
|
status = await GenericHttpDownloader.getStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
@ -62,10 +66,12 @@ export class DownloadManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async pauseDownload() {
|
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();
|
await RealDebridDownloader.pauseDownload();
|
||||||
} else {
|
} else {
|
||||||
await PythonInstance.pauseDownload();
|
await GenericHttpDownloader.pauseDownload();
|
||||||
}
|
}
|
||||||
|
|
||||||
WindowManager.mainWindow?.setProgressBar(-1);
|
WindowManager.mainWindow?.setProgressBar(-1);
|
||||||
@ -73,20 +79,16 @@ export class DownloadManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async resumeDownload(game: Game) {
|
static async resumeDownload(game: Game) {
|
||||||
if (game.downloader === Downloader.RealDebrid) {
|
return this.startDownload(game);
|
||||||
RealDebridDownloader.startDownload(game);
|
|
||||||
this.currentDownloader = Downloader.RealDebrid;
|
|
||||||
} else {
|
|
||||||
PythonInstance.startDownload(game);
|
|
||||||
this.currentDownloader = Downloader.Torrent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async cancelDownload(gameId: number) {
|
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);
|
RealDebridDownloader.cancelDownload(gameId);
|
||||||
} else {
|
} else {
|
||||||
PythonInstance.cancelDownload(gameId);
|
GenericHttpDownloader.cancelDownload(gameId);
|
||||||
}
|
}
|
||||||
|
|
||||||
WindowManager.mainWindow?.setProgressBar(-1);
|
WindowManager.mainWindow?.setProgressBar(-1);
|
||||||
@ -94,12 +96,40 @@ export class DownloadManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async startDownload(game: Game) {
|
static async startDownload(game: Game) {
|
||||||
if (game.downloader === Downloader.RealDebrid) {
|
switch (game.downloader) {
|
||||||
RealDebridDownloader.startDownload(game);
|
case Downloader.Gofile: {
|
||||||
this.currentDownloader = Downloader.RealDebrid;
|
const id = game!.uri!.split("/").pop();
|
||||||
} else {
|
|
||||||
PythonInstance.startDownload(game);
|
const token = await GofileApi.authorize();
|
||||||
this.currentDownloader = Downloader.Torrent;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
109
src/main/services/download/generic-http-downloader.ts
Normal file
109
src/main/services/download/generic-http-downloader.ts
Normal file
@ -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<number, HttpDownload>();
|
||||||
|
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<string, string>
|
||||||
|
) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,68 +1,54 @@
|
|||||||
import type { ChildProcess } from "node:child_process";
|
import { WindowManager } from "../window-manager";
|
||||||
import { logger } from "../logger";
|
import path from "node:path";
|
||||||
import { sleep } from "@main/helpers";
|
|
||||||
import { startAria2 } from "../aria2c";
|
|
||||||
import Aria2 from "aria2";
|
|
||||||
|
|
||||||
export class HttpDownload {
|
export class HttpDownload {
|
||||||
private static connected = false;
|
private downloadItem: Electron.DownloadItem;
|
||||||
private static aria2c: ChildProcess | null = null;
|
|
||||||
|
|
||||||
private static aria2 = new Aria2({});
|
constructor(
|
||||||
|
private downloadPath: string,
|
||||||
|
private downloadUrl: string,
|
||||||
|
private headers?: Record<string, string>
|
||||||
|
) {}
|
||||||
|
|
||||||
private static async connect() {
|
public getStatus() {
|
||||||
this.aria2c = startAria2();
|
return {
|
||||||
|
completedLength: this.downloadItem.getReceivedBytes(),
|
||||||
let retries = 0;
|
totalLength: this.downloadItem.getTotalBytes(),
|
||||||
|
downloadSpeed: this.downloadItem.getCurrentBytesPerSecond(),
|
||||||
while (retries < 4 && !this.connected) {
|
folderName: this.downloadItem.getFilename(),
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,162 +1,72 @@
|
|||||||
import { Game } from "@main/entity";
|
import { Game } from "@main/entity";
|
||||||
import { RealDebridClient } from "../real-debrid";
|
import { RealDebridClient } from "../real-debrid";
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
import { calculateETA } from "./helpers";
|
|
||||||
import { DownloadProgress } from "@types";
|
|
||||||
import { HttpDownload } from "./http-download";
|
import { HttpDownload } from "./http-download";
|
||||||
|
import { GenericHttpDownloader } from "./generic-http-downloader";
|
||||||
|
|
||||||
export class RealDebridDownloader {
|
export class RealDebridDownloader extends GenericHttpDownloader {
|
||||||
private static downloads = new Map<number, string>();
|
|
||||||
private static downloadingGame: Game | null = null;
|
|
||||||
|
|
||||||
private static realDebridTorrentId: string | null = null;
|
private static realDebridTorrentId: string | null = null;
|
||||||
|
|
||||||
private static async getRealDebridDownloadUrl() {
|
private static async getRealDebridDownloadUrl() {
|
||||||
if (this.realDebridTorrentId) {
|
if (this.realDebridTorrentId) {
|
||||||
const torrentInfo = await RealDebridClient.getTorrentInfo(
|
let torrentInfo = await RealDebridClient.getTorrentInfo(
|
||||||
this.realDebridTorrentId
|
this.realDebridTorrentId
|
||||||
);
|
);
|
||||||
|
|
||||||
const { status, links } = torrentInfo;
|
if (torrentInfo.status === "waiting_files_selection") {
|
||||||
|
|
||||||
if (status === "waiting_files_selection") {
|
|
||||||
await RealDebridClient.selectAllFiles(this.realDebridTorrentId);
|
await RealDebridClient.selectAllFiles(this.realDebridTorrentId);
|
||||||
return null;
|
|
||||||
|
torrentInfo = await RealDebridClient.getTorrentInfo(
|
||||||
|
this.realDebridTorrentId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { links, status } = torrentInfo;
|
||||||
|
|
||||||
if (status === "downloaded") {
|
if (status === "downloaded") {
|
||||||
const [link] = links;
|
const [link] = links;
|
||||||
|
|
||||||
const { download } = await RealDebridClient.unrestrictLink(link);
|
const { download } = await RealDebridClient.unrestrictLink(link);
|
||||||
return decodeURIComponent(download);
|
return decodeURIComponent(download);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async getStatus() {
|
if (this.downloadingGame?.uri) {
|
||||||
if (this.downloadingGame) {
|
const { download } = await RealDebridClient.unrestrictLink(
|
||||||
const gid = this.downloads.get(this.downloadingGame.id)!;
|
this.downloadingGame?.uri
|
||||||
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 = {
|
return decodeURIComponent(download);
|
||||||
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
|
|
||||||
);
|
|
||||||
|
|
||||||
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;
|
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) {
|
static async startDownload(game: Game) {
|
||||||
this.downloadingGame = game;
|
|
||||||
|
|
||||||
if (this.downloads.has(game.id)) {
|
if (this.downloads.has(game.id)) {
|
||||||
await this.resumeDownload(game.id!);
|
await this.resumeDownload(game.id!);
|
||||||
|
this.downloadingGame = game;
|
||||||
return;
|
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();
|
const downloadUrl = await this.getRealDebridDownloadUrl();
|
||||||
|
|
||||||
if (downloadUrl) {
|
if (downloadUrl) {
|
||||||
this.realDebridTorrentId = null;
|
this.realDebridTorrentId = null;
|
||||||
|
|
||||||
const gid = await HttpDownload.startDownload(
|
const httpDownload = new HttpDownload(game.downloadPath!, downloadUrl);
|
||||||
game.downloadPath!,
|
httpDownload.startDownload();
|
||||||
downloadUrl
|
|
||||||
);
|
|
||||||
|
|
||||||
this.downloads.set(game.id!, gid);
|
this.downloads.set(game.id!, httpDownload);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
63
src/main/services/hosters/gofile.ts
Normal file
63
src/main/services/hosters/gofile.ts
Normal file
@ -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<string, GofileContentChild>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
2
src/main/services/hosters/index.ts
Normal file
2
src/main/services/hosters/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./gofile";
|
||||||
|
export * from "./qiwi";
|
15
src/main/services/hosters/qiwi.ts
Normal file
15
src/main/services/hosters/qiwi.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,4 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { JSDOM } from "jsdom";
|
|
||||||
import { requestWebPage } from "@main/helpers";
|
import { requestWebPage } from "@main/helpers";
|
||||||
import { HowLongToBeatCategory } from "@types";
|
import { HowLongToBeatCategory } from "@types";
|
||||||
import { formatName } from "@shared";
|
import { formatName } from "@shared";
|
||||||
@ -52,10 +51,7 @@ const parseListItems = ($lis: Element[]) => {
|
|||||||
export const getHowLongToBeatGame = async (
|
export const getHowLongToBeatGame = async (
|
||||||
id: string
|
id: string
|
||||||
): Promise<HowLongToBeatCategory[]> => {
|
): Promise<HowLongToBeatCategory[]> => {
|
||||||
const response = await requestWebPage(`https://howlongtobeat.com/game/${id}`);
|
const document = await requestWebPage(`https://howlongtobeat.com/game/${id}`);
|
||||||
|
|
||||||
const { window } = new JSDOM(response);
|
|
||||||
const { document } = window;
|
|
||||||
|
|
||||||
const $ul = document.querySelector(".shadow_shadow ul");
|
const $ul = document.querySelector(".shadow_shadow ul");
|
||||||
if (!$ul) return [];
|
if (!$ul) return [];
|
||||||
|
@ -64,59 +64,67 @@ export class HydraApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static handleSignOut() {
|
||||||
|
this.userAuth = {
|
||||||
|
authToken: "",
|
||||||
|
refreshToken: "",
|
||||||
|
expirationTimestamp: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
static async setupApi() {
|
static async setupApi() {
|
||||||
this.instance = axios.create({
|
this.instance = axios.create({
|
||||||
baseURL: import.meta.env.MAIN_VITE_API_URL,
|
baseURL: import.meta.env.MAIN_VITE_API_URL,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.instance.interceptors.request.use(
|
// this.instance.interceptors.request.use(
|
||||||
(request) => {
|
// (request) => {
|
||||||
logger.log(" ---- REQUEST -----");
|
// logger.log(" ---- REQUEST -----");
|
||||||
logger.log(request.method, request.url, request.params, request.data);
|
// logger.log(request.method, request.url, request.params, request.data);
|
||||||
return request;
|
// return request;
|
||||||
},
|
// },
|
||||||
(error) => {
|
// (error) => {
|
||||||
logger.error("request error", error);
|
// logger.error("request error", error);
|
||||||
return Promise.reject(error);
|
// return Promise.reject(error);
|
||||||
}
|
// }
|
||||||
);
|
// );
|
||||||
|
|
||||||
this.instance.interceptors.response.use(
|
// this.instance.interceptors.response.use(
|
||||||
(response) => {
|
// (response) => {
|
||||||
logger.log(" ---- RESPONSE -----");
|
// logger.log(" ---- RESPONSE -----");
|
||||||
logger.log(
|
// logger.log(
|
||||||
response.status,
|
// response.status,
|
||||||
response.config.method,
|
// response.config.method,
|
||||||
response.config.url,
|
// response.config.url,
|
||||||
response.data
|
// response.data
|
||||||
);
|
// );
|
||||||
return response;
|
// return response;
|
||||||
},
|
// },
|
||||||
(error) => {
|
// (error) => {
|
||||||
logger.error(" ---- RESPONSE ERROR -----");
|
// logger.error(" ---- RESPONSE ERROR -----");
|
||||||
|
|
||||||
const { config } = error;
|
// const { config } = error;
|
||||||
|
|
||||||
logger.error(
|
// logger.error(
|
||||||
config.method,
|
// config.method,
|
||||||
config.baseURL,
|
// config.baseURL,
|
||||||
config.url,
|
// config.url,
|
||||||
config.headers,
|
// config.headers,
|
||||||
config.data
|
// config.data
|
||||||
);
|
// );
|
||||||
|
|
||||||
if (error.response) {
|
// if (error.response) {
|
||||||
logger.error("Response", error.response.status, error.response.data);
|
// logger.error("Response", error.response.status, error.response.data);
|
||||||
} else if (error.request) {
|
// } else if (error.request) {
|
||||||
logger.error("Request", error.request);
|
// logger.error("Request", error.request);
|
||||||
} else {
|
// } else {
|
||||||
logger.error("Error", error.message);
|
// logger.error("Error", error.message);
|
||||||
}
|
// }
|
||||||
|
|
||||||
logger.error(" ----- END RESPONSE ERROR -------");
|
// logger.error(" ----- END RESPONSE ERROR -------");
|
||||||
return Promise.reject(error);
|
// return Promise.reject(error);
|
||||||
}
|
// }
|
||||||
);
|
// );
|
||||||
|
|
||||||
const userAuth = await userAuthRepository.findOne({
|
const userAuth = await userAuthRepository.findOne({
|
||||||
where: { id: 1 },
|
where: { id: 1 },
|
||||||
|
@ -46,7 +46,7 @@ export class RealDebridClient {
|
|||||||
static async selectAllFiles(id: string) {
|
static async selectAllFiles(id: string) {
|
||||||
const searchParams = new URLSearchParams({ files: "all" });
|
const searchParams = new URLSearchParams({ files: "all" });
|
||||||
|
|
||||||
await this.instance.post(
|
return this.instance.post(
|
||||||
`/torrents/selectFiles/${id}`,
|
`/torrents/selectFiles/${id}`,
|
||||||
searchParams.toString()
|
searchParams.toString()
|
||||||
);
|
);
|
||||||
|
@ -8,11 +8,25 @@ export class RepacksManager {
|
|||||||
private static repacksIndex = new flexSearch.Index();
|
private static repacksIndex = new flexSearch.Index();
|
||||||
|
|
||||||
public static async updateRepacks() {
|
public static async updateRepacks() {
|
||||||
this.repacks = await repackRepository.find({
|
this.repacks = await repackRepository
|
||||||
|
.find({
|
||||||
order: {
|
order: {
|
||||||
createdAt: "DESC",
|
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++) {
|
for (let i = 0; i < this.repacks.length; i++) {
|
||||||
this.repacksIndex.remove(i);
|
this.repacksIndex.remove(i);
|
||||||
|
@ -10,6 +10,7 @@ import type {
|
|||||||
StartGameDownloadPayload,
|
StartGameDownloadPayload,
|
||||||
GameRunning,
|
GameRunning,
|
||||||
FriendRequestAction,
|
FriendRequestAction,
|
||||||
|
UpdateProfileProps,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("electron", {
|
contextBridge.exposeInMainWorld("electron", {
|
||||||
@ -137,8 +138,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
getMe: () => ipcRenderer.invoke("getMe"),
|
getMe: () => ipcRenderer.invoke("getMe"),
|
||||||
undoFriendship: (userId: string) =>
|
undoFriendship: (userId: string) =>
|
||||||
ipcRenderer.invoke("undoFriendship", userId),
|
ipcRenderer.invoke("undoFriendship", userId),
|
||||||
updateProfile: (displayName: string, newProfileImagePath: string | null) =>
|
updateProfile: (updateProfile: UpdateProfileProps) =>
|
||||||
ipcRenderer.invoke("updateProfile", displayName, newProfileImagePath),
|
ipcRenderer.invoke("updateProfile", updateProfile),
|
||||||
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
|
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
|
||||||
updateFriendRequest: (userId: string, action: FriendRequestAction) =>
|
updateFriendRequest: (userId: string, action: FriendRequestAction) =>
|
||||||
ipcRenderer.invoke("updateFriendRequest", userId, action),
|
ipcRenderer.invoke("updateFriendRequest", userId, action),
|
||||||
@ -151,6 +152,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
unblockUser: (userId: string) => ipcRenderer.invoke("unblockUser", userId),
|
unblockUser: (userId: string) => ipcRenderer.invoke("unblockUser", userId),
|
||||||
getUserFriends: (userId: string, take: number, skip: number) =>
|
getUserFriends: (userId: string, take: number, skip: number) =>
|
||||||
ipcRenderer.invoke("getUserFriends", userId, take, skip),
|
ipcRenderer.invoke("getUserFriends", userId, take, skip),
|
||||||
|
getUserBlocks: (take: number, skip: number) =>
|
||||||
|
ipcRenderer.invoke("getUserBlocks", take, skip),
|
||||||
|
|
||||||
/* Auth */
|
/* Auth */
|
||||||
signOut: () => ipcRenderer.invoke("signOut"),
|
signOut: () => ipcRenderer.invoke("signOut"),
|
||||||
|
@ -26,7 +26,7 @@ globalStyle("html, body, #root, main", {
|
|||||||
globalStyle("body", {
|
globalStyle("body", {
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
userSelect: "none",
|
userSelect: "none",
|
||||||
fontFamily: "'Fira Mono', monospace",
|
fontFamily: "Noto Sans, sans-serif",
|
||||||
fontSize: vars.size.body,
|
fontSize: vars.size.body,
|
||||||
background: vars.color.background,
|
background: vars.color.background,
|
||||||
color: vars.color.body,
|
color: vars.color.body,
|
||||||
|
@ -108,7 +108,7 @@ export function App() {
|
|||||||
fetchFriendRequests();
|
fetchFriendRequests();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [fetchUserDetails, updateUserDetails, dispatch]);
|
}, [fetchUserDetails, fetchFriendRequests, updateUserDetails, dispatch]);
|
||||||
|
|
||||||
const onSignIn = useCallback(() => {
|
const onSignIn = useCallback(() => {
|
||||||
fetchUserDetails().then((response) => {
|
fetchUserDetails().then((response) => {
|
||||||
@ -118,7 +118,13 @@ export function App() {
|
|||||||
showSuccessToast(t("successfully_signed_in"));
|
showSuccessToast(t("successfully_signed_in"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [fetchUserDetails, t, showSuccessToast, updateUserDetails]);
|
}, [
|
||||||
|
fetchUserDetails,
|
||||||
|
fetchFriendRequests,
|
||||||
|
t,
|
||||||
|
showSuccessToast,
|
||||||
|
updateUserDetails,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = window.electron.onGamesRunning((gamesRunning) => {
|
const unsubscribe = window.electron.onGamesRunning((gamesRunning) => {
|
||||||
|
@ -45,7 +45,6 @@ export const description = style({
|
|||||||
maxWidth: "700px",
|
maxWidth: "700px",
|
||||||
color: vars.color.muted,
|
color: vars.color.muted,
|
||||||
textAlign: "left",
|
textAlign: "left",
|
||||||
fontFamily: "'Fira Sans', sans-serif",
|
|
||||||
lineHeight: "20px",
|
lineHeight: "20px",
|
||||||
marginTop: `${SPACING_UNIT * 2}px`,
|
marginTop: `${SPACING_UNIT * 2}px`,
|
||||||
});
|
});
|
||||||
|
@ -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`,
|
animation: `${scaleFadeIn} 0.2s cubic-bezier(0.33, 1, 0.68, 1) 0s 1 normal none running`,
|
||||||
backgroundColor: vars.color.background,
|
backgroundColor: vars.color.background,
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
|
minWidth: "400px",
|
||||||
maxWidth: "600px",
|
maxWidth: "600px",
|
||||||
color: vars.color.body,
|
color: vars.color.body,
|
||||||
maxHeight: "100%",
|
maxHeight: "100%",
|
||||||
|
@ -5,4 +5,7 @@ export const VERSION_CODENAME = "Leviticus";
|
|||||||
export const DOWNLOADER_NAME = {
|
export const DOWNLOADER_NAME = {
|
||||||
[Downloader.RealDebrid]: "Real-Debrid",
|
[Downloader.RealDebrid]: "Real-Debrid",
|
||||||
[Downloader.Torrent]: "Torrent",
|
[Downloader.Torrent]: "Torrent",
|
||||||
|
[Downloader.Gofile]: "Gofile",
|
||||||
|
[Downloader.PixelDrain]: "PixelDrain",
|
||||||
|
[Downloader.Qiwi]: "Qiwi",
|
||||||
};
|
};
|
||||||
|
7
src/renderer/src/declaration.d.ts
vendored
7
src/renderer/src/declaration.d.ts
vendored
@ -17,6 +17,7 @@ import type {
|
|||||||
FriendRequest,
|
FriendRequest,
|
||||||
FriendRequestAction,
|
FriendRequestAction,
|
||||||
UserFriends,
|
UserFriends,
|
||||||
|
UserBlocks,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { DiskSpace } from "check-disk-space";
|
import type { DiskSpace } from "check-disk-space";
|
||||||
|
|
||||||
@ -135,14 +136,12 @@ declare global {
|
|||||||
take: number,
|
take: number,
|
||||||
skip: number
|
skip: number
|
||||||
) => Promise<UserFriends>;
|
) => Promise<UserFriends>;
|
||||||
|
getUserBlocks: (take: number, skip: number) => Promise<UserBlocks>;
|
||||||
|
|
||||||
/* Profile */
|
/* Profile */
|
||||||
getMe: () => Promise<UserProfile | null>;
|
getMe: () => Promise<UserProfile | null>;
|
||||||
undoFriendship: (userId: string) => Promise<void>;
|
undoFriendship: (userId: string) => Promise<void>;
|
||||||
updateProfile: (
|
updateProfile: (updateProfile: UpdateProfileProps) => Promise<UserProfile>;
|
||||||
displayName: string,
|
|
||||||
newProfileImagePath: string | null
|
|
||||||
) => Promise<UserProfile>;
|
|
||||||
getFriendRequests: () => Promise<FriendRequest[]>;
|
getFriendRequests: () => Promise<FriendRequest[]>;
|
||||||
updateFriendRequest: (
|
updateFriendRequest: (
|
||||||
userId: string,
|
userId: string,
|
||||||
|
@ -22,9 +22,10 @@ export function useDownload() {
|
|||||||
);
|
);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const startDownload = (payload: StartGameDownloadPayload) => {
|
const startDownload = async (payload: StartGameDownloadPayload) => {
|
||||||
dispatch(clearDownload());
|
dispatch(clearDownload());
|
||||||
window.electron.startGameDownload(payload).then((game) => {
|
|
||||||
|
return window.electron.startGameDownload(payload).then((game) => {
|
||||||
updateLibrary();
|
updateLibrary();
|
||||||
|
|
||||||
return game;
|
return game;
|
||||||
|
@ -8,8 +8,9 @@ import {
|
|||||||
setFriendsModalHidden,
|
setFriendsModalHidden,
|
||||||
} from "@renderer/features";
|
} from "@renderer/features";
|
||||||
import { profileBackgroundFromProfileImage } from "@renderer/helpers";
|
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 { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||||
|
import { logger } from "@renderer/logger";
|
||||||
|
|
||||||
export function useUserDetails() {
|
export function useUserDetails() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@ -43,7 +44,10 @@ export function useUserDetails() {
|
|||||||
if (userDetails.profileImageUrl) {
|
if (userDetails.profileImageUrl) {
|
||||||
const profileBackground = await profileBackgroundFromProfileImage(
|
const profileBackground = await profileBackgroundFromProfileImage(
|
||||||
userDetails.profileImageUrl
|
userDetails.profileImageUrl
|
||||||
);
|
).catch((err) => {
|
||||||
|
logger.error("profileBackgroundFromProfileImage", err);
|
||||||
|
return `#151515B3`;
|
||||||
|
});
|
||||||
dispatch(setProfileBackground(profileBackground));
|
dispatch(setProfileBackground(profileBackground));
|
||||||
|
|
||||||
window.localStorage.setItem(
|
window.localStorage.setItem(
|
||||||
@ -74,12 +78,8 @@ export function useUserDetails() {
|
|||||||
}, [clearUserDetails]);
|
}, [clearUserDetails]);
|
||||||
|
|
||||||
const patchUser = useCallback(
|
const patchUser = useCallback(
|
||||||
async (displayName: string, imageProfileUrl: string | null) => {
|
async (props: UpdateProfileProps) => {
|
||||||
const response = await window.electron.updateProfile(
|
const response = await window.electron.updateProfile(props);
|
||||||
displayName,
|
|
||||||
imageProfileUrl
|
|
||||||
);
|
|
||||||
|
|
||||||
return updateUserDetails(response);
|
return updateUserDetails(response);
|
||||||
},
|
},
|
||||||
[updateUserDetails]
|
[updateUserDetails]
|
||||||
@ -99,7 +99,7 @@ export function useUserDetails() {
|
|||||||
dispatch(setFriendsModalVisible({ initialTab, userId }));
|
dispatch(setFriendsModalVisible({ initialTab, userId }));
|
||||||
fetchFriendRequests();
|
fetchFriendRequests();
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch, fetchFriendRequests]
|
||||||
);
|
);
|
||||||
|
|
||||||
const hideFriendsModal = useCallback(() => {
|
const hideFriendsModal = useCallback(() => {
|
||||||
|
@ -8,12 +8,10 @@ import { HashRouter, Route, Routes } from "react-router-dom";
|
|||||||
|
|
||||||
import * as Sentry from "@sentry/electron/renderer";
|
import * as Sentry from "@sentry/electron/renderer";
|
||||||
|
|
||||||
import "@fontsource/fira-mono/400.css";
|
import "@fontsource/noto-sans/400.css";
|
||||||
import "@fontsource/fira-mono/500.css";
|
import "@fontsource/noto-sans/500.css";
|
||||||
import "@fontsource/fira-mono/700.css";
|
import "@fontsource/noto-sans/700.css";
|
||||||
import "@fontsource/fira-sans/400.css";
|
|
||||||
import "@fontsource/fira-sans/500.css";
|
|
||||||
import "@fontsource/fira-sans/700.css";
|
|
||||||
import "react-loading-skeleton/dist/skeleton.css";
|
import "react-loading-skeleton/dist/skeleton.css";
|
||||||
|
|
||||||
import { App } from "./app";
|
import { App } from "./app";
|
||||||
|
@ -132,9 +132,7 @@ export function Downloads() {
|
|||||||
<ArrowDownIcon size={24} />
|
<ArrowDownIcon size={24} />
|
||||||
</div>
|
</div>
|
||||||
<h2>{t("no_downloads_title")}</h2>
|
<h2>{t("no_downloads_title")}</h2>
|
||||||
<p style={{ fontFamily: "Fira Sans" }}>
|
<p>{t("no_downloads_description")}</p>
|
||||||
{t("no_downloads_description")}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -19,7 +19,10 @@ export function DescriptionHeader() {
|
|||||||
date: shopDetails?.release_date.date,
|
date: shopDetails?.release_date.date,
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{Array.isArray(shopDetails.publishers) && (
|
||||||
<p>{t("publisher", { publisher: shopDetails.publishers[0] })}</p>
|
<p>{t("publisher", { publisher: shopDetails.publishers[0] })}</p>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -101,7 +101,6 @@ export const descriptionContent = style({
|
|||||||
export const description = style({
|
export const description = style({
|
||||||
userSelect: "text",
|
userSelect: "text",
|
||||||
lineHeight: "22px",
|
lineHeight: "22px",
|
||||||
fontFamily: "'Fira Sans', sans-serif",
|
|
||||||
fontSize: "16px",
|
fontSize: "16px",
|
||||||
padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 2}px`,
|
padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 2}px`,
|
||||||
"@media": {
|
"@media": {
|
||||||
|
@ -23,7 +23,7 @@ import {
|
|||||||
} from "@renderer/context";
|
} from "@renderer/context";
|
||||||
import { useDownload } from "@renderer/hooks";
|
import { useDownload } from "@renderer/hooks";
|
||||||
import { GameOptionsModal, RepacksModal } from "./modals";
|
import { GameOptionsModal, RepacksModal } from "./modals";
|
||||||
import { Downloader } from "@shared";
|
import { Downloader, getDownloadersForUri } from "@shared";
|
||||||
|
|
||||||
export function GameDetails() {
|
export function GameDetails() {
|
||||||
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
|
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
|
||||||
@ -70,6 +70,9 @@ export function GameDetails() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectRepackUri = (repack: GameRepack, downloader: Downloader) =>
|
||||||
|
repack.uris.find((uri) => getDownloadersForUri(uri).includes(downloader))!;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GameDetailsContextProvider>
|
<GameDetailsContextProvider>
|
||||||
<GameDetailsContextConsumer>
|
<GameDetailsContextConsumer>
|
||||||
@ -96,6 +99,7 @@ export function GameDetails() {
|
|||||||
downloader,
|
downloader,
|
||||||
shop: shop as GameShop,
|
shop: shop as GameShop,
|
||||||
downloadPath,
|
downloadPath,
|
||||||
|
uri: selectRepackUri(repack, downloader),
|
||||||
});
|
});
|
||||||
|
|
||||||
await updateGame();
|
await updateGame();
|
||||||
|
@ -9,6 +9,7 @@ export const panel = recipe({
|
|||||||
height: "72px",
|
height: "72px",
|
||||||
minHeight: "72px",
|
minHeight: "72px",
|
||||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
|
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
|
||||||
|
backgroundColor: vars.color.darkBackground,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
|
@ -20,13 +20,16 @@ export const hintText = style({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const downloaders = style({
|
export const downloaders = style({
|
||||||
display: "flex",
|
display: "grid",
|
||||||
gap: `${SPACING_UNIT}px`,
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
gridTemplateColumns: "repeat(2, 1fr)",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const downloaderOption = style({
|
export const downloaderOption = style({
|
||||||
flex: "1",
|
|
||||||
position: "relative",
|
position: "relative",
|
||||||
|
":only-child": {
|
||||||
|
gridColumn: "1 / -1",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const downloaderIcon = style({
|
export const downloaderIcon = style({
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { DiskSpace } from "check-disk-space";
|
import { DiskSpace } from "check-disk-space";
|
||||||
import * as styles from "./download-settings-modal.css";
|
import * as styles from "./download-settings-modal.css";
|
||||||
import { Button, Link, Modal, TextField } from "@renderer/components";
|
import { Button, Link, Modal, TextField } from "@renderer/components";
|
||||||
import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react";
|
import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react";
|
||||||
import { Downloader, formatBytes } from "@shared";
|
import { Downloader, formatBytes, getDownloadersForUris } from "@shared";
|
||||||
|
|
||||||
import type { GameRepack } from "@types";
|
import type { GameRepack } from "@types";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
@ -23,8 +23,6 @@ export interface DownloadSettingsModalProps {
|
|||||||
repack: GameRepack | null;
|
repack: GameRepack | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const downloaders = [Downloader.Torrent, Downloader.RealDebrid];
|
|
||||||
|
|
||||||
export function DownloadSettingsModal({
|
export function DownloadSettingsModal({
|
||||||
visible,
|
visible,
|
||||||
onClose,
|
onClose,
|
||||||
@ -36,9 +34,8 @@ export function DownloadSettingsModal({
|
|||||||
const [diskFreeSpace, setDiskFreeSpace] = useState<DiskSpace | null>(null);
|
const [diskFreeSpace, setDiskFreeSpace] = useState<DiskSpace | null>(null);
|
||||||
const [selectedPath, setSelectedPath] = useState("");
|
const [selectedPath, setSelectedPath] = useState("");
|
||||||
const [downloadStarting, setDownloadStarting] = useState(false);
|
const [downloadStarting, setDownloadStarting] = useState(false);
|
||||||
const [selectedDownloader, setSelectedDownloader] = useState(
|
const [selectedDownloader, setSelectedDownloader] =
|
||||||
Downloader.Torrent
|
useState<Downloader | null>(null);
|
||||||
);
|
|
||||||
|
|
||||||
const userPreferences = useAppSelector(
|
const userPreferences = useAppSelector(
|
||||||
(state) => state.userPreferences.value
|
(state) => state.userPreferences.value
|
||||||
@ -50,6 +47,10 @@ export function DownloadSettingsModal({
|
|||||||
}
|
}
|
||||||
}, [visible, selectedPath]);
|
}, [visible, selectedPath]);
|
||||||
|
|
||||||
|
const downloaders = useMemo(() => {
|
||||||
|
return getDownloadersForUris(repack?.uris ?? []);
|
||||||
|
}, [repack?.uris]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userPreferences?.downloadsPath) {
|
if (userPreferences?.downloadsPath) {
|
||||||
setSelectedPath(userPreferences.downloadsPath);
|
setSelectedPath(userPreferences.downloadsPath);
|
||||||
@ -59,9 +60,27 @@ export function DownloadSettingsModal({
|
|||||||
.then((defaultDownloadsPath) => setSelectedPath(defaultDownloadsPath));
|
.then((defaultDownloadsPath) => setSelectedPath(defaultDownloadsPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userPreferences?.realDebridApiToken)
|
const filteredDownloaders = downloaders.filter((downloader) => {
|
||||||
setSelectedDownloader(Downloader.RealDebrid);
|
if (downloader === Downloader.RealDebrid)
|
||||||
}, [userPreferences?.downloadsPath, userPreferences?.realDebridApiToken]);
|
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) => {
|
const getDiskFreeSpace = (path: string) => {
|
||||||
window.electron.getDiskFreeSpace(path).then((result) => {
|
window.electron.getDiskFreeSpace(path).then((result) => {
|
||||||
@ -85,7 +104,7 @@ export function DownloadSettingsModal({
|
|||||||
if (repack) {
|
if (repack) {
|
||||||
setDownloadStarting(true);
|
setDownloadStarting(true);
|
||||||
|
|
||||||
startDownload(repack, selectedDownloader, selectedPath).finally(() => {
|
startDownload(repack, selectedDownloader!, selectedPath).finally(() => {
|
||||||
setDownloadStarting(false);
|
setDownloadStarting(false);
|
||||||
onClose();
|
onClose();
|
||||||
});
|
});
|
||||||
@ -167,7 +186,10 @@ export function DownloadSettingsModal({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button onClick={handleStartClick} disabled={downloadStarting}>
|
<Button
|
||||||
|
onClick={handleStartClick}
|
||||||
|
disabled={downloadStarting || selectedDownloader === null}
|
||||||
|
>
|
||||||
<DownloadIcon />
|
<DownloadIcon />
|
||||||
{t("download_now")}
|
{t("download_now")}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -15,7 +15,6 @@ export const gameOptionHeader = style({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const gameOptionHeaderDescription = style({
|
export const gameOptionHeaderDescription = style({
|
||||||
fontFamily: "'Fira Sans', sans-serif",
|
|
||||||
fontWeight: "400",
|
fontWeight: "400",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -44,8 +44,10 @@ export function RepacksModal({
|
|||||||
}, [repacks]);
|
}, [repacks]);
|
||||||
|
|
||||||
const getInfoHash = useCallback(async () => {
|
const getInfoHash = useCallback(async () => {
|
||||||
|
if (game?.uri?.startsWith("magnet:")) {
|
||||||
const torrent = await parseTorrent(game?.uri ?? "");
|
const torrent = await parseTorrent(game?.uri ?? "");
|
||||||
if (torrent.infoHash) setInfoHash(torrent.infoHash);
|
if (torrent.infoHash) setInfoHash(torrent.infoHash);
|
||||||
|
}
|
||||||
}, [game]);
|
}, [game]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -74,6 +76,13 @@ export function RepacksModal({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkIfLastDownloadedOption = (repack: GameRepack) => {
|
||||||
|
if (infoHash) return repack.uris.some((uri) => uri.includes(infoHash));
|
||||||
|
if (!game?.uri) return false;
|
||||||
|
|
||||||
|
return repack.uris.some((uri) => uri.includes(game?.uri ?? ""));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DownloadSettingsModal
|
<DownloadSettingsModal
|
||||||
@ -95,9 +104,7 @@ export function RepacksModal({
|
|||||||
|
|
||||||
<div className={styles.repacks}>
|
<div className={styles.repacks}>
|
||||||
{filteredRepacks.map((repack) => {
|
{filteredRepacks.map((repack) => {
|
||||||
const isLastDownloadedOption =
|
const isLastDownloadedOption = checkIfLastDownloadedOption(repack);
|
||||||
infoHash !== null &&
|
|
||||||
repack.magnet.toLowerCase().includes(infoHash);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
@ -46,7 +46,6 @@ export const requirementButton = style({
|
|||||||
export const requirementsDetails = style({
|
export const requirementsDetails = style({
|
||||||
padding: `${SPACING_UNIT * 2}px`,
|
padding: `${SPACING_UNIT * 2}px`,
|
||||||
lineHeight: "22px",
|
lineHeight: "22px",
|
||||||
fontFamily: "'Fira Sans', sans-serif",
|
|
||||||
fontSize: "16px",
|
fontSize: "16px",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -42,10 +42,3 @@ export const downloadSourcesHeader = style({
|
|||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const separator = style({
|
|
||||||
height: "100%",
|
|
||||||
width: "1px",
|
|
||||||
backgroundColor: vars.color.border,
|
|
||||||
margin: `${SPACING_UNIT}px 0`,
|
|
||||||
});
|
|
||||||
|
@ -82,9 +82,7 @@ export function SettingsDownloadSources() {
|
|||||||
onAddDownloadSource={handleAddDownloadSource}
|
onAddDownloadSource={handleAddDownloadSource}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p style={{ fontFamily: '"Fira Sans"' }}>
|
<p>{t("download_sources_description")}</p>
|
||||||
{t("download_sources_description")}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className={styles.downloadSourcesHeader}>
|
<div className={styles.downloadSourcesHeader}>
|
||||||
<Button
|
<Button
|
||||||
@ -136,15 +134,6 @@ export function SettingsDownloadSources() {
|
|||||||
downloadSource.downloadCount.toLocaleString(),
|
downloadSource.downloadCount.toLocaleString(),
|
||||||
})}
|
})}
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
<div className={styles.separator} />
|
|
||||||
|
|
||||||
<small>
|
|
||||||
{t("download_options", {
|
|
||||||
count: downloadSource.repackCount,
|
|
||||||
countFormatted: downloadSource.repackCount.toLocaleString(),
|
|
||||||
})}
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -9,6 +9,5 @@ export const form = style({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const description = style({
|
export const description = style({
|
||||||
fontFamily: "'Fira Sans', sans-serif",
|
|
||||||
marginBottom: `${SPACING_UNIT * 2}px`,
|
marginBottom: `${SPACING_UNIT * 2}px`,
|
||||||
});
|
});
|
||||||
|
@ -4,7 +4,6 @@ import {
|
|||||||
XCircleIcon,
|
XCircleIcon,
|
||||||
} from "@primer/octicons-react";
|
} from "@primer/octicons-react";
|
||||||
import * as styles from "./user-friend-modal.css";
|
import * as styles from "./user-friend-modal.css";
|
||||||
import cn from "classnames";
|
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@ -12,21 +11,26 @@ export type UserFriendItemProps = {
|
|||||||
userId: string;
|
userId: string;
|
||||||
profileImageUrl: string | null;
|
profileImageUrl: string | null;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
onClickItem: (userId: string) => void;
|
|
||||||
} & (
|
} & (
|
||||||
| { type: "ACCEPTED"; onClickUndoFriendship: (userId: string) => void }
|
| {
|
||||||
|
type: "ACCEPTED";
|
||||||
|
onClickUndoFriendship: (userId: string) => void;
|
||||||
|
onClickItem: (userId: string) => void;
|
||||||
|
}
|
||||||
|
| { type: "BLOCKED"; onClickUnblock: (userId: string) => void }
|
||||||
| {
|
| {
|
||||||
type: "SENT" | "RECEIVED";
|
type: "SENT" | "RECEIVED";
|
||||||
onClickCancelRequest: (userId: string) => void;
|
onClickCancelRequest: (userId: string) => void;
|
||||||
onClickAcceptRequest: (userId: string) => void;
|
onClickAcceptRequest: (userId: string) => void;
|
||||||
onClickRefuseRequest: (userId: string) => void;
|
onClickRefuseRequest: (userId: string) => void;
|
||||||
|
onClickItem: (userId: string) => void;
|
||||||
}
|
}
|
||||||
| { type: null }
|
| { type: null; onClickItem: (userId: string) => void }
|
||||||
);
|
);
|
||||||
|
|
||||||
export const UserFriendItem = (props: UserFriendItemProps) => {
|
export const UserFriendItem = (props: UserFriendItemProps) => {
|
||||||
const { t } = useTranslation("user_profile");
|
const { t } = useTranslation("user_profile");
|
||||||
const { userId, profileImageUrl, displayName, type, onClickItem } = props;
|
const { userId, profileImageUrl, displayName, type } = props;
|
||||||
|
|
||||||
const getRequestDescription = () => {
|
const getRequestDescription = () => {
|
||||||
if (type === "ACCEPTED" || type === null) return null;
|
if (type === "ACCEPTED" || type === null) return null;
|
||||||
@ -86,15 +90,69 @@ export const UserFriendItem = (props: UserFriendItemProps) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === "BLOCKED") {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={styles.cancelRequestButton}
|
||||||
|
onClick={() => props.onClickUnblock(userId)}
|
||||||
|
title={t("unblock")}
|
||||||
|
>
|
||||||
|
<XCircleIcon size={28} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (type === "BLOCKED") {
|
||||||
return (
|
return (
|
||||||
<div className={cn(styles.friendListContainer, styles.profileContentBox)}>
|
<div className={styles.friendListContainer}>
|
||||||
|
<div className={styles.friendListButton} style={{ cursor: "inherit" }}>
|
||||||
|
<div className={styles.friendAvatarContainer}>
|
||||||
|
{profileImageUrl ? (
|
||||||
|
<img
|
||||||
|
className={styles.profileAvatar}
|
||||||
|
alt={displayName}
|
||||||
|
src={profileImageUrl}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<PersonIcon size={24} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
flex: "1",
|
||||||
|
minWidth: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p className={styles.friendListDisplayName}>{displayName}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
right: "8px",
|
||||||
|
display: "flex",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getRequestActions()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.friendListContainer}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.friendListButton}
|
className={styles.friendListButton}
|
||||||
onClick={() => onClickItem(userId)}
|
onClick={() => props.onClickItem(userId)}
|
||||||
>
|
>
|
||||||
<div className={styles.friendAvatarContainer}>
|
<div className={styles.friendAvatarContainer}>
|
||||||
{profileImageUrl ? (
|
{profileImageUrl ? (
|
||||||
|
@ -40,20 +40,16 @@ export const UserFriendModalAddFriend = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetAndClose = () => {
|
|
||||||
setFriendCode("");
|
|
||||||
closeModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClickRequest = (userId: string) => {
|
const handleClickRequest = (userId: string) => {
|
||||||
resetAndClose();
|
closeModal();
|
||||||
navigate(`/user/${userId}`);
|
navigate(`/user/${userId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClickSeeProfile = () => {
|
const handleClickSeeProfile = () => {
|
||||||
resetAndClose();
|
closeModal();
|
||||||
// TODO: add validation for this input?
|
if (friendCode.length === 8) {
|
||||||
navigate(`/user/${friendCode}`);
|
navigate(`/user/${friendCode}`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancelFriendRequest = (userId: string) => {
|
const handleCancelFriendRequest = (userId: string) => {
|
||||||
@ -122,7 +118,8 @@ export const UserFriendModalAddFriend = ({
|
|||||||
gap: `${SPACING_UNIT * 2}px`,
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h3>Pendentes</h3>
|
<h3>{t("pending")}</h3>
|
||||||
|
{friendRequests.length === 0 && <p>{t("no_pending_invites")}</p>}
|
||||||
{friendRequests.map((request) => {
|
{friendRequests.map((request) => {
|
||||||
return (
|
return (
|
||||||
<UserFriendItem
|
<UserFriendItem
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||||
import { UserFriend } from "@types";
|
import { UserFriend } from "@types";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { UserFriendItem } from "./user-friend-item";
|
import { UserFriendItem } from "./user-friend-item";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useToast, useUserDetails } from "@renderer/hooks";
|
import { useToast, useUserDetails } from "@renderer/hooks";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
|
||||||
|
|
||||||
export interface UserFriendModalListProps {
|
export interface UserFriendModalListProps {
|
||||||
userId: string;
|
userId: string;
|
||||||
@ -22,14 +23,17 @@ export const UserFriendModalList = ({
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [maxPage, setMaxPage] = useState(0);
|
const [maxPage, setMaxPage] = useState(0);
|
||||||
const [friends, setFriends] = useState<UserFriend[]>([]);
|
const [friends, setFriends] = useState<UserFriend[]>([]);
|
||||||
|
const listContainer = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const { userDetails, undoFriendship } = useUserDetails();
|
const { userDetails, undoFriendship } = useUserDetails();
|
||||||
const isMe = userDetails?.id == userId;
|
const isMe = userDetails?.id == userId;
|
||||||
|
|
||||||
const loadNextPage = () => {
|
const loadNextPage = () => {
|
||||||
if (page > maxPage) return;
|
if (page > maxPage) return;
|
||||||
|
setIsLoading(true);
|
||||||
window.electron
|
window.electron
|
||||||
.getUserFriends(userId, pageSize, page * pageSize)
|
.getUserFriends(userId, pageSize, page * pageSize)
|
||||||
.then((newPage) => {
|
.then((newPage) => {
|
||||||
@ -40,9 +44,29 @@ export const UserFriendModalList = ({
|
|||||||
setFriends([...friends, ...newPage.friends]);
|
setFriends([...friends, ...newPage.friends]);
|
||||||
setPage(page + 1);
|
setPage(page + 1);
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {})
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
const scrollTop = listContainer.current?.scrollTop || 0;
|
||||||
|
const scrollHeight = listContainer.current?.scrollHeight || 0;
|
||||||
|
const clientHeight = listContainer.current?.clientHeight || 0;
|
||||||
|
const maxScrollTop = scrollHeight - clientHeight;
|
||||||
|
|
||||||
|
if (scrollTop < maxScrollTop * 0.9 || isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadNextPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
listContainer.current?.addEventListener("scroll", handleScroll);
|
||||||
|
return () =>
|
||||||
|
listContainer.current?.removeEventListener("scroll", handleScroll);
|
||||||
|
}, [isLoading]);
|
||||||
|
|
||||||
const reloadList = () => {
|
const reloadList = () => {
|
||||||
setPage(0);
|
setPage(0);
|
||||||
setMaxPage(0);
|
setMaxPage(0);
|
||||||
@ -70,13 +94,18 @@ export const UserFriendModalList = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||||
<div
|
<div
|
||||||
|
ref={listContainer}
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
maxHeight: "400px",
|
||||||
|
overflowY: "scroll",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{!isLoading && friends.length === 0 && <p>{t("no_friends_added")}</p>}
|
||||||
{friends.map((friend) => {
|
{friends.map((friend) => {
|
||||||
return (
|
return (
|
||||||
<UserFriendItem
|
<UserFriendItem
|
||||||
@ -86,10 +115,21 @@ export const UserFriendModalList = ({
|
|||||||
onClickItem={handleClickFriend}
|
onClickItem={handleClickFriend}
|
||||||
onClickUndoFriendship={handleUndoFriendship}
|
onClickUndoFriendship={handleUndoFriendship}
|
||||||
type={isMe ? "ACCEPTED" : null}
|
type={isMe ? "ACCEPTED" : null}
|
||||||
key={friend.id}
|
key={"modal" + friend.id}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{isLoading && (
|
||||||
|
<Skeleton
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "54px",
|
||||||
|
overflow: "hidden",
|
||||||
|
borderRadius: "4px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</SkeletonTheme>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,17 +1,6 @@
|
|||||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||||
import { style } from "@vanilla-extract/css";
|
import { style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
export const profileContentBox = style({
|
|
||||||
display: "flex",
|
|
||||||
gap: `${SPACING_UNIT * 3}px`,
|
|
||||||
alignItems: "center",
|
|
||||||
borderRadius: "4px",
|
|
||||||
border: `solid 1px ${vars.color.border}`,
|
|
||||||
width: "100%",
|
|
||||||
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.7)",
|
|
||||||
transition: "all ease 0.3s",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const friendAvatarContainer = style({
|
export const friendAvatarContainer = style({
|
||||||
width: "35px",
|
width: "35px",
|
||||||
minWidth: "35px",
|
minWidth: "35px",
|
||||||
@ -42,8 +31,14 @@ export const profileAvatar = style({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const friendListContainer = style({
|
export const friendListContainer = style({
|
||||||
|
display: "flex",
|
||||||
|
gap: `${SPACING_UNIT * 3}px`,
|
||||||
|
alignItems: "center",
|
||||||
|
borderRadius: "4px",
|
||||||
|
border: `solid 1px ${vars.color.border}`,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "54px",
|
height: "54px",
|
||||||
|
minHeight: "54px",
|
||||||
transition: "all ease 0.2s",
|
transition: "all ease 0.2s",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
":hover": {
|
":hover": {
|
||||||
@ -90,3 +85,15 @@ export const cancelRequestButton = style({
|
|||||||
color: vars.color.danger,
|
color: vars.color.danger,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const friendCodeButton = style({
|
||||||
|
color: vars.color.body,
|
||||||
|
cursor: "pointer",
|
||||||
|
display: "flex",
|
||||||
|
gap: `${SPACING_UNIT / 2}px`,
|
||||||
|
alignItems: "center",
|
||||||
|
transition: "all ease 0.2s",
|
||||||
|
":hover": {
|
||||||
|
color: vars.color.muted,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { Button, Modal } from "@renderer/components";
|
import { Button, Modal } from "@renderer/components";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { UserFriendModalAddFriend } from "./user-friend-modal-add-friend";
|
import { UserFriendModalAddFriend } from "./user-friend-modal-add-friend";
|
||||||
import { useUserDetails } from "@renderer/hooks";
|
import { useToast, useUserDetails } from "@renderer/hooks";
|
||||||
import { UserFriendModalList } from "./user-friend-modal-list";
|
import { UserFriendModalList } from "./user-friend-modal-list";
|
||||||
|
import { CopyIcon } from "@primer/octicons-react";
|
||||||
|
import * as styles from "./user-friend-modal.css";
|
||||||
|
|
||||||
export enum UserFriendModalTab {
|
export enum UserFriendModalTab {
|
||||||
FriendsList,
|
FriendsList,
|
||||||
@ -32,6 +34,8 @@ export const UserFriendModal = ({
|
|||||||
initialTab || UserFriendModalTab.FriendsList
|
initialTab || UserFriendModalTab.FriendsList
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { showSuccessToast } = useToast();
|
||||||
|
|
||||||
const { userDetails } = useUserDetails();
|
const { userDetails } = useUserDetails();
|
||||||
const isMe = userDetails?.id == userId;
|
const isMe = userDetails?.id == userId;
|
||||||
|
|
||||||
@ -53,6 +57,11 @@ export const UserFriendModal = ({
|
|||||||
return <></>;
|
return <></>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const copyToClipboard = useCallback(() => {
|
||||||
|
navigator.clipboard.writeText(userDetails!.id);
|
||||||
|
showSuccessToast(t("friend_code_copied"));
|
||||||
|
}, [userDetails, showSuccessToast, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal visible={visible} title={t("friends")} onClose={onClose}>
|
<Modal visible={visible} title={t("friends")} onClose={onClose}>
|
||||||
<div
|
<div
|
||||||
@ -64,6 +73,23 @@ export const UserFriendModal = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isMe && (
|
{isMe && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p>Seu código de amigo: </p>
|
||||||
|
<button
|
||||||
|
className={styles.friendCodeButton}
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
>
|
||||||
|
<h3>{userDetails.id}</h3>
|
||||||
|
<CopyIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<section style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
|
<section style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
|
||||||
{tabs.map((tab, index) => {
|
{tabs.map((tab, index) => {
|
||||||
return (
|
return (
|
||||||
@ -77,6 +103,7 @@ export const UserFriendModal = ({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</section>
|
</section>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{renderTab()}
|
{renderTab()}
|
||||||
</div>
|
</div>
|
||||||
|
@ -25,9 +25,7 @@ export const UserBlockModal = ({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
<div className={styles.signOutModalContent}>
|
<div className={styles.signOutModalContent}>
|
||||||
<p style={{ fontFamily: "Fira Sans" }}>
|
<p>{t("user_block_modal_text", { displayName })}</p>
|
||||||
{t("user_block_modal_text", { displayName })}
|
|
||||||
</p>
|
|
||||||
<div className={styles.signOutModalButtonsContainer}>
|
<div className={styles.signOutModalButtonsContainer}>
|
||||||
<Button onClick={onConfirm} theme="danger">
|
<Button onClick={onConfirm} theme="danger">
|
||||||
{t("block_user")}
|
{t("block_user")}
|
||||||
|
@ -25,7 +25,7 @@ import {
|
|||||||
XCircleIcon,
|
XCircleIcon,
|
||||||
} from "@primer/octicons-react";
|
} from "@primer/octicons-react";
|
||||||
import { Button, Link } from "@renderer/components";
|
import { Button, Link } from "@renderer/components";
|
||||||
import { UserEditProfileModal } from "./user-edit-modal";
|
import { UserProfileSettingsModal } from "./user-profile-settings-modal";
|
||||||
import { UserSignOutModal } from "./user-sign-out-modal";
|
import { UserSignOutModal } from "./user-sign-out-modal";
|
||||||
import { UserFriendModalTab } from "../shared-modals/user-friend-modal";
|
import { UserFriendModalTab } from "../shared-modals/user-friend-modal";
|
||||||
import { UserBlockModal } from "./user-block-modal";
|
import { UserBlockModal } from "./user-block-modal";
|
||||||
@ -60,7 +60,8 @@ export function UserContent({
|
|||||||
|
|
||||||
const [profileContentBoxBackground, setProfileContentBoxBackground] =
|
const [profileContentBoxBackground, setProfileContentBoxBackground] =
|
||||||
useState<string | undefined>();
|
useState<string | undefined>();
|
||||||
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
const [showProfileSettingsModal, setShowProfileSettingsModal] =
|
||||||
|
useState(false);
|
||||||
const [showSignOutModal, setShowSignOutModal] = useState(false);
|
const [showSignOutModal, setShowSignOutModal] = useState(false);
|
||||||
const [showUserBlockModal, setShowUserBlockModal] = useState(false);
|
const [showUserBlockModal, setShowUserBlockModal] = useState(false);
|
||||||
|
|
||||||
@ -95,7 +96,7 @@ export function UserContent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleEditProfile = () => {
|
const handleEditProfile = () => {
|
||||||
setShowEditProfileModal(true);
|
setShowProfileSettingsModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnClickFriend = (userId: string) => {
|
const handleOnClickFriend = (userId: string) => {
|
||||||
@ -114,7 +115,7 @@ export function UserContent({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMe) fetchFriendRequests();
|
if (isMe) fetchFriendRequests();
|
||||||
}, [isMe]);
|
}, [isMe, fetchFriendRequests]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMe && profileBackground) {
|
if (isMe && profileBackground) {
|
||||||
@ -128,7 +129,7 @@ export function UserContent({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [profileBackground, isMe]);
|
}, [profileBackground, isMe, userProfile.profileImageUrl]);
|
||||||
|
|
||||||
const handleFriendAction = (userId: string, action: FriendAction) => {
|
const handleFriendAction = (userId: string, action: FriendAction) => {
|
||||||
try {
|
try {
|
||||||
@ -159,13 +160,18 @@ export function UserContent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const showFriends = isMe || userProfile.totalFriends > 0;
|
const showFriends = isMe || userProfile.totalFriends > 0;
|
||||||
|
const showProfileContent =
|
||||||
|
isMe ||
|
||||||
|
userProfile.profileVisibility === "PUBLIC" ||
|
||||||
|
(userProfile.relation?.status === "ACCEPTED" &&
|
||||||
|
userProfile.profileVisibility === "FRIENDS");
|
||||||
|
|
||||||
const getProfileActions = () => {
|
const getProfileActions = () => {
|
||||||
if (isMe) {
|
if (isMe) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button theme="outline" onClick={handleEditProfile}>
|
<Button theme="outline" onClick={handleEditProfile}>
|
||||||
{t("edit_profile")}
|
{t("settings")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button theme="danger" onClick={() => setShowSignOutModal(true)}>
|
<Button theme="danger" onClick={() => setShowSignOutModal(true)}>
|
||||||
@ -251,9 +257,9 @@ export function UserContent({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<UserEditProfileModal
|
<UserProfileSettingsModal
|
||||||
visible={showEditProfileModal}
|
visible={showProfileSettingsModal}
|
||||||
onClose={() => setShowEditProfileModal(false)}
|
onClose={() => setShowProfileSettingsModal(false)}
|
||||||
updateUserProfile={updateUserProfile}
|
updateUserProfile={updateUserProfile}
|
||||||
userProfile={userProfile}
|
userProfile={userProfile}
|
||||||
/>
|
/>
|
||||||
@ -361,6 +367,7 @@ export function UserContent({
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{showProfileContent && (
|
||||||
<div className={styles.profileContent}>
|
<div className={styles.profileContent}>
|
||||||
<div className={styles.profileGameSection}>
|
<div className={styles.profileGameSection}>
|
||||||
<h2>{t("activity")}</h2>
|
<h2>{t("activity")}</h2>
|
||||||
@ -371,11 +378,7 @@ export function UserContent({
|
|||||||
<TelescopeIcon size={24} />
|
<TelescopeIcon size={24} />
|
||||||
</div>
|
</div>
|
||||||
<h2>{t("no_recent_activity_title")}</h2>
|
<h2>{t("no_recent_activity_title")}</h2>
|
||||||
{isMe && (
|
{isMe && <p>{t("no_recent_activity_description")}</p>}
|
||||||
<p style={{ fontFamily: "Fira Sans" }}>
|
|
||||||
{t("no_recent_activity_description")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
@ -439,7 +442,9 @@ export function UserContent({
|
|||||||
{userProfile.libraryGames.length}
|
{userProfile.libraryGames.length}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<small>{t("total_play_time", { amount: formatPlayTime() })}</small>
|
<small>
|
||||||
|
{t("total_play_time", { amount: formatPlayTime() })}
|
||||||
|
</small>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "grid",
|
display: "grid",
|
||||||
@ -450,7 +455,10 @@ export function UserContent({
|
|||||||
{userProfile.libraryGames.map((game) => (
|
{userProfile.libraryGames.map((game) => (
|
||||||
<button
|
<button
|
||||||
key={game.objectID}
|
key={game.objectID}
|
||||||
className={cn(styles.gameListItem, styles.profileContentBox)}
|
className={cn(
|
||||||
|
styles.gameListItem,
|
||||||
|
styles.profileContentBox
|
||||||
|
)}
|
||||||
onClick={() => handleGameClick(game)}
|
onClick={() => handleGameClick(game)}
|
||||||
title={game.title}
|
title={game.title}
|
||||||
>
|
>
|
||||||
@ -547,6 +555,7 @@ export function UserContent({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,147 +0,0 @@
|
|||||||
import { Button, Modal, TextField } from "@renderer/components";
|
|
||||||
import { UserProfile } from "@types";
|
|
||||||
import * as styles from "./user.css";
|
|
||||||
import { DeviceCameraIcon, PersonIcon } from "@primer/octicons-react";
|
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
|
||||||
import { useToast, useUserDetails } from "@renderer/hooks";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
export interface UserEditProfileModalProps {
|
|
||||||
userProfile: UserProfile;
|
|
||||||
visible: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
updateUserProfile: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UserEditProfileModal = ({
|
|
||||||
userProfile,
|
|
||||||
visible,
|
|
||||||
onClose,
|
|
||||||
updateUserProfile,
|
|
||||||
}: UserEditProfileModalProps) => {
|
|
||||||
const { t } = useTranslation("user_profile");
|
|
||||||
|
|
||||||
const [displayName, setDisplayName] = useState("");
|
|
||||||
const [newImagePath, setNewImagePath] = useState<string | null>(null);
|
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
|
||||||
|
|
||||||
const { patchUser } = useUserDetails();
|
|
||||||
|
|
||||||
const { showSuccessToast, showErrorToast } = useToast();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setDisplayName(userProfile.displayName);
|
|
||||||
}, [userProfile.displayName]);
|
|
||||||
|
|
||||||
const handleChangeProfileAvatar = async () => {
|
|
||||||
const { filePaths } = await window.electron.showOpenDialog({
|
|
||||||
properties: ["openFile"],
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
name: "Image",
|
|
||||||
extensions: ["jpg", "jpeg", "png", "webp"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (filePaths && filePaths.length > 0) {
|
|
||||||
const path = filePaths[0];
|
|
||||||
|
|
||||||
setNewImagePath(path);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveProfile: React.FormEventHandler<HTMLFormElement> = async (
|
|
||||||
event
|
|
||||||
) => {
|
|
||||||
event.preventDefault();
|
|
||||||
setIsSaving(true);
|
|
||||||
|
|
||||||
patchUser(displayName, newImagePath)
|
|
||||||
.then(async () => {
|
|
||||||
await updateUserProfile();
|
|
||||||
showSuccessToast(t("saved_successfully"));
|
|
||||||
cleanFormAndClose();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
showErrorToast(t("try_again"));
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsSaving(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetModal = () => {
|
|
||||||
setDisplayName(userProfile.displayName);
|
|
||||||
setNewImagePath(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cleanFormAndClose = () => {
|
|
||||||
resetModal();
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const avatarUrl = useMemo(() => {
|
|
||||||
if (newImagePath) return `local:${newImagePath}`;
|
|
||||||
if (userProfile.profileImageUrl) return userProfile.profileImageUrl;
|
|
||||||
return null;
|
|
||||||
}, [newImagePath, userProfile.profileImageUrl]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal
|
|
||||||
visible={visible}
|
|
||||||
title={t("edit_profile")}
|
|
||||||
onClose={cleanFormAndClose}
|
|
||||||
>
|
|
||||||
<form
|
|
||||||
onSubmit={handleSaveProfile}
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: `${SPACING_UNIT * 3}px`,
|
|
||||||
width: "350px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.profileAvatarEditContainer}
|
|
||||||
onClick={handleChangeProfileAvatar}
|
|
||||||
>
|
|
||||||
{avatarUrl ? (
|
|
||||||
<img
|
|
||||||
className={styles.profileAvatar}
|
|
||||||
alt={userProfile.displayName}
|
|
||||||
src={avatarUrl}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<PersonIcon size={96} />
|
|
||||||
)}
|
|
||||||
<div className={styles.editProfileImageBadge}>
|
|
||||||
<DeviceCameraIcon size={16} />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
label={t("display_name")}
|
|
||||||
value={displayName}
|
|
||||||
required
|
|
||||||
minLength={3}
|
|
||||||
containerProps={{ style: { width: "100%" } }}
|
|
||||||
onChange={(e) => setDisplayName(e.target.value)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
disabled={isSaving}
|
|
||||||
style={{ alignSelf: "end" }}
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
{isSaving ? t("saving") : t("save")}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -0,0 +1 @@
|
|||||||
|
export * from "./user-profile-settings-modal";
|
@ -0,0 +1,118 @@
|
|||||||
|
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||||
|
import { UserFriend } from "@types";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { useToast, useUserDetails } from "@renderer/hooks";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { UserFriendItem } from "@renderer/pages/shared-modals/user-friend-modal/user-friend-item";
|
||||||
|
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
|
||||||
|
|
||||||
|
const pageSize = 12;
|
||||||
|
|
||||||
|
export const UserEditProfileBlockList = () => {
|
||||||
|
const { t } = useTranslation("user_profile");
|
||||||
|
const { showErrorToast } = useToast();
|
||||||
|
|
||||||
|
const [page, setPage] = useState(0);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [maxPage, setMaxPage] = useState(0);
|
||||||
|
const [blocks, setBlocks] = useState<UserFriend[]>([]);
|
||||||
|
const listContainer = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const { unblockUser } = useUserDetails();
|
||||||
|
|
||||||
|
const loadNextPage = () => {
|
||||||
|
if (page > maxPage) return;
|
||||||
|
setIsLoading(true);
|
||||||
|
window.electron
|
||||||
|
.getUserBlocks(pageSize, page * pageSize)
|
||||||
|
.then((newPage) => {
|
||||||
|
if (page === 0) {
|
||||||
|
setMaxPage(newPage.totalBlocks / pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
setBlocks([...blocks, ...newPage.blocks]);
|
||||||
|
setPage(page + 1);
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
const scrollTop = listContainer.current?.scrollTop || 0;
|
||||||
|
const scrollHeight = listContainer.current?.scrollHeight || 0;
|
||||||
|
const clientHeight = listContainer.current?.clientHeight || 0;
|
||||||
|
const maxScrollTop = scrollHeight - clientHeight;
|
||||||
|
|
||||||
|
if (scrollTop < maxScrollTop * 0.9 || isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadNextPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
listContainer.current?.addEventListener("scroll", handleScroll);
|
||||||
|
return () =>
|
||||||
|
listContainer.current?.removeEventListener("scroll", handleScroll);
|
||||||
|
}, [isLoading]);
|
||||||
|
|
||||||
|
const reloadList = () => {
|
||||||
|
setPage(0);
|
||||||
|
setMaxPage(0);
|
||||||
|
setBlocks([]);
|
||||||
|
loadNextPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reloadList();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleUnblock = (userId: string) => {
|
||||||
|
unblockUser(userId)
|
||||||
|
.then(() => {
|
||||||
|
reloadList();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showErrorToast(t("try_again"));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||||
|
<div
|
||||||
|
ref={listContainer}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
maxHeight: "400px",
|
||||||
|
overflowY: "scroll",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!isLoading && blocks.length === 0 && <p>{t("no_blocked_users")}</p>}
|
||||||
|
{blocks.map((friend) => {
|
||||||
|
return (
|
||||||
|
<UserFriendItem
|
||||||
|
userId={friend.id}
|
||||||
|
displayName={friend.displayName}
|
||||||
|
profileImageUrl={friend.profileImageUrl}
|
||||||
|
onClickUnblock={handleUnblock}
|
||||||
|
type={"BLOCKED"}
|
||||||
|
key={friend.id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{isLoading && (
|
||||||
|
<Skeleton
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "54px",
|
||||||
|
overflow: "hidden",
|
||||||
|
borderRadius: "4px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SkeletonTheme>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,149 @@
|
|||||||
|
import { DeviceCameraIcon, PersonIcon } from "@primer/octicons-react";
|
||||||
|
import { Button, SelectField, TextField } from "@renderer/components";
|
||||||
|
import { useToast, useUserDetails } from "@renderer/hooks";
|
||||||
|
import { UserProfile } from "@types";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import * as styles from "../user.css";
|
||||||
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
|
|
||||||
|
export interface UserEditProfileProps {
|
||||||
|
userProfile: UserProfile;
|
||||||
|
updateUserProfile: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserEditProfile = ({
|
||||||
|
userProfile,
|
||||||
|
updateUserProfile,
|
||||||
|
}: UserEditProfileProps) => {
|
||||||
|
const { t } = useTranslation("user_profile");
|
||||||
|
|
||||||
|
const [form, setForm] = useState({
|
||||||
|
displayName: userProfile.displayName,
|
||||||
|
profileVisibility: userProfile.profileVisibility,
|
||||||
|
imageProfileUrl: null as string | null,
|
||||||
|
});
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
const { patchUser } = useUserDetails();
|
||||||
|
|
||||||
|
const { showSuccessToast, showErrorToast } = useToast();
|
||||||
|
|
||||||
|
const [profileVisibilityOptions, setProfileVisibilityOptions] = useState<
|
||||||
|
{ value: string; label: string }[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setProfileVisibilityOptions([
|
||||||
|
{ value: "PUBLIC", label: t("public") },
|
||||||
|
{ value: "FRIENDS", label: t("friends_only") },
|
||||||
|
{ value: "PRIVATE", label: t("private") },
|
||||||
|
]);
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
|
const handleChangeProfileAvatar = async () => {
|
||||||
|
const { filePaths } = await window.electron.showOpenDialog({
|
||||||
|
properties: ["openFile"],
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: "Image",
|
||||||
|
extensions: ["jpg", "jpeg", "png", "webp"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filePaths && filePaths.length > 0) {
|
||||||
|
const path = filePaths[0];
|
||||||
|
|
||||||
|
setForm({ ...form, imageProfileUrl: path });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProfileVisibilityChange = (event) => {
|
||||||
|
setForm({
|
||||||
|
...form,
|
||||||
|
profileVisibility: event.target.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveProfile: React.FormEventHandler<HTMLFormElement> = async (
|
||||||
|
event
|
||||||
|
) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setIsSaving(true);
|
||||||
|
|
||||||
|
patchUser(form)
|
||||||
|
.then(async () => {
|
||||||
|
await updateUserProfile();
|
||||||
|
showSuccessToast(t("saved_successfully"));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showErrorToast(t("try_again"));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsSaving(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const avatarUrl = useMemo(() => {
|
||||||
|
if (form.imageProfileUrl) return `local:${form.imageProfileUrl}`;
|
||||||
|
if (userProfile.profileImageUrl) return userProfile.profileImageUrl;
|
||||||
|
return null;
|
||||||
|
}, [form, userProfile]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSaveProfile}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: `${SPACING_UNIT * 3}px`,
|
||||||
|
width: "350px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.profileAvatarEditContainer}
|
||||||
|
onClick={handleChangeProfileAvatar}
|
||||||
|
>
|
||||||
|
{avatarUrl ? (
|
||||||
|
<img
|
||||||
|
className={styles.profileAvatar}
|
||||||
|
alt={userProfile.displayName}
|
||||||
|
src={avatarUrl}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<PersonIcon size={96} />
|
||||||
|
)}
|
||||||
|
<div className={styles.editProfileImageBadge}>
|
||||||
|
<DeviceCameraIcon size={16} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label={t("display_name")}
|
||||||
|
value={form.displayName}
|
||||||
|
required
|
||||||
|
minLength={3}
|
||||||
|
containerProps={{ style: { width: "100%" } }}
|
||||||
|
onChange={(e) => setForm({ ...form, displayName: e.target.value })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectField
|
||||||
|
label={t("privacy")}
|
||||||
|
value={form.profileVisibility}
|
||||||
|
onChange={handleProfileVisibilityChange}
|
||||||
|
options={profileVisibilityOptions.map((visiblity) => ({
|
||||||
|
key: visiblity.value,
|
||||||
|
value: visiblity.value,
|
||||||
|
label: visiblity.label,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button disabled={isSaving} style={{ alignSelf: "end" }} type="submit">
|
||||||
|
{isSaving ? t("saving") : t("save")}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,73 @@
|
|||||||
|
import { Button, Modal } from "@renderer/components";
|
||||||
|
import { UserProfile } from "@types";
|
||||||
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { UserEditProfile } from "./user-edit-profile";
|
||||||
|
import { UserEditProfileBlockList } from "./user-block-list";
|
||||||
|
|
||||||
|
export interface UserProfileSettingsModalProps {
|
||||||
|
userProfile: UserProfile;
|
||||||
|
visible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
updateUserProfile: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserProfileSettingsModal = ({
|
||||||
|
userProfile,
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
updateUserProfile,
|
||||||
|
}: UserProfileSettingsModalProps) => {
|
||||||
|
const { t } = useTranslation("user_profile");
|
||||||
|
|
||||||
|
const tabs = [t("edit_profile"), t("blocked_users")];
|
||||||
|
|
||||||
|
const [currentTabIndex, setCurrentTabIndex] = useState(0);
|
||||||
|
|
||||||
|
const renderTab = () => {
|
||||||
|
if (currentTabIndex == 0) {
|
||||||
|
return (
|
||||||
|
<UserEditProfile
|
||||||
|
userProfile={userProfile}
|
||||||
|
updateUserProfile={updateUserProfile}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTabIndex == 1) {
|
||||||
|
return <UserEditProfileBlockList />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal visible={visible} title={t("settings")} onClose={onClose}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<section style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
|
||||||
|
{tabs.map((tab, index) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={tab}
|
||||||
|
theme={index === currentTabIndex ? "primary" : "outline"}
|
||||||
|
onClick={() => setCurrentTabIndex(index)}
|
||||||
|
>
|
||||||
|
{tab}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
{renderTab()}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -23,7 +23,7 @@ export const UserSignOutModal = ({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
<div className={styles.signOutModalContent}>
|
<div className={styles.signOutModalContent}>
|
||||||
<p style={{ fontFamily: "Fira Sans" }}>{t("sign_out_modal_text")}</p>
|
<p>{t("sign_out_modal_text")}</p>
|
||||||
<div className={styles.signOutModalButtonsContainer}>
|
<div className={styles.signOutModalButtonsContainer}>
|
||||||
<Button onClick={onConfirm} theme="danger">
|
<Button onClick={onConfirm} theme="danger">
|
||||||
{t("sign_out")}
|
{t("sign_out")}
|
||||||
|
@ -60,6 +60,7 @@ export const friendListDisplayName = style({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const profileAvatarEditContainer = style({
|
export const profileAvatarEditContainer = style({
|
||||||
|
alignSelf: "center",
|
||||||
width: "128px",
|
width: "128px",
|
||||||
height: "128px",
|
height: "128px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
@ -31,7 +31,7 @@ export const User = () => {
|
|||||||
navigate(-1);
|
navigate(-1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [dispatch, userId, t]);
|
}, [dispatch, navigate, showErrorToast, userId, t]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getUserProfile();
|
getUserProfile();
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
export enum Downloader {
|
export enum Downloader {
|
||||||
RealDebrid,
|
RealDebrid,
|
||||||
Torrent,
|
Torrent,
|
||||||
|
Gofile,
|
||||||
|
PixelDrain,
|
||||||
|
Qiwi,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DownloadSourceStatus {
|
export enum DownloadSourceStatus {
|
||||||
@ -51,6 +54,8 @@ export const removeSpecialEditionFromName = (name: string) =>
|
|||||||
export const removeDuplicateSpaces = (name: string) =>
|
export const removeDuplicateSpaces = (name: string) =>
|
||||||
name.replace(/\s{2,}/g, " ");
|
name.replace(/\s{2,}/g, " ");
|
||||||
|
|
||||||
|
export const replaceDotsWithSpace = (name: string) => name.replace(/\./g, " ");
|
||||||
|
|
||||||
export const replaceUnderscoreWithSpace = (name: string) =>
|
export const replaceUnderscoreWithSpace = (name: string) =>
|
||||||
name.replace(/_/g, " ");
|
name.replace(/_/g, " ");
|
||||||
|
|
||||||
@ -58,8 +63,38 @@ export const formatName = pipe<string>(
|
|||||||
removeReleaseYearFromName,
|
removeReleaseYearFromName,
|
||||||
removeSpecialEditionFromName,
|
removeSpecialEditionFromName,
|
||||||
replaceUnderscoreWithSpace,
|
replaceUnderscoreWithSpace,
|
||||||
|
replaceDotsWithSpace,
|
||||||
(str) => str.replace(/DIRECTOR'S CUT/g, ""),
|
(str) => str.replace(/DIRECTOR'S CUT/g, ""),
|
||||||
removeSymbolsFromName,
|
removeSymbolsFromName,
|
||||||
removeDuplicateSpaces,
|
removeDuplicateSpaces,
|
||||||
(str) => str.trim()
|
(str) => str.trim()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const realDebridHosts = ["https://1fichier.com", "https://mediafire.com"];
|
||||||
|
|
||||||
|
export const getDownloadersForUri = (uri: string) => {
|
||||||
|
if (uri.startsWith("https://gofile.io")) return [Downloader.Gofile];
|
||||||
|
|
||||||
|
if (uri.startsWith("https://pixeldrain.com")) return [Downloader.PixelDrain];
|
||||||
|
if (uri.startsWith("https://qiwi.gg")) return [Downloader.Qiwi];
|
||||||
|
|
||||||
|
if (realDebridHosts.some((host) => uri.startsWith(host)))
|
||||||
|
return [Downloader.RealDebrid];
|
||||||
|
|
||||||
|
if (uri.startsWith("magnet:")) {
|
||||||
|
return [Downloader.Torrent, Downloader.RealDebrid];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDownloadersForUris = (uris: string[]) => {
|
||||||
|
const downloadersSet = uris.reduce<Set<Downloader>>((prev, next) => {
|
||||||
|
const downloaders = getDownloadersForUri(next);
|
||||||
|
downloaders.forEach((downloader) => prev.add(downloader));
|
||||||
|
|
||||||
|
return prev;
|
||||||
|
}, new Set());
|
||||||
|
|
||||||
|
return Array.from(downloadersSet);
|
||||||
|
};
|
||||||
|
@ -67,7 +67,11 @@ export interface SteamAppDetails {
|
|||||||
export interface GameRepack {
|
export interface GameRepack {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
|
/**
|
||||||
|
* @deprecated Use uris instead
|
||||||
|
*/
|
||||||
magnet: string;
|
magnet: string;
|
||||||
|
uris: string[];
|
||||||
repacker: string;
|
repacker: string;
|
||||||
fileSize: string | null;
|
fileSize: string | null;
|
||||||
uploadDate: Date | string | null;
|
uploadDate: Date | string | null;
|
||||||
@ -194,6 +198,7 @@ export interface StartGameDownloadPayload {
|
|||||||
objectID: string;
|
objectID: string;
|
||||||
title: string;
|
title: string;
|
||||||
shop: GameShop;
|
shop: GameShop;
|
||||||
|
uri: string;
|
||||||
downloadPath: string;
|
downloadPath: string;
|
||||||
downloader: Downloader;
|
downloader: Downloader;
|
||||||
}
|
}
|
||||||
@ -282,6 +287,11 @@ export interface UserFriends {
|
|||||||
friends: UserFriend[];
|
friends: UserFriend[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserBlocks {
|
||||||
|
totalBlocks: number;
|
||||||
|
blocks: UserFriend[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface FriendRequest {
|
export interface FriendRequest {
|
||||||
id: string;
|
id: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@ -310,6 +320,13 @@ export interface UserProfile {
|
|||||||
relation: UserRelation | null;
|
relation: UserRelation | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpdateProfileProps {
|
||||||
|
displayName?: string;
|
||||||
|
profileVisibility?: "PUBLIC" | "PRIVATE" | "FRIENDS";
|
||||||
|
profileImageUrl?: string | null;
|
||||||
|
bio?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DownloadSource {
|
export interface DownloadSource {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
import libtorrent as lt
|
|
||||||
|
|
||||||
class Downloader:
|
|
||||||
def __init__(self, port: str):
|
|
||||||
self.torrent_handles = {}
|
|
||||||
self.downloading_game_id = -1
|
|
||||||
self.session = lt.session({'listen_interfaces': '0.0.0.0:{port}'.format(port=port)})
|
|
||||||
|
|
||||||
def start_download(self, game_id: int, magnet: str, save_path: str):
|
|
||||||
params = {'url': magnet, 'save_path': save_path}
|
|
||||||
torrent_handle = self.session.add_torrent(params)
|
|
||||||
self.torrent_handles[game_id] = torrent_handle
|
|
||||||
torrent_handle.set_flags(lt.torrent_flags.auto_managed)
|
|
||||||
torrent_handle.resume()
|
|
||||||
|
|
||||||
self.downloading_game_id = game_id
|
|
||||||
|
|
||||||
def pause_download(self, game_id: int):
|
|
||||||
torrent_handle = self.torrent_handles.get(game_id)
|
|
||||||
if torrent_handle:
|
|
||||||
torrent_handle.pause()
|
|
||||||
torrent_handle.unset_flags(lt.torrent_flags.auto_managed)
|
|
||||||
self.downloading_game_id = -1
|
|
||||||
|
|
||||||
def cancel_download(self, game_id: int):
|
|
||||||
torrent_handle = self.torrent_handles.get(game_id)
|
|
||||||
if torrent_handle:
|
|
||||||
torrent_handle.pause()
|
|
||||||
self.session.remove_torrent(torrent_handle)
|
|
||||||
self.torrent_handles[game_id] = None
|
|
||||||
self.downloading_game_id = -1
|
|
||||||
|
|
||||||
def abort_session(self):
|
|
||||||
for game_id in self.torrent_handles:
|
|
||||||
torrent_handle = self.torrent_handles[game_id]
|
|
||||||
torrent_handle.pause()
|
|
||||||
self.session.remove_torrent(torrent_handle)
|
|
||||||
|
|
||||||
self.session.abort()
|
|
||||||
self.torrent_handles = {}
|
|
||||||
self.downloading_game_id = -1
|
|
||||||
|
|
||||||
def get_download_status(self):
|
|
||||||
if self.downloading_game_id == -1:
|
|
||||||
return None
|
|
||||||
|
|
||||||
torrent_handle = self.torrent_handles.get(self.downloading_game_id)
|
|
||||||
|
|
||||||
status = torrent_handle.status()
|
|
||||||
info = torrent_handle.get_torrent_info()
|
|
||||||
|
|
||||||
return {
|
|
||||||
'folderName': info.name() if info else "",
|
|
||||||
'fileSize': info.total_size() if info else 0,
|
|
||||||
'gameId': self.downloading_game_id,
|
|
||||||
'progress': status.progress,
|
|
||||||
'downloadSpeed': status.download_rate,
|
|
||||||
'numPeers': status.num_peers,
|
|
||||||
'numSeeds': status.num_seeds,
|
|
||||||
'status': status.state,
|
|
||||||
'bytesDownloaded': status.progress * info.total_size() if info else status.all_time_download,
|
|
||||||
}
|
|
@ -3,19 +3,19 @@ from http.server import HTTPServer, BaseHTTPRequestHandler
|
|||||||
import json
|
import json
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import psutil
|
import psutil
|
||||||
from downloader import Downloader
|
from torrent_downloader import TorrentDownloader
|
||||||
|
|
||||||
torrent_port = sys.argv[1]
|
torrent_port = sys.argv[1]
|
||||||
http_port = sys.argv[2]
|
http_port = sys.argv[2]
|
||||||
rpc_password = sys.argv[3]
|
rpc_password = sys.argv[3]
|
||||||
start_download_payload = sys.argv[4]
|
start_download_payload = sys.argv[4]
|
||||||
|
|
||||||
downloader = None
|
torrent_downloader = None
|
||||||
|
|
||||||
if start_download_payload:
|
if start_download_payload:
|
||||||
initial_download = json.loads(urllib.parse.unquote(start_download_payload))
|
initial_download = json.loads(urllib.parse.unquote(start_download_payload))
|
||||||
downloader = Downloader(torrent_port)
|
torrent_downloader = TorrentDownloader(torrent_port)
|
||||||
downloader.start_download(initial_download['game_id'], initial_download['magnet'], initial_download['save_path'])
|
torrent_downloader.start_download(initial_download['game_id'], initial_download['magnet'], initial_download['save_path'])
|
||||||
|
|
||||||
class Handler(BaseHTTPRequestHandler):
|
class Handler(BaseHTTPRequestHandler):
|
||||||
rpc_password_header = 'x-hydra-rpc-password'
|
rpc_password_header = 'x-hydra-rpc-password'
|
||||||
@ -48,7 +48,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.send_header("Content-type", "application/json")
|
self.send_header("Content-type", "application/json")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
|
||||||
status = downloader.get_download_status()
|
status = torrent_downloader.get_download_status()
|
||||||
|
|
||||||
self.wfile.write(json.dumps(status).encode('utf-8'))
|
self.wfile.write(json.dumps(status).encode('utf-8'))
|
||||||
|
|
||||||
@ -71,7 +71,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.wfile.write(json.dumps(process_list).encode('utf-8'))
|
self.wfile.write(json.dumps(process_list).encode('utf-8'))
|
||||||
|
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
global downloader
|
global torrent_downloader
|
||||||
|
|
||||||
if self.path == "/action":
|
if self.path == "/action":
|
||||||
if self.headers.get(self.rpc_password_header) != rpc_password:
|
if self.headers.get(self.rpc_password_header) != rpc_password:
|
||||||
@ -83,18 +83,18 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
post_data = self.rfile.read(content_length)
|
post_data = self.rfile.read(content_length)
|
||||||
data = json.loads(post_data.decode('utf-8'))
|
data = json.loads(post_data.decode('utf-8'))
|
||||||
|
|
||||||
if downloader is None:
|
if torrent_downloader is None:
|
||||||
downloader = Downloader(torrent_port)
|
torrent_downloader = TorrentDownloader(torrent_port)
|
||||||
|
|
||||||
if data['action'] == 'start':
|
if data['action'] == 'start':
|
||||||
downloader.start_download(data['game_id'], data['magnet'], data['save_path'])
|
torrent_downloader.start_download(data['game_id'], data['magnet'], data['save_path'])
|
||||||
elif data['action'] == 'pause':
|
elif data['action'] == 'pause':
|
||||||
downloader.pause_download(data['game_id'])
|
torrent_downloader.pause_download(data['game_id'])
|
||||||
elif data['action'] == 'cancel':
|
elif data['action'] == 'cancel':
|
||||||
downloader.cancel_download(data['game_id'])
|
torrent_downloader.cancel_download(data['game_id'])
|
||||||
elif data['action'] == 'kill-torrent':
|
elif data['action'] == 'kill-torrent':
|
||||||
downloader.abort_session()
|
torrent_downloader.abort_session()
|
||||||
downloader = None
|
torrent_downloader = None
|
||||||
|
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
158
torrent-client/torrent_downloader.py
Normal file
158
torrent-client/torrent_downloader.py
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
import libtorrent as lt
|
||||||
|
|
||||||
|
class TorrentDownloader:
|
||||||
|
def __init__(self, port: str):
|
||||||
|
self.torrent_handles = {}
|
||||||
|
self.downloading_game_id = -1
|
||||||
|
self.session = lt.session({'listen_interfaces': '0.0.0.0:{port}'.format(port=port)})
|
||||||
|
self.trackers = [
|
||||||
|
"udp://tracker.opentrackr.org:1337/announce",
|
||||||
|
"http://tracker.opentrackr.org:1337/announce",
|
||||||
|
"udp://open.tracker.cl:1337/announce",
|
||||||
|
"udp://open.demonii.com:1337/announce",
|
||||||
|
"udp://open.stealth.si:80/announce",
|
||||||
|
"udp://tracker.torrent.eu.org:451/announce",
|
||||||
|
"udp://exodus.desync.com:6969/announce",
|
||||||
|
"udp://tracker.theoks.net:6969/announce",
|
||||||
|
"udp://tracker-udp.gbitt.info:80/announce",
|
||||||
|
"udp://explodie.org:6969/announce",
|
||||||
|
"https://tracker.tamersunion.org:443/announce",
|
||||||
|
"udp://tracker2.dler.org:80/announce",
|
||||||
|
"udp://tracker1.myporn.club:9337/announce",
|
||||||
|
"udp://tracker.tiny-vps.com:6969/announce",
|
||||||
|
"udp://tracker.dler.org:6969/announce",
|
||||||
|
"udp://tracker.bittor.pw:1337/announce",
|
||||||
|
"udp://tracker.0x7c0.com:6969/announce",
|
||||||
|
"udp://retracker01-msk-virt.corbina.net:80/announce",
|
||||||
|
"udp://opentracker.io:6969/announce",
|
||||||
|
"udp://open.free-tracker.ga:6969/announce",
|
||||||
|
"udp://new-line.net:6969/announce",
|
||||||
|
"udp://moonburrow.club:6969/announce",
|
||||||
|
"udp://leet-tracker.moe:1337/announce",
|
||||||
|
"udp://bt2.archive.org:6969/announce",
|
||||||
|
"udp://bt1.archive.org:6969/announce",
|
||||||
|
"http://tracker2.dler.org:80/announce",
|
||||||
|
"http://tracker1.bt.moack.co.kr:80/announce",
|
||||||
|
"http://tracker.dler.org:6969/announce",
|
||||||
|
"http://tr.kxmp.cf:80/announce",
|
||||||
|
"udp://u.peer-exchange.download:6969/announce",
|
||||||
|
"udp://ttk2.nbaonlineservice.com:6969/announce",
|
||||||
|
"udp://tracker.tryhackx.org:6969/announce",
|
||||||
|
"udp://tracker.srv00.com:6969/announce",
|
||||||
|
"udp://tracker.skynetcloud.site:6969/announce",
|
||||||
|
"udp://tracker.jamesthebard.net:6969/announce",
|
||||||
|
"udp://tracker.fnix.net:6969/announce",
|
||||||
|
"udp://tracker.filemail.com:6969/announce",
|
||||||
|
"udp://tracker.farted.net:6969/announce",
|
||||||
|
"udp://tracker.edkj.club:6969/announce",
|
||||||
|
"udp://tracker.dump.cl:6969/announce",
|
||||||
|
"udp://tracker.deadorbit.nl:6969/announce",
|
||||||
|
"udp://tracker.darkness.services:6969/announce",
|
||||||
|
"udp://tracker.ccp.ovh:6969/announce",
|
||||||
|
"udp://tamas3.ynh.fr:6969/announce",
|
||||||
|
"udp://ryjer.com:6969/announce",
|
||||||
|
"udp://run.publictracker.xyz:6969/announce",
|
||||||
|
"udp://public.tracker.vraphim.com:6969/announce",
|
||||||
|
"udp://p4p.arenabg.com:1337/announce",
|
||||||
|
"udp://p2p.publictracker.xyz:6969/announce",
|
||||||
|
"udp://open.u-p.pw:6969/announce",
|
||||||
|
"udp://open.publictracker.xyz:6969/announce",
|
||||||
|
"udp://open.dstud.io:6969/announce",
|
||||||
|
"udp://open.demonoid.ch:6969/announce",
|
||||||
|
"udp://odd-hd.fr:6969/announce",
|
||||||
|
"udp://martin-gebhardt.eu:25/announce",
|
||||||
|
"udp://jutone.com:6969/announce",
|
||||||
|
"udp://isk.richardsw.club:6969/announce",
|
||||||
|
"udp://evan.im:6969/announce",
|
||||||
|
"udp://epider.me:6969/announce",
|
||||||
|
"udp://d40969.acod.regrucolo.ru:6969/announce",
|
||||||
|
"udp://bt.rer.lol:6969/announce",
|
||||||
|
"udp://amigacity.xyz:6969/announce",
|
||||||
|
"udp://1c.premierzal.ru:6969/announce",
|
||||||
|
"https://trackers.run:443/announce",
|
||||||
|
"https://tracker.yemekyedim.com:443/announce",
|
||||||
|
"https://tracker.renfei.net:443/announce",
|
||||||
|
"https://tracker.pmman.tech:443/announce",
|
||||||
|
"https://tracker.lilithraws.org:443/announce",
|
||||||
|
"https://tracker.imgoingto.icu:443/announce",
|
||||||
|
"https://tracker.cloudit.top:443/announce",
|
||||||
|
"https://tracker-zhuqiy.dgj055.icu:443/announce",
|
||||||
|
"http://tracker.renfei.net:8080/announce",
|
||||||
|
"http://tracker.mywaifu.best:6969/announce",
|
||||||
|
"http://tracker.ipv6tracker.org:80/announce",
|
||||||
|
"http://tracker.files.fm:6969/announce",
|
||||||
|
"http://tracker.edkj.club:6969/announce",
|
||||||
|
"http://tracker.bt4g.com:2095/announce",
|
||||||
|
"http://tracker-zhuqiy.dgj055.icu:80/announce",
|
||||||
|
"http://t1.aag.moe:17715/announce",
|
||||||
|
"http://t.overflow.biz:6969/announce",
|
||||||
|
"http://bittorrent-tracker.e-n-c-r-y-p-t.net:1337/announce",
|
||||||
|
"udp://torrents.artixlinux.org:6969/announce",
|
||||||
|
"udp://mail.artixlinux.org:6969/announce",
|
||||||
|
"udp://ipv4.rer.lol:2710/announce",
|
||||||
|
"udp://concen.org:6969/announce",
|
||||||
|
"udp://bt.rer.lol:2710/announce",
|
||||||
|
"udp://aegir.sexy:6969/announce",
|
||||||
|
"https://www.peckservers.com:9443/announce",
|
||||||
|
"https://tracker.ipfsscan.io:443/announce",
|
||||||
|
"https://tracker.gcrenwp.top:443/announce",
|
||||||
|
"http://www.peckservers.com:9000/announce",
|
||||||
|
"http://tracker1.itzmx.com:8080/announce",
|
||||||
|
"http://ch3oh.ru:6969/announce",
|
||||||
|
"http://bvarf.tracker.sh:2086/announce",
|
||||||
|
]
|
||||||
|
|
||||||
|
def start_download(self, game_id: int, magnet: str, save_path: str):
|
||||||
|
params = {'url': magnet, 'save_path': save_path, 'trackers': self.trackers}
|
||||||
|
torrent_handle = self.session.add_torrent(params)
|
||||||
|
self.torrent_handles[game_id] = torrent_handle
|
||||||
|
torrent_handle.set_flags(lt.torrent_flags.auto_managed)
|
||||||
|
torrent_handle.resume()
|
||||||
|
|
||||||
|
self.downloading_game_id = game_id
|
||||||
|
|
||||||
|
def pause_download(self, game_id: int):
|
||||||
|
torrent_handle = self.torrent_handles.get(game_id)
|
||||||
|
if torrent_handle:
|
||||||
|
torrent_handle.pause()
|
||||||
|
torrent_handle.unset_flags(lt.torrent_flags.auto_managed)
|
||||||
|
self.downloading_game_id = -1
|
||||||
|
|
||||||
|
def cancel_download(self, game_id: int):
|
||||||
|
torrent_handle = self.torrent_handles.get(game_id)
|
||||||
|
if torrent_handle:
|
||||||
|
torrent_handle.pause()
|
||||||
|
self.session.remove_torrent(torrent_handle)
|
||||||
|
self.torrent_handles[game_id] = None
|
||||||
|
self.downloading_game_id = -1
|
||||||
|
|
||||||
|
def abort_session(self):
|
||||||
|
for game_id in self.torrent_handles:
|
||||||
|
torrent_handle = self.torrent_handles[game_id]
|
||||||
|
torrent_handle.pause()
|
||||||
|
self.session.remove_torrent(torrent_handle)
|
||||||
|
|
||||||
|
self.session.abort()
|
||||||
|
self.torrent_handles = {}
|
||||||
|
self.downloading_game_id = -1
|
||||||
|
|
||||||
|
def get_download_status(self):
|
||||||
|
if self.downloading_game_id == -1:
|
||||||
|
return None
|
||||||
|
|
||||||
|
torrent_handle = self.torrent_handles.get(self.downloading_game_id)
|
||||||
|
|
||||||
|
status = torrent_handle.status()
|
||||||
|
info = torrent_handle.get_torrent_info()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'folderName': info.name() if info else "",
|
||||||
|
'fileSize': info.total_size() if info else 0,
|
||||||
|
'gameId': self.downloading_game_id,
|
||||||
|
'progress': status.progress,
|
||||||
|
'downloadSpeed': status.download_rate,
|
||||||
|
'numPeers': status.num_peers,
|
||||||
|
'numSeeds': status.num_seeds,
|
||||||
|
'status': status.state,
|
||||||
|
'bytesDownloaded': status.progress * info.total_size() if info else status.all_time_download,
|
||||||
|
}
|
36
yarn.lock
36
yarn.lock
@ -943,15 +943,10 @@
|
|||||||
resolved "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz"
|
resolved "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz"
|
||||||
integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==
|
integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==
|
||||||
|
|
||||||
"@fontsource/fira-mono@^5.0.13":
|
"@fontsource/noto-sans@^5.0.22":
|
||||||
version "5.0.13"
|
version "5.0.22"
|
||||||
resolved "https://registry.npmjs.org/@fontsource/fira-mono/-/fira-mono-5.0.13.tgz"
|
resolved "https://registry.yarnpkg.com/@fontsource/noto-sans/-/noto-sans-5.0.22.tgz#2c5249347ba84fef16e71a58e0ec01b460174093"
|
||||||
integrity sha512-fZDjR2BdAqmauEbTjcIT62zYzbOgDa5+IQH34D2k8Pxmy1T815mAqQkZciWZVQ9dc/BgdTtTUV9HJ2ulBNwchg==
|
integrity sha512-PwjvKPGFbgpwfKjWZj1zeUvd7ExUW2AqHE9PF9ysAJ2gOuzIHWE6mEVIlchYif7WC2pQhn+g0w6xooCObVi+4A==
|
||||||
|
|
||||||
"@fontsource/fira-sans@^5.0.20":
|
|
||||||
version "5.0.20"
|
|
||||||
resolved "https://registry.npmjs.org/@fontsource/fira-sans/-/fira-sans-5.0.20.tgz"
|
|
||||||
integrity sha512-inmUjoKPrgnO4uUaZTAgP0b6YdhDfA52axHXvdTwgLvwd2kn3ZgK52UZoxD0VnrvTOjLA/iE4oC0tNtz4nyb5g==
|
|
||||||
|
|
||||||
"@humanwhocodes/config-array@^0.11.14":
|
"@humanwhocodes/config-array@^0.11.14":
|
||||||
version "0.11.14"
|
version "0.11.14"
|
||||||
@ -2677,14 +2672,6 @@ aria-query@^5.3.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
dequal "^2.0.3"
|
dequal "^2.0.3"
|
||||||
|
|
||||||
aria2@^4.1.2:
|
|
||||||
version "4.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/aria2/-/aria2-4.1.2.tgz#0ecbc50beea82856c88b4de71dac336154f67362"
|
|
||||||
integrity sha512-qTBr2RY8RZQmiUmbj2KXFvkErNxU4aTHZszszzwhE8svy2PEVX+IYR/c4Rp9Tuw4QkeU8cylGy6McV6Yl8i7Qw==
|
|
||||||
dependencies:
|
|
||||||
node-fetch "^2.6.1"
|
|
||||||
ws "^7.4.0"
|
|
||||||
|
|
||||||
array-buffer-byte-length@^1.0.1:
|
array-buffer-byte-length@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz"
|
resolved "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz"
|
||||||
@ -3751,10 +3738,10 @@ electron-vite@^2.0.0:
|
|||||||
magic-string "^0.30.5"
|
magic-string "^0.30.5"
|
||||||
picocolors "^1.0.0"
|
picocolors "^1.0.0"
|
||||||
|
|
||||||
electron@^30.0.9:
|
electron@^30.3.0:
|
||||||
version "30.0.9"
|
version "30.3.1"
|
||||||
resolved "https://registry.npmjs.org/electron/-/electron-30.0.9.tgz"
|
resolved "https://registry.yarnpkg.com/electron/-/electron-30.3.1.tgz#fe27ca2a4739bec832b2edd6f46140ab46bf53a0"
|
||||||
integrity sha512-ArxgdGHVu3o5uaP+Tqj8cJDvU03R6vrGrOqiMs7JXLnvQHMqXJIIxmFKQAIdJW8VoT3ac3hD21tA7cPO10RLow==
|
integrity sha512-Ai/OZ7VlbFAVYMn9J5lyvtr+ZWyEbXDVd5wBLb5EVrp4352SRmMAmN5chcIe3n9mjzcgehV9n4Hwy15CJW+YbA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@electron/get" "^2.0.0"
|
"@electron/get" "^2.0.0"
|
||||||
"@types/node" "^20.9.0"
|
"@types/node" "^20.9.0"
|
||||||
@ -5833,7 +5820,7 @@ node-domexception@^1.0.0:
|
|||||||
resolved "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz"
|
resolved "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz"
|
||||||
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
|
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
|
||||||
|
|
||||||
node-fetch@^2.6.1, node-fetch@^2.6.7:
|
node-fetch@^2.6.7:
|
||||||
version "2.7.0"
|
version "2.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
|
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
|
||||||
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
|
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
|
||||||
@ -7629,11 +7616,6 @@ wrappy@1:
|
|||||||
resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
|
resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
|
||||||
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
|
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
|
||||||
|
|
||||||
ws@^7.4.0:
|
|
||||||
version "7.5.10"
|
|
||||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9"
|
|
||||||
integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
|
|
||||||
|
|
||||||
ws@^8.16.0:
|
ws@^8.16.0:
|
||||||
version "8.17.0"
|
version "8.17.0"
|
||||||
resolved "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz"
|
resolved "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz"
|
||||||
|
Loading…
Reference in New Issue
Block a user