"use client"; import { useState, useMemo } from "react"; import { ArrowDown01, CloudUpload, Grid2X2, Image, List, Loader2, Music, Video, } from "lucide-react"; import type { LucideIcon } from "lucide-react"; import { toast } from "sonner"; import { useEditor } from "@/hooks/use-editor"; import { useFileUpload } from "@/hooks/use-file-upload"; import { useRevealItem } from "@/hooks/use-reveal-item"; import { processMediaAssets } from "@/lib/media/processing"; import { cn } from "@/utils/ui"; import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; import type { MediaAsset } from "@/types/assets"; import type { CreateTimelineElement } from "@/types/timeline"; import { Button } from "@/components/ui/button"; import { MediaDragOverlay } from "@/components/editor/assets-panel/drag-overlay"; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger, } from "@/components/ui/context-menu"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { DraggableItem } from "@/components/editor/draggable-item"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { useAssetsPanelStore } from "@/stores/assets-panel-store"; export function MediaView() { const editor = useEditor(); const mediaFiles = editor.media.getAssets(); const activeProject = editor.project.getActive(); const { mediaViewMode, setMediaViewMode, highlightMediaId, clearHighlight } = useAssetsPanelStore(); const { highlightedId, registerElement } = useRevealItem( highlightMediaId, clearHighlight, ); const [isProcessing, setIsProcessing] = useState(false); const [progress, setProgress] = useState(0); const [sortBy, setSortBy] = useState<"name" | "type" | "duration" | "size">( "name", ); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); const processFiles = async ({ files }: { files: FileList }) => { if (!files || files.length === 0) return; if (!activeProject) { toast.error("No active project"); return; } setIsProcessing(true); setProgress(0); try { 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, }); } } catch (error) { console.error("Error processing files:", error); toast.error("Failed to process files"); } finally { setIsProcessing(false); setProgress(0); } }; const { isDragOver, dragProps, openFilePicker, fileInputProps } = useFileUpload({ accept: "image/*,video/*,audio/*", multiple: true, onFilesSelected: (files) => processFiles({ files }), }); const handleRemove = async ({ event, id, }: { event: React.MouseEvent; id: string; }) => { event.stopPropagation(); if (!activeProject) { toast.error("No active project"); return; } await editor.media.removeMediaAsset({ projectId: activeProject.metadata.id, id, }); }; const addElementAtTime = ({ asset, startTime, }: { asset: MediaAsset; startTime: number; }): boolean => { const element = createElementFromMedia({ asset, startTime }); editor.timeline.insertElement({ element, placement: { mode: "auto" }, }); return true; }; const filteredMediaItems = useMemo(() => { const filtered = mediaFiles.filter((item) => !item.ephemeral); filtered.sort((a, b) => { let valueA: string | number; let valueB: string | number; switch (sortBy) { 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 sortOrder === "asc" ? -1 : 1; if (valueA > valueB) return sortOrder === "asc" ? 1 : -1; return 0; }); return filtered; }, [mediaFiles, sortBy, sortOrder]); const previewComponents = useMemo(() => { const previews = new Map(); filteredMediaItems.forEach((item) => { previews.set(item.id, ); }); return previews; }, [filteredMediaItems]); const renderPreview = (item: MediaAsset) => previewComponents.get(item.id); return ( <>

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

{ if (sortBy === key) { setSortOrder( sortOrder === "asc" ? "desc" : "asc", ); } else { setSortBy(key); setSortOrder("asc"); } }} /> { if (sortBy === key) { setSortOrder( sortOrder === "asc" ? "desc" : "asc", ); } else { setSortBy(key); setSortOrder("asc"); } }} /> { if (sortBy === key) { setSortOrder( sortOrder === "asc" ? "desc" : "asc", ); } else { setSortBy(key); setSortOrder("asc"); } }} /> { if (sortBy === key) { setSortOrder( sortOrder === "asc" ? "desc" : "asc", ); } else { setSortBy(key); setSortOrder("asc"); } }} />

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

{isDragOver || filteredMediaItems.length === 0 ? ( ) : mediaViewMode === "grid" ? ( ) : ( )}
); } function MediaItemWithContextMenu({ item, children, onRemove, }: { item: MediaAsset; children: React.ReactNode; onRemove: ({ event, id }: { event: React.MouseEvent; id: string }) => void; }) { return ( {children} Export clips onRemove({ event, id: item.id })} > Delete ); } function GridView({ items, renderPreview, onRemove, onAddToTimeline, highlightedId, registerElement, }: { items: MediaAsset[]; renderPreview: (item: MediaAsset) => React.ReactNode; onRemove: ({ event, id }: { event: React.MouseEvent; id: string }) => void; onAddToTimeline: ({ asset, startTime, }: { asset: MediaAsset; startTime: number; }) => boolean; highlightedId: string | null; registerElement: (id: string, element: HTMLElement | null) => void; }) { return (
{items.map((item) => (
registerElement(item.id, el)}> onAddToTimeline({ asset: item, startTime: currentTime }) } isRounded={false} variant="card" isHighlighted={highlightedId === item.id} />
))}
); } function ListView({ items, renderPreview, onRemove, onAddToTimeline, highlightedId, registerElement, }: { items: MediaAsset[]; renderPreview: (item: MediaAsset) => React.ReactNode; onRemove: ({ event, id }: { event: React.MouseEvent; id: string }) => void; onAddToTimeline: ({ asset, startTime, }: { asset: MediaAsset; startTime: number; }) => boolean; highlightedId: string | null; registerElement: (id: string, element: HTMLElement | null) => void; }) { return (
{items.map((item) => (
registerElement(item.id, el)}> onAddToTimeline({ asset: item, startTime: currentTime }) } variant="compact" isHighlighted={highlightedId === item.id} />
))}
); } const 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: Icon, label, duration, variant, }: { icon: LucideIcon; label: string; duration?: number; variant: "muted" | "bordered"; }) { const iconClassName = cn("size-6", variant === "bordered" && "mb-1"); return (
); } function MediaPreview({ item }: { item: MediaAsset }) { if (item.type === "image") { return (
{item.name}
); } if (item.type === "video") { if (item.thumbnailUrl) { return (
{item.name}
); } return ( ); } if (item.type === "audio") { return ( ); } return ; } function SortMenuItem({ label, sortKey, currentSortBy, currentSortOrder, onSort, }: { label: string; sortKey: "name" | "type" | "duration" | "size"; currentSortBy: string; currentSortOrder: "asc" | "desc"; onSort: ({ key }: { key: "name" | "type" | "duration" | "size" }) => void; }) { const isActive = currentSortBy === sortKey; const arrow = isActive ? (currentSortOrder === "asc" ? "↑" : "↓") : ""; return ( onSort({ key: sortKey })}> {label} {arrow} ); } function createElementFromMedia({ asset, startTime, }: { asset: MediaAsset; startTime: number; }): CreateTimelineElement { const duration = asset.duration ?? TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION; switch (asset.type) { case "video": return { type: "video", name: asset.name, mediaId: asset.id, startTime, duration, trimStart: 0, trimEnd: 0, muted: false, hidden: false, transform: { scale: 1, position: { x: 0, y: 0 }, rotate: 0 }, opacity: 1, }; case "image": return { type: "image", name: asset.name, mediaId: asset.id, startTime, duration, trimStart: 0, trimEnd: 0, hidden: false, transform: { scale: 1, position: { x: 0, y: 0 }, rotate: 0 }, opacity: 1, }; case "audio": return { type: "audio", sourceType: "upload", name: asset.name, mediaId: asset.id, startTime, duration, trimStart: 0, trimEnd: 0, volume: 1, muted: false, }; default: throw new Error(`Unsupported media type: ${asset.type}`); } }