From 8fa33119d6bb6380c2fc886bb658165beb3479a9 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 2 Jan 2026 23:20:08 +0200 Subject: [PATCH 01/15] feat: add support for VikingFile and display if link is available --- .env.example | 1 + python_rpc/http_downloader.py | 13 ++- .../services/download/download-manager.ts | 24 +++++ src/main/services/hosters/index.ts | 1 + src/main/services/hosters/vikingfile.ts | 51 ++++++++++ src/main/vite-env.d.ts | 1 + src/renderer/src/constants.ts | 1 + .../modals/download-settings-modal.scss | 66 ++++++++++--- .../modals/download-settings-modal.tsx | 98 +++++++++++++++---- src/shared/constants.ts | 1 + src/shared/index.ts | 3 + src/types/index.ts | 1 + 12 files changed, 226 insertions(+), 35 deletions(-) create mode 100644 src/main/services/hosters/vikingfile.ts diff --git a/.env.example b/.env.example index 051d8aa3..e13fc1bd 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,7 @@ MAIN_VITE_API_URL= MAIN_VITE_AUTH_URL= MAIN_VITE_WS_URL= +MAIN_VITE_NIMBUS_API_URL= RENDERER_VITE_REAL_DEBRID_REFERRAL_ID= RENDERER_VITE_TORBOX_REFERRAL_CODE= MAIN_VITE_LAUNCHER_SUBDOMAIN= diff --git a/python_rpc/http_downloader.py b/python_rpc/http_downloader.py index 71e4b57e..0ddf5f8e 100644 --- a/python_rpc/http_downloader.py +++ b/python_rpc/http_downloader.py @@ -11,12 +11,19 @@ class HttpDownloader: ) ) - def start_download(self, url: str, save_path: str, header: str, out: str = None): + def start_download(self, url: str, save_path: str, header, out: str = None): if self.download: self.aria2.resume([self.download]) else: - downloads = self.aria2.add(url, options={"header": header, "dir": save_path, "out": out}) - + options = {"dir": save_path} + if header: + if isinstance(header, list): + options["header"] = header + else: + options["header"] = header + if out: + options["out"] = out + downloads = self.aria2.add(url, options=options) self.download = downloads[0] def pause_download(self): diff --git a/src/main/services/download/download-manager.ts b/src/main/services/download/download-manager.ts index c36bf8ce..7fcdcd02 100644 --- a/src/main/services/download/download-manager.ts +++ b/src/main/services/download/download-manager.ts @@ -8,6 +8,7 @@ import { DatanodesApi, MediafireApi, PixelDrainApi, + VikingFileApi, } from "../hosters"; import { PythonRPC } from "../python-rpc"; import { @@ -499,6 +500,29 @@ export class DownloadManager { allow_multiple_connections: true, }; } + case Downloader.VikingFile: { + logger.log( + `[DownloadManager] Processing VikingFile download for URI: ${download.uri}` + ); + try { + const downloadUrl = await VikingFileApi.getDownloadUrl(download.uri); + logger.log(`[DownloadManager] VikingFile direct URL obtained`); + return { + action: "start", + game_id: downloadId, + url: downloadUrl, + save_path: download.downloadPath, + header: + "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", + }; + } catch (error) { + logger.error( + `[DownloadManager] Error processing VikingFile download:`, + error + ); + throw error; + } + } } } diff --git a/src/main/services/hosters/index.ts b/src/main/services/hosters/index.ts index 5f918811..e22fb680 100644 --- a/src/main/services/hosters/index.ts +++ b/src/main/services/hosters/index.ts @@ -5,3 +5,4 @@ export * from "./mediafire"; export * from "./pixeldrain"; export * from "./buzzheavier"; export * from "./fuckingfast"; +export * from "./vikingfile"; diff --git a/src/main/services/hosters/vikingfile.ts b/src/main/services/hosters/vikingfile.ts new file mode 100644 index 00000000..dae971c4 --- /dev/null +++ b/src/main/services/hosters/vikingfile.ts @@ -0,0 +1,51 @@ +import axios from "axios"; + +interface UnlockResponse { + link: string; + hoster: string; +} + +export class VikingFileApi { + private static readonly browserHeaders = { + "User-Agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", + Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + Referer: "https://vikingfile.com/", + }; + + public static async getDownloadUrl(uri: string): Promise { + const unlockResponse = await axios.post( + `${import.meta.env.MAIN_VITE_NIMBUS_API_URL}/hosters/unlock`, + { url: uri } + ); + + if (!unlockResponse.data.link) { + throw new Error("Failed to unlock VikingFile URL"); + } + + const redirectUrl = unlockResponse.data.link; + + // Follow the redirect to get the final Cloudflare storage URL + try { + const redirectResponse = await axios.head(redirectUrl, { + headers: this.browserHeaders, + maxRedirects: 0, + validateStatus: (status) => + status === 301 || status === 302 || status === 200, + }); + + if ( + redirectResponse.headers.location || + redirectResponse.status === 301 || + redirectResponse.status === 302 + ) { + return redirectResponse.headers.location || redirectUrl; + } + + return redirectUrl; + } catch (error) { + // Fallback to the redirect URL if following redirect fails + return redirectUrl; + } + } +} diff --git a/src/main/vite-env.d.ts b/src/main/vite-env.d.ts index 7b0ed536..888d8329 100644 --- a/src/main/vite-env.d.ts +++ b/src/main/vite-env.d.ts @@ -7,6 +7,7 @@ interface ImportMetaEnv { readonly MAIN_VITE_CHECKOUT_URL: string; readonly MAIN_VITE_EXTERNAL_RESOURCES_URL: string; readonly MAIN_VITE_WS_URL: string; + readonly MAIN_VITE_NIMBUS_API_URL: string; readonly MAIN_VITE_LAUNCHER_SUBDOMAIN: string; readonly ELECTRON_RENDERER_URL: string; } diff --git a/src/renderer/src/constants.ts b/src/renderer/src/constants.ts index 89de5503..153fd644 100644 --- a/src/renderer/src/constants.ts +++ b/src/renderer/src/constants.ts @@ -14,6 +14,7 @@ export const DOWNLOADER_NAME = { [Downloader.FuckingFast]: "FuckingFast", [Downloader.TorBox]: "TorBox", [Downloader.Hydra]: "Nimbus", + [Downloader.VikingFile]: "VikingFile", }; export const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120; diff --git a/src/renderer/src/pages/game-details/modals/download-settings-modal.scss b/src/renderer/src/pages/game-details/modals/download-settings-modal.scss index 1b7c51e8..b2885cb3 100644 --- a/src/renderer/src/pages/game-details/modals/download-settings-modal.scss +++ b/src/renderer/src/pages/game-details/modals/download-settings-modal.scss @@ -19,23 +19,67 @@ color: globals.$body-color; } - &__downloaders { - display: grid; - gap: globals.$spacing-unit; - grid-template-columns: repeat(2, 1fr); + &__downloaders-list { + display: flex; + flex-direction: column; + gap: calc(globals.$spacing-unit / 2); + max-height: 200px; + overflow-y: auto; + border: 1px solid globals.$border-color; + border-radius: 4px; + padding: calc(globals.$spacing-unit / 2); + background-color: globals.$dark-background-color; } - &__downloader-option { - position: relative; + &__downloader-item { + display: flex; + align-items: center; + gap: calc(globals.$spacing-unit * 1.5); + padding: calc(globals.$spacing-unit * 1.5) calc(globals.$spacing-unit * 2); + border: 1px solid transparent; + border-radius: 4px; + background-color: transparent; + cursor: pointer; + transition: + background-color 0.15s ease, + border-color 0.15s ease; + color: globals.$body-color; + font-size: 14px; + text-align: left; - &:only-child { - grid-column: 1 / -1; + &:hover:not(&--disabled) { + background-color: rgba(255, 255, 255, 0.05); + } + + &--selected { + background-color: rgba(255, 255, 255, 0.08); + } + + &--disabled { + opacity: 0.5; + cursor: not-allowed; } } - &__downloader-icon { - position: absolute; - left: calc(globals.$spacing-unit * 2); + &__downloader-name { + flex: 1; + } + + &__availability-indicator { + width: 10px; + height: 10px; + border-radius: 50%; + flex-shrink: 0; + + &--available { + background-color: #22c55e; + box-shadow: 0 0 6px rgba(34, 197, 94, 0.5); + } + + &--unavailable { + background-color: #ef4444; + box-shadow: 0 0 6px rgba(239, 68, 68, 0.5); + } } &__path-error { diff --git a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx index a6c32b6e..f488d23a 100644 --- a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx @@ -7,8 +7,13 @@ import { Modal, TextField, } from "@renderer/components"; -import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react"; -import { Downloader, formatBytes, getDownloadersForUris } from "@shared"; +import { DownloadIcon } from "@primer/octicons-react"; +import { + Downloader, + formatBytes, + getDownloadersForUri, + getDownloadersForUris, +} from "@shared"; import type { GameRepack } from "@types"; import { DOWNLOADER_NAME } from "@renderer/constants"; import { useAppSelector, useFeature, useToast } from "@renderer/hooks"; @@ -82,6 +87,41 @@ export function DownloadSettingsModal({ return getDownloadersForUris(repack?.uris ?? []); }, [repack?.uris]); + const downloadOptions = useMemo(() => { + if (!repack) return []; + + const unavailableUrisSet = new Set(repack.unavailableUris ?? []); + + const downloaderMap = new Map< + Downloader, + { hasAvailable: boolean; hasUnavailable: boolean } + >(); + + for (const uri of repack.uris) { + const uriDownloaders = getDownloadersForUri(uri); + if (uriDownloaders.length > 0) { + const downloader = uriDownloaders[0]; + const isAvailable = !unavailableUrisSet.has(uri); + + const existing = downloaderMap.get(downloader); + if (existing) { + existing.hasAvailable = existing.hasAvailable || isAvailable; + existing.hasUnavailable = existing.hasUnavailable || !isAvailable; + } else { + downloaderMap.set(downloader, { + hasAvailable: isAvailable, + hasUnavailable: !isAvailable, + }); + } + } + } + + return Array.from(downloaderMap.entries()).map(([downloader, status]) => ({ + downloader, + isAvailable: status.hasAvailable, + })); + }, [repack]); + const getDefaultDownloader = useCallback( (availableDownloaders: Downloader[]) => { if (availableDownloaders.length === 0) return null; @@ -186,31 +226,47 @@ export function DownloadSettingsModal({
{t("downloader")} -
- {downloaders.map((downloader) => { - const shouldDisableButton = - (downloader === Downloader.RealDebrid && +
+ {downloadOptions.map((option) => { + const isUnavailable = !option.isAvailable; + const shouldDisableOption = + isUnavailable || + (option.downloader === Downloader.RealDebrid && !userPreferences?.realDebridApiToken) || - (downloader === Downloader.TorBox && + (option.downloader === Downloader.TorBox && !userPreferences?.torBoxApiToken) || - (downloader === Downloader.Hydra && + (option.downloader === Downloader.Hydra && !isFeatureEnabled(Feature.Nimbus)); + const isSelected = selectedDownloader === option.downloader; + return ( - + + {DOWNLOADER_NAME[option.downloader]} + + + ); })}
diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 6e900175..5c28a27e 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -10,6 +10,7 @@ export enum Downloader { Hydra, Buzzheavier, FuckingFast, + VikingFile, } export enum DownloadSourceStatus { diff --git a/src/shared/index.ts b/src/shared/index.ts index d54ef387..36996e1d 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -124,6 +124,9 @@ export const getDownloadersForUri = (uri: string) => { if (uri.startsWith("https://fuckingfast.co")) { return [Downloader.FuckingFast]; } + if (uri.startsWith("https://vikingfile.com")) { + return [Downloader.VikingFile]; + } if (realDebridHosts.some((host) => uri.startsWith(host))) return [Downloader.RealDebrid]; diff --git a/src/types/index.ts b/src/types/index.ts index de792b05..2330c93b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -20,6 +20,7 @@ export interface GameRepack { title: string; fileSize: string | null; uris: string[]; + unavailableUris: string[]; uploadDate: string | null; downloadSourceId: string; downloadSourceName: string; From d929fbaeaa703c627afe00e4b0108c87f219d9da Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 2 Jan 2026 23:23:08 +0200 Subject: [PATCH 02/15] refactor: simplify header assignment in HttpDownloader --- python_rpc/http_downloader.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/python_rpc/http_downloader.py b/python_rpc/http_downloader.py index 0ddf5f8e..723f6d4b 100644 --- a/python_rpc/http_downloader.py +++ b/python_rpc/http_downloader.py @@ -17,10 +17,7 @@ class HttpDownloader: else: options = {"dir": save_path} if header: - if isinstance(header, list): - options["header"] = header - else: - options["header"] = header + options["header"] = header if out: options["out"] = out downloads = self.aria2.add(url, options=options) From 9a7ad148e3409c07c8bca050db2639ba4df2ce56 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 2 Jan 2026 23:24:20 +0200 Subject: [PATCH 03/15] fix: use logger for error handling in VikingFile.ts --- src/main/services/hosters/vikingfile.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/services/hosters/vikingfile.ts b/src/main/services/hosters/vikingfile.ts index dae971c4..ae09bba2 100644 --- a/src/main/services/hosters/vikingfile.ts +++ b/src/main/services/hosters/vikingfile.ts @@ -1,4 +1,5 @@ import axios from "axios"; +import { logger } from "../logger"; interface UnlockResponse { link: string; @@ -44,7 +45,10 @@ export class VikingFileApi { return redirectUrl; } catch (error) { - // Fallback to the redirect URL if following redirect fails + logger.error( + `[VikingFile] Error following redirect, using redirect URL:`, + error + ); return redirectUrl; } } From c9729fb3ebfc9129f179dabcc3dc7bcd9c32570b Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 2 Jan 2026 23:59:21 +0200 Subject: [PATCH 04/15] chore: update build and release workflows to include MAIN_VITE_NIMBUS_API_URL --- .github/workflows/build.yml | 2 ++ .github/workflows/release.yml | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5086d8e5..31354bc3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -57,6 +57,7 @@ jobs: MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }} MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }} MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_STAGING_URL }} + MAIN_VITE_NIMBUS_API_URL: ${{ vars.MAIN_VITE_NIMBUS_API_URL }} RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }} MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -73,6 +74,7 @@ jobs: MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }} MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }} MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_STAGING_URL }} + MAIN_VITE_NIMBUS_API_URL: ${{ vars.MAIN_VITE_NIMBUS_API_URL }} RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }} MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index df01b358..45163c4b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,9 +54,10 @@ jobs: MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }} MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }} MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }} + MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_URL }} + MAIN_VITE_NIMBUS_API_URL: ${{ vars.MAIN_VITE_NIMBUS_API_URL }} RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }} MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }} - MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_URL }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }} @@ -71,9 +72,10 @@ jobs: MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }} MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }} MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }} + MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_URL }} + MAIN_VITE_NIMBUS_API_URL: ${{ vars.MAIN_VITE_NIMBUS_API_URL }} RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }} MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }} - MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_URL }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }} From 2bc026677583fd2d78a2b74cde965dc0b7fb7c7a Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 3 Jan 2026 00:18:07 +0200 Subject: [PATCH 05/15] feat: add loading state to download button and enhance UI with spinner --- src/locales/en/translation.json | 1 + .../modals/download-settings-modal.scss | 13 +++++++++++++ .../modals/download-settings-modal.tsx | 15 ++++++++++++--- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 07029def..c71eab4e 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -175,6 +175,7 @@ "repacks_modal_description": "Choose the repack you want to download", "select_folder_hint": "To change the default folder, go to the <0>Settings", "download_now": "Download now", + "loading": "Loading...", "no_shop_details": "Could not retrieve shop details.", "download_options": "Download options", "download_path": "Download path", diff --git a/src/renderer/src/pages/game-details/modals/download-settings-modal.scss b/src/renderer/src/pages/game-details/modals/download-settings-modal.scss index b2885cb3..4c33ebb4 100644 --- a/src/renderer/src/pages/game-details/modals/download-settings-modal.scss +++ b/src/renderer/src/pages/game-details/modals/download-settings-modal.scss @@ -93,4 +93,17 @@ &__change-path-button { align-self: flex-end; } + + &__loading-spinner { + animation: spin 1s linear infinite; + } +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } } diff --git a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx index f488d23a..71558e6d 100644 --- a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx @@ -7,7 +7,7 @@ import { Modal, TextField, } from "@renderer/components"; -import { DownloadIcon } from "@primer/octicons-react"; +import { DownloadIcon, SyncIcon } from "@primer/octicons-react"; import { Downloader, formatBytes, @@ -323,8 +323,17 @@ export function DownloadSettingsModal({ !hasWritePermission } > - - {t("download_now")} + {downloadStarting ? ( + <> + + {t("loading")} + + ) : ( + <> + + {t("download_now")} + + )}
From de483da51caf12cb0d0fc5a3b2fcbb81432db95e Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 3 Jan 2026 01:08:25 +0200 Subject: [PATCH 06/15] fix: handle download not found exception in HttpDownloader and enforce IPv4 in HTTP agents --- python_rpc/http_downloader.py | 7 ++++++- src/main/services/aria2.ts | 1 + src/main/services/hosters/buzzheavier.ts | 14 ++++++++++++++ src/main/services/hosters/vikingfile.ts | 4 ++++ src/main/services/python-rpc.ts | 4 ++++ 5 files changed, 29 insertions(+), 1 deletion(-) diff --git a/python_rpc/http_downloader.py b/python_rpc/http_downloader.py index 723f6d4b..40428bd7 100644 --- a/python_rpc/http_downloader.py +++ b/python_rpc/http_downloader.py @@ -1,4 +1,5 @@ import aria2p +from aria2p.client import ClientException as DownloadNotFound class HttpDownloader: def __init__(self): @@ -36,7 +37,11 @@ class HttpDownloader: if self.download == None: return None - download = self.aria2.get_download(self.download.gid) + try: + download = self.aria2.get_download(self.download.gid) + except DownloadNotFound: + self.download = None + return None response = { 'folderName': download.name, diff --git a/src/main/services/aria2.ts b/src/main/services/aria2.ts index f3f49018..438008b2 100644 --- a/src/main/services/aria2.ts +++ b/src/main/services/aria2.ts @@ -21,6 +21,7 @@ export class Aria2 { "--rpc-listen-all", "--file-allocation=none", "--allow-overwrite=true", + "--disable-ipv6", ], { stdio: "inherit", windowsHide: true } ); diff --git a/src/main/services/hosters/buzzheavier.ts b/src/main/services/hosters/buzzheavier.ts index 9ef2d830..5aa209b4 100644 --- a/src/main/services/hosters/buzzheavier.ts +++ b/src/main/services/hosters/buzzheavier.ts @@ -1,4 +1,6 @@ import axios from "axios"; +import http from "http"; +import https from "https"; import { HOSTER_USER_AGENT, extractHosterFilename, @@ -28,6 +30,12 @@ export class BuzzheavierApi { await axios.get(baseUrl, { headers: { "User-Agent": HOSTER_USER_AGENT }, timeout: 30000, + httpAgent: new http.Agent({ + family: 4, // Force IPv4 + }), + httpsAgent: new https.Agent({ + family: 4, // Force IPv4 + }), }); const downloadUrl = `${baseUrl}/download`; @@ -43,6 +51,12 @@ export class BuzzheavierApi { validateStatus: (status) => status === 200 || status === 204 || status === 301 || status === 302, timeout: 30000, + httpAgent: new http.Agent({ + family: 4, // Force IPv4 + }), + httpsAgent: new https.Agent({ + family: 4, // Force IPv4 + }), }); const hxRedirect = headResponse.headers["hx-redirect"]; diff --git a/src/main/services/hosters/vikingfile.ts b/src/main/services/hosters/vikingfile.ts index ae09bba2..5f97a767 100644 --- a/src/main/services/hosters/vikingfile.ts +++ b/src/main/services/hosters/vikingfile.ts @@ -1,4 +1,5 @@ import axios from "axios"; +import https from "https"; import { logger } from "../logger"; interface UnlockResponse { @@ -33,6 +34,9 @@ export class VikingFileApi { maxRedirects: 0, validateStatus: (status) => status === 301 || status === 302 || status === 200, + httpsAgent: new https.Agent({ + family: 4, // Force IPv4 + }), }); if ( diff --git a/src/main/services/python-rpc.ts b/src/main/services/python-rpc.ts index 2a1dce79..919f6e7e 100644 --- a/src/main/services/python-rpc.ts +++ b/src/main/services/python-rpc.ts @@ -1,4 +1,5 @@ import axios from "axios"; +import http from "http"; import cp from "node:child_process"; import fs from "node:fs"; @@ -31,6 +32,9 @@ export class PythonRPC { public static readonly RPC_PORT = "8084"; public static readonly rpc = axios.create({ baseURL: `http://localhost:${this.RPC_PORT}`, + httpAgent: new http.Agent({ + family: 4, // Force IPv4 + }), }); private static pythonProcess: cp.ChildProcess | null = null; From 765ec70dd0e2b6bcd1e4e38899e287c0974baf4d Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 3 Jan 2026 01:40:21 +0200 Subject: [PATCH 07/15] refactor: streamline downloader logic in DownloadSettingsModal --- .../pages/game-details/modals/download-settings-modal.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx index 71558e6d..0b8aff7d 100644 --- a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx @@ -99,10 +99,9 @@ export function DownloadSettingsModal({ for (const uri of repack.uris) { const uriDownloaders = getDownloadersForUri(uri); - if (uriDownloaders.length > 0) { - const downloader = uriDownloaders[0]; - const isAvailable = !unavailableUrisSet.has(uri); + const isAvailable = !unavailableUrisSet.has(uri); + for (const downloader of uriDownloaders) { const existing = downloaderMap.get(downloader); if (existing) { existing.hasAvailable = existing.hasAvailable || isAvailable; From 4584783f44faa50c22863daec1ee733f2419571a Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 3 Jan 2026 04:47:46 +0200 Subject: [PATCH 08/15] refactor: enhance download progress tracking in DownloadManager --- .../services/download/download-manager.ts | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/main/services/download/download-manager.ts b/src/main/services/download/download-manager.ts index 7fcdcd02..bc6746e2 100644 --- a/src/main/services/download/download-manager.ts +++ b/src/main/services/download/download-manager.ts @@ -151,14 +151,28 @@ export class DownloadManager { if (!isDownloadingMetadata && !isCheckingFiles) { if (!download) return null; - await downloadsSublevel.put(downloadId, { + const updatedDownload = { ...download, bytesDownloaded, fileSize, progress, folderName, - status: "active", - }); + status: "active" as const, + }; + + await downloadsSublevel.put(downloadId, updatedDownload); + + return { + numPeers, + numSeeds, + downloadSpeed, + timeRemaining: calculateETA(fileSize, bytesDownloaded, downloadSpeed), + isDownloadingMetadata, + isCheckingFiles, + progress, + gameId: downloadId, + download: updatedDownload, + } as DownloadProgress; } return { From 6c4e8c406f71c5850b9fd5db7b476a27d2a9f245 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 3 Jan 2026 19:44:39 +0200 Subject: [PATCH 09/15] refactor: update HTTP module imports to use node: prefix for consistency --- src/main/services/hosters/buzzheavier.ts | 4 ++-- src/main/services/hosters/vikingfile.ts | 2 +- src/main/services/python-rpc.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/services/hosters/buzzheavier.ts b/src/main/services/hosters/buzzheavier.ts index 5aa209b4..581f5a87 100644 --- a/src/main/services/hosters/buzzheavier.ts +++ b/src/main/services/hosters/buzzheavier.ts @@ -1,6 +1,6 @@ import axios from "axios"; -import http from "http"; -import https from "https"; +import http from "node:http"; +import https from "node:https"; import { HOSTER_USER_AGENT, extractHosterFilename, diff --git a/src/main/services/hosters/vikingfile.ts b/src/main/services/hosters/vikingfile.ts index 5f97a767..ad89b876 100644 --- a/src/main/services/hosters/vikingfile.ts +++ b/src/main/services/hosters/vikingfile.ts @@ -1,5 +1,5 @@ import axios from "axios"; -import https from "https"; +import https from "node:https"; import { logger } from "../logger"; interface UnlockResponse { diff --git a/src/main/services/python-rpc.ts b/src/main/services/python-rpc.ts index 919f6e7e..d04b00ab 100644 --- a/src/main/services/python-rpc.ts +++ b/src/main/services/python-rpc.ts @@ -1,5 +1,5 @@ import axios from "axios"; -import http from "http"; +import http from "node:http"; import cp from "node:child_process"; import fs from "node:fs"; From 87fcbaa56e23f3af9195ab588625ba05baa1d9ca Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Sat, 3 Jan 2026 21:07:09 +0000 Subject: [PATCH 10/15] chore: bump version to 3.8.0 and update translations for downloader status and notifications --- package.json | 2 +- src/locales/en/translation.json | 6 + src/locales/es/translation.json | 51 +++ src/locales/pt-BR/translation.json | 51 +++ src/locales/ru/translation.json | 51 +++ src/main/services/aria2.ts | 1 - src/main/services/hosters/vikingfile.ts | 12 - src/renderer/src/constants.ts | 2 +- .../modals/download-settings-modal.scss | 129 ++++++- .../modals/download-settings-modal.tsx | 318 ++++++++++++------ .../modals/real-debrid-info-modal.scss | 37 ++ .../modals/real-debrid-info-modal.tsx | 59 ++++ 12 files changed, 594 insertions(+), 125 deletions(-) create mode 100644 src/renderer/src/pages/game-details/modals/real-debrid-info-modal.scss create mode 100644 src/renderer/src/pages/game-details/modals/real-debrid-info-modal.tsx diff --git a/package.json b/package.json index bb74198f..43aa7b78 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydralauncher", - "version": "3.7.6", + "version": "3.8.0", "description": "Hydra", "main": "./out/main/index.js", "author": "Los Broxas", diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 7180fab2..8ddeb83d 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -185,6 +185,12 @@ "open_screenshot": "Open screenshot {{number}}", "download_settings": "Download settings", "downloader": "Downloader", + "downloader_online": "Online", + "downloader_not_configured": "Available but not configured", + "downloader_offline": "Link is offline", + "downloader_not_available": "Not available", + "recommended": "Recommended", + "go_to_settings": "Go to Settings", "select_executable": "Select", "no_executable_selected": "No executable selected", "open_folder": "Open folder", diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index 12dae377..f4cf078c 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -182,6 +182,12 @@ "open_screenshot": "Abrir captura número {{number}}", "download_settings": "Descargar ajustes", "downloader": "Descargador", + "downloader_online": "En línea", + "downloader_not_configured": "Disponible pero no configurado", + "downloader_offline": "El enlace está fuera de línea", + "downloader_not_available": "No disponible", + "recommended": "Recomendado", + "go_to_settings": "Ir a Ajustes", "select_executable": "Seleccionar", "no_executable_selected": "Sin ejecutable seleccionado", "open_folder": "Abrir carpeta", @@ -651,6 +657,7 @@ "sending": "Enviando", "friend_request_sent": "Solicitud de amistad enviada", "friends": "Amistades", + "badges": "Insignias", "friends_list": "Lista de amistades", "user_not_found": "Usuario no encontrado", "block_user": "Bloquear usuario", @@ -661,12 +668,16 @@ "ignore_request": "Ignorar solicitud", "cancel_request": "Cancelar solicitud", "undo_friendship": "Deshacer amistad", + "friendship_removed": "Amigo eliminado", "request_accepted": "Solicitud aceptada", "user_blocked_successfully": "Usuario bloqueado exitosamente", "user_block_modal_text": "Esto va a bloquear a {{displayName}}", "blocked_users": "Usuarios bloqueados", "unblock": "Desbloquear", "no_friends_added": "No tenés amistades añadidas", + "view_all": "Ver todo", + "load_more": "Cargar más", + "loading": "Cargando", "pending": "Pendiente", "no_pending_invites": "No tenés invitaciones pendientes", "no_blocked_users": "No has bloqueado a nadie", @@ -690,6 +701,7 @@ "report_reason_other": "Otros", "profile_reported": "Perfil reportado", "your_friend_code": "Tu código de amistad:", + "copy_friend_code": "Copiar código de amistad", "upload_banner": "Subir banner", "uploading_banner": "Subiendo banner…", "background_image_updated": "Imagen de fondo actualizada", @@ -715,6 +727,9 @@ "game_added_to_pinned": "Juego añadido a fijados", "user_reviews": "Reseñas", "loading_reviews": "Cargando reseñas...", + "wrapped_2025": "Wrapped 2025", + "view_my_wrapped_button": "Ver Mi Wrapped 2025", + "view_wrapped_button": "Ver Wrapped 2025 de {{displayName}}", "no_reviews": "Sin reseñas aún", "delete_review": "Eliminar reseña" }, @@ -767,5 +782,41 @@ "all_games": "Todos los Juegos", "recently_played": "Jugados Recientemente", "favorites": "Favoritos" + }, + "notifications_page": { + "title": "Notificaciones", + "mark_all_as_read": "Marcar todo como leído", + "clear_all": "Limpiar todo", + "loading": "Cargando...", + "empty_title": "Sin notificaciones", + "empty_description": "¡Estás al día! Volvé más tarde para ver nuevas actualizaciones.", + "empty_filter_description": "No hay notificaciones que coincidan con este filtro.", + "filter_all": "Todas", + "filter_unread": "No leídas", + "filter_friends": "Amigos", + "filter_badges": "Insignias", + "filter_upvotes": "Votos", + "filter_local": "Locales", + "load_more": "Cargar más", + "dismiss": "Descartar", + "accept": "Aceptar", + "refuse": "Rechazar", + "notification": "Notificación", + "friend_request_received_title": "¡Nueva solicitud de amistad!", + "friend_request_received_description": "{{displayName}} quiere ser tu amigo", + "friend_request_accepted_title": "¡Solicitud de amistad aceptada!", + "friend_request_accepted_description": "{{displayName}} aceptó tu solicitud de amistad", + "badge_received_title": "¡Obtuviste una nueva insignia!", + "badge_received_description": "{{badgeName}}", + "review_upvote_title": "¡Tu reseña de {{gameTitle}} recibió votos!", + "review_upvote_description": "Tu reseña recibió {{count}} nuevos votos", + "marked_all_as_read": "Todas las notificaciones marcadas como leídas", + "failed_to_mark_as_read": "Error al marcar las notificaciones como leídas", + "cleared_all": "Todas las notificaciones eliminadas", + "failed_to_clear": "Error al eliminar las notificaciones", + "failed_to_load": "Error al cargar las notificaciones", + "failed_to_dismiss": "Error al descartar la notificación", + "friend_request_accepted": "Solicitud de amistad aceptada", + "friend_request_refused": "Solicitud de amistad rechazada" } } diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index 719f72f7..5f7d672a 100755 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -172,6 +172,12 @@ "open_screenshot": "Ver captura de tela {{number}}", "download_settings": "Ajustes do download", "downloader": "Downloader", + "downloader_online": "Online", + "downloader_not_configured": "Disponível mas não configurado", + "downloader_offline": "Link está offline", + "downloader_not_available": "Não disponível", + "recommended": "Recomendado", + "go_to_settings": "Ir para Configurações", "select_executable": "Explorar", "no_executable_selected": "Nenhum executável selecionado", "open_folder": "Abrir pasta", @@ -654,6 +660,7 @@ "see_profile": "Ver perfil", "friend_request_sent": "Pedido de amizade enviado", "friends": "Amigos", + "badges": "Insígnias", "add": "Adicionar", "sending": "Enviando", "friends_list": "Lista de amigos", @@ -666,12 +673,16 @@ "ignore_request": "Ignorar pedido", "cancel_request": "Cancelar pedido", "undo_friendship": "Desfazer amizade", + "friendship_removed": "Amigo removido", "request_accepted": "Pedido de amizade aceito", "user_blocked_successfully": "Usuário bloqueado com sucesso", "user_block_modal_text": "Bloquear {{displayName}}", "blocked_users": "Usuários bloqueados", "unblock": "Desbloquear", "no_friends_added": "Você ainda não possui amigos adicionados", + "view_all": "Ver todos", + "load_more": "Carregar mais", + "loading": "Carregando", "pending": "Pendentes", "no_pending_invites": "Você não possui convites de amizade pendentes", "no_blocked_users": "Você não tem nenhum usuário bloqueado", @@ -695,6 +706,7 @@ "report_reason_other": "Outro", "profile_reported": "Perfil reportado", "your_friend_code": "Seu código de amigo:", + "copy_friend_code": "Copiar código de amigo", "upload_banner": "Carregar banner", "uploading_banner": "Carregando banner…", "background_image_updated": "Imagem de fundo salva", @@ -724,6 +736,9 @@ "manual_playtime_tooltip": "Este tempo de jogo foi atualizado manualmente", "user_reviews": "Avaliações", "loading_reviews": "Carregando avaliações...", + "wrapped_2025": "Wrapped 2025", + "view_my_wrapped_button": "Ver Meu Wrapped 2025", + "view_wrapped_button": "Ver Wrapped 2025 de {{displayName}}", "no_reviews": "Ainda não há avaliações", "delete_review": "Excluir avaliação" }, @@ -776,5 +791,41 @@ "all_games": "Todos os Jogos", "recently_played": "Jogados Recentemente", "favorites": "Favoritos" + }, + "notifications_page": { + "title": "Notificações", + "mark_all_as_read": "Marcar todas como lidas", + "clear_all": "Limpar todas", + "loading": "Carregando...", + "empty_title": "Sem notificações", + "empty_description": "Você está em dia! Volte mais tarde para ver novas atualizações.", + "empty_filter_description": "Nenhuma notificação corresponde a este filtro.", + "filter_all": "Todas", + "filter_unread": "Não lidas", + "filter_friends": "Amigos", + "filter_badges": "Insígnias", + "filter_upvotes": "Votos", + "filter_local": "Locais", + "load_more": "Carregar mais", + "dismiss": "Descartar", + "accept": "Aceitar", + "refuse": "Recusar", + "notification": "Notificação", + "friend_request_received_title": "Nova solicitação de amizade!", + "friend_request_received_description": "{{displayName}} quer ser seu amigo", + "friend_request_accepted_title": "Solicitação de amizade aceita!", + "friend_request_accepted_description": "{{displayName}} aceitou sua solicitação de amizade", + "badge_received_title": "Você recebeu uma nova insígnia!", + "badge_received_description": "{{badgeName}}", + "review_upvote_title": "Sua avaliação de {{gameTitle}} recebeu votos!", + "review_upvote_description": "Sua avaliação recebeu {{count}} novos votos", + "marked_all_as_read": "Todas as notificações marcadas como lidas", + "failed_to_mark_as_read": "Falha ao marcar notificações como lidas", + "cleared_all": "Todas as notificações limpas", + "failed_to_clear": "Falha ao limpar notificações", + "failed_to_load": "Falha ao carregar notificações", + "failed_to_dismiss": "Falha ao descartar notificação", + "friend_request_accepted": "Solicitação de amizade aceita", + "friend_request_refused": "Solicitação de amizade recusada" } } diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index 1cf7ae2f..d43a0976 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -182,6 +182,12 @@ "open_screenshot": "Открыть скриншот {{number}}", "download_settings": "Параметры загрузки", "downloader": "Загрузчик", + "downloader_online": "Онлайн", + "downloader_not_configured": "Доступен, но не настроен", + "downloader_offline": "Ссылка недоступна", + "downloader_not_available": "Недоступно", + "recommended": "Рекомендуется", + "go_to_settings": "Перейти в настройки", "select_executable": "Выбрать", "no_executable_selected": "Файл не выбран", "open_folder": "Открыть папку", @@ -651,6 +657,7 @@ "sending": "Отправка", "friend_request_sent": "Запрос в друзья отправлен", "friends": "Друзья", + "badges": "Значки", "friends_list": "Список друзей", "user_not_found": "Пользователь не найден", "block_user": "Заблокировать пользователя", @@ -661,12 +668,16 @@ "ignore_request": "Игнорировать запрос", "cancel_request": "Отменить запрос", "undo_friendship": "Удалить друга", + "friendship_removed": "Друг удален", "request_accepted": "Запрос принят", "user_blocked_successfully": "Пользователь успешно заблокирован", "user_block_modal_text": "{{displayName}} будет заблокирован", "blocked_users": "Заблокированные пользователи", "unblock": "Разблокировать", "no_friends_added": "Вы ещё не добавили ни одного друга", + "view_all": "Показать все", + "load_more": "Загрузить еще", + "loading": "Загрузка", "pending": "Ожидание", "no_pending_invites": "У вас нет запросов ожидающих ответа", "no_blocked_users": "Вы не заблокировали ни одного пользователя", @@ -690,6 +701,7 @@ "report_reason_other": "Другое", "profile_reported": "Жалоба на профиль отправлена", "your_friend_code": "Код вашего друга:", + "copy_friend_code": "Копировать код друга", "upload_banner": "Загрузить баннер", "uploading_banner": "Загрузка баннера...", "background_image_updated": "Фоновое изображение обновлено", @@ -712,6 +724,9 @@ "karma_description": "Заработана положительными оценками отзывов", "user_reviews": "Отзывы", "loading_reviews": "Загрузка отзывов...", + "wrapped_2025": "Wrapped 2025", + "view_my_wrapped_button": "Просмотреть мой Wrapped 2025", + "view_wrapped_button": "Просмотреть Wrapped 2025 {{displayName}}", "no_reviews": "Пока нет отзывов", "delete_review": "Удалить отзыв" }, @@ -764,5 +779,41 @@ "all_games": "Все игры", "recently_played": "Недавно сыгранные", "favorites": "Избранное" + }, + "notifications_page": { + "title": "Уведомления", + "mark_all_as_read": "Отметить все как прочитанные", + "clear_all": "Очистить все", + "loading": "Загрузка...", + "empty_title": "Нет уведомлений", + "empty_description": "Вы в курсе всех событий! Загляните позже за новыми обновлениями.", + "empty_filter_description": "Нет уведомлений, соответствующих этому фильтру.", + "filter_all": "Все", + "filter_unread": "Непрочитанные", + "filter_friends": "Друзья", + "filter_badges": "Значки", + "filter_upvotes": "Голоса", + "filter_local": "Локальные", + "load_more": "Загрузить еще", + "dismiss": "Отклонить", + "accept": "Принять", + "refuse": "Отклонить", + "notification": "Уведомление", + "friend_request_received_title": "Новый запрос в друзья!", + "friend_request_received_description": "{{displayName}} хочет добавить вас в друзья", + "friend_request_accepted_title": "Запрос в друзья принят!", + "friend_request_accepted_description": "{{displayName}} принял ваш запрос в друзья", + "badge_received_title": "Вы получили новый значок!", + "badge_received_description": "{{badgeName}}", + "review_upvote_title": "Ваш отзыв на {{gameTitle}} получил голоса!", + "review_upvote_description": "Ваш отзыв получил {{count}} новых голосов", + "marked_all_as_read": "Все уведомления отмечены как прочитанные", + "failed_to_mark_as_read": "Не удалось отметить уведомления как прочитанные", + "cleared_all": "Все уведомления очищены", + "failed_to_clear": "Не удалось очистить уведомления", + "failed_to_load": "Не удалось загрузить уведомления", + "failed_to_dismiss": "Не удалось отклонить уведомление", + "friend_request_accepted": "Запрос в друзья принят", + "friend_request_refused": "Запрос в друзья отклонен" } } diff --git a/src/main/services/aria2.ts b/src/main/services/aria2.ts index 438008b2..f3f49018 100644 --- a/src/main/services/aria2.ts +++ b/src/main/services/aria2.ts @@ -21,7 +21,6 @@ export class Aria2 { "--rpc-listen-all", "--file-allocation=none", "--allow-overwrite=true", - "--disable-ipv6", ], { stdio: "inherit", windowsHide: true } ); diff --git a/src/main/services/hosters/vikingfile.ts b/src/main/services/hosters/vikingfile.ts index ad89b876..97b4236c 100644 --- a/src/main/services/hosters/vikingfile.ts +++ b/src/main/services/hosters/vikingfile.ts @@ -8,13 +8,6 @@ interface UnlockResponse { } export class VikingFileApi { - private static readonly browserHeaders = { - "User-Agent": - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", - Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - Referer: "https://vikingfile.com/", - }; - public static async getDownloadUrl(uri: string): Promise { const unlockResponse = await axios.post( `${import.meta.env.MAIN_VITE_NIMBUS_API_URL}/hosters/unlock`, @@ -27,16 +20,11 @@ export class VikingFileApi { const redirectUrl = unlockResponse.data.link; - // Follow the redirect to get the final Cloudflare storage URL try { const redirectResponse = await axios.head(redirectUrl, { - headers: this.browserHeaders, maxRedirects: 0, validateStatus: (status) => status === 301 || status === 302 || status === 200, - httpsAgent: new https.Agent({ - family: 4, // Force IPv4 - }), }); if ( diff --git a/src/renderer/src/constants.ts b/src/renderer/src/constants.ts index 153fd644..d227969b 100644 --- a/src/renderer/src/constants.ts +++ b/src/renderer/src/constants.ts @@ -1,6 +1,6 @@ import { Downloader } from "@shared"; -export const VERSION_CODENAME = "Supernova"; +export const VERSION_CODENAME = "Harbinger"; export const DOWNLOADER_NAME = { [Downloader.RealDebrid]: "Real-Debrid", diff --git a/src/renderer/src/pages/game-details/modals/download-settings-modal.scss b/src/renderer/src/pages/game-details/modals/download-settings-modal.scss index 4c33ebb4..79c8252d 100644 --- a/src/renderer/src/pages/game-details/modals/download-settings-modal.scss +++ b/src/renderer/src/pages/game-details/modals/download-settings-modal.scss @@ -19,25 +19,52 @@ color: globals.$body-color; } + &__downloaders-list-wrapper { + border: 1px solid globals.$border-color; + overflow: hidden; + background-color: globals.$dark-background-color; + } + &__downloaders-list { display: flex; flex-direction: column; - gap: calc(globals.$spacing-unit / 2); + gap: 0; max-height: 200px; overflow-y: auto; - border: 1px solid globals.$border-color; - border-radius: 4px; - padding: calc(globals.$spacing-unit / 2); - background-color: globals.$dark-background-color; + overflow-x: hidden; + padding: 0; + + &::-webkit-scrollbar { + width: 8px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 12px; + } + + &::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.3); + } + } + + &__recommendation-badge { + margin-left: calc(globals.$spacing-unit); + font-size: 10px; } &__downloader-item { display: flex; align-items: center; - gap: calc(globals.$spacing-unit * 1.5); + gap: 8px; padding: calc(globals.$spacing-unit * 1.5) calc(globals.$spacing-unit * 2); border: 1px solid transparent; - border-radius: 4px; + border-bottom: 1px solid globals.$border-color; + border-radius: 0; background-color: transparent; cursor: pointer; transition: @@ -46,8 +73,10 @@ color: globals.$body-color; font-size: 14px; text-align: left; + height: 48px; + box-sizing: border-box; - &:hover:not(&--disabled) { + &:hover { background-color: rgba(255, 255, 255, 0.05); } @@ -55,19 +84,63 @@ background-color: rgba(255, 255, 255, 0.08); } - &--disabled { - opacity: 0.5; - cursor: not-allowed; + &--last { + border-bottom: none; } } - &__downloader-name { - flex: 1; + &__downloader-item-wrapper { + display: flex; + flex-direction: column; + } + + &__recommendation-badge { + margin-left: auto; + } + + &__check-icon { + color: white; + flex-shrink: 0; + } + + &__check-icon-wrapper { + margin-left: auto; + display: flex; + align-items: center; + width: 20px; + height: 20px; + justify-content: center; + flex-shrink: 0; + } + + &__recommendation-badge { + margin-left: auto; + display: flex; + align-items: center; + height: 20px; + justify-content: center; + flex-shrink: 0; + + .badge { + padding: 2px 6px; + font-size: 10px; + line-height: 1.2; + height: 16px; + display: flex; + align-items: center; + white-space: nowrap; + } + } + + &__availability-indicator-wrapper { + display: flex; + align-items: center; + flex-shrink: 0; } &__availability-indicator { - width: 10px; - height: 10px; + width: 8px; + height: 8px; border-radius: 50%; flex-shrink: 0; @@ -80,6 +153,32 @@ background-color: #ef4444; box-shadow: 0 0 6px rgba(239, 68, 68, 0.5); } + + &--not-present { + background-color: #6b7280; + box-shadow: 0 0 6px rgba(107, 114, 128, 0.5); + } + + &--warning { + background-color: #eab308; + box-shadow: 0 0 6px rgba(234, 179, 8, 0.5); + } + } + + @keyframes pulse { + 0%, + 100% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.7; + transform: scale(1.1); + } + } + + &__availability-indicator--pulsating { + animation: pulse 2s ease-in-out infinite; } &__path-error { diff --git a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx index 0b8aff7d..597f6ae5 100644 --- a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx @@ -1,22 +1,25 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { + Badge, Button, CheckboxField, Link, Modal, TextField, } from "@renderer/components"; -import { DownloadIcon, SyncIcon } from "@primer/octicons-react"; import { - Downloader, - formatBytes, - getDownloadersForUri, - getDownloadersForUris, -} from "@shared"; + DownloadIcon, + SyncIcon, + CheckCircleFillIcon, +} from "@primer/octicons-react"; +import { Downloader, formatBytes, getDownloadersForUri } from "@shared"; import type { GameRepack } from "@types"; import { DOWNLOADER_NAME } from "@renderer/constants"; import { useAppSelector, useFeature, useToast } from "@renderer/hooks"; +import { motion } from "framer-motion"; +import { Tooltip } from "react-tooltip"; +import { RealDebridInfoModal } from "./real-debrid-info-modal"; import "./download-settings-modal.scss"; export interface DownloadSettingsModalProps { @@ -56,6 +59,7 @@ export function DownloadSettingsModal({ const [hasWritePermission, setHasWritePermission] = useState( null ); + const [showRealDebridModal, setShowRealDebridModal] = useState(false); const { isFeatureEnabled, Feature } = useFeature(); @@ -83,51 +87,115 @@ export function DownloadSettingsModal({ } }, [visible, checkFolderWritePermission, selectedPath]); - const downloaders = useMemo(() => { - return getDownloadersForUris(repack?.uris ?? []); - }, [repack?.uris]); - const downloadOptions = useMemo(() => { - if (!repack) return []; - - const unavailableUrisSet = new Set(repack.unavailableUris ?? []); + const unavailableUrisSet = new Set(repack?.unavailableUris ?? []); const downloaderMap = new Map< Downloader, { hasAvailable: boolean; hasUnavailable: boolean } >(); - for (const uri of repack.uris) { - const uriDownloaders = getDownloadersForUri(uri); - const isAvailable = !unavailableUrisSet.has(uri); + if (repack) { + for (const uri of repack.uris) { + const uriDownloaders = getDownloadersForUri(uri); + const isAvailable = !unavailableUrisSet.has(uri); - for (const downloader of uriDownloaders) { - const existing = downloaderMap.get(downloader); - if (existing) { - existing.hasAvailable = existing.hasAvailable || isAvailable; - existing.hasUnavailable = existing.hasUnavailable || !isAvailable; - } else { - downloaderMap.set(downloader, { - hasAvailable: isAvailable, - hasUnavailable: !isAvailable, - }); + for (const downloader of uriDownloaders) { + const existing = downloaderMap.get(downloader); + if (existing) { + existing.hasAvailable = existing.hasAvailable || isAvailable; + existing.hasUnavailable = existing.hasUnavailable || !isAvailable; + } else { + downloaderMap.set(downloader, { + hasAvailable: isAvailable, + hasUnavailable: !isAvailable, + }); + } } } } - return Array.from(downloaderMap.entries()).map(([downloader, status]) => ({ - downloader, - isAvailable: status.hasAvailable, - })); - }, [repack]); + const allDownloaders = Object.values(Downloader).filter( + (value) => typeof value === "number" + ) as Downloader[]; + + return allDownloaders + .filter((downloader) => downloader !== Downloader.Hydra) // Temporarily comment out Nimbus + .map((downloader) => { + const status = downloaderMap.get(downloader); + const canHandle = status !== undefined; + const isAvailable = status?.hasAvailable ?? false; + + let isConfigured = true; + if (downloader === Downloader.RealDebrid) { + isConfigured = !!userPreferences?.realDebridApiToken; + } else if (downloader === Downloader.TorBox) { + isConfigured = !!userPreferences?.torBoxApiToken; + } + // } else if (downloader === Downloader.Hydra) { + // isConfigured = isFeatureEnabled(Feature.Nimbus); + // } + + const isAvailableButNotConfigured = + isAvailable && !isConfigured && canHandle; + + return { + downloader, + isAvailable: isAvailable && isConfigured, + canHandle, + isAvailableButNotConfigured, + }; + }) + .sort((a, b) => { + if (a.isAvailable && !b.isAvailable) return -1; + if (!a.isAvailable && b.isAvailable) return 1; + if ( + a.canHandle && + !a.isAvailable && + !a.isAvailableButNotConfigured && + !b.canHandle + ) + return -1; + if ( + !a.canHandle && + b.canHandle && + !b.isAvailable && + !b.isAvailableButNotConfigured + ) + return 1; + if (a.isAvailableButNotConfigured && !b.canHandle) return -1; + if (!a.canHandle && b.isAvailableButNotConfigured) return 1; + if ( + a.isAvailableButNotConfigured && + b.canHandle && + !b.isAvailable && + !b.isAvailableButNotConfigured + ) + return 1; + if ( + !a.isAvailableButNotConfigured && + a.canHandle && + !a.isAvailable && + b.isAvailableButNotConfigured + ) + return -1; + return 0; + }); + }, [ + repack, + userPreferences?.realDebridApiToken, + userPreferences?.torBoxApiToken, + isFeatureEnabled, + Feature, + ]); const getDefaultDownloader = useCallback( (availableDownloaders: Downloader[]) => { if (availableDownloaders.length === 0) return null; - if (availableDownloaders.includes(Downloader.Hydra)) { - return Downloader.Hydra; - } + // if (availableDownloaders.includes(Downloader.Hydra)) { + // return Downloader.Hydra; + // } if (availableDownloaders.includes(Downloader.RealDebrid)) { return Downloader.RealDebrid; @@ -151,26 +219,12 @@ export function DownloadSettingsModal({ .then((defaultDownloadsPath) => setSelectedPath(defaultDownloadsPath)); } - const filteredDownloaders = downloaders.filter((downloader) => { - if (downloader === Downloader.RealDebrid) - return userPreferences?.realDebridApiToken; - if (downloader === Downloader.TorBox) - return userPreferences?.torBoxApiToken; - if (downloader === Downloader.Hydra) - return isFeatureEnabled(Feature.Nimbus); - return true; - }); + const availableDownloaders = downloadOptions + .filter((option) => option.isAvailable) + .map((option) => option.downloader); - setSelectedDownloader(getDefaultDownloader(filteredDownloaders)); - }, [ - Feature, - isFeatureEnabled, - getDefaultDownloader, - userPreferences?.downloadsPath, - downloaders, - userPreferences?.realDebridApiToken, - userPreferences?.torBoxApiToken, - ]); + setSelectedDownloader(getDefaultDownloader(availableDownloaders)); + }, [getDefaultDownloader, userPreferences?.downloadsPath, downloadOptions]); const handleChooseDownloadsPath = async () => { const { filePaths } = await window.electron.showOpenDialog({ @@ -225,49 +279,111 @@ export function DownloadSettingsModal({
{t("downloader")} -
- {downloadOptions.map((option) => { - const isUnavailable = !option.isAvailable; - const shouldDisableOption = - isUnavailable || - (option.downloader === Downloader.RealDebrid && - !userPreferences?.realDebridApiToken) || - (option.downloader === Downloader.TorBox && - !userPreferences?.torBoxApiToken) || - (option.downloader === Downloader.Hydra && - !isFeatureEnabled(Feature.Nimbus)); +
+
+ {downloadOptions.map((option, index) => { + const isSelected = selectedDownloader === option.downloader; + const tooltipId = `availability-indicator-${option.downloader}`; + const isLastItem = index === downloadOptions.length - 1; - const isSelected = selectedDownloader === option.downloader; + const Indicator = option.isAvailable ? motion.span : "span"; - return ( - - ); - })} + return ( +
+ +
+ ); + })} +
@@ -319,7 +435,14 @@ export function DownloadSettingsModal({ disabled={ downloadStarting || selectedDownloader === null || - !hasWritePermission + !hasWritePermission || + downloadOptions.some( + (option) => + option.downloader === selectedDownloader && + (option.isAvailableButNotConfigured || + (!option.isAvailable && option.canHandle) || + !option.canHandle) + ) } > {downloadStarting ? ( @@ -335,6 +458,11 @@ export function DownloadSettingsModal({ )}
+ + setShowRealDebridModal(false)} + /> ); } diff --git a/src/renderer/src/pages/game-details/modals/real-debrid-info-modal.scss b/src/renderer/src/pages/game-details/modals/real-debrid-info-modal.scss new file mode 100644 index 00000000..222ce4ec --- /dev/null +++ b/src/renderer/src/pages/game-details/modals/real-debrid-info-modal.scss @@ -0,0 +1,37 @@ +@use "../../../scss/globals.scss"; + +.real-debrid-info-modal { + &__content { + display: flex; + flex-direction: column; + gap: calc(globals.$spacing-unit * 2.5); + width: 100%; + max-width: 500px; + } + + &__description-container { + display: flex; + flex-direction: column; + gap: calc(globals.$spacing-unit * 1.5); + } + + &__description { + margin: 0; + color: globals.$body-color; + line-height: 1.6; + } + + &__create-account { + display: flex; + align-items: center; + gap: calc(globals.$spacing-unit / 2); + color: #c0c1c7; + text-decoration: underline; + font-size: 14px; + + &:hover { + text-decoration: underline; + } + } +} + diff --git a/src/renderer/src/pages/game-details/modals/real-debrid-info-modal.tsx b/src/renderer/src/pages/game-details/modals/real-debrid-info-modal.tsx new file mode 100644 index 00000000..be82bd76 --- /dev/null +++ b/src/renderer/src/pages/game-details/modals/real-debrid-info-modal.tsx @@ -0,0 +1,59 @@ +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { Button, Link, Modal } from "@renderer/components"; +import { LinkExternalIcon } from "@primer/octicons-react"; +import "./real-debrid-info-modal.scss"; + +const realDebridReferralId = import.meta.env + .RENDERER_VITE_REAL_DEBRID_REFERRAL_ID; + +const REAL_DEBRID_URL = realDebridReferralId + ? `https://real-debrid.com/?id=${realDebridReferralId}` + : "https://real-debrid.com"; + +export interface RealDebridInfoModalProps { + visible: boolean; + onClose: () => void; +} + +export function RealDebridInfoModal({ + visible, + onClose, +}: Readonly) { + const { t } = useTranslation("game_details"); + const { t: tSettings } = useTranslation("settings"); + const navigate = useNavigate(); + + return ( + +
+
+

+ {tSettings("real_debrid_description")} +

+ + + {tSettings("create_real_debrid_account")} + +
+ + +
+
+ ); +} + From 290209f37294a6df0a2cc637aeb4efd1bd9fda3d Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Sat, 3 Jan 2026 21:07:35 +0000 Subject: [PATCH 11/15] chore: remove unnecessary blank lines in RealDebridInfoModal component files --- .../src/pages/game-details/modals/real-debrid-info-modal.scss | 1 - .../src/pages/game-details/modals/real-debrid-info-modal.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/src/renderer/src/pages/game-details/modals/real-debrid-info-modal.scss b/src/renderer/src/pages/game-details/modals/real-debrid-info-modal.scss index 222ce4ec..5a97ae92 100644 --- a/src/renderer/src/pages/game-details/modals/real-debrid-info-modal.scss +++ b/src/renderer/src/pages/game-details/modals/real-debrid-info-modal.scss @@ -34,4 +34,3 @@ } } } - diff --git a/src/renderer/src/pages/game-details/modals/real-debrid-info-modal.tsx b/src/renderer/src/pages/game-details/modals/real-debrid-info-modal.tsx index be82bd76..be539db7 100644 --- a/src/renderer/src/pages/game-details/modals/real-debrid-info-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/real-debrid-info-modal.tsx @@ -56,4 +56,3 @@ export function RealDebridInfoModal({ ); } - From 87895bb715bc19edc49ec70d048f10e4f31b914a Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 3 Jan 2026 23:22:40 +0200 Subject: [PATCH 12/15] refactor: enhance disabled state styling and logic in DownloadSettingsModal --- .../modals/download-settings-modal.scss | 20 +++++++++++++++---- .../modals/download-settings-modal.tsx | 5 +++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/pages/game-details/modals/download-settings-modal.scss b/src/renderer/src/pages/game-details/modals/download-settings-modal.scss index 79c8252d..d935daf9 100644 --- a/src/renderer/src/pages/game-details/modals/download-settings-modal.scss +++ b/src/renderer/src/pages/game-details/modals/download-settings-modal.scss @@ -87,6 +87,22 @@ &--last { border-bottom: none; } + + &:disabled { + cursor: default; + + &:hover { + background-color: transparent; + } + + .download-settings-modal__downloader-name { + opacity: 0.5; + } + + .download-settings-modal__availability-indicator-wrapper { + opacity: 0.5; + } + } } &__downloader-item-wrapper { @@ -94,10 +110,6 @@ flex-direction: column; } - &__recommendation-badge { - margin-left: auto; - } - &__check-icon { color: white; flex-shrink: 0; diff --git a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx index 597f6ae5..f9dc6fa0 100644 --- a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx @@ -288,6 +288,10 @@ export function DownloadSettingsModal({ const Indicator = option.isAvailable ? motion.span : "span"; + const isDisabled = + !option.canHandle || + (!option.isAvailable && !option.isAvailableButNotConfigured); + return (
{ if ( option.downloader === Downloader.RealDebrid && From fe6553bcdc8ecec8200ec443bf80945e345ab966 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 3 Jan 2026 23:23:35 +0200 Subject: [PATCH 13/15] chore: remove unused HTTPS import in vikingfile service --- src/main/services/hosters/vikingfile.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/services/hosters/vikingfile.ts b/src/main/services/hosters/vikingfile.ts index 97b4236c..0c6d30dc 100644 --- a/src/main/services/hosters/vikingfile.ts +++ b/src/main/services/hosters/vikingfile.ts @@ -1,5 +1,4 @@ import axios from "axios"; -import https from "node:https"; import { logger } from "../logger"; interface UnlockResponse { From 01938f8905b57dc984bd431888469d26af10e2ad Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 3 Jan 2026 23:34:19 +0200 Subject: [PATCH 14/15] refactor: simplify downloader sorting and enhance availability indicators in DownloadSettingsModal --- .../modals/download-settings-modal.scss | 5 - .../modals/download-settings-modal.tsx | 191 +++++++++--------- 2 files changed, 98 insertions(+), 98 deletions(-) diff --git a/src/renderer/src/pages/game-details/modals/download-settings-modal.scss b/src/renderer/src/pages/game-details/modals/download-settings-modal.scss index d935daf9..75add6d3 100644 --- a/src/renderer/src/pages/game-details/modals/download-settings-modal.scss +++ b/src/renderer/src/pages/game-details/modals/download-settings-modal.scss @@ -52,11 +52,6 @@ } } - &__recommendation-badge { - margin-left: calc(globals.$spacing-unit); - font-size: 10px; - } - &__downloader-item { display: flex; align-items: center; diff --git a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx index f9dc6fa0..9e3802e1 100644 --- a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx @@ -119,6 +119,17 @@ export function DownloadSettingsModal({ (value) => typeof value === "number" ) as Downloader[]; + const getDownloaderPriority = (option: { + isAvailable: boolean; + canHandle: boolean; + isAvailableButNotConfigured: boolean; + }) => { + if (option.isAvailable) return 0; + if (option.canHandle && !option.isAvailableButNotConfigured) return 1; + if (option.isAvailableButNotConfigured) return 2; + return 3; + }; + return allDownloaders .filter((downloader) => downloader !== Downloader.Hydra) // Temporarily comment out Nimbus .map((downloader) => { @@ -146,41 +157,7 @@ export function DownloadSettingsModal({ isAvailableButNotConfigured, }; }) - .sort((a, b) => { - if (a.isAvailable && !b.isAvailable) return -1; - if (!a.isAvailable && b.isAvailable) return 1; - if ( - a.canHandle && - !a.isAvailable && - !a.isAvailableButNotConfigured && - !b.canHandle - ) - return -1; - if ( - !a.canHandle && - b.canHandle && - !b.isAvailable && - !b.isAvailableButNotConfigured - ) - return 1; - if (a.isAvailableButNotConfigured && !b.canHandle) return -1; - if (!a.canHandle && b.isAvailableButNotConfigured) return 1; - if ( - a.isAvailableButNotConfigured && - b.canHandle && - !b.isAvailable && - !b.isAvailableButNotConfigured - ) - return 1; - if ( - !a.isAvailableButNotConfigured && - a.canHandle && - !a.isAvailable && - b.isAvailableButNotConfigured - ) - return -1; - return 0; - }); + .sort((a, b) => getDownloaderPriority(a) - getDownloaderPriority(b)); }, [ repack, userPreferences?.realDebridApiToken, @@ -292,6 +269,90 @@ export function DownloadSettingsModal({ !option.canHandle || (!option.isAvailable && !option.isAvailableButNotConfigured); + const getAvailabilityIndicator = () => { + if (option.isAvailable) { + return ( + + ); + } + + if (option.isAvailableButNotConfigured) { + return ( + + ); + } + + if (option.canHandle) { + return ( + + ); + } + + return ( + + ); + }; + + const getRightContent = () => { + if (isSelected) { + return ( + + + + ); + } + + if ( + option.downloader === Downloader.RealDebrid && + option.canHandle + ) { + return ( +
+ {t("recommended")} +
+ ); + } + + return null; + }; + return (
- {option.isAvailable ? ( - - ) : option.isAvailableButNotConfigured ? ( - - ) : option.canHandle ? ( - - ) : ( - - )} + {getAvailabilityIndicator()}
- {isSelected ? ( - - - - ) : option.downloader === Downloader.RealDebrid && - option.canHandle ? ( -
- {t("recommended")} -
- ) : null} + {getRightContent()}
); From 4dfdc4d79884a7134f84c6fb8577efd5458113f8 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 3 Jan 2026 23:40:07 +0200 Subject: [PATCH 15/15] chore: remove commented code in DownloadSettingsModal --- .../src/pages/game-details/modals/download-settings-modal.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx index 9e3802e1..0a2c6721 100644 --- a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx @@ -170,10 +170,6 @@ export function DownloadSettingsModal({ (availableDownloaders: Downloader[]) => { if (availableDownloaders.length === 0) return null; - // if (availableDownloaders.includes(Downloader.Hydra)) { - // return Downloader.Hydra; - // } - if (availableDownloaders.includes(Downloader.RealDebrid)) { return Downloader.RealDebrid; }