Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions apps/desktop/src-tauri/src/deeplink_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ pub enum DeepLinkAction {
mode: RecordingMode,
},
StopRecording,
PauseRecording,
ResumeRecording,
TogglePauseRecording,
RestartRecording,
SwitchMicrophone {
mic_label: Option<String>,
},
SwitchCamera {
camera: Option<DeviceOrModelID>,
},
OpenEditor {
project_path: PathBuf,
},
Expand Down Expand Up @@ -146,6 +156,42 @@ impl DeepLinkAction {
DeepLinkAction::StopRecording => {
crate::recording::stop_recording(app.clone(), app.state()).await
}
DeepLinkAction::PauseRecording => {
let state = app.state::<ArcLock<App>>();
if state.read().await.current_recording().is_none() {
return Err("Recording not in progress".to_string());
}
crate::recording::pause_recording(app.clone(), state).await
}
Comment on lines 159 to 165
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For deeplinks, do you want pause/resume/toggle to behave like StopRecording and error when there’s no active recording? Right now these will succeed silently when nothing is recording.

Suggested change
DeepLinkAction::PauseRecording => {
crate::recording::pause_recording(app.clone(), app.state()).await
}
DeepLinkAction::PauseRecording => {
let state = app.state::<ArcLock<App>>();
if state.read().await.current_recording().is_none() {
return Err("Recording not in progress".to_string());
}
crate::recording::pause_recording(app.clone(), state).await
}

DeepLinkAction::ResumeRecording => {
let state = app.state::<ArcLock<App>>();
if state.read().await.current_recording().is_none() {
return Err("Recording not in progress".to_string());
}
crate::recording::resume_recording(app.clone(), state).await
}
DeepLinkAction::TogglePauseRecording => {
let state = app.state::<ArcLock<App>>();
if state.read().await.current_recording().is_none() {
return Err("Recording not in progress".to_string());
}
crate::recording::toggle_pause_recording(app.clone(), state).await
}
DeepLinkAction::RestartRecording => {
crate::recording::restart_recording(app.clone(), app.state())
.await
.map(|_| ())
}
DeepLinkAction::SwitchMicrophone { mic_label } => {
crate::set_mic_input(
app.state::<ArcLock<App>>(),
mic_label.filter(|label| !label.trim().is_empty()),
)
.await
}
DeepLinkAction::SwitchCamera { camera } => {
crate::set_camera_input(app.clone(), app.state(), camera, None).await
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor robustness: mirror the mic label handling and treat an empty DeviceID("") as unset so it doesn't trigger 3 init retries.

Suggested change
crate::set_camera_input(app.clone(), app.state(), camera, None).await
let camera = camera.filter(|id| match id {
DeviceOrModelID::DeviceID(device_id) => !device_id.trim().is_empty(),
DeviceOrModelID::ModelID(_) => true,
});
crate::set_camera_input(app.clone(), app.state(), camera, None).await

}
DeepLinkAction::OpenEditor { project_path } => {
crate::open_project_from_path(Path::new(&project_path), app.clone())
}
Expand All @@ -155,3 +201,46 @@ impl DeepLinkAction {
}
}
}

#[cfg(test)]
mod tests {
use super::{ActionParseFromUrlError, DeepLinkAction};
use tauri::Url;

fn action_url(payload: &str) -> Url {
let mut url = Url::parse("cap-desktop://action").expect("valid action url");
url.query_pairs_mut().append_pair("value", payload);
url
}

#[test]
fn parses_pause_recording_action() {
let url = action_url(r#""pause_recording""#);

let action = DeepLinkAction::try_from(&url).expect("parse pause action");
assert!(matches!(action, DeepLinkAction::PauseRecording));
}

#[test]
fn parses_switch_microphone_action() {
let url = action_url(r#"{"switch_microphone":{"mic_label":"Studio Mic"}}"#);

let action = DeepLinkAction::try_from(&url).expect("parse switch microphone action");
match action {
DeepLinkAction::SwitchMicrophone { mic_label } => {
assert_eq!(mic_label.as_deref(), Some("Studio Mic"));
}
other => panic!("unexpected action: {other:?}"),
}
}

#[test]
fn rejects_non_action_domain() {
let mut url = Url::parse("cap-desktop://signin").expect("valid signin url");
url.query_pairs_mut()
.append_pair("value", r#""stop_recording""#);

let error = DeepLinkAction::try_from(&url).expect_err("signin deeplink is not action");
assert!(matches!(error, ActionParseFromUrlError::NotAction));
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice to have: quick parse smoke tests for the other new actions, just to guard the serde rename plumbing.

Suggested change
}
#[test]
fn parses_restart_recording_action() {
let url = action_url(r#""restart_recording""#);
let action = DeepLinkAction::try_from(&url).expect("parse restart action");
assert!(matches!(action, DeepLinkAction::RestartRecording));
}
#[test]
fn parses_switch_camera_action() {
let url = action_url(r#"{"switch_camera":{"camera":{"DeviceID":"camera-1"}}}"#);
let action = DeepLinkAction::try_from(&url).expect("parse switch camera action");
match action {
DeepLinkAction::SwitchCamera { camera } => {
assert!(camera.is_some());
}
other => panic!("unexpected action: {other:?}"),
}
}
}