import { useEditor } from "@/editor/use-editor"; import { useElementSelection } from "@/timeline/hooks/element/use-element-selection"; import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent, } from "@/components/ui/tooltip"; import { Button } from "@/components/ui/button"; import { SplitButton, SplitButtonLeft, SplitButtonRight, SplitButtonSeparator, } from "@/components/ui/split-button"; import { Slider } from "@/components/ui/slider"; import { TIMELINE_ZOOM_BUTTON_FACTOR } from "./interaction"; import { TIMELINE_ZOOM_MAX } from "@/timeline/scale"; import { sliderToZoom, zoomToSlider } from "@/timeline/zoom-utils"; import { ScenesView } from "@/components/editor/scenes-view"; import { type TActionWithOptionalArgs, invokeAction } from "@/actions"; import { canToggleSourceAudio, getSourceAudioActionLabel, isSourceAudioSeparated, } from "@/timeline/audio-separation"; import { hasMediaId } from "@/timeline"; import { cn } from "@/utils/ui"; import { useTimelineStore } from "@/timeline/timeline-store"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Bookmark02Icon, Delete02Icon, SnowIcon, ScissorIcon, MagnetIcon, SearchAddIcon, SearchMinusIcon, Copy01Icon, AlignLeftIcon, AlignRightIcon, Link02Icon, Layers01Icon, Chart03Icon, Unlink02Icon, } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { OcRippleIcon } from "@/components/icons"; import { GraphEditorPopover } from "./graph-editor/popover"; import { PopoverTrigger } from "@/components/ui/popover"; import { useGraphEditorController } from "./graph-editor/use-controller"; 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_ZOOM_MAX, zoomLevel * TIMELINE_ZOOM_BUTTON_FACTOR) : Math.max(minZoom, zoomLevel / TIMELINE_ZOOM_BUTTON_FACTOR); setZoomLevel({ zoom: newZoomLevel }); }; return (
setZoomLevel({ zoom })} onZoom={handleZoom} />
); } function ToolbarLeftSection() { const editor = useEditor(); const mediaAssets = useEditor((currentEditor) => currentEditor.media.getAssets(), ); const { selectedElements } = useElementSelection(); const graphEditor = useGraphEditorController(); const isCurrentlyBookmarked = useEditor((e) => e.scenes.isBookmarked({ time: e.playback.getCurrentTime() }), ); const selectedElement = selectedElements.length === 1 ? (editor.timeline.getElementsWithTracks({ elements: selectedElements, })[0] ?? null) : null; const selectedMediaAsset = (() => { if (!selectedElement) { return null; } const { element } = selectedElement; if (!hasMediaId(element)) { return null; } return mediaAssets.find((asset) => asset.id === element.mediaId) ?? null; })(); const canToggleSelectedSourceAudio = !!selectedElement && canToggleSourceAudio(selectedElement.element, selectedMediaAsset); const sourceAudioLabel = selectedElement?.element.type === "video" ? getSourceAudioActionLabel({ element: selectedElement.element, }) : "Extract audio"; const isSelectedSourceAudioSeparated = selectedElement?.element.type === "video" && isSourceAudioSeparated({ element: selectedElement.element, }); const handleAction = ({ action, event, }: { action: TActionWithOptionalArgs; event: React.MouseEvent; }) => { event.stopPropagation(); invokeAction(action); }; return (
} 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={sourceAudioLabel} disabled={!canToggleSelectedSourceAudio} onClick={({ event }) => handleAction({ action: "toggle-source-audio", event }) } /> } tooltip="Duplicate element" onClick={({ event }) => handleAction({ action: "duplicate-selected", event }) } /> } tooltip="Freeze frame (coming soon)" disabled={true} onClick={({ event: _event }) => {}} /> } tooltip="Delete element" onClick={({ event }) => handleAction({ action: "delete-selected", event }) } />
} isActive={isCurrentlyBookmarked} tooltip={isCurrentlyBookmarked ? "Remove bookmark" : "Add bookmark"} onClick={({ event }) => handleAction({ action: "toggle-bookmark", event }) } /> } tooltip={graphEditor.tooltip} disabled={!graphEditor.canOpen} buttonWrapper={(button) => graphEditor.canOpen ? ( {button} ) : ( button ) } />
); } function SceneSelector() { const editor = useEditor(); const currentScene = editor.scenes.getActiveScene(); return (
{currentScene?.name || "No Scene"} {}}>
); } function ToolbarRightSection({ zoomLevel, minZoom, onZoomChange, onZoom, }: { zoomLevel: number; minZoom: number; onZoomChange: (zoom: number) => void; onZoom: (options: { direction: "in" | "out" }) => void; }) { const snappingEnabled = useTimelineStore((s) => s.snappingEnabled); const rippleEditingEnabled = useTimelineStore((s) => s.rippleEditingEnabled); const toggleSnapping = useTimelineStore((s) => s.toggleSnapping); const toggleRippleEditing = useTimelineStore((s) => s.toggleRippleEditing); return (
} isActive={snappingEnabled} tooltip="Auto snapping" onClick={() => toggleSnapping()} /> } isActive={rippleEditingEnabled} tooltip="Ripple editing" onClick={() => toggleRippleEditing()} />
onZoomChange(sliderToZoom({ sliderPosition: values[0], minZoom })) } min={0} max={1} step={0.005} />
); } function ToolbarButton({ icon, tooltip, onClick, disabled, isActive, buttonWrapper, }: { icon: React.ReactNode; tooltip: string; onClick?: ({ event }: { event: React.MouseEvent }) => void; disabled?: boolean; isActive?: boolean; buttonWrapper?: (button: React.ReactElement) => React.ReactElement; }) { const button = ( ); const trigger = disabled ? ( {button} ) : buttonWrapper ? ( buttonWrapper(button) ) : ( button ); return ( {trigger} {tooltip} ); }