From 68da487f304018b6e8d06459def8fb45514ab7d4 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Fri, 1 Aug 2025 19:57:26 +0200 Subject: style: complete re-design of whole editor --- .../src/components/editor/media-panel/index.tsx | 6 +- .../src/components/editor/media-panel/tabbar.tsx | 5 +- .../components/editor/media-panel/views/audio.tsx | 1 + .../components/editor/media-panel/views/media.tsx | 507 +++++++++++++++------ .../components/editor/media-panel/views/text.tsx | 2 +- 5 files changed, 385 insertions(+), 136 deletions(-) (limited to 'apps/web/src/components/editor/media-panel') diff --git a/apps/web/src/components/editor/media-panel/index.tsx b/apps/web/src/components/editor/media-panel/index.tsx index 359612f8..a5cd6686 100644 --- a/apps/web/src/components/editor/media-panel/index.tsx +++ b/apps/web/src/components/editor/media-panel/index.tsx @@ -5,6 +5,7 @@ import { MediaView } from "./views/media"; import { useMediaPanelStore, Tab } from "./store"; import { TextView } from "./views/text"; import { AudioView } from "./views/audio"; +import { Separator } from "@/components/ui/separator"; export function MediaPanel() { const { activeTab } = useMediaPanelStore(); @@ -46,9 +47,10 @@ export function MediaPanel() { }; return ( -
+
-
{viewMap[activeTab]}
+ +
{viewMap[activeTab]}
); } diff --git a/apps/web/src/components/editor/media-panel/tabbar.tsx b/apps/web/src/components/editor/media-panel/tabbar.tsx index 92482559..a3733e4d 100644 --- a/apps/web/src/components/editor/media-panel/tabbar.tsx +++ b/apps/web/src/components/editor/media-panel/tabbar.tsx @@ -61,7 +61,7 @@ export function TabBar() { }, []); return ( -
+
{(Object.keys(tabs) as Tab[]).map((tabKey) => { const tab = tabs[tabKey]; @@ -83,7 +83,6 @@ export function TabBar() { key={tabKey} > - {tab.label}
); })} diff --git a/apps/web/src/components/editor/media-panel/views/audio.tsx b/apps/web/src/components/editor/media-panel/views/audio.tsx index ba64d749..0d3c3f1e 100644 --- a/apps/web/src/components/editor/media-panel/views/audio.tsx +++ b/apps/web/src/components/editor/media-panel/views/audio.tsx @@ -9,6 +9,7 @@ export function AudioView() {
setSearch(e.target.value)} /> diff --git a/apps/web/src/components/editor/media-panel/views/media.tsx b/apps/web/src/components/editor/media-panel/views/media.tsx index d1533736..772ef4ab 100644 --- a/apps/web/src/components/editor/media-panel/views/media.tsx +++ b/apps/web/src/components/editor/media-panel/views/media.tsx @@ -3,8 +3,18 @@ import { useDragDrop } from "@/hooks/use-drag-drop"; import { processMediaFiles } from "@/lib/media-processing"; import { useMediaStore, type MediaItem } from "@/stores/media-store"; -import { Image, Loader2, Music, Plus, Video } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; +import { + ArrowDown01, + CloudUpload, + Grid2X2, + Image, + List, + Loader2, + Music, + Search, + Video, +} from "lucide-react"; +import { useEffect, useRef, useState, useMemo } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { MediaDragOverlay } from "@/components/editor/media-panel/drag-overlay"; @@ -14,27 +24,62 @@ import { ContextMenuItem, ContextMenuTrigger, } from "@/components/ui/context-menu"; -import { Input } from "@/components/ui/input"; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; + 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 { ScrollArea } from "@/components/ui/scroll-area"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { usePanelStore } from "@/stores/panel-store"; + +function MediaItemWithContextMenu({ + item, + children, + onRemove, +}: { + item: MediaItem; + children: React.ReactNode; + onRemove: (e: React.MouseEvent, id: string) => Promise; +}) { + return ( + + {children} + + Export clips + onRemove(e, item.id)} + > + Delete + + + + ); +} export function MediaView() { const { mediaItems, addMediaItem, removeMediaItem } = useMediaStore(); const { activeProject } = useProjectStore(); + const { mediaViewMode, setMediaViewMode } = usePanelStore(); const fileInputRef = useRef(null); const [isProcessing, setIsProcessing] = useState(false); const [progress, setProgress] = useState(0); const [searchQuery, setSearchQuery] = useState(""); const [mediaFilter, setMediaFilter] = useState("all"); + const [sortBy, setSortBy] = useState<"name" | "type" | "duration" | "size">( + "name" + ); + const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); const processFiles = async (files: FileList | File[]) => { if (!files || files.length === 0) return; @@ -100,7 +145,7 @@ export function MediaView() { const [filteredMediaItems, setFilteredMediaItems] = useState(mediaItems); useEffect(() => { - const filtered = mediaItems.filter((item) => { + let filtered = mediaItems.filter((item) => { if (mediaFilter && mediaFilter !== "all" && item.type !== mediaFilter) { return false; } @@ -115,79 +160,117 @@ export function MediaView() { return true; }); + filtered.sort((a, b) => { + let valueA: any; + let valueB: any; + + 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; + }); + setFilteredMediaItems(filtered); - }, [mediaItems, mediaFilter, searchQuery]); + }, [mediaItems, mediaFilter, searchQuery, sortBy, sortOrder]); - const renderPreview = (item: MediaItem) => { - // Render a preview for each media type (image, video, audio, unknown) - if (item.type === "image") { - return ( -
- {item.name} -
- ); - } + const previewComponents = useMemo(() => { + const previews = new Map(); - if (item.type === "video") { - if (item.thumbnailUrl) { - return ( -
+ filteredMediaItems.forEach((item) => { + let preview: React.ReactNode; + + if (item.type === "image") { + preview = ( +
{item.name} -
-
+ ); + } else if (item.type === "video") { + if (item.thumbnailUrl) { + preview = ( +
+ {item.name} +
+
+ {item.duration && ( +
+ {formatDuration(item.duration)} +
+ )} +
+ ); + } else { + preview = ( +
+
+ ); + } + } else if (item.type === "audio") { + preview = ( +
+ + Audio {item.duration && ( -
+ {formatDuration(item.duration)} -
+ )}
); + } else { + preview = ( +
+ + Unknown +
+ ); } - return ( -
-
- ); - } - if (item.type === "audio") { - return ( -
- - Audio - {item.duration && ( - - {formatDuration(item.duration)} - - )} -
- ); - } + previews.set(item.id, preview); + }); - return ( -
- - Unknown -
- ); - }; + return previews; + }, [filteredMediaItems]); + + const renderPreview = (item: MediaItem) => previewComponents.get(item.id); return ( <> @@ -207,43 +290,152 @@ export function MediaView() { >
{/* Search and filter controls */} -
- - setSearchQuery(e.target.value)} - /> +
+
+ + + + + + +

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

+
+ + + + + + + + + { + if (sortBy === "name") { + setSortOrder( + sortOrder === "asc" ? "desc" : "asc" + ); + } else { + setSortBy("name"); + setSortOrder("asc"); + } + }} + > + Name{" "} + {sortBy === "name" && + (sortOrder === "asc" ? "↑" : "↓")} + + { + if (sortBy === "type") { + setSortOrder( + sortOrder === "asc" ? "desc" : "asc" + ); + } else { + setSortBy("type"); + setSortOrder("asc"); + } + }} + > + Type{" "} + {sortBy === "type" && + (sortOrder === "asc" ? "↑" : "↓")} + + { + if (sortBy === "duration") { + setSortOrder( + sortOrder === "asc" ? "desc" : "asc" + ); + } else { + setSortBy("duration"); + setSortOrder("asc"); + } + }} + > + Duration{" "} + {sortBy === "duration" && + (sortOrder === "asc" ? "↑" : "↓")} + + { + if (sortBy === "size") { + setSortOrder( + sortOrder === "asc" ? "desc" : "asc" + ); + } else { + setSortBy("size"); + setSortOrder("asc"); + } + }} + > + File Size{" "} + {sortBy === "size" && + (sortOrder === "asc" ? "↑" : "↓")} + + + + +

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

+
+
+
+
+
- -
+
+
{isDragOver || filteredMediaItems.length === 0 ? ( + ) : mediaViewMode === "grid" ? ( + ) : ( -
- {/* Render each media item as a draggable button */} - {filteredMediaItems.map((item) => ( - - - - useTimelineStore - .getState() - .addMediaAtTime(item, currentTime) - } - rounded={false} - /> - - - Export clips - handleRemove(e, item.id)} - > - Delete - - - - ))} -
+ )}
- +
); } + +function GridView({ + filteredMediaItems, + renderPreview, + handleRemove, +}: { + filteredMediaItems: MediaItem[]; + renderPreview: (item: MediaItem) => React.ReactNode; + handleRemove: (e: React.MouseEvent, id: string) => Promise; +}) { + return ( +
+ {filteredMediaItems.map((item) => ( + + + useTimelineStore.getState().addMediaAtTime(item, currentTime) + } + rounded={false} + variant="card" + /> + + ))} +
+ ); +} + +function ListView({ + filteredMediaItems, + renderPreview, + handleRemove, + formatDuration, +}: { + filteredMediaItems: MediaItem[]; + renderPreview: (item: MediaItem) => React.ReactNode; + handleRemove: (e: React.MouseEvent, id: string) => Promise; + formatDuration: (duration: number) => string; +}) { + return ( +
+ {filteredMediaItems.map((item) => ( + + + useTimelineStore.getState().addMediaAtTime(item, currentTime) + } + variant="compact" + /> + + ))} +
+ ); +} diff --git a/apps/web/src/components/editor/media-panel/views/text.tsx b/apps/web/src/components/editor/media-panel/views/text.tsx index 0686190e..ca8e88c9 100644 --- a/apps/web/src/components/editor/media-panel/views/text.tsx +++ b/apps/web/src/components/editor/media-panel/views/text.tsx @@ -32,7 +32,7 @@ export function TextView() { +
Default text
} -- cgit v1.3.1