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
This commit is contained in:
Lucas Fernandes Nogueira 2020-03-15 18:09:44 -03:00 committed by GitHub
parent b4a08e88fd
commit 37e8e79a04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 334 additions and 1 deletions

View File

@ -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
}

View File

@ -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) { loadAsset: function loadAsset(assetName, assetType) {
return this.promisified({ return this.promisified({
cmd: 'loadAsset', cmd: 'loadAsset',

View File

@ -22,6 +22,7 @@ tar = "0.4"
flate2 = "1" flate2 = "1"
error-chain = "0.12" error-chain = "0.12"
rand = "0.7" rand = "0.7"
nfd = "0.0.4"
tauri-utils = {version = "0.4", path = "../tauri-utils"} tauri-utils = {version = "0.4", path = "../tauri-utils"}
[dev-dependencies] [dev-dependencies]

33
tauri-api/src/dialog.rs Normal file
View File

@ -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)
}

View File

@ -9,6 +9,7 @@ pub mod file;
pub mod rpc; pub mod rpc;
pub mod version; pub mod version;
pub mod tcp; pub mod tcp;
pub mod dialog;
pub use tauri_utils::*; pub use tauri_utils::*;

View File

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

View File

@ -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)
})

View File

@ -22,6 +22,22 @@
<button id="set-title">Set title</button> <button id="set-title">Set title</button>
</div> </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> <div id="response"></div>
<script> <script>
@ -43,5 +59,6 @@
<script src="communication.js"></script> <script src="communication.js"></script>
<script src="fs.js"></script> <script src="fs.js"></script>
<script src="window.js"></script> <script src="window.js"></script>
<script src="dialog.js"></script>
</body> </body>
</html> </html>

View File

@ -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<String|String[]>} 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<String>} 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) { loadAsset: function loadAsset(assetName, assetType) {
return this.promisified({ return this.promisified({
cmd: 'loadAsset', cmd: 'loadAsset',
@ -393,4 +443,4 @@ if (document.readyState === 'complete' || document.readyState === 'interactive')
__openLinks() __openLinks()
}, true) }, true)
} }
</script> <div> <button id="log">Call Log API</button> <button id="request">Call Request (async) API</button> <button id="event">Send event to Rust</button> </div> <div style="margin-top:24px"> <input id="path-to-read" placeholder="Type the path to read..."> <button id="read">Read</button> </div> <div style="margin-top:24px"> <input id="url" value="https://tauri.studio"> <button id="open-url">Open URL</button> </div> <div style="margin-top:24px"> <input id="title" value="Awesome Tauri Example!"> <button id="set-title">Set title</button> </div> <div id="response"></div> <script>function registerResponse(e){document.getElementById("response").innerHTML="object"==typeof e?JSON.stringify(e):e}function addClickEnterHandler(e,n,t){e.addEventListener("click",t),n.addEventListener("keyup",function(e){13===e.keyCode&&t()})}</script> <script>window.onTauriInit=function(){window.tauri.listen("rust-event",function(e){document.getElementById("response").innerHTML=JSON.stringify(e)})},document.getElementById("log").addEventListener("click",function(){window.tauri.invoke({cmd:"logOperation",event:"tauri-click",payload:"this payload is optional because we used Option in Rust"})}),document.getElementById("request").addEventListener("click",function(){window.tauri.promisified({cmd:"performRequest",endpoint:"dummy endpoint arg",body:{id:5,name:"test"}}).then(registerResponse).catch(registerResponse)}),document.getElementById("event").addEventListener("click",function(){window.tauri.emit("js-event","this is the payload string")});</script> <script>function arrayBufferToBase64(e,n){var t=new Blob([e],{type:"application/octet-binary"}),a=new FileReader;a.onload=function(e){var t=e.target.result;n(t.substr(t.indexOf(",")+1))},a.readAsDataURL(t)}var pathInput=document.getElementById("path-to-read");addClickEnterHandler(document.getElementById("read"),pathInput,function(){var a=pathInput.value,r=a.match(/\S+\.\S+$/g);(r?window.tauri.readBinaryFile(a):window.tauri.readDir(a)).then(function(e){if(r)if(a.includes(".png")||a.includes(".jpg"))arrayBufferToBase64(new Uint8Array(e),function(e){registerResponse('<img src="'+("data:image/png;base64,"+e)+'"></img>')});else{var t=String.fromCharCode.apply(null,e);registerResponse('<textarea id="file-response" style="height: 400px"></textarea><button id="file-save">Save</button>');var n=document.getElementById("file-response");n.value=t,document.getElementById("file-save").addEventListener("click",function(){window.tauri.writeFile({file:a,contents:n.value}).catch(registerResponse)})}else registerResponse(e)}).catch(registerResponse)});</script> <script>var urlInput=document.getElementById("url");addClickEnterHandler(document.getElementById("open-url"),urlInput,function(){window.tauri.open(urlInput.value)});var titleInput=document.getElementById("title");addClickEnterHandler(document.getElementById("set-title"),titleInput,function(){window.tauri.setTitle(titleInput.value)});</script> </body></html> </script> <div> <button id="log">Call Log API</button> <button id="request">Call Request (async) API</button> <button id="event">Send event to Rust</button> </div> <div style="margin-top:24px"> <input id="path-to-read" placeholder="Type the path to read..."> <button id="read">Read</button> </div> <div style="margin-top:24px"> <input id="url" value="https://tauri.studio"> <button id="open-url">Open URL</button> </div> <div style="margin-top:24px"> <input id="title" value="Awesome Tauri Example!"> <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>function registerResponse(e){document.getElementById("response").innerHTML="object"==typeof e?JSON.stringify(e):e}function addClickEnterHandler(e,n,t){e.addEventListener("click",t),n.addEventListener("keyup",function(e){13===e.keyCode&&t()})}</script> <script>window.onTauriInit=function(){window.tauri.listen("rust-event",function(e){document.getElementById("response").innerHTML=JSON.stringify(e)})},document.getElementById("log").addEventListener("click",function(){window.tauri.invoke({cmd:"logOperation",event:"tauri-click",payload:"this payload is optional because we used Option in Rust"})}),document.getElementById("request").addEventListener("click",function(){window.tauri.promisified({cmd:"performRequest",endpoint:"dummy endpoint arg",body:{id:5,name:"test"}}).then(registerResponse).catch(registerResponse)}),document.getElementById("event").addEventListener("click",function(){window.tauri.emit("js-event","this is the payload string")});</script> <script>function arrayBufferToBase64(e,n){var t=new Blob([e],{type:"application/octet-binary"}),a=new FileReader;a.onload=function(e){var t=e.target.result;n(t.substr(t.indexOf(",")+1))},a.readAsDataURL(t)}var pathInput=document.getElementById("path-to-read");addClickEnterHandler(document.getElementById("read"),pathInput,function(){var a=pathInput.value,r=a.match(/\S+\.\S+$/g);(r?window.tauri.readBinaryFile(a):window.tauri.readDir(a)).then(function(e){if(r)if(a.includes(".png")||a.includes(".jpg"))arrayBufferToBase64(new Uint8Array(e),function(e){registerResponse('<img src="'+("data:image/png;base64,"+e)+'"></img>')});else{var t=String.fromCharCode.apply(null,e);registerResponse('<textarea id="file-response" style="height: 400px"></textarea><button id="file-save">Save</button>');var n=document.getElementById("file-response");n.value=t,document.getElementById("file-save").addEventListener("click",function(){window.tauri.writeFile({file:a,contents:n.value}).catch(registerResponse)})}else registerResponse(e)}).catch(registerResponse)});</script> <script>var urlInput=document.getElementById("url");addClickEnterHandler(document.getElementById("open-url"),urlInput,function(){window.tauri.open(urlInput.value)});var titleInput=document.getElementById("title");addClickEnterHandler(document.getElementById("set-title"),titleInput,function(){window.tauri.setTitle(titleInput.value)});</script> <script>var defaultPathInput=document.getElementById("dialog-default-path"),filterInput=document.getElementById("dialog-filter"),multipleInput=document.getElementById("dialog-multiple"),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)});</script> </body></html>

View File

@ -2,6 +2,7 @@ mod cmd;
mod salt; mod salt;
#[allow(dead_code)] #[allow(dead_code)]
mod file_system; mod file_system;
mod dialog;
#[cfg(not(any(feature = "dev-server", feature = "embedded-server")))] #[cfg(not(any(feature = "dev-server", feature = "embedded-server")))]
use std::path::PathBuf; use std::path::PathBuf;
@ -94,6 +95,22 @@ pub(crate) fn handle<T: 'static>(webview: &mut WebView<'_, T>, arg: &str) -> cra
Emit { event, payload } => { Emit { event, payload } => {
crate::event::on_event(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")))] #[cfg(not(any(feature = "dev-server", feature = "embedded-server")))]
LoadAsset { LoadAsset {
asset, asset,

View File

@ -6,6 +6,22 @@ pub struct ReadDirOptions {
pub recursive: bool 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)] #[derive(Deserialize)]
#[serde(tag = "cmd", rename_all = "camelCase")] #[serde(tag = "cmd", rename_all = "camelCase")]
pub enum Cmd { pub enum Cmd {
@ -67,6 +83,18 @@ pub enum Cmd {
event: String, event: String,
payload: Option<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")))] #[cfg(not(any(feature = "dev-server", feature = "embedded-server")))]
LoadAsset { LoadAsset {
asset: String, asset: String,

View File

@ -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,
);
}

View File

@ -42,6 +42,10 @@ error_chain! {
description("Command Error") description("Command Error")
display("Command Error: '{}'", t) 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>( pub fn execute_promise<T: 'static, F: FnOnce() -> crate::Result<String> + Send + 'static>(
webview: &mut WebView<'_, T>, webview: &mut WebView<'_, T>,
task: F, task: F,