diff options
| author | Maze <167211895+mazeincoding@users.noreply.github.com> | 2026-01-31 00:20:04 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-31 00:20:04 +0100 |
| commit | 7bf0984698e43b9aa2dd879f65951318ed410390 (patch) | |
| tree | ccf2cd590a47d550152afc07129cb55c71a9ec35 /apps/web/src/components/editor/media-panel/views/media.tsx | |
| parent | 0173db99448d1c4c6415dcf15272e17551b48b04 (diff) | |
| download | opencut-7bf0984698e43b9aa2dd879f65951318ed410390.tar.gz | |
codebase overhaul (#697)
Diffstat (limited to 'apps/web/src/components/editor/media-panel/views/media.tsx')
| -rw-r--r-- | apps/web/src/components/editor/media-panel/views/media.tsx | 545 |
1 files changed, 0 insertions, 545 deletions
diff --git a/apps/web/src/components/editor/media-panel/views/media.tsx b/apps/web/src/components/editor/media-panel/views/media.tsx deleted file mode 100644 index 07a44a7c..00000000 --- a/apps/web/src/components/editor/media-panel/views/media.tsx +++ /dev/null @@ -1,545 +0,0 @@ -"use client"; - -import { useDragDrop } from "@/hooks/use-drag-drop"; -import { processMediaFiles } from "@/lib/media-processing"; -import { useMediaStore } from "@/stores/media-store"; -import { MediaFile } from "@/types/media"; -import { - ArrowDown01, - CloudUpload, - Grid2X2, - Image, - List, - Loader2, - Music, - Video, -} from "lucide-react"; -import { useRef, useState, useMemo } from "react"; -import { useHighlightScroll } from "@/hooks/use-highlight-scroll"; -import { toast } from "sonner"; -import { Button } from "@/components/ui/button"; -import { MediaDragOverlay } from "@/components/editor/media-panel/drag-overlay"; -import { - ContextMenu, - ContextMenuContent, - ContextMenuItem, - ContextMenuTrigger, -} from "@/components/ui/context-menu"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { DraggableMediaItem } from "@/components/ui/draggable-item"; -import { useProjectStore } from "@/stores/project-store"; -import { useTimelineStore } from "@/stores/timeline-store"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { usePanelStore } from "@/stores/panel-store"; -import { useMediaPanelStore } from "../store"; - -function MediaItemWithContextMenu({ - item, - children, - onRemove, -}: { - item: MediaFile; - children: React.ReactNode; - onRemove: (e: React.MouseEvent, id: string) => Promise<void>; -}) { - return ( - <ContextMenu> - <ContextMenuTrigger>{children}</ContextMenuTrigger> - <ContextMenuContent> - <ContextMenuItem>Export clips</ContextMenuItem> - <ContextMenuItem - variant="destructive" - onClick={(e) => onRemove(e, item.id)} - > - Delete - </ContextMenuItem> - </ContextMenuContent> - </ContextMenu> - ); -} - -export function MediaView() { - const { mediaFiles, addMediaFile, removeMediaFile } = useMediaStore(); - const { activeProject } = useProjectStore(); - const { mediaViewMode, setMediaViewMode } = usePanelStore(); - const fileInputRef = useRef<HTMLInputElement>(null); - 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 { highlightMediaId, clearHighlight } = useMediaPanelStore(); - const { highlightedId, registerElement } = useHighlightScroll( - highlightMediaId, - clearHighlight - ); - - const processFiles = async (files: FileList | File[]) => { - if (!files || files.length === 0) return; - if (!activeProject) { - toast.error("No active project"); - return; - } - - setIsProcessing(true); - setProgress(0); - try { - const processedItems = await processMediaFiles(files, (p) => - setProgress(p) - ); - for (const item of processedItems) { - await addMediaFile(activeProject.id, item); - } - } catch (error) { - console.error("Error processing files:", error); - toast.error("Failed to process files"); - } finally { - setIsProcessing(false); - setProgress(0); - } - }; - - const { isDragOver, dragProps } = useDragDrop({ - // When files are dropped, process them - onDrop: processFiles, - }); - - const handleFileSelect = () => fileInputRef.current?.click(); // Open file picker - - const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { - if (e.target.files) processFiles(e.target.files); - e.target.value = ""; // Reset input - }; - - const handleRemove = async (e: React.MouseEvent, id: string) => { - e.stopPropagation(); - - if (!activeProject) { - toast.error("No active project"); - return; - } - - await removeMediaFile(activeProject.id, id); - }; - - const formatDuration = (duration: number) => { - // Format seconds as mm:ss - const min = Math.floor(duration / 60); - const sec = Math.floor(duration % 60); - return `${min}:${sec.toString().padStart(2, "0")}`; - }; - - const filteredMediaItems = useMemo(() => { - let filtered = mediaFiles.filter((item) => { - if (item.ephemeral) return false; - return true; - }); - - 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<string, React.ReactNode>(); - - filteredMediaItems.forEach((item) => { - let preview: React.ReactNode; - - if (item.type === "image") { - preview = ( - <div className="w-full h-full flex items-center justify-center"> - <img - src={item.url} - alt={item.name} - className="w-full max-h-full object-cover" - loading="lazy" - /> - </div> - ); - } else if (item.type === "video") { - if (item.thumbnailUrl) { - preview = ( - <div className="relative w-full h-full"> - <img - src={item.thumbnailUrl} - alt={item.name} - className="w-full h-full object-cover rounded" - loading="lazy" - /> - <div className="absolute inset-0 flex items-center justify-center bg-black/20 rounded"> - <Video className="h-6 w-6 text-white drop-shadow-md" /> - </div> - {item.duration && ( - <div className="absolute bottom-1 right-1 bg-black/70 text-white text-xs px-1 rounded"> - {formatDuration(item.duration)} - </div> - )} - </div> - ); - } else { - preview = ( - <div className="w-full h-full bg-muted/30 flex flex-col items-center justify-center text-muted-foreground rounded"> - <Video className="h-6 w-6 mb-1" /> - <span className="text-xs">Video</span> - {item.duration && ( - <span className="text-xs opacity-70"> - {formatDuration(item.duration)} - </span> - )} - </div> - ); - } - } else if (item.type === "audio") { - preview = ( - <div className="w-full h-full bg-linear-to-br from-green-500/20 to-emerald-500/20 flex flex-col items-center justify-center text-muted-foreground rounded border border-green-500/20"> - <Music className="h-6 w-6 mb-1" /> - <span className="text-xs">Audio</span> - {item.duration && ( - <span className="text-xs opacity-70"> - {formatDuration(item.duration)} - </span> - )} - </div> - ); - } else { - preview = ( - <div className="w-full h-full bg-muted/30 flex flex-col items-center justify-center text-muted-foreground rounded"> - <Image className="h-6 w-6" /> - <span className="text-xs mt-1">Unknown</span> - </div> - ); - } - - previews.set(item.id, preview); - }); - - return previews; - }, [filteredMediaItems]); - - const renderPreview = (item: MediaFile) => previewComponents.get(item.id); - - return ( - <> - {/* Hidden file input for uploading media */} - <input - ref={fileInputRef} - type="file" - accept="image/*,video/*,audio/*" - multiple - className="hidden" - onChange={handleFileChange} - /> - - <div - className={`h-full flex flex-col gap-1 transition-colors relative ${isDragOver ? "bg-accent/30" : ""}`} - {...dragProps} - > - <div className="p-3 pb-2 bg-panel"> - {/* Search and filter controls */} - <div className="flex items-center gap-2"> - <Button - variant="outline" - size="lg" - onClick={handleFileSelect} - disabled={isProcessing} - className="!bg-background px-4 flex-1 justify-center items-center h-9 opacity-100 hover:opacity-75 transition-opacity" - > - {isProcessing ? ( - <Loader2 className="h-4 w-4 animate-spin" /> - ) : ( - <CloudUpload className="h-4 w-4" /> - )} - <span>Upload</span> - </Button> - <div className="flex items-center gap-0"> - <TooltipProvider> - <Tooltip> - <TooltipTrigger asChild> - <Button - size="icon" - variant="text" - onClick={() => - setMediaViewMode( - mediaViewMode === "grid" ? "list" : "grid" - ) - } - disabled={isProcessing} - className="justify-center items-center" - > - {mediaViewMode === "grid" ? ( - <List strokeWidth={1.5} className="!size-[1.05rem]" /> - ) : ( - <Grid2X2 - strokeWidth={1.5} - className="!size-[1.05rem]" - /> - )} - </Button> - </TooltipTrigger> - <TooltipContent> - <p> - {mediaViewMode === "grid" - ? "Switch to list view" - : "Switch to grid view"} - </p> - </TooltipContent> - <Tooltip> - <DropdownMenu> - <TooltipTrigger asChild> - <DropdownMenuTrigger asChild> - <Button - size="icon" - variant="text" - disabled={isProcessing} - className="justify-center items-center" - > - <ArrowDown01 - strokeWidth={1.5} - className="!size-[1.05rem]" - /> - </Button> - </DropdownMenuTrigger> - </TooltipTrigger> - <DropdownMenuContent align="end"> - <DropdownMenuItem - onClick={() => { - if (sortBy === "name") { - setSortOrder( - sortOrder === "asc" ? "desc" : "asc" - ); - } else { - setSortBy("name"); - setSortOrder("asc"); - } - }} - > - Name{" "} - {sortBy === "name" && - (sortOrder === "asc" ? "↑" : "↓")} - </DropdownMenuItem> - <DropdownMenuItem - onClick={() => { - if (sortBy === "type") { - setSortOrder( - sortOrder === "asc" ? "desc" : "asc" - ); - } else { - setSortBy("type"); - setSortOrder("asc"); - } - }} - > - Type{" "} - {sortBy === "type" && - (sortOrder === "asc" ? "↑" : "↓")} - </DropdownMenuItem> - <DropdownMenuItem - onClick={() => { - if (sortBy === "duration") { - setSortOrder( - sortOrder === "asc" ? "desc" : "asc" - ); - } else { - setSortBy("duration"); - setSortOrder("asc"); - } - }} - > - Duration{" "} - {sortBy === "duration" && - (sortOrder === "asc" ? "↑" : "↓")} - </DropdownMenuItem> - <DropdownMenuItem - onClick={() => { - if (sortBy === "size") { - setSortOrder( - sortOrder === "asc" ? "desc" : "asc" - ); - } else { - setSortBy("size"); - setSortOrder("asc"); - } - }} - > - File Size{" "} - {sortBy === "size" && - (sortOrder === "asc" ? "↑" : "↓")} - </DropdownMenuItem> - </DropdownMenuContent> - </DropdownMenu> - <TooltipContent> - <p> - Sort by {sortBy} ( - {sortOrder === "asc" ? "ascending" : "descending"}) - </p> - </TooltipContent> - </Tooltip> - </Tooltip> - </TooltipProvider> - </div> - </div> - </div> - - <div className="h-full w-full overflow-y-auto scrollbar-thin pt-1"> - <div className="flex-1 p-3 pt-0 w-full"> - {isDragOver || filteredMediaItems.length === 0 ? ( - <MediaDragOverlay - isVisible={true} - isProcessing={isProcessing} - progress={progress} - onClick={handleFileSelect} - isEmptyState={filteredMediaItems.length === 0 && !isDragOver} - /> - ) : mediaViewMode === "grid" ? ( - <GridView - filteredMediaItems={filteredMediaItems} - renderPreview={renderPreview} - handleRemove={handleRemove} - highlightedId={highlightedId} - registerElement={registerElement} - /> - ) : ( - <ListView - filteredMediaItems={filteredMediaItems} - renderPreview={renderPreview} - handleRemove={handleRemove} - highlightedId={highlightedId} - registerElement={registerElement} - /> - )} - </div> - </div> - </div> - </> - ); -} - -function GridView({ - filteredMediaItems, - renderPreview, - handleRemove, - highlightedId, - registerElement, -}: { - filteredMediaItems: MediaFile[]; - renderPreview: (item: MediaFile) => React.ReactNode; - handleRemove: (e: React.MouseEvent, id: string) => Promise<void>; - highlightedId: string | null; - registerElement: (id: string, element: HTMLElement | null) => void; -}) { - const { addElementAtTime } = useTimelineStore(); - - return ( - <div - className="grid gap-2" - style={{ - gridTemplateColumns: "repeat(auto-fill, 160px)", - }} - > - {filteredMediaItems.map((item) => ( - <div key={item.id} ref={(el) => registerElement(item.id, el)}> - <MediaItemWithContextMenu item={item} onRemove={handleRemove}> - <DraggableMediaItem - name={item.name} - preview={renderPreview(item)} - dragData={{ - id: item.id, - type: item.type, - name: item.name, - }} - showPlusOnDrag={false} - onAddToTimeline={(currentTime) => - addElementAtTime(item, currentTime) - } - rounded={false} - variant="card" - isHighlighted={highlightedId === item.id} - /> - </MediaItemWithContextMenu> - </div> - ))} - </div> - ); -} - -function ListView({ - filteredMediaItems, - renderPreview, - handleRemove, - highlightedId, - registerElement, -}: { - filteredMediaItems: MediaFile[]; - renderPreview: (item: MediaFile) => React.ReactNode; - handleRemove: (e: React.MouseEvent, id: string) => Promise<void>; - highlightedId: string | null; - registerElement: (id: string, element: HTMLElement | null) => void; -}) { - const { addElementAtTime } = useTimelineStore(); - - return ( - <div className="space-y-1"> - {filteredMediaItems.map((item) => ( - <div key={item.id} ref={(el) => registerElement(item.id, el)}> - <MediaItemWithContextMenu item={item} onRemove={handleRemove}> - <DraggableMediaItem - name={item.name} - preview={renderPreview(item)} - dragData={{ - id: item.id, - type: item.type, - name: item.name, - }} - showPlusOnDrag={false} - onAddToTimeline={(currentTime) => - addElementAtTime(item, currentTime) - } - variant="compact" - isHighlighted={highlightedId === item.id} - /> - </MediaItemWithContextMenu> - </div> - ))} - </div> - ); -} |
