mirror of
https://github.com/tauri-apps/tauri.git
synced 2024-11-29 13:03:09 +03:00
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:
parent
b4a08e88fd
commit
37e8e79a04
32
cli/tauri.js/api/dialog.js
Normal file
32
cli/tauri.js/api/dialog.js
Normal 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
|
||||
}
|
@ -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',
|
||||
|
@ -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
tauri-api/src/dialog.rs
Normal file
33
tauri-api/src/dialog.rs
Normal 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)
|
||||
}
|
@ -9,6 +9,7 @@ pub mod file;
|
||||
pub mod rpc;
|
||||
pub mod version;
|
||||
pub mod tcp;
|
||||
pub mod dialog;
|
||||
|
||||
pub use tauri_utils::*;
|
||||
|
||||
|
@ -49,6 +49,8 @@ execute = []
|
||||
open = []
|
||||
event = []
|
||||
updater = []
|
||||
open-dialog = []
|
||||
save-dialog = []
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["dev-server", "all-api"]
|
||||
|
20
tauri/examples/communication/dist/dialog.js
vendored
Normal file
20
tauri/examples/communication/dist/dialog.js
vendored
Normal 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)
|
||||
})
|
17
tauri/examples/communication/dist/index.html
vendored
17
tauri/examples/communication/dist/index.html
vendored
@ -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>
|
||||
|
@ -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) {
|
||||
return this.promisified({
|
||||
cmd: 'loadAsset',
|
||||
@ -393,4 +443,4 @@ if (document.readyState === 'complete' || document.readyState === 'interactive')
|
||||
__openLinks()
|
||||
}, 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>
|
@ -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,
|
||||
|
@ -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
tauri/src/endpoints/dialog.rs
Normal file
54
tauri/src/endpoints/dialog.rs
Normal 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,
|
||||
);
|
||||
}
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user