From 0386624ff5fd6d82d64d6b103ddc1a7e5045f45b Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Mon, 4 Aug 2025 12:35:14 +0200 Subject: fix: click empty area on timeline to move playhead --- apps/web/src/components/editor/timeline/index.tsx | 117 +++++++++++++++++++--- 1 file changed, 102 insertions(+), 15 deletions(-) (limited to 'apps/web/src/components/editor/timeline') diff --git a/apps/web/src/components/editor/timeline/index.tsx b/apps/web/src/components/editor/timeline/index.tsx index 7a2cc7f2..dd81b7b3 100644 --- a/apps/web/src/components/editor/timeline/index.tsx +++ b/apps/web/src/components/editor/timeline/index.tsx @@ -167,11 +167,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, @@ -179,12 +200,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 @@ -199,8 +246,8 @@ export function Timeline() { if (!isMouseDown) { console.log( JSON.stringify({ - ignoredClickWithoutMouseDown: true, - timeStamp: e.timeStamp, + debug_click: "REJECTED - no mousedown", + mouseTracking: mouseTrackingRef.current, }) ); return; @@ -214,11 +261,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; @@ -226,27 +272,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 @@ -254,24 +327,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; @@ -289,7 +377,6 @@ export function Timeline() { // Use frame snapping for timeline clicking const projectFps = activeProject?.fps || 30; const time = snapTimeToFrame(rawTime, projectFps); - seek(time); }, [ -- cgit v1.3.1 From 9c6556ea05693701450c1977267b761e0fb3488c Mon Sep 17 00:00:00 2001 From: Mathis Boulais <77585805+MathisBls@users.noreply.github.com> Date: Tue, 5 Aug 2025 12:57:47 +0200 Subject: fix: restore horizontal scroll sync after Radix ScrollArea removal (#518) --- apps/web/src/components/editor/timeline/index.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) (limited to 'apps/web/src/components/editor/timeline') diff --git a/apps/web/src/components/editor/timeline/index.tsx b/apps/web/src/components/editor/timeline/index.tsx index dd81b7b3..bb8dfbc0 100644 --- a/apps/web/src/components/editor/timeline/index.tsx +++ b/apps/web/src/components/editor/timeline/index.tsx @@ -513,15 +513,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; -- cgit v1.3.1 From 4f6d0142866653507fabf34644a238be44951b30 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 5 Aug 2025 14:07:05 +0200 Subject: fix: remaining timeline issues with targeting wrong radix ui scroll element --- apps/web/src/components/editor/snap-indicator.tsx | 4 +--- .../src/components/editor/timeline/timeline-playhead.tsx | 8 ++------ apps/web/src/hooks/use-timeline-playhead.ts | 16 ++++------------ 3 files changed, 7 insertions(+), 21 deletions(-) (limited to 'apps/web/src/components/editor/timeline') diff --git a/apps/web/src/components/editor/snap-indicator.tsx b/apps/web/src/components/editor/snap-indicator.tsx index 0a51e107..ac884b71 100644 --- a/apps/web/src/components/editor/snap-indicator.tsx +++ b/apps/web/src/components/editor/snap-indicator.tsx @@ -28,9 +28,7 @@ export function SnapIndicator({ // Track scroll position to lock snap indicator to frame useEffect(() => { - const tracksViewport = tracksScrollRef.current?.querySelector( - "[data-radix-scroll-area-viewport]" - ) as HTMLElement; + const tracksViewport = tracksScrollRef.current; if (!tracksViewport) return; diff --git a/apps/web/src/components/editor/timeline/timeline-playhead.tsx b/apps/web/src/components/editor/timeline/timeline-playhead.tsx index 695843a4..e84512ce 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 diff --git a/apps/web/src/hooks/use-timeline-playhead.ts b/apps/web/src/hooks/use-timeline-playhead.ts index ac59a8cd..08587228 100644 --- a/apps/web/src/hooks/use-timeline-playhead.ts +++ b/apps/web/src/hooks/use-timeline-playhead.ts @@ -119,12 +119,8 @@ export function useTimelinePlayhead({ // Auto-scroll function during dragging const performAutoScroll = useCallback(() => { - 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 rulerViewport = rulerScrollRef.current; + const tracksViewport = tracksScrollRef.current; if (!rulerViewport || !tracksViewport || !isScrubbing) return; @@ -240,12 +236,8 @@ export function useTimelinePlayhead({ // Only auto-scroll during playback, not during manual interactions if (!isPlaying || isScrubbing) return; - 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 rulerViewport = rulerScrollRef.current; + const tracksViewport = tracksScrollRef.current; if (!rulerViewport || !tracksViewport) return; const playheadPx = playheadPosition * 50 * zoomLevel; // TIMELINE_CONSTANTS.PIXELS_PER_SECOND = 50 -- cgit v1.3.1 From 2c90ddd5d5f16c868f5e04943b64e0a3a78147f1 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 6 Aug 2025 12:30:48 +0200 Subject: feat: allow external file drops to individual timeline tracks --- .../components/editor/timeline/timeline-track.tsx | 47 ++++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) (limited to 'apps/web/src/components/editor/timeline') diff --git a/apps/web/src/components/editor/timeline/timeline-track.tsx b/apps/web/src/components/editor/timeline/timeline-track.tsx index 1982d03e..75910c46 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"; @@ -689,8 +688,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 +1050,47 @@ 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 timeline at the drop 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) { + // Add to current track at the drop position + addElementToTrack(track.id, { + type: "media", + mediaId: addedItem.id, + name: addedItem.name, + duration: addedItem.duration || 5, + startTime: snappedTime, + 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); -- cgit v1.3.1 From ce58b5199adae444c6ade029c14c496379932d83 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 6 Aug 2025 12:52:11 +0200 Subject: feat: external files always create new tracks at playhead position --- apps/web/src/components/editor/timeline/index.tsx | 18 ++++++++++++++++-- .../src/components/editor/timeline/timeline-track.tsx | 12 ++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) (limited to 'apps/web/src/components/editor/timeline') diff --git a/apps/web/src/components/editor/timeline/index.tsx b/apps/web/src/components/editor/timeline/index.tsx index bb8dfbc0..8bb20d72 100644 --- a/apps/web/src/components/editor/timeline/index.tsx +++ b/apps/web/src/components/editor/timeline/index.tsx @@ -53,7 +53,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, @@ -490,7 +490,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) { diff --git a/apps/web/src/components/editor/timeline/timeline-track.tsx b/apps/web/src/components/editor/timeline/timeline-track.tsx index 75910c46..ff4797fb 100644 --- a/apps/web/src/components/editor/timeline/timeline-track.tsx +++ b/apps/web/src/components/editor/timeline/timeline-track.tsx @@ -15,6 +15,7 @@ import { usePlaybackStore } from "@/stores/playback-store"; import type { TimelineElement as TimelineElementType, DragData, + TrackType, } from "@/types/timeline"; import { snapTimeToFrame, @@ -1061,7 +1062,7 @@ export function TimelineTrackContent({ return; } - // Process and add files to timeline at the drop position + // Process and add files to new timeline tracks at playhead position processMediaFiles(e.dataTransfer.files) .then(async (processedItems) => { for (const processedItem of processedItems) { @@ -1074,13 +1075,16 @@ export function TimelineTrackContent({ ); if (addedItem) { - // Add to current track at the drop position - addElementToTrack(track.id, { + 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: snappedTime, + startTime: currentTime, trimStart: 0, trimEnd: 0, }); -- cgit v1.3.1 From 8c205273b7f812103ef328aa66812260e1df3183 Mon Sep 17 00:00:00 2001 From: Sai Teja Date: Sun, 10 Aug 2025 03:06:48 +0530 Subject: fix(timeline): keep playhead below dialogs (#533) --- apps/web/src/components/editor/timeline/timeline-playhead.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'apps/web/src/components/editor/timeline') diff --git a/apps/web/src/components/editor/timeline/timeline-playhead.tsx b/apps/web/src/components/editor/timeline/timeline-playhead.tsx index e84512ce..a1be5b26 100644 --- a/apps/web/src/components/editor/timeline/timeline-playhead.tsx +++ b/apps/web/src/components/editor/timeline/timeline-playhead.tsx @@ -122,7 +122,7 @@ export function TimelinePlayhead({ return (
Date: Wed, 13 Aug 2025 00:52:19 +0200 Subject: feat: implement mute video tracks --- apps/web/src/components/editor/preview-panel.tsx | 12 ++- .../editor/timeline/timeline-element.tsx | 25 +++-- apps/web/src/stores/timeline-store.ts | 103 ++++++--------------- apps/web/src/types/timeline.ts | 1 + 4 files changed, 55 insertions(+), 86 deletions(-) (limited to 'apps/web/src/components/editor/timeline') diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx index 785162a6..3873ebca 100644 --- a/apps/web/src/components/editor/preview-panel.tsx +++ b/apps/web/src/components/editor/preview-panel.tsx @@ -235,7 +235,7 @@ export function PreviewPanel() { const activeElements: ActiveElement[] = []; // Iterate tracks from bottom to top so topmost track renders last (on top) - ;[...tracks].reverse().forEach((track) => { + [...tracks].reverse().forEach((track) => { track.elements.forEach((element) => { if (element.hidden) return; const elementStart = element.startTime; @@ -436,7 +436,7 @@ export function PreviewPanel() { trimStart={element.trimStart} trimEnd={element.trimEnd} clipDuration={element.duration} - trackMuted={elementData.track.muted} + trackMuted={element.muted || elementData.track.muted} />
); @@ -462,14 +462,18 @@ export function PreviewPanel() { // Audio elements (no visual representation) if (mediaItem.type === "audio") { 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 ( @@ -279,9 +286,9 @@ export function TimelineElement({ {renderElementContent()}
- {element.hidden && ( + {(hasAudio ? isMuted : element.hidden) && (
- {isAudio ? ( + {hasAudio ? ( ) : ( @@ -309,9 +316,9 @@ export function TimelineElement({ Split at playhead - - {isAudio ? ( - element.hidden ? ( + + {hasAudio ? ( + isMuted ? ( ) : ( @@ -322,8 +329,8 @@ export function TimelineElement({ )} - {isAudio - ? element.hidden + {hasAudio + ? isMuted ? "Unmute" : "Mute" : element.hidden diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts index ed2ed612..3eada6de 100644 --- a/apps/web/src/stores/timeline-store.ts +++ b/apps/web/src/stores/timeline-store.ts @@ -29,10 +29,10 @@ const getElementNameWithSuffix = ( ): string => { // Remove existing suffixes to prevent accumulation const baseName = originalName - .replace(/ \(left\)$/, "") - .replace(/ \(right\)$/, "") - .replace(/ \(audio\)$/, "") - .replace(/ \(split \d+\)$/, ""); + .replace(/ \(left\)$/i, "") + .replace(/ \(right\)$/i, "") + .replace(/ \(audio\)$/i, "") + .replace(/ \(split \d+\)$/i, ""); return `${baseName} (${suffix})`; }; @@ -126,6 +126,7 @@ interface TimelineStore { ) => void; toggleTrackMute: (trackId: string) => void; toggleElementHidden: (trackId: string, elementId: string) => void; + toggleElementMuted: (trackId: string, elementId: string) => void; // Split operations for elements splitElement: ( @@ -322,7 +323,6 @@ export const useTimelineStore = create((set, get) => { addTrack: (type) => { get().pushHistory(); - // Generate proper track name based on type const trackName = type === "media" ? "Media Track" @@ -347,7 +347,6 @@ export const useTimelineStore = create((set, get) => { insertTrackAt: (type, index) => { get().pushHistory(); - // Generate proper track name based on type const trackName = type === "media" ? "Media Track" @@ -392,13 +391,6 @@ export const useTimelineStore = create((set, get) => { get().pushHistory(); - // If track has no elements, just remove it normally - if (trackToRemove.elements.length === 0) { - updateTracksAndSave(_tracks.filter((track) => track.id !== trackId)); - return; - } - - // Find all the time ranges occupied by elements in the track being removed const occupiedRanges = trackToRemove.elements.map((element) => ({ startTime: element.startTime, endTime: @@ -406,10 +398,8 @@ export const useTimelineStore = create((set, get) => { (element.duration - element.trimStart - element.trimEnd), })); - // Sort ranges by start time occupiedRanges.sort((a, b) => a.startTime - b.startTime); - // Merge overlapping ranges to get consolidated gaps const mergedRanges: Array<{ startTime: number; endTime: number; @@ -426,11 +416,9 @@ export const useTimelineStore = create((set, get) => { } else { const lastRange = mergedRanges[mergedRanges.length - 1]; if (range.startTime <= lastRange.endTime) { - // Overlapping or adjacent ranges, merge them lastRange.endTime = Math.max(lastRange.endTime, range.endTime); lastRange.duration = lastRange.endTime - lastRange.startTime; } else { - // Non-overlapping range, add as new mergedRanges.push({ startTime: range.startTime, endTime: range.endTime, @@ -440,17 +428,14 @@ export const useTimelineStore = create((set, get) => { } } - // Remove the track and apply ripple effects to remaining tracks const updatedTracks = _tracks .filter((track) => track.id !== trackId) .map((track) => { const updatedElements = track.elements.map((element) => { let newStartTime = element.startTime; - // Process gaps from right to left (latest to earliest) to avoid cumulative shifts for (let i = mergedRanges.length - 1; i >= 0; i--) { const gap = mergedRanges[i]; - // If this element starts after the gap, shift it left by the gap duration if (newStartTime >= gap.endTime) { newStartTime -= gap.duration; } @@ -462,7 +447,6 @@ export const useTimelineStore = create((set, get) => { }; }); - // Check for overlaps and resolve them if necessary const hasOverlaps = checkElementOverlaps(updatedElements); if (hasOverlaps) { const resolvedElements = resolveElementOverlaps(updatedElements); @@ -478,33 +462,28 @@ export const useTimelineStore = create((set, get) => { addElementToTrack: (trackId, elementData) => { get().pushHistory(); - // Validate element type matches track type const track = get()._tracks.find((t) => t.id === trackId); if (!track) { console.error("Track not found:", trackId); return; } - // Use utility function for validation const validation = validateElementTrackCompatibility(elementData, track); if (!validation.isValid) { console.error(validation.errorMessage); return; } - // For media elements, validate mediaId exists if (elementData.type === "media" && !elementData.mediaId) { console.error("Media element must have mediaId"); return; } - // For text elements, validate required text properties if (elementData.type === "text" && !elementData.content) { console.error("Text element must have content"); return; } - // Check if this is the first element being added to the timeline const currentState = get(); const totalElementsInTimeline = currentState._tracks.reduce( (total, track) => total + track.elements.length, @@ -518,10 +497,9 @@ export const useTimelineStore = create((set, get) => { startTime: elementData.startTime || 0, trimStart: 0, trimEnd: 0, - } as TimelineElement; // Type assertion since we trust the caller passes valid data + ...(elementData.type === "media" ? { muted: false } : {}), + } as TimelineElement; - // If this is the first element and it's a media element, automatically set the project canvas size - // to match the media's aspect ratio and FPS (for videos) if (isFirstElement && newElement.type === "media") { const mediaStore = useMediaStore.getState(); const mediaItem = mediaStore.mediaItems.find( @@ -539,7 +517,6 @@ export const useTimelineStore = create((set, get) => { ); } - // Set project FPS from the first video element if (mediaItem && mediaItem.type === "video" && mediaItem.fps) { const projectStore = useProjectStore.getState(); if (projectStore.activeProject) { @@ -591,7 +568,6 @@ export const useTimelineStore = create((set, get) => { const { _tracks, rippleEditingEnabled } = get(); if (!rippleEditingEnabled) { - // If ripple editing is disabled, use regular removal get().removeElementFromTrack(trackId, elementId, pushHistory); return; } @@ -608,15 +584,12 @@ export const useTimelineStore = create((set, get) => { element.duration - element.trimStart - element.trimEnd; const elementEndTime = elementStartTime + elementDuration; - // Remove the element and shift all elements that come after it const updatedTracks = _tracks .map((currentTrack) => { - // Only apply ripple effects to the same track unless multi-track ripple is enabled const shouldApplyRipple = currentTrack.id === trackId; const updatedElements = currentTrack.elements .filter((currentElement) => { - // Remove the target element if ( currentElement.id === elementId && currentTrack.id === trackId @@ -626,12 +599,10 @@ export const useTimelineStore = create((set, get) => { return true; }) .map((currentElement) => { - // Only apply ripple effects if we should process this track if (!shouldApplyRipple) { return currentElement; } - // Shift elements that start after the removed element if (currentElement.startTime >= elementEndTime) { return { ...currentElement, @@ -644,10 +615,8 @@ export const useTimelineStore = create((set, get) => { return currentElement; }); - // Check for overlaps and resolve them if necessary const hasOverlaps = checkElementOverlaps(updatedElements); if (hasOverlaps) { - // Resolve overlaps by adjusting element positions const resolvedElements = resolveElementOverlaps(updatedElements); return { ...currentTrack, elements: resolvedElements }; } @@ -670,7 +639,6 @@ export const useTimelineStore = create((set, get) => { if (!elementToMove || !toTrack) return; - // Validate element type compatibility with target track const validation = validateElementTrackCompatibility( elementToMove, toTrack @@ -776,7 +744,6 @@ export const useTimelineStore = create((set, get) => { const { _tracks, rippleEditingEnabled } = get(); if (!rippleEditingEnabled) { - // If ripple editing is disabled, use regular update get().updateElementStartTime(trackId, elementId, newStartTime); return; } @@ -796,9 +763,7 @@ export const useTimelineStore = create((set, get) => { newStartTime + (element.duration - element.trimStart - element.trimEnd); const timeDelta = newStartTime - oldStartTime; - // Update tracks based on multi-track ripple setting const updatedTracks = _tracks.map((currentTrack) => { - // Only apply ripple effects to the same track unless multi-track ripple is enabled const shouldApplyRipple = currentTrack.id === trackId; const updatedElements = currentTrack.elements.map((currentElement) => { @@ -806,12 +771,10 @@ export const useTimelineStore = create((set, get) => { return { ...currentElement, startTime: Math.max(0, newStartTime) }; } - // Only apply ripple effects if we should process this track if (!shouldApplyRipple) { return currentElement; } - // For ripple editing, we need to move elements that come after the moved element const currentElementStart = currentElement.startTime; const currentElementEnd = currentElement.startTime + @@ -819,19 +782,14 @@ export const useTimelineStore = create((set, get) => { currentElement.trimStart - currentElement.trimEnd); - // If moving element to the right (positive delta) if (timeDelta > 0) { - // Move elements that start after the original position of the moved element if (currentElementStart >= oldEndTime) { return { ...currentElement, startTime: currentElementStart + timeDelta, }; } - } - // If moving element to the left (negative delta) - else if (timeDelta < 0) { - // Move elements that start after the new position of the moved element + } else if (timeDelta < 0) { if ( currentElementStart >= newEndTime && currentElementStart >= oldStartTime @@ -846,10 +804,8 @@ export const useTimelineStore = create((set, get) => { return currentElement; }); - // Check for overlaps and resolve them if necessary const hasOverlaps = checkElementOverlaps(updatedElements); if (hasOverlaps) { - // Resolve overlaps by adjusting element positions const resolvedElements = resolveElementOverlaps(updatedElements); return { ...currentTrack, elements: resolvedElements }; } @@ -887,6 +843,24 @@ export const useTimelineStore = create((set, get) => { ); }, + toggleElementMuted: (trackId, elementId) => { + get().pushHistory(); + updateTracksAndSave( + get()._tracks.map((track) => + track.id === trackId + ? { + ...track, + elements: track.elements.map((element) => + element.id === elementId && element.type === "media" + ? { ...element, muted: !element.muted } + : element + ), + } + : track + ) + ); + }, + updateTextElement: (trackId, elementId, updates) => { get().pushHistory(); updateTracksAndSave( @@ -1050,12 +1024,10 @@ export const useTimelineStore = create((set, get) => { get().pushHistory(); - // Find existing audio track or prepare to create one const existingAudioTrack = _tracks.find((t) => t.type === "audio"); const audioElementId = generateUUID(); if (existingAudioTrack) { - // Add audio element to existing audio track updateTracksAndSave( get()._tracks.map((track) => track.id === existingAudioTrack.id @@ -1074,7 +1046,6 @@ export const useTimelineStore = create((set, get) => { ) ); } else { - // Create new audio track with the audio element in a single atomic update const newAudioTrack: TimelineTrack = { id: generateUUID(), name: "Audio Track", @@ -1124,7 +1095,6 @@ export const useTimelineStore = create((set, get) => { return { success: false, error: "No active project found" }; } - // Import required media processing functions const { getFileType, getImageDimensions, @@ -1141,7 +1111,6 @@ export const useTimelineStore = create((set, get) => { }; } - // Process the new media file const mediaData: any = { name: newFile.name, type: fileType, @@ -1150,7 +1119,6 @@ export const useTimelineStore = create((set, get) => { }; try { - // Get media-specific metadata if (fileType === "image") { const { width, height } = await getImageDimensions(newFile); mediaData.width = width; @@ -1175,7 +1143,6 @@ export const useTimelineStore = create((set, get) => { }; } - // Add new media item to store try { await mediaStore.addMediaItem( projectStore.activeProject.id, @@ -1188,7 +1155,6 @@ export const useTimelineStore = create((set, get) => { }; } - // Find the newly created media item const newMediaItem = mediaStore.mediaItems.find( (item) => item.file === newFile ); @@ -1202,7 +1168,6 @@ export const useTimelineStore = create((set, get) => { get().pushHistory(); - // Update the timeline element to reference the new media updateTracksAndSave( _tracks.map((track) => track.id === trackId @@ -1214,7 +1179,6 @@ export const useTimelineStore = create((set, get) => { ...c, mediaId: newMediaItem.id, name: newMediaItem.name, - // Update duration if the new media has a different duration duration: newMediaItem.duration || c.duration, } : c @@ -1355,22 +1319,18 @@ export const useTimelineStore = create((set, get) => { }); }, - // Persistence methods loadProjectTimeline: async (projectId) => { try { const tracks = await storageService.loadTimeline(projectId); if (tracks) { updateTracks(tracks); } else { - // No timeline saved yet, initialize with default const defaultTracks = ensureMainTrack([]); updateTracks(defaultTracks); } - // Clear history when loading a project set({ history: [], redoStack: [] }); } catch (error) { console.error("Failed to load timeline:", error); - // Initialize with default on error const defaultTracks = ensureMainTrack([]); updateTracks(defaultTracks); set({ history: [], redoStack: [] }); @@ -1429,8 +1389,6 @@ export const useTimelineStore = create((set, get) => { }, findOrCreateTrack: (trackType) => { - // Always create new text track to allow multiple text elements - // Insert text tracks at the top if (trackType === "text") { return get().insertTrackAt(trackType, 0); } @@ -1448,10 +1406,8 @@ export const useTimelineStore = create((set, get) => { const duration = item.duration || TIMELINE_CONSTANTS.DEFAULT_IMAGE_DURATION; - // Get all tracks of the right type const tracks = get()._tracks.filter((t) => t.type === trackType); - // Try to find a track with no overlap let targetTrackId = null; for (const track of tracks) { if (!get().checkElementOverlap(track.id, currentTime, duration)) { @@ -1460,7 +1416,6 @@ export const useTimelineStore = create((set, get) => { } } - // If no free track found, create a new one if (!targetTrackId) { targetTrackId = get().addTrack(trackType); } @@ -1473,12 +1428,13 @@ export const useTimelineStore = create((set, get) => { startTime: currentTime, trimStart: 0, trimEnd: 0, + muted: false, }); return true; }, addTextAtTime: (item, currentTime = 0) => { - const targetTrackId = get().insertTrackAt("text", 0); // Always create new text track at the top + const targetTrackId = get().insertTrackAt("text", 0); get().addElementToTrack(targetTrackId, { type: "text", @@ -1516,12 +1472,13 @@ export const useTimelineStore = create((set, get) => { startTime: 0, trimStart: 0, trimEnd: 0, + muted: false, }); return true; }, addTextToNewTrack: (item) => { - const targetTrackId = get().insertTrackAt("text", 0); // Always create new text track at the top + const targetTrackId = get().insertTrackAt("text", 0); get().addElementToTrack(targetTrackId, { type: "text", diff --git a/apps/web/src/types/timeline.ts b/apps/web/src/types/timeline.ts index cd17b7dd..cf946453 100644 --- a/apps/web/src/types/timeline.ts +++ b/apps/web/src/types/timeline.ts @@ -18,6 +18,7 @@ interface BaseTimelineElement { export interface MediaElement extends BaseTimelineElement { type: "media"; mediaId: string; + muted?: boolean; } // Text element with embedded text data -- cgit v1.3.1