From 77b6f1b2ad6f6d98153d2c10fe8693c770180b54 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 13 Dec 2025 16:10:57 +0200 Subject: [PATCH 01/19] feat: add peak speed tracking and management in download slice --- src/renderer/src/features/download-slice.ts | 17 +++++++ .../src/pages/downloads/download-group.tsx | 47 +++++++++---------- 2 files changed, 40 insertions(+), 24 deletions(-) diff --git a/src/renderer/src/features/download-slice.ts b/src/renderer/src/features/download-slice.ts index 0330cca3..ae0c9c9d 100644 --- a/src/renderer/src/features/download-slice.ts +++ b/src/renderer/src/features/download-slice.ts @@ -12,6 +12,7 @@ export interface DownloadState { gameId: string | null; gamesWithDeletionInProgress: string[]; extraction: ExtractionInfo | null; + peakSpeeds: Record; } const initialState: DownloadState = { @@ -19,6 +20,7 @@ const initialState: DownloadState = { gameId: null, gamesWithDeletionInProgress: [], extraction: null, + peakSpeeds: {}, }; export const downloadSlice = createSlice({ @@ -62,6 +64,19 @@ export const downloadSlice = createSlice({ clearExtraction: (state) => { state.extraction = null; }, + updatePeakSpeed: ( + state, + action: PayloadAction<{ gameId: string; speed: number }> + ) => { + const { gameId, speed } = action.payload; + const currentPeak = state.peakSpeeds[gameId] || 0; + if (speed > currentPeak) { + state.peakSpeeds[gameId] = speed; + } + }, + clearPeakSpeed: (state, action: PayloadAction) => { + state.peakSpeeds[action.payload] = 0; + }, }, }); @@ -72,4 +87,6 @@ export const { removeGameFromDeleting, setExtractionProgress, clearExtraction, + updatePeakSpeed, + clearPeakSpeed, } = downloadSlice.actions; diff --git a/src/renderer/src/pages/downloads/download-group.tsx b/src/renderer/src/pages/downloads/download-group.tsx index 52fbcdfd..56ab398b 100644 --- a/src/renderer/src/pages/downloads/download-group.tsx +++ b/src/renderer/src/pages/downloads/download-group.tsx @@ -11,10 +11,12 @@ import { addMilliseconds } from "date-fns"; import { DOWNLOADER_NAME } from "@renderer/constants"; import { useAppSelector, + useAppDispatch, useDownload, useLibrary, useDate, } from "@renderer/hooks"; +import { updatePeakSpeed, clearPeakSpeed } from "@renderer/features"; import "./download-group.scss"; import { useTranslation } from "react-i18next"; @@ -512,7 +514,8 @@ export function DownloadGroup({ const { formatDistance } = useDate(); - const [peakSpeeds, setPeakSpeeds] = useState>({}); + const dispatch = useAppDispatch(); + const peakSpeeds = useAppSelector((state) => state.download.peakSpeeds); const speedHistoryRef = useRef>({}); const [dominantColors, setDominantColors] = useState>( {} @@ -577,28 +580,23 @@ export function DownloadGroup({ }, [library, lastPacket?.gameId]); useEffect(() => { - if (lastPacket?.gameId && lastPacket.downloadSpeed !== undefined) { - const gameId = lastPacket.gameId; + if (!lastPacket?.gameId || lastPacket.downloadSpeed === undefined) return; - const currentPeak = peakSpeeds[gameId] || 0; - if (lastPacket.downloadSpeed > currentPeak) { - setPeakSpeeds((prev) => ({ - ...prev, - [gameId]: lastPacket.downloadSpeed, - })); - } + const gameId = lastPacket.gameId; + const downloadSpeed = lastPacket.downloadSpeed; - if (!speedHistoryRef.current[gameId]) { - speedHistoryRef.current[gameId] = []; - } + dispatch(updatePeakSpeed({ gameId, speed: downloadSpeed })); - speedHistoryRef.current[gameId].push(lastPacket.downloadSpeed); - - if (speedHistoryRef.current[gameId].length > 120) { - speedHistoryRef.current[gameId].shift(); - } + if (!speedHistoryRef.current[gameId]) { + speedHistoryRef.current[gameId] = []; } - }, [lastPacket?.gameId, lastPacket?.downloadSpeed, peakSpeeds]); + + speedHistoryRef.current[gameId].push(downloadSpeed); + + if (speedHistoryRef.current[gameId].length > 120) { + speedHistoryRef.current[gameId].shift(); + } + }, [lastPacket, dispatch]); useEffect(() => { for (const game of library) { @@ -610,11 +608,11 @@ export function DownloadGroup({ // Fresh download - clear any old data if (speedHistoryRef.current[game.id]?.length > 0) { speedHistoryRef.current[game.id] = []; - setPeakSpeeds((prev) => ({ ...prev, [game.id]: 0 })); + dispatch(clearPeakSpeed(game.id)); } } } - }, [library]); + }, [library, dispatch]); useEffect(() => { const timeouts: NodeJS.Timeout[] = []; @@ -624,9 +622,10 @@ export function DownloadGroup({ game.download?.progress === 1 && speedHistoryRef.current[game.id]?.length > 0 ) { + const gameId = game.id; const timeout = setTimeout(() => { - speedHistoryRef.current[game.id] = []; - setPeakSpeeds((prev) => ({ ...prev, [game.id]: 0 })); + speedHistoryRef.current[gameId] = []; + dispatch(clearPeakSpeed(gameId)); }, 10_000); timeouts.push(timeout); } @@ -637,7 +636,7 @@ export function DownloadGroup({ clearTimeout(timeout); } }; - }, [library]); + }, [library, dispatch]); useEffect(() => { if (library.length > 0 && title === t("download_in_progress")) { From 67f863e0f32a245346bfbb00d1f8964adb9856fc Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 13 Dec 2025 17:41:15 +0200 Subject: [PATCH 02/19] fix: remove unused peak speed update dispatch in DownloadGroup component --- src/renderer/src/features/download-slice.ts | 9 +++++++++ src/renderer/src/pages/downloads/download-group.tsx | 6 ++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/features/download-slice.ts b/src/renderer/src/features/download-slice.ts index ae0c9c9d..80d78b0d 100644 --- a/src/renderer/src/features/download-slice.ts +++ b/src/renderer/src/features/download-slice.ts @@ -30,6 +30,15 @@ export const downloadSlice = createSlice({ setLastPacket: (state, action: PayloadAction) => { state.lastPacket = action.payload; if (!state.gameId && action.payload) state.gameId = action.payload.gameId; + + // Track peak speed atomically when packet arrives + if (action.payload?.gameId && action.payload.downloadSpeed != null) { + const { gameId, downloadSpeed } = action.payload; + const currentPeak = state.peakSpeeds[gameId] || 0; + if (downloadSpeed > currentPeak) { + state.peakSpeeds[gameId] = downloadSpeed; + } + } }, clearDownload: (state) => { state.lastPacket = null; diff --git a/src/renderer/src/pages/downloads/download-group.tsx b/src/renderer/src/pages/downloads/download-group.tsx index 56ab398b..7c3f0f77 100644 --- a/src/renderer/src/pages/downloads/download-group.tsx +++ b/src/renderer/src/pages/downloads/download-group.tsx @@ -16,7 +16,7 @@ import { useLibrary, useDate, } from "@renderer/hooks"; -import { updatePeakSpeed, clearPeakSpeed } from "@renderer/features"; +import { clearPeakSpeed } from "@renderer/features"; import "./download-group.scss"; import { useTranslation } from "react-i18next"; @@ -585,8 +585,6 @@ export function DownloadGroup({ const gameId = lastPacket.gameId; const downloadSpeed = lastPacket.downloadSpeed; - dispatch(updatePeakSpeed({ gameId, speed: downloadSpeed })); - if (!speedHistoryRef.current[gameId]) { speedHistoryRef.current[gameId] = []; } @@ -596,7 +594,7 @@ export function DownloadGroup({ if (speedHistoryRef.current[gameId].length > 120) { speedHistoryRef.current[gameId].shift(); } - }, [lastPacket, dispatch]); + }, [lastPacket]); useEffect(() => { for (const game of library) { From 67ea9e78a2d5ba50bd588312be9af2d340971b41 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 13 Dec 2025 18:14:52 +0200 Subject: [PATCH 03/19] feat: enhance download tracking by adding speed history management in download slice --- src/renderer/src/features/download-slice.ts | 17 ++++- .../src/pages/downloads/download-group.tsx | 74 +++---------------- 2 files changed, 28 insertions(+), 63 deletions(-) diff --git a/src/renderer/src/features/download-slice.ts b/src/renderer/src/features/download-slice.ts index 80d78b0d..f70421c0 100644 --- a/src/renderer/src/features/download-slice.ts +++ b/src/renderer/src/features/download-slice.ts @@ -13,6 +13,7 @@ export interface DownloadState { gamesWithDeletionInProgress: string[]; extraction: ExtractionInfo | null; peakSpeeds: Record; + speedHistory: Record; } const initialState: DownloadState = { @@ -21,6 +22,7 @@ const initialState: DownloadState = { gamesWithDeletionInProgress: [], extraction: null, peakSpeeds: {}, + speedHistory: {}, }; export const downloadSlice = createSlice({ @@ -31,13 +33,25 @@ export const downloadSlice = createSlice({ state.lastPacket = action.payload; if (!state.gameId && action.payload) state.gameId = action.payload.gameId; - // Track peak speed atomically when packet arrives + // Track peak speed and speed history atomically when packet arrives if (action.payload?.gameId && action.payload.downloadSpeed != null) { const { gameId, downloadSpeed } = action.payload; + + // Update peak speed if this is higher const currentPeak = state.peakSpeeds[gameId] || 0; if (downloadSpeed > currentPeak) { state.peakSpeeds[gameId] = downloadSpeed; } + + // Update speed history for chart + if (!state.speedHistory[gameId]) { + state.speedHistory[gameId] = []; + } + state.speedHistory[gameId].push(downloadSpeed); + // Keep only last 120 entries + if (state.speedHistory[gameId].length > 120) { + state.speedHistory[gameId].shift(); + } } }, clearDownload: (state) => { @@ -85,6 +99,7 @@ export const downloadSlice = createSlice({ }, clearPeakSpeed: (state, action: PayloadAction) => { state.peakSpeeds[action.payload] = 0; + state.speedHistory[action.payload] = []; }, }, }); diff --git a/src/renderer/src/pages/downloads/download-group.tsx b/src/renderer/src/pages/downloads/download-group.tsx index 7c3f0f77..bf6584f2 100644 --- a/src/renderer/src/pages/downloads/download-group.tsx +++ b/src/renderer/src/pages/downloads/download-group.tsx @@ -11,12 +11,10 @@ import { addMilliseconds } from "date-fns"; import { DOWNLOADER_NAME } from "@renderer/constants"; import { useAppSelector, - useAppDispatch, useDownload, useLibrary, useDate, } from "@renderer/hooks"; -import { clearPeakSpeed } from "@renderer/features"; import "./download-group.scss"; import { useTranslation } from "react-i18next"; @@ -514,9 +512,9 @@ export function DownloadGroup({ const { formatDistance } = useDate(); - const dispatch = useAppDispatch(); + // Get speed history and peak speeds from Redux (centralized state) + const speedHistory = useAppSelector((state) => state.download.speedHistory); const peakSpeeds = useAppSelector((state) => state.download.peakSpeeds); - const speedHistoryRef = useRef>({}); const [dominantColors, setDominantColors] = useState>( {} ); @@ -579,62 +577,8 @@ export function DownloadGroup({ }); }, [library, lastPacket?.gameId]); - useEffect(() => { - if (!lastPacket?.gameId || lastPacket.downloadSpeed === undefined) return; - - const gameId = lastPacket.gameId; - const downloadSpeed = lastPacket.downloadSpeed; - - if (!speedHistoryRef.current[gameId]) { - speedHistoryRef.current[gameId] = []; - } - - speedHistoryRef.current[gameId].push(downloadSpeed); - - if (speedHistoryRef.current[gameId].length > 120) { - speedHistoryRef.current[gameId].shift(); - } - }, [lastPacket]); - - useEffect(() => { - for (const game of library) { - if ( - game.download && - game.download.progress < 0.01 && - game.download.status !== "paused" - ) { - // Fresh download - clear any old data - if (speedHistoryRef.current[game.id]?.length > 0) { - speedHistoryRef.current[game.id] = []; - dispatch(clearPeakSpeed(game.id)); - } - } - } - }, [library, dispatch]); - - useEffect(() => { - const timeouts: NodeJS.Timeout[] = []; - - for (const game of library) { - if ( - game.download?.progress === 1 && - speedHistoryRef.current[game.id]?.length > 0 - ) { - const gameId = game.id; - const timeout = setTimeout(() => { - speedHistoryRef.current[gameId] = []; - dispatch(clearPeakSpeed(gameId)); - }, 10_000); - timeouts.push(timeout); - } - } - - return () => { - for (const timeout of timeouts) { - clearTimeout(timeout); - } - }; - }, [library, dispatch]); + // Speed history and peak speeds are now tracked in Redux (in setLastPacket reducer) + // No local effect needed - data is updated atomically when packets arrive useEffect(() => { if (library.length > 0 && title === t("download_in_progress")) { @@ -839,7 +783,13 @@ export function DownloadGroup({ ? (lastPacket?.downloadSpeed ?? 0) : 0; const finalDownloadSize = getFinalDownloadSize(game); - const peakSpeed = peakSpeeds[game.id] || 0; + // Use lastPacket.gameId for lookup since that's the key used to store the data + // Fall back to game.id if lastPacket is not available + const dataKey = lastPacket?.gameId ?? game.id; + const gameSpeedHistory = speedHistory[dataKey] ?? []; + const storedPeak = peakSpeeds[dataKey]; + // Use stored peak if available and > 0, otherwise use current speed as initial value + const peakSpeed = storedPeak !== undefined && storedPeak > 0 ? storedPeak : downloadSpeed; let currentProgress = game.download?.progress || 0; if (isGameExtracting) { @@ -861,7 +811,7 @@ export function DownloadGroup({ currentProgress={currentProgress} dominantColor={dominantColor} lastPacket={lastPacket} - speedHistory={speedHistoryRef.current[game.id] || []} + speedHistory={gameSpeedHistory} formatSpeed={formatSpeed} calculateETA={calculateETA} pauseDownload={pauseDownload} From 78d2be85f2fc51b20267d8c39b1a1c1dc561e8ba Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 13 Dec 2025 18:15:21 +0200 Subject: [PATCH 04/19] style: format peak speed calculation for improved readability in DownloadGroup component --- src/renderer/src/pages/downloads/download-group.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/pages/downloads/download-group.tsx b/src/renderer/src/pages/downloads/download-group.tsx index bf6584f2..31d74d2e 100644 --- a/src/renderer/src/pages/downloads/download-group.tsx +++ b/src/renderer/src/pages/downloads/download-group.tsx @@ -789,7 +789,8 @@ export function DownloadGroup({ const gameSpeedHistory = speedHistory[dataKey] ?? []; const storedPeak = peakSpeeds[dataKey]; // Use stored peak if available and > 0, otherwise use current speed as initial value - const peakSpeed = storedPeak !== undefined && storedPeak > 0 ? storedPeak : downloadSpeed; + const peakSpeed = + storedPeak !== undefined && storedPeak > 0 ? storedPeak : downloadSpeed; let currentProgress = game.download?.progress || 0; if (isGameExtracting) { From 95a7bc2236feeb83fb320189a0f411e1a7002f89 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 13 Dec 2025 18:32:16 +0200 Subject: [PATCH 05/19] feat: add new translation keys for network and peak in English and Portuguese locales --- src/locales/en/translation.json | 4 +++- src/locales/pt-BR/translation.json | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 9be4ff26..85a44236 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -420,7 +420,9 @@ "delete_archive_title": "Would you like to delete {{fileName}}?", "delete_archive_description": "The file has been successfully extracted and it's no longer needed.", "yes": "Yes", - "no": "No" + "no": "No", + "network": "NETWORK", + "peak": "PEAK" }, "settings": { "downloads_path": "Downloads path", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index ee0da176..719f72f7 100755 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -408,7 +408,9 @@ "delete_archive_title": "Deseja deletar {{fileName}}?", "delete_archive_description": "O arquivo foi extraído com sucesso e não é mais necessário.", "yes": "Sim", - "no": "Não" + "no": "Não", + "network": "REDE", + "peak": "PICO" }, "settings": { "downloads_path": "Diretório dos downloads", From 142bd3156c3a045a547e9fa9045e1d36c5400d6d Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sun, 14 Dec 2025 11:27:29 +0200 Subject: [PATCH 06/19] fix: ensure downloader value is properly checked and converted to number in DownloadGroup component --- src/renderer/src/pages/downloads/download-group.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/pages/downloads/download-group.tsx b/src/renderer/src/pages/downloads/download-group.tsx index 31d74d2e..6a22148a 100644 --- a/src/renderer/src/pages/downloads/download-group.tsx +++ b/src/renderer/src/pages/downloads/download-group.tsx @@ -412,10 +412,12 @@ function HeroDownloadView({ )} - {game.download?.downloader && ( + {game.download?.downloader !== undefined && (
- {DOWNLOADER_NAME[game.download.downloader]} + + {DOWNLOADER_NAME[Number(game.download.downloader)]} +
)} @@ -856,7 +858,9 @@ export function DownloadGroup({
- {DOWNLOADER_NAME[game.download!.downloader]} + + {DOWNLOADER_NAME[Number(game.download!.downloader)]} +
{extraction?.visibleId === game.id ? ( From cc5e0014f76a249ed32097f8b94528d3a5add20d Mon Sep 17 00:00:00 2001 From: "Kiwo.2" Date: Sun, 14 Dec 2025 15:04:51 +0100 Subject: [PATCH 07/19] Re-written some parts, Adjusted to new version --- src/locales/hu/translation.json | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/locales/hu/translation.json b/src/locales/hu/translation.json index c2a59873..7decc890 100644 --- a/src/locales/hu/translation.json +++ b/src/locales/hu/translation.json @@ -22,7 +22,7 @@ "downloading": "{{title}} ({{percentage}} - Letöltés…)", "filter": "Könyvtár szűrése", "home": "Főoldal", - "queued": "A(z) {{title}} (Várakozósorban van)", + "queued": "{{title}} (Várakozásban)", "game_has_no_executable": "A játékhoz nincs tallózva futtatható fájl", "sign_in": "Bejelentkezés", "friends": "Barátok", @@ -94,6 +94,12 @@ "header": { "search": "Keresés", "search_library": "Könyvtár böngészése", + "recent_searches": "Korábbi Keresések", + "suggestions": "Találatok", + "clear_history": "Törlés", + "remove_from_history": "Törlés az előzményekből", + "loading": "Töltés...", + "no_results": "Nincs találat", "home": "Főoldal", "catalogue": "Katalógus", "library": "Könyvtár", @@ -109,6 +115,7 @@ "downloading": "{{title}} letöltése… ({{percentage}} kész) - Befejezés {{eta}} - {{speed}}", "calculating_eta": "{{title}} letöltése… ({{percentage}} kész) - Hátralévő idő…", "checking_files": "A(z) {{title}} fájljaiból… ({{percentage}} kész)", + "extracting": "{{title}} kicsomagolása… ({{percentage}} kicsomagolva)", "installing_common_redist": "{{log}}…", "installation_complete": "Telepítés befejezve", "installation_complete_message": "A(z) Alapvető segédprogramok sikeresen telepítve" @@ -165,7 +172,7 @@ "playing_now": "Játékban: ", "change": "Változtatás", "repacks_modal_description": "Válaszd ki a repacket amit leszeretnél tölteni", - "select_folder_hint": "A letöltési mappát a <0>Beállítások menüjében változtathatod meg", + "select_folder_hint": "A letöltési mappát a <0>Beállításokban változtathatod meg", "download_now": "Letöltés", "no_shop_details": "A bolt adatai nem érhetőek el.", "download_options": "Letöltési opciók", @@ -196,6 +203,7 @@ "danger_zone_section_description": "Itt eltávolítható a játék a könyvtáradból, vagy a fájlok amelyek a Hydra által lettek letöltve", "download_in_progress": "Letöltés folyamatban", "download_paused": "Letöltés szüneteltetve", + "extracting": "Kicsomagolás", "last_downloaded_option": "Utoljára letöltött", "new_download_option": "Új", "create_steam_shortcut": "Steam parancsikon létrehozása", @@ -397,7 +405,7 @@ "delete_modal_description": "Ez eltávolítja a telepítési fájlokat a számítógépedről", "install": "Telepít", "download_in_progress": "Folyamatban lévő", - "queued_downloads": "Várakozósoron lévő letöltések", + "queued_downloads": "Várakozásban lévő letöltések", "downloads_completed": "Befejezett", "queued": "Várakozásban", "no_downloads_title": "Oly üres..", @@ -409,6 +417,10 @@ "options": "Kezelés", "extract": "Fájlok kibontása", "extracting": "Fájlok kibontása…" + "delete_archive_title": "Szeretnéd törölni ezt a fájlt? {{fileName}}", + "delete_archive_description": "A tömörített fájl ki lett csomagolva, s többé nincs rá szükség. ", + "yes": "Igen", + "no": "Nem" }, "settings": { "downloads_path": "Letöltési útvonalak", @@ -669,7 +681,7 @@ "no_blocked_users": "Nincs letiltott felhasználó", "friend_code_copied": "Barát kód kimásolva", "undo_friendship_modal_text": "Ezáltal megszünteted a barátságod vele: {{displayName}}", - "privacy_hint": "Hogy beállítsd ki láthassa ezt, menj a <0>Beállítások menüjébe", + "privacy_hint": "Hogy beállítsd ki láthassa ezt, menj a <0>Beállításokba", "locked_profile": "Ez a profil privát", "image_process_failure": "Hiba a kép feldolgozása közben", "required_field": "Ez a mező kötelező", From 214267df7e206cd584d6489da28d706608357709 Mon Sep 17 00:00:00 2001 From: "Kiwo.2" Date: Sun, 14 Dec 2025 15:09:40 +0100 Subject: [PATCH 08/19] Re-written some parts, Adjusted to new version --- src/locales/hu/translation.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/locales/hu/translation.json b/src/locales/hu/translation.json index 7decc890..61d4cdc9 100644 --- a/src/locales/hu/translation.json +++ b/src/locales/hu/translation.json @@ -94,7 +94,7 @@ "header": { "search": "Keresés", "search_library": "Könyvtár böngészése", - "recent_searches": "Korábbi Keresések", + "recent_searches": "Korábbi Keresések", "suggestions": "Találatok", "clear_history": "Törlés", "remove_from_history": "Törlés az előzményekből", @@ -420,7 +420,7 @@ "delete_archive_title": "Szeretnéd törölni ezt a fájlt? {{fileName}}", "delete_archive_description": "A tömörített fájl ki lett csomagolva, s többé nincs rá szükség. ", "yes": "Igen", - "no": "Nem" + "no": "Nem" }, "settings": { "downloads_path": "Letöltési útvonalak", From d5e6bed3b7bb682c351505cea99eaa641cbfdb2d Mon Sep 17 00:00:00 2001 From: "Kiwo.2" Date: Sun, 14 Dec 2025 15:58:27 +0100 Subject: [PATCH 09/19] Re-written some parts, Adjusted to new version --- src/locales/hu/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/hu/translation.json b/src/locales/hu/translation.json index 61d4cdc9..b83fec51 100644 --- a/src/locales/hu/translation.json +++ b/src/locales/hu/translation.json @@ -416,7 +416,7 @@ "resume_seeding": "Seedelés folytatása", "options": "Kezelés", "extract": "Fájlok kibontása", - "extracting": "Fájlok kibontása…" + "extracting": "Fájlok kibontása…", "delete_archive_title": "Szeretnéd törölni ezt a fájlt? {{fileName}}", "delete_archive_description": "A tömörített fájl ki lett csomagolva, s többé nincs rá szükség. ", "yes": "Igen", From affa7a2b2e3dd4708b0f9b0905af7178a2bf3205 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Tue, 16 Dec 2025 15:42:34 +0200 Subject: [PATCH 10/19] feat: add fullscreen media modal to profile hero for avatar display --- proto | 2 +- .../fullscreen-media-modal.scss | 61 +++++++++++++ .../fullscreen-media-modal.tsx | 88 +++++++++++++++++++ src/renderer/src/components/index.ts | 1 + .../profile/profile-hero/profile-hero.tsx | 21 ++++- 5 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.scss create mode 100644 src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.tsx diff --git a/proto b/proto index 7a23620f..6f11c99c 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 7a23620f930f6fbb84c0abcaab5149a34ab4b4eb +Subproject commit 6f11c99c572420a282ba5149b6866e39b8a4569c diff --git a/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.scss b/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.scss new file mode 100644 index 00000000..454653b8 --- /dev/null +++ b/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.scss @@ -0,0 +1,61 @@ +@use "../../scss/globals.scss"; + +.fullscreen-media-modal { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + position: relative; + background-color: rgba(0, 0, 0, 0.5); + + &__close-button { + position: absolute; + top: calc(globals.$spacing-unit * 4); + right: calc(globals.$spacing-unit * 3); + cursor: pointer; + color: globals.$body-color; + background-color: rgba(0, 0, 0, 0.5); + border-radius: 50%; + border: 1px solid globals.$border-color; + padding: globals.$spacing-unit; + display: flex; + align-items: center; + justify-content: center; + transition: all ease 0.2s; + z-index: 10; + + &:hover { + background-color: rgba(0, 0, 0, 0.8); + transform: scale(1.1); + } + } + + &__image-container { + max-width: 90%; + max-height: 90%; + display: flex; + justify-content: center; + align-items: center; + } + + &__image { + max-width: 100%; + max-height: 60vh; + object-fit: contain; + border-radius: 8px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); + animation: image-appear 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; + } +} + +@keyframes image-appear { + 0% { + opacity: 0; + transform: scale(0.85); + } + 100% { + opacity: 1; + transform: scale(1); + } +} diff --git a/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.tsx b/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.tsx new file mode 100644 index 00000000..12052834 --- /dev/null +++ b/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.tsx @@ -0,0 +1,88 @@ +import { useCallback, useEffect, useRef } from "react"; +import { createPortal } from "react-dom"; +import { XIcon } from "@primer/octicons-react"; +import { useTranslation } from "react-i18next"; + +import { Backdrop } from "../backdrop/backdrop"; +import "./fullscreen-media-modal.scss"; + +export interface FullscreenMediaModalProps { + visible: boolean; + onClose: () => void; + src: string | null | undefined; + alt?: string; +} + +export function FullscreenMediaModal({ + visible, + onClose, + src, + alt, +}: FullscreenMediaModalProps) { + const containerRef = useRef(null); + + const { t } = useTranslation("modal"); + + useEffect(() => { + if (visible) { + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + onClose(); + } + }; + + window.addEventListener("keydown", onKeyDown); + + return () => { + window.removeEventListener("keydown", onKeyDown); + }; + } + + return () => {}; + }, [onClose, visible]); + + useEffect(() => { + const onMouseDown = (e: MouseEvent) => { + if (containerRef.current) { + const clickedOnImage = containerRef.current.contains(e.target as Node); + + if (!clickedOnImage) { + onClose(); + } + } + }; + + if (visible) { + window.addEventListener("mousedown", onMouseDown); + } + + return () => { + window.removeEventListener("mousedown", onMouseDown); + }; + }, [onClose, visible]); + + if (!visible || !src) return null; + + return createPortal( + +
+ + +
+ {alt} +
+
+
, + document.body + ); +} diff --git a/src/renderer/src/components/index.ts b/src/renderer/src/components/index.ts index e8876fcb..8bb028bd 100644 --- a/src/renderer/src/components/index.ts +++ b/src/renderer/src/components/index.ts @@ -20,3 +20,4 @@ export * from "./game-context-menu/game-context-menu"; export * from "./game-context-menu/use-game-actions"; export * from "./star-rating/star-rating"; export * from "./search-dropdown/search-dropdown"; +export * from "./fullscreen-media-modal/fullscreen-media-modal"; diff --git a/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx b/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx index fc354d01..f9109067 100644 --- a/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx +++ b/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx @@ -9,7 +9,12 @@ import { XCircleFillIcon, } from "@primer/octicons-react"; import { buildGameDetailsPath } from "@renderer/helpers"; -import { Avatar, Button, Link } from "@renderer/components"; +import { + Avatar, + Button, + FullscreenMediaModal, + Link, +} from "@renderer/components"; import { useTranslation } from "react-i18next"; import { useAppSelector, @@ -33,6 +38,7 @@ type FriendAction = export function ProfileHero() { const [showEditProfileModal, setShowEditProfileModal] = useState(false); + const [showFullscreenAvatar, setShowFullscreenAvatar] = useState(false); const [isPerformingAction, setIsPerformingAction] = useState(false); const { @@ -246,10 +252,12 @@ export function ProfileHero() { ]); const handleAvatarClick = useCallback(() => { - if (isMe) { + if (userProfile?.profileImageUrl) { + setShowFullscreenAvatar(true); + } else if (isMe) { setShowEditProfileModal(true); } - }, [isMe]); + }, [isMe, userProfile?.profileImageUrl]); const currentGame = useMemo(() => { if (isMe) { @@ -272,6 +280,13 @@ export function ProfileHero() { onClose={() => setShowEditProfileModal(false)} /> + setShowFullscreenAvatar(false)} + src={userProfile?.profileImageUrl} + alt={userProfile?.displayName} + /> +
Date: Tue, 16 Dec 2025 15:43:43 +0200 Subject: [PATCH 11/19] refactor(fullscreen-media-modal): remove unused useCallback import --- .../fullscreen-media-modal/fullscreen-media-modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.tsx b/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.tsx index 12052834..2d2abb6d 100644 --- a/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.tsx +++ b/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef } from "react"; +import { useEffect, useRef } from "react"; import { createPortal } from "react-dom"; import { XIcon } from "@primer/octicons-react"; import { useTranslation } from "react-i18next"; From 1524e73ee6d901858b27ab938c7c75c6f64aaa01 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Tue, 16 Dec 2025 15:49:10 +0200 Subject: [PATCH 12/19] refactor(fullscreen-media-modal): replace div with dialog element for improved semantics --- .../fullscreen-media-modal/fullscreen-media-modal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.tsx b/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.tsx index 2d2abb6d..700bddb1 100644 --- a/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.tsx +++ b/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.tsx @@ -65,7 +65,7 @@ export function FullscreenMediaModal({ return createPortal( -
+
-
+ , document.body ); From 677f34fe3d632cdd2aaef0253c35023468e4254d Mon Sep 17 00:00:00 2001 From: moi952 <52632400+moi952@users.noreply.github.com> Date: Tue, 16 Dec 2025 22:41:53 +0100 Subject: [PATCH 13/19] feat: add missing French translations --- src/locales/fr/translation.json | 226 +++++++++++++++++++++++++++++++- 1 file changed, 220 insertions(+), 6 deletions(-) diff --git a/src/locales/fr/translation.json b/src/locales/fr/translation.json index 8fc07722..48bf6086 100644 --- a/src/locales/fr/translation.json +++ b/src/locales/fr/translation.json @@ -27,7 +27,69 @@ "friends": "Amis", "need_help": "Besoin d'aide ?", "favorites": "Favoris", - "playable_button_title": "Afficher uniquement les jeux que vous pouvez jouer maintenant" + "playable_button_title": "Afficher uniquement les jeux que vous pouvez jouer maintenant", + "library": "Bibliothèque", + "add_custom_game_tooltip": "Ajouter un jeu personnalisé", + "show_playable_only_tooltip": "Afficher uniquement les jeux jouables", + "custom_game_modal": "Ajouter un jeu personnalisé", + "custom_game_modal_description": "Ajoutez un jeu personnalisé à votre bibliothèque en sélectionnant un fichier exécutable", + "custom_game_modal_executable_path": "Chemin de l'exécutable", + "custom_game_modal_select_executable": "Sélectionner un fichier exécutable", + "custom_game_modal_title": "Titre", + "custom_game_modal_enter_title": "Entrer le titre", + "custom_game_modal_browse": "Parcourir", + "custom_game_modal_cancel": "Annuler", + "custom_game_modal_add": "Ajouter le jeu", + "custom_game_modal_adding": "Ajout du jeu…", + "custom_game_modal_success": "Jeu personnalisé ajouté avec succès", + "custom_game_modal_failed": "Échec de l’ajout du jeu personnalisé", + "custom_game_modal_executable": "Exécutable", + "edit_game_modal": "Personnaliser les ressources", + "edit_game_modal_description": "Personnalisez les ressources et les détails du jeu", + "edit_game_modal_title": "Titre", + "edit_game_modal_enter_title": "Entrer le titre", + "edit_game_modal_image": "Image", + "edit_game_modal_select_image": "Sélectionner une image", + "edit_game_modal_browse": "Parcourir", + "edit_game_modal_image_preview": "Aperçu de l’image", + "edit_game_modal_icon": "Icône", + "edit_game_modal_select_icon": "Sélectionner une icône", + "edit_game_modal_icon_preview": "Aperçu de l’icône", + "edit_game_modal_logo": "Logo", + "edit_game_modal_select_logo": "Sélectionner un logo", + "edit_game_modal_logo_preview": "Aperçu du logo", + "edit_game_modal_hero": "Bannière de la bibliothèque", + "edit_game_modal_select_hero": "Sélectionner l’image de bannière", + "edit_game_modal_hero_preview": "Aperçu de la bannière", + "edit_game_modal_cancel": "Annuler", + "edit_game_modal_update": "Mettre à jour", + "edit_game_modal_updating": "Mise à jour…", + "edit_game_modal_fill_required": "Veuillez remplir tous les champs requis", + "edit_game_modal_success": "Ressources mises à jour avec succès", + "edit_game_modal_failed": "Échec de la mise à jour des ressources", + "edit_game_modal_image_filter": "Image", + "edit_game_modal_icon_resolution": "Résolution recommandée : 256x256px", + "edit_game_modal_logo_resolution": "Résolution recommandée : 640x360px", + "edit_game_modal_hero_resolution": "Résolution recommandée : 1920x620px", + "edit_game_modal_assets": "Ressources", + "edit_game_modal_drop_icon_image_here": "Déposez l’image de l’icône ici", + "edit_game_modal_drop_logo_image_here": "Déposez l’image du logo ici", + "edit_game_modal_drop_hero_image_here": "Déposez l’image de la bannière ici", + "edit_game_modal_drop_to_replace_icon": "Déposez pour remplacer l’icône", + "edit_game_modal_drop_to_replace_logo": "Déposez pour remplacer le logo", + "edit_game_modal_drop_to_replace_hero": "Déposez pour remplacer la bannière", + "install_decky_plugin": "Installer le plugin Decky", + "update_decky_plugin": "Mettre à jour le plugin Decky", + "decky_plugin_installed_version": "Plugin Decky (v{{version}})", + "install_decky_plugin_title": "Installer le plugin Decky Hydra", + "install_decky_plugin_message": "Cela téléchargera et installera le plugin Hydra pour Decky Loader. Des permissions élevées peuvent être requises. Continuer ?", + "update_decky_plugin_title": "Mettre à jour le plugin Decky Hydra", + "update_decky_plugin_message": "Une nouvelle version du plugin Decky Hydra est disponible. Souhaitez-vous la mettre à jour maintenant ?", + "decky_plugin_installed": "Plugin Decky v{{version}} installé avec succès", + "decky_plugin_installation_failed": "Échec de l’installation du plugin Decky : {{error}}", + "decky_plugin_installation_error": "Erreur lors de l’installation du plugin Decky : {{error}}", + "confirm": "Confirmer", + "cancel": "Annuler" }, "header": { "search": "Rechercher", @@ -37,7 +99,15 @@ "search_results": "Résultats de la recherche", "settings": "Paramètres", "version_available_install": "Version {{version}} disponible. Cliquez ici pour redémarrer et installer.", - "version_available_download": "Version {{version}} disponible. Cliquez ici pour télécharger." + "version_available_download": "Version {{version}} disponible. Cliquez ici pour télécharger.", + "search_library": "Rechercher dans la bibliothèque", + "recent_searches": "Recherches récentes", + "suggestions": "Suggestions", + "clear_history": "Effacer", + "remove_from_history": "Supprimer de l'historique", + "loading": "Chargement…", + "no_results": "Aucun résultat", + "library": "Bibliothèque" }, "bottom_panel": { "no_downloads_in_progress": "Aucun téléchargement en cours", @@ -47,7 +117,8 @@ "checking_files": "Vérification des fichiers de {{title}}… ({{percentage}} terminé)", "installing_common_redist": "{{log}}…", "installation_complete": "Installation terminée", - "installation_complete_message": "Redistribuables communs installés avec succès" + "installation_complete_message": "Redistribuables communs installés avec succès", + "extracting": "Extraction de {{title}}… ({{percentage}} terminé)" }, "catalogue": { "search": "Filtrer…", @@ -198,7 +269,113 @@ "download_error_not_cached_on_hydra": "Ce téléchargement n'est pas disponible sur Nimbus.", "game_removed_from_favorites": "Jeu retiré des favoris", "game_added_to_favorites": "Jeu ajouté aux favoris", - "automatically_extract_downloaded_files": "Extraire automatiquement les fichiers téléchargés" + "automatically_extract_downloaded_files": "Extraire automatiquement les fichiers téléchargés", + "already_in_library": "Déjà dans la bibliothèque", + "create_shortcut_simple": "Créer un raccourci", + "properties": "Propriétés", + "extracting": "Extraction en cours", + "new_download_option": "Nouveau", + "create_steam_shortcut": "Créer un raccourci Steam", + "you_might_need_to_restart_steam": "Vous devrez peut-être redémarrer Steam pour voir les changements", + "add_to_favorites": "Ajouter aux favoris", + "remove_from_favorites": "Retirer des favoris", + "failed_update_favorites": "Échec de la mise à jour des favoris", + "game_removed_from_library": "Jeu retiré de la bibliothèque", + "failed_remove_from_library": "Échec de la suppression du jeu de la bibliothèque", + "files_removed_success": "Fichiers supprimés avec succès", + "failed_remove_files": "Échec de la suppression des fichiers", + "rating_count": "Évaluations", + "show_more": "Afficher plus", + "show_less": "Afficher moins", + "reviews": "Avis", + "review_played_for": "Temps de jeu", + "leave_a_review": "Laisser un avis", + "write_review_placeholder": "Partagez votre avis sur ce jeu…", + "sort_newest": "Les plus récents", + "sort_oldest": "Les plus anciens", + "sort_highest_score": "Meilleure note", + "sort_lowest_score": "Note la plus basse", + "sort_most_voted": "Les plus votés", + "no_reviews_yet": "Aucun avis pour le moment", + "be_first_to_review": "Soyez le premier à donner votre avis !", + "rating": "Note", + "rating_stats": "Évaluation", + "rating_very_negative": "Très négatif", + "rating_negative": "Négatif", + "rating_neutral": "Neutre", + "rating_positive": "Positif", + "rating_very_positive": "Très positif", + "submit_review": "Envoyer", + "submitting": "Envoi…", + "review_submitted_successfully": "Avis envoyé avec succès !", + "review_submission_failed": "Échec de l’envoi de l’avis. Veuillez réessayer.", + "review_cannot_be_empty": "Le champ de l’avis ne peut pas être vide.", + "review_deleted_successfully": "Avis supprimé avec succès.", + "review_deletion_failed": "Échec de la suppression de l’avis.", + "loading_reviews": "Chargement des avis…", + "loading_more_reviews": "Chargement de plus d’avis…", + "load_more_reviews": "Charger plus d’avis", + "you_seemed_to_enjoy_this_game": "Vous semblez avoir apprécié ce jeu", + "would_you_recommend_this_game": "Souhaitez-vous laisser un avis sur ce jeu ?", + "yes": "Oui", + "maybe_later": "Peut-être plus tard", + "backup_failed": "Échec de la sauvegarde", + "update_playtime_title": "Mettre à jour le temps de jeu", + "update_playtime_description": "Mettre à jour manuellement le temps de jeu pour {{game}}", + "update_playtime": "Mettre à jour le temps de jeu", + "update_playtime_success": "Temps de jeu mis à jour avec succès", + "update_playtime_error": "Échec de la mise à jour du temps de jeu", + "update_game_playtime": "Mettre à jour le temps de jeu", + "manual_playtime_warning": "Vos heures seront marquées comme modifiées manuellement et cela ne peut pas être annulé.", + "manual_playtime_tooltip": "Ce temps de jeu a été modifié manuellement", + "game_removed_from_pinned": "Jeu retiré des épinglés", + "game_added_to_pinned": "Jeu ajouté aux épinglés", + "create_start_menu_shortcut": "Créer un raccourci dans le menu Démarrer", + "invalid_wine_prefix_path": "Chemin du préfixe Wine invalide", + "invalid_wine_prefix_path_description": "Le chemin du préfixe Wine est invalide. Veuillez vérifier et réessayer.", + "missing_wine_prefix": "Un préfixe Wine est requis pour créer une sauvegarde sous Linux", + "artifact_renamed": "Sauvegarde renommée avec succès", + "rename_artifact": "Renommer la sauvegarde", + "rename_artifact_description": "Renommez la sauvegarde avec un nom plus descriptif", + "artifact_name_label": "Nom de la sauvegarde", + "artifact_name_placeholder": "Entrez un nom pour la sauvegarde", + "save_changes": "Enregistrer les modifications", + "required_field": "Ce champ est requis", + "max_length_field": "Ce champ doit contenir moins de {{length}} caractères", + "freeze_backup": "Épingler pour éviter l’écrasement automatique", + "unfreeze_backup": "Désépingler", + "backup_frozen": "Sauvegarde épinglée", + "backup_unfrozen": "Sauvegarde désépinglée", + "backup_freeze_failed": "Échec de l’épinglage de la sauvegarde", + "backup_freeze_failed_description": "Vous devez laisser au moins un emplacement libre pour les sauvegardes automatiques", + "edit_game_modal_button": "Personnaliser les ressources du jeu", + "game_details": "Détails du jeu", + "prices": "Prix", + "no_prices_found": "Aucun prix trouvé", + "view_all_prices": "Cliquer pour voir tous les prix", + "retail_price": "Prix officiel", + "keyshop_price": "Prix Keyshop", + "historical_retail": "Historique officiel", + "historical_keyshop": "Historique Keyshop", + "language": "Langue", + "caption": "Sous-titres", + "audio": "Audio", + "filter_by_source": "Filtrer par source", + "no_repacks_found": "Aucune source trouvée pour ce jeu", + "delete_review": "Supprimer l’avis", + "remove_review": "Retirer l’avis", + "delete_review_modal_title": "Voulez-vous vraiment supprimer votre avis ?", + "delete_review_modal_description": "Cette action est irréversible.", + "delete_review_modal_delete_button": "Supprimer", + "delete_review_modal_cancel_button": "Annuler", + "vote_failed": "Échec de l’enregistrement de votre vote. Veuillez réessayer.", + "show_original": "Afficher l’original", + "show_translation": "Afficher la traduction", + "show_original_translated_from": "Afficher l’original (traduit depuis {{language}})", + "hide_original": "Masquer l’original", + "review_from_blocked_user": "Avis d’un utilisateur bloqué", + "show": "Afficher", + "hide": "Masquer" }, "activation": { "title": "Activer Hydra", @@ -237,7 +414,11 @@ "resume_seeding": "Reprendre le partage", "options": "Gérer", "extract": "Extraire les fichiers", - "extracting": "Extraction des fichiers…" + "extracting": "Extraction des fichiers…", + "delete_archive_title": "Voulez-vous supprimer {{fileName}} ?", + "delete_archive_description": "Le fichier a été extrait avec succès et n’est plus nécessaire.", + "yes": "Oui", + "no": "Non" }, "settings": { "downloads_path": "Chemin des téléchargements", @@ -366,7 +547,40 @@ "bottom-left": "En bas à gauche", "bottom-center": "En bas au centre", "bottom-right": "En bas à droite", - "enable_friend_start_game_notifications": "Quand un ami commence à jouer à un jeu" + "enable_friend_start_game_notifications": "Quand un ami commence à jouer à un jeu", + "adding": "Ajout…", + "failed_add_download_source": "Échec de l’ajout de la source de téléchargement. Veuillez réessayer.", + "download_source_already_exists": "Cette URL de source existe déjà", + "download_source_pending_matching": "Mise à jour imminente", + "download_source_matched": "À jour", + "download_source_matching": "Mise à jour", + "download_source_failed": "Erreur", + "download_source_no_information": "Aucune information disponible", + "removed_all_download_sources": "Toutes les sources de téléchargement supprimées", + "download_sources_synced_successfully": "Toutes les sources de téléchargement ont été synchronisées", + "importing": "Importation…", + "hydra_cloud": "Hydra Cloud", + "debrid": "Debrid", + "enable_steam_achievements": "Activer la recherche de succès Steam", + "alignment": "Alignement", + "variation": "Variation", + "default": "Par défaut", + "rare": "Rare", + "platinum": "Platine", + "hidden": "Caché", + "test_notification": "Notification de test", + "achievement_sound_volume": "Volume du son de succès", + "select_achievement_sound": "Sélectionner un son de succès", + "change_achievement_sound": "Changer le son de succès", + "remove_achievement_sound": "Supprimer le son de succès", + "preview_sound": "Prévisualiser le son", + "select": "Sélectionner", + "preview": "Aperçu", + "remove": "Supprimer", + "no_sound_file_selected": "Aucun fichier sonore sélectionné", + "notification_preview": "Aperçu de la notification de succès", + "autoplay_trailers_on_game_page": "Lire automatiquement les bandes-annonces sur la page du jeu", + "hide_to_tray_on_game_start": "Réduire Hydra dans la barre système au lancement d’un jeu" }, "notifications": { "download_complete": "Téléchargement terminé", From 24d65b50b432405358d9659d03a304ba58f7ad3b Mon Sep 17 00:00:00 2001 From: Moyasee Date: Thu, 18 Dec 2025 22:56:50 +0200 Subject: [PATCH 14/19] style: enhance overlay styling and adjust close button positioning --- .../fullscreen-media-modal.scss | 27 ++++++++++++++++--- .../fullscreen-media-modal.tsx | 5 ++-- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.scss b/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.scss index 454653b8..d46958ff 100644 --- a/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.scss +++ b/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.scss @@ -1,5 +1,21 @@ @use "../../scss/globals.scss"; +.fullscreen-media-modal__overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(2px); + z-index: globals.$backdrop-z-index; + display: flex; + justify-content: center; + align-items: center; + padding: 0; + margin: 0; +} + .fullscreen-media-modal { display: flex; justify-content: center; @@ -7,12 +23,17 @@ width: 100%; height: 100%; position: relative; - background-color: rgba(0, 0, 0, 0.5); + margin: 0; + padding: 0; + border: none; + background: transparent; + max-width: none; + max-height: none; &__close-button { position: absolute; - top: calc(globals.$spacing-unit * 4); - right: calc(globals.$spacing-unit * 3); + top: calc(globals.$spacing-unit * 5); + right: calc(globals.$spacing-unit * 4); cursor: pointer; color: globals.$body-color; background-color: rgba(0, 0, 0, 0.5); diff --git a/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.tsx b/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.tsx index 700bddb1..f3722154 100644 --- a/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.tsx +++ b/src/renderer/src/components/fullscreen-media-modal/fullscreen-media-modal.tsx @@ -3,7 +3,6 @@ import { createPortal } from "react-dom"; import { XIcon } from "@primer/octicons-react"; import { useTranslation } from "react-i18next"; -import { Backdrop } from "../backdrop/backdrop"; import "./fullscreen-media-modal.scss"; export interface FullscreenMediaModalProps { @@ -64,7 +63,7 @@ export function FullscreenMediaModal({ if (!visible || !src) return null; return createPortal( - +
-
, +
, document.body ); } From f8ac284bc2d2601b3abf1b2a1b5f22e1da817315 Mon Sep 17 00:00:00 2001 From: Wkeynhk Date: Thu, 25 Dec 2025 14:16:11 +0300 Subject: [PATCH 15/19] New hosters --- .../events/torrenting/start-game-download.ts | 37 ++++- .../services/download/download-manager.ts | 131 ++++++++++++++---- src/main/services/hosters/buzzheavier.ts | 82 +++++++++++ src/main/services/hosters/fuckingfast.ts | 129 +++++++++++++++++ src/main/services/hosters/index.ts | 2 + src/renderer/src/constants.ts | 2 + src/shared/constants.ts | 2 + src/shared/index.ts | 10 ++ 8 files changed, 365 insertions(+), 30 deletions(-) create mode 100644 src/main/services/hosters/buzzheavier.ts create mode 100644 src/main/services/hosters/fuckingfast.ts diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts index 4375698f..940f3025 100644 --- a/src/main/events/torrenting/start-game-download.ts +++ b/src/main/events/torrenting/start-game-download.ts @@ -82,7 +82,6 @@ const startGameDownload = async ( queued: true, extracting: false, automaticallyExtract, - extractionProgress: 0, }; try { @@ -124,6 +123,42 @@ const startGameDownload = async ( } if (err instanceof Error) { + if (downloader === Downloader.Buzzheavier) { + if (err.message.includes("Rate limit")) { + return { + ok: false, + error: "Buzzheavier: Rate limit exceeded", + }; + } + if ( + err.message.includes("not found") || + err.message.includes("deleted") + ) { + return { + ok: false, + error: "Buzzheavier: File not found", + }; + } + } + + if (downloader === Downloader.FuckingFast) { + if (err.message.includes("Rate limit")) { + return { + ok: false, + error: "FuckingFast: Rate limit exceeded", + }; + } + if ( + err.message.includes("not found") || + err.message.includes("deleted") + ) { + return { + ok: false, + error: "FuckingFast: File not found", + }; + } + } + return { ok: false, error: err.message }; } diff --git a/src/main/services/download/download-manager.ts b/src/main/services/download/download-manager.ts index c208fa32..ce06e5e4 100644 --- a/src/main/services/download/download-manager.ts +++ b/src/main/services/download/download-manager.ts @@ -20,14 +20,47 @@ import { RealDebridClient } from "./real-debrid"; import path from "path"; import { logger } from "../logger"; import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level"; -import { orderBy } from "lodash-es"; +import { sortBy } from "lodash-es"; import { TorBoxClient } from "./torbox"; import { GameFilesManager } from "../game-files-manager"; import { HydraDebridClient } from "./hydra-debrid"; +import { BuzzheavierApi, FuckingFastApi } from "@main/services/hosters"; export class DownloadManager { private static downloadingGameId: string | null = null; + private static extractFilename(url: string, originalUrl?: string): string | undefined { + if (originalUrl && originalUrl.includes('#')) { + const hashPart = originalUrl.split('#')[1]; + if (hashPart && !hashPart.startsWith('http')) { + return hashPart; + } + } + + if (url.includes('#')) { + const hashPart = url.split('#')[1]; + if (hashPart && !hashPart.startsWith('http')) { + return hashPart; + } + } + + try { + const urlObj = new URL(url); + const pathname = urlObj.pathname; + const filename = pathname.split('/').pop(); + if (filename && filename.length > 0) { + return filename; + } + } catch { + } + + return undefined; + } + + private static sanitizeFilename(filename: string): string { + return filename.replace(/[<>:"/\\|?*]/g, '_'); + } + public static async startRPC( download?: Download, downloadsToSeed?: Download[] @@ -126,10 +159,21 @@ export class DownloadManager { } ); + if (WindowManager.mainWindow && download) { + WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress); + WindowManager.mainWindow.webContents.send( + "on-download-progress", + JSON.parse( + JSON.stringify({ + ...status, + game, + }) + ) + ); + } + const shouldExtract = download.automaticallyExtract; - // Handle download completion BEFORE sending progress to renderer - // This ensures extraction starts and DB is updated before UI reacts if (progress === 1 && download) { publishDownloadCompleteNotification(game); @@ -143,7 +187,6 @@ export class DownloadManager { shouldSeed: true, queued: false, extracting: shouldExtract, - extractionProgress: shouldExtract ? 0 : download.extractionProgress, }); } else { await downloadsSublevel.put(gameId, { @@ -152,22 +195,12 @@ export class DownloadManager { shouldSeed: false, queued: false, extracting: shouldExtract, - extractionProgress: shouldExtract ? 0 : download.extractionProgress, }); this.cancelDownload(gameId); } if (shouldExtract) { - // Send initial extraction progress BEFORE download progress - // This ensures the UI shows extraction immediately - WindowManager.mainWindow?.webContents.send( - "on-extraction-progress", - game.shop, - game.objectId, - 0 - ); - const gameFilesManager = new GameFilesManager( game.shop, game.objectId @@ -194,10 +227,10 @@ export class DownloadManager { .values() .all() .then((games) => { - return orderBy( + return sortBy( games.filter((game) => game.status === "paused" && game.queued), "timestamp", - "desc" + "DESC" ); }); @@ -209,18 +242,6 @@ export class DownloadManager { this.downloadingGameId = null; } } - - // Send progress to renderer after completion handling - if (WindowManager.mainWindow && download) { - WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress); - WindowManager.mainWindow.webContents.send( - "on-download-progress", - structuredClone({ - ...status, - game, - }) - ); - } } } @@ -360,6 +381,58 @@ export class DownloadManager { save_path: download.downloadPath, }; } + case Downloader.Buzzheavier: { + logger.log(`[DownloadManager] Processing Buzzheavier download for URI: ${download.uri}`); + try { + const directUrl = await BuzzheavierApi.getDirectLink(download.uri); + logger.log(`[DownloadManager] Buzzheavier direct URL obtained`); + + const filename = this.extractFilename(directUrl, download.uri); + const sanitizedFilename = filename ? this.sanitizeFilename(filename) : undefined; + + if (sanitizedFilename) { + logger.log(`[DownloadManager] Using filename: ${sanitizedFilename}`); + } + + return { + action: "start", + game_id: downloadId, + url: directUrl, + save_path: download.downloadPath, + out: sanitizedFilename, + allow_multiple_connections: true, + }; + } catch (error) { + logger.error(`[DownloadManager] Error processing Buzzheavier download:`, error); + throw error; + } + } + case Downloader.FuckingFast: { + logger.log(`[DownloadManager] Processing FuckingFast download for URI: ${download.uri}`); + try { + const directUrl = await FuckingFastApi.getDirectLink(download.uri); + logger.log(`[DownloadManager] FuckingFast direct URL obtained`); + + const filename = this.extractFilename(directUrl, download.uri); + const sanitizedFilename = filename ? this.sanitizeFilename(filename) : undefined; + + if (sanitizedFilename) { + logger.log(`[DownloadManager] Using filename: ${sanitizedFilename}`); + } + + return { + action: "start", + game_id: downloadId, + url: directUrl, + save_path: download.downloadPath, + out: sanitizedFilename, + allow_multiple_connections: true, + }; + } catch (error) { + logger.error(`[DownloadManager] Error processing FuckingFast download:`, error); + throw error; + } + } case Downloader.Mediafire: { const downloadUrl = await MediafireApi.getDownloadUrl(download.uri); @@ -426,4 +499,4 @@ export class DownloadManager { await PythonRPC.rpc.post("/action", payload); this.downloadingGameId = levelKeys.game(download.shop, download.objectId); } -} +} \ No newline at end of file diff --git a/src/main/services/hosters/buzzheavier.ts b/src/main/services/hosters/buzzheavier.ts new file mode 100644 index 00000000..7f8b7733 --- /dev/null +++ b/src/main/services/hosters/buzzheavier.ts @@ -0,0 +1,82 @@ +import axios from "axios"; +import { + HOSTER_USER_AGENT, + extractHosterFilename, + handleHosterError, +} from "./fuckingfast"; +import { logger } from "@main/services"; + +export class BuzzheavierApi { + private static readonly BUZZHEAVIER_DOMAINS = [ + "buzzheavier.com", + "bzzhr.co", + "fuckingfast.net", + ]; + + private static isSupportedDomain(url: string): boolean { + const lowerUrl = url.toLowerCase(); + return this.BUZZHEAVIER_DOMAINS.some((domain) => lowerUrl.includes(domain)); + } + + private static async getBuzzheavierDirectLink(url: string): Promise { + try { + const baseUrl = url.split("#")[0]; + logger.log(`[Buzzheavier] Starting download link extraction for: ${baseUrl}`); + + await axios.get(baseUrl, { + headers: { "User-Agent": HOSTER_USER_AGENT }, + timeout: 30000, + }); + + const downloadUrl = `${baseUrl}/download`; + logger.log(`[Buzzheavier] Making HEAD request to: ${downloadUrl}`); + const headResponse = await axios.head(downloadUrl, { + headers: { + "hx-current-url": baseUrl, + "hx-request": "true", + referer: baseUrl, + "User-Agent": HOSTER_USER_AGENT, + }, + maxRedirects: 0, + validateStatus: (status) => + status === 200 || status === 204 || status === 301 || status === 302, + timeout: 30000, + }); + + const hxRedirect = headResponse.headers["hx-redirect"]; + logger.log(`[Buzzheavier] Received hx-redirect header: ${hxRedirect}`); + if (!hxRedirect) { + logger.error(`[Buzzheavier] No hx-redirect header found. Status: ${headResponse.status}`); + throw new Error( + "Could not extract download link. File may be deleted or is a directory." + ); + } + + const domain = new URL(baseUrl).hostname; + const directLink = hxRedirect.startsWith("/dl/") + ? `https://${domain}${hxRedirect}` + : hxRedirect; + logger.log(`[Buzzheavier] Extracted direct link`); + return directLink; + } catch (error) { + logger.error(`[Buzzheavier] Error in getBuzzheavierDirectLink:`, error); + handleHosterError(error); + } + } + + public static async getDirectLink(url: string): Promise { + if (!this.isSupportedDomain(url)) { + throw new Error( + `Unsupported domain. Supported domains: ${this.BUZZHEAVIER_DOMAINS.join(", ")}` + ); + } + return this.getBuzzheavierDirectLink(url); + } + + public static async getFilename( + url: string, + directUrl?: string + ): Promise { + return extractHosterFilename(url, directUrl); + } +} \ No newline at end of file diff --git a/src/main/services/hosters/fuckingfast.ts b/src/main/services/hosters/fuckingfast.ts new file mode 100644 index 00000000..c9001d9a --- /dev/null +++ b/src/main/services/hosters/fuckingfast.ts @@ -0,0 +1,129 @@ +import axios from "axios"; +import { logger } from "@main/services"; + +export const HOSTER_USER_AGENT = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"; + +export async function extractHosterFilename( + url: string, + directUrl?: string +): Promise { + if (url.includes("#")) { + const fragment = url.split("#")[1]; + if (fragment && !fragment.startsWith("http")) { + return fragment; + } + } + + if (directUrl) { + try { + const response = await axios.head(directUrl, { + timeout: 10000, + headers: { "User-Agent": HOSTER_USER_AGENT }, + }); + + const contentDisposition = response.headers["content-disposition"]; + if (contentDisposition) { + const filenameMatch = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec( + contentDisposition + ); + if (filenameMatch && filenameMatch[1]) { + return filenameMatch[1].replace(/['"]/g, ""); + } + } + } catch { + // Ignore errors + } + + const urlPath = new URL(directUrl).pathname; + const filename = urlPath.split("/").pop()?.split("?")[0]; + if (filename) { + return filename; + } + } + + return "downloaded_file"; +} + +export function handleHosterError(error: unknown): never { + if (axios.isAxiosError(error)) { + if (error.response?.status === 404) { + throw new Error("File not found"); + } + if (error.response?.status === 429) { + throw new Error("Rate limit exceeded. Please try again later."); + } + if (error.response?.status === 403) { + throw new Error("Access denied. File may be private or deleted."); + } + throw new Error(`Network error: ${error.response?.status || "Unknown"}`); + } + throw error; +} + +// ============================================ +// FuckingFast API Class +// ============================================ +export class FuckingFastApi { + private static readonly FUCKINGFAST_DOMAINS = ["fuckingfast.co"]; + + private static readonly FUCKINGFAST_REGEX = + /window\.open\("(https:\/\/fuckingfast\.co\/dl\/[^"]*)"\)/; + + private static isSupportedDomain(url: string): boolean { + const lowerUrl = url.toLowerCase(); + return this.FUCKINGFAST_DOMAINS.some((domain) => lowerUrl.includes(domain)); + } + + private static async getFuckingFastDirectLink(url: string): Promise { + try { + logger.log(`[FuckingFast] Starting download link extraction for: ${url}`); + const response = await axios.get(url, { + headers: { "User-Agent": HOSTER_USER_AGENT }, + timeout: 30000, + }); + + const html = response.data; + + if (html.toLowerCase().includes("rate limit")) { + logger.error(`[FuckingFast] Rate limit detected`); + throw new Error( + "Rate limit exceeded. Please wait a few minutes and try again." + ); + } + + if (html.includes("File Not Found Or Deleted")) { + logger.error(`[FuckingFast] File not found or deleted`); + throw new Error("File not found or deleted"); + } + + const match = this.FUCKINGFAST_REGEX.exec(html); + if (!match || !match[1]) { + logger.error(`[FuckingFast] Could not extract download link`); + throw new Error("Could not extract download link from page"); + } + + logger.log(`[FuckingFast] Successfully extracted direct link`); + return match[1]; + } catch (error) { + logger.error(`[FuckingFast] Error:`, error); + handleHosterError(error); + } + } + + public static async getDirectLink(url: string): Promise { + if (!this.isSupportedDomain(url)) { + throw new Error( + `Unsupported domain. Supported domains: ${this.FUCKINGFAST_DOMAINS.join(", ")}` + ); + } + return this.getFuckingFastDirectLink(url); + } + + public static async getFilename( + url: string, + directUrl?: string + ): Promise { + return extractHosterFilename(url, directUrl); + } +} \ No newline at end of file diff --git a/src/main/services/hosters/index.ts b/src/main/services/hosters/index.ts index 3f3b9ac9..5f918811 100644 --- a/src/main/services/hosters/index.ts +++ b/src/main/services/hosters/index.ts @@ -3,3 +3,5 @@ export * from "./qiwi"; export * from "./datanodes"; export * from "./mediafire"; export * from "./pixeldrain"; +export * from "./buzzheavier"; +export * from "./fuckingfast"; diff --git a/src/renderer/src/constants.ts b/src/renderer/src/constants.ts index 3329a0cc..89de5503 100644 --- a/src/renderer/src/constants.ts +++ b/src/renderer/src/constants.ts @@ -10,6 +10,8 @@ export const DOWNLOADER_NAME = { [Downloader.Qiwi]: "Qiwi", [Downloader.Datanodes]: "Datanodes", [Downloader.Mediafire]: "Mediafire", + [Downloader.Buzzheavier]: "Buzzheavier", + [Downloader.FuckingFast]: "FuckingFast", [Downloader.TorBox]: "TorBox", [Downloader.Hydra]: "Nimbus", }; diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 619dca65..6e900175 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -8,6 +8,8 @@ export enum Downloader { Mediafire, TorBox, Hydra, + Buzzheavier, + FuckingFast, } export enum DownloadSourceStatus { diff --git a/src/shared/index.ts b/src/shared/index.ts index 615fa36e..d54ef387 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -114,6 +114,16 @@ export const getDownloadersForUri = (uri: string) => { if (uri.startsWith("https://datanodes.to")) return [Downloader.Datanodes]; if (uri.startsWith("https://www.mediafire.com")) return [Downloader.Mediafire]; + if ( + uri.startsWith("https://buzzheavier.com") || + uri.startsWith("https://bzzhr.co") || + uri.startsWith("https://fuckingfast.net") + ) { + return [Downloader.Buzzheavier]; + } + if (uri.startsWith("https://fuckingfast.co")) { + return [Downloader.FuckingFast]; + } if (realDebridHosts.some((host) => uri.startsWith(host))) return [Downloader.RealDebrid]; From 37f085e2c0e0d5d400c8f09b76993695b81e3316 Mon Sep 17 00:00:00 2001 From: Wkeynhk Date: Fri, 26 Dec 2025 01:13:36 +0300 Subject: [PATCH 16/19] . --- .../events/torrenting/start-game-download.ts | 4 +- .../services/download/download-manager.ts | 153 ++++++------------ 2 files changed, 47 insertions(+), 110 deletions(-) diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts index 940f3025..97de9f28 100644 --- a/src/main/events/torrenting/start-game-download.ts +++ b/src/main/events/torrenting/start-game-download.ts @@ -41,7 +41,6 @@ const startGameDownload = async ( const game = await gamesSublevel.get(gameKey); const gameAssets = await gamesShopAssetsSublevel.get(gameKey); - /* Delete any previous download */ await downloadsSublevel.del(gameKey); if (game) { @@ -82,6 +81,7 @@ const startGameDownload = async ( queued: true, extracting: false, automaticallyExtract, + extractionProgress: null, }; try { @@ -166,4 +166,4 @@ const startGameDownload = async ( } }; -registerEvent("startGameDownload", startGameDownload); +registerEvent("startGameDownload", startGameDownload); \ No newline at end of file diff --git a/src/main/services/download/download-manager.ts b/src/main/services/download/download-manager.ts index ce06e5e4..8cd16487 100644 --- a/src/main/services/download/download-manager.ts +++ b/src/main/services/download/download-manager.ts @@ -30,29 +30,21 @@ export class DownloadManager { private static downloadingGameId: string | null = null; private static extractFilename(url: string, originalUrl?: string): string | undefined { - if (originalUrl && originalUrl.includes('#')) { + if (originalUrl?.includes('#')) { const hashPart = originalUrl.split('#')[1]; - if (hashPart && !hashPart.startsWith('http')) { - return hashPart; - } + if (hashPart && !hashPart.startsWith('http')) return hashPart; } if (url.includes('#')) { const hashPart = url.split('#')[1]; - if (hashPart && !hashPart.startsWith('http')) { - return hashPart; - } + if (hashPart && !hashPart.startsWith('http')) return hashPart; } try { const urlObj = new URL(url); - const pathname = urlObj.pathname; - const filename = pathname.split('/').pop(); - if (filename && filename.length > 0) { - return filename; - } - } catch { - } + const filename = urlObj.pathname.split('/').pop(); + if (filename?.length) return filename; + } catch {} return undefined; } @@ -61,6 +53,24 @@ export class DownloadManager { return filename.replace(/[<>:"/\\|?*]/g, '_'); } + private static createDownloadPayload(directUrl: string, originalUrl: string, downloadId: string, savePath: string) { + const filename = this.extractFilename(directUrl, originalUrl); + const sanitizedFilename = filename ? this.sanitizeFilename(filename) : undefined; + + if (sanitizedFilename) { + logger.log(`[DownloadManager] Using filename: ${sanitizedFilename}`); + } + + return { + action: "start" as const, + game_id: downloadId, + url: directUrl, + save_path: savePath, + out: sanitizedFilename, + allow_multiple_connections: true, + }; + } + public static async startRPC( download?: Download, downloadsToSeed?: Download[] @@ -86,9 +96,7 @@ export class DownloadManager { } private static async getDownloadStatus() { - const response = await PythonRPC.rpc.get( - "/status" - ); + const response = await PythonRPC.rpc.get("/status"); if (response.data === null || !this.downloadingGameId) return null; const downloadId = this.downloadingGameId; @@ -104,8 +112,7 @@ export class DownloadManager { status, } = response.data; - const isDownloadingMetadata = - status === LibtorrentStatus.DownloadingMetadata; + const isDownloadingMetadata = status === LibtorrentStatus.DownloadingMetadata; const isCheckingFiles = status === LibtorrentStatus.CheckingFiles; const download = await downloadsSublevel.get(downloadId); @@ -154,21 +161,14 @@ export class DownloadManager { const userPreferences = await db.get( levelKeys.userPreferences, - { - valueEncoding: "json", - } + { valueEncoding: "json" } ); if (WindowManager.mainWindow && download) { WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress); WindowManager.mainWindow.webContents.send( "on-download-progress", - JSON.parse( - JSON.stringify({ - ...status, - game, - }) - ) + JSON.parse(JSON.stringify({ ...status, game })) ); } @@ -177,10 +177,7 @@ export class DownloadManager { if (progress === 1 && download) { publishDownloadCompleteNotification(game); - if ( - userPreferences?.seedAfterDownloadComplete && - download.downloader === Downloader.Torrent - ) { + if (userPreferences?.seedAfterDownloadComplete && download.downloader === Downloader.Torrent) { await downloadsSublevel.put(gameId, { ...download, status: "seeding", @@ -201,38 +198,25 @@ export class DownloadManager { } if (shouldExtract) { - const gameFilesManager = new GameFilesManager( - game.shop, - game.objectId - ); + const gameFilesManager = new GameFilesManager(game.shop, game.objectId); - if ( - FILE_EXTENSIONS_TO_EXTRACT.some((ext) => - download.folderName?.endsWith(ext) - ) - ) { + if (FILE_EXTENSIONS_TO_EXTRACT.some((ext) => download.folderName?.endsWith(ext))) { gameFilesManager.extractDownloadedFile(); } else { gameFilesManager - .extractFilesInDirectory( - path.join(download.downloadPath, download.folderName!) - ) - .then(() => { - gameFilesManager.setExtractionComplete(); - }); + .extractFilesInDirectory(path.join(download.downloadPath, download.folderName!)) + .then(() => gameFilesManager.setExtractionComplete()); } } const downloads = await downloadsSublevel .values() .all() - .then((games) => { - return sortBy( - games.filter((game) => game.status === "paused" && game.queued), - "timestamp", - "DESC" - ); - }); + .then((games) => sortBy( + games.filter((game) => game.status === "paused" && game.queued), + "timestamp", + "DESC" + )); const [nextItemOnQueue] = downloads; @@ -259,9 +243,7 @@ export class DownloadManager { if (!download) return; - const totalSize = await getDirSize( - path.join(download.downloadPath, status.folderName) - ); + const totalSize = await getDirSize(path.join(download.downloadPath, status.folderName)); if (totalSize < status.fileSize) { await this.cancelDownload(status.gameId); @@ -282,10 +264,7 @@ export class DownloadManager { static async pauseDownload(downloadKey = this.downloadingGameId) { await PythonRPC.rpc - .post("/action", { - action: "pause", - game_id: downloadKey, - } as PauseDownloadPayload) + .post("/action", { action: "pause", game_id: downloadKey } as PauseDownloadPayload) .catch(() => {}); if (downloadKey === this.downloadingGameId) { @@ -300,13 +279,8 @@ export class DownloadManager { static async cancelDownload(downloadKey = this.downloadingGameId) { await PythonRPC.rpc - .post("/action", { - action: "cancel", - game_id: downloadKey, - }) - .catch((err) => { - logger.error("Failed to cancel game download", err); - }); + .post("/action", { action: "cancel", game_id: downloadKey }) + .catch((err) => logger.error("Failed to cancel game download", err)); if (downloadKey === this.downloadingGameId) { WindowManager.mainWindow?.setProgressBar(-1); @@ -339,7 +313,6 @@ export class DownloadManager { const id = download.uri.split("/").pop(); const token = await GofileApi.authorize(); const downloadLink = await GofileApi.getDownloadLink(id!); - await GofileApi.checkDownloadUrl(downloadLink); return { @@ -386,22 +359,7 @@ export class DownloadManager { try { const directUrl = await BuzzheavierApi.getDirectLink(download.uri); logger.log(`[DownloadManager] Buzzheavier direct URL obtained`); - - const filename = this.extractFilename(directUrl, download.uri); - const sanitizedFilename = filename ? this.sanitizeFilename(filename) : undefined; - - if (sanitizedFilename) { - logger.log(`[DownloadManager] Using filename: ${sanitizedFilename}`); - } - - return { - action: "start", - game_id: downloadId, - url: directUrl, - save_path: download.downloadPath, - out: sanitizedFilename, - allow_multiple_connections: true, - }; + return this.createDownloadPayload(directUrl, download.uri, downloadId, download.downloadPath); } catch (error) { logger.error(`[DownloadManager] Error processing Buzzheavier download:`, error); throw error; @@ -412,22 +370,7 @@ export class DownloadManager { try { const directUrl = await FuckingFastApi.getDirectLink(download.uri); logger.log(`[DownloadManager] FuckingFast direct URL obtained`); - - const filename = this.extractFilename(directUrl, download.uri); - const sanitizedFilename = filename ? this.sanitizeFilename(filename) : undefined; - - if (sanitizedFilename) { - logger.log(`[DownloadManager] Using filename: ${sanitizedFilename}`); - } - - return { - action: "start", - game_id: downloadId, - url: directUrl, - save_path: download.downloadPath, - out: sanitizedFilename, - allow_multiple_connections: true, - }; + return this.createDownloadPayload(directUrl, download.uri, downloadId, download.downloadPath); } catch (error) { logger.error(`[DownloadManager] Error processing FuckingFast download:`, error); throw error; @@ -435,7 +378,6 @@ export class DownloadManager { } case Downloader.Mediafire: { const downloadUrl = await MediafireApi.getDownloadUrl(download.uri); - return { action: "start", game_id: downloadId, @@ -452,7 +394,6 @@ export class DownloadManager { }; case Downloader.RealDebrid: { const downloadUrl = await RealDebridClient.getDownloadUrl(download.uri); - if (!downloadUrl) throw new Error(DownloadError.NotCachedOnRealDebrid); return { @@ -465,7 +406,6 @@ export class DownloadManager { } case Downloader.TorBox: { const { name, url } = await TorBoxClient.getDownloadInfo(download.uri); - if (!url) return; return { action: "start", @@ -477,10 +417,7 @@ export class DownloadManager { }; } case Downloader.Hydra: { - const downloadUrl = await HydraDebridClient.getDownloadUrl( - download.uri - ); - + const downloadUrl = await HydraDebridClient.getDownloadUrl(download.uri); if (!downloadUrl) throw new Error(DownloadError.NotCachedOnHydra); return { From 067f7a00bef41e8ede399de88b58161a76c985f9 Mon Sep 17 00:00:00 2001 From: Wkeynhk Date: Fri, 26 Dec 2025 01:19:04 +0300 Subject: [PATCH 17/19] . --- src/main/events/torrenting/start-game-download.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts index 97de9f28..7e57dff2 100644 --- a/src/main/events/torrenting/start-game-download.ts +++ b/src/main/events/torrenting/start-game-download.ts @@ -81,7 +81,7 @@ const startGameDownload = async ( queued: true, extracting: false, automaticallyExtract, - extractionProgress: null, + extractionProgress: 0, }; try { From 7f0dc5eee46bd8fddb4dfe13fc39063790a2af13 Mon Sep 17 00:00:00 2001 From: Wkeynhk Date: Fri, 26 Dec 2025 01:25:56 +0300 Subject: [PATCH 18/19] . --- src/main/services/download/download-manager.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/services/download/download-manager.ts b/src/main/services/download/download-manager.ts index 8cd16487..68890a66 100644 --- a/src/main/services/download/download-manager.ts +++ b/src/main/services/download/download-manager.ts @@ -44,7 +44,9 @@ export class DownloadManager { const urlObj = new URL(url); const filename = urlObj.pathname.split('/').pop(); if (filename?.length) return filename; - } catch {} + } catch { + // Invalid URL + } return undefined; } From 358f41b4ba33e1755ecac968f9f606775f431e04 Mon Sep 17 00:00:00 2001 From: Wkeynhk Date: Fri, 26 Dec 2025 01:34:09 +0300 Subject: [PATCH 19/19] . --- src/main/events/torrenting/start-game-download.ts | 2 +- src/main/services/download/download-manager.ts | 2 +- src/main/services/hosters/buzzheavier.ts | 2 +- src/main/services/hosters/fuckingfast.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts index 7e57dff2..e44ba936 100644 --- a/src/main/events/torrenting/start-game-download.ts +++ b/src/main/events/torrenting/start-game-download.ts @@ -166,4 +166,4 @@ const startGameDownload = async ( } }; -registerEvent("startGameDownload", startGameDownload); \ No newline at end of file +registerEvent("startGameDownload", startGameDownload); diff --git a/src/main/services/download/download-manager.ts b/src/main/services/download/download-manager.ts index 68890a66..bc9ddf1d 100644 --- a/src/main/services/download/download-manager.ts +++ b/src/main/services/download/download-manager.ts @@ -438,4 +438,4 @@ export class DownloadManager { await PythonRPC.rpc.post("/action", payload); this.downloadingGameId = levelKeys.game(download.shop, download.objectId); } -} \ No newline at end of file +} diff --git a/src/main/services/hosters/buzzheavier.ts b/src/main/services/hosters/buzzheavier.ts index 7f8b7733..819c8f25 100644 --- a/src/main/services/hosters/buzzheavier.ts +++ b/src/main/services/hosters/buzzheavier.ts @@ -79,4 +79,4 @@ export class BuzzheavierApi { ): Promise { return extractHosterFilename(url, directUrl); } -} \ No newline at end of file +} diff --git a/src/main/services/hosters/fuckingfast.ts b/src/main/services/hosters/fuckingfast.ts index c9001d9a..00d0ff58 100644 --- a/src/main/services/hosters/fuckingfast.ts +++ b/src/main/services/hosters/fuckingfast.ts @@ -126,4 +126,4 @@ export class FuckingFastApi { ): Promise { return extractHosterFilename(url, directUrl); } -} \ No newline at end of file +}