mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-03 08:43:48 +03:00
feat: adding real debrid user auth
This commit is contained in:
parent
86816dc3c3
commit
183b85d66a
@ -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",
|
||||||
|
@ -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());
|
||||||
|
14
src/main/events/user-preferences/authenticate-real-debrid.ts
Normal file
14
src/main/events/user-preferences/authenticate-real-debrid.ts
Normal 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);
|
@ -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: {
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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: {
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -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: (
|
||||||
|
@ -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>
|
||||||
|
@ -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 |
@ -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 |
@ -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 |
@ -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",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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}
|
||||||
|
@ -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} />,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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";
|
||||||
|
1
src/renderer/src/declaration.d.ts
vendored
1
src/renderer/src/declaration.d.ts
vendored
@ -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>;
|
||||||
|
@ -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";
|
||||||
|
32
src/renderer/src/features/toast-slice.ts
Normal file
32
src/renderer/src/features/toast-slice.ts
Normal 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;
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user