feat: adding real debrid user auth

This commit is contained in:
Chubby Granny Chaser 2024-05-28 14:01:28 +01:00
parent 86816dc3c3
commit 183b85d66a
No known key found for this signature in database
24 changed files with 234 additions and 137 deletions

View File

@ -34,8 +34,8 @@
"bottom_panel": { "bottom_panel": {
"no_downloads_in_progress": "No downloads in progress", "no_downloads_in_progress": "No downloads in progress",
"downloading_metadata": "Downloading {{title}} metadata…", "downloading_metadata": "Downloading {{title}} metadata…",
"checking_files": "Checking {{title}} files… ({{percentage}} complete)", "downloading": "Downloading {{title}}… ({{percentage}} complete) - Conclusion {{eta}} - {{speed}}",
"downloading": "Downloading {{title}}… ({{percentage}} complete) - Conclusion {{eta}} - {{speed}}" "calculating_eta": "Downloading {{title}}… ({{percentage}} complete) - Calculating remaining time…"
}, },
"catalogue": { "catalogue": {
"next_page": "Next page", "next_page": "Next page",
@ -55,15 +55,15 @@
"remove_from_list": "Remove", "remove_from_list": "Remove",
"space_left_on_disk": "{{space}} left on disk", "space_left_on_disk": "{{space}} left on disk",
"eta": "Conclusion {{eta}}", "eta": "Conclusion {{eta}}",
"calculating_eta": "Calculating remaining time…",
"downloading_metadata": "Downloading metadata…", "downloading_metadata": "Downloading metadata…",
"checking_files": "Checking files…",
"filter": "Filter repacks", "filter": "Filter repacks",
"requirements": "System requirements", "requirements": "System requirements",
"minimum": "Minimum", "minimum": "Minimum",
"recommended": "Recommended", "recommended": "Recommended",
"no_minimum_requirements": "{{title}} doesn't provide minimum requirements information", "no_minimum_requirements": "{{title}} doesn't provide minimum requirements information",
"no_recommended_requirements": "{{title}} doesn't provide recommended requirements information", "no_recommended_requirements": "{{title}} doesn't provide recommended requirements information",
"paused_progress": "{{progress}} (Paused)", "paused": "Paused",
"release_date": "Released on {{date}}", "release_date": "Released on {{date}}",
"publisher": "Published by {{publisher}}", "publisher": "Published by {{publisher}}",
"copy_link_to_clipboard": "Copy link", "copy_link_to_clipboard": "Copy link",
@ -126,7 +126,6 @@
"filter": "Filter downloaded games", "filter": "Filter downloaded games",
"remove": "Remove", "remove": "Remove",
"downloading_metadata": "Downloading metadata…", "downloading_metadata": "Downloading metadata…",
"checking_files": "Checking files…",
"starting_download": "Starting download…", "starting_download": "Starting download…",
"deleting": "Deleting installer…", "deleting": "Deleting installer…",
"delete": "Remove installer", "delete": "Remove installer",

View File

@ -30,6 +30,7 @@ import "./user-preferences/auto-launch";
import "./autoupdater/check-for-updates"; import "./autoupdater/check-for-updates";
import "./autoupdater/restart-and-install-update"; import "./autoupdater/restart-and-install-update";
import "./autoupdater/continue-to-main-window"; import "./autoupdater/continue-to-main-window";
import "./user-preferences/authenticate-real-debrid";
ipcMain.handle("ping", () => "pong"); ipcMain.handle("ping", () => "pong");
ipcMain.handle("getVersion", () => app.getVersion()); ipcMain.handle("getVersion", () => app.getVersion());

View File

@ -0,0 +1,14 @@
import { RealDebridClient } from "@main/services/real-debrid";
import { registerEvent } from "../register-event";
const authenticateRealDebrid = async (
_event: Electron.IpcMainInvokeEvent,
apiToken: string
) => {
RealDebridClient.authorize(apiToken);
const user = await RealDebridClient.getUser();
return user;
};
registerEvent("authenticateRealDebrid", authenticateRealDebrid);

View File

@ -86,7 +86,7 @@ const loadState = async (userPreferences: UserPreferences | null) => {
import("./events"); import("./events");
if (userPreferences?.realDebridApiToken) if (userPreferences?.realDebridApiToken)
await RealDebridClient.authorize(userPreferences?.realDebridApiToken); RealDebridClient.authorize(userPreferences?.realDebridApiToken);
const game = await gameRepository.findOne({ const game = await gameRepository.findOne({
where: { where: {

View File

@ -92,7 +92,7 @@ export class DownloadManager {
const status = await this.aria2.call("tellStatus", this.gid); const status = await this.aria2.call("tellStatus", this.gid);
const downloadingMetadata = status.bittorrent && !status.bittorrent?.info; const isDownloadingMetadata = status.bittorrent && !status.bittorrent?.info;
if (status.followedBy?.length) { if (status.followedBy?.length) {
this.gid = status.followedBy[0]; this.gid = status.followedBy[0];
@ -103,7 +103,7 @@ export class DownloadManager {
const progress = const progress =
Number(status.completedLength) / Number(status.totalLength); Number(status.completedLength) / Number(status.totalLength);
if (!downloadingMetadata) { if (!isDownloadingMetadata) {
const update: QueryDeepPartialEntity<Game> = { const update: QueryDeepPartialEntity<Game> = {
bytesDownloaded: Number(status.completedLength), bytesDownloaded: Number(status.completedLength),
fileSize: Number(status.totalLength), fileSize: Number(status.totalLength),
@ -127,7 +127,7 @@ export class DownloadManager {
relations: { repack: true }, relations: { repack: true },
}); });
if (progress === 1 && game && !downloadingMetadata) { if (progress === 1 && game && !isDownloadingMetadata) {
await this.publishNotification(); await this.publishNotification();
/* /*
Only cancel bittorrent downloads to stop seeding Only cancel bittorrent downloads to stop seeding
@ -150,7 +150,7 @@ export class DownloadManager {
numSeeds: Number(status.numSeeders ?? 0), numSeeds: Number(status.numSeeders ?? 0),
downloadSpeed: Number(status.downloadSpeed), downloadSpeed: Number(status.downloadSpeed),
timeRemaining: this.getETA(status), timeRemaining: this.getETA(status),
downloadingMetadata: !!downloadingMetadata, isDownloadingMetadata: !!isDownloadingMetadata,
game, game,
} as DownloadProgress; } as DownloadProgress;

View File

@ -1,10 +1,11 @@
import { Game } from "@main/entity"; import { Game } from "@main/entity";
import axios, { AxiosInstance } from "axios";
import type { import type {
RealDebridAddMagnet, RealDebridAddMagnet,
RealDebridTorrentInfo, RealDebridTorrentInfo,
RealDebridUnrestrictLink, RealDebridUnrestrictLink,
} from "./real-debrid.types"; RealDebridUser,
import axios, { AxiosInstance } from "axios"; } from "@types";
const base = "https://api.real-debrid.com/rest/1.0"; const base = "https://api.real-debrid.com/rest/1.0";
@ -29,6 +30,11 @@ export class RealDebridClient {
return response.data; return response.data;
} }
static async getUser() {
const response = await this.instance.get<RealDebridUser>(`/user`);
return response.data;
}
static async selectAllFiles(id: string) { static async selectAllFiles(id: string) {
const searchParams = new URLSearchParams({ files: "all" }); const searchParams = new URLSearchParams({ files: "all" });
@ -65,30 +71,29 @@ export class RealDebridClient {
const hash = RealDebridClient.extractSHA1FromMagnet(game!.repack.magnet); const hash = RealDebridClient.extractSHA1FromMagnet(game!.repack.magnet);
let torrent = torrents.find((t) => t.hash === hash); let torrent = torrents.find((t) => t.hash === hash);
// User haven't downloaded this torrent yet
if (!torrent) { if (!torrent) {
const magnet = await RealDebridClient.addMagnet(game!.repack.magnet); const magnet = await RealDebridClient.addMagnet(game!.repack.magnet);
if (magnet && magnet.id) { if (magnet) {
await RealDebridClient.selectAllFiles(magnet.id); await RealDebridClient.selectAllFiles(magnet.id);
torrent = await RealDebridClient.getInfo(magnet.id); torrent = await RealDebridClient.getInfo(magnet.id);
const { links } = torrent;
const { download } = await RealDebridClient.unrestrictLink(links[0]);
if (!download) {
throw new Error("Torrent not cached on Real Debrid");
}
return download;
} }
} }
if (torrent) {
const { links } = torrent;
const { download } = await RealDebridClient.unrestrictLink(links[0]);
if (!download) {
throw new Error("Torrent not cached on Real Debrid");
}
return download;
}
throw new Error(); throw new Error();
} }
static async authorize(apiToken: string) { static authorize(apiToken: string) {
this.instance = axios.create({ this.instance = axios.create({
baseURL: base, baseURL: base,
headers: { headers: {

View File

@ -1,51 +0,0 @@
export interface RealDebridUnrestrictLink {
id: string;
filename: string;
mimeType: string;
filesize: number;
link: string;
host: string;
host_icon: string;
chunks: number;
crc: number;
download: string;
streamable: number;
}
export interface RealDebridAddMagnet {
id: string;
// URL of the created ressource
uri: string;
}
export interface RealDebridTorrentInfo {
id: string;
filename: string;
original_filename: string; // Original name of the torrent
hash: string; // SHA1 Hash of the torrent
bytes: number; // Size of selected files only
original_bytes: number; // Total size of the torrent
host: string; // Host main domain
split: number; // Split size of links
progress: number; // Possible values: 0 to 100
status: string; // Current status of the torrent: magnet_error, magnet_conversion, waiting_files_selection, queued, downloading, downloaded, error, virus, compressing, uploading, dead
added: string; // jsonDate
files: [
{
id: number;
path: string; // Path to the file inside the torrent, starting with "/"
bytes: number;
selected: number; // 0 or 1
},
{
id: number;
path: string; // Path to the file inside the torrent, starting with "/"
bytes: number;
selected: number; // 0 or 1
},
];
links: string[];
ended: string; // !! Only present when finished, jsonDate
speed: number; // !! Only present in "downloading", "compressing", "uploading" status
seeders: number; // !! Only present in "downloading", "magnet_conversion" status
}

View File

@ -49,6 +49,8 @@ contextBridge.exposeInMainWorld("electron", {
updateUserPreferences: (preferences: UserPreferences) => updateUserPreferences: (preferences: UserPreferences) =>
ipcRenderer.invoke("updateUserPreferences", preferences), ipcRenderer.invoke("updateUserPreferences", preferences),
autoLaunch: (enabled: boolean) => ipcRenderer.invoke("autoLaunch", enabled), autoLaunch: (enabled: boolean) => ipcRenderer.invoke("autoLaunch", enabled),
authenticateRealDebrid: (apiToken: string) =>
ipcRenderer.invoke("authenticateRealDebrid", apiToken),
/* Library */ /* Library */
addGameToLibrary: ( addGameToLibrary: (

View File

@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef } from "react"; import { useCallback, useEffect, useRef } from "react";
import { Sidebar, BottomPanel, Header } from "@renderer/components"; import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
import { import {
useAppDispatch, useAppDispatch,
@ -18,6 +18,7 @@ import {
clearSearch, clearSearch,
setUserPreferences, setUserPreferences,
toggleDraggingDisabled, toggleDraggingDisabled,
closeToast,
} from "@renderer/features"; } from "@renderer/features";
document.body.classList.add(themeClass); document.body.classList.add(themeClass);
@ -41,6 +42,7 @@ export function App() {
const draggingDisabled = useAppSelector( const draggingDisabled = useAppSelector(
(state) => state.window.draggingDisabled (state) => state.window.draggingDisabled
); );
const toast = useAppSelector((state) => state.toast);
useEffect(() => { useEffect(() => {
Promise.all([window.electron.getUserPreferences(), updateLibrary()]).then( Promise.all([window.electron.getUserPreferences(), updateLibrary()]).then(
@ -108,6 +110,10 @@ export function App() {
}); });
}, [dispatch, draggingDisabled]); }, [dispatch, draggingDisabled]);
const handleToastClose = useCallback(() => {
dispatch(closeToast());
}, [dispatch]);
return ( return (
<> <>
{window.electron.platform === "win32" && ( {window.electron.platform === "win32" && (
@ -128,6 +134,13 @@ export function App() {
<section ref={contentRef} className={styles.content}> <section ref={contentRef} className={styles.content}>
<Outlet /> <Outlet />
<Toast
visible={toast.visible}
message={toast.message}
type={toast.type}
onClose={handleToastClose}
/>
</section> </section>
</article> </article>
</main> </main>

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"><path fill="currentColor" d="M3.537 0C2.165 0 1.66.506 1.66 1.879V18.44a4.262 4.262 0 0 0 .02.433c.031.3.037.59.316.92c.027.033.311.245.311.245c.153.075.258.13.43.2l8.335 3.491c.433.199.614.276.928.27h.002c.314.006.495-.071.928-.27l8.335-3.492c.172-.07.277-.124.43-.2c0 0 .284-.211.311-.243c.28-.33.285-.621.316-.92a4.261 4.261 0 0 0 .02-.434V1.879c0-1.373-.506-1.88-1.878-1.88zm13.366 3.11h.68c1.138 0 1.688.553 1.688 1.696v1.88h-1.374v-1.8c0-.369-.17-.54-.523-.54h-.235c-.367 0-.537.17-.537.539v5.81c0 .369.17.54.537.54h.262c.353 0 .523-.171.523-.54V8.619h1.373v2.143c0 1.144-.562 1.71-1.7 1.71h-.694c-1.138 0-1.7-.566-1.7-1.71V4.82c0-1.144.562-1.709 1.7-1.709zm-12.186.08h3.114v1.274H6.117v2.603h1.648v1.275H6.117v2.774h1.74v1.275h-3.14zm3.816 0h2.198c1.138 0 1.7.564 1.7 1.708v2.445c0 1.144-.562 1.71-1.7 1.71h-.799v3.338h-1.4zm4.53 0h1.4v9.201h-1.4zm-3.13 1.235v3.392h.575c.354 0 .523-.171.523-.54V4.965c0-.368-.17-.54-.523-.54zm-3.74 10.147a1.708 1.708 0 0 1 .591.108a1.745 1.745 0 0 1 .49.299l-.452.546a1.247 1.247 0 0 0-.308-.195a.91.91 0 0 0-.363-.068a.658.658 0 0 0-.28.06a.703.703 0 0 0-.224.163a.783.783 0 0 0-.151.243a.799.799 0 0 0-.056.299v.008a.852.852 0 0 0 .056.31a.7.7 0 0 0 .157.245a.736.736 0 0 0 .238.16a.774.774 0 0 0 .303.058a.79.79 0 0 0 .445-.116v-.339h-.548v-.565H7.37v1.255a2.019 2.019 0 0 1-.524.307a1.789 1.789 0 0 1-.683.123a1.642 1.642 0 0 1-.602-.107a1.46 1.46 0 0 1-.478-.3a1.371 1.371 0 0 1-.318-.455a1.438 1.438 0 0 1-.115-.58v-.008a1.426 1.426 0 0 1 .113-.57a1.449 1.449 0 0 1 .312-.46a1.418 1.418 0 0 1 .474-.309a1.58 1.58 0 0 1 .598-.111a1.708 1.708 0 0 1 .045 0zm11.963.008a2.006 2.006 0 0 1 .612.094a1.61 1.61 0 0 1 .507.277l-.386.546a1.562 1.562 0 0 0-.39-.205a1.178 1.178 0 0 0-.388-.07a.347.347 0 0 0-.208.052a.154.154 0 0 0-.07.127v.008a.158.158 0 0 0 .022.084a.198.198 0 0 0 .076.066a.831.831 0 0 0 .147.06c.062.02.14.04.236.061a3.389 3.389 0 0 1 .43.122a1.292 1.292 0 0 1 .328.17a.678.678 0 0 1 .207.24a.739.739 0 0 1 .071.337v.008a.865.865 0 0 1-.081.382a.82.82 0 0 1-.229.285a1.032 1.032 0 0 1-.353.18a1.606 1.606 0 0 1-.46.061a2.16 2.16 0 0 1-.71-.116a1.718 1.718 0 0 1-.593-.346l.43-.514c.277.223.578.335.9.335a.457.457 0 0 0 .236-.05a.157.157 0 0 0 .082-.142v-.008a.15.15 0 0 0-.02-.077a.204.204 0 0 0-.073-.066a.753.753 0 0 0-.143-.062a2.45 2.45 0 0 0-.233-.062a5.036 5.036 0 0 1-.413-.113a1.26 1.26 0 0 1-.331-.16a.72.72 0 0 1-.222-.243a.73.73 0 0 1-.082-.36v-.008a.863.863 0 0 1 .074-.359a.794.794 0 0 1 .214-.283a1.007 1.007 0 0 1 .34-.185a1.423 1.423 0 0 1 .448-.066a2.006 2.006 0 0 1 .025 0m-9.358.025h.742l1.183 2.81h-.825l-.203-.499H8.623l-.198.498h-.81zm2.197.02h.814l.663 1.08l.663-1.08h.814v2.79h-.766v-1.602l-.711 1.091h-.016l-.707-1.083v1.593h-.754zm3.469 0h2.235v.658h-1.473v.422h1.334v.61h-1.334v.442h1.493v.658h-2.255zm-5.3.897l-.315.793h.624zm-1.145 5.19h8.014l-4.09 1.348z"/></svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -1 +0,0 @@
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M41.4193 7.30899C41.4193 7.30899 45.3046 5.79399 44.9808 9.47328C44.8729 10.9883 43.9016 16.2908 43.1461 22.0262L40.5559 39.0159C40.5559 39.0159 40.3401 41.5048 38.3974 41.9377C36.4547 42.3705 33.5408 40.4227 33.0011 39.9898C32.5694 39.6652 24.9068 34.7955 22.2086 32.4148C21.4531 31.7655 20.5897 30.4669 22.3165 28.9519L33.6487 18.1305C34.9438 16.8319 36.2389 13.8019 30.8426 17.4812L15.7331 27.7616C15.7331 27.7616 14.0063 28.8437 10.7686 27.8698L3.75342 25.7055C3.75342 25.7055 1.16321 24.0823 5.58815 22.459C16.3807 17.3729 29.6555 12.1786 41.4193 7.30899Z" fill="currentColor"></path> </g></svg>

Before

Width:  |  Height:  |  Size: 838 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0,0,256,256" width="16px" height="16px"><g fill="currentColor" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><g transform="scale(5.12,5.12)"><path d="M5.91992,6l14.66211,21.375l-14.35156,16.625h3.17969l12.57617,-14.57812l10,14.57813h12.01367l-15.31836,-22.33008l13.51758,-15.66992h-3.16992l-11.75391,13.61719l-9.3418,-13.61719zM9.7168,8h7.16406l23.32227,34h-7.16406z"></path></g></g></svg>

Before

Width:  |  Height:  |  Size: 702 B

View File

@ -9,6 +9,7 @@ export const bottomPanel = style({
alignItems: "center", alignItems: "center",
transition: "all ease 0.2s", transition: "all ease 0.2s",
justifyContent: "space-between", justifyContent: "space-between",
position: "relative",
zIndex: "1", zIndex: "1",
}); });

View File

@ -1,10 +1,10 @@
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useDownload } from "@renderer/hooks"; import { useDownload } from "@renderer/hooks";
import * as styles from "./bottom-panel.css"; import * as styles from "./bottom-panel.css";
import { vars } from "../../theme.css";
import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { VERSION_CODENAME } from "@renderer/constants"; import { VERSION_CODENAME } from "@renderer/constants";
@ -25,6 +25,16 @@ export function BottomPanel() {
const status = useMemo(() => { const status = useMemo(() => {
if (isGameDownloading) { if (isGameDownloading) {
if (lastPacket?.isDownloadingMetadata)
return t("downloading_metadata", { title: lastPacket?.game.title });
if (!eta) {
return t("calculating_eta", {
title: lastPacket?.game.title,
percentage: progress,
});
}
return t("downloading", { return t("downloading", {
title: lastPacket?.game.title, title: lastPacket?.game.title,
percentage: progress, percentage: progress,
@ -34,17 +44,18 @@ export function BottomPanel() {
} }
return t("no_downloads_in_progress"); return t("no_downloads_in_progress");
}, [t, isGameDownloading, lastPacket?.game, progress, eta, downloadSpeed]); }, [
t,
isGameDownloading,
lastPacket?.game,
lastPacket?.isDownloadingMetadata,
progress,
eta,
downloadSpeed,
]);
return ( return (
<footer <footer className={styles.bottomPanel}>
className={styles.bottomPanel}
style={{
background: isGameDownloading
? `linear-gradient(90deg, ${vars.color.background} ${progress}, ${vars.color.darkBackground} ${progress})`
: vars.color.darkBackground,
}}
>
<button <button
type="button" type="button"
className={styles.downloadsButton} className={styles.downloadsButton}

View File

@ -2,7 +2,6 @@ import { DownloadIcon, FileDirectoryIcon } from "@primer/octicons-react";
import type { CatalogueEntry } from "@types"; import type { CatalogueEntry } from "@types";
import SteamLogo from "@renderer/assets/steam-logo.svg?react"; import SteamLogo from "@renderer/assets/steam-logo.svg?react";
import EpicGamesLogo from "@renderer/assets/epic-games-logo.svg?react";
import * as styles from "./game-card.css"; import * as styles from "./game-card.css";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -16,7 +15,6 @@ export interface GameCardProps
} }
const shopIcon = { const shopIcon = {
epic: <EpicGamesLogo className={styles.shopIcon} />,
steam: <SteamLogo className={styles.shopIcon} />, steam: <SteamLogo className={styles.shopIcon} />,
}; };

View File

@ -8,3 +8,4 @@ export * from "./sidebar/sidebar";
export * from "./text-field/text-field"; export * from "./text-field/text-field";
export * from "./checkbox-field/checkbox-field"; export * from "./checkbox-field/checkbox-field";
export * from "./link/link"; export * from "./link/link";
export * from "./toast/toast";

View File

@ -73,6 +73,7 @@ declare global {
preferences: Partial<UserPreferences> preferences: Partial<UserPreferences>
) => Promise<void>; ) => Promise<void>;
autoLaunch: (enabled: boolean) => Promise<void>; autoLaunch: (enabled: boolean) => Promise<void>;
authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>;
/* Hardware */ /* Hardware */
getDiskFreeSpace: (path: string) => Promise<DiskSpace>; getDiskFreeSpace: (path: string) => Promise<DiskSpace>;

View File

@ -3,3 +3,4 @@ export * from "./library-slice";
export * from "./use-preferences-slice"; export * from "./use-preferences-slice";
export * from "./download-slice"; export * from "./download-slice";
export * from "./window-slice"; export * from "./window-slice";
export * from "./toast-slice";

View File

@ -0,0 +1,32 @@
import { createSlice } from "@reduxjs/toolkit";
import type { PayloadAction } from "@reduxjs/toolkit";
import { ToastProps } from "@renderer/components/toast/toast";
export interface ToastState {
message: string;
type: ToastProps["type"];
visible: boolean;
}
const initialState: ToastState = {
message: "",
type: "success",
visible: false,
};
export const toastSlice = createSlice({
name: "toast",
initialState,
reducers: {
showToast: (state, action: PayloadAction<Omit<ToastState, "visible">>) => {
console.log(action.payload);
state.message = action.payload.message;
state.visible = true;
},
closeToast: (state) => {
state.visible = false;
},
},
});
export const { showToast, closeToast } = toastSlice.actions;

View File

@ -42,7 +42,7 @@ export function HeroPanel() {
if (game?.progress === 1) return <HeroPanelPlaytime />; if (game?.progress === 1) return <HeroPanelPlaytime />;
if (game?.status === "active") { if (game?.status === "active") {
if (lastPacket?.downloadingMetadata && isGameDownloading) { if (lastPacket?.isDownloadingMetadata && isGameDownloading) {
return ( return (
<> <>
<p>{progress}</p> <p>{progress}</p>
@ -65,7 +65,8 @@ export function HeroPanel() {
{isGameDownloading {isGameDownloading
? progress ? progress
: formatDownloadProgress(game?.progress)} : formatDownloadProgress(game?.progress)}
{eta && <small>{t("eta", { eta })}</small>}
<small>{eta ? t("eta", { eta }) : t("calculating_eta")}</small>
</p> </p>
<p className={styles.downloadDetailsRow}> <p className={styles.downloadDetailsRow}>
@ -87,7 +88,9 @@ export function HeroPanel() {
return ( return (
<> <>
<p>{t("paused_progress", { progress: formattedProgress })}</p> <p className={styles.downloadDetailsRow}>
{formattedProgress} <small>{t("paused")}</small>
</p>
<p> <p>
{formatBytes(game.bytesDownloaded)} / {finalDownloadSize} {formatBytes(game.bytesDownloaded)} / {finalDownloadSize}
</p> </p>

View File

@ -5,6 +5,8 @@ import { Button, CheckboxField, Link, TextField } from "@renderer/components";
import * as styles from "./settings-real-debrid.css"; import * as styles from "./settings-real-debrid.css";
import type { UserPreferences } from "@types"; import type { UserPreferences } from "@types";
import { SPACING_UNIT } from "@renderer/theme.css"; import { SPACING_UNIT } from "@renderer/theme.css";
import { showToast } from "@renderer/features";
import { useAppDispatch } from "@renderer/hooks";
const REAL_DEBRID_API_TOKEN_URL = "https://real-debrid.com/apitoken"; const REAL_DEBRID_API_TOKEN_URL = "https://real-debrid.com/apitoken";
@ -22,6 +24,8 @@ export function SettingsRealDebrid({
realDebridApiToken: null as string | null, realDebridApiToken: null as string | null,
}); });
const dispatch = useAppDispatch();
const { t } = useTranslation("settings"); const { t } = useTranslation("settings");
useEffect(() => { useEffect(() => {
@ -33,11 +37,36 @@ export function SettingsRealDebrid({
} }
}, [userPreferences]); }, [userPreferences]);
const handleFormSubmit: React.FormEventHandler<HTMLFormElement> = (event) => { const handleFormSubmit: React.FormEventHandler<HTMLFormElement> = async (
event
) => {
event.preventDefault(); event.preventDefault();
updateUserPreferences({ dispatch(
realDebridApiToken: form.useRealDebrid ? form.realDebridApiToken : null, showToast({
}); message: t("real_debrid_authenticated"),
type: "success",
})
);
if (form.useRealDebrid) {
const user = await window.electron.authenticateRealDebrid(
form.realDebridApiToken!
);
console.log(user);
if (user.type === "premium") {
dispatch(
showToast({
message: t("real_debrid_authenticated"),
type: "success",
})
);
}
}
// updateUserPreferences({
// realDebridApiToken: form.useRealDebrid ? form.realDebridApiToken : null,
// });
}; };
const isButtonDisabled = form.useRealDebrid && !form.realDebridApiToken; const isButtonDisabled = form.useRealDebrid && !form.realDebridApiToken;

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Button } from "@renderer/components"; import { Button } from "@renderer/components";
import * as styles from "./settings.css"; import * as styles from "./settings.css";
@ -7,7 +7,6 @@ import { UserPreferences } from "@types";
import { SettingsRealDebrid } from "./settings-real-debrid"; import { SettingsRealDebrid } from "./settings-real-debrid";
import { SettingsGeneral } from "./settings-general"; import { SettingsGeneral } from "./settings-general";
import { SettingsBehavior } from "./settings-behavior"; import { SettingsBehavior } from "./settings-behavior";
import { Toast } from "@renderer/components/toast/toast";
const categories = ["general", "behavior", "real_debrid"]; const categories = ["general", "behavior", "real_debrid"];
@ -15,7 +14,6 @@ export function Settings() {
const [currentCategory, setCurrentCategory] = useState(categories.at(0)!); const [currentCategory, setCurrentCategory] = useState(categories.at(0)!);
const [userPreferences, setUserPreferences] = const [userPreferences, setUserPreferences] =
useState<UserPreferences | null>(null); useState<UserPreferences | null>(null);
const [isToastVisible, setIsToastVisible] = useState(false);
const { t } = useTranslation("settings"); const { t } = useTranslation("settings");
@ -28,12 +26,9 @@ export function Settings() {
const handleUpdateUserPreferences = async ( const handleUpdateUserPreferences = async (
values: Partial<UserPreferences> values: Partial<UserPreferences>
) => { ) => {
setIsToastVisible(false);
await window.electron.updateUserPreferences(values); await window.electron.updateUserPreferences(values);
window.electron.getUserPreferences().then((userPreferences) => { window.electron.getUserPreferences().then((userPreferences) => {
setUserPreferences(userPreferences); setUserPreferences(userPreferences);
setIsToastVisible(true);
}); });
}; };
@ -64,37 +59,24 @@ export function Settings() {
); );
}; };
const handleToastClose = useCallback(() => {
setIsToastVisible(false);
}, []);
return ( return (
<> <section className={styles.container}>
<section className={styles.container}> <div className={styles.content}>
<div className={styles.content}> <section className={styles.settingsCategories}>
<section className={styles.settingsCategories}> {categories.map((category) => (
{categories.map((category) => ( <Button
<Button key={category}
key={category} theme={currentCategory === category ? "primary" : "outline"}
theme={currentCategory === category ? "primary" : "outline"} onClick={() => setCurrentCategory(category)}
onClick={() => setCurrentCategory(category)} >
> {t(category)}
{t(category)} </Button>
</Button> ))}
))} </section>
</section>
<h2>{t(currentCategory)}</h2> <h2>{t(currentCategory)}</h2>
{renderCategory()} {renderCategory()}
</div> </div>
</section> </section>
<Toast
message="Settings have been saved"
visible={isToastVisible}
onClose={handleToastClose}
type="success"
/>
</>
); );
} }

View File

@ -5,6 +5,7 @@ import {
librarySlice, librarySlice,
searchSlice, searchSlice,
userPreferencesSlice, userPreferencesSlice,
toastSlice,
} from "@renderer/features"; } from "@renderer/features";
export const store = configureStore({ export const store = configureStore({
@ -14,6 +15,7 @@ export const store = configureStore({
library: librarySlice.reducer, library: librarySlice.reducer,
userPreferences: userPreferencesSlice.reducer, userPreferences: userPreferencesSlice.reducer,
download: downloadSlice.reducer, download: downloadSlice.reducer,
toast: toastSlice.reducer,
}, },
}); });

View File

@ -114,7 +114,7 @@ export interface DownloadProgress {
timeRemaining: number; timeRemaining: number;
numPeers: number; numPeers: number;
numSeeds: number; numSeeds: number;
downloadingMetadata: boolean; isDownloadingMetadata: boolean;
progress: number; progress: number;
bytesDownloaded: number; bytesDownloaded: number;
fileSize: number; fileSize: number;
@ -166,3 +166,59 @@ export interface StartGameDownloadPayload {
downloadPath: string; downloadPath: string;
downloader: Downloader; downloader: Downloader;
} }
export interface RealDebridUnrestrictLink {
id: string;
filename: string;
mimeType: string;
filesize: number;
link: string;
host: string;
host_icon: string;
chunks: number;
crc: number;
download: string;
streamable: number;
}
export interface RealDebridAddMagnet {
id: string;
// URL of the created ressource
uri: string;
}
export interface RealDebridTorrentInfo {
id: string;
filename: string;
original_filename: string;
hash: string;
bytes: number;
original_bytes: number;
host: string;
split: number;
progress: number;
status: string;
added: string;
files: {
id: number;
path: string;
bytes: number;
selected: number;
}[];
links: string[];
ended: string;
speed: number;
seeders: number;
}
export interface RealDebridUser {
id: number;
username: string;
email: string;
points: number;
locale: string;
avatar: string;
type: string;
premium: number;
expiration: string;
}