From 783e9ac8462751adc14d78b070095fd63e45bda0 Mon Sep 17 00:00:00 2001 From: cashmere Date: Mon, 27 Jul 2026 01:21:20 +0200 Subject: feat: voice-over recording into the timeline MediaRecorder mic capture (defensive mimeType picking, permission/no-mic toasts) with elapsed display in the timeline toolbar. Stop produces an audio asset and inserts it at the playhead on an audio track as ONE BatchCommand undo step, freeze-frame precedent. voice-over action registered, no default keybinding. 20 tests; smoke-tested in Chromium with a fake mic end to end. --- .../src/timeline/components/timeline-toolbar.tsx | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) (limited to 'apps/web/src/timeline/components') diff --git a/apps/web/src/timeline/components/timeline-toolbar.tsx b/apps/web/src/timeline/components/timeline-toolbar.tsx index 6208ef27..47906931 100644 --- a/apps/web/src/timeline/components/timeline-toolbar.tsx +++ b/apps/web/src/timeline/components/timeline-toolbar.tsx @@ -43,12 +43,17 @@ import { Layers01Icon, Chart03Icon, Unlink02Icon, + MicIcon, + StopIcon, } 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"; +import { formatElapsedSeconds } from "@/voiceover/recorder"; +import { useRecordingElapsedSeconds } from "@/voiceover/use-recording-elapsed"; +import { useVoiceOverStore } from "@/voiceover/voiceover-store"; export function TimelineToolbar({ zoomLevel, @@ -95,6 +100,10 @@ function ToolbarLeftSection() { const isCurrentlyBookmarked = useEditor((e) => e.scenes.isBookmarked({ time: e.playback.getCurrentTime() }), ); + const isRecordingVoiceOver = useVoiceOverStore( + (state) => state.status === "recording", + ); + const voiceOverElapsedSeconds = useRecordingElapsedSeconds(); const canFreezeFrame = useEditor((e) => { const time = e.playback.getCurrentTime(); const { overlay, main } = e.scenes.getActiveScene().tracks; @@ -202,6 +211,23 @@ function ToolbarLeftSection() { onClick={({ event }) => handleAction({ action: "freeze-frame", event })} /> + + } + tooltip={ + isRecordingVoiceOver ? "Stop voice-over" : "Record voice-over" + } + isActive={isRecordingVoiceOver} + onClick={({ event }) => handleAction({ action: "voice-over", event })} + /> + + {isRecordingVoiceOver && ( + + {formatElapsedSeconds({ seconds: voiceOverElapsedSeconds })} + + )} + } tooltip="Delete element" -- cgit v1.3.1