From 0afa0484db52c9395e2a5d91bc2d50f1cfcc8a66 Mon Sep 17 00:00:00 2001 From: zmeyer44 Date: Fri, 17 Jul 2026 10:08:55 -0400 Subject: [PATCH] Add download all button to shared folder page Adds a "Download all (N)" button to the folder view of the shared link page. Refactors the existing single-file download into a bare downloadFile helper so the per-file buttons and the bulk button share one code path. Downloads run sequentially rather than in parallel: each file consumes one of the share link's maxDownloads budget, and parallel blob clicks get throttled by the browser. Failures are tallied into a single toast instead of one per file. Scoped to the files in the current folder view, not recursive into subfolders. Co-Authored-By: Claude Opus 4.8 --- apps/web/app/shared/[token]/page.tsx | 68 ++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 14 deletions(-) diff --git a/apps/web/app/shared/[token]/page.tsx b/apps/web/app/shared/[token]/page.tsx index 00d2448..ca3388b 100644 --- a/apps/web/app/shared/[token]/page.tsx +++ b/apps/web/app/shared/[token]/page.tsx @@ -54,28 +54,53 @@ export default function SharedPage({ ); const getDownloadUrl = trpc.shares.getDownloadUrl.useMutation(); + const [isDownloadingAll, setIsDownloadingAll] = useState(false); + + const downloadFile = async (fileId?: string) => { + const result = await getDownloadUrl.mutateAsync({ + token, + fileId, + password: enteredPassword, + }); + const response = await fetch(result.url); + if (!response.ok) throw new Error(`Download failed (${response.status})`); + const blob = await response.blob(); + const blobUrl = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = blobUrl; + a.download = result.filename; + a.click(); + setTimeout(() => URL.revokeObjectURL(blobUrl), 100); + }; const handleDownload = async (fileId?: string) => { try { - const result = await getDownloadUrl.mutateAsync({ - token, - fileId, - password: enteredPassword, - }); - const response = await fetch(result.url); - if (!response.ok) throw new Error(`Download failed (${response.status})`); - const blob = await response.blob(); - const blobUrl = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = blobUrl; - a.download = result.filename; - a.click(); - setTimeout(() => URL.revokeObjectURL(blobUrl), 100); + await downloadFile(fileId); } catch (err) { toast.error((err as Error).message); } }; + const handleDownloadAll = async (files: { id: string }[]) => { + setIsDownloadingAll(true); + let failed = 0; + // Sequential: each file consumes one of the link's maxDownloads budget, + // and parallel blob clicks get throttled by the browser. + for (const file of files) { + try { + await downloadFile(file.id); + } catch { + failed++; + } + } + setIsDownloadingAll(false); + if (failed > 0) { + toast.error( + `${failed} of ${files.length} ${failed === 1 ? "file" : "files"} failed to download`, + ); + } + }; + const navigateToFolder = (folderId: string) => { setCurrentFolderId(folderId); }; @@ -231,6 +256,21 @@ export default function SharedPage({ ))} + {displayFiles && displayFiles.length > 0 && ( + + )} + {browseQuery.isLoading && isBrowsing ? (