From 9779aed8c1a99444c9f0f17877199deecd905fca Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sun, 30 Nov 2025 08:05:45 +0200 Subject: [PATCH 1/3] fix: auto-resuming download isnt working after restart --- src/main/events/torrenting/resume-game-download.ts | 7 ++++++- src/main/main.ts | 4 ++-- src/main/services/download/download-manager.ts | 6 +++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/events/torrenting/resume-game-download.ts b/src/main/events/torrenting/resume-game-download.ts index 48bb1c12..80d1429f 100644 --- a/src/main/events/torrenting/resume-game-download.ts +++ b/src/main/events/torrenting/resume-game-download.ts @@ -13,7 +13,12 @@ const resumeGameDownload = async ( const download = await downloadsSublevel.get(gameKey); - if (download?.status === "paused") { + // Allow resuming if status is "paused" OR "active" (for cases where app was closed during download) + if ( + download && + (download.status === "paused" || download.status === "active") && + download.progress !== 1 + ) { await DownloadManager.pauseDownload(); for await (const [key, value] of downloadsSublevel.iterator()) { diff --git a/src/main/main.ts b/src/main/main.ts index 1cadcebd..c176efa7 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,5 +1,5 @@ import { downloadsSublevel } from "./level/sublevels/downloads"; -import { sortBy } from "lodash-es"; +import { orderBy } from "lodash-es"; import { Downloader } from "@shared"; import { levelKeys, db } from "./level"; import type { UserPreferences } from "@types"; @@ -68,7 +68,7 @@ export const loadState = async () => { .values() .all() .then((games) => { - return sortBy(games, "timestamp", "DESC"); + return orderBy(games, "timestamp", "desc"); }); downloads.forEach((download) => { diff --git a/src/main/services/download/download-manager.ts b/src/main/services/download/download-manager.ts index 4dcebbb0..1a79f8f0 100644 --- a/src/main/services/download/download-manager.ts +++ b/src/main/services/download/download-manager.ts @@ -20,7 +20,7 @@ import { RealDebridClient } from "./real-debrid"; import path from "path"; import { logger } from "../logger"; import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level"; -import { sortBy } from "lodash-es"; +import { orderBy } from "lodash-es"; import { TorBoxClient } from "./torbox"; import { GameFilesManager } from "../game-files-manager"; import { HydraDebridClient } from "./hydra-debrid"; @@ -194,10 +194,10 @@ export class DownloadManager { .values() .all() .then((games) => { - return sortBy( + return orderBy( games.filter((game) => game.status === "paused" && game.queued), "timestamp", - "DESC" + "desc" ); }); From 9bdb216e0fb1f66ac39995435ef080702d8df28a Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sun, 30 Nov 2025 08:23:49 +0200 Subject: [PATCH 2/3] fix: deleted comment --- src/main/events/torrenting/resume-game-download.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/events/torrenting/resume-game-download.ts b/src/main/events/torrenting/resume-game-download.ts index 80d1429f..4525e2df 100644 --- a/src/main/events/torrenting/resume-game-download.ts +++ b/src/main/events/torrenting/resume-game-download.ts @@ -13,7 +13,6 @@ const resumeGameDownload = async ( const download = await downloadsSublevel.get(gameKey); - // Allow resuming if status is "paused" OR "active" (for cases where app was closed during download) if ( download && (download.status === "paused" || download.status === "active") && From a89b0bb2a8b8c42eaf972bdba91e9e6557c49680 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Sun, 30 Nov 2025 06:25:17 +0000 Subject: [PATCH 3/3] style: refactor download group component to optimize download state management and improve UI responsiveness --- .../src/pages/downloads/download-group.scss | 14 +-- .../src/pages/downloads/download-group.tsx | 102 +++++++++++++++++- 2 files changed, 101 insertions(+), 15 deletions(-) diff --git a/src/renderer/src/pages/downloads/download-group.scss b/src/renderer/src/pages/downloads/download-group.scss index 4fd6ec02..0b9deea3 100644 --- a/src/renderer/src/pages/downloads/download-group.scss +++ b/src/renderer/src/pages/downloads/download-group.scss @@ -109,21 +109,15 @@ display: flex; align-items: center; transition: opacity 0.2s ease; + outline: none; &:hover { opacity: 0.8; } - &:focus { - outline: 2px solid rgba(255, 255, 255, 0.5); - outline-offset: 4px; - border-radius: 4px; - } - + &:focus, &:focus-visible { - outline: 2px solid rgba(255, 255, 255, 0.5); - outline-offset: 4px; - border-radius: 4px; + outline: none; } } @@ -205,7 +199,7 @@ &__hero-action-row { display: flex; justify-content: space-between; - align-items: flex-end; + align-items: flex-start; gap: calc(globals.$spacing-unit * 3); margin-top: calc(globals.$spacing-unit * 4); margin-bottom: calc(globals.$spacing-unit * 2); diff --git a/src/renderer/src/pages/downloads/download-group.tsx b/src/renderer/src/pages/downloads/download-group.tsx index a202af36..bcecbc7c 100644 --- a/src/renderer/src/pages/downloads/download-group.tsx +++ b/src/renderer/src/pages/downloads/download-group.tsx @@ -397,6 +397,14 @@ function HeroDownloadView({ )} + + {game.download?.downloader && ( +
+
+ {DOWNLOADER_NAME[game.download.downloader]} +
+
+ )}
@@ -437,14 +445,54 @@ export function DownloadGroup({ const { lastPacket, - pauseDownload, - resumeDownload, + pauseDownload: pauseDownloadOriginal, + resumeDownload: resumeDownloadOriginal, cancelDownload, isGameDeleting, pauseSeeding, resumeSeeding, } = useDownload(); + // Wrap resumeDownload with optimistic update + const resumeDownload = useCallback( + async (shop: GameShop, objectId: string) => { + const gameId = `${shop}:${objectId}`; + + // Optimistically mark as downloading + setOptimisticallyResumed((prev) => ({ ...prev, [gameId]: true })); + + try { + await resumeDownloadOriginal(shop, objectId); + } catch (error) { + // If resume fails, remove optimistic state + setOptimisticallyResumed((prev) => { + const next = { ...prev }; + delete next[gameId]; + return next; + }); + throw error; + } + }, + [resumeDownloadOriginal] + ); + + // Wrap pauseDownload to clear optimistic state + const pauseDownload = useCallback( + async (shop: GameShop, objectId: string) => { + const gameId = `${shop}:${objectId}`; + + // Clear optimistic state when pausing + setOptimisticallyResumed((prev) => { + const next = { ...prev }; + delete next[gameId]; + return next; + }); + + await pauseDownloadOriginal(shop, objectId); + }, + [pauseDownloadOriginal] + ); + const { formatDistance } = useDate(); const [peakSpeeds, setPeakSpeeds] = useState>({}); @@ -452,6 +500,9 @@ export function DownloadGroup({ const [dominantColors, setDominantColors] = useState>( {} ); + const [optimisticallyResumed, setOptimisticallyResumed] = useState< + Record + >({}); const extractDominantColor = useCallback( async (imageUrl: string, gameId: string) => { @@ -469,6 +520,45 @@ export function DownloadGroup({ [dominantColors] ); + // Clear optimistic state when actual download starts or library updates + useEffect(() => { + if (lastPacket?.gameId) { + const gameId = lastPacket.gameId; + + // Clear optimistic state when actual download starts + setOptimisticallyResumed((prev) => { + const next = { ...prev }; + delete next[gameId]; + return next; + }); + } + }, [lastPacket?.gameId]); + + // Clear optimistic state for games that are no longer active after library update + useEffect(() => { + setOptimisticallyResumed((prev) => { + const next = { ...prev }; + let changed = false; + + for (const gameId in next) { + if (next[gameId]) { + const game = library.find((g) => g.id === gameId); + // Clear if game doesn't exist or download status is not active + if ( + !game || + game.download?.status !== "active" || + lastPacket?.gameId === gameId + ) { + delete next[gameId]; + changed = true; + } + } + } + + return changed ? next : prev; + }); + }, [library, lastPacket?.gameId]); + useEffect(() => { if (lastPacket?.gameId && lastPacket.downloadSpeed !== undefined) { const gameId = lastPacket.gameId; @@ -552,10 +642,12 @@ export function DownloadGroup({ const isGameDownloadingMap = useMemo(() => { const map: Record = {}; for (const game of library) { - map[game.id] = lastPacket?.gameId === game.id; + map[game.id] = + lastPacket?.gameId === game.id || + optimisticallyResumed[game.id] === true; } return map; - }, [library, lastPacket?.gameId]); + }, [library, lastPacket?.gameId, optimisticallyResumed]); const getFinalDownloadSize = (game: LibraryGame) => { const download = game.download!; @@ -830,7 +922,7 @@ export function DownloadGroup({ disabled={isGameDeleting(game.id)} className="download-group__simple-menu-btn" > - + )} {isQueuedGroup && game.download?.progress !== 1 && (