From 40c7fbb4f86130c83fbd9029e83c5e7550e3219d Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Sun, 6 Jul 2025 20:45:29 +0200 Subject: refactor: move to a typed-tracks system and add support for text --- apps/web/src/components/editor/timeline-clip.tsx | 385 ----------------------- 1 file changed, 385 deletions(-) delete mode 100644 apps/web/src/components/editor/timeline-clip.tsx (limited to 'apps/web/src/components/editor/timeline-clip.tsx') diff --git a/apps/web/src/components/editor/timeline-clip.tsx b/apps/web/src/components/editor/timeline-clip.tsx deleted file mode 100644 index ddc58f8d..00000000 --- a/apps/web/src/components/editor/timeline-clip.tsx +++ /dev/null @@ -1,385 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { Button } from "../ui/button"; -import { - MoreVertical, - Scissors, - Trash2, - SplitSquareHorizontal, - Music, - ChevronRight, - ChevronLeft, -} 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 { TimelineClipProps, ResizeState } from "@/types/timeline"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, -} from "../ui/dropdown-menu"; -import { isDragging } from "motion/react"; - -export function TimelineClip({ - clip, - track, - zoomLevel, - isSelected, - onClipMouseDown, - onClipClick, -}: TimelineClipProps) { - const { mediaItems } = useMediaStore(); - const { - updateClipTrim, - addClipToTrack, - removeClipFromTrack, - dragState, - splitClip, - splitAndKeepLeft, - splitAndKeepRight, - separateAudio, - } = useTimelineStore(); - const { currentTime } = usePlaybackStore(); - - const [resizing, setResizing] = useState(null); - const [clipMenuOpen, setClipMenuOpen] = useState(false); - - const effectiveDuration = clip.duration - clip.trimStart - clip.trimEnd; - const clipWidth = Math.max(80, effectiveDuration * 50 * zoomLevel); - - // Use real-time position during drag, otherwise use stored position - const isBeingDragged = dragState.clipId === clip.id; - const clipStartTime = - isBeingDragged && dragState.isDragging - ? dragState.currentTime - : clip.startTime; - const clipLeft = clipStartTime * 50 * zoomLevel; - - const getTrackColor = (type: string) => { - switch (type) { - case "video": - return "bg-blue-500/20 border-blue-500/30"; - case "audio": - return "bg-green-500/20 border-green-500/30"; - case "effects": - return "bg-purple-500/20 border-purple-500/30"; - default: - return "bg-gray-500/20 border-gray-500/30"; - } - }; - - // Resize handles for trimming clips - const handleResizeStart = ( - e: React.MouseEvent, - clipId: string, - side: "left" | "right" - ) => { - e.stopPropagation(); - e.preventDefault(); - - setResizing({ - clipId, - side, - startX: e.clientX, - initialTrimStart: clip.trimStart, - initialTrimEnd: clip.trimEnd, - }); - }; - - const updateTrimFromMouseMove = (e: { clientX: number }) => { - if (!resizing) return; - - const deltaX = e.clientX - resizing.startX; - const deltaTime = deltaX / (50 * zoomLevel); - - if (resizing.side === "left") { - const newTrimStart = Math.max( - 0, - Math.min( - clip.duration - clip.trimEnd - 0.1, - resizing.initialTrimStart + deltaTime - ) - ); - updateClipTrim(track.id, clip.id, newTrimStart, clip.trimEnd); - } else { - const newTrimEnd = Math.max( - 0, - Math.min( - clip.duration - clip.trimStart - 0.1, - resizing.initialTrimEnd - deltaTime - ) - ); - updateClipTrim(track.id, clip.id, clip.trimStart, newTrimEnd); - } - }; - - const handleResizeMove = (e: React.MouseEvent) => { - updateTrimFromMouseMove(e); - }; - - const handleResizeEnd = () => { - setResizing(null); - }; - - const handleDeleteClip = () => { - removeClipFromTrack(track.id, clip.id); - setClipMenuOpen(false); - toast.success("Clip deleted"); - }; - - const handleSplitClip = () => { - const effectiveStart = clip.startTime; - const effectiveEnd = - clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); - - if (currentTime <= effectiveStart || currentTime >= effectiveEnd) { - toast.error("Playhead must be within clip to split"); - return; - } - - const secondClipId = splitClip(track.id, clip.id, currentTime); - if (secondClipId) { - toast.success("Clip split successfully"); - } else { - toast.error("Failed to split clip"); - } - setClipMenuOpen(false); - }; - - const handleSplitAndKeepLeft = () => { - const effectiveStart = clip.startTime; - const effectiveEnd = - clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); - - if (currentTime <= effectiveStart || currentTime >= effectiveEnd) { - toast.error("Playhead must be within clip"); - return; - } - - splitAndKeepLeft(track.id, clip.id, currentTime); - toast.success("Split and kept left portion"); - setClipMenuOpen(false); - }; - - const handleSplitAndKeepRight = () => { - const effectiveStart = clip.startTime; - const effectiveEnd = - clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); - - if (currentTime <= effectiveStart || currentTime >= effectiveEnd) { - toast.error("Playhead must be within clip"); - return; - } - - splitAndKeepRight(track.id, clip.id, currentTime); - toast.success("Split and kept right portion"); - setClipMenuOpen(false); - }; - - const handleSeparateAudio = () => { - const mediaItem = mediaItems.find((item) => item.id === clip.mediaId); - - if (!mediaItem || mediaItem.type !== "video") { - toast.error("Audio separation only available for video clips"); - return; - } - - const audioClipId = separateAudio(track.id, clip.id); - if (audioClipId) { - toast.success("Audio separated to audio track"); - } else { - toast.error("Failed to separate audio"); - } - setClipMenuOpen(false); - }; - - const canSplitAtPlayhead = () => { - const effectiveStart = clip.startTime; - const effectiveEnd = - clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); - return currentTime > effectiveStart && currentTime < effectiveEnd; - }; - - const canSeparateAudio = () => { - const mediaItem = mediaItems.find((item) => item.id === clip.mediaId); - return mediaItem?.type === "video" && track.type === "video"; - }; - - const renderClipContent = () => { - const mediaItem = mediaItems.find((item) => item.id === clip.mediaId); - - if (!mediaItem) { - return ( - {clip.name} - ); - } - - if (mediaItem.type === "image") { - return ( -
- {mediaItem.name} -
- ); - } - - if (mediaItem.type === "video" && mediaItem.thumbnailUrl) { - return ( -
-
- {mediaItem.name} -
- - {clip.name} - -
- ); - } - - if (mediaItem.type === "audio") { - return ( -
-
- -
-
- ); - } - - return ( - {clip.name} - ); - }; - - const handleClipMouseDown = (e: React.MouseEvent) => { - if (onClipMouseDown) { - onClipMouseDown(e, clip); - } - }; - - return ( -
-
onClipClick && onClipClick(e, clip)} - onMouseDown={handleClipMouseDown} - onContextMenu={(e) => onClipMouseDown && onClipMouseDown(e, clip)} - > -
- {renderClipContent()} -
- - {isSelected && ( - <> -
handleResizeStart(e, clip.id, "left")} - /> -
handleResizeStart(e, clip.id, "right")} - /> - - )} - -
- - - - - - {/* Split operations - only available when playhead is within clip */} - - - - Split - - - - - Split at Playhead - - - - Split and Keep Left - - - - Split and Keep Right - - - - - {/* Audio separation - only available for video clips */} - {canSeparateAudio() && ( - <> - - - - Separate Audio - - - )} - - - - - Delete Clip - - - -
-
-
- ); -} -- cgit v1.3.1