Merge pull request #1881 from hydralauncher/fix/downloads-ui

fix: auto-resuming download isnt working after restart
This commit is contained in:
Chubby Granny Chaser
2025-11-30 06:26:08 +00:00
committed by GitHub
5 changed files with 111 additions and 21 deletions

View File

@@ -13,7 +13,11 @@ const resumeGameDownload = async (
const download = await downloadsSublevel.get(gameKey); const download = await downloadsSublevel.get(gameKey);
if (download?.status === "paused") { if (
download &&
(download.status === "paused" || download.status === "active") &&
download.progress !== 1
) {
await DownloadManager.pauseDownload(); await DownloadManager.pauseDownload();
for await (const [key, value] of downloadsSublevel.iterator()) { for await (const [key, value] of downloadsSublevel.iterator()) {

View File

@@ -1,5 +1,5 @@
import { downloadsSublevel } from "./level/sublevels/downloads"; import { downloadsSublevel } from "./level/sublevels/downloads";
import { sortBy } from "lodash-es"; import { orderBy } from "lodash-es";
import { Downloader } from "@shared"; import { Downloader } from "@shared";
import { levelKeys, db } from "./level"; import { levelKeys, db } from "./level";
import type { UserPreferences } from "@types"; import type { UserPreferences } from "@types";
@@ -68,7 +68,7 @@ export const loadState = async () => {
.values() .values()
.all() .all()
.then((games) => { .then((games) => {
return sortBy(games, "timestamp", "DESC"); return orderBy(games, "timestamp", "desc");
}); });
downloads.forEach((download) => { downloads.forEach((download) => {

View File

@@ -20,7 +20,7 @@ import { RealDebridClient } from "./real-debrid";
import path from "path"; import path from "path";
import { logger } from "../logger"; import { logger } from "../logger";
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level"; import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
import { sortBy } from "lodash-es"; import { orderBy } from "lodash-es";
import { TorBoxClient } from "./torbox"; import { TorBoxClient } from "./torbox";
import { GameFilesManager } from "../game-files-manager"; import { GameFilesManager } from "../game-files-manager";
import { HydraDebridClient } from "./hydra-debrid"; import { HydraDebridClient } from "./hydra-debrid";
@@ -194,10 +194,10 @@ export class DownloadManager {
.values() .values()
.all() .all()
.then((games) => { .then((games) => {
return sortBy( return orderBy(
games.filter((game) => game.status === "paused" && game.queued), games.filter((game) => game.status === "paused" && game.queued),
"timestamp", "timestamp",
"DESC" "desc"
); );
}); });

View File

@@ -109,21 +109,15 @@
display: flex; display: flex;
align-items: center; align-items: center;
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
outline: none;
&:hover { &:hover {
opacity: 0.8; opacity: 0.8;
} }
&:focus { &:focus,
outline: 2px solid rgba(255, 255, 255, 0.5);
outline-offset: 4px;
border-radius: 4px;
}
&:focus-visible { &:focus-visible {
outline: 2px solid rgba(255, 255, 255, 0.5); outline: none;
outline-offset: 4px;
border-radius: 4px;
} }
} }
@@ -205,7 +199,7 @@
&__hero-action-row { &__hero-action-row {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: flex-end; align-items: flex-start;
gap: calc(globals.$spacing-unit * 3); gap: calc(globals.$spacing-unit * 3);
margin-top: calc(globals.$spacing-unit * 4); margin-top: calc(globals.$spacing-unit * 4);
margin-bottom: calc(globals.$spacing-unit * 2); margin-bottom: calc(globals.$spacing-unit * 2);

View File

@@ -397,6 +397,14 @@ function HeroDownloadView({
</div> </div>
</div> </div>
)} )}
{game.download?.downloader && (
<div className="download-group__stat-item">
<div className="download-group__stat-content">
<Badge>{DOWNLOADER_NAME[game.download.downloader]}</Badge>
</div>
</div>
)}
</div> </div>
<div className="download-group__speed-chart"> <div className="download-group__speed-chart">
@@ -437,14 +445,54 @@ export function DownloadGroup({
const { const {
lastPacket, lastPacket,
pauseDownload, pauseDownload: pauseDownloadOriginal,
resumeDownload, resumeDownload: resumeDownloadOriginal,
cancelDownload, cancelDownload,
isGameDeleting, isGameDeleting,
pauseSeeding, pauseSeeding,
resumeSeeding, resumeSeeding,
} = useDownload(); } = 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 { formatDistance } = useDate();
const [peakSpeeds, setPeakSpeeds] = useState<Record<string, number>>({}); const [peakSpeeds, setPeakSpeeds] = useState<Record<string, number>>({});
@@ -452,6 +500,9 @@ export function DownloadGroup({
const [dominantColors, setDominantColors] = useState<Record<string, string>>( const [dominantColors, setDominantColors] = useState<Record<string, string>>(
{} {}
); );
const [optimisticallyResumed, setOptimisticallyResumed] = useState<
Record<string, boolean>
>({});
const extractDominantColor = useCallback( const extractDominantColor = useCallback(
async (imageUrl: string, gameId: string) => { async (imageUrl: string, gameId: string) => {
@@ -469,6 +520,45 @@ export function DownloadGroup({
[dominantColors] [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(() => { useEffect(() => {
if (lastPacket?.gameId && lastPacket.downloadSpeed !== undefined) { if (lastPacket?.gameId && lastPacket.downloadSpeed !== undefined) {
const gameId = lastPacket.gameId; const gameId = lastPacket.gameId;
@@ -552,10 +642,12 @@ export function DownloadGroup({
const isGameDownloadingMap = useMemo(() => { const isGameDownloadingMap = useMemo(() => {
const map: Record<string, boolean> = {}; const map: Record<string, boolean> = {};
for (const game of library) { 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; return map;
}, [library, lastPacket?.gameId]); }, [library, lastPacket?.gameId, optimisticallyResumed]);
const getFinalDownloadSize = (game: LibraryGame) => { const getFinalDownloadSize = (game: LibraryGame) => {
const download = game.download!; const download = game.download!;
@@ -830,7 +922,7 @@ export function DownloadGroup({
disabled={isGameDeleting(game.id)} disabled={isGameDeleting(game.id)}
className="download-group__simple-menu-btn" className="download-group__simple-menu-btn"
> >
<DownloadIcon size={16} /> <PlayIcon size={16} />
</Button> </Button>
)} )}
{isQueuedGroup && game.download?.progress !== 1 && ( {isQueuedGroup && game.download?.progress !== 1 && (