From 37e8e79a04b90681696d5a5936d29a328a9634a2 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Sun, 15 Mar 2020 18:09:44 -0300 Subject: [PATCH] feat(tauri) add dialog API (#514) * feat(tauri) add dialog API * feat(example) add dialog API to the communication example * fix(dialog) transform backslash so it works on windows --- cli/tauri.js/api/dialog.js | 32 ++++++++++ cli/tauri.js/templates/tauri.js | 60 +++++++++++++++++++ tauri-api/Cargo.toml | 1 + tauri-api/src/dialog.rs | 33 ++++++++++ tauri-api/src/lib.rs | 1 + tauri/Cargo.toml | 2 + tauri/examples/communication/dist/dialog.js | 20 +++++++ tauri/examples/communication/dist/index.html | 17 ++++++ .../communication/dist/index.tauri.html | 52 +++++++++++++++- tauri/src/endpoints.rs | 17 ++++++ tauri/src/endpoints/cmd.rs | 28 +++++++++ tauri/src/endpoints/dialog.rs | 54 +++++++++++++++++ tauri/src/lib.rs | 18 ++++++ 13 files changed, 334 insertions(+), 1 deletion(-) create mode 100644 cli/tauri.js/api/dialog.js create mode 100644 tauri-api/src/dialog.rs create mode 100644 tauri/examples/communication/dist/dialog.js create mode 100644 tauri/src/endpoints/dialog.rs diff --git a/cli/tauri.js/api/dialog.js b/cli/tauri.js/api/dialog.js new file mode 100644 index 000000000..5dbfc2b67 --- /dev/null +++ b/cli/tauri.js/api/dialog.js @@ -0,0 +1,32 @@ +import tauri from './tauri' + +/** + * @name openDialog + * @description Open a file/directory selection dialog + * @param {String} [options] + * @param {String} [options.filter] + * @param {String} [options.defaultPath] + * @param {Boolean} [options.multiple=false] + * @param {Boolean} [options.directory=false] + * @returns {Promise} promise resolving to the select path(s) + */ +function open (options = {}) { + return tauri.openDialog(options) +} + +/** + * @name save + * @description Open a file/directory save dialog + * @param {String} [options] + * @param {String} [options.filter] + * @param {String} [options.defaultPath] + * @returns {Promise} promise resolving to the select path + */ +function save (options = {}) { + return tauri.saveDialog(options) +} + +export { + open, + save +} diff --git a/cli/tauri.js/templates/tauri.js b/cli/tauri.js/templates/tauri.js index 041be01d3..26ad81018 100644 --- a/cli/tauri.js/templates/tauri.js +++ b/cli/tauri.js/templates/tauri.js @@ -378,6 +378,66 @@ window.tauri = { <% } %> }, + <% if (ctx.dev) { %> + /** + * @name openDialog + * @description Open a file/directory selection dialog + * @param {String} [options] + * @param {String} [options.filter] + * @param {String} [options.defaultPath] + * @param {Boolean} [options.multiple=false] + * @param {Boolean} [options.directory=false] + * @returns {Promise} promise resolving to the select path(s) + */ + <% } %> + openDialog: function openDialog(options) { + <% if (tauri.whitelist.openDialog === true || tauri.whitelist.all === true) { %> + var opts = options || {} + if (_typeof(options) === 'object') { + opts.default_path = opts.defaultPath + Object.freeze(options); + } + return this.promisified({ + cmd: 'openDialog', + options: opts + }); + <% } else { %> + <% if (ctx.dev) { %> + return __whitelistWarning('openDialog') + <% } %> + return __reject() + <% } %> + }, + + <% if (ctx.dev) { %> + /** + * @name saveDialog + * @description Open a file/directory save dialog + * @param {String} [options] + * @param {String} [options.filter] + * @param {String} [options.defaultPath] + * @returns {Promise} promise resolving to the select path + */ + <% } %> + saveDialog: function saveDialog(options) { + <% if (tauri.whitelist.saveDialog === true || tauri.whitelist.all === true) { %> + var opts = options || {} + if (_typeof(options) === 'object') { + opts.default_path = opts.defaultPath + Object.freeze(options); + } + return this.promisified({ + cmd: 'saveDialog', + options: opts + }); + <% } else { %> + <% if (ctx.dev) { %> + return __whitelistWarning('saveDialog') + <% } %> + return __reject() + <% } %> + }, + loadAsset: function loadAsset(assetName, assetType) { return this.promisified({ cmd: 'loadAsset', diff --git a/tauri-api/Cargo.toml b/tauri-api/Cargo.toml index b0719000f..e5eea80ae 100644 --- a/tauri-api/Cargo.toml +++ b/tauri-api/Cargo.toml @@ -22,6 +22,7 @@ tar = "0.4" flate2 = "1" error-chain = "0.12" rand = "0.7" +nfd = "0.0.4" tauri-utils = {version = "0.4", path = "../tauri-utils"} [dev-dependencies] diff --git a/tauri-api/src/dialog.rs b/tauri-api/src/dialog.rs new file mode 100644 index 000000000..a908340c0 --- /dev/null +++ b/tauri-api/src/dialog.rs @@ -0,0 +1,33 @@ +use nfd::{DialogType, open_dialog}; +pub use nfd::Response; + +fn open_dialog_internal(dialog_type: DialogType, filter: Option, default_path: Option) -> crate::Result { + open_dialog(filter.as_deref(), default_path.as_deref(), dialog_type) + .map_err(|err| crate::Error::with_chain(err, "open dialog failed")) + .and_then(|response| { + match response { + Response::Cancel => Err(crate::Error::from("user cancelled")), + _ => Ok(response) + } + }) +} + +/// Open single select file dialog +pub fn select(filter_list: Option, default_path: Option) -> crate::Result { + open_dialog_internal(DialogType::SingleFile, filter_list, default_path) +} + +/// Open mulitple select file dialog +pub fn select_multiple(filter_list: Option, default_path: Option) -> crate::Result { + open_dialog_internal(DialogType::MultipleFiles, filter_list, default_path) +} + +/// Open save dialog +pub fn save_file(filter_list: Option, default_path: Option) -> crate::Result { + open_dialog_internal(DialogType::SaveFile, filter_list, default_path) +} + +/// Open pick folder dialog +pub fn pick_folder(default_path: Option) -> crate::Result { + open_dialog_internal(DialogType::PickFolder, None, default_path) +} diff --git a/tauri-api/src/lib.rs b/tauri-api/src/lib.rs index 927831208..14fd10061 100644 --- a/tauri-api/src/lib.rs +++ b/tauri-api/src/lib.rs @@ -9,6 +9,7 @@ pub mod file; pub mod rpc; pub mod version; pub mod tcp; +pub mod dialog; pub use tauri_utils::*; diff --git a/tauri/Cargo.toml b/tauri/Cargo.toml index 47f1bbc3a..f176fffca 100644 --- a/tauri/Cargo.toml +++ b/tauri/Cargo.toml @@ -49,6 +49,8 @@ execute = [] open = [] event = [] updater = [] +open-dialog = [] +save-dialog = [] [package.metadata.docs.rs] features = ["dev-server", "all-api"] diff --git a/tauri/examples/communication/dist/dialog.js b/tauri/examples/communication/dist/dialog.js new file mode 100644 index 000000000..450150b94 --- /dev/null +++ b/tauri/examples/communication/dist/dialog.js @@ -0,0 +1,20 @@ +var defaultPathInput = document.getElementById('dialog-default-path') +var filterInput = document.getElementById('dialog-filter') +var multipleInput = document.getElementById('dialog-multiple') +var directoryInput = document.getElementById('dialog-directory') + +document.getElementById('open-dialog').addEventListener('click', function () { + window.tauri.openDialog({ + defaultPath: defaultPathInput.value || null, + filter: filterInput.value || null, + multiple: multipleInput.checked, + directory: directoryInput.checked + }).then(registerResponse).catch(registerResponse) +}) + +document.getElementById('save-dialog').addEventListener('click', function () { + window.tauri.saveDialog({ + defaultPath: defaultPathInput.value || null, + filter: filterInput.value || null + }).then(registerResponse).catch(registerResponse) +}) diff --git a/tauri/examples/communication/dist/index.html b/tauri/examples/communication/dist/index.html index 22b30d063..028330308 100644 --- a/tauri/examples/communication/dist/index.html +++ b/tauri/examples/communication/dist/index.html @@ -22,6 +22,22 @@ +
+ + +
+ + +
+
+ + +
+ + + +
+
+ diff --git a/tauri/examples/communication/dist/index.tauri.html b/tauri/examples/communication/dist/index.tauri.html index d26a09931..b5f31a82d 100644 --- a/tauri/examples/communication/dist/index.tauri.html +++ b/tauri/examples/communication/dist/index.tauri.html @@ -333,6 +333,56 @@ window.tauri = { }, + + /** + * @name openDialog + * @description Open a file/directory selection dialog + * @param {String} [options] + * @param {String} [options.filter] + * @param {String} [options.defaultPath] + * @param {Boolean} [options.multiple=false] + * @param {Boolean} [options.directory=false] + * @returns {Promise} promise resolving to the select path(s) + */ + + openDialog: function openDialog(options) { + + var opts = options || {} + if (_typeof(options) === 'object') { + opts.default_path = opts.defaultPath + Object.freeze(options); + } + return this.promisified({ + cmd: 'openDialog', + options: opts + }); + + }, + + + /** + * @name saveDialog + * @description Open a file/directory save dialog + * @param {String} [options] + * @param {String} [options.filter] + * @param {String} [options.defaultPath] + * @returns {Promise} promise resolving to the select path + */ + + saveDialog: function saveDialog(options) { + + var opts = options || {} + if (_typeof(options) === 'object') { + opts.default_path = opts.defaultPath + Object.freeze(options); + } + return this.promisified({ + cmd: 'saveDialog', + options: opts + }); + + }, + loadAsset: function loadAsset(assetName, assetType) { return this.promisified({ cmd: 'loadAsset', @@ -393,4 +443,4 @@ if (document.readyState === 'complete' || document.readyState === 'interactive') __openLinks() }, true) } -
\ No newline at end of file +
\ No newline at end of file diff --git a/tauri/src/endpoints.rs b/tauri/src/endpoints.rs index a795dc8d6..595a81327 100644 --- a/tauri/src/endpoints.rs +++ b/tauri/src/endpoints.rs @@ -2,6 +2,7 @@ mod cmd; mod salt; #[allow(dead_code)] mod file_system; +mod dialog; #[cfg(not(any(feature = "dev-server", feature = "embedded-server")))] use std::path::PathBuf; @@ -94,6 +95,22 @@ pub(crate) fn handle(webview: &mut WebView<'_, T>, arg: &str) -> cra Emit { event, payload } => { crate::event::on_event(event, payload); } + #[cfg(any(feature = "all-api", feature = "open-dialog"))] + OpenDialog { + options, + callback, + error + } => { + dialog::open(webview, options, callback, error); + } + #[cfg(any(feature = "all-api", feature = "save-dialog"))] + SaveDialog { + options, + callback, + error, + } => { + dialog::save(webview, options, callback, error); + } #[cfg(not(any(feature = "dev-server", feature = "embedded-server")))] LoadAsset { asset, diff --git a/tauri/src/endpoints/cmd.rs b/tauri/src/endpoints/cmd.rs index c2a65c264..3d564c061 100644 --- a/tauri/src/endpoints/cmd.rs +++ b/tauri/src/endpoints/cmd.rs @@ -6,6 +6,22 @@ pub struct ReadDirOptions { pub recursive: bool } +#[derive(Deserialize)] +pub struct OpenDialogOptions { + pub filter: Option, + #[serde(default)] + pub multiple: bool, + #[serde(default)] + pub directory: bool, + pub default_path: Option, +} + +#[derive(Deserialize)] +pub struct SaveDialogOptions { + pub filter: Option, + pub default_path: Option, +} + #[derive(Deserialize)] #[serde(tag = "cmd", rename_all = "camelCase")] pub enum Cmd { @@ -67,6 +83,18 @@ pub enum Cmd { event: String, payload: Option, }, + #[cfg(any(feature = "all-api", feature = "open-dialog"))] + OpenDialog { + options: OpenDialogOptions, + callback: String, + error: String, + }, + #[cfg(any(feature = "all-api", feature = "save-dialog"))] + SaveDialog { + options: SaveDialogOptions, + callback: String, + error: String, + }, #[cfg(not(any(feature = "dev-server", feature = "embedded-server")))] LoadAsset { asset: String, diff --git a/tauri/src/endpoints/dialog.rs b/tauri/src/endpoints/dialog.rs new file mode 100644 index 000000000..0195384ee --- /dev/null +++ b/tauri/src/endpoints/dialog.rs @@ -0,0 +1,54 @@ +use crate::api::dialog::{select, select_multiple, save_file, pick_folder, Response}; +use super::cmd::{OpenDialogOptions, SaveDialogOptions}; +use web_view::WebView; + +fn map_response(response: Response) -> String { + match response { + Response::Okay(path) => format!(r#""{}""#, path).replace("\\", "\\\\"), + Response::OkayMultiple(paths) => format!("{:?}", paths), + Response::Cancel => panic!("unexpected response type") + } +} + +pub fn open( + webview: &mut WebView<'_, T>, + options: OpenDialogOptions, + callback: String, + error: String, +) { + crate::execute_promise_sync( + webview, + move || { + let response = if options.multiple { + select_multiple(options.filter, options.default_path) + } else if options.directory { + pick_folder(options.default_path) + } else { + select(options.filter, options.default_path) + }; + response + .map(|r| map_response(r)) + .map_err(|e| crate::ErrorKind::Dialog(e.to_string()).into()) + }, + callback, + error, + ); +} + +pub fn save( + webview: &mut WebView<'_, T>, + options: SaveDialogOptions, + callback: String, + error: String, +) { + crate::execute_promise_sync( + webview, + move || { + save_file(options.filter, options.default_path) + .map(|r| map_response(r)) + .map_err(|e| crate::ErrorKind::Dialog(e.to_string()).into()) + }, + callback, + error, + ); +} \ No newline at end of file diff --git a/tauri/src/lib.rs b/tauri/src/lib.rs index 0fe1795af..3c344a39b 100644 --- a/tauri/src/lib.rs +++ b/tauri/src/lib.rs @@ -42,6 +42,10 @@ error_chain! { description("Command Error") display("Command Error: '{}'", t) } + Dialog(t: String) { + description("Dialog Error") + display("Dialog Error: '{}'", t) + } } } @@ -55,6 +59,20 @@ pub fn spawn () + Send + 'static>(task: F) { }); } +pub fn execute_promise_sync crate::Result + Send + 'static>( + webview: &mut WebView<'_, T>, + task: F, + callback: String, + error: String, +) { + let handle = webview.handle(); + let callback_string = + api::rpc::format_callback_result(task().map_err(|err| err.to_string()), callback, error); + handle + .dispatch(move |_webview| _webview.eval(callback_string.as_str())) + .expect("Failed to dispatch promise callback"); +} + pub fn execute_promise crate::Result + Send + 'static>( webview: &mut WebView<'_, T>, task: F,