diff --git a/.changes/on-download-hook.md b/.changes/on-download-hook.md new file mode 100644 index 000000000000..a25a749102b2 --- /dev/null +++ b/.changes/on-download-hook.md @@ -0,0 +1,5 @@ +--- +"tauri": patch:feat +--- + +Added `WindowBuilder::on_download` to handle download request events. diff --git a/.changes/runtime-on-download-hooks.md b/.changes/runtime-on-download-hooks.md new file mode 100644 index 000000000000..3f2a0d31e651 --- /dev/null +++ b/.changes/runtime-on-download-hooks.md @@ -0,0 +1,6 @@ +--- +"tauri-runtime": patch:feat +"tauri-runtime-wry": patch:feat +--- + +Added download event closure via `PendingWindow::download_handler`. diff --git a/core/tauri-runtime-wry/src/lib.rs b/core/tauri-runtime-wry/src/lib.rs index 244f8769613b..61dde457bbb9 100644 --- a/core/tauri-runtime-wry/src/lib.rs +++ b/core/tauri-runtime-wry/src/lib.rs @@ -17,7 +17,8 @@ use tauri_runtime::{ webview::{WebviewIpcHandler, WindowBuilder, WindowBuilderBase}, window::{ dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}, - CursorIcon, DetachedWindow, FileDropEvent, PendingWindow, RawWindow, WindowEvent, + CursorIcon, DetachedWindow, DownloadEvent, FileDropEvent, PendingWindow, RawWindow, + WindowEvent, }, DeviceEventFilter, Dispatch, Error, EventLoopProxy, ExitRequestedEventAction, Icon, Result, RunEvent, RunIteration, Runtime, RuntimeHandle, RuntimeInitArgs, UserAttentionType, UserEvent, @@ -2719,8 +2720,6 @@ fn create_webview( label, ipc_handler, url, - #[cfg(target_os = "android")] - on_webview_created, .. } = pending; @@ -2852,6 +2851,25 @@ fn create_webview( }); } + if let Some(download_handler) = pending.download_handler { + let download_handler_ = download_handler.clone(); + webview_builder = webview_builder.with_download_started_handler(move |url, path| { + if let Ok(url) = url.parse() { + download_handler_(DownloadEvent::Requested { + url, + destination: path, + }) + } else { + false + } + }); + webview_builder = webview_builder.with_download_completed_handler(move |url, path, success| { + if let Ok(url) = url.parse() { + download_handler(DownloadEvent::Finished { url, path, success }); + } + }); + } + if let Some(page_load_handler) = pending.on_page_load_handler { webview_builder = webview_builder.with_on_page_load_handler(move |event, url| { let _ = Url::parse(&url).map(|url| { @@ -2954,7 +2972,7 @@ fn create_webview( #[cfg(target_os = "android")] { - if let Some(on_webview_created) = on_webview_created { + if let Some(on_webview_created) = pending.on_webview_created { webview_builder = webview_builder.on_webview_created(move |ctx| { on_webview_created(tauri_runtime::window::CreationContext { env: ctx.env, diff --git a/core/tauri-runtime/src/window.rs b/core/tauri-runtime/src/window.rs index 6a436f56eab1..30b2902a2b83 100644 --- a/core/tauri-runtime/src/window.rs +++ b/core/tauri-runtime/src/window.rs @@ -19,7 +19,7 @@ use std::{ hash::{Hash, Hasher}, marker::PhantomData, path::PathBuf, - sync::mpsc::Sender, + sync::{mpsc::Sender, Arc}, }; use self::dpi::PhysicalPosition; @@ -36,6 +36,30 @@ type NavigationHandler = dyn Fn(&Url) -> bool + Send; type OnPageLoadHandler = dyn Fn(Url, PageLoadEvent) + Send; +type DownloadHandler = dyn Fn(DownloadEvent) -> bool + Send + Sync; + +/// Download event. +pub enum DownloadEvent<'a> { + /// Download requested. + Requested { + /// The url being downloaded. + url: Url, + /// Represents where the file will be downloaded to. + /// Can be used to set the download location by assigning a new path to it. + /// The assigned path _must_ be absolute. + destination: &'a mut PathBuf, + }, + /// Download finished. + Finished { + /// The URL of the original download request. + url: Url, + /// Potentially representing the filesystem path the file was downloaded to. + path: Option, + /// Indicates if the download succeeded or not. + success: bool, + }, +} + /// Kind of event for the page load handler. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PageLoadEvent { @@ -240,6 +264,8 @@ pub struct PendingWindow> { /// A handler to decide if incoming url is allowed to navigate. pub navigation_handler: Option>, + pub download_handler: Option>, + /// The resolved URL to load on the webview. pub url: String, @@ -284,6 +310,7 @@ impl> PendingWindow { label, ipc_handler: None, navigation_handler: None, + download_handler: None, url: "tauri://localhost".to_string(), #[cfg(target_os = "android")] on_webview_created: None, @@ -313,6 +340,7 @@ impl> PendingWindow { label, ipc_handler: None, navigation_handler: None, + download_handler: None, url: "tauri://localhost".to_string(), #[cfg(target_os = "android")] on_webview_created: None, diff --git a/core/tauri/src/window/mod.rs b/core/tauri/src/window/mod.rs index 2f64419aa654..d5c3bad684db 100644 --- a/core/tauri/src/window/mod.rs +++ b/core/tauri/src/window/mod.rs @@ -64,6 +64,7 @@ use std::{ pub(crate) type WebResourceRequestHandler = dyn Fn(http::Request>, &mut http::Response>) + Send + Sync; pub(crate) type NavigationHandler = dyn Fn(&Url) -> bool + Send; +pub(crate) type DownloadHandler = dyn Fn(Window, DownloadEvent<'_>) -> bool + Send + Sync; pub(crate) type UriSchemeProtocolHandler = Box>, UriSchemeResponder) + Send + Sync>; pub(crate) type OnPageLoad = dyn Fn(Window, PageLoadPayload<'_>) + Send + Sync + 'static; @@ -92,6 +93,38 @@ impl<'a> PageLoadPayload<'a> { } } +/// Download event for the [`WindowBuilder#method.on_download`] hook. +#[non_exhaustive] +pub enum DownloadEvent<'a> { + /// Download requested. + Requested { + /// The url being downloaded. + url: Url, + /// Represents where the file will be downloaded to. + /// Can be used to set the download location by assigning a new path to it. + /// The assigned path _must_ be absolute. + destination: &'a mut PathBuf, + }, + /// Download finished. + Finished { + /// The URL of the original download request. + url: Url, + /// Potentially representing the filesystem path the file was downloaded to. + /// + /// A value of `None` being passed instead of a `PathBuf` does not necessarily indicate that the download + /// did not succeed, and may instead indicate some other failure - always check the third parameter if you need to + /// know if the download succeeded. + /// + /// ## Platform-specific: + /// + /// - **macOS**: The second parameter indicating the path the file was saved to is always empty, due to API + /// limitations. + path: Option, + /// Indicates if the download succeeded or not. + success: bool, + }, +} + /// Monitor descriptor. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] @@ -149,6 +182,7 @@ pub struct WindowBuilder<'a, R: Runtime> { pub(crate) webview_attributes: WebviewAttributes, web_resource_request_handler: Option>, navigation_handler: Option>, + download_handler: Option>>, on_page_load_handler: Option>>, #[cfg(desktop)] on_menu_event: Option>>, @@ -228,6 +262,7 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> { webview_attributes: WebviewAttributes::new(url), web_resource_request_handler: None, navigation_handler: None, + download_handler: None, on_page_load_handler: None, #[cfg(desktop)] on_menu_event: None, @@ -267,6 +302,7 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> { window_builder: >::WindowBuilder::with_config( config, ), + download_handler: None, web_resource_request_handler: None, #[cfg(desktop)] menu: None, @@ -353,6 +389,47 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> { self } + /// Set a download event handler to be notified when a download is requested or finished. + /// + /// Returning `false` prevents the download from happening on a [`DownloadEvent::Requested`] event. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri::{ + /// utils::config::{Csp, CspDirectiveSources, WindowUrl}, + /// window::{DownloadEvent, WindowBuilder}, + /// }; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// WindowBuilder::new(app, "core", WindowUrl::App("index.html".into())) + /// .on_download(|window, event| { + /// match event { + /// DownloadEvent::Requested { url, destination } => { + /// println!("downloading {}", url); + /// *destination = "/home/tauri/target/path".into(); + /// } + /// DownloadEvent::Finished { url, path, success } => { + /// println!("downloaded {} to {:?}, success: {}", url, path, success); + /// } + /// _ => (), + /// } + /// // let the download start + /// true + /// }) + /// .build()?; + /// Ok(()) + /// }); + /// ``` + pub fn on_download, DownloadEvent<'_>) -> bool + Send + Sync + 'static>( + mut self, + f: F, + ) -> Self { + self.download_handler.replace(Arc::new(f)); + self + } + /// Defines a closure to be executed when a page load event is triggered. /// The event can be either [`PageLoadEvent::Started`] if the page has started loading /// or [`PageLoadEvent::Finished`] when the page finishes loading. @@ -361,18 +438,16 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> { /// /// ```rust,no_run /// use tauri::{ - /// utils::config::{Csp, CspDirectiveSources, WindowUrl}, + /// utils::config::WindowUrl, /// window::{PageLoadEvent, WindowBuilder}, /// }; - /// use http::header::HeaderValue; - /// use std::collections::HashMap; /// tauri::Builder::default() /// .setup(|app| { /// WindowBuilder::new(app, "core", WindowUrl::App("index.html".into())) /// .on_page_load(|window, payload| { /// match payload.event() { /// PageLoadEvent::Started => { - /// println!("{} finished loading", payload.url()); + /// println!("{} started loading", payload.url()); /// } /// PageLoadEvent::Finished => { /// println!("{} finished loading", payload.url()); @@ -444,6 +519,28 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> { pending.navigation_handler = self.navigation_handler.take(); pending.web_resource_request_handler = self.web_resource_request_handler.take(); + if let Some(download_handler) = self.download_handler.take() { + let label = pending.label.clone(); + let manager = self.app_handle.manager.clone(); + pending.download_handler.replace(Arc::new(move |event| { + if let Some(w) = manager.get_window(&label) { + download_handler( + w, + match event { + tauri_runtime::window::DownloadEvent::Requested { url, destination } => { + DownloadEvent::Requested { url, destination } + } + tauri_runtime::window::DownloadEvent::Finished { url, path, success } => { + DownloadEvent::Finished { url, path, success } + } + }, + ) + } else { + false + } + })); + } + if let Some(on_page_load_handler) = self.on_page_load_handler.take() { let label = pending.label.clone(); let manager = self.app_handle.manager.clone();