#![doc(
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
)]
use serde::{Deserialize, Serialize};
use tauri::{
plugin::{Builder, TauriPlugin},
Manager, Runtime,
};
use std::{
path::{Path, PathBuf},
sync::mpsc::sync_channel,
};
pub use models::*;
pub use tauri_plugin_fs::FilePath;
#[cfg(desktop)]
mod desktop;
#[cfg(mobile)]
mod mobile;
mod commands;
mod error;
mod models;
pub use error::{Error, Result};
#[cfg(desktop)]
use desktop::*;
#[cfg(mobile)]
use mobile::*;
#[cfg(desktop)]
pub use desktop::Dialog;
#[cfg(mobile)]
pub use mobile::Dialog;
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "lowercase")]
pub enum PickerMode {
Document,
Media,
Image,
Video,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "lowercase")]
pub enum FileAccessMode {
Copy,
Scoped,
}
pub(crate) const OK: &str = "Ok";
#[cfg(mobile)]
pub(crate) const CANCEL: &str = "Cancel";
#[cfg(mobile)]
pub(crate) const YES: &str = "Yes";
#[cfg(mobile)]
pub(crate) const NO: &str = "No";
macro_rules! blocking_fn {
($self:ident, $fn:ident) => {{
let (tx, rx) = sync_channel(0);
let cb = move |response| {
tx.send(response).unwrap();
};
$self.$fn(cb);
rx.recv().unwrap()
}};
}
pub trait DialogExt<R: Runtime> {
fn dialog(&self) -> &Dialog<R>;
}
impl<R: Runtime, T: Manager<R>> crate::DialogExt<R> for T {
fn dialog(&self) -> &Dialog<R> {
self.state::<Dialog<R>>().inner()
}
}
impl<R: Runtime> Dialog<R> {
pub fn message(&self, message: impl Into<String>) -> MessageDialogBuilder<R> {
MessageDialogBuilder::new(
self.clone(),
self.app_handle().package_info().name.clone(),
message,
)
}
pub fn file(&self) -> FileDialogBuilder<R> {
FileDialogBuilder::new(self.clone())
}
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
#[allow(unused_mut)]
let mut builder = Builder::new("dialog");
#[cfg(not(target_os = "android"))]
{
builder = builder.js_init_script(include_str!("init-iife.js").to_string());
}
builder
.invoke_handler(tauri::generate_handler![
commands::open,
commands::save,
commands::message,
])
.setup(|app, api| {
#[cfg(mobile)]
let dialog = mobile::init(app, api)?;
#[cfg(desktop)]
let dialog = desktop::init(app, api)?;
app.manage(dialog);
Ok(())
})
.build()
}
pub struct MessageDialogBuilder<R: Runtime> {
#[allow(dead_code)]
pub(crate) dialog: Dialog<R>,
pub(crate) title: String,
pub(crate) message: String,
pub(crate) kind: MessageDialogKind,
pub(crate) buttons: MessageDialogButtons,
#[cfg(desktop)]
pub(crate) parent: Option<crate::desktop::WindowHandle>,
}
#[cfg(mobile)]
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MessageDialogPayload<'a> {
title: &'a String,
message: &'a String,
kind: &'a MessageDialogKind,
ok_button_label: Option<&'a str>,
no_button_label: Option<&'a str>,
cancel_button_label: Option<&'a str>,
}
unsafe impl<R: Runtime> Send for MessageDialogBuilder<R> {}
impl<R: Runtime> MessageDialogBuilder<R> {
pub fn new(dialog: Dialog<R>, title: impl Into<String>, message: impl Into<String>) -> Self {
Self {
dialog,
title: title.into(),
message: message.into(),
kind: MessageDialogKind::default(),
buttons: MessageDialogButtons::default(),
#[cfg(desktop)]
parent: None,
}
}
#[cfg(mobile)]
pub(crate) fn payload(&self) -> MessageDialogPayload<'_> {
let (ok_button_label, no_button_label, cancel_button_label) = match &self.buttons {
MessageDialogButtons::Ok => (Some(OK), None, None),
MessageDialogButtons::OkCancel => (Some(OK), None, Some(CANCEL)),
MessageDialogButtons::YesNo => (Some(YES), Some(NO), None),
MessageDialogButtons::YesNoCancel => (Some(YES), Some(NO), Some(CANCEL)),
MessageDialogButtons::OkCustom(ok) => (Some(ok.as_str()), None, None),
MessageDialogButtons::OkCancelCustom(ok, cancel) => {
(Some(ok.as_str()), None, Some(cancel.as_str()))
}
MessageDialogButtons::YesNoCancelCustom(yes, no, cancel) => {
(Some(yes.as_str()), Some(no.as_str()), Some(cancel.as_str()))
}
};
MessageDialogPayload {
title: &self.title,
message: &self.message,
kind: &self.kind,
ok_button_label,
no_button_label,
cancel_button_label,
}
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self
}
#[cfg(desktop)]
pub fn parent<W: raw_window_handle::HasWindowHandle + raw_window_handle::HasDisplayHandle>(
mut self,
parent: &W,
) -> Self {
if let (Ok(window_handle), Ok(display_handle)) =
(parent.window_handle(), parent.display_handle())
{
self.parent.replace(crate::desktop::WindowHandle::new(
window_handle.as_raw(),
display_handle.as_raw(),
));
}
self
}
pub fn buttons(mut self, buttons: MessageDialogButtons) -> Self {
self.buttons = buttons;
self
}
pub fn kind(mut self, kind: MessageDialogKind) -> Self {
self.kind = kind;
self
}
pub fn show<F: FnOnce(bool) + Send + 'static>(self, f: F) {
let ok_label = match &self.buttons {
MessageDialogButtons::OkCustom(ok) => Some(ok.clone()),
MessageDialogButtons::OkCancelCustom(ok, _) => Some(ok.clone()),
MessageDialogButtons::YesNoCancelCustom(yes, _, _) => Some(yes.clone()),
_ => None,
};
show_message_dialog(self, move |res| {
let sucess = match res {
MessageDialogResult::Ok | MessageDialogResult::Yes => true,
MessageDialogResult::Custom(s) => {
ok_label.map_or(s == OK, |ok_label| ok_label == s)
}
_ => false,
};
f(sucess)
})
}
pub fn show_with_result<F: FnOnce(MessageDialogResult) + Send + 'static>(self, f: F) {
show_message_dialog(self, f)
}
pub fn blocking_show(self) -> bool {
blocking_fn!(self, show)
}
pub fn blocking_show_with_result(self) -> MessageDialogResult {
blocking_fn!(self, show_with_result)
}
}
#[derive(Debug, Serialize)]
pub(crate) struct Filter {
pub name: String,
pub extensions: Vec<String>,
}
#[derive(Debug)]
pub struct FileDialogBuilder<R: Runtime> {
#[allow(dead_code)]
pub(crate) dialog: Dialog<R>,
pub(crate) filters: Vec<Filter>,
pub(crate) starting_directory: Option<PathBuf>,
pub(crate) file_name: Option<String>,
pub(crate) title: Option<String>,
pub(crate) can_create_directories: Option<bool>,
pub(crate) picker_mode: Option<PickerMode>,
pub(crate) file_access_mode: Option<FileAccessMode>,
#[cfg(desktop)]
pub(crate) parent: Option<crate::desktop::WindowHandle>,
}
#[cfg(mobile)]
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct FileDialogPayload<'a> {
file_name: &'a Option<String>,
filters: &'a Vec<Filter>,
multiple: bool,
picker_mode: &'a Option<PickerMode>,
file_access_mode: &'a Option<FileAccessMode>,
}
unsafe impl<R: Runtime> Send for FileDialogBuilder<R> {}
impl<R: Runtime> FileDialogBuilder<R> {
pub fn new(dialog: Dialog<R>) -> Self {
Self {
dialog,
filters: Vec::new(),
starting_directory: None,
file_name: None,
title: None,
can_create_directories: None,
picker_mode: None,
file_access_mode: None,
#[cfg(desktop)]
parent: None,
}
}
#[cfg(mobile)]
pub(crate) fn payload(&self, multiple: bool) -> FileDialogPayload<'_> {
FileDialogPayload {
file_name: &self.file_name,
filters: &self.filters,
multiple,
picker_mode: &self.picker_mode,
file_access_mode: &self.file_access_mode,
}
}
#[must_use]
pub fn add_filter(mut self, name: impl Into<String>, extensions: &[&str]) -> Self {
self.filters.push(Filter {
name: name.into(),
extensions: extensions.iter().map(|e| e.to_string()).collect(),
});
self
}
#[must_use]
pub fn set_directory<P: AsRef<Path>>(mut self, directory: P) -> Self {
self.starting_directory.replace(directory.as_ref().into());
self
}
#[must_use]
pub fn set_file_name(mut self, file_name: impl Into<String>) -> Self {
self.file_name.replace(file_name.into());
self
}
#[cfg(desktop)]
#[must_use]
pub fn set_parent<
W: raw_window_handle::HasWindowHandle + raw_window_handle::HasDisplayHandle,
>(
mut self,
parent: &W,
) -> Self {
if let (Ok(window_handle), Ok(display_handle)) =
(parent.window_handle(), parent.display_handle())
{
self.parent.replace(crate::desktop::WindowHandle::new(
window_handle.as_raw(),
display_handle.as_raw(),
));
}
self
}
#[must_use]
pub fn set_title(mut self, title: impl Into<String>) -> Self {
self.title.replace(title.into());
self
}
pub fn set_can_create_directories(mut self, can: bool) -> Self {
self.can_create_directories.replace(can);
self
}
pub fn set_picker_mode(mut self, mode: PickerMode) -> Self {
self.picker_mode.replace(mode);
self
}
pub fn set_file_access_mode(mut self, mode: FileAccessMode) -> Self {
self.file_access_mode.replace(mode);
self
}
pub fn pick_file<F: FnOnce(Option<FilePath>) + Send + 'static>(self, f: F) {
pick_file(self, f)
}
pub fn pick_files<F: FnOnce(Option<Vec<FilePath>>) + Send + 'static>(self, f: F) {
pick_files(self, f)
}
#[cfg(desktop)]
pub fn pick_folder<F: FnOnce(Option<FilePath>) + Send + 'static>(self, f: F) {
pick_folder(self, f)
}
#[cfg(desktop)]
pub fn pick_folders<F: FnOnce(Option<Vec<FilePath>>) + Send + 'static>(self, f: F) {
pick_folders(self, f)
}
pub fn save_file<F: FnOnce(Option<FilePath>) + Send + 'static>(self, f: F) {
save_file(self, f)
}
}
impl<R: Runtime> FileDialogBuilder<R> {
pub fn blocking_pick_file(self) -> Option<FilePath> {
blocking_fn!(self, pick_file)
}
pub fn blocking_pick_files(self) -> Option<Vec<FilePath>> {
blocking_fn!(self, pick_files)
}
#[cfg(desktop)]
pub fn blocking_pick_folder(self) -> Option<FilePath> {
blocking_fn!(self, pick_folder)
}
#[cfg(desktop)]
pub fn blocking_pick_folders(self) -> Option<Vec<FilePath>> {
blocking_fn!(self, pick_folders)
}
pub fn blocking_save_file(self) -> Option<FilePath> {
blocking_fn!(self, save_file)
}
}