From fca99d6126c31fbb18ed9f1034cee6f940b040e8 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 10 Feb 2026 16:28:21 +0100 Subject: refactor --- .../components/editor/timeline/audio-waveform.tsx | 176 ------- .../src/components/editor/timeline/bookmarks.tsx | 111 ----- .../src/components/editor/timeline/drag-line.tsx | 28 -- apps/web/src/components/editor/timeline/index.tsx | 535 -------------------- .../components/editor/timeline/snap-indicator.tsx | 52 -- .../editor/timeline/timeline-element.tsx | 554 --------------------- .../editor/timeline/timeline-playhead.tsx | 92 ---- .../components/editor/timeline/timeline-ruler.tsx | 108 ---- .../components/editor/timeline/timeline-tick.tsx | 39 -- .../editor/timeline/timeline-toolbar.tsx | 354 ------------- .../components/editor/timeline/timeline-track.tsx | 117 ----- 11 files changed, 2166 deletions(-) delete mode 100644 apps/web/src/components/editor/timeline/audio-waveform.tsx delete mode 100644 apps/web/src/components/editor/timeline/bookmarks.tsx delete mode 100644 apps/web/src/components/editor/timeline/drag-line.tsx delete mode 100644 apps/web/src/components/editor/timeline/index.tsx delete mode 100644 apps/web/src/components/editor/timeline/snap-indicator.tsx delete mode 100644 apps/web/src/components/editor/timeline/timeline-element.tsx delete mode 100644 apps/web/src/components/editor/timeline/timeline-playhead.tsx delete mode 100644 apps/web/src/components/editor/timeline/timeline-ruler.tsx delete mode 100644 apps/web/src/components/editor/timeline/timeline-tick.tsx delete mode 100644 apps/web/src/components/editor/timeline/timeline-toolbar.tsx delete mode 100644 apps/web/src/components/editor/timeline/timeline-track.tsx (limited to 'apps/web/src/components/editor/timeline') diff --git a/apps/web/src/components/editor/timeline/audio-waveform.tsx b/apps/web/src/components/editor/timeline/audio-waveform.tsx deleted file mode 100644 index bbc5cb83..00000000 --- a/apps/web/src/components/editor/timeline/audio-waveform.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import WaveSurfer from "wavesurfer.js"; - -interface AudioWaveformProps { - audioUrl?: string; - audioBuffer?: AudioBuffer; - height?: number; - className?: string; -} - -function extractPeaks({ - buffer, - length = 512, -}: { - buffer: AudioBuffer; - length?: number; -}): number[][] { - const channels = buffer.numberOfChannels; - const peaks: number[][] = []; - - for (let c = 0; c < channels; c++) { - const data = buffer.getChannelData(c); - const step = Math.floor(data.length / length); - const channelPeaks: number[] = []; - - for (let i = 0; i < length; i++) { - const start = i * step; - const end = Math.min(start + step, data.length); - let max = 0; - for (let j = start; j < end; j++) { - const abs = Math.abs(data[j]); - if (abs > max) max = abs; - } - channelPeaks.push(max); - } - peaks.push(channelPeaks); - } - - return peaks; -} - -export function AudioWaveform({ - audioUrl, - audioBuffer, - height = 32, - className = "", -}: AudioWaveformProps) { - const waveformRef = useRef(null); - const wavesurfer = useRef(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(false); - - useEffect(() => { - let mounted = true; - const ws = wavesurfer.current; - - const initWaveSurfer = async () => { - if (!waveformRef.current || (!audioUrl && !audioBuffer)) return; - - try { - if (ws) { - wavesurfer.current = null; - } - - const newWaveSurfer = WaveSurfer.create({ - container: waveformRef.current, - waveColor: "rgba(255, 255, 255, 0.6)", - progressColor: "rgba(255, 255, 255, 0.9)", - cursorColor: "transparent", - barWidth: 2, - barGap: 1, - height, - normalize: true, - interact: false, - }); - - if (mounted) { - wavesurfer.current = newWaveSurfer; - } else { - try { - newWaveSurfer.destroy(); - } catch {} - return; - } - - newWaveSurfer.on("ready", () => { - if (mounted) { - setIsLoading(false); - setError(false); - } - }); - - newWaveSurfer.on("error", (err) => { - if (mounted) { - console.error("WaveSurfer error:", err); - setError(true); - setIsLoading(false); - } - }); - - if (audioBuffer) { - const peaks = extractPeaks({ buffer: audioBuffer }); - newWaveSurfer.load("", peaks, audioBuffer.duration); - } else if (audioUrl) { - await newWaveSurfer.load(audioUrl); - } - } catch (err) { - if (mounted) { - console.error("Failed to initialize WaveSurfer:", err); - setError(true); - setIsLoading(false); - } - } - }; - - if (ws) { - const wsToDestroy = ws; - wavesurfer.current = null; - - requestAnimationFrame(() => { - try { - wsToDestroy.destroy(); - } catch {} - if (mounted) { - initWaveSurfer(); - } - }); - } else { - initWaveSurfer(); - } - - return () => { - mounted = false; - - const wsToDestroy = wavesurfer.current; - - wavesurfer.current = null; - - if (wsToDestroy) { - requestAnimationFrame(() => { - try { - wsToDestroy.destroy(); - } catch {} - }); - } - }; - }, [audioUrl, audioBuffer, height]); - - if (error) { - return ( -
- Audio unavailable -
- ); - } - - return ( -
- {isLoading && ( -
- Loading... -
- )} -
-
- ); -} - -export default AudioWaveform; diff --git a/apps/web/src/components/editor/timeline/bookmarks.tsx b/apps/web/src/components/editor/timeline/bookmarks.tsx deleted file mode 100644 index cfcb1e23..00000000 --- a/apps/web/src/components/editor/timeline/bookmarks.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { useEditor } from "@/hooks/use-editor"; -import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; -import { getSnappedSeekTime } from "@/lib/time"; -import { Bookmark02Icon } from "@hugeicons/core-free-icons"; -import { HugeiconsIcon } from "@hugeicons/react"; - -interface TimelineBookmarksRowProps { - zoomLevel: number; - dynamicTimelineWidth: number; - handleWheel: (e: React.WheelEvent) => void; - handleTimelineContentClick: (e: React.MouseEvent) => void; - handleRulerTrackingMouseDown: (e: React.MouseEvent) => void; - handleRulerMouseDown: (e: React.MouseEvent) => void; -} - -export function TimelineBookmarksRow({ - zoomLevel, - dynamicTimelineWidth, - handleWheel, - handleTimelineContentClick, - handleRulerTrackingMouseDown, - handleRulerMouseDown, -}: TimelineBookmarksRowProps) { - const editor = useEditor(); - const activeScene = editor.scenes.getActiveScene(); - - return ( -
- -
- ); -} - -export function TimelineBookmark({ - time, - zoomLevel, -}: { - time: number; - zoomLevel: number; -}) { - const editor = useEditor(); - const activeProject = editor.project.getActive(); - const duration = editor.timeline.getTotalDuration(); - - const handleBookmarkActivate = ({ - event, - }: { - event: - | React.MouseEvent - | React.KeyboardEvent; - }) => { - event.stopPropagation(); - const framesPerSecond = activeProject?.settings.fps ?? 30; - const snappedTime = getSnappedSeekTime({ - rawTime: time, - duration, - fps: framesPerSecond, - }); - editor.playback.seek({ time: snappedTime }); - }; - - return ( - - ); -} diff --git a/apps/web/src/components/editor/timeline/drag-line.tsx b/apps/web/src/components/editor/timeline/drag-line.tsx deleted file mode 100644 index f31c6e28..00000000 --- a/apps/web/src/components/editor/timeline/drag-line.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { getDropLineY } from "@/lib/timeline/drop-utils"; -import type { TimelineTrack, DropTarget } from "@/types/timeline"; - -interface DragLineProps { - dropTarget: DropTarget | null; - tracks: TimelineTrack[]; - isVisible: boolean; - headerHeight?: number; -} - -export function DragLine({ - dropTarget, - tracks, - isVisible, - headerHeight = 0, -}: DragLineProps) { - if (!isVisible || !dropTarget) return null; - - const y = getDropLineY({ dropTarget, tracks }); - const lineTop = y + headerHeight; - - return ( -
- ); -} diff --git a/apps/web/src/components/editor/timeline/index.tsx b/apps/web/src/components/editor/timeline/index.tsx deleted file mode 100644 index 2ff17345..00000000 --- a/apps/web/src/components/editor/timeline/index.tsx +++ /dev/null @@ -1,535 +0,0 @@ -"use client"; - -import { ScrollArea } from "@/components/ui/scroll-area"; -import { - Delete02Icon, - TaskAdd02Icon, - ViewIcon, - ViewOffSlashIcon, - VolumeHighIcon, - VolumeOffIcon, -} from "@hugeicons/core-free-icons"; -import { HugeiconsIcon, type IconSvgElement } from "@hugeicons/react"; -import { - ContextMenu, - ContextMenuContent, - ContextMenuItem, - ContextMenuTrigger, -} from "../../ui/context-menu"; -import { useTimelineZoom } from "@/hooks/timeline/use-timeline-zoom"; -import { useState, useRef, useCallback } from "react"; -import { TimelineTrackContent } from "./timeline-track"; -import { TimelinePlayhead } from "./timeline-playhead"; -import { SelectionBox } from "../selection-box"; -import { useSelectionBox } from "@/hooks/timeline/use-selection-box"; -import { SnapIndicator } from "./snap-indicator"; -import type { SnapPoint } from "@/hooks/timeline/use-timeline-snapping"; -import type { TimelineTrack } from "@/types/timeline"; -import { - TIMELINE_CONSTANTS, - TRACK_ICONS, -} from "@/constants/timeline-constants"; -import { useElementInteraction } from "@/hooks/timeline/element/use-element-interaction"; -import { - getTrackHeight, - getCumulativeHeightBefore, - getTotalTracksHeight, - canTracktHaveAudio, - canTrackBeHidden, - getTimelineZoomMin, - getTimelinePaddingPx, - isMainTrack, -} from "@/lib/timeline"; -import { TimelineToolbar } from "./timeline-toolbar"; -import { useScrollSync } from "@/hooks/timeline/use-scroll-sync"; -import { useElementSelection } from "@/hooks/timeline/element/use-element-selection"; -import { useTimelineSeek } from "@/hooks/timeline/use-timeline-seek"; -import { useTimelineDragDrop } from "@/hooks/timeline/use-timeline-drag-drop"; -import { TimelineRuler } from "./timeline-ruler"; -import { TimelineBookmarksRow } from "./bookmarks"; -import { useTimelineStore } from "@/stores/timeline-store"; -import { useEditor } from "@/hooks/use-editor"; -import { useTimelinePlayhead } from "@/hooks/timeline/use-timeline-playhead"; -import { DragLine } from "./drag-line"; -import { invokeAction } from "@/lib/actions"; - -export function Timeline() { - const tracksContainerHeight = { min: 0, max: 800 }; - const { snappingEnabled } = useTimelineStore(); - const { clearElementSelection, setElementSelection } = useElementSelection(); - const editor = useEditor(); - const timeline = editor.timeline; - const tracks = timeline.getTracks(); - const seek = (time: number) => editor.playback.seek({ time }); - - // refs - const timelineRef = useRef(null); - const timelineHeaderRef = useRef(null); - const rulerRef = useRef(null); - const tracksContainerRef = useRef(null); - const tracksScrollRef = useRef(null); - const trackLabelsRef = useRef(null); - const playheadRef = useRef(null); - const trackLabelsScrollRef = useRef(null); - - // state - const [isResizing, setIsResizing] = useState(false); - const [currentSnapPoint, setCurrentSnapPoint] = useState( - null, - ); - - const handleSnapPointChange = useCallback((snapPoint: SnapPoint | null) => { - setCurrentSnapPoint(snapPoint); - }, []); - const handleResizeStateChange = useCallback( - ({ isResizing: nextIsResizing }: { isResizing: boolean }) => { - setIsResizing(nextIsResizing); - if (!nextIsResizing) { - setCurrentSnapPoint(null); - } - }, - [], - ); - - const timelineDuration = timeline.getTotalDuration() || 0; - const minZoomLevel = getTimelineZoomMin({ - duration: timelineDuration, - containerWidth: tracksContainerRef.current?.clientWidth, - }); - - const savedViewState = editor.project.getTimelineViewState(); - - const { zoomLevel, setZoomLevel, handleWheel, saveScrollPosition } = - useTimelineZoom({ - containerRef: timelineRef, - minZoom: minZoomLevel, - initialZoom: savedViewState?.zoomLevel, - initialScrollLeft: savedViewState?.scrollLeft, - initialPlayheadTime: savedViewState?.playheadTime, - tracksScrollRef, - rulerScrollRef: tracksScrollRef, - }); - - const { - dragState, - dragDropTarget, - handleElementMouseDown, - handleElementClick, - lastMouseXRef, - } = useElementInteraction({ - zoomLevel, - timelineRef, - tracksContainerRef, - tracksScrollRef, - headerRef: timelineHeaderRef, - snappingEnabled, - onSnapPointChange: handleSnapPointChange, - }); - - const { handleRulerMouseDown: handlePlayheadRulerMouseDown } = - useTimelinePlayhead({ - zoomLevel, - rulerRef, - rulerScrollRef: tracksScrollRef, - tracksScrollRef, - playheadRef, - }); - - const { isDragOver, dropTarget, dragProps } = useTimelineDragDrop({ - containerRef: tracksContainerRef, - headerRef: timelineHeaderRef, - zoomLevel, - }); - - const { - selectionBox, - handleMouseDown: handleSelectionMouseDown, - isSelecting, - shouldIgnoreClick, - } = useSelectionBox({ - containerRef: tracksContainerRef, - onSelectionComplete: (elements) => { - setElementSelection({ elements }); - }, - tracksScrollRef, - zoomLevel, - }); - - const containerWidth = tracksContainerRef.current?.clientWidth || 1000; - const contentWidth = - timelineDuration * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel; - const paddingPx = getTimelinePaddingPx({ - containerWidth, - zoomLevel, - minZoom: minZoomLevel, - }); - const dynamicTimelineWidth = Math.max( - contentWidth + paddingPx, - containerWidth, - ); - - const showSnapIndicator = - snappingEnabled && - currentSnapPoint !== null && - (dragState.isDragging || isResizing); - - const { - handleTracksMouseDown, - handleTracksClick, - handleRulerMouseDown, - handleRulerClick, - } = useTimelineSeek({ - playheadRef, - trackLabelsRef, - rulerScrollRef: tracksScrollRef, - tracksScrollRef, - zoomLevel, - duration: timeline.getTotalDuration(), - isSelecting, - clearSelectedElements: clearElementSelection, - seek, - }); - - useScrollSync({ - tracksScrollRef, - trackLabelsScrollRef, - }); - - const timelineHeaderHeight = - timelineHeaderRef.current?.getBoundingClientRect().height ?? 0; - - return ( -
- setZoomLevel(zoom)} - /> - -
- -
-
-
- . -
-
- . -
- {tracks.length > 0 && ( -
- -
- {tracks.map((track) => ( -
-
- {process.env.NODE_ENV === "development" && - isMainTrack(track) && ( -
- )} - {canTracktHaveAudio(track) && ( - - editor.timeline.toggleTrackMute({ - trackId: track.id, - }) - } - /> - )} - {canTrackBeHidden(track) && ( - - editor.timeline.toggleTrackVisibility({ - trackId: track.id, - }) - } - /> - )} - -
-
- ))} -
- -
- )} -
- -
- - - - { - const isDirectTarget = event.target === event.currentTarget; - if (!isDirectTarget) return; - event.stopPropagation(); - handleTracksMouseDown(event); - handleSelectionMouseDown(event); - }} - onClick={(event) => { - const isDirectTarget = event.target === event.currentTarget; - if (!isDirectTarget) return; - event.stopPropagation(); - handleTracksClick(event); - }} - onWheel={(event) => { - if ( - event.shiftKey || - Math.abs(event.deltaX) > Math.abs(event.deltaY) - ) { - return; - } - handleWheel(event); - }} - onScroll={() => { - saveScrollPosition(); - }} - > -
-
- - -
- -
- {tracks.length === 0 ? ( -
- ) : ( - tracks.map((track, index) => ( - - -
- { - handleSelectionMouseDown(event); - handleTracksMouseDown(event); - }} - onTrackClick={handleTracksClick} - shouldIgnoreClick={shouldIgnoreClick} - /> -
-
- - } - onClick={(e) => { - e.stopPropagation(); - invokeAction("paste-copied"); - }} - > - Paste elements - - { - e.stopPropagation(); - timeline.toggleTrackMute({ - trackId: track.id, - }); - }} - > - - - {canTracktHaveAudio(track) && track.muted - ? "Unmute track" - : "Mute track"} - - - { - e.stopPropagation(); - timeline.toggleTrackVisibility({ - trackId: track.id, - }); - }} - > - - - {canTrackBeHidden(track) && track.hidden - ? "Show track" - : "Hide track"} - - - { - e.stopPropagation(); - timeline.removeTrack({ - trackId: track.id, - }); - }} - variant="destructive" - > - - Delete track - - -
- )) - )} -
-
- -
-
-
-
- ); -} - -function TrackIcon({ track }: { track: TimelineTrack }) { - return <>{TRACK_ICONS[track.type]}; -} - -function TrackToggleIcon({ - isOff, - icons, - onClick, -}: { - isOff: boolean; - icons: { - on: IconSvgElement; - off: IconSvgElement; - }; - onClick: () => void; -}) { - return ( - <> - {isOff ? ( - - ) : ( - - )} - - ); -} diff --git a/apps/web/src/components/editor/timeline/snap-indicator.tsx b/apps/web/src/components/editor/timeline/snap-indicator.tsx deleted file mode 100644 index 914edccb..00000000 --- a/apps/web/src/components/editor/timeline/snap-indicator.tsx +++ /dev/null @@ -1,52 +0,0 @@ -"use client"; - -import { useSnapIndicatorPosition } from "@/hooks/timeline/use-snap-indicator-position"; -import type { SnapPoint } from "@/hooks/timeline/use-timeline-snapping"; -import type { TimelineTrack } from "@/types/timeline"; - -interface SnapIndicatorProps { - snapPoint: SnapPoint | null; - zoomLevel: number; - isVisible: boolean; - tracks: TimelineTrack[]; - timelineRef: React.RefObject; - trackLabelsRef?: React.RefObject; - tracksScrollRef: React.RefObject; -} - -export function SnapIndicator({ - snapPoint, - zoomLevel, - isVisible, - tracks, - timelineRef, - trackLabelsRef, - tracksScrollRef, -}: SnapIndicatorProps) { - const { leftPosition, topPosition, height } = useSnapIndicatorPosition({ - snapPoint, - zoomLevel, - tracks, - timelineRef, - trackLabelsRef, - tracksScrollRef, - }); - - if (!isVisible || !snapPoint) { - return null; - } - - return ( -
-
-
- ); -} diff --git a/apps/web/src/components/editor/timeline/timeline-element.tsx b/apps/web/src/components/editor/timeline/timeline-element.tsx deleted file mode 100644 index fd292ad8..00000000 --- a/apps/web/src/components/editor/timeline/timeline-element.tsx +++ /dev/null @@ -1,554 +0,0 @@ -"use client"; - -import { useEditor } from "@/hooks/use-editor"; -import { useAssetsPanelStore } from "@/stores/assets-panel-store"; -import AudioWaveform from "./audio-waveform"; -import { useTimelineElementResize } from "@/hooks/timeline/element/use-element-resize"; -import type { SnapPoint } from "@/hooks/timeline/use-timeline-snapping"; -import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; -import { - getTrackClasses, - getTrackHeight, - canElementHaveAudio, - canElementBeHidden, - hasMediaId, -} from "@/lib/timeline"; -import { - ContextMenu, - ContextMenuContent, - ContextMenuItem, - ContextMenuSeparator, - ContextMenuTrigger, -} from "../../ui/context-menu"; -import type { - TimelineElement as TimelineElementType, - TimelineTrack, - ElementDragState, -} from "@/types/timeline"; -import type { MediaAsset } from "@/types/assets"; -import { mediaSupportsAudio } from "@/lib/media/media-utils"; -import { getActionDefinition, type TAction, invokeAction } from "@/lib/actions"; -import { useElementSelection } from "@/hooks/timeline/element/use-element-selection"; -import Image from "next/image"; -import { - ScissorIcon, - Delete02Icon, - Copy01Icon, - ViewIcon, - ViewOffSlashIcon, - VolumeHighIcon, - VolumeOffIcon, - VolumeMute02Icon, - Search01Icon, - Exchange01Icon, -} from "@hugeicons/core-free-icons"; -import { HugeiconsIcon } from "@hugeicons/react"; -import { uppercase } from "@/utils/string"; -import type { ComponentProps } from "react"; - -function getDisplayShortcut(action: TAction) { - const { defaultShortcuts } = getActionDefinition(action); - if (!defaultShortcuts?.length) { - return ""; - } - - return uppercase({ - string: defaultShortcuts[0].replace("+", " "), - }); -} - -interface TimelineElementProps { - element: TimelineElementType; - track: TimelineTrack; - zoomLevel: number; - isSelected: boolean; - onSnapPointChange?: (snapPoint: SnapPoint | null) => void; - onResizeStateChange?: (params: { isResizing: boolean }) => void; - onElementMouseDown: ( - e: React.MouseEvent, - element: TimelineElementType, - ) => void; - onElementClick: (e: React.MouseEvent, element: TimelineElementType) => void; - dragState: ElementDragState; -} - -export function TimelineElement({ - element, - track, - zoomLevel, - isSelected, - onSnapPointChange, - onResizeStateChange, - onElementMouseDown, - onElementClick, - dragState, -}: TimelineElementProps) { - const editor = useEditor(); - const { selectedElements } = useElementSelection(); - const { requestRevealMedia } = useAssetsPanelStore(); - - const mediaAssets = editor.media.getAssets(); - let mediaAsset: MediaAsset | null = null; - - if (hasMediaId(element)) { - mediaAsset = - mediaAssets.find((asset) => asset.id === element.mediaId) ?? null; - } - - const hasAudio = mediaSupportsAudio({ media: mediaAsset }); - - const { handleResizeStart, isResizing, currentStartTime, currentDuration } = - useTimelineElementResize({ - element, - track, - zoomLevel, - onSnapPointChange, - onResizeStateChange, - }); - - const isCurrentElementSelected = selectedElements.some( - (selected) => - selected.elementId === element.id && selected.trackId === track.id, - ); - - const isBeingDragged = dragState.elementId === element.id; - const dragOffsetY = - isBeingDragged && dragState.isDragging - ? dragState.currentMouseY - dragState.startMouseY - : 0; - const elementStartTime = - isBeingDragged && dragState.isDragging - ? dragState.currentTime - : element.startTime; - const displayedStartTime = isResizing ? currentStartTime : elementStartTime; - const displayedDuration = isResizing ? currentDuration : element.duration; - const elementWidth = - displayedDuration * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel; - const elementLeft = displayedStartTime * 50 * zoomLevel; - - const handleRevealInMedia = ({ event }: { event: React.MouseEvent }) => { - event.stopPropagation(); - if (hasMediaId(element)) { - requestRevealMedia(element.mediaId); - } - }; - - const isMuted = canElementHaveAudio(element) && element.muted === true; - - return ( - - -
- -
-
- - }> - Split - - - {canElementHaveAudio(element) && hasAudio && ( - 1} - isCurrentElementSelected={isCurrentElementSelected} - isMuted={isMuted} - /> - )} - {canElementBeHidden(element) && ( - 1} - isCurrentElementSelected={isCurrentElementSelected} - /> - )} - {selectedElements.length === 1 && ( - }> - Duplicate - - )} - {selectedElements.length === 1 && hasMediaId(element) && ( - <> - } - onClick={(event) => handleRevealInMedia({ event })} - > - Reveal media - - } - disabled - > - Replace media - - - )} - - 1} - isCurrentElementSelected={isCurrentElementSelected} - elementType={element.type} - selectedCount={selectedElements.length} - /> - -
- ); -} - -function ElementInner({ - element, - track, - isSelected, - isBeingDragged, - hasAudio, - isMuted, - mediaAssets, - onElementClick, - onElementMouseDown, - handleResizeStart, -}: { - element: TimelineElementType; - track: TimelineTrack; - isSelected: boolean; - isBeingDragged: boolean; - hasAudio: boolean; - isMuted: boolean; - mediaAssets: MediaAsset[]; - onElementClick: (e: React.MouseEvent, element: TimelineElementType) => void; - onElementMouseDown: ( - e: React.MouseEvent, - element: TimelineElementType, - ) => void; - handleResizeStart: (params: { - e: React.MouseEvent; - elementId: string; - side: "left" | "right"; - }) => void; -}) { - return ( -
- - - {isSelected && ( - <> - - - - )} -
- ); -} - -function ResizeHandle({ - side, - elementId, - handleResizeStart, -}: { - side: "left" | "right"; - elementId: string; - handleResizeStart: (params: { - e: React.MouseEvent; - elementId: string; - side: "left" | "right"; - }) => void; -}) { - const isLeft = side === "left"; - return ( - - ); -} - -function ElementContent({ - element, - track, - isSelected, - mediaAssets, -}: { - element: TimelineElementType; - track: TimelineTrack; - isSelected: boolean; - mediaAssets: MediaAsset[]; -}) { - if (element.type === "text") { - return ( -
- {element.content} -
- ); - } - - if (element.type === "sticker") { - return ( -
- {element.name} - {element.name} -
- ); - } - - if (element.type === "audio") { - const audioBuffer = - element.sourceType === "library" ? element.buffer : undefined; - - const audioUrl = - element.sourceType === "library" - ? element.sourceUrl - : mediaAssets.find((asset) => asset.id === element.mediaId)?.url; - - if (audioBuffer || audioUrl) { - return ( -
-
- -
-
- ); - } - - return ( - - {element.name} - - ); - } - - const mediaAsset = mediaAssets.find((asset) => asset.id === element.mediaId); - if (!mediaAsset) { - return ( - - {element.name} - - ); - } - - if ( - mediaAsset.type === "image" || - (mediaAsset.type === "video" && mediaAsset.thumbnailUrl) - ) { - const trackHeight = getTrackHeight({ type: track.type }); - const tileWidth = trackHeight * (16 / 9); - const imageUrl = - mediaAsset.type === "image" ? mediaAsset.url : mediaAsset.thumbnailUrl; - - return ( -
-
-
-
-
- ); - } - - return ( - {element.name} - ); -} - -function CopyMenuItem() { - return ( - }> - Copy - - ); -} - -function MuteMenuItem({ - isMultipleSelected, - isCurrentElementSelected, - isMuted, -}: { - isMultipleSelected: boolean; - isCurrentElementSelected: boolean; - isMuted: boolean; -}) { - const getIcon = () => { - if (isMultipleSelected && isCurrentElementSelected) { - return ; - } - return isMuted ? ( - - ) : ( - - ); - }; - - return ( - - {isMuted ? "Unmute" : "Mute"} - - ); -} - -function VisibilityMenuItem({ - element, - isMultipleSelected, - isCurrentElementSelected, -}: { - element: TimelineElementType; - isMultipleSelected: boolean; - isCurrentElementSelected: boolean; -}) { - const isHidden = canElementBeHidden(element) && element.hidden; - - const getIcon = () => { - if (isMultipleSelected && isCurrentElementSelected) { - return ; - } - return isHidden ? ( - - ) : ( - - ); - }; - - return ( - - {isHidden ? "Show" : "Hide"} - - ); -} - -function DeleteMenuItem({ - isMultipleSelected, - isCurrentElementSelected, - elementType, - selectedCount, -}: { - isMultipleSelected: boolean; - isCurrentElementSelected: boolean; - elementType: TimelineElementType["type"]; - selectedCount: number; -}) { - return ( - } - > - {isMultipleSelected && isCurrentElementSelected - ? `Delete ${selectedCount} elements` - : `Delete ${elementType === "text" ? "text" : "clip"}`} - - ); -} - -function ActionMenuItem({ - action, - children, - ...props -}: Omit, "onClick" | "textRight"> & { - action: TAction; -}) { - return ( - { - event.stopPropagation(); - invokeAction(action); - }} - textRight={getDisplayShortcut(action)} - {...props} - > - {children} - - ); -} diff --git a/apps/web/src/components/editor/timeline/timeline-playhead.tsx b/apps/web/src/components/editor/timeline/timeline-playhead.tsx deleted file mode 100644 index 8af35dc9..00000000 --- a/apps/web/src/components/editor/timeline/timeline-playhead.tsx +++ /dev/null @@ -1,92 +0,0 @@ -"use client"; - -import { useRef } from "react"; -import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; -import { useTimelinePlayhead } from "@/hooks/timeline/use-timeline-playhead"; -import { useEditor } from "@/hooks/use-editor"; - -interface TimelinePlayheadProps { - zoomLevel: number; - rulerRef: React.RefObject; - rulerScrollRef: React.RefObject; - tracksScrollRef: React.RefObject; - timelineRef: React.RefObject; - playheadRef?: React.RefObject; - isSnappingToPlayhead?: boolean; -} - -export function TimelinePlayhead({ - zoomLevel, - rulerRef, - rulerScrollRef, - tracksScrollRef, - timelineRef, - playheadRef: externalPlayheadRef, - isSnappingToPlayhead = false, -}: TimelinePlayheadProps) { - const editor = useEditor(); - const duration = editor.timeline.getTotalDuration(); - const internalPlayheadRef = useRef(null); - const playheadRef = externalPlayheadRef || internalPlayheadRef; - - const { playheadPosition, handlePlayheadMouseDown } = useTimelinePlayhead({ - zoomLevel, - rulerRef, - rulerScrollRef, - tracksScrollRef, - playheadRef, - }); - - const timelineContainerHeight = - tracksScrollRef.current?.clientHeight ?? - timelineRef.current?.clientHeight ?? - 400; - const totalHeight = Math.max(0, timelineContainerHeight - 4); - - const timelinePosition = - playheadPosition * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel; - const leftPosition = timelinePosition; - - const handlePlayheadKeyDown = ( - event: React.KeyboardEvent, - ) => { - if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return; - - event.preventDefault(); - const step = 1 / Math.max(1, editor.project.getActive().settings.fps); - const direction = event.key === "ArrowRight" ? 1 : -1; - const nextTime = Math.max( - 0, - Math.min(duration, playheadPosition + direction * step), - ); - - editor.playback.seek({ time: nextTime }); - }; - - return ( -
-
- -
-
- ); -} diff --git a/apps/web/src/components/editor/timeline/timeline-ruler.tsx b/apps/web/src/components/editor/timeline/timeline-ruler.tsx deleted file mode 100644 index dfa0534b..00000000 --- a/apps/web/src/components/editor/timeline/timeline-ruler.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import type { JSX } from "react"; -import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; -import { DEFAULT_FPS } from "@/constants/project-constants"; -import { useEditor } from "@/hooks/use-editor"; -import { getRulerConfig, shouldShowLabel } from "@/lib/timeline/ruler-utils"; -import { useScrollPosition } from "@/hooks/timeline/use-scroll-position"; -import { TimelineTick } from "./timeline-tick"; - -interface TimelineRulerProps { - zoomLevel: number; - dynamicTimelineWidth: number; - rulerRef: React.Ref; - tracksScrollRef: React.RefObject; - handleWheel: (e: React.WheelEvent) => void; - handleTimelineContentClick: (e: React.MouseEvent) => void; - handleRulerTrackingMouseDown: (e: React.MouseEvent) => void; - handleRulerMouseDown: (e: React.MouseEvent) => void; -} - -export function TimelineRuler({ - zoomLevel, - dynamicTimelineWidth, - rulerRef, - tracksScrollRef, - handleWheel, - handleTimelineContentClick, - handleRulerTrackingMouseDown, - handleRulerMouseDown, -}: TimelineRulerProps) { - const editor = useEditor(); - const duration = editor.timeline.getTotalDuration(); - const pixelsPerSecond = TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel; - const visibleDuration = dynamicTimelineWidth / pixelsPerSecond; - const effectiveDuration = Math.max(duration, visibleDuration); - const project = editor.project.getActive(); - const fps = project?.settings.fps ?? DEFAULT_FPS; - const { labelIntervalSeconds, tickIntervalSeconds } = getRulerConfig({ - zoomLevel, - fps, - }); - const tickCount = Math.ceil(effectiveDuration / tickIntervalSeconds) + 1; - - const { scrollLeft, viewportWidth } = useScrollPosition({ - scrollRef: tracksScrollRef, - }); - - const bufferPx = 200; - const visibleStartTime = Math.max( - 0, - (scrollLeft - bufferPx) / pixelsPerSecond, - ); - const visibleEndTime = - (scrollLeft + viewportWidth + bufferPx) / pixelsPerSecond; - - const startTickIndex = Math.max( - 0, - Math.floor(visibleStartTime / tickIntervalSeconds), - ); - const endTickIndex = Math.min( - tickCount - 1, - Math.ceil(visibleEndTime / tickIntervalSeconds), - ); - - const timelineTicks: Array = []; - for (let tickIndex = startTickIndex; tickIndex <= endTickIndex; tickIndex += 1) { - const time = tickIndex * tickIntervalSeconds; - if (time > effectiveDuration) break; - - const showLabel = shouldShowLabel({ time, labelIntervalSeconds }); - timelineTicks.push( - , - ); - } - - return ( -
{}} - > -
- {timelineTicks} -
-
- ); -} diff --git a/apps/web/src/components/editor/timeline/timeline-tick.tsx b/apps/web/src/components/editor/timeline/timeline-tick.tsx deleted file mode 100644 index 9402d25e..00000000 --- a/apps/web/src/components/editor/timeline/timeline-tick.tsx +++ /dev/null @@ -1,39 +0,0 @@ -"use client"; - -import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; -import { formatRulerLabel } from "@/lib/timeline/ruler-utils"; - -interface TimelineTickProps { - time: number; - zoomLevel: number; - fps: number; - showLabel: boolean; -} - -export function TimelineTick({ - time, - zoomLevel, - fps, - showLabel, -}: TimelineTickProps) { - const leftPosition = time * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel; - - if (showLabel) { - const label = formatRulerLabel({ timeInSeconds: time, fps }); - return ( - - {label} - - ); - } - - return ( -
- ); -} diff --git a/apps/web/src/components/editor/timeline/timeline-toolbar.tsx b/apps/web/src/components/editor/timeline/timeline-toolbar.tsx deleted file mode 100644 index 6ac6fba5..00000000 --- a/apps/web/src/components/editor/timeline/timeline-toolbar.tsx +++ /dev/null @@ -1,354 +0,0 @@ -import { useEditor } from "@/hooks/use-editor"; -import { - TooltipProvider, - Tooltip, - TooltipTrigger, - TooltipContent, -} from "@/components/ui/tooltip"; -import { Button } from "@/components/ui/button"; -import { SkipBack, SplitSquareHorizontal } from "lucide-react"; -import { - SplitButton, - SplitButtonLeft, - SplitButtonRight, - SplitButtonSeparator, -} from "@/components/ui/split-button"; -import { Slider } from "@/components/ui/slider"; -import { formatTimeCode } from "@/lib/time"; -import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; -import { sliderToZoom, zoomToSlider } from "@/lib/timeline/zoom-utils"; -import { EditableTimecode } from "@/components/editable-timecode"; -import { ScenesView } from "../scenes-view"; -import { type TAction, invokeAction } from "@/lib/actions"; -import { cn } from "@/utils/ui"; -import { useTimelineStore } from "@/stores/timeline-store"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { - Bookmark02Icon, - Delete02Icon, - SnowIcon, - ScissorIcon, - MagnetIcon, - Link04Icon, - SearchAddIcon, - SearchMinusIcon, - PauseIcon, - PlayIcon, - Copy01Icon, - AlignLeftIcon, - AlignRightIcon, - Layers01Icon, -} from "@hugeicons/core-free-icons"; -import { HugeiconsIcon } from "@hugeicons/react"; - -export function TimelineToolbar({ - zoomLevel, - minZoom, - setZoomLevel, -}: { - zoomLevel: number; - minZoom: number; - setZoomLevel: ({ zoom }: { zoom: number }) => void; -}) { - const handleZoom = ({ direction }: { direction: "in" | "out" }) => { - const newZoomLevel = - direction === "in" - ? Math.min( - TIMELINE_CONSTANTS.ZOOM_MAX, - zoomLevel * TIMELINE_CONSTANTS.ZOOM_BUTTON_FACTOR, - ) - : Math.max(minZoom, zoomLevel / TIMELINE_CONSTANTS.ZOOM_BUTTON_FACTOR); - setZoomLevel({ zoom: newZoomLevel }); - }; - - return ( - -
- - - - - setZoomLevel({ zoom })} - onZoom={handleZoom} - /> -
-
- ); -} - -function ToolbarLeftSection() { - const editor = useEditor(); - const currentTime = editor.playback.getCurrentTime(); - const isPlaying = editor.playback.getIsPlaying(); - const currentBookmarked = editor.scenes.isBookmarked({ time: currentTime }); - - const handleAction = ({ - action, - event, - }: { - action: TAction; - event: React.MouseEvent; - }) => { - event.stopPropagation(); - invokeAction(action); - }; - - return ( -
- - - ) : ( - - ) - } - tooltip={isPlaying ? "Pause" : "Play"} - onClick={({ event }) => - handleAction({ action: "toggle-play", event }) - } - /> - - } - tooltip="Go to start" - onClick={({ event }) => handleAction({ action: "goto-start", event })} - /> - -
- - - -
- - } - tooltip="Split element" - onClick={({ event }) => handleAction({ action: "split", event })} - /> - - } - tooltip="Split left" - onClick={({ event }) => handleAction({ action: "split-left", event })} - /> - - } - tooltip="Split right" - onClick={({ event }) => - handleAction({ action: "split-right", event }) - } - /> - - } - tooltip="Coming soon" /* separate audio */ - disabled={true} - onClick={({ event: _event }) => {}} - /> - - } - tooltip="Duplicate element" - onClick={({ event }) => - handleAction({ action: "duplicate-selected", event }) - } - /> - - } - tooltip="Coming soon" /* freeze frame */ - disabled={true} - onClick={({ event: _event }) => {}} - /> - - } - tooltip="Delete element" - onClick={({ event }) => - handleAction({ action: "delete-selected", event }) - } - /> - -
- - - - } - tooltip={currentBookmarked ? "Remove bookmark" : "Add bookmark"} - onClick={({ event }) => - handleAction({ action: "toggle-bookmark", event }) - } - /> - - -
- ); -} - -function TimeDisplay() { - const editor = useEditor(); - const currentTime = editor.playback.getCurrentTime(); - const totalDuration = editor.timeline.getTotalDuration(); - const fps = editor.project.getActive().settings.fps; - - return ( -
- editor.playback.seek({ time })} - className="text-center" - /> -
/
-
- {formatTimeCode({ - timeInSeconds: totalDuration, - format: "HH:MM:SS:FF", - fps, - })} -
-
- ); -} - -function SceneSelector() { - const editor = useEditor(); - const currentScene = editor.scenes.getActiveScene(); - - return ( -
- - {currentScene?.name || "No Scene"} - - - {}} type="button"> - - - - -
- ); -} - -function ToolbarRightSection({ - zoomLevel, - minZoom, - onZoomChange, - onZoom, -}: { - zoomLevel: number; - minZoom: number; - onZoomChange: (zoom: number) => void; - onZoom: (options: { direction: "in" | "out" }) => void; -}) { - const { - snappingEnabled, - rippleEditingEnabled, - toggleSnapping, - toggleRippleEditing, - } = useTimelineStore(); - - return ( -
- - - } - tooltip="Auto snapping" - onClick={() => toggleSnapping()} - /> - - - } - tooltip="Ripple editing" - onClick={() => toggleRippleEditing()} - /> - - -
- -
- - - onZoomChange(sliderToZoom({ sliderPosition: values[0], minZoom })) - } - min={0} - max={1} - step={0.005} - /> - -
-
- ); -} - -function ToolbarButton({ - icon, - tooltip, - onClick, - disabled, -}: { - icon: React.ReactNode; - tooltip: string; - onClick: ({ event }: { event: React.MouseEvent }) => void; - disabled?: boolean; -}) { - return ( - - - - - {tooltip} - - ); -} diff --git a/apps/web/src/components/editor/timeline/timeline-track.tsx b/apps/web/src/components/editor/timeline/timeline-track.tsx deleted file mode 100644 index 47a9f282..00000000 --- a/apps/web/src/components/editor/timeline/timeline-track.tsx +++ /dev/null @@ -1,117 +0,0 @@ -"use client"; - -import { useElementSelection } from "@/hooks/timeline/element/use-element-selection"; -import { TimelineElement } from "./timeline-element"; -import type { TimelineTrack } from "@/types/timeline"; -import type { TimelineElement as TimelineElementType } from "@/types/timeline"; -import type { SnapPoint } from "@/hooks/timeline/use-timeline-snapping"; -import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; -import { useEdgeAutoScroll } from "@/hooks/timeline/use-edge-auto-scroll"; -import type { ElementDragState } from "@/types/timeline"; -import { useEditor } from "@/hooks/use-editor"; -import { cn } from "@/utils/ui"; - -interface TimelineTrackContentProps { - track: TimelineTrack; - zoomLevel: number; - dragState: ElementDragState; - rulerScrollRef: React.RefObject; - tracksScrollRef: React.RefObject; - lastMouseXRef: React.RefObject; - onSnapPointChange?: (snapPoint: SnapPoint | null) => void; - onResizeStateChange?: (params: { isResizing: boolean }) => void; - onElementMouseDown: (params: { - event: React.MouseEvent; - element: TimelineElementType; - track: TimelineTrack; - }) => void; - onElementClick: (params: { - event: React.MouseEvent; - element: TimelineElementType; - track: TimelineTrack; - }) => void; - onTrackMouseDown?: (event: React.MouseEvent) => void; - onTrackClick?: (event: React.MouseEvent) => void; - shouldIgnoreClick?: () => boolean; -} - -export function TimelineTrackContent({ - track, - zoomLevel, - dragState, - rulerScrollRef, - tracksScrollRef, - lastMouseXRef, - onSnapPointChange, - onResizeStateChange, - onElementMouseDown, - onElementClick, - onTrackMouseDown, - onTrackClick, - shouldIgnoreClick, -}: TimelineTrackContentProps) { - const editor = useEditor(); - const { isElementSelected, clearElementSelection } = useElementSelection(); - - const duration = editor.timeline.getTotalDuration(); - - useEdgeAutoScroll({ - isActive: dragState.isDragging, - getMouseClientX: () => lastMouseXRef.current ?? 0, - rulerScrollRef, - tracksScrollRef, - contentWidth: duration * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel, - }); - - const hasSelectedElements = track.elements.some((element) => - isElementSelected({ trackId: track.id, elementId: element.id }), - ); - - return ( - - ); -} -- cgit v1.3.1