From 4018fd1177fc9b31437e025674a84d8a8de89a20 Mon Sep 17 00:00:00 2001 From: Brian Carlsen Date: Thu, 3 Nov 2022 22:09:01 +0100 Subject: [PATCH 1/3] Added `dialog` module features. --- Cargo.toml | 5 +- README.md | 3 +- dist/dialog.js | 118 ++++++++++++++++++++++ src/dialog.rs | 261 +++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 + 5 files changed, 386 insertions(+), 3 deletions(-) create mode 100644 dist/dialog.js create mode 100644 src/dialog.rs diff --git a/Cargo.toml b/Cargo.toml index 8c2523a..bb44c0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,9 +20,10 @@ wasm-bindgen-test = "0.3.33" tauri-sys = { path = ".", features = ["all"] } [features] -all = ["app", "clipboard", "event", "mocks", "tauri"] +all = ["app", "clipboard", "dialog", "event", "mocks", "tauri"] app = ["dep:semver"] clipboard = [] +dialog = [] event = [] mocks = [] -tauri = ["dep:url"] \ No newline at end of file +tauri = ["dep:url"] diff --git a/README.md b/README.md index 08eca59..e525664 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ All modules are gated by accordingly named Cargo features. It is recommended you - **all**: Enables all modules. - **app**: Enables the `app` module. - **clipboard**: Enables the `clipboard` module. +- **dialog**: Enables the `dialog` module. - **event**: Enables the `event` module. - **mocks**: Enables the `mocks` module. - **tauri**: Enables the `tauri` module. @@ -54,7 +55,7 @@ These API bindings are not completely on-par with `@tauri-apps/api` yet, but her - [x] `app` - [ ] `cli` - [x] `clipboard` -- [ ] `dialog` +- [x] `dialog` - [x] `event` - [ ] `fs` - [ ] `global_shortcut` diff --git a/dist/dialog.js b/dist/dialog.js new file mode 100644 index 0000000..7f8f98a --- /dev/null +++ b/dist/dialog.js @@ -0,0 +1,118 @@ +// tauri/tooling/api/src/tauri.ts +function uid() { + return window.crypto.getRandomValues(new Uint32Array(1))[0]; +} +function transformCallback(callback, once3 = false) { + const identifier = uid(); + const prop = `_${identifier}`; + Object.defineProperty(window, prop, { + value: (result) => { + if (once3) { + Reflect.deleteProperty(window, prop); + } + return callback?.(result); + }, + writable: false, + configurable: true + }); + return identifier; +} +async function invoke(cmd, args = {}) { + return new Promise((resolve, reject) => { + const callback = transformCallback((e) => { + resolve(e); + Reflect.deleteProperty(window, `_${error}`); + }, true); + const error = transformCallback((e) => { + reject(e); + Reflect.deleteProperty(window, `_${callback}`); + }, true); + window.__TAURI_IPC__({ + cmd, + callback, + error, + ...args + }); + }); +} + +// tauri/tooling/api/src/helpers/tauri.ts +async function invokeTauriCommand(command) { + return invoke("tauri", command); +} + +// tauri/tooling/api/src/helpers/dialog.ts +async function ask(message, options) { + return await invokeTauriCommand({ + __tauriModule: "Dialog", + message: { + cmd: "askDialog", + message: message.toString(), + title: options?.title?.toString(), + type: options?.type + } + }); +} +async function confirm(message, options) { + return await invokeTauriCommand({ + __tauriModule: "Dialog", + message: { + cmd: "confirmDialog", + message: message.toString(), + title: options?.title?.toString(), + type: options?.type + } + }); +} +async function open(options) { + if(!options) { + options = {multiple: false}; + } + return await invokeTauriCommand({ + __tauriModule: "Dialog", + message: { + cmd: "openDialog", + options + } + }); +} +async function open_multiple(options) { + if(!options) { + options = {multiple: true}; + } + return await invokeTauriCommand({ + __tauriModule: "Dialog", + message: { + cmd: "openDialog", + options + } + }); +} +async function message(message, options) { + await invokeTauriCommand({ + __tauriModule: "Dialog", + message: { + cmd: "messageDialog", + message: message.toString(), + title: options?.title?.toString(), + type: options?.type + } + }); +} +async function save(options) { + return await invokeTauriCommand({ + __tauriModule: "Dialog", + message: { + cmd: "saveDialog", + options + } + }); +} +export { + ask, + confirm, + open, + open_multiple, + message, + save +} diff --git a/src/dialog.rs b/src/dialog.rs new file mode 100644 index 0000000..68b36ec --- /dev/null +++ b/src/dialog.rs @@ -0,0 +1,261 @@ +//! User interaction with the file system using dialog boxes. +//! +//! # Example +//! +//! ```rust,no_run +//! use tauri_api::dialog::open; +//! +//! let path = open.await; +//! ``` +use serde::Serialize; +use std::path::PathBuf; + +/// Extension filter for the file dialog. +/// +/// # Example +/// +/// ```rust,no_run +/// let filter = DialogFilter { +/// extension: vec![".jpg", ".jpeg", ".png", ".bmp"], +/// name: "images", +/// }; +/// ``` +#[derive(Serialize)] +pub struct DialogFilter { + /// Extensions to filter, without a `.` prefix. + pub extensions: Vec, + + /// Filter name + pub name: String, +} + +/// Types of a [`message`] dialog. +#[derive(Serialize)] +pub enum MessageDialogType { + Error, + Info, + Warning, +} + +/// Options for the [`message`] dialog. +#[derive(Serialize)] +pub struct MessageDialogOptions { + /// The title of the dialog. Defaults to the app name. + pub title: Option, + + /// The type of the dialog. Defaults to MessageDialogType::Info. + pub kind: MessageDialogType, +} + +impl MessageDialogOptions { + /// Creates a new `MessageDialogOptions` with sensible default values. + pub fn new() -> Self { + Self { + title: None, + kind: MessageDialogType::Info, + } + } +} + +/// Options for an [`open`] dialog. +#[derive(Serialize)] +pub struct OpenDialogOptions { + /// Initial directory or file path. + pub default_path: Option, + + /// Whether the dialog is a directory selection or not. + pub directory: bool, + + /// The filters of the dialog. + pub filters: Vec, + + /// Whether the dialgo allows multiple selection or not. + pub multiple: bool, + + /// If `directory` is `true`, indicatees that it will be read recursivley later. + /// Defines whether subdirectories will be allowed on the scope or not. + pub recursive: bool, + + /// The title of the dialog window. + pub title: Option, +} + +impl OpenDialogOptions { + /// Creates a new `OpenDialogOptions` with sensible default values. + pub fn new() -> Self { + Self { + default_path: None, + directory: false, + filters: Vec::new(), + multiple: false, + recursive: false, + title: None, + } + } +} + +/// Options for the save dialog. +#[derive(Serialize)] +pub struct SaveDialogOptions { + /// Initial directory of the file path. + /// If it's not a directory path, the dialog interface will change to that folder. + /// If it's not an existing directory, the file name will be set to the dialog's + /// file name input and the dialog will be set to the parent folder. + pub default_path: Option, + + /// The filters of the dialog. + pub filters: Vec, + + /// The title of the dialog window. + pub title: Option, +} + +impl SaveDialogOptions { + /// Creates a new `SaveDialogOptions` with sensible default values. + pub fn new() -> Self { + Self { + default_path: None, + filters: Vec::new(), + title: None, + } + } +} + +/// Show a question dialog with `Yes` and `No` buttons. +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_api::dialog::{ask, MessageDialogOptions}; +/// +/// let yes = ask("Are you sure?", None).await; +/// ``` +/// @param message Message to display. +/// @param options Dialog options. +/// @returns Whether the user selected `Yes` or `No`. +#[inline(always)] +pub async fn ask(message: &str, options: Option) -> Option { + inner::ask(message, serde_wasm_bindgen::to_value(&options).unwrap()) + .await + .as_bool() +} + +/// Shows a question dialog with `Ok` and `Cancel` buttons. +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_api::dialog::{confirm, MessageDialogOptions}; +/// +/// let confirmed = confirm("Are you sure?", None).await; +/// ``` +/// @returns Whether the user selelced `Ok` or `Cancel`. +pub async fn confirm(message: &str, options: Option) -> Option { + inner::confirm(message, serde_wasm_bindgen::to_value(&options).unwrap()) + .await + .as_bool() +} + +/// Shows a message dialog with an `Ok` button. +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_api::dialog::{message, MessageDialogOptions}; +/// +/// message("Tauri is awesome", None).await; +/// ``` +/// @param message Message to display. +/// @param options Dialog options. +/// @returns Promise resolved when user closes the dialog. +pub async fn message(message: &str, options: Option) { + inner::message(message, serde_wasm_bindgen::to_value(&options).unwrap()).await +} + +/// Opens a file/directory selection dialog for a single file. +/// `multiple` field of [`options`](OpenDialogOptions) must be `false`, if provided. +/// +/// The selected paths are added to the filesystem and asset protocol allowlist scopes. +/// When security is mroe important than the ease of use of this API, +/// prefer writing a dedicated command instead. +/// +/// Note that the allowlist scope change is not persisited, +/// so the values are cleared when the applicaiton is restarted. +/// You can save it to the filessytem using the [tauri-plugin-persisted-scope](https://github.com/tauri-apps/tauri-plugin-persisted-scope). +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_api::dialog::{open, OpenDialogOptions}; +/// +/// let file = open(None).await; +/// ``` +/// @param options Dialog options. +/// @returns List of file paths, or `None` if user cancelled the dialog. +pub async fn open(options: Option) -> Option { + let file = inner::open(serde_wasm_bindgen::to_value(&options).unwrap()).await; + serde_wasm_bindgen::from_value(file).unwrap() +} + +/// Opens a file/directory selection dialog for multiple files. +/// `multiple` field of [`options`](OpenDialogOptions) must be `true`, if provided. +/// +/// The selected paths are added to the filesystem and asset protocol allowlist scopes. +/// When security is mroe important than the ease of use of this API, +/// prefer writing a dedicated command instead. +/// +/// Note that the allowlist scope change is not persisited, +/// so the values are cleared when the applicaiton is restarted. +/// You can save it to the filessytem using the [tauri-plugin-persisted-scope](https://github.com/tauri-apps/tauri-plugin-persisted-scope). +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_api::dialog::{open, OpenDialogOptions}; +/// +/// let files = open_multiple(None).await; +/// ``` +/// @param options Dialog options. +/// @returns List of file paths, or `None` if user cancelled the dialog. +pub async fn open_multiple(options: Option) -> Option> { + let files = inner::open_multiple(serde_wasm_bindgen::to_value(&options).unwrap()).await; + serde_wasm_bindgen::from_value(files).unwrap() +} + +/// Opens a file/directory save dialog. +/// +/// The selected paths are added to the filesystem and asset protocol allowlist scopes. +/// When security is mroe important than the ease of use of this API, +/// prefer writing a dedicated command instead. +/// +/// Note that the allowlist scope change is not persisited, +/// so the values are cleared when the applicaiton is restarted. +/// You can save it to the filessytem using the [tauri-plugin-persisted-scope](https://github.com/tauri-apps/tauri-plugin-persisted-scope). +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_api::dialog::{save, SaveDialogOptions}; +/// +/// let file = save(None).await; +/// ``` +/// @param options Dialog options. +/// @returns File path, or `None` if user cancelled the dialog. +pub async fn save(options: Option) -> Option { + let path = inner::save(serde_wasm_bindgen::to_value(&options).unwrap()).await; + serde_wasm_bindgen::from_value(path).unwrap() +} + +mod inner { + use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; + + #[wasm_bindgen(module = "/dist/dialog.js")] + extern "C" { + pub async fn ask(message: &str, options: JsValue) -> JsValue; + pub async fn confirm(message: &str, options: JsValue) -> JsValue; + pub async fn open(options: JsValue) -> JsValue; + pub async fn open_multiple(options: JsValue) -> JsValue; + pub async fn message(message: &str, option: JsValue); + pub async fn save(options: JsValue) -> JsValue; + } +} diff --git a/src/lib.rs b/src/lib.rs index 20d4fbd..b8c0f53 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,8 @@ use wasm_bindgen::JsValue; pub mod app; #[cfg(feature = "clipboard")] pub mod clipboard; +#[cfg(feature = "dialog")] +pub mod dialog; #[cfg(feature = "event")] pub mod event; #[cfg(feature = "mocks")] From ca061b9c1a4cb2f53ac6d421ca107529ce0771b9 Mon Sep 17 00:00:00 2001 From: Brian Carlsen Date: Sun, 13 Nov 2022 17:05:42 +0100 Subject: [PATCH 2/3] Added additional examples for `open` and `open_multiple` in documentation. --- src/dialog.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/dialog.rs b/src/dialog.rs index 68b36ec..7f133f0 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -189,6 +189,10 @@ pub async fn message(message: &str, options: Option) { /// use tauri_api::dialog::{open, OpenDialogOptions}; /// /// let file = open(None).await; +/// +/// let mut opts = OpenDialogOptions::new(); +/// opts.directory = true; +/// let dir = open(Some(opts)).await; /// ``` /// @param options Dialog options. /// @returns List of file paths, or `None` if user cancelled the dialog. @@ -214,6 +218,11 @@ pub async fn open(options: Option) -> Option { /// use tauri_api::dialog::{open, OpenDialogOptions}; /// /// let files = open_multiple(None).await; +/// +/// let mut opts = OpenDialogOptions::new(); +/// opts.multiple = true; +/// opts.directory = true; +/// let dirs = open(Some(opts)).await; /// ``` /// @param options Dialog options. /// @returns List of file paths, or `None` if user cancelled the dialog. From 104704cb8868580ff1ecbf3f9910c1b68fa1ef77 Mon Sep 17 00:00:00 2001 From: Brian Carlsen Date: Sun, 13 Nov 2022 17:27:33 +0100 Subject: [PATCH 3/3] Fixed serialization issue for snake_case to CamelCase. --- src/dialog.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/dialog.rs b/src/dialog.rs index 7f133f0..2127741 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -5,7 +5,7 @@ //! ```rust,no_run //! use tauri_api::dialog::open; //! -//! let path = open.await; +//! let path = open(None).await; //! ``` use serde::Serialize; use std::path::PathBuf; @@ -61,6 +61,7 @@ impl MessageDialogOptions { #[derive(Serialize)] pub struct OpenDialogOptions { /// Initial directory or file path. + #[serde(rename(serialize = "defaultPath"))] pub default_path: Option, /// Whether the dialog is a directory selection or not. @@ -101,6 +102,7 @@ pub struct SaveDialogOptions { /// If it's not a directory path, the dialog interface will change to that folder. /// If it's not an existing directory, the file name will be set to the dialog's /// file name input and the dialog will be set to the parent folder. + #[serde(rename(serialize = "defaultPath"))] pub default_path: Option, /// The filters of the dialog.