diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-03-31 23:24:33 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-03-31 23:24:33 +0200 |
| commit | 608dc1a62fdb133e218f45796cf936bdfb27c7ae (patch) | |
| tree | 605132265bab1f345b0edfee8bdadfcaf8e973db /apps/web/src/components/editor/panels/timeline | |
| parent | cc150c7cfb859dfc38372210ec5bcc8bd196bbc4 (diff) | |
| parent | f1d5932c54178c4f8eac61d3d3e0e4a30101780f (diff) | |
| download | opencut-608dc1a62fdb133e218f45796cf936bdfb27c7ae.tar.gz | |
Merge branch 'feature/separate-audio'
Diffstat (limited to 'apps/web/src/components/editor/panels/timeline')
| -rw-r--r-- | apps/web/src/components/editor/panels/timeline/timeline-element.tsx | 35 | ||||
| -rw-r--r-- | apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx | 63 |
2 files changed, 93 insertions, 5 deletions
diff --git a/apps/web/src/components/editor/panels/timeline/timeline-element.tsx b/apps/web/src/components/editor/panels/timeline/timeline-element.tsx index f196da44..bb30b0b0 100644 --- a/apps/web/src/components/editor/panels/timeline/timeline-element.tsx +++ b/apps/web/src/components/editor/panels/timeline/timeline-element.tsx @@ -40,6 +40,11 @@ import type { import type { MediaAsset } from "@/lib/media/types";
import { mediaSupportsAudio } from "@/lib/media/media-utils";
import {
+ canToggleSourceAudio,
+ getSourceAudioActionLabel,
+ isSourceAudioSeparated,
+} from "@/lib/timeline/audio-separation";
+import {
getActionDefinition,
type TAction,
type TActionWithOptionalArgs,
@@ -61,7 +66,9 @@ import { Search01Icon,
Exchange01Icon,
KeyframeIcon,
+ Link02Icon,
MagicWand05Icon,
+ Unlink02Icon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { uppercase } from "@/utils/string";
@@ -280,6 +287,19 @@ export function TimelineElement({ };
const isMuted = canElementHaveAudio(element) && element.muted === true;
+ const canToggleCurrentSourceAudio =
+ selectedElements.length === 1 &&
+ isCurrentElementSelected &&
+ canToggleSourceAudio({
+ element,
+ mediaAsset,
+ });
+ const sourceAudioLabel =
+ element.type === "video"
+ ? getSourceAudioActionLabel({ element })
+ : "Extract audio";
+ const isElementSourceAudioSeparated =
+ element.type === "video" && isSourceAudioSeparated({ element });
return (
<ContextMenu>
@@ -334,6 +354,21 @@ export function TimelineElement({ isMuted={isMuted}
/>
)}
+ {canToggleCurrentSourceAudio && (
+ <ContextMenuItem
+ icon={
+ <HugeiconsIcon
+ icon={isElementSourceAudioSeparated ? Unlink02Icon : Link02Icon}
+ />
+ }
+ onClick={(event: React.MouseEvent) => {
+ event.stopPropagation();
+ invokeAction("toggle-source-audio");
+ }}
+ >
+ {sourceAudioLabel}
+ </ContextMenuItem>
+ )}
{canElementBeHidden(element) && (
<VisibilityMenuItem
element={element}
diff --git a/apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx b/apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx index f398264c..abe36f88 100644 --- a/apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx +++ b/apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx @@ -1,4 +1,5 @@ import { useEditor } from "@/hooks/use-editor"; +import { useElementSelection } from "@/hooks/timeline/element/use-element-selection"; import { TooltipProvider, Tooltip, @@ -6,7 +7,6 @@ import { TooltipContent, } from "@/components/ui/tooltip"; import { Button } from "@/components/ui/button"; -import { SplitSquareHorizontal } from "lucide-react"; import { SplitButton, SplitButtonLeft, @@ -18,6 +18,12 @@ import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; import { sliderToZoom, zoomToSlider } from "@/lib/timeline/zoom-utils"; import { ScenesView } from "@/components/editor/scenes-view"; import { type TActionWithOptionalArgs, invokeAction } from "@/lib/actions"; +import { + canToggleSourceAudio, + getSourceAudioActionLabel, + isSourceAudioSeparated, +} from "@/lib/timeline/audio-separation"; +import { hasMediaId } from "@/lib/timeline"; import { cn } from "@/utils/ui"; import { useTimelineStore } from "@/stores/timeline-store"; import { ScrollArea } from "@/components/ui/scroll-area"; @@ -33,8 +39,10 @@ import { Copy01Icon, AlignLeftIcon, AlignRightIcon, + Link02Icon, Layers01Icon, Chart03Icon, + Unlink02Icon, } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { OcRippleIcon } from "@/components/icons"; @@ -78,9 +86,47 @@ export function TimelineToolbar({ } function ToolbarLeftSection() { + const editor = useEditor(); + const mediaAssets = useEditor((currentEditor) => currentEditor.media.getAssets()); + const { selectedElements } = useElementSelection(); 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({ + element: selectedElement.element, + mediaAsset: 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, @@ -117,10 +163,16 @@ function ToolbarLeftSection() { /> <ToolbarButton - icon={<SplitSquareHorizontal />} - tooltip="Separate audio (coming soon)" - disabled={true} - onClick={({ event: _event }) => {}} + icon={ + <HugeiconsIcon + icon={isSelectedSourceAudioSeparated ? Unlink02Icon : Link02Icon} + /> + } + tooltip={sourceAudioLabel} + disabled={!canToggleSelectedSourceAudio} + onClick={({ event }) => + handleAction({ action: "toggle-source-audio", event }) + } /> <ToolbarButton @@ -275,6 +327,7 @@ function ToolbarButton({ <Button variant={isActive ? "secondary" : "text"} size="icon" + disabled={disabled} onClick={(event) => onClick({ event })} className={cn( "rounded-sm", |
