diff --git a/app/(dashboard)/projects/[projectId]/project-content-client.tsx b/app/(dashboard)/projects/[projectId]/project-content-client.tsx index b992420..a186a41 100644 --- a/app/(dashboard)/projects/[projectId]/project-content-client.tsx +++ b/app/(dashboard)/projects/[projectId]/project-content-client.tsx @@ -27,8 +27,10 @@ import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { DropdownMenu, + DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { @@ -105,6 +107,7 @@ export function ProjectContentClient({ const [selectedVideoIds, setSelectedVideoIds] = useState([]); const [selectionMode, setSelectionMode] = useState(false); const [isDownloading, setIsDownloading] = useState(false); + const [includeAssetsInDownload, setIncludeAssetsInDownload] = useState(false); const [isDeletingSelected, setIsDeletingSelected] = useState(false); const [showDeleteSelectedDialog, setShowDeleteSelectedDialog] = useState(false); const [showMoveSelectedDialog, setShowMoveSelectedDialog] = useState(false); @@ -184,7 +187,7 @@ export function ProjectContentClient({ }, []); const startProjectDownload = useCallback( - async (videoIds?: string[], options?: { allVersions?: boolean }) => { + async (videoIds?: string[], options?: { allVersions?: boolean; includeAssets?: boolean }) => { if (!canDownloadProject || isDownloading) return; const searchParams = new URLSearchParams(); @@ -194,6 +197,9 @@ export function ProjectContentClient({ if (options?.allVersions) { searchParams.set('versions', 'all'); } + if (options?.includeAssets) { + searchParams.set('assets', '1'); + } const query = searchParams.toString() ? `?${searchParams.toString()}` : ''; setIsDownloading(true); @@ -359,11 +365,28 @@ export function ProjectContentClient({ - startProjectDownload()}> + setIncludeAssetsInDownload(checked === true)} + onSelect={(event) => event.preventDefault()} + > + Include assets + + + + startProjectDownload(undefined, { includeAssets: includeAssetsInDownload }) + } + > Latest version only startProjectDownload(undefined, { allVersions: true })} + onClick={() => + startProjectDownload(undefined, { + allVersions: true, + includeAssets: includeAssetsInDownload, + }) + } > All versions @@ -446,11 +469,30 @@ export function ProjectContentClient({ - startProjectDownload(selectedVideoIds)}> + setIncludeAssetsInDownload(checked === true)} + onSelect={(event) => event.preventDefault()} + > + Include assets + + + + startProjectDownload(selectedVideoIds, { + includeAssets: includeAssetsInDownload, + }) + } + > Latest version only startProjectDownload(selectedVideoIds, { allVersions: true })} + onClick={() => + startProjectDownload(selectedVideoIds, { + allVersions: true, + includeAssets: includeAssetsInDownload, + }) + } > All versions diff --git a/app/api/projects/[projectId]/download/route.ts b/app/api/projects/[projectId]/download/route.ts index 24668fa..9dfd30d 100644 --- a/app/api/projects/[projectId]/download/route.ts +++ b/app/api/projects/[projectId]/download/route.ts @@ -23,6 +23,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) { const { projectId } = await params; const requestedVideoIds = parseRequestedVideoIds(request.nextUrl.searchParams.get('videoIds')); const includeAllVersions = request.nextUrl.searchParams.get('versions') === 'all'; + const includeAssets = request.nextUrl.searchParams.get('assets') === '1'; if (requestedVideoIds && requestedVideoIds.length === 0) { return apiErrors.badRequest('At least one video must be selected for download'); @@ -93,7 +94,10 @@ export async function GET(request: NextRequest, { params }: RouteParams) { } } - const manifest = buildProjectDownloadManifest(project.name, videos, { includeAllVersions }); + const manifest = buildProjectDownloadManifest(project.name, videos, { + includeAllVersions, + includeAssets, + }); const validationError = validateProjectDownloadManifest(manifest); if (validationError) { return apiErrors.badRequest(validationError); diff --git a/lib/project-download.ts b/lib/project-download.ts index af00f50..09c7a71 100644 --- a/lib/project-download.ts +++ b/lib/project-download.ts @@ -223,6 +223,8 @@ function bigintToSafeNumber(value: bigint): number | null { export type BuildProjectDownloadManifestOptions = { /** Include every version of each video. Defaults to latest version only. */ includeAllVersions?: boolean; + /** Include b-rolls and other attached assets. Defaults to videos only. */ + includeAssets?: boolean; }; export function buildProjectDownloadManifest( @@ -230,7 +232,7 @@ export function buildProjectDownloadManifest( videos: VideoRow[], options: BuildProjectDownloadManifestOptions = {} ): ProjectDownloadManifest { - const { includeAllVersions = false } = options; + const { includeAllVersions = false, includeAssets = false } = options; const files: ProjectDownloadManifestFile[] = []; const usedNames = new Set(); @@ -257,15 +259,17 @@ export function buildProjectDownloadManifest( }); } - for (const asset of video.assets) { - const url = assetDownloadUrl(video.id, asset); - if (!url) continue; + if (includeAssets) { + for (const asset of video.assets) { + const url = assetDownloadUrl(video.id, asset); + if (!url) continue; - files.push({ - fileName: makeUniqueName(buildAssetFileName(videoIndex, videoTitle, asset), usedNames), - url, - sizeBytes: bigintToSafeNumber(asset.sizeBytes), - }); + files.push({ + fileName: makeUniqueName(buildAssetFileName(videoIndex, videoTitle, asset), usedNames), + url, + sizeBytes: bigintToSafeNumber(asset.sizeBytes), + }); + } } });