diff --git a/apps/desktop/src-tauri/src/export.rs b/apps/desktop/src-tauri/src/export.rs index 0eba1e546a..3ba63d6268 100644 --- a/apps/desktop/src-tauri/src/export.rs +++ b/apps/desktop/src-tauri/src/export.rs @@ -293,6 +293,7 @@ pub async fn generate_export_preview( .iter() .map(|s| RenderSegment { cursor: s.cursor.clone(), + keyboard: s.keyboard.clone(), decoders: s.decoders.clone(), }) .collect(); diff --git a/apps/desktop/src-tauri/src/import.rs b/apps/desktop/src-tauri/src/import.rs index c37b701bb9..2af5408a85 100644 --- a/apps/desktop/src-tauri/src/import.rs +++ b/apps/desktop/src-tauri/src/import.rs @@ -506,6 +506,7 @@ pub async fn start_video_import(app: AppHandle, source_path: PathBuf) -> Result< mic: None, system_audio: None, cursor: None, + keyboard: None, }], cursors: Cursors::default(), status: Some(StudioRecordingStatus::InProgress), @@ -599,6 +600,7 @@ pub async fn start_video_import(app: AppHandle, source_path: PathBuf) -> Result< mic: None, system_audio, cursor: None, + keyboard: None, }], cursors: Cursors::default(), status: Some(StudioRecordingStatus::Complete), diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 38fed85f50..6ee79cd62a 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -1971,6 +1971,51 @@ async fn generate_zoom_segments_from_clicks( Ok(zoom_segments) } +#[tauri::command] +#[specta::specta] +#[instrument(skip(editor_instance))] +async fn generate_keyboard_segments( + editor_instance: WindowEditorInstance, + grouping_threshold_ms: f64, + linger_duration_ms: f64, + show_modifiers: bool, + show_special_keys: bool, +) -> Result, String> { + let meta = editor_instance.meta(); + + let RecordingMetaInner::Studio(studio_meta) = &meta.inner else { + return Ok(vec![]); + }; + + let segments = match studio_meta.as_ref() { + StudioRecordingMeta::MultipleSegments { inner, .. } => &inner.segments, + _ => return Ok(vec![]), + }; + + let mut all_events = cap_project::KeyboardEvents { presses: vec![] }; + + for segment in segments { + let events = segment.keyboard_events(&meta); + all_events.presses.extend(events.presses); + } + + all_events.presses.sort_by(|a, b| { + a.time_ms + .partial_cmp(&b.time_ms) + .unwrap_or(std::cmp::Ordering::Equal) + }); + + let grouped = cap_project::group_key_events( + &all_events, + grouping_threshold_ms, + linger_duration_ms, + show_modifiers, + show_special_keys, + ); + + Ok(grouped) +} + #[tauri::command] #[specta::specta] #[instrument] @@ -2960,6 +3005,7 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { set_project_config, update_project_config_in_memory, generate_zoom_segments_from_clicks, + generate_keyboard_segments, permissions::open_permission_settings, permissions::do_permissions_check, permissions::request_permission, diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index d65bce27fc..247e58e4d8 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -2370,6 +2370,8 @@ fn project_config_from_recording( scene_segments: Vec::new(), mask_segments: Vec::new(), text_segments: Vec::new(), + caption_segments: Vec::new(), + keyboard_segments: Vec::new(), }); config diff --git a/apps/desktop/src/routes/editor/CaptionsTab.tsx b/apps/desktop/src/routes/editor/CaptionsTab.tsx index 356bfa493b..c7a97d68dd 100644 --- a/apps/desktop/src/routes/editor/CaptionsTab.tsx +++ b/apps/desktop/src/routes/editor/CaptionsTab.tsx @@ -364,6 +364,31 @@ export function CaptionsTab() { if (result && result.segments.length > 0) { setProject("captions", "segments", result.segments); updateCaptionSetting("enabled", true); + + const trackSegments = result.segments.map( + (seg: { + id: string; + start: number; + end: number; + text: string; + words?: Array<{ text: string; start: number; end: number }>; + }) => ({ + id: seg.id, + start: seg.start, + end: seg.end, + text: seg.text, + words: seg.words ?? [], + fadeDurationOverride: null, + lingerDurationOverride: null, + positionOverride: null, + colorOverride: null, + backgroundColorOverride: null, + fontSizeOverride: null, + }), + ); + setProject("timeline", "captionSegments", trackSegments); + setEditorState("timeline", "tracks", "caption", true); + toast.success("Captions generated successfully!"); } else { toast.error( @@ -395,52 +420,14 @@ export function CaptionsTab() { } }; - const deleteSegment = (id: string) => { - if (!project?.captions?.segments) return; - - setProject( - "captions", - "segments", - project.captions.segments.filter((segment) => segment.id !== id), - ); - }; - - const updateSegment = ( - id: string, - updates: Partial<{ start: number; end: number; text: string }>, - ) => { - if (!project?.captions?.segments) return; - - setProject( - "captions", - "segments", - project.captions.segments.map((segment) => - segment.id === id ? { ...segment, ...updates } : segment, - ), - ); - }; - - const addSegment = (time: number) => { - if (!project?.captions) return; - - const id = `segment-${Date.now()}`; - setProject("captions", "segments", [ - ...project.captions.segments, - { - id, - start: time, - end: time + 2, - text: "New caption", - }, - ]); - }; - const hasCaptions = createMemo( - () => (project.captions?.segments?.length ?? 0) > 0, + () => + (project.timeline?.captionSegments?.length ?? 0) > 0 || + (project.captions?.segments?.length ?? 0) > 0, ); return ( - }> + } badge="Beta">
@@ -890,117 +877,106 @@ export function CaptionsTab() {
- - }> -
-
- -
- -
- - {(segment) => ( -
-
-
-
- - - updateSegment(segment.id, { - start: parseFloat(e.target.value), - }) - } - /> -
-
- - - updateSegment(segment.id, { - end: parseFloat(e.target.value), - }) - } - /> -
-
- -
- -
-