"use client"; import Image from "next/image"; import { useMemo, useState } from "react"; import { PanelView } from "@/components/editor/panels/assets/views/base-panel"; import { MediaDragOverlay } from "@/components/editor/panels/assets/drag-overlay"; import { DraggableItem } from "@/components/editor/panels/assets/draggable-item"; import { Button } from "@/components/ui/button"; import { toast } from "sonner"; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger, } from "@/components/ui/context-menu"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { DEFAULT_NEW_ELEMENT_DURATION } from "@/timeline/creation"; import { mediaTimeFromSeconds, type MediaTime } from "@/wasm"; import { useEditor } from "@/editor/use-editor"; import { useFileUpload } from "@/media/use-file-upload"; import { invokeAction } from "@/actions"; import { processMediaAssets } from "@/media/processing"; import { showMediaUploadToast } from "@/media/upload-toast"; import { SelectableItem, SelectableSurface, useSelection, useSelectionScope, } from "@/selection"; import { buildElementFromMedia } from "@/timeline/element-utils"; import { type MediaSortKey, type MediaSortOrder, type MediaViewMode, useAssetsPanelStore, } from "@/components/editor/panels/assets/assets-panel-store"; import { MASKABLE_ELEMENT_TYPES } from "@/timeline"; import type { MediaAsset } from "@/media/types"; import { cn } from "@/utils/ui"; import { CloudUploadIcon, GridViewIcon, LeftToRightListDashIcon, SortingOneNineIcon, Image02Icon, MusicNote03Icon, Video01Icon, } from "@hugeicons/core-free-icons"; import { HugeiconsIcon, type IconSvgElement } from "@hugeicons/react"; export function MediaView() { const editor = useEditor(); const mediaFiles = useEditor((e) => e.media.getAssets()); const activeProject = useEditor((e) => e.project.getActive()); const { mediaViewMode, setMediaViewMode, highlightMediaId, clearHighlight, mediaSortBy, mediaSortOrder, setMediaSort, } = useAssetsPanelStore(); const [isProcessing, setIsProcessing] = useState(false); const [progress, setProgress] = useState(0); const processFiles = async ({ files }: { files: File[] }) => { if (!files || files.length === 0) return; if (!activeProject) { toast.error("No active project"); return; } setIsProcessing(true); setProgress(0); try { await showMediaUploadToast({ filesCount: files.length, promise: async () => { const processedAssets = await processMediaAssets({ files, onProgress: (progress: { progress: number }) => setProgress(progress.progress), }); for (const asset of processedAssets) { await editor.media.addMediaAsset({ projectId: activeProject.metadata.id, asset, }); } return { uploadedCount: processedAssets.length, assetNames: processedAssets.map((asset) => asset.name), }; }, }); } catch (error) { console.error("Error processing files:", error); } finally { setIsProcessing(false); setProgress(0); } }; const { isDragOver, dragProps, openFilePicker, fileInputProps } = useFileUpload({ accept: "image/*,video/*,audio/*", multiple: true, onFilesSelected: (files) => processFiles({ files }), }); const handleRemove = ({ event, ids, }: { event: React.MouseEvent; ids: string[]; }) => { event.stopPropagation(); invokeAction("remove-media-assets", { projectId: activeProject.metadata.id, assetIds: ids, }); }; const handleSort = ({ key }: { key: MediaSortKey }) => { if (mediaSortBy === key) { setMediaSort({ key, order: mediaSortOrder === "asc" ? "desc" : "asc", }); } else { setMediaSort({ key, order: "asc" }); } }; const filteredMediaItems = useMemo(() => { const filtered = mediaFiles.filter((item) => !item.ephemeral); filtered.sort((a, b) => { let valueA: string | number; let valueB: string | number; switch (mediaSortBy) { case "name": valueA = a.name.toLowerCase(); valueB = b.name.toLowerCase(); break; case "type": valueA = a.type; valueB = b.type; break; case "duration": valueA = a.duration || 0; valueB = b.duration || 0; break; case "size": valueA = a.file.size; valueB = b.file.size; break; default: return 0; } if (valueA < valueB) return mediaSortOrder === "asc" ? -1 : 1; if (valueA > valueB) return mediaSortOrder === "asc" ? 1 : -1; return 0; }); return filtered; }, [mediaFiles, mediaSortBy, mediaSortOrder]); const orderedMediaIds = useMemo(() => { return filteredMediaItems.map((item) => item.id); }, [filteredMediaItems]); return ( <> } className={cn(isDragOver && "bg-accent/30")} contentClassName="h-full" {...dragProps} > {isDragOver || filteredMediaItems.length === 0 ? ( ) : ( )} ); } function MediaScopeRegistrar() { useSelectionScope(); return null; } function MediaAssetDraggable({ item, preview, variant, isRounded, }: { item: MediaAsset; preview: React.ReactNode; variant: "card" | "compact"; isRounded?: boolean; }) { const editor = useEditor(); const addElementAtTime = ({ asset, startTime, }: { asset: MediaAsset; startTime: MediaTime; }) => { const duration = asset.duration != null ? mediaTimeFromSeconds({ seconds: asset.duration }) : DEFAULT_NEW_ELEMENT_DURATION; const element = buildElementFromMedia({ mediaId: asset.id, mediaType: asset.type, name: asset.name, duration, startTime, }); editor.timeline.insertElement({ element, placement: { mode: "auto" }, }); }; return ( addElementAtTime({ asset: item, startTime: currentTime }) } variant={variant} isRounded={isRounded} /> ); } function MediaItemWithContextMenu({ item, children, onRemove, }: { item: MediaAsset; children: React.ReactNode; onRemove: ({ event, ids, }: { event: React.MouseEvent; ids: string[]; }) => void; }) { const { isSelected, selectedIds } = useSelection(); const idsToDelete = isSelected(item.id) ? selectedIds : [item.id]; const deleteLabel = idsToDelete.length > 1 ? `Delete ${idsToDelete.length} items` : "Delete"; return ( {children} Export clips ) => onRemove({ event, ids: idsToDelete }) } > {deleteLabel} ); } function MediaItemList({ items, mode, onRemove, }: { items: MediaAsset[]; mode: MediaViewMode; onRemove: ({ event, ids, }: { event: React.MouseEvent; ids: string[]; }) => void; }) { const isGrid = mode === "grid"; return (
{items.map((item) => ( } variant={isGrid ? "card" : "compact"} isRounded={isGrid ? false : undefined} /> ))}
); } function formatDuration({ duration }: { duration: number }) { const min = Math.floor(duration / 60); const sec = Math.floor(duration % 60); return `${min}:${sec.toString().padStart(2, "0")}`; } function MediaDurationBadge({ duration }: { duration?: number }) { if (!duration) return null; return (
{formatDuration({ duration })}
); } function MediaDurationLabel({ duration }: { duration?: number }) { if (!duration) return null; return ( {formatDuration({ duration })} ); } function MediaTypePlaceholder({ icon, label, duration, variant, }: { icon: IconSvgElement; label: string; duration?: number; variant: "muted" | "bordered"; }) { const iconClassName = cn("size-6", variant === "bordered" && "mb-1"); return (
{label}
); } function MediaPreview({ item, variant = "grid", }: { item: MediaAsset; variant?: "grid" | "compact"; }) { const shouldShowDurationBadge = variant === "grid"; if (item.type === "image") { return (
{item.name}
); } if (item.type === "video") { if (item.thumbnailUrl) { return (
{item.name} {shouldShowDurationBadge ? ( ) : null}
); } return ( ); } if (item.type === "audio") { return ( ); } return ( ); } function MediaActions({ mediaViewMode, setMediaViewMode, isProcessing, sortBy, sortOrder, onSort, onImport, }: { mediaViewMode: MediaViewMode; setMediaViewMode: (mode: MediaViewMode) => void; isProcessing: boolean; sortBy: MediaSortKey; sortOrder: MediaSortOrder; onSort: ({ key }: { key: MediaSortKey }) => void; onImport: () => void; }) { return (

{mediaViewMode === "grid" ? "Switch to list view" : "Switch to grid view"}

Sort by {sortBy} ( {sortOrder === "asc" ? "ascending" : "descending"})

); } function SortMenuItem({ label, sortKey, currentSortBy, currentSortOrder, onSort, }: { label: string; sortKey: MediaSortKey; currentSortBy: MediaSortKey; currentSortOrder: MediaSortOrder; onSort: ({ key }: { key: MediaSortKey }) => void; }) { const isActive = currentSortBy === sortKey; const arrow = isActive ? (currentSortOrder === "asc" ? "↑" : "↓") : ""; return ( onSort({ key: sortKey })}> {label} {arrow} ); }