feat: show live progress while downloading named files

Bunny/cross-origin downloads are fetched into a blob before saving, which
on large files or slow connections looked stuck (spinner only). Stream the
body through a counting transform and show real byte progress in a toast:
per-file percent for single downloads and file N/M + percent for bulk.

- Progress is measured from Content-Length + received bytes (not estimated).
- The blob is assembled by the browser from the stream (can be disk-backed),
  so we don't accumulate chunks in the JS heap.
- Only the blob path shows a toast; same-origin (R2/S3/MinIO) and the >10 GB
  fallback use the browser's native download UI.
This commit is contained in:
yusufipk
2026-07-10 22:27:53 +07:00
parent 8845c2c643
commit 5821f73d38
4 changed files with 157 additions and 21 deletions
@@ -10,7 +10,12 @@ import type {
VideoData,
} from '@/components/video-page/types';
import { resolvePublicBunnyCdnHostname } from '@/lib/bunny-cdn';
import { downloadNamedFile, extensionFromUrl, navigateDownload } from '@/lib/client/download-file';
import {
downloadNamedFile,
downloadProgressLabel,
extensionFromUrl,
navigateDownload,
} from '@/lib/client/download-file';
function sanitizeDownloadFileName(value: string): string {
return value
@@ -143,8 +148,25 @@ export function useDownloadActions({ activeVersion, video }: UseDownloadActionsP
(activeVersion.providerId === 'direct'
? extensionFromUrl(activeVersion.originalUrl)
: '') || 'mp4';
const saved = await downloadNamedFile(downloadUrl, `${baseName}.${fallbackExt}`);
if (!saved) {
// The file is pulled into the browser before it can be saved, which on
// a big file / slow connection takes a while with no native download UI
// — show live progress so it doesn't look stuck.
const toastId = `download-${activeVersion.id}`;
toast.loading(`Downloading “${baseName}”…`, { id: toastId, duration: Infinity });
const saved = await downloadNamedFile(downloadUrl, `${baseName}.${fallbackExt}`, (p) => {
toast.loading(`Downloading “${baseName}`, {
id: toastId,
description: downloadProgressLabel(p),
duration: Infinity,
});
});
if (saved) {
toast.success(`${baseName}” downloaded`, { id: toastId, duration: 4000 });
} else {
// Too large to buffer (or fetch blocked): let the browser download it
// directly (its own progress UI, CDN filename).
toast.dismiss(toastId);
navigateDownload(downloadUrl);
}
}