refactor(core): use nfd for file dialogs, closes #1251 (#1257)

This commit is contained in:
Lucas Fernandes Nogueira 2021-02-18 11:43:41 -03:00 committed by GitHub
parent e7bd8c5920
commit 2326bcd399
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 235 additions and 176 deletions

View File

@ -0,0 +1,6 @@
---
"tauri-api": minor
"api": minor
---
The file dialog API now uses [rfd](https://github.com/PolyMeilex/rfd). The filter option is now an array of `{ name: string, extensions: string[] }`.

View File

@ -1,16 +1,21 @@
import { invoke } from './tauri'
export interface DialogFilter {
name: string
extensions: string[]
}
export interface OpenDialogOptions {
filter?: string
filters?: DialogFilter[]
defaultPath?: string
multiple?: boolean
directory?: boolean
}
export type SaveDialogOptions = Pick<
OpenDialogOptions,
'filter' | 'defaultPath'
>
export interface SaveDialogOptions {
filters?: DialogFilter[]
defaultPath?: string
}
/**
* @name openDialog

View File

@ -27,7 +27,7 @@ tar = "0.4"
flate2 = "1.0"
thiserror = "1.0.23"
rand = "0.8"
nfd = "0.0.4"
rfd = "0.2.1"
tinyfiledialogs = "3.3"
reqwest = { version = "0.11", features = [ "json", "multipart" ] }
bytes = { version = "1", features = ["serde"] }

View File

@ -1,10 +1,52 @@
use std::path::Path;
pub use nfd::Response;
use nfd::{open_dialog, DialogType};
use std::path::{Path, PathBuf};
use rfd::FileDialog;
use tinyfiledialogs::{message_box_ok, message_box_yes_no, MessageBoxIcon, YesNo};
/// The file dialog builder.
/// Constructs file picker dialogs that can select single/multiple files or directories.
#[derive(Default)]
pub struct FileDialogBuilder(FileDialog);
impl FileDialogBuilder {
/// Gets the default file dialog builder.
pub fn new() -> Self {
Default::default()
}
/// Add file extension filter. Takes in the name of the filter, and list of extensions
pub fn add_filter(mut self, name: impl AsRef<str>, extensions: &[&str]) -> Self {
self.0 = self.0.add_filter(name.as_ref(), extensions);
self
}
/// Set starting directory of the dialog.
pub fn set_directory<P: AsRef<Path>>(mut self, directory: P) -> Self {
self.0 = self.0.set_directory(&directory);
self
}
/// Pick one file.
pub fn pick_file(self) -> Option<PathBuf> {
self.0.pick_file()
}
/// Pick multiple files.
pub fn pick_files(self) -> Option<Vec<PathBuf>> {
self.0.pick_files()
}
/// Pick one folder.
pub fn pick_folder(self) -> Option<PathBuf> {
self.0.pick_folder()
}
/// Opens save file dialog.
pub fn save_file(self) -> Option<PathBuf> {
self.0.save_file()
}
}
/// Response for the ask dialog
pub enum AskResponse {
/// User confirmed.
@ -30,52 +72,3 @@ pub fn ask(title: impl AsRef<str>, message: impl AsRef<str>) -> AskResponse {
pub fn message(title: impl AsRef<str>, message: impl AsRef<str>) {
message_box_ok(title.as_ref(), message.as_ref(), MessageBoxIcon::Info);
}
fn open_dialog_internal(
dialog_type: DialogType,
filter: Option<impl AsRef<str>>,
default_path: Option<impl AsRef<Path>>,
) -> crate::Result<Response> {
let response = open_dialog(
filter.map(|s| s.as_ref().to_string()).as_deref(),
default_path
.map(|s| s.as_ref().to_string_lossy().to_string())
.as_deref(),
dialog_type,
)
.map_err(|e| crate::Error::Dialog(e.to_string()))?;
match response {
Response::Cancel => Err(crate::Error::DialogCancelled),
_ => Ok(response),
}
}
/// Open single select file dialog
pub fn select(
filter_list: Option<impl AsRef<str>>,
default_path: Option<impl AsRef<Path>>,
) -> crate::Result<Response> {
open_dialog_internal(DialogType::SingleFile, filter_list, default_path)
}
/// Open multiple select file dialog
pub fn select_multiple(
filter_list: Option<impl AsRef<str>>,
default_path: Option<impl AsRef<Path>>,
) -> crate::Result<Response> {
open_dialog_internal(DialogType::MultipleFiles, filter_list, default_path)
}
/// Open save dialog
pub fn save_file(
filter_list: Option<impl AsRef<str>>,
default_path: Option<impl AsRef<Path>>,
) -> crate::Result<Response> {
open_dialog_internal(DialogType::SaveFile, filter_list, default_path)
}
/// Open pick folder dialog
pub fn pick_folder(default_path: Option<impl AsRef<Path>>) -> crate::Result<Response> {
let filter: Option<String> = None;
open_dialog_internal(DialogType::PickFolder, filter, default_path)
}

View File

@ -852,12 +852,6 @@ dependencies = [
"slab",
]
[[package]]
name = "gcc"
version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2"
[[package]]
name = "gdk"
version = "0.13.2"
@ -1636,15 +1630,6 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "nfd"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e752e3c216bc8a491c5b59fa46da10f1379ae450b19ac688e07f4bb55042e98"
dependencies = [
"gcc",
]
[[package]]
name = "nix"
version = "0.18.0"
@ -2329,6 +2314,27 @@ dependencies = [
"winreg",
]
[[package]]
name = "rfd"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3de7c6d5eab0f6b212d1b5a376639d91061bbfbdc2d7c7c5214063bd6ce99581"
dependencies = [
"block",
"cocoa-foundation",
"dispatch",
"glib-sys",
"gobject-sys",
"gtk-sys",
"js-sys",
"lazy_static",
"objc",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winapi 0.3.9",
]
[[package]]
name = "runas"
version = "0.2.1"
@ -2750,11 +2756,11 @@ dependencies = [
"either",
"flate2",
"http",
"nfd",
"notify-rust",
"once_cell",
"rand 0.8.3",
"reqwest",
"rfd",
"semver",
"serde",
"serde_json",

View File

@ -27,42 +27,58 @@
function openDialog() {
open({
defaultPath: defaultPath,
filter: filter,
multiple: multiple,
directory: directory
defaultPath,
filters: filter ? [{
name: 'Tauri Example',
extensions: filter.split(',').map(f => f.trim())
}] : [],
multiple,
directory
}).then(function (res) {
var pathToRead = res
var isFile = pathToRead.match(/\S+\.\S+$/g)
readBinaryFile(pathToRead).then(function (response) {
if (isFile) {
if (pathToRead.includes('.png') || pathToRead.includes('.jpg')) {
arrayBufferToBase64(new Uint8Array(response), function (base64) {
var src = 'data:image/png;base64,' + base64
onMessage('<img src="' + src + '"></img>')
})
if (Array.isArray(res)) {
onMessage(res)
} else {
var pathToRead = res
var isFile = pathToRead.match(/\S+\.\S+$/g)
readBinaryFile(pathToRead).then(function (response) {
if (isFile) {
if (pathToRead.includes('.png') || pathToRead.includes('.jpg')) {
arrayBufferToBase64(new Uint8Array(response), function (base64) {
var src = 'data:image/png;base64,' + base64
onMessage('<img src="' + src + '"></img>')
})
} else {
onMessage(res)
}
} else {
onMessage(res)
}
} else {
onMessage(res)
}
}).catch(onMessage(res))
}).catch(onMessage(res))
}
}).catch(onMessage)
}
function saveDialog() {
save({
defaultPath: defaultPath,
filter: filter,
defaultPath,
filters: filter ? [{
name: 'Tauri Example',
extensions: filter.split(',').map(f => f.trim())
}] : [],
}).then(onMessage).catch(onMessage)
}
</script>
<style>
#dialog-filter {
width: 260px;
}
</style>
<div style="margin-top: 24px">
<input id="dialog-default-path" placeholder="Default path" bind:value={defaultPath} />
<input id="dialog-filter" placeholder="Extensions filter" bind:value={filter} />
<input id="dialog-filter" placeholder="Extensions filter, comma-separated" bind:value={filter} />
<div>
<input type="checkbox" id="dialog-multiple" bind:checked={multiple} />
<label for="dialog-multiple">Multiple</label>

File diff suppressed because one or more lines are too long

View File

@ -7,31 +7,37 @@ document.getElementById("open-dialog").addEventListener("click", function () {
window.__TAURI__.dialog
.open({
defaultPath: defaultPathInput.value || null,
filter: filterInput.value || null,
filters: filterInput.value ? [{
name: 'Tauri Example',
extensions: filterInput.value.split(',').map(f => f.trim())
}] : [],
multiple: multipleInput.checked,
directory: directoryInput.checked,
})
.then(function (res) {
console.log(res);
var pathToRead = res;
var isFile = pathToRead.match(/\S+\.\S+$/g);
window.__TAURI__.fs
.readBinaryFile(pathToRead)
.then(function (response) {
if (isFile) {
if (pathToRead.includes(".png") || pathToRead.includes(".jpg")) {
arrayBufferToBase64(new Uint8Array(response), function (base64) {
var src = "data:image/png;base64," + base64;
registerResponse('<img src="' + src + '"></img>');
});
if (Array.isArray(res)) {
registerResponse(res);
} else {
var pathToRead = res;
var isFile = pathToRead.match(/\S+\.\S+$/g);
window.__TAURI__.fs
.readBinaryFile(pathToRead)
.then(function (response) {
if (isFile) {
if (pathToRead.includes(".png") || pathToRead.includes(".jpg")) {
arrayBufferToBase64(new Uint8Array(response), function (base64) {
var src = "data:image/png;base64," + base64;
registerResponse('<img src="' + src + '"></img>');
});
} else {
registerResponse(res);
}
} else {
registerResponse(res);
}
} else {
registerResponse(res);
}
})
.catch(registerResponse(res));
})
.catch(registerResponse(res));
}
})
.catch(registerResponse);
});
@ -40,7 +46,10 @@ document.getElementById("save-dialog").addEventListener("click", function () {
window.__TAURI__.dialog
.save({
defaultPath: defaultPathInput.value || null,
filter: filterInput.value || null,
filters: filterInput.value ? [{
name: 'Tauri Example',
extensions: filterInput.value.split(',').map(f => f.trim())
}] : [],
})
.then(registerResponse)
.catch(registerResponse);

View File

@ -852,12 +852,6 @@ dependencies = [
"slab",
]
[[package]]
name = "gcc"
version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2"
[[package]]
name = "gdk"
version = "0.13.2"
@ -1636,15 +1630,6 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "nfd"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e752e3c216bc8a491c5b59fa46da10f1379ae450b19ac688e07f4bb55042e98"
dependencies = [
"gcc",
]
[[package]]
name = "nix"
version = "0.18.0"
@ -2329,6 +2314,27 @@ dependencies = [
"winreg",
]
[[package]]
name = "rfd"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3de7c6d5eab0f6b212d1b5a376639d91061bbfbdc2d7c7c5214063bd6ce99581"
dependencies = [
"block",
"cocoa-foundation",
"dispatch",
"glib-sys",
"gobject-sys",
"gtk-sys",
"js-sys",
"lazy_static",
"objc",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winapi 0.3.9",
]
[[package]]
name = "runas"
version = "0.2.1"
@ -2750,11 +2756,11 @@ dependencies = [
"either",
"flate2",
"http",
"nfd",
"notify-rust",
"once_cell",
"rand 0.8.3",
"reqwest",
"rfd",
"semver",
"serde",
"serde_json",

File diff suppressed because one or more lines are too long

View File

@ -807,12 +807,6 @@ dependencies = [
"slab",
]
[[package]]
name = "gcc"
version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2"
[[package]]
name = "gdk"
version = "0.13.2"
@ -1591,15 +1585,6 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "nfd"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e752e3c216bc8a491c5b59fa46da10f1379ae450b19ac688e07f4bb55042e98"
dependencies = [
"gcc",
]
[[package]]
name = "nix"
version = "0.18.0"
@ -2278,6 +2263,27 @@ dependencies = [
"winreg",
]
[[package]]
name = "rfd"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3de7c6d5eab0f6b212d1b5a376639d91061bbfbdc2d7c7c5214063bd6ce99581"
dependencies = [
"block",
"cocoa-foundation",
"dispatch",
"glib-sys",
"gobject-sys",
"gtk-sys",
"js-sys",
"lazy_static",
"objc",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winapi 0.3.9",
]
[[package]]
name = "runas"
version = "0.2.1"
@ -2692,11 +2698,11 @@ dependencies = [
"either",
"flate2",
"http",
"nfd",
"notify-rust",
"once_cell",
"rand 0.8.3",
"reqwest",
"rfd",
"semver",
"serde",
"serde_json",

View File

@ -1,18 +1,25 @@
use crate::api::dialog::{
ask as ask_dialog, message as message_dialog, pick_folder, save_file, select, select_multiple,
AskResponse, Response,
ask as ask_dialog, message as message_dialog, AskResponse, FileDialogBuilder,
};
use serde::Deserialize;
use serde_json::Value as JsonValue;
use std::path::PathBuf;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DialogFilter {
name: String,
extensions: Vec<String>,
}
/// The options for the open dialog API.
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenDialogOptions {
/// The initial path of the dialog.
pub filter: Option<String>,
/// The filters of the dialog.
#[serde(default)]
pub filters: Vec<DialogFilter>,
/// Whether the dialog allows multiple selection or not.
#[serde(default)]
pub multiple: bool,
@ -27,8 +34,9 @@ pub struct OpenDialogOptions {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SaveDialogOptions {
/// The initial path of the dialog.
pub filter: Option<String>,
/// The filters of the dialog.
#[serde(default)]
pub filters: Vec<DialogFilter>,
/// The initial path of the dialog.
pub default_path: Option<PathBuf>,
}
@ -59,13 +67,13 @@ impl Cmd {
match self {
Self::OpenDialog { options } => {
#[cfg(open_dialog)]
return open(options).and_then(super::to_value);
return open(options);
#[cfg(not(open_dialog))]
Err(crate::Error::ApiNotAllowlisted("title".to_string()));
}
Self::SaveDialog { options } => {
#[cfg(save_dialog)]
return save(options).and_then(super::to_value);
return save(options);
#[cfg(not(save_dialog))]
Err(crate::Error::ApiNotAllowlisted("saveDialog".to_string()));
}
@ -97,36 +105,40 @@ impl Cmd {
}
}
/// maps a dialog response to a JS value to eval
#[cfg(any(open_dialog, save_dialog))]
fn map_response(response: Response) -> JsonValue {
match response {
Response::Okay(path) => path.into(),
Response::OkayMultiple(paths) => paths.into(),
Response::Cancel => JsonValue::Null,
}
}
/// Shows an open dialog.
#[cfg(open_dialog)]
pub fn open(options: OpenDialogOptions) -> crate::Result<JsonValue> {
let response = if options.multiple {
select_multiple(options.filter, options.default_path)
} else if options.directory {
pick_folder(options.default_path)
let mut dialog_builder = FileDialogBuilder::new();
if let Some(default_path) = options.default_path {
dialog_builder = dialog_builder.set_directory(default_path);
}
for filter in options.filters {
let extensions: Vec<&str> = filter.extensions.iter().map(|s| &**s).collect();
dialog_builder = dialog_builder.add_filter(filter.name, &extensions);
}
let response = if options.directory {
serde_json::to_value(dialog_builder.pick_folder())?
} else if options.multiple {
serde_json::to_value(dialog_builder.pick_files())?
} else {
select(options.filter, options.default_path)
serde_json::to_value(dialog_builder.pick_file())?
};
let res = response.map(map_response)?;
Ok(res)
Ok(response)
}
/// Shows a save dialog.
#[cfg(save_dialog)]
pub fn save(options: SaveDialogOptions) -> crate::Result<JsonValue> {
save_file(options.filter, options.default_path)
.map(map_response)
.map_err(Into::into)
let mut dialog_builder = FileDialogBuilder::new();
if let Some(default_path) = options.default_path {
dialog_builder = dialog_builder.set_directory(default_path);
}
for filter in options.filters {
let extensions: Vec<&str> = filter.extensions.iter().map(|s| &**s).collect();
dialog_builder = dialog_builder.add_filter(filter.name, &extensions);
}
let response = dialog_builder.save_file();
Ok(serde_json::to_value(response)?)
}
/// Shows a dialog with a yes/no question.