feat: make asset downloads opt-in via "Include assets" toggle

Project/selected downloads now include only videos by default. Add an
"Include assets" checkbox toggle to both download dropdowns (default off)
that adds b-rolls and other attached assets to the download when enabled.

- buildProjectDownloadManifest gains an includeAssets option (default false).
- Download route reads ?assets=1 and passes it through.
This commit is contained in:
yusufipk
2026-07-10 21:03:34 +07:00
parent 57c5a127d1
commit 8d7d064647
3 changed files with 65 additions and 15 deletions
@@ -27,8 +27,10 @@ import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'; } from '@/components/ui/dropdown-menu';
import { import {
@@ -105,6 +107,7 @@ export function ProjectContentClient({
const [selectedVideoIds, setSelectedVideoIds] = useState<string[]>([]); const [selectedVideoIds, setSelectedVideoIds] = useState<string[]>([]);
const [selectionMode, setSelectionMode] = useState(false); const [selectionMode, setSelectionMode] = useState(false);
const [isDownloading, setIsDownloading] = useState(false); const [isDownloading, setIsDownloading] = useState(false);
const [includeAssetsInDownload, setIncludeAssetsInDownload] = useState(false);
const [isDeletingSelected, setIsDeletingSelected] = useState(false); const [isDeletingSelected, setIsDeletingSelected] = useState(false);
const [showDeleteSelectedDialog, setShowDeleteSelectedDialog] = useState(false); const [showDeleteSelectedDialog, setShowDeleteSelectedDialog] = useState(false);
const [showMoveSelectedDialog, setShowMoveSelectedDialog] = useState(false); const [showMoveSelectedDialog, setShowMoveSelectedDialog] = useState(false);
@@ -184,7 +187,7 @@ export function ProjectContentClient({
}, []); }, []);
const startProjectDownload = useCallback( const startProjectDownload = useCallback(
async (videoIds?: string[], options?: { allVersions?: boolean }) => { async (videoIds?: string[], options?: { allVersions?: boolean; includeAssets?: boolean }) => {
if (!canDownloadProject || isDownloading) return; if (!canDownloadProject || isDownloading) return;
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
@@ -194,6 +197,9 @@ export function ProjectContentClient({
if (options?.allVersions) { if (options?.allVersions) {
searchParams.set('versions', 'all'); searchParams.set('versions', 'all');
} }
if (options?.includeAssets) {
searchParams.set('assets', '1');
}
const query = searchParams.toString() ? `?${searchParams.toString()}` : ''; const query = searchParams.toString() ? `?${searchParams.toString()}` : '';
setIsDownloading(true); setIsDownloading(true);
@@ -359,11 +365,28 @@ export function ProjectContentClient({
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => startProjectDownload()}> <DropdownMenuCheckboxItem
checked={includeAssetsInDownload}
onCheckedChange={(checked) => setIncludeAssetsInDownload(checked === true)}
onSelect={(event) => event.preventDefault()}
>
Include assets
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() =>
startProjectDownload(undefined, { includeAssets: includeAssetsInDownload })
}
>
Latest version only Latest version only
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => startProjectDownload(undefined, { allVersions: true })} onClick={() =>
startProjectDownload(undefined, {
allVersions: true,
includeAssets: includeAssetsInDownload,
})
}
> >
All versions All versions
</DropdownMenuItem> </DropdownMenuItem>
@@ -446,11 +469,30 @@ export function ProjectContentClient({
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => startProjectDownload(selectedVideoIds)}> <DropdownMenuCheckboxItem
checked={includeAssetsInDownload}
onCheckedChange={(checked) => setIncludeAssetsInDownload(checked === true)}
onSelect={(event) => event.preventDefault()}
>
Include assets
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() =>
startProjectDownload(selectedVideoIds, {
includeAssets: includeAssetsInDownload,
})
}
>
Latest version only Latest version only
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => startProjectDownload(selectedVideoIds, { allVersions: true })} onClick={() =>
startProjectDownload(selectedVideoIds, {
allVersions: true,
includeAssets: includeAssetsInDownload,
})
}
> >
All versions All versions
</DropdownMenuItem> </DropdownMenuItem>
@@ -23,6 +23,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
const { projectId } = await params; const { projectId } = await params;
const requestedVideoIds = parseRequestedVideoIds(request.nextUrl.searchParams.get('videoIds')); const requestedVideoIds = parseRequestedVideoIds(request.nextUrl.searchParams.get('videoIds'));
const includeAllVersions = request.nextUrl.searchParams.get('versions') === 'all'; const includeAllVersions = request.nextUrl.searchParams.get('versions') === 'all';
const includeAssets = request.nextUrl.searchParams.get('assets') === '1';
if (requestedVideoIds && requestedVideoIds.length === 0) { if (requestedVideoIds && requestedVideoIds.length === 0) {
return apiErrors.badRequest('At least one video must be selected for download'); 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); const validationError = validateProjectDownloadManifest(manifest);
if (validationError) { if (validationError) {
return apiErrors.badRequest(validationError); return apiErrors.badRequest(validationError);
+13 -9
View File
@@ -223,6 +223,8 @@ function bigintToSafeNumber(value: bigint): number | null {
export type BuildProjectDownloadManifestOptions = { export type BuildProjectDownloadManifestOptions = {
/** Include every version of each video. Defaults to latest version only. */ /** Include every version of each video. Defaults to latest version only. */
includeAllVersions?: boolean; includeAllVersions?: boolean;
/** Include b-rolls and other attached assets. Defaults to videos only. */
includeAssets?: boolean;
}; };
export function buildProjectDownloadManifest( export function buildProjectDownloadManifest(
@@ -230,7 +232,7 @@ export function buildProjectDownloadManifest(
videos: VideoRow[], videos: VideoRow[],
options: BuildProjectDownloadManifestOptions = {} options: BuildProjectDownloadManifestOptions = {}
): ProjectDownloadManifest { ): ProjectDownloadManifest {
const { includeAllVersions = false } = options; const { includeAllVersions = false, includeAssets = false } = options;
const files: ProjectDownloadManifestFile[] = []; const files: ProjectDownloadManifestFile[] = [];
const usedNames = new Set<string>(); const usedNames = new Set<string>();
@@ -257,15 +259,17 @@ export function buildProjectDownloadManifest(
}); });
} }
for (const asset of video.assets) { if (includeAssets) {
const url = assetDownloadUrl(video.id, asset); for (const asset of video.assets) {
if (!url) continue; const url = assetDownloadUrl(video.id, asset);
if (!url) continue;
files.push({ files.push({
fileName: makeUniqueName(buildAssetFileName(videoIndex, videoTitle, asset), usedNames), fileName: makeUniqueName(buildAssetFileName(videoIndex, videoTitle, asset), usedNames),
url, url,
sizeBytes: bigintToSafeNumber(asset.sizeBytes), sizeBytes: bigintToSafeNumber(asset.sizeBytes),
}); });
}
} }
}); });