mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-29 22:01:03 +00:00
style: refactor download group component to optimize download state management and improve UI responsiveness
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
Reference in New Issue
Block a user