mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-06-06 13:53:54 +02:00
feat(deep-link): Add deep link support for desktop (#916)
This commit is contained in:
committed by
Lucas Nogueira
parent
eb1679b997
commit
021d23bef3
@@ -14,3 +14,33 @@ pub(crate) async fn get_current<R: Runtime>(
|
||||
) -> Result<Option<Vec<url::Url>>> {
|
||||
deep_link.get_current()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn register<R: Runtime>(
|
||||
_app: AppHandle<R>,
|
||||
_window: Window<R>,
|
||||
deep_link: State<'_, DeepLink<R>>,
|
||||
protocol: String,
|
||||
) -> Result<()> {
|
||||
deep_link.register(protocol)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn unregister<R: Runtime>(
|
||||
_app: AppHandle<R>,
|
||||
_window: Window<R>,
|
||||
deep_link: State<'_, DeepLink<R>>,
|
||||
protocol: String,
|
||||
) -> Result<()> {
|
||||
deep_link.unregister(protocol)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn is_registered<R: Runtime>(
|
||||
_app: AppHandle<R>,
|
||||
_window: Window<R>,
|
||||
deep_link: State<'_, DeepLink<R>>,
|
||||
protocol: String,
|
||||
) -> Result<bool> {
|
||||
deep_link.is_registered(protocol)
|
||||
}
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
|
||||
// This module is also imported in build.rs!
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use tauri_utils::config::DeepLinkProtocol;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AssociatedDomain {
|
||||
@@ -32,5 +31,16 @@ where
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Config {
|
||||
pub domains: Vec<AssociatedDomain>,
|
||||
/// Mobile requires `https://<host>` urls.
|
||||
pub mobile: Vec<AssociatedDomain>,
|
||||
/// Desktop requires urls starting with `<scheme>://`.
|
||||
/// These urls are also active in dev mode on Android.
|
||||
pub desktop: DesktopProtocol,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum DesktopProtocol {
|
||||
One(DeepLinkProtocol),
|
||||
List(Vec<DeepLinkProtocol>),
|
||||
}
|
||||
|
||||
@@ -8,8 +8,21 @@ pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("unsupported platform")]
|
||||
UnsupportedPlatform,
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error(transparent)]
|
||||
Tauri(#[from] tauri::Error),
|
||||
#[cfg(target_os = "windows")]
|
||||
#[error(transparent)]
|
||||
Windows(#[from] windows_result::Error),
|
||||
#[cfg(target_os = "linux")]
|
||||
#[error(transparent)]
|
||||
Ini(#[from] ini::Error),
|
||||
#[cfg(target_os = "linux")]
|
||||
#[error(transparent)]
|
||||
ParseIni(#[from] ini::ParseError),
|
||||
#[cfg(mobile)]
|
||||
#[error(transparent)]
|
||||
PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),
|
||||
|
||||
@@ -82,32 +82,279 @@ mod imp {
|
||||
pub struct DeepLink<R: Runtime>(pub(crate) PluginHandle<R>);
|
||||
|
||||
impl<R: Runtime> DeepLink<R> {
|
||||
/// Get the current URLs that triggered the deep link.
|
||||
/// Get the current URLs that triggered the deep link. Use this on app load to check whether your app was started via a deep link.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Windows / Linux**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`).
|
||||
pub fn get_current(&self) -> crate::Result<Option<Vec<url::Url>>> {
|
||||
self.0
|
||||
.run_mobile_plugin::<GetCurrentResponse>("getCurrent", ())
|
||||
.map(|v| v.url.map(|url| vec![url]))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Register the app as the default handler for the specified protocol.
|
||||
///
|
||||
/// - `protocol`: The name of the protocol without `://`. For example, if you want your app to handle `tauri://` links, call this method with `tauri` as the protocol.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **macOS / Android / iOS**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`).
|
||||
pub fn register<S: AsRef<str>>(&self, _protocol: S) -> crate::Result<()> {
|
||||
Err(crate::Error::UnsupportedPlatform)
|
||||
}
|
||||
|
||||
/// Unregister the app as the default handler for the specified protocol.
|
||||
///
|
||||
/// - `protocol`: The name of the protocol without `://`.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Linux**: Can only unregister the scheme if it was initially registered with [`register`](`Self::register`). May not work on older distros.
|
||||
/// - **macOS / Android / iOS**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`).
|
||||
pub fn unregister<S: AsRef<str>>(&self, _protocol: S) -> crate::Result<()> {
|
||||
Err(crate::Error::UnsupportedPlatform)
|
||||
}
|
||||
|
||||
/// Check whether the app is the default handler for the specified protocol.
|
||||
///
|
||||
/// - `protocol`: The name of the protocol without `://`.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **macOS / Android / iOS**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`).
|
||||
pub fn is_registered<S: AsRef<str>>(&self, _protocol: S) -> crate::Result<bool> {
|
||||
Err(crate::Error::UnsupportedPlatform)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
mod imp {
|
||||
use std::sync::Mutex;
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::{
|
||||
fs::{create_dir_all, File},
|
||||
io::Write,
|
||||
process::Command,
|
||||
};
|
||||
#[cfg(target_os = "linux")]
|
||||
use tauri::Manager;
|
||||
use tauri::{AppHandle, Runtime};
|
||||
#[cfg(windows)]
|
||||
use windows_registry::CURRENT_USER;
|
||||
|
||||
/// Access to the deep-link APIs.
|
||||
pub struct DeepLink<R: Runtime> {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) app: AppHandle<R>,
|
||||
#[allow(dead_code)]
|
||||
pub(crate) current: Mutex<Option<Vec<url::Url>>>,
|
||||
}
|
||||
|
||||
impl<R: Runtime> DeepLink<R> {
|
||||
/// Get the current URLs that triggered the deep link.
|
||||
/// Get the current URLs that triggered the deep link. Use this on app load to check whether your app was started via a deep link.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Windows / Linux**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`).
|
||||
pub fn get_current(&self) -> crate::Result<Option<Vec<url::Url>>> {
|
||||
Ok(self.current.lock().unwrap().clone())
|
||||
#[cfg(not(any(windows, target_os = "linux")))]
|
||||
return Ok(self.current.lock().unwrap().clone());
|
||||
#[cfg(any(windows, target_os = "linux"))]
|
||||
Err(crate::Error::UnsupportedPlatform)
|
||||
}
|
||||
|
||||
/// Register the app as the default handler for the specified protocol.
|
||||
///
|
||||
/// - `protocol`: The name of the protocol without `://`. For example, if you want your app to handle `tauri://` links, call this method with `tauri` as the protocol.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **macOS / Android / iOS**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`).
|
||||
pub fn register<S: AsRef<str>>(&self, _protocol: S) -> crate::Result<()> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let key_base = format!("Software\\Classes\\{}", _protocol.as_ref());
|
||||
|
||||
let exe = dunce::simplified(&tauri::utils::platform::current_exe()?)
|
||||
.display()
|
||||
.to_string();
|
||||
|
||||
let key_reg = CURRENT_USER.create(&key_base)?;
|
||||
key_reg.set_string(
|
||||
"",
|
||||
&format!("URL:{} protocol", self.app.config().identifier),
|
||||
)?;
|
||||
key_reg.set_string("URL Protocol", "")?;
|
||||
|
||||
let icon_reg = CURRENT_USER.create(format!("{key_base}\\DefaultIcon"))?;
|
||||
icon_reg.set_string("", &format!("{},0", &exe))?;
|
||||
|
||||
let cmd_reg = CURRENT_USER.create(format!("{key_base}\\shell\\open\\command"))?;
|
||||
|
||||
cmd_reg.set_string("", &format!("{} \"%1\"", &exe))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let bin = tauri::utils::platform::current_exe()?;
|
||||
let file_name = format!(
|
||||
"{}-handler.desktop",
|
||||
bin.file_name().unwrap().to_string_lossy()
|
||||
);
|
||||
let appimage = self.app.env().appimage;
|
||||
let exec = appimage
|
||||
.clone()
|
||||
.unwrap_or_else(|| bin.into_os_string())
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
let target = self.app.path().data_dir()?.join("applications");
|
||||
|
||||
create_dir_all(&target)?;
|
||||
|
||||
let target_file = target.join(&file_name);
|
||||
|
||||
let mime_type = format!("x-scheme-handler/{};", _protocol.as_ref());
|
||||
|
||||
if let Ok(mut desktop_file) = ini::Ini::load_from_file(&target_file) {
|
||||
if let Some(section) = desktop_file.section_mut(Some("Desktop Entry")) {
|
||||
if let Some(mimes) = section.remove("MimeType") {
|
||||
section.append("MimeType", format!("{mimes};{mime_type};"))
|
||||
} else {
|
||||
section.append("MimeType", format!("{mime_type};"))
|
||||
}
|
||||
desktop_file.write_to_file(&target_file)?;
|
||||
}
|
||||
} else {
|
||||
let mut file = File::create(target_file)?;
|
||||
file.write_all(
|
||||
format!(
|
||||
include_str!("template.desktop"),
|
||||
name = self
|
||||
.app
|
||||
.config()
|
||||
.product_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| file_name.clone()),
|
||||
exec = exec,
|
||||
mime_type = mime_type
|
||||
)
|
||||
.as_bytes(),
|
||||
)?;
|
||||
}
|
||||
|
||||
Command::new("update-desktop-database")
|
||||
.arg(target)
|
||||
.status()?;
|
||||
|
||||
Command::new("xdg-mime")
|
||||
.args(["default", &file_name, _protocol.as_ref()])
|
||||
.status()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(any(windows, target_os = "linux")))]
|
||||
Err(crate::Error::UnsupportedPlatform)
|
||||
}
|
||||
|
||||
/// Unregister the app as the default handler for the specified protocol.
|
||||
///
|
||||
/// - `protocol`: The name of the protocol without `://`.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Linux**: Can only unregister the scheme if it was initially registered with [`register`](`Self::register`). May not work on older distros.
|
||||
/// - **macOS / Android / iOS**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`).
|
||||
pub fn unregister<S: AsRef<str>>(&self, _protocol: S) -> crate::Result<()> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
CURRENT_USER.remove_tree(format!("Software\\Classes\\{}", _protocol.as_ref()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let mimeapps_path = self.app.path().config_dir()?.join("mimeapps.list");
|
||||
let mut mimeapps = ini::Ini::load_from_file(&mimeapps_path)?;
|
||||
|
||||
let file_name = format!(
|
||||
"{}-handler.desktop",
|
||||
tauri::utils::platform::current_exe()?
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
);
|
||||
|
||||
if let Some(section) = mimeapps.section_mut(Some("Default Applications")) {
|
||||
let scheme = format!("x-scheme-handler/{}", _protocol.as_ref());
|
||||
|
||||
if section.get(&scheme).unwrap_or_default() == file_name {
|
||||
section.remove(scheme);
|
||||
}
|
||||
}
|
||||
|
||||
mimeapps.write_to_file(mimeapps_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(any(windows, target_os = "linux")))]
|
||||
Err(crate::Error::UnsupportedPlatform)
|
||||
}
|
||||
|
||||
/// Check whether the app is the default handler for the specified protocol.
|
||||
///
|
||||
/// - `protocol`: The name of the protocol without `://`.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **macOS / Android / iOS**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`).
|
||||
pub fn is_registered<S: AsRef<str>>(&self, _protocol: S) -> crate::Result<bool> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let cmd_reg = CURRENT_USER.open(format!(
|
||||
"Software\\Classes\\{}\\shell\\open\\command",
|
||||
_protocol.as_ref()
|
||||
))?;
|
||||
|
||||
let registered_cmd: String = cmd_reg.get_string("")?;
|
||||
|
||||
let exe = dunce::simplified(&tauri::utils::platform::current_exe()?)
|
||||
.display()
|
||||
.to_string();
|
||||
|
||||
Ok(registered_cmd == format!("{} \"%1\"", &exe))
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let file_name = format!(
|
||||
"{}-handler.desktop",
|
||||
tauri::utils::platform::current_exe()?
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
);
|
||||
|
||||
let output = Command::new("xdg-mime")
|
||||
.args([
|
||||
"query",
|
||||
"default",
|
||||
&format!("x-scheme-handler/{}", _protocol.as_ref()),
|
||||
])
|
||||
.output()?;
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).contains(&file_name))
|
||||
}
|
||||
|
||||
#[cfg(not(any(windows, target_os = "linux")))]
|
||||
Err(crate::Error::UnsupportedPlatform)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,7 +375,12 @@ impl<R: Runtime, T: Manager<R>> crate::DeepLinkExt<R> for T {
|
||||
/// Initializes the plugin.
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
|
||||
Builder::new("deep-link")
|
||||
.invoke_handler(tauri::generate_handler![commands::get_current])
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::get_current,
|
||||
commands::register,
|
||||
commands::unregister,
|
||||
commands::is_registered
|
||||
])
|
||||
.setup(|app, api| {
|
||||
app.manage(init_deep_link(app, api)?);
|
||||
Ok(())
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name={name}
|
||||
Exec={exec} %u
|
||||
Terminal=false
|
||||
MimeType={mime_type}
|
||||
NoDisplay=true
|
||||
Reference in New Issue
Block a user