Эх сурвалжийг харах

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
Lucas Fernandes Nogueira 5 жил өмнө
parent
commit
37e8e79a04

+ 32 - 0
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<String|String[]>} 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<String>} promise resolving to the select path
+ */
+function save (options = {}) {
+  return tauri.saveDialog(options)
+}
+
+export {
+  open,
+  save
+}

+ 60 - 0
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<String|String[]>} 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<String>} 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',

+ 1 - 0
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]

+ 33 - 0
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<String>, default_path: Option<String>) -> crate::Result<Response> {
+  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<String>, default_path: Option<String>) -> crate::Result<Response> {
+    open_dialog_internal(DialogType::SingleFile, filter_list, default_path)
+}
+
+/// Open mulitple select file dialog
+pub fn select_multiple(filter_list: Option<String>, default_path: Option<String>) -> crate::Result<Response> {
+    open_dialog_internal(DialogType::MultipleFiles, filter_list, default_path)
+}
+
+/// Open save dialog
+pub fn save_file(filter_list: Option<String>, default_path: Option<String>) -> crate::Result<Response> {
+    open_dialog_internal(DialogType::SaveFile, filter_list, default_path)
+}
+
+/// Open pick folder dialog
+pub fn pick_folder(default_path: Option<String>) -> crate::Result<Response> {
+    open_dialog_internal(DialogType::PickFolder, None, default_path)
+}

+ 1 - 0
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::*;
 

+ 2 - 0
tauri/Cargo.toml

@@ -49,6 +49,8 @@ execute = []
 open = []
 event = []
 updater = []
+open-dialog = []
+save-dialog = []
 
 [package.metadata.docs.rs]
 features = ["dev-server", "all-api"]

+ 20 - 0
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)
+})

+ 17 - 0
tauri/examples/communication/dist/index.html

@@ -22,6 +22,22 @@
       <button id="set-title">Set title</button>
     </div>
 
+    <div style="margin-top: 24px">
+      <input id="dialog-default-path" placeholder="Default path">
+      <input id="dialog-filter" placeholder="Extensions filter">
+      <div>
+        <input type="checkbox" id="dialog-multiple">
+        <label>Multiple</label>
+      </div>
+      <div>
+        <input type="checkbox" id="dialog-directory">
+        <label>Directory</label>
+      </div>
+
+      <button id="open-dialog">Open dialog</button>
+      <button id="save-dialog">Open save dialog</button>
+    </div>
+
     <div id="response"></div>
 
     <script>
@@ -43,5 +59,6 @@
     <script src="communication.js"></script>
     <script src="fs.js"></script>
     <script src="window.js"></script>
+    <script src="dialog.js"></script>
   </body>
 </html>

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 50 - 0
tauri/examples/communication/dist/index.tauri.html


+ 17 - 0
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<T: 'static>(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,

+ 28 - 0
tauri/src/endpoints/cmd.rs

@@ -6,6 +6,22 @@ pub struct ReadDirOptions {
   pub recursive: bool
 }
 
+#[derive(Deserialize)]
+pub struct OpenDialogOptions {
+  pub filter: Option<String>,
+  #[serde(default)]
+  pub multiple: bool,
+  #[serde(default)]
+  pub directory: bool,
+  pub default_path: Option<String>,
+}
+
+#[derive(Deserialize)]
+pub struct SaveDialogOptions {
+  pub filter: Option<String>,
+  pub default_path: Option<String>,
+}
+
 #[derive(Deserialize)]
 #[serde(tag = "cmd", rename_all = "camelCase")]
 pub enum Cmd {
@@ -67,6 +83,18 @@ pub enum Cmd {
     event: String,
     payload: Option<String>,
   },
+  #[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,

+ 54 - 0
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<T: 'static>(
+  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<T: 'static>(
+  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,
+  );
+}

+ 18 - 0
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<F: FnOnce() -> () + Send + 'static>(task: F) {
   });
 }
 
+pub fn execute_promise_sync<T: 'static, F: FnOnce() -> crate::Result<String> + 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<T: 'static, F: FnOnce() -> crate::Result<String> + Send + 'static>(
   webview: &mut WebView<'_, T>,
   task: F,

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно