feat(downloads): M5 — Download Center frontend

- api.ts: download types + methods (profiles, enqueue, list/manage, file url, share,
  usage, index, admin storage/quota)
- format.ts: formatBytes / formatSpeed
- i18n: en/hu/de downloads.json (trilingual)
- Downloads nav module (Download icon, active-count badge, hidden for demo) placed after
  Messages; urlState page + App render + Header title
- DownloadButton: self-contained per-card/player affordance (demo-hidden, reflects the
  per-user download index: downloaded / in-queue), opens the preset dialog
- DownloadDialog: preset picker + optional display name -> enqueue + 'View' toast
- ProfileEditor: manage built-in + custom formats (full spec form incl. SponsorBlock)
- DownloadCenter: Queue / Library / Shared / (admin) System tabs, live progress via
  useLiveQuery, usage bar, pause/resume/cancel/delete, save-to-device, rename, share,
  admin storage dashboard + per-user quota editor
- wired DownloadButton into VideoCard + PlayerModal
- lib/nav.ts: decoupled navigator so the download toast can jump to the page

tsc + vite build clean. Verified in a headless authed browser: page + tabs render, Library
shows a real download with usage bar + actions, no console errors.
This commit is contained in:
2026-07-03 01:52:58 +02:00
parent e61e0e740f
commit d39fccd399
17 changed files with 1551 additions and 1 deletions
+20
View File
@@ -5,6 +5,26 @@ export function relativeTime(iso: string | null): string {
return relativeFromMs(new Date(iso).getTime());
}
/** Human file size, binary units (1024). e.g. 501747 -> "490 KB", 5368709120 -> "5.0 GB". */
export function formatBytes(bytes: number | null | undefined): string {
const n = Number(bytes);
if (!n || n < 0) return "0 B";
const units = ["B", "KB", "MB", "GB", "TB"];
let i = 0;
let v = n;
while (v >= 1024 && i < units.length - 1) {
v /= 1024;
i++;
}
return `${v >= 100 || i === 0 ? Math.round(v) : v.toFixed(1)} ${units[i]}`;
}
/** Download speed from bytes/sec, e.g. "2.4 MB/s". */
export function formatSpeed(bps: number | null | undefined): string {
if (!bps) return "";
return `${formatBytes(bps)}/s`;
}
/** Relative-time label from an epoch-millis timestamp (the ms-based half of relativeTime, for
* client-side notifications whose timestamps are already numbers). One implementation, one
* set of `time.*` strings — shared instead of re-implemented per component. */