From 59a6c539a150adf9ff9ed3d91854b2bea6e7c517 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Thu, 3 Jul 2025 20:43:23 +0200 Subject: feat: implement media panel with tab navigation --- apps/web/src/components/editor/media-panel.tsx | 389 ------------------------- 1 file changed, 389 deletions(-) delete mode 100644 apps/web/src/components/editor/media-panel.tsx (limited to 'apps/web/src/components/editor/media-panel.tsx') diff --git a/apps/web/src/components/editor/media-panel.tsx b/apps/web/src/components/editor/media-panel.tsx deleted file mode 100644 index 73ac0ada..00000000 --- a/apps/web/src/components/editor/media-panel.tsx +++ /dev/null @@ -1,389 +0,0 @@ -"use client"; - -import { useDragDrop } from "@/hooks/use-drag-drop"; -import { processMediaFiles } from "@/lib/media-processing"; -import { useMediaStore, type MediaItem } from "@/stores/media-store"; -import { useTimelineStore } from "@/stores/timeline-store"; -import { Image, Music, MusicIcon, Plus, TextIcon, Upload, Video, VideoIcon } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; -import { toast } from "sonner"; -import { AspectRatio } from "../ui/aspect-ratio"; -import { Button } from "../ui/button"; -import { DragOverlay } from "../ui/drag-overlay"; -import { - ContextMenu, - ContextMenuContent, - ContextMenuItem, - ContextMenuTrigger, -} from "../ui/context-menu"; -import { Input } from "../ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "../ui/select"; -import { cn } from "@/lib/utils"; - -// MediaPanel lets users add, view, and drag media (images, videos, audio) into the project. -// You can upload files or drag them from your computer. Dragging from here to the timeline adds them to your video project. - -export function MediaPanel() { - const { mediaItems, addMediaItem, removeMediaItem } = useMediaStore(); - const fileInputRef = useRef(null); - const [isProcessing, setIsProcessing] = useState(false); - const [progress, setProgress] = useState(0); - const [searchQuery, setSearchQuery] = useState(""); - const [mediaFilter, setMediaFilter] = useState("all"); - - const processFiles = async (files: FileList | File[]) => { - if (!files || files.length === 0) return; - setIsProcessing(true); - setProgress(0); - try { - // Process files (extract metadata, generate thumbnails, etc.) - const processedItems = await processMediaFiles(files, (p) => - setProgress(p) - ); - // Add each processed media item to the store - for (const item of processedItems) { - await addMediaItem(item); - } - } catch (error) { - // Show error toast if processing fails - 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) => { - // When files are selected via file picker, process them - if (e.target.files) processFiles(e.target.files); - e.target.value = ""; // Reset input - }; - - const handleRemove = async (e: React.MouseEvent, id: string) => { - // Remove a media item from the store - e.stopPropagation(); - - // Remove tracks automatically when delete media - const { tracks, removeTrack } = useTimelineStore.getState(); - tracks.forEach((track) => { - const clipsToRemove = track.clips.filter((clip) => clip.mediaId === id); - clipsToRemove.forEach((clip) => { - useTimelineStore.getState().removeClipFromTrack(track.id, clip.id); - }); - // Only remove track if it becomes empty and has no other clips - const updatedTrack = useTimelineStore - .getState() - .tracks.find((t) => t.id === track.id); - if (updatedTrack && updatedTrack.clips.length === 0) { - removeTrack(track.id); - } - }); - await removeMediaItem(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 startDrag = (e: React.DragEvent, item: MediaItem) => { - // When dragging a media item, set drag data for timeline to read - e.dataTransfer.setData( - "application/x-media-item", - JSON.stringify({ - id: item.id, - type: item.type, - name: item.name, - }) - ); - e.dataTransfer.effectAllowed = "copy"; - }; - - const [filteredMediaItems, setFilteredMediaItems] = useState(mediaItems); - - useEffect(() => { - const filtered = mediaItems.filter((item) => { - if (mediaFilter && mediaFilter !== "all" && item.type !== mediaFilter) { - return false; - } - - if ( - searchQuery && - !item.name.toLowerCase().includes(searchQuery.toLowerCase()) - ) { - return false; - } - - return true; - }); - - setFilteredMediaItems(filtered); - }, [mediaItems, mediaFilter, searchQuery]); - - const renderPreview = (item: MediaItem) => { - // Render a preview for each media type (image, video, audio, unknown) - if (item.type === "image") { - return ( -
- {item.name} -
- ); - } - - if (item.type === "video") { - if (item.thumbnailUrl) { - return ( -
- {item.name} -
-
- {item.duration && ( -
- {formatDuration(item.duration)} -
- )} -
- ); - } - return ( -
-
- ); - } - - if (item.type === "audio") { - return ( -
- - Audio - {item.duration && ( - - {formatDuration(item.duration)} - - )} -
- ); - } - - return ( -
- - Unknown -
- ); - }; - - return ( - <> - {/* Hidden file input for uploading media */} - - -
- {/* Show overlay when dragging files over the panel */} - - - - -
- {/* Button to add/upload media */} -
- {/* Search and filter controls */} - - setSearchQuery(e.target.value)} - /> - - {/* Add media button */} - -
-
- -
- {/* Show message if no media, otherwise show media grid */} - {filteredMediaItems.length === 0 ? ( -
-
- -
-

- No media in project -

-

- Drag files here or use the button above -

-
- ) : ( -
- {/* Render each media item as a draggable button */} - {filteredMediaItems.map((item) => ( -
- - - - - - Export clips - handleRemove(e, item.id)} - > - Delete - - - -
- ))} -
- )} -
-
- - ); -} - -type Tab = "media" | "audio" | "text"; - -function TabBar() { - const [activeTab, setActiveTab] = useState("media"); - - const tabs = [ - { - icon: VideoIcon, - label: "Media", - }, - { - icon: MusicIcon, - label: "Audio", - }, - { - icon: TextIcon, - label: "Text", - }, - ]; - - return ( -
- {tabs.map((tab) => ( -
setActiveTab(tab.label.toLowerCase() as Tab)} - key={tab.label} - > - - {tab.label} -
- ))} -
- ); -} -- cgit v1.3.1