diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs new file mode 100644 index 000000000..0d0c79d9d --- /dev/null +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -0,0 +1,145 @@ +use cap_recording::RecordingMode; +use serde::{Deserialize, Serialize}; +use tauri::{AppHandle, Manager, Url}; + +use crate::{recording::StartRecordingInputs, windows::ShowCapWindow, App, ArcLock}; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum CaptureMode { + Screen(String), + Window(String), +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum DeepLinkAction { + StartRecording { + capture_mode: CaptureMode, + camera_label: Option, + mic_label: Option, + capture_system_audio: bool, + mode: RecordingMode, + }, + StopRecording, + OpenEditor { + project_path: String, + }, + OpenSettings { + page: Option, + }, +} + +pub fn handle(app_handle: &AppHandle, urls: Vec) { + #[cfg(debug_assertions)] + println!("Handling deep actions for: {:?}", &urls); + + let actions: Vec<_> = urls + .into_iter() + .filter(|url| !url.as_str().is_empty()) + .filter_map(|url| { + DeepLinkAction::try_from(&url) + .map_err(|e| match e { + ActionParseFromUrlError::ParseFailed(msg) => { + eprintln!("Failed to parse deep link \"{}\": {}", &url, msg) + } + ActionParseFromUrlError::Invalid => { + eprintln!("Invalid deep link format \"{}\"", &url) + } + // Likely login action, not handled here. + ActionParseFromUrlError::NotAction => {} + }) + .ok() + }) + .collect(); + + if actions.is_empty() { + return; + } + + let app_handle = app_handle.clone(); + tauri::async_runtime::spawn(async move { + for action in actions { + if let Err(e) = action.execute(&app_handle).await { + eprintln!("Failed to handle deep link action: {}", e); + } + } + }); +} + +pub enum ActionParseFromUrlError { + ParseFailed(String), + Invalid, + NotAction, +} + +impl TryFrom<&Url> for DeepLinkAction { + type Error = ActionParseFromUrlError; + + fn try_from(url: &Url) -> Result { + match url.domain() { + Some(v) if v != "action" => Err(ActionParseFromUrlError::NotAction), + _ => Err(ActionParseFromUrlError::Invalid), + }?; + + let params = url + .query_pairs() + .collect::>(); + let json_value = params + .get("value") + .ok_or(ActionParseFromUrlError::Invalid)?; + let action: Self = serde_json::from_str(json_value) + .map_err(|e| ActionParseFromUrlError::ParseFailed(e.to_string()))?; + Ok(action) + } +} + +impl DeepLinkAction { + pub async fn execute(self, app: &AppHandle) -> Result<(), String> { + match self { + DeepLinkAction::StartRecording { + capture_mode, + camera_label, + mic_label, + capture_system_audio, + mode, + } => { + let state = app.state::>(); + + crate::set_camera_input(app.clone(), state.clone(), camera_label).await?; + crate::set_mic_input(state.clone(), mic_label).await?; + + use cap_media::sources::ScreenCaptureTarget; + let capture_target: ScreenCaptureTarget = match capture_mode { + CaptureMode::Screen(name) => cap_media::sources::list_screens() + .into_iter() + .find(|(s, _)| s.name == name) + .map(|(s, _)| ScreenCaptureTarget::Screen { id: s.id }) + .ok_or(format!("No screen with name \"{}\"", &name))?, + CaptureMode::Window(name) => cap_media::sources::list_windows() + .into_iter() + .find(|(w, _)| w.name == name) + .map(|(w, _)| ScreenCaptureTarget::Window { id: w.id }) + .ok_or(format!("No window with name \"{}\"", &name))?, + }; + + let inputs = StartRecordingInputs { + capture_target, + capture_system_audio, + mode, + }; + + crate::recording::start_recording(app.clone(), state, inputs).await + } + DeepLinkAction::StopRecording => { + crate::recording::stop_recording(app.clone(), app.state()).await + } + DeepLinkAction::OpenEditor { project_path } => { + crate::open_project_from_path(&project_path.into(), app.clone()) + } + DeepLinkAction::OpenSettings { page } => { + crate::show_window(app.clone(), ShowCapWindow::Settings { page }).await + } + } + } +} diff --git a/apps/desktop/src-tauri/src/events.rs b/apps/desktop/src-tauri/src/events.rs deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index e90c9fb62..6a8c08b9c 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -2,6 +2,7 @@ mod audio; mod auth; mod camera; mod captions; +mod deeplink_actions; mod flags; mod general_settings; mod hotkeys; @@ -67,6 +68,7 @@ use std::{ }; use tauri::Window; use tauri::{AppHandle, Manager, State, WindowEvent}; +use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_dialog::DialogExt; use tauri_plugin_notification::{NotificationExt, PermissionState}; use tauri_plugin_opener::OpenerExt; @@ -2016,6 +2018,14 @@ pub async fn run(recording_logging_handle: LoggingHandle) { .await; }); + // Registering deep links at runtime is not possible on macOS, + // so deep links can only be tested on the bundled application, + // which must be installed in the /Applications directory. + let app_handle = app.clone(); + app.deep_link().on_open_url(move |event| { + deeplink_actions::handle(&app_handle, event.urls()); + }); + Ok(()) }) .on_window_event(|window, event| { diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 3d3cafac4..448fd064c 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -24,13 +24,7 @@ "deep-link": { "desktop": { "schemes": ["cap-desktop"] - }, - "mobile": [ - { - "host": "cap.so", - "pathPrefix": ["/signin"] - } - ] + } } }, "bundle": {