diff --git a/apps/web/src/components/editor/timeline-element.tsx b/apps/web/src/components/editor/timeline-element.tsx index 71b2cd26..8cccd0e4 100644 --- a/apps/web/src/components/editor/timeline-element.tsx +++ b/apps/web/src/components/editor/timeline-element.tsx @@ -1,470 +1,470 @@ -"use client"; - -import { useState } from "react"; -import { Button } from "../ui/button"; -import { - MoreVertical, - Scissors, - Trash2, - SplitSquareHorizontal, - Music, - ChevronRight, - ChevronLeft, - Type, - Copy, - RefreshCw, -} from "lucide-react"; -import { useMediaStore } from "@/stores/media-store"; -import { useTimelineStore } from "@/stores/timeline-store"; -import { usePlaybackStore } from "@/stores/playback-store"; -import AudioWaveform from "./audio-waveform"; -import { toast } from "sonner"; -import { TimelineElementProps, TrackType } from "@/types/timeline"; -import { useTimelineElementResize } from "@/hooks/use-timeline-element-resize"; -import { - getTrackElementClasses, - TIMELINE_CONSTANTS, - getTrackHeight, -} from "@/constants/timeline-constants"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, -} from "../ui/dropdown-menu"; -import { - ContextMenu, - ContextMenuContent, - ContextMenuItem, - ContextMenuSeparator, - ContextMenuTrigger, -} from "../ui/context-menu"; - -export function TimelineElement({ - element, - track, - zoomLevel, - isSelected, - onElementMouseDown, - onElementClick, -}: TimelineElementProps) { - const { mediaItems } = useMediaStore(); - const { - updateElementTrim, - updateElementDuration, - removeElementFromTrack, - dragState, - splitElement, - splitAndKeepLeft, - splitAndKeepRight, - separateAudio, - addElementToTrack, - replaceElementMedia, - } = useTimelineStore(); - const { currentTime } = usePlaybackStore(); - - const [elementMenuOpen, setElementMenuOpen] = useState(false); - - const { - resizing, - isResizing, - handleResizeStart, - handleResizeMove, - handleResizeEnd, - } = useTimelineElementResize({ - element, - track, - zoomLevel, - onUpdateTrim: updateElementTrim, - onUpdateDuration: updateElementDuration, - }); - - const effectiveDuration = - element.duration - element.trimStart - element.trimEnd; - const elementWidth = Math.max( - TIMELINE_CONSTANTS.ELEMENT_MIN_WIDTH, - effectiveDuration * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel - ); - - // Use real-time position during drag, otherwise use stored position - const isBeingDragged = dragState.elementId === element.id; - const elementStartTime = - isBeingDragged && dragState.isDragging - ? dragState.currentTime - : element.startTime; - const elementLeft = elementStartTime * 50 * zoomLevel; - - const handleDeleteElement = () => { - removeElementFromTrack(track.id, element.id); - setElementMenuOpen(false); - }; - - const handleSplitElement = () => { - const effectiveStart = element.startTime; - const effectiveEnd = - element.startTime + - (element.duration - element.trimStart - element.trimEnd); - - if (currentTime <= effectiveStart || currentTime >= effectiveEnd) { - toast.error("Playhead must be within element to split"); - return; - } - - const secondElementId = splitElement(track.id, element.id, currentTime); - if (!secondElementId) { - toast.error("Failed to split element"); - } - setElementMenuOpen(false); - }; - - const handleSplitAndKeepLeft = () => { - const effectiveStart = element.startTime; - const effectiveEnd = - element.startTime + - (element.duration - element.trimStart - element.trimEnd); - - if (currentTime <= effectiveStart || currentTime >= effectiveEnd) { - toast.error("Playhead must be within element"); - return; - } - - splitAndKeepLeft(track.id, element.id, currentTime); - setElementMenuOpen(false); - }; - - const handleSplitAndKeepRight = () => { - const effectiveStart = element.startTime; - const effectiveEnd = - element.startTime + - (element.duration - element.trimStart - element.trimEnd); - - if (currentTime <= effectiveStart || currentTime >= effectiveEnd) { - toast.error("Playhead must be within element"); - return; - } - - splitAndKeepRight(track.id, element.id, currentTime); - setElementMenuOpen(false); - }; - - const handleSeparateAudio = () => { - if (element.type !== "media") { - toast.error("Audio separation only available for media elements"); - return; - } - - const mediaItem = mediaItems.find((item) => item.id === element.mediaId); - if (!mediaItem || mediaItem.type !== "video") { - toast.error("Audio separation only available for video elements"); - return; - } - - const audioElementId = separateAudio(track.id, element.id); - if (!audioElementId) { - toast.error("Failed to separate audio"); - } - setElementMenuOpen(false); - }; - - const canSplitAtPlayhead = () => { - const effectiveStart = element.startTime; - const effectiveEnd = - element.startTime + - (element.duration - element.trimStart - element.trimEnd); - return currentTime > effectiveStart && currentTime < effectiveEnd; - }; - - const canSeparateAudio = () => { - if (element.type !== "media") return false; - const mediaItem = mediaItems.find((item) => item.id === element.mediaId); - return mediaItem?.type === "video" && track.type === "media"; - }; - - const handleElementSplitContext = () => { - const effectiveStart = element.startTime; - const effectiveEnd = - element.startTime + - (element.duration - element.trimStart - element.trimEnd); - - if (currentTime > effectiveStart && currentTime < effectiveEnd) { - const secondElementId = splitElement(track.id, element.id, currentTime); - if (!secondElementId) { - toast.error("Failed to split element"); - } - } else { - toast.error("Playhead must be within element to split"); - } - }; - - const handleElementDuplicateContext = () => { - const { id, ...elementWithoutId } = element; - addElementToTrack(track.id, { - ...elementWithoutId, - name: element.name + " (copy)", - startTime: - element.startTime + - (element.duration - element.trimStart - element.trimEnd) + - 0.1, - }); - }; - - const handleElementDeleteContext = () => { - removeElementFromTrack(track.id, element.id); - }; - - const handleReplaceClip = () => { - if (element.type !== "media") { - toast.error("Replace is only available for media clips"); - return; - } - - // Create a file input to select replacement media - const input = document.createElement("input"); - input.type = "file"; - input.accept = "video/*,audio/*,image/*"; - input.onchange = async (e) => { - const file = (e.target as HTMLInputElement).files?.[0]; - if (!file) return; - - try { - const success = await replaceElementMedia(track.id, element.id, file); - if (success) { - toast.success("Clip replaced successfully"); - } else { - toast.error("Failed to replace clip"); - } - } catch (error) { - toast.error("Failed to replace clip"); - console.log( - JSON.stringify({ error: "Failed to replace clip", details: error }) - ); - } - }; - input.click(); - }; - - const renderElementContent = () => { - if (element.type === "text") { - return ( -