diff options
| author | ryu <ryumacodes@gmail.com> | 2025-08-13 14:01:07 +0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-08-13 14:01:07 +0500 |
| commit | 0ae09c9df5591e2d18991a9f98647826997fe740 (patch) | |
| tree | 69738461bd269ad418bc82c47e64a2670cee7f19 /apps/web/src/components/editor/timeline | |
| parent | e71788014d137a74f10420601451ddfbb84701bb (diff) | |
| parent | f7fddff1fc8359fc056a2a90488121980fb8ead2 (diff) | |
| download | opencut-0ae09c9df5591e2d18991a9f98647826997fe740.tar.gz | |
Merge branch 'staging' into feat/timeline-return-to-start
Diffstat (limited to 'apps/web/src/components/editor/timeline')
4 files changed, 188 insertions, 45 deletions
diff --git a/apps/web/src/components/editor/timeline/index.tsx b/apps/web/src/components/editor/timeline/index.tsx index bff0a90a..669d556c 100644 --- a/apps/web/src/components/editor/timeline/index.tsx +++ b/apps/web/src/components/editor/timeline/index.tsx @@ -55,7 +55,7 @@ import { SelectionBox } from "../selection-box"; import { useSelectionBox } from "@/hooks/use-selection-box"; import { SnapIndicator } from "../snap-indicator"; import { SnapPoint } from "@/hooks/use-timeline-snapping"; -import type { DragData, TimelineTrack } from "@/types/timeline"; +import type { DragData, TimelineTrack, TrackType } from "@/types/timeline"; import { getTrackHeight, getCumulativeHeightBefore, @@ -169,11 +169,32 @@ export function Timeline() { const handleTimelineMouseDown = useCallback((e: React.MouseEvent) => { // Only track mouse down on timeline background areas (not elements) const target = e.target as HTMLElement; + console.log( + JSON.stringify({ + debug_mousedown: "START", + target_class: target.className, + target_parent_class: target.parentElement?.className, + clientX: e.clientX, + clientY: e.clientY, + timeStamp: e.timeStamp, + }) + ); + const isTimelineBackground = !target.closest(".timeline-element") && !playheadRef.current?.contains(target) && !target.closest("[data-track-labels]"); + console.log( + JSON.stringify({ + debug_mousedown: "CHECK", + isTimelineBackground, + hasTimelineElement: !!target.closest(".timeline-element"), + hasPlayhead: !!playheadRef.current?.contains(target), + hasTrackLabels: !!target.closest("[data-track-labels]"), + }) + ); + if (isTimelineBackground) { mouseTrackingRef.current = { isMouseDown: true, @@ -181,12 +202,38 @@ export function Timeline() { downY: e.clientY, downTime: e.timeStamp, }; + console.log( + JSON.stringify({ + debug_mousedown: "TRACKED", + mouseTracking: mouseTrackingRef.current, + }) + ); + } else { + console.log( + JSON.stringify({ + debug_mousedown: "IGNORED - not timeline background", + }) + ); } }, []); // Timeline content click to seek handler const handleTimelineContentClick = useCallback( (e: React.MouseEvent) => { + console.log( + JSON.stringify({ + debug_click: "START", + target: (e.target as HTMLElement).className, + target_parent: (e.target as HTMLElement).parentElement?.className, + mouseTracking: mouseTrackingRef.current, + isSelecting, + justFinishedSelecting, + clickX: e.clientX, + clickY: e.clientY, + timeStamp: e.timeStamp, + }) + ); + const { isMouseDown, downX, downY, downTime } = mouseTrackingRef.current; // Reset mouse tracking @@ -201,8 +248,8 @@ export function Timeline() { if (!isMouseDown) { console.log( JSON.stringify({ - ignoredClickWithoutMouseDown: true, - timeStamp: e.timeStamp, + debug_click: "REJECTED - no mousedown", + mouseTracking: mouseTrackingRef.current, }) ); return; @@ -216,11 +263,10 @@ export function Timeline() { if (deltaX > 5 || deltaY > 5 || deltaTime > 500) { console.log( JSON.stringify({ - ignoredDragNotClick: true, + debug_click: "REJECTED - movement too large", deltaX, deltaY, deltaTime, - timeStamp: e.timeStamp, }) ); return; @@ -228,27 +274,54 @@ export function Timeline() { // Don't seek if this was a selection box operation if (isSelecting || justFinishedSelecting) { + console.log( + JSON.stringify({ + debug_click: "REJECTED - selection operation", + isSelecting, + justFinishedSelecting, + }) + ); return; } // Don't seek if clicking on timeline elements, but still deselect if ((e.target as HTMLElement).closest(".timeline-element")) { + console.log( + JSON.stringify({ + debug_click: "REJECTED - clicked timeline element", + }) + ); return; } // Don't seek if clicking on playhead if (playheadRef.current?.contains(e.target as Node)) { + console.log( + JSON.stringify({ + debug_click: "REJECTED - clicked playhead", + }) + ); return; } // Don't seek if clicking on track labels if ((e.target as HTMLElement).closest("[data-track-labels]")) { + console.log( + JSON.stringify({ + debug_click: "REJECTED - clicked track labels", + }) + ); clearSelectedElements(); return; } // Clear selected elements when clicking empty timeline area - console.log(JSON.stringify({ clearingSelectedElements: true })); + console.log( + JSON.stringify({ + debug_click: "PROCEEDING - clearing elements", + clearingSelectedElements: true, + }) + ); clearSelectedElements(); // Determine if we're clicking in ruler or tracks area @@ -256,24 +329,39 @@ export function Timeline() { "[data-ruler-area]" ); + console.log( + JSON.stringify({ + debug_click: "CALCULATING POSITION", + isRulerClick, + clientX: e.clientX, + clientY: e.clientY, + target_element: (e.target as HTMLElement).tagName, + target_class: (e.target as HTMLElement).className, + }) + ); + let mouseX: number; let scrollLeft = 0; if (isRulerClick) { // Calculate based on ruler position - const rulerContent = rulerScrollRef.current?.querySelector( - "[data-radix-scroll-area-viewport]" - ) as HTMLElement; - if (!rulerContent) return; + const rulerContent = rulerScrollRef.current; + if (!rulerContent) { + console.log( + JSON.stringify({ + debug_click: "ERROR - no ruler container found", + }) + ); + return; + } const rect = rulerContent.getBoundingClientRect(); mouseX = e.clientX - rect.left; scrollLeft = rulerContent.scrollLeft; } else { - // Calculate based on tracks content position - const tracksContent = tracksScrollRef.current?.querySelector( - "[data-radix-scroll-area-viewport]" - ) as HTMLElement; - if (!tracksContent) return; + const tracksContent = tracksScrollRef.current; + if (!tracksContent) { + return; + } const rect = tracksContent.getBoundingClientRect(); mouseX = e.clientX - rect.left; scrollLeft = tracksContent.scrollLeft; @@ -291,7 +379,6 @@ export function Timeline() { // Use frame snapping for timeline clicking const projectFps = activeProject?.fps || 30; const time = snapTimeToFrame(rawTime, projectFps); - seek(time); }, [ @@ -405,7 +492,21 @@ export function Timeline() { item.name === processedItem.name && item.url === processedItem.url ); if (addedItem) { - useTimelineStore.getState().addMediaToNewTrack(addedItem); + const trackType: TrackType = + addedItem.type === "audio" ? "audio" : "media"; + const targetTrackId = useTimelineStore + .getState() + .insertTrackAt(trackType, 0); + + useTimelineStore.getState().addElementToTrack(targetTrackId, { + type: "media", + mediaId: addedItem.id, + name: addedItem.name, + duration: addedItem.duration || 5, + startTime: currentTime, + trimStart: 0, + trimEnd: 0, + }); } } } catch (error) { @@ -428,15 +529,9 @@ export function Timeline() { // --- Scroll synchronization effect --- useEffect(() => { - const rulerViewport = rulerScrollRef.current?.querySelector( - "[data-radix-scroll-area-viewport]" - ) as HTMLElement; - const tracksViewport = tracksScrollRef.current?.querySelector( - "[data-radix-scroll-area-viewport]" - ) as HTMLElement; - const trackLabelsViewport = trackLabelsScrollRef.current?.querySelector( - "[data-radix-scroll-area-viewport]" - ) as HTMLElement; + const rulerViewport = rulerScrollRef.current; + const tracksViewport = tracksScrollRef.current; + const trackLabelsViewport = trackLabelsScrollRef.current; if (!rulerViewport || !tracksViewport) return; diff --git a/apps/web/src/components/editor/timeline/timeline-element.tsx b/apps/web/src/components/editor/timeline/timeline-element.tsx index b26a14bb..f2623161 100644 --- a/apps/web/src/components/editor/timeline/timeline-element.tsx +++ b/apps/web/src/components/editor/timeline/timeline-element.tsx @@ -50,6 +50,7 @@ export function TimelineElement({ replaceElementMedia, rippleEditingEnabled, toggleElementHidden, + toggleElementMuted, } = useTimelineStore(); const { currentTime } = usePlaybackStore(); @@ -57,7 +58,7 @@ export function TimelineElement({ element.type === "media" ? mediaItems.find((item) => item.id === element.mediaId) : null; - const isAudio = mediaItem?.type === "audio"; + const hasAudio = mediaItem?.type === "audio" || mediaItem?.type === "video"; const { resizing, handleResizeStart, handleResizeMove, handleResizeEnd } = useTimelineElementResize({ @@ -124,8 +125,12 @@ export function TimelineElement({ } }; - const handleToggleElementHidden = (e: React.MouseEvent) => { + const handleToggleElementContext = (e: React.MouseEvent) => { e.stopPropagation(); + if (hasAudio && element.type === "media") { + toggleElementMuted(track.id, element.id); + return; + } toggleElementHidden(track.id, element.id); }; @@ -246,6 +251,8 @@ export function TimelineElement({ } }; + const isMuted = element.type === "media" ? element.muted === true : false; + return ( <ContextMenu> <ContextMenuTrigger asChild> @@ -279,9 +286,9 @@ export function TimelineElement({ {renderElementContent()} </div> - {element.hidden && ( + {(hasAudio ? isMuted : element.hidden) && ( <div className="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center pointer-events-none"> - {isAudio ? ( + {hasAudio ? ( <VolumeX className="h-6 w-6 text-white" /> ) : ( <EyeOff className="h-6 w-6 text-white" /> @@ -309,9 +316,9 @@ export function TimelineElement({ <Scissors className="h-4 w-4 mr-2" /> Split at playhead </ContextMenuItem> - <ContextMenuItem onClick={handleToggleElementHidden}> - {isAudio ? ( - element.hidden ? ( + <ContextMenuItem onClick={handleToggleElementContext}> + {hasAudio ? ( + isMuted ? ( <Volume2 className="h-4 w-4 mr-2" /> ) : ( <VolumeX className="h-4 w-4 mr-2" /> @@ -322,8 +329,8 @@ export function TimelineElement({ <EyeOff className="h-4 w-4 mr-2" /> )} <span> - {isAudio - ? element.hidden + {hasAudio + ? isMuted ? "Unmute" : "Mute" : element.hidden diff --git a/apps/web/src/components/editor/timeline/timeline-playhead.tsx b/apps/web/src/components/editor/timeline/timeline-playhead.tsx index 248c6176..48c60596 100644 --- a/apps/web/src/components/editor/timeline/timeline-playhead.tsx +++ b/apps/web/src/components/editor/timeline/timeline-playhead.tsx @@ -51,9 +51,7 @@ export function TimelinePlayhead({ // Track scroll position to lock playhead to frame useEffect(() => { - const tracksViewport = tracksScrollRef.current?.querySelector( - "[data-radix-scroll-area-viewport]" - ) as HTMLElement; + const tracksViewport = tracksScrollRef.current; if (!tracksViewport) return; @@ -86,9 +84,7 @@ export function TimelinePlayhead({ // Get the timeline content width and viewport width for right boundary const timelineContentWidth = duration * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel; - const tracksViewport = tracksScrollRef.current?.querySelector( - "[data-radix-scroll-area-viewport]" - ) as HTMLElement; + const tracksViewport = tracksScrollRef.current; const viewportWidth = tracksViewport?.clientWidth || 1000; // Constrain playhead to never appear outside the timeline area @@ -126,7 +122,7 @@ export function TimelinePlayhead({ return ( <div ref={playheadRef} - className="absolute pointer-events-auto z-150" + className="absolute pointer-events-auto z-40" style={{ left: `${leftPosition}px`, top: 0, diff --git a/apps/web/src/components/editor/timeline/timeline-track.tsx b/apps/web/src/components/editor/timeline/timeline-track.tsx index 1982d03e..ff4797fb 100644 --- a/apps/web/src/components/editor/timeline/timeline-track.tsx +++ b/apps/web/src/components/editor/timeline/timeline-track.tsx @@ -4,11 +4,10 @@ import { useRef, useState, useEffect } from "react"; import { useTimelineStore } from "@/stores/timeline-store"; import { useMediaStore } from "@/stores/media-store"; import { toast } from "sonner"; +import { processMediaFiles } from "@/lib/media-processing"; import { TimelineElement } from "./timeline-element"; import { TimelineTrack, - sortTracksByOrder, - ensureMainTrack, getMainTrack, canElementGoOnTrack, } from "@/types/timeline"; @@ -16,6 +15,7 @@ import { usePlaybackStore } from "@/stores/playback-store"; import type { TimelineElement as TimelineElementType, DragData, + TrackType, } from "@/types/timeline"; import { snapTimeToFrame, @@ -689,8 +689,9 @@ export function TimelineTrackContent({ const hasMediaItem = e.dataTransfer.types.includes( "application/x-media-item" ); + const hasFiles = e.dataTransfer.files?.length > 0; - if (!hasTimelineElement && !hasMediaItem) return; + if (!hasTimelineElement && !hasMediaItem && !hasFiles) return; const trackContainer = e.currentTarget.querySelector( ".track-elements-container" @@ -1050,6 +1051,50 @@ export function TimelineTrackContent({ trimEnd: 0, }); } + } else if (hasFiles) { + // External file drops + const { activeProject } = useProjectStore.getState(); + const { addMediaItem } = useMediaStore.getState(); + const { addElementToTrack } = useTimelineStore.getState(); + + if (!activeProject) { + toast.error("No active project"); + return; + } + + // Process and add files to new timeline tracks at playhead position + processMediaFiles(e.dataTransfer.files) + .then(async (processedItems) => { + for (const processedItem of processedItems) { + await addMediaItem(activeProject.id, processedItem); + const currentMediaItems = useMediaStore.getState().mediaItems; + const addedItem = currentMediaItems.find( + (item) => + item.name === processedItem.name && + item.url === processedItem.url + ); + + if (addedItem) { + const trackType: TrackType = + addedItem.type === "audio" ? "audio" : "media"; + const targetTrackId = insertTrackAt(trackType, 0); + + addElementToTrack(targetTrackId, { + type: "media", + mediaId: addedItem.id, + name: addedItem.name, + duration: addedItem.duration || 5, + startTime: currentTime, + trimStart: 0, + trimEnd: 0, + }); + } + } + }) + .catch((error) => { + console.error("Error processing external files:", error); + toast.error("Failed to process dropped files"); + }); } } catch (error) { console.error("Error handling drop:", error); |
