feat: implement reveal in media feature

This commit is contained in:
Maze Winther
2025-08-18 10:54:28 +02:00
parent 13dff382c4
commit 756a7c7caa
5 changed files with 145 additions and 57 deletions
@@ -71,9 +71,16 @@ export const tabs: { [key in Tab]: { icon: LucideIcon; label: string } } = {
interface MediaPanelStore {
activeTab: Tab;
setActiveTab: (tab: Tab) => void;
highlightMediaId: string | null;
requestRevealMedia: (mediaId: string) => void;
clearHighlight: () => void;
}
export const useMediaPanelStore = create<MediaPanelStore>((set) => ({
activeTab: "media",
setActiveTab: (tab) => set({ activeTab: tab }),
highlightMediaId: null,
requestRevealMedia: (mediaId) =>
set({ activeTab: "media", highlightMediaId: mediaId }),
clearHighlight: () => set({ highlightMediaId: null }),
}));
@@ -11,10 +11,10 @@ import {
List,
Loader2,
Music,
Search,
Video,
} from "lucide-react";
import { useEffect, useRef, useState, useMemo } from "react";
import { useRef, useState, useMemo, useEffect } 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";
@@ -33,7 +33,6 @@ import {
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,
@@ -41,6 +40,7 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { usePanelStore } from "@/stores/panel-store";
import { useMediaPanelStore } from "../store";
function MediaItemWithContextMenu({
item,
@@ -80,6 +80,11 @@ export function MediaView() {
"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;
@@ -206,7 +211,7 @@ export function MediaView() {
<img
src={item.url}
alt={item.name}
className="max-w-full max-h-full object-contain"
className="w-full max-h-full object-cover"
loading="lazy"
/>
</div>
@@ -435,7 +440,7 @@ export function MediaView() {
</div>
</div>
<div className="h-full w-full overflow-y-auto scrollbar-thin">
<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
@@ -450,13 +455,16 @@ export function MediaView() {
filteredMediaItems={filteredMediaItems}
renderPreview={renderPreview}
handleRemove={handleRemove}
highlightedId={highlightedId}
registerElement={registerElement}
/>
) : (
<ListView
filteredMediaItems={filteredMediaItems}
renderPreview={renderPreview}
handleRemove={handleRemove}
formatDuration={formatDuration}
highlightedId={highlightedId}
registerElement={registerElement}
/>
)}
</div>
@@ -470,10 +478,14 @@ function GridView({
filteredMediaItems,
renderPreview,
handleRemove,
highlightedId,
registerElement,
}: {
filteredMediaItems: MediaItem[];
renderPreview: (item: MediaItem) => React.ReactNode;
handleRemove: (e: React.MouseEvent, id: string) => Promise<void>;
highlightedId: string | null;
registerElement: (id: string, element: HTMLElement | null) => void;
}) {
return (
<div
@@ -483,27 +495,26 @@ function GridView({
}}
>
{filteredMediaItems.map((item) => (
<MediaItemWithContextMenu
key={item.id}
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) =>
useTimelineStore.getState().addMediaAtTime(item, currentTime)
}
rounded={false}
variant="card"
/>
</MediaItemWithContextMenu>
<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) =>
useTimelineStore.getState().addMediaAtTime(item, currentTime)
}
rounded={false}
variant="card"
isHighlighted={highlightedId === item.id}
/>
</MediaItemWithContextMenu>
</div>
))}
</div>
);
@@ -513,36 +524,37 @@ function ListView({
filteredMediaItems,
renderPreview,
handleRemove,
formatDuration,
highlightedId,
registerElement,
}: {
filteredMediaItems: MediaItem[];
renderPreview: (item: MediaItem) => React.ReactNode;
handleRemove: (e: React.MouseEvent, id: string) => Promise<void>;
formatDuration: (duration: number) => string;
highlightedId: string | null;
registerElement: (id: string, element: HTMLElement | null) => void;
}) {
return (
<div className="space-y-1">
{filteredMediaItems.map((item) => (
<MediaItemWithContextMenu
key={item.id}
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) =>
useTimelineStore.getState().addMediaAtTime(item, currentTime)
}
variant="compact"
/>
</MediaItemWithContextMenu>
<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) =>
useTimelineStore.getState().addMediaAtTime(item, currentTime)
}
variant="compact"
isHighlighted={highlightedId === item.id}
/>
</MediaItemWithContextMenu>
</div>
))}
</div>
);
@@ -4,6 +4,7 @@ import {
Scissors,
Trash2,
Copy,
Search,
RefreshCw,
EyeOff,
Eye,
@@ -29,6 +30,7 @@ import {
ContextMenuSeparator,
ContextMenuTrigger,
} from "../../ui/context-menu";
import { useMediaPanelStore } from "../media-panel/store";
export function TimelineElement({
element,
@@ -69,6 +71,8 @@ export function TimelineElement({
onUpdateDuration: updateElementDuration,
});
const { requestRevealMedia } = useMediaPanelStore.getState();
const effectiveDuration =
element.duration - element.trimStart - element.trimEnd;
const elementWidth = Math.max(
@@ -166,6 +170,16 @@ export function TimelineElement({
input.click();
};
const handleRevealInMedia = (e: React.MouseEvent) => {
e.stopPropagation();
if (element.type !== "media") {
toast.error("Reveal is only available for media clips");
return;
}
requestRevealMedia(element.mediaId);
};
const renderElementContent = () => {
if (element.type === "text") {
return (
@@ -348,10 +362,16 @@ export function TimelineElement({
Duplicate {element.type === "text" ? "text" : "clip"}
</ContextMenuItem>
{element.type === "media" && (
<ContextMenuItem onClick={handleReplaceClip}>
<RefreshCw className="h-4 w-4 mr-2" />
Replace clip
</ContextMenuItem>
<>
<ContextMenuItem onClick={handleRevealInMedia}>
<Search className="h-4 w-4 mr-2" />
Reveal in media
</ContextMenuItem>
<ContextMenuItem onClick={handleReplaceClip}>
<RefreshCw className="h-4 w-4 mr-2" />
Replace clip
</ContextMenuItem>
</>
)}
<ContextMenuSeparator />
<ContextMenuItem