mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 13:56:16 +00:00
Merge pull request #1881 from hydralauncher/fix/downloads-ui
fix: auto-resuming download isnt working after restart
This commit is contained in:
@@ -13,7 +13,11 @@ const resumeGameDownload = async (
|
||||
|
||||
const download = await downloadsSublevel.get(gameKey);
|
||||
|
||||
if (download?.status === "paused") {
|
||||
if (
|
||||
download &&
|
||||
(download.status === "paused" || download.status === "active") &&
|
||||
download.progress !== 1
|
||||
) {
|
||||
await DownloadManager.pauseDownload();
|
||||
|
||||
for await (const [key, value] of downloadsSublevel.iterator()) {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -397,6 +397,14 @@ function HeroDownloadView({
|
||||
</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 className="download-group__speed-chart">
|
||||
@@ -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<Record<string, number>>({});
|
||||
@@ -452,6 +500,9 @@ export function DownloadGroup({
|
||||
const [dominantColors, setDominantColors] = useState<Record<string, string>>(
|
||||
{}
|
||||
);
|
||||
const [optimisticallyResumed, setOptimisticallyResumed] = useState<
|
||||
Record<string, boolean>
|
||||
>({});
|
||||
|
||||
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<string, boolean> = {};
|
||||
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"
|
||||
>
|
||||
<DownloadIcon size={16} />
|
||||
<PlayIcon size={16} />
|
||||
</Button>
|
||||
)}
|
||||
{isQueuedGroup && game.download?.progress !== 1 && (
|
||||
|
||||
Reference in New Issue
Block a user