From e225272ec3ad86781e2664e10de8d5e2af83a398 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Thu, 26 Jun 2025 00:37:35 +0200 Subject: fix: some timeline issues --- apps/web/src/components/editor/timeline-clip.tsx | 553 +++++++++++------------ 1 file changed, 276 insertions(+), 277 deletions(-) (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 index 1bb2b77f..4c0d3b77 100644 --- a/apps/web/src/components/editor/timeline-clip.tsx +++ b/apps/web/src/components/editor/timeline-clip.tsx @@ -1,277 +1,276 @@ -"use client"; - -import { useState } from "react"; -import { Button } from "../ui/button"; -import { MoreVertical, Scissors, Trash2 } from "lucide-react"; -import { useMediaStore } from "@/stores/media-store"; -import { useTimelineStore } from "@/stores/timeline-store"; -import { usePlaybackStore } from "@/stores/playback-store"; -import { useDragClip } from "@/hooks/use-drag-clip"; -import AudioWaveform from "./audio-waveform"; -import { toast } from "sonner"; -import { TimelineClipProps, ResizeState } from "@/types/timeline"; - -export function TimelineClip({ - clip, - track, - zoomLevel, - isSelected, - onContextMenu, - onClipMouseDown, - onClipClick, -}: TimelineClipProps) { - const { mediaItems } = useMediaStore(); - const { updateClipTrim, addClipToTrack, removeClipFromTrack } = - useTimelineStore(); - const { currentTime } = usePlaybackStore(); - const { draggedClipId, getDraggedClipPosition } = - useDragClip(zoomLevel); - - 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 dragPosition = getDraggedClipPosition(clip.id); - const clipStartTime = dragPosition !== null ? dragPosition : clip.startTime; - const clipLeft = clipStartTime * 50 * zoomLevel; - - const isBeingDragged = draggedClipId === clip.id; - - 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"; - } - }; - - 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); - }; - - const handleSplitClip = () => { - // Use current playback time as split point - const splitTime = currentTime; - // Only split if splitTime is within the clip's effective range - const effectiveStart = clip.startTime; - const effectiveEnd = - clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); - - if (splitTime <= effectiveStart || splitTime >= effectiveEnd) { - toast.error("Playhead must be within clip to split"); - return; - } - - const firstDuration = splitTime - effectiveStart; - const secondDuration = effectiveEnd - splitTime; - - // First part: adjust original clip - updateClipTrim( - track.id, - clip.id, - clip.trimStart, - clip.trimEnd + secondDuration - ); - - // Second part: add new clip after split - addClipToTrack(track.id, { - mediaId: clip.mediaId, - name: clip.name + " (cut)", - duration: clip.duration, - startTime: splitTime, - trimStart: clip.trimStart + firstDuration, - trimEnd: clip.trimEnd, - }); - - setClipMenuOpen(false); - toast.success("Clip split successfully"); - }; - - 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 ( -
-
- -
-
- ); - } - - // Fallback for videos without thumbnails - return ( - {clip.name} - ); - }; - - return ( -
onClipMouseDown(e, clip)} - onClick={(e) => onClipClick(e, clip)} - onMouseMove={handleResizeMove} - onMouseUp={handleResizeEnd} - onMouseLeave={handleResizeEnd} - tabIndex={0} - onContextMenu={(e) => onContextMenu(e, clip.id)} - > - {/* Left trim handle */} -
handleResizeStart(e, clip.id, "left")} - /> - - {/* Clip content */} -
- {renderClipContent()} - - {/* Clip options menu */} -
- - - {clipMenuOpen && ( -
e.stopPropagation()} - > - - -
- )} -
-
- - {/* Right trim handle */} -
handleResizeStart(e, clip.id, "right")} - /> -
- ); -} +"use client"; + +import { useState } from "react"; +import { Button } from "../ui/button"; +import { MoreVertical, Scissors, Trash2 } from "lucide-react"; +import { useMediaStore } from "@/stores/media-store"; +import { useTimelineStore } from "@/stores/timeline-store"; +import { usePlaybackStore } from "@/stores/playback-store"; +import { useDragClip } from "@/hooks/use-drag-clip"; +import AudioWaveform from "./audio-waveform"; +import { toast } from "sonner"; +import { TimelineClipProps, ResizeState } from "@/types/timeline"; + +export function TimelineClip({ + clip, + track, + zoomLevel, + isSelected, + onContextMenu, + onClipMouseDown, + onClipClick, +}: TimelineClipProps) { + const { mediaItems } = useMediaStore(); + const { updateClipTrim, addClipToTrack, removeClipFromTrack, dragState } = + 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"; + } + }; + + 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); + }; + + const handleSplitClip = () => { + // Use current playback time as split point + const splitTime = currentTime; + // Only split if splitTime is within the clip's effective range + const effectiveStart = clip.startTime; + const effectiveEnd = + clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); + + if (splitTime <= effectiveStart || splitTime >= effectiveEnd) { + toast.error("Playhead must be within clip to split"); + return; + } + + const firstDuration = splitTime - effectiveStart; + const secondDuration = effectiveEnd - splitTime; + + // First part: adjust original clip + updateClipTrim( + track.id, + clip.id, + clip.trimStart, + clip.trimEnd + secondDuration + ); + + // Second part: add new clip after split + addClipToTrack(track.id, { + mediaId: clip.mediaId, + name: clip.name + " (cut)", + duration: clip.duration, + startTime: splitTime, + trimStart: clip.trimStart + firstDuration, + trimEnd: clip.trimEnd, + }); + + setClipMenuOpen(false); + toast.success("Clip split successfully"); + }; + + 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 ( +
+
+ +
+
+ ); + } + + // Fallback for videos without thumbnails + return ( + {clip.name} + ); + }; + + return ( +
onClipMouseDown(e, clip)} + onClick={(e) => onClipClick(e, clip)} + onMouseMove={handleResizeMove} + onMouseUp={handleResizeEnd} + onMouseLeave={handleResizeEnd} + tabIndex={0} + onContextMenu={(e) => onContextMenu(e, clip.id)} + > + {/* Left trim handle */} +
handleResizeStart(e, clip.id, "left")} + /> + + {/* Clip content */} +
+ {renderClipContent()} + + {/* Clip options menu */} +
+ + + {clipMenuOpen && ( +
e.stopPropagation()} + > + + +
+ )} +
+
+ + {/* Right trim handle */} +
handleResizeStart(e, clip.id, "right")} + /> +
+ ); +} -- cgit v1.3.1