mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 09:36:08 +00:00
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:
@@ -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
@@ -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),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user