diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index a117028487..d41cfc9212 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -4,7 +4,7 @@ use cap_recording::{ use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; use tauri::{AppHandle, Manager, Url}; -use tracing::trace; +use tracing::{warn, error}; use crate::{App, ArcLock, recording::StartRecordingInputs, windows::ShowCapWindow}; @@ -32,25 +32,30 @@ pub enum DeepLinkAction { OpenSettings { page: Option, }, + StartDefaultRecording, + ResumeRecording, + TogglePauseRecording, } pub fn handle(app_handle: &AppHandle, urls: Vec) { - trace!("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) + .map_err(|e| { + let mut safe_url = url.clone(); + safe_url.set_query(None); + safe_url.set_fragment(None); + match e { + ActionParseFromUrlError::ParseFailed(msg) => { + error!("Failed to parse deep link \"{}\": {}", safe_url, msg) + } + ActionParseFromUrlError::Invalid => { + warn!("Invalid deep link format \"{}\"", safe_url) + } + ActionParseFromUrlError::NotAction => {} } - // Likely login action, not handled here. - ActionParseFromUrlError::NotAction => {} }) .ok() }) @@ -64,7 +69,7 @@ pub fn handle(app_handle: &AppHandle, urls: Vec) { 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}"); + error!("Failed to handle deep link action: {e}"); } } }); @@ -80,14 +85,35 @@ impl TryFrom<&Url> for DeepLinkAction { type Error = ActionParseFromUrlError; fn try_from(url: &Url) -> Result { + let scheme = url.scheme().to_lowercase(); + #[cfg(target_os = "macos")] - if url.scheme() == "file" { + if scheme == "file" { return url .to_file_path() .map(|project_path| Self::OpenEditor { project_path }) .map_err(|_| ActionParseFromUrlError::Invalid); } + if scheme == "cap" { + let host = url.host_str().unwrap_or_default(); + let path = url.path().trim_matches('/'); + + let action = if host.eq_ignore_ascii_case("record") || path.eq_ignore_ascii_case("record") { + Some(Self::StartDefaultRecording) + } else if host.eq_ignore_ascii_case("stop") || path.eq_ignore_ascii_case("stop") { + Some(Self::StopRecording) + } else if host.eq_ignore_ascii_case("pause") || path.eq_ignore_ascii_case("pause") { + Some(Self::TogglePauseRecording) + } else if host.eq_ignore_ascii_case("resume") || path.eq_ignore_ascii_case("resume") { + Some(Self::ResumeRecording) + } else { + None + }; + + return action.ok_or(ActionParseFromUrlError::Invalid); + } + match url.domain() { Some(v) if v != "action" => Err(ActionParseFromUrlError::NotAction), _ => Err(ActionParseFromUrlError::Invalid), @@ -107,6 +133,17 @@ impl TryFrom<&Url> for DeepLinkAction { impl DeepLinkAction { pub async fn execute(self, app: &AppHandle) -> Result<(), String> { + match &self { + DeepLinkAction::StartRecording { .. } + | DeepLinkAction::StartDefaultRecording + | DeepLinkAction::StopRecording + | DeepLinkAction::ResumeRecording + | DeepLinkAction::TogglePauseRecording => { + crate::notifications::NotificationType::DeepLinkTriggered.send_always(app); + } + _ => {} + } + match self { DeepLinkAction::StartRecording { capture_mode, @@ -125,12 +162,12 @@ impl DeepLinkAction { .into_iter() .find(|(s, _)| s.name == name) .map(|(s, _)| ScreenCaptureTarget::Display { id: s.id }) - .ok_or(format!("No screen with name \"{}\"", &name))?, + .ok_or_else(|| format!("No screen with name \"{}\"", &name))?, CaptureMode::Window(name) => cap_recording::screen_capture::list_windows() .into_iter() .find(|(w, _)| w.name == name) .map(|(w, _)| ScreenCaptureTarget::Window { id: w.id }) - .ok_or(format!("No window with name \"{}\"", &name))?, + .ok_or_else(|| format!("No window with name \"{}\"", &name))?, }; let inputs = StartRecordingInputs { @@ -153,6 +190,15 @@ impl DeepLinkAction { DeepLinkAction::OpenSettings { page } => { crate::show_window(app.clone(), ShowCapWindow::Settings { page }).await } + DeepLinkAction::StartDefaultRecording => { + crate::RequestOpenRecordingPicker { target_mode: None }.emit(app).map_err(|e| e.to_string()) + } + DeepLinkAction::ResumeRecording => { + crate::recording::resume_recording(app.clone(), app.state()).await + } + DeepLinkAction::TogglePauseRecording => { + crate::recording::toggle_pause_recording(app.clone(), app.state()).await + } } } } diff --git a/apps/desktop/src-tauri/src/notifications.rs b/apps/desktop/src-tauri/src/notifications.rs index d872fd8380..4cd4a2f90a 100644 --- a/apps/desktop/src-tauri/src/notifications.rs +++ b/apps/desktop/src-tauri/src/notifications.rs @@ -14,6 +14,7 @@ pub enum NotificationType { ScreenshotCopiedToClipboard, ScreenshotSaveFailed, ScreenshotCopyFailed, + DeepLinkTriggered, } impl NotificationType { @@ -62,6 +63,11 @@ impl NotificationType { "Unable to copy screenshot to clipboard. Please try again", true, ), + NotificationType::DeepLinkTriggered => ( + "Action Triggered", + "An action was triggered via a deep link", + false, + ), } } @@ -86,12 +92,25 @@ impl NotificationType { pub fn send(self, app: &tauri::AppHandle) { send_notification(app, self); } + + pub fn send_always(self, app: &tauri::AppHandle) { + send_notification_always(app, self); + } } pub fn send_notification(app: &tauri::AppHandle, notification_type: NotificationType) { - let enable_notifications = GeneralSettingsStore::get(app) - .map(|settings| settings.is_some_and(|s| s.enable_notifications)) - .unwrap_or(false); + _send_notification(app, notification_type, false); +} + +pub fn send_notification_always(app: &tauri::AppHandle, notification_type: NotificationType) { + _send_notification(app, notification_type, true); +} + +fn _send_notification(app: &tauri::AppHandle, notification_type: NotificationType, always: bool) { + let enable_notifications = always + || GeneralSettingsStore::get(app) + .map(|settings| settings.is_some_and(|s| s.enable_notifications)) + .unwrap_or(false); if !enable_notifications { return; @@ -112,6 +131,7 @@ pub fn send_notification(app: &tauri::AppHandle, notification_type: Notification | NotificationType::ScreenshotCopiedToClipboard | NotificationType::ScreenshotSaveFailed | NotificationType::ScreenshotCopyFailed + | NotificationType::DeepLinkTriggered ); if !skip_sound { diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 691c2f0995..80aba7fd80 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -30,7 +30,7 @@ "updater": { "active": false, "pubkey": "" }, "deep-link": { "desktop": { - "schemes": ["cap-desktop"] + "schemes": ["cap-desktop", "cap"] } } },