feat(downloads): restore package disclosure motion and remaining size

Restore smooth 300 ms expand and collapse transitions without removing row virtualization. Limit animated disclosure rows globally, preserve live item updates during motion, support rapid reversal, and keep large queues bounded.

Add a Remaining sidebar metric that sums only unfinished known bytes while clearly marking unknown open sizes. Cover multi-package limits, live transition merges, honest remaining-size semantics, translations, and accessibility.
This commit is contained in:
Sucukdeluxe
2026-08-14 22:24:00 +02:00
parent 3974853634
commit 9cf89b8493
10 changed files with 433 additions and 95 deletions
@@ -1,4 +1,5 @@
import type { DownloadItem, DownloadStatus, PackageEntry } from "../../../shared/types";
import { humanSize } from "../../download-format";
import { DOWNLOAD_FILE_ROW_HEIGHT, DOWNLOAD_PACKAGE_ROW_HEIGHT, type DownloadVirtualRowInput } from "./download-virtualizer";
export type DownloadDisplayMode = "packages" | "files";
@@ -89,14 +90,37 @@ export function getDownloadQueueTotalBytes(items: Iterable<DownloadItem>): numbe
return total;
}
function isPendingDownloadItem(item: DownloadItem): boolean {
return item.status !== "completed" && item.status !== "cancelled" && item.status !== "failed";
}
export function getPendingDownloadItemCount(items: Iterable<DownloadItem>): number {
let count = 0;
for (const item of items) {
if (item.status !== "completed" && item.status !== "cancelled" && item.status !== "failed") count += 1;
if (isPendingDownloadItem(item)) count += 1;
}
return count;
}
export function getRemainingDownloadBytes(items: Iterable<DownloadItem>): { bytes: number; unknownItems: number } {
let bytes = 0;
let unknownItems = 0;
for (const item of items) {
if (!isPendingDownloadItem(item)) continue;
if (!item.totalBytes || item.totalBytes <= 0) {
unknownItems += 1;
continue;
}
bytes += Math.max(0, item.totalBytes - Math.max(0, item.downloadedBytes));
}
return { bytes, unknownItems };
}
export function formatRemainingDownloadBytes(summary: { bytes: number; unknownItems: number }): string {
if (summary.unknownItems <= 0) return humanSize(summary.bytes);
return summary.bytes > 0 ? `${humanSize(summary.bytes)}` : "Unbekannt";
}
export function getDownloadSpeedBps(packageSpeeds: Record<string, number>): number {
let total = 0;
for (const speed of Object.values(packageSpeeds)) {