mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-07-24 17:20:51 +02:00
feat(dialog): add plugin (#306)
This commit is contained in:
committed by
GitHub
parent
4d32919f0f
commit
7a8633f429
@@ -0,0 +1,282 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{command, Manager, Runtime, State, Window};
|
||||
|
||||
use crate::{Dialog, FileDialogBuilder, FileResponse, MessageDialogKind, Result};
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum OpenResponse {
|
||||
#[cfg(desktop)]
|
||||
Folders(Option<Vec<PathBuf>>),
|
||||
#[cfg(desktop)]
|
||||
Folder(Option<PathBuf>),
|
||||
Files(Option<Vec<FileResponse>>),
|
||||
File(Option<FileResponse>),
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DialogFilter {
|
||||
name: String,
|
||||
extensions: Vec<String>,
|
||||
}
|
||||
|
||||
/// The options for the open dialog API.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OpenDialogOptions {
|
||||
/// The title of the dialog window.
|
||||
title: Option<String>,
|
||||
/// The filters of the dialog.
|
||||
#[serde(default)]
|
||||
filters: Vec<DialogFilter>,
|
||||
/// Whether the dialog allows multiple selection or not.
|
||||
#[serde(default)]
|
||||
multiple: bool,
|
||||
/// Whether the dialog is a directory selection (`true` value) or file selection (`false` value).
|
||||
#[serde(default)]
|
||||
directory: bool,
|
||||
/// The initial path of the dialog.
|
||||
default_path: Option<PathBuf>,
|
||||
/// If [`Self::directory`] is true, indicates that it will be read recursively later.
|
||||
/// Defines whether subdirectories will be allowed on the scope or not.
|
||||
#[serde(default)]
|
||||
#[cfg_attr(mobile, allow(dead_code))]
|
||||
recursive: bool,
|
||||
}
|
||||
|
||||
/// The options for the save dialog API.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(mobile, allow(dead_code))]
|
||||
pub struct SaveDialogOptions {
|
||||
/// The title of the dialog window.
|
||||
title: Option<String>,
|
||||
/// The filters of the dialog.
|
||||
#[serde(default)]
|
||||
filters: Vec<DialogFilter>,
|
||||
/// The initial path of the dialog.
|
||||
default_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn set_default_path<R: Runtime>(
|
||||
mut dialog_builder: FileDialogBuilder<R>,
|
||||
default_path: PathBuf,
|
||||
) -> FileDialogBuilder<R> {
|
||||
if default_path.is_file() || !default_path.exists() {
|
||||
if let (Some(parent), Some(file_name)) = (default_path.parent(), default_path.file_name()) {
|
||||
if parent.components().count() > 0 {
|
||||
dialog_builder = dialog_builder.set_directory(parent);
|
||||
}
|
||||
dialog_builder = dialog_builder.set_file_name(file_name.to_string_lossy());
|
||||
} else {
|
||||
dialog_builder = dialog_builder.set_directory(default_path);
|
||||
}
|
||||
dialog_builder
|
||||
} else {
|
||||
dialog_builder.set_directory(default_path)
|
||||
}
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn open<R: Runtime>(
|
||||
window: Window<R>,
|
||||
dialog: State<'_, Dialog<R>>,
|
||||
options: OpenDialogOptions,
|
||||
) -> Result<OpenResponse> {
|
||||
let mut dialog_builder = dialog.file();
|
||||
#[cfg(any(windows, target_os = "macos"))]
|
||||
{
|
||||
dialog_builder = dialog_builder.set_parent(&window);
|
||||
}
|
||||
if let Some(title) = options.title {
|
||||
dialog_builder = dialog_builder.set_title(&title);
|
||||
}
|
||||
if let Some(default_path) = options.default_path {
|
||||
dialog_builder = set_default_path(dialog_builder, 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 res = if options.directory {
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
if options.multiple {
|
||||
let folders = dialog_builder.blocking_pick_folders();
|
||||
if let Some(folders) = &folders {
|
||||
for folder in folders {
|
||||
window
|
||||
.fs_scope()
|
||||
.allow_directory(folder, options.recursive)?;
|
||||
}
|
||||
}
|
||||
OpenResponse::Folders(folders)
|
||||
} else {
|
||||
let folder = dialog_builder.blocking_pick_folder();
|
||||
if let Some(path) = &folder {
|
||||
window.fs_scope().allow_directory(path, options.recursive)?;
|
||||
}
|
||||
OpenResponse::Folder(folder)
|
||||
}
|
||||
}
|
||||
#[cfg(mobile)]
|
||||
return Err(crate::Error::FolderPickerNotImplemented);
|
||||
} else if options.multiple {
|
||||
let files = dialog_builder.blocking_pick_files();
|
||||
if let Some(files) = &files {
|
||||
for file in files {
|
||||
window.fs_scope().allow_file(&file.path)?;
|
||||
}
|
||||
}
|
||||
OpenResponse::Files(files)
|
||||
} else {
|
||||
let file = dialog_builder.blocking_pick_file();
|
||||
if let Some(file) = &file {
|
||||
window.fs_scope().allow_file(&file.path)?;
|
||||
}
|
||||
OpenResponse::File(file)
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
#[command]
|
||||
pub(crate) async fn save<R: Runtime>(
|
||||
window: Window<R>,
|
||||
dialog: State<'_, Dialog<R>>,
|
||||
options: SaveDialogOptions,
|
||||
) -> Result<Option<PathBuf>> {
|
||||
#[cfg(mobile)]
|
||||
return Err(crate::Error::FileSaveDialogNotImplemented);
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
let mut dialog_builder = dialog.file();
|
||||
#[cfg(any(windows, target_os = "macos"))]
|
||||
{
|
||||
dialog_builder = dialog_builder.set_parent(&window);
|
||||
}
|
||||
if let Some(title) = options.title {
|
||||
dialog_builder = dialog_builder.set_title(&title);
|
||||
}
|
||||
if let Some(default_path) = options.default_path {
|
||||
dialog_builder = set_default_path(dialog_builder, 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 path = dialog_builder.blocking_save_file();
|
||||
if let Some(p) = &path {
|
||||
window.fs_scope().allow_file(p)?;
|
||||
}
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
}
|
||||
|
||||
fn message_dialog<R: Runtime>(
|
||||
#[allow(unused_variables)] window: Window<R>,
|
||||
dialog: State<'_, Dialog<R>>,
|
||||
title: Option<String>,
|
||||
message: String,
|
||||
type_: Option<MessageDialogKind>,
|
||||
ok_button_label: Option<String>,
|
||||
cancel_button_label: Option<String>,
|
||||
) -> bool {
|
||||
let mut builder = dialog.message(message);
|
||||
|
||||
if let Some(title) = title {
|
||||
builder = builder.title(title);
|
||||
}
|
||||
|
||||
#[cfg(any(windows, target_os = "macos"))]
|
||||
{
|
||||
builder = builder.parent(&window);
|
||||
}
|
||||
|
||||
if let Some(type_) = type_ {
|
||||
builder = builder.kind(type_);
|
||||
}
|
||||
|
||||
if let Some(ok) = ok_button_label {
|
||||
builder = builder.ok_button_label(ok);
|
||||
}
|
||||
|
||||
if let Some(cancel) = cancel_button_label {
|
||||
builder = builder.cancel_button_label(cancel);
|
||||
}
|
||||
|
||||
builder.blocking_show()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn message<R: Runtime>(
|
||||
window: Window<R>,
|
||||
dialog: State<'_, Dialog<R>>,
|
||||
title: Option<String>,
|
||||
message: String,
|
||||
type_: Option<MessageDialogKind>,
|
||||
ok_button_label: Option<String>,
|
||||
) -> Result<bool> {
|
||||
Ok(message_dialog(
|
||||
window,
|
||||
dialog,
|
||||
title,
|
||||
message,
|
||||
type_,
|
||||
ok_button_label,
|
||||
None,
|
||||
))
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn ask<R: Runtime>(
|
||||
window: Window<R>,
|
||||
dialog: State<'_, Dialog<R>>,
|
||||
title: Option<String>,
|
||||
message: String,
|
||||
type_: Option<MessageDialogKind>,
|
||||
ok_button_label: Option<String>,
|
||||
cancel_button_label: Option<String>,
|
||||
) -> Result<bool> {
|
||||
Ok(message_dialog(
|
||||
window,
|
||||
dialog,
|
||||
title,
|
||||
message,
|
||||
type_,
|
||||
ok_button_label,
|
||||
cancel_button_label,
|
||||
))
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn confirm<R: Runtime>(
|
||||
window: Window<R>,
|
||||
dialog: State<'_, Dialog<R>>,
|
||||
title: Option<String>,
|
||||
message: String,
|
||||
type_: Option<MessageDialogKind>,
|
||||
ok_button_label: Option<String>,
|
||||
cancel_button_label: Option<String>,
|
||||
) -> Result<bool> {
|
||||
Ok(message_dialog(
|
||||
window,
|
||||
dialog,
|
||||
title,
|
||||
message,
|
||||
type_,
|
||||
ok_button_label,
|
||||
cancel_button_label,
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Use native message and file open/save dialogs.
|
||||
//!
|
||||
//! This module exposes non-blocking APIs on its root, relying on callback closures
|
||||
//! to give results back. This is particularly useful when running dialogs from the main thread.
|
||||
//! When using on asynchronous contexts such as async commands, the [`blocking`] APIs are recommended.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
use tauri::{plugin::PluginApi, AppHandle, Runtime};
|
||||
|
||||
use crate::{models::*, FileDialogBuilder, MessageDialogBuilder};
|
||||
|
||||
const OK: &str = "Ok";
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
type FileDialog = rfd::FileDialog;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
type FileDialog = rfd::AsyncFileDialog;
|
||||
|
||||
pub fn init<R: Runtime, C: DeserializeOwned>(
|
||||
app: &AppHandle<R>,
|
||||
_api: PluginApi<R, C>,
|
||||
) -> crate::Result<Dialog<R>> {
|
||||
Ok(Dialog(app.clone()))
|
||||
}
|
||||
|
||||
/// Access to the dialog APIs.
|
||||
#[derive(Debug)]
|
||||
pub struct Dialog<R: Runtime>(AppHandle<R>);
|
||||
|
||||
impl<R: Runtime> Clone for Dialog<R> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> Dialog<R> {
|
||||
pub(crate) fn app_handle(&self) -> &AppHandle<R> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
macro_rules! run_dialog {
|
||||
($e:expr, $h: ident) => {{
|
||||
std::thread::spawn(move || {
|
||||
let response = $e;
|
||||
$h(response);
|
||||
});
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
macro_rules! run_dialog {
|
||||
($e:expr, $h: ident) => {{
|
||||
std::thread::spawn(move || {
|
||||
let context = glib::MainContext::default();
|
||||
context.invoke_with_priority(glib::PRIORITY_HIGH, move || {
|
||||
let response = $e;
|
||||
$h(response);
|
||||
});
|
||||
});
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
macro_rules! run_file_dialog {
|
||||
($e:expr, $h: ident) => {{
|
||||
std::thread::spawn(move || {
|
||||
let response = tauri::async_runtime::block_on($e);
|
||||
$h(response);
|
||||
});
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
macro_rules! run_file_dialog {
|
||||
($e:expr, $h: ident) => {{
|
||||
std::thread::spawn(move || {
|
||||
let context = glib::MainContext::default();
|
||||
context.invoke_with_priority(glib::PRIORITY_HIGH, move || {
|
||||
let response = $e;
|
||||
$h(response);
|
||||
});
|
||||
});
|
||||
}};
|
||||
}
|
||||
|
||||
impl From<MessageDialogKind> for rfd::MessageLevel {
|
||||
fn from(kind: MessageDialogKind) -> Self {
|
||||
match kind {
|
||||
MessageDialogKind::Info => Self::Info,
|
||||
MessageDialogKind::Warning => Self::Warning,
|
||||
MessageDialogKind::Error => Self::Error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> From<FileDialogBuilder<R>> for FileDialog {
|
||||
fn from(d: FileDialogBuilder<R>) -> Self {
|
||||
let mut builder = FileDialog::new();
|
||||
|
||||
if let Some(title) = d.title {
|
||||
builder = builder.set_title(&title);
|
||||
}
|
||||
if let Some(starting_directory) = d.starting_directory {
|
||||
builder = builder.set_directory(starting_directory);
|
||||
}
|
||||
if let Some(file_name) = d.file_name {
|
||||
builder = builder.set_file_name(&file_name);
|
||||
}
|
||||
for filter in d.filters {
|
||||
let v: Vec<&str> = filter.extensions.iter().map(|x| &**x).collect();
|
||||
builder = builder.add_filter(&filter.name, &v);
|
||||
}
|
||||
#[cfg(desktop)]
|
||||
if let Some(_parent) = d.parent {
|
||||
// TODO builder = builder.set_parent(&parent);
|
||||
}
|
||||
|
||||
builder
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> From<MessageDialogBuilder<R>> for rfd::MessageDialog {
|
||||
fn from(d: MessageDialogBuilder<R>) -> Self {
|
||||
let mut dialog = rfd::MessageDialog::new()
|
||||
.set_title(&d.title)
|
||||
.set_description(&d.message)
|
||||
.set_level(d.kind.into());
|
||||
|
||||
let buttons = match (d.ok_button_label, d.cancel_button_label) {
|
||||
(Some(ok), Some(cancel)) => Some(rfd::MessageButtons::OkCancelCustom(ok, cancel)),
|
||||
(Some(ok), None) => Some(rfd::MessageButtons::OkCustom(ok)),
|
||||
(None, Some(cancel)) => Some(rfd::MessageButtons::OkCancelCustom(OK.into(), cancel)),
|
||||
(None, None) => None,
|
||||
};
|
||||
if let Some(buttons) = buttons {
|
||||
dialog = dialog.set_buttons(buttons);
|
||||
}
|
||||
|
||||
if let Some(_parent) = d.parent {
|
||||
// TODO dialog.set_parent(parent);
|
||||
}
|
||||
|
||||
dialog
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pick_file<R: Runtime, F: FnOnce(Option<PathBuf>) + Send + 'static>(
|
||||
dialog: FileDialogBuilder<R>,
|
||||
f: F,
|
||||
) {
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let f = |path: Option<rfd::FileHandle>| f(path.map(|p| p.path().to_path_buf()));
|
||||
run_file_dialog!(FileDialog::from(dialog).pick_file(), f)
|
||||
}
|
||||
|
||||
pub fn pick_files<R: Runtime, F: FnOnce(Option<Vec<PathBuf>>) + Send + 'static>(
|
||||
dialog: FileDialogBuilder<R>,
|
||||
f: F,
|
||||
) {
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let f = |paths: Option<Vec<rfd::FileHandle>>| {
|
||||
f(paths.map(|list| list.into_iter().map(|p| p.path().to_path_buf()).collect()))
|
||||
};
|
||||
run_file_dialog!(FileDialog::from(dialog).pick_files(), f)
|
||||
}
|
||||
|
||||
pub fn pick_folder<R: Runtime, F: FnOnce(Option<PathBuf>) + Send + 'static>(
|
||||
dialog: FileDialogBuilder<R>,
|
||||
f: F,
|
||||
) {
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let f = |path: Option<rfd::FileHandle>| f(path.map(|p| p.path().to_path_buf()));
|
||||
run_file_dialog!(FileDialog::from(dialog).pick_folder(), f)
|
||||
}
|
||||
|
||||
pub fn pick_folders<R: Runtime, F: FnOnce(Option<Vec<PathBuf>>) + Send + 'static>(
|
||||
dialog: FileDialogBuilder<R>,
|
||||
f: F,
|
||||
) {
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let f = |paths: Option<Vec<rfd::FileHandle>>| {
|
||||
f(paths.map(|list| list.into_iter().map(|p| p.path().to_path_buf()).collect()))
|
||||
};
|
||||
run_file_dialog!(FileDialog::from(dialog).pick_folders(), f)
|
||||
}
|
||||
|
||||
pub fn save_file<R: Runtime, F: FnOnce(Option<PathBuf>) + Send + 'static>(
|
||||
dialog: FileDialogBuilder<R>,
|
||||
f: F,
|
||||
) {
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let f = |path: Option<rfd::FileHandle>| f(path.map(|p| p.path().to_path_buf()));
|
||||
run_file_dialog!(FileDialog::from(dialog).save_file(), f)
|
||||
}
|
||||
|
||||
/// Shows a message dialog
|
||||
pub fn show_message_dialog<R: Runtime, F: FnOnce(bool) + Send + 'static>(
|
||||
dialog: MessageDialogBuilder<R>,
|
||||
f: F,
|
||||
) {
|
||||
run_dialog!(rfd::MessageDialog::from(dialog).show(), f);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::{ser::Serializer, Serialize};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Tauri(#[from] tauri::Error),
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
#[cfg(mobile)]
|
||||
#[error(transparent)]
|
||||
PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),
|
||||
#[cfg(mobile)]
|
||||
#[error("Folder picker is not implemented on mobile")]
|
||||
FolderPickerNotImplemented,
|
||||
#[cfg(mobile)]
|
||||
#[error("File save dialog is not implemented on mobile")]
|
||||
FileSaveDialogNotImplemented,
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,544 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{
|
||||
plugin::{Builder, TauriPlugin},
|
||||
Manager, Runtime,
|
||||
};
|
||||
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::mpsc::sync_channel,
|
||||
};
|
||||
|
||||
pub use models::*;
|
||||
|
||||
#[cfg(desktop)]
|
||||
mod desktop;
|
||||
#[cfg(mobile)]
|
||||
mod mobile;
|
||||
|
||||
mod commands;
|
||||
mod error;
|
||||
mod models;
|
||||
|
||||
pub use error::{Error, Result};
|
||||
|
||||
#[cfg(desktop)]
|
||||
use desktop::*;
|
||||
#[cfg(mobile)]
|
||||
use mobile::*;
|
||||
|
||||
macro_rules! blocking_fn {
|
||||
($self:ident, $fn:ident) => {{
|
||||
let (tx, rx) = sync_channel(0);
|
||||
let cb = move |response| {
|
||||
tx.send(response).unwrap();
|
||||
};
|
||||
$self.$fn(cb);
|
||||
rx.recv().unwrap()
|
||||
}};
|
||||
}
|
||||
|
||||
/// Extensions to [`tauri::App`], [`tauri::AppHandle`] and [`tauri::Window`] to access the dialog APIs.
|
||||
pub trait DialogExt<R: Runtime> {
|
||||
fn dialog(&self) -> &Dialog<R>;
|
||||
}
|
||||
|
||||
impl<R: Runtime, T: Manager<R>> crate::DialogExt<R> for T {
|
||||
fn dialog(&self) -> &Dialog<R> {
|
||||
self.state::<Dialog<R>>().inner()
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> Dialog<R> {
|
||||
pub fn message(&self, message: impl Into<String>) -> MessageDialogBuilder<R> {
|
||||
MessageDialogBuilder::new(
|
||||
self.clone(),
|
||||
self.app_handle().package_info().name.clone(),
|
||||
message,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn file(&self) -> FileDialogBuilder<R> {
|
||||
FileDialogBuilder::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the plugin.
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new("dialog")
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::open,
|
||||
commands::save,
|
||||
commands::message,
|
||||
commands::ask,
|
||||
commands::confirm
|
||||
])
|
||||
.setup(|app, api| {
|
||||
#[cfg(mobile)]
|
||||
let dialog = mobile::init(app, api)?;
|
||||
#[cfg(desktop)]
|
||||
let dialog = desktop::init(app, api)?;
|
||||
app.manage(dialog);
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
/// A builder for message dialogs.
|
||||
pub struct MessageDialogBuilder<R: Runtime> {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) dialog: Dialog<R>,
|
||||
pub(crate) title: String,
|
||||
pub(crate) message: String,
|
||||
pub(crate) kind: MessageDialogKind,
|
||||
pub(crate) ok_button_label: Option<String>,
|
||||
pub(crate) cancel_button_label: Option<String>,
|
||||
#[cfg(desktop)]
|
||||
pub(crate) parent: Option<raw_window_handle::RawWindowHandle>,
|
||||
}
|
||||
|
||||
/// Payload for the message dialog mobile API.
|
||||
#[cfg(mobile)]
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MessageDialogPayload<'a> {
|
||||
title: &'a String,
|
||||
message: &'a String,
|
||||
kind: &'a MessageDialogKind,
|
||||
ok_button_label: &'a Option<String>,
|
||||
cancel_button_label: &'a Option<String>,
|
||||
}
|
||||
|
||||
// raw window handle :(
|
||||
unsafe impl<R: Runtime> Send for MessageDialogBuilder<R> {}
|
||||
|
||||
impl<R: Runtime> MessageDialogBuilder<R> {
|
||||
/// Creates a new message dialog builder.
|
||||
pub fn new(dialog: Dialog<R>, title: impl Into<String>, message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
dialog,
|
||||
title: title.into(),
|
||||
message: message.into(),
|
||||
kind: Default::default(),
|
||||
ok_button_label: None,
|
||||
cancel_button_label: None,
|
||||
#[cfg(desktop)]
|
||||
parent: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(mobile)]
|
||||
pub(crate) fn payload(&self) -> MessageDialogPayload<'_> {
|
||||
MessageDialogPayload {
|
||||
title: &self.title,
|
||||
message: &self.message,
|
||||
kind: &self.kind,
|
||||
ok_button_label: &self.ok_button_label,
|
||||
cancel_button_label: &self.cancel_button_label,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the dialog title.
|
||||
pub fn title(mut self, title: impl Into<String>) -> Self {
|
||||
self.title = title.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set parent windows explicitly (optional)
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Linux:** Unsupported.
|
||||
#[cfg(desktop)]
|
||||
pub fn parent<W: raw_window_handle::HasRawWindowHandle>(mut self, parent: &W) -> Self {
|
||||
self.parent.replace(parent.raw_window_handle());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the label for the OK button.
|
||||
pub fn ok_button_label(mut self, label: impl Into<String>) -> Self {
|
||||
self.ok_button_label.replace(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the label for the Cancel button.
|
||||
pub fn cancel_button_label(mut self, label: impl Into<String>) -> Self {
|
||||
self.cancel_button_label.replace(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set type of a dialog.
|
||||
///
|
||||
/// Depending on the system it can result in type specific icon to show up,
|
||||
/// the will inform user it message is a error, warning or just information.
|
||||
pub fn kind(mut self, kind: MessageDialogKind) -> Self {
|
||||
self.kind = kind;
|
||||
self
|
||||
}
|
||||
|
||||
/// Shows a message dialog
|
||||
pub fn show<F: FnOnce(bool) + Send + 'static>(self, f: F) {
|
||||
show_message_dialog(self, f)
|
||||
}
|
||||
|
||||
//// Shows a message dialog.
|
||||
pub fn blocking_show(self) -> bool {
|
||||
blocking_fn!(self, show)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FileResponse {
|
||||
pub base64_data: Option<String>,
|
||||
pub duration: Option<u64>,
|
||||
pub height: Option<usize>,
|
||||
pub width: Option<usize>,
|
||||
pub mime_type: Option<String>,
|
||||
pub modified_at: Option<u64>,
|
||||
pub name: Option<String>,
|
||||
pub path: PathBuf,
|
||||
pub size: u64,
|
||||
}
|
||||
|
||||
impl FileResponse {
|
||||
#[cfg(desktop)]
|
||||
fn new(path: PathBuf) -> Self {
|
||||
Self {
|
||||
base64_data: None,
|
||||
duration: None,
|
||||
height: None,
|
||||
width: None,
|
||||
mime_type: None,
|
||||
modified_at: None,
|
||||
name: path.file_name().map(|f| f.to_string_lossy().into_owned()),
|
||||
path,
|
||||
size: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(crate) struct Filter {
|
||||
pub name: String,
|
||||
pub extensions: Vec<String>,
|
||||
}
|
||||
|
||||
/// The file dialog builder.
|
||||
///
|
||||
/// Constructs file picker dialogs that can select single/multiple files or directories.
|
||||
#[derive(Debug)]
|
||||
pub struct FileDialogBuilder<R: Runtime> {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) dialog: Dialog<R>,
|
||||
pub(crate) filters: Vec<Filter>,
|
||||
pub(crate) starting_directory: Option<PathBuf>,
|
||||
pub(crate) file_name: Option<String>,
|
||||
pub(crate) title: Option<String>,
|
||||
#[cfg(desktop)]
|
||||
pub(crate) parent: Option<raw_window_handle::RawWindowHandle>,
|
||||
}
|
||||
|
||||
#[cfg(mobile)]
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FileDialogPayload<'a> {
|
||||
filters: &'a Vec<Filter>,
|
||||
multiple: bool,
|
||||
}
|
||||
|
||||
// raw window handle :(
|
||||
unsafe impl<R: Runtime> Send for FileDialogBuilder<R> {}
|
||||
|
||||
impl<R: Runtime> FileDialogBuilder<R> {
|
||||
/// Gets the default file dialog builder.
|
||||
pub fn new(dialog: Dialog<R>) -> Self {
|
||||
Self {
|
||||
dialog,
|
||||
filters: Vec::new(),
|
||||
starting_directory: None,
|
||||
file_name: None,
|
||||
title: None,
|
||||
#[cfg(desktop)]
|
||||
parent: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(mobile)]
|
||||
pub(crate) fn payload(&self, multiple: bool) -> FileDialogPayload<'_> {
|
||||
FileDialogPayload {
|
||||
filters: &self.filters,
|
||||
multiple,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add file extension filter. Takes in the name of the filter, and list of extensions
|
||||
#[must_use]
|
||||
pub fn add_filter(mut self, name: impl Into<String>, extensions: &[&str]) -> Self {
|
||||
self.filters.push(Filter {
|
||||
name: name.into(),
|
||||
extensions: extensions.iter().map(|e| e.to_string()).collect(),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Set starting directory of the dialog.
|
||||
#[must_use]
|
||||
pub fn set_directory<P: AsRef<Path>>(mut self, directory: P) -> Self {
|
||||
self.starting_directory.replace(directory.as_ref().into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set starting file name of the dialog.
|
||||
#[must_use]
|
||||
pub fn set_file_name(mut self, file_name: impl Into<String>) -> Self {
|
||||
self.file_name.replace(file_name.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the parent window of the dialog.
|
||||
#[cfg(desktop)]
|
||||
#[must_use]
|
||||
pub fn set_parent<W: raw_window_handle::HasRawWindowHandle>(mut self, parent: &W) -> Self {
|
||||
self.parent.replace(parent.raw_window_handle());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the title of the dialog.
|
||||
#[must_use]
|
||||
pub fn set_title(mut self, title: impl Into<String>) -> Self {
|
||||
self.title.replace(title.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Shows the dialog to select a single file.
|
||||
/// This is not a blocking operation,
|
||||
/// and should be used when running on the main thread to avoid deadlocks with the event loop.
|
||||
///
|
||||
/// For usage in other contexts such as commands, prefer [`Self::pick_file`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use tauri::api::dialog::FileDialogBuilder;
|
||||
/// tauri::Builder::default()
|
||||
/// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"))
|
||||
/// .expect("failed to build tauri app")
|
||||
/// .run(|_app, _event| {
|
||||
/// FileDialogBuilder::new().pick_file(|file_path| {
|
||||
/// // do something with the optional file path here
|
||||
/// // the file path is `None` if the user closed the dialog
|
||||
/// })
|
||||
/// })
|
||||
/// ```
|
||||
pub fn pick_file<F: FnOnce(Option<FileResponse>) + Send + 'static>(self, f: F) {
|
||||
#[cfg(desktop)]
|
||||
let f = |path: Option<PathBuf>| f(path.map(FileResponse::new));
|
||||
pick_file(self, f)
|
||||
}
|
||||
|
||||
/// Shows the dialog to select multiple files.
|
||||
/// This is not a blocking operation,
|
||||
/// and should be used when running on the main thread to avoid deadlocks with the event loop.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use tauri::api::dialog::FileDialogBuilder;
|
||||
/// tauri::Builder::default()
|
||||
/// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"))
|
||||
/// .expect("failed to build tauri app")
|
||||
/// .run(|_app, _event| {
|
||||
/// FileDialogBuilder::new().pick_files(|file_paths| {
|
||||
/// // do something with the optional file paths here
|
||||
/// // the file paths value is `None` if the user closed the dialog
|
||||
/// })
|
||||
/// })
|
||||
/// ```
|
||||
pub fn pick_files<F: FnOnce(Option<Vec<FileResponse>>) + Send + 'static>(self, f: F) {
|
||||
#[cfg(desktop)]
|
||||
let f = |paths: Option<Vec<PathBuf>>| {
|
||||
f(paths.map(|p| {
|
||||
p.into_iter()
|
||||
.map(FileResponse::new)
|
||||
.collect::<Vec<FileResponse>>()
|
||||
}))
|
||||
};
|
||||
pick_files(self, f)
|
||||
}
|
||||
|
||||
/// Shows the dialog to select a single folder.
|
||||
/// This is not a blocking operation,
|
||||
/// and should be used when running on the main thread to avoid deadlocks with the event loop.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use tauri::api::dialog::FileDialogBuilder;
|
||||
/// tauri::Builder::default()
|
||||
/// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"))
|
||||
/// .expect("failed to build tauri app")
|
||||
/// .run(|_app, _event| {
|
||||
/// FileDialogBuilder::new().pick_folder(|folder_path| {
|
||||
/// // do something with the optional folder path here
|
||||
/// // the folder path is `None` if the user closed the dialog
|
||||
/// })
|
||||
/// })
|
||||
/// ```
|
||||
#[cfg(desktop)]
|
||||
pub fn pick_folder<F: FnOnce(Option<PathBuf>) + Send + 'static>(self, f: F) {
|
||||
pick_folder(self, f)
|
||||
}
|
||||
|
||||
/// Shows the dialog to select multiple folders.
|
||||
/// This is not a blocking operation,
|
||||
/// and should be used when running on the main thread to avoid deadlocks with the event loop.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use tauri::api::dialog::FileDialogBuilder;
|
||||
/// tauri::Builder::default()
|
||||
/// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"))
|
||||
/// .expect("failed to build tauri app")
|
||||
/// .run(|_app, _event| {
|
||||
/// FileDialogBuilder::new().pick_folders(|file_paths| {
|
||||
/// // do something with the optional folder paths here
|
||||
/// // the folder paths value is `None` if the user closed the dialog
|
||||
/// })
|
||||
/// })
|
||||
/// ```
|
||||
#[cfg(desktop)]
|
||||
pub fn pick_folders<F: FnOnce(Option<Vec<PathBuf>>) + Send + 'static>(self, f: F) {
|
||||
pick_folders(self, f)
|
||||
}
|
||||
|
||||
/// Shows the dialog to save a file.
|
||||
///
|
||||
/// This is not a blocking operation,
|
||||
/// and should be used when running on the main thread to avoid deadlocks with the event loop.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use tauri::api::dialog::FileDialogBuilder;
|
||||
/// tauri::Builder::default()
|
||||
/// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"))
|
||||
/// .expect("failed to build tauri app")
|
||||
/// .run(|_app, _event| {
|
||||
/// FileDialogBuilder::new().save_file(|file_path| {
|
||||
/// // do something with the optional file path here
|
||||
/// // the file path is `None` if the user closed the dialog
|
||||
/// })
|
||||
/// })
|
||||
/// ```
|
||||
#[cfg(desktop)]
|
||||
pub fn save_file<F: FnOnce(Option<PathBuf>) + Send + 'static>(self, f: F) {
|
||||
save_file(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Blocking APIs.
|
||||
impl<R: Runtime> FileDialogBuilder<R> {
|
||||
/// Shows the dialog to select a single file.
|
||||
/// This is a blocking operation,
|
||||
/// and should *NOT* be used when running on the main thread context.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use tauri::api::dialog::blocking::FileDialogBuilder;
|
||||
/// #[tauri::command]
|
||||
/// async fn my_command() {
|
||||
/// let file_path = FileDialogBuilder::new().pick_file();
|
||||
/// // do something with the optional file path here
|
||||
/// // the file path is `None` if the user closed the dialog
|
||||
/// }
|
||||
/// ```
|
||||
pub fn blocking_pick_file(self) -> Option<FileResponse> {
|
||||
blocking_fn!(self, pick_file)
|
||||
}
|
||||
|
||||
/// Shows the dialog to select multiple files.
|
||||
/// This is a blocking operation,
|
||||
/// and should *NOT* be used when running on the main thread context.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use tauri::api::dialog::blocking::FileDialogBuilder;
|
||||
/// #[tauri::command]
|
||||
/// async fn my_command() {
|
||||
/// let file_path = FileDialogBuilder::new().pick_files();
|
||||
/// // do something with the optional file paths here
|
||||
/// // the file paths value is `None` if the user closed the dialog
|
||||
/// }
|
||||
/// ```
|
||||
pub fn blocking_pick_files(self) -> Option<Vec<FileResponse>> {
|
||||
blocking_fn!(self, pick_files)
|
||||
}
|
||||
|
||||
/// Shows the dialog to select a single folder.
|
||||
/// This is a blocking operation,
|
||||
/// and should *NOT* be used when running on the main thread context.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use tauri::api::dialog::blocking::FileDialogBuilder;
|
||||
/// #[tauri::command]
|
||||
/// async fn my_command() {
|
||||
/// let folder_path = FileDialogBuilder::new().pick_folder();
|
||||
/// // do something with the optional folder path here
|
||||
/// // the folder path is `None` if the user closed the dialog
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg(desktop)]
|
||||
pub fn blocking_pick_folder(self) -> Option<PathBuf> {
|
||||
blocking_fn!(self, pick_folder)
|
||||
}
|
||||
|
||||
/// Shows the dialog to select multiple folders.
|
||||
/// This is a blocking operation,
|
||||
/// and should *NOT* be used when running on the main thread context.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use tauri::api::dialog::blocking::FileDialogBuilder;
|
||||
/// #[tauri::command]
|
||||
/// async fn my_command() {
|
||||
/// let folder_paths = FileDialogBuilder::new().pick_folders();
|
||||
/// // do something with the optional folder paths here
|
||||
/// // the folder paths value is `None` if the user closed the dialog
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg(desktop)]
|
||||
pub fn blocking_pick_folders(self) -> Option<Vec<PathBuf>> {
|
||||
blocking_fn!(self, pick_folders)
|
||||
}
|
||||
|
||||
/// Shows the dialog to save a file.
|
||||
/// This is a blocking operation,
|
||||
/// and should *NOT* be used when running on the main thread context.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use tauri::api::dialog::blocking::FileDialogBuilder;
|
||||
/// #[tauri::command]
|
||||
/// async fn my_command() {
|
||||
/// let file_path = FileDialogBuilder::new().save_file();
|
||||
/// // do something with the optional file path here
|
||||
/// // the file path is `None` if the user closed the dialog
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg(desktop)]
|
||||
pub fn blocking_save_file(self) -> Option<PathBuf> {
|
||||
blocking_fn!(self, save_file)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::{de::DeserializeOwned, Deserialize};
|
||||
use tauri::{
|
||||
plugin::{PluginApi, PluginHandle},
|
||||
AppHandle, Runtime,
|
||||
};
|
||||
|
||||
use crate::{FileDialogBuilder, FileResponse, MessageDialogBuilder};
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
const PLUGIN_IDENTIFIER: &str = "app.tauri.dialog";
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
tauri::ios_plugin_binding!(init_plugin_dialog);
|
||||
|
||||
// initializes the Kotlin or Swift plugin classes
|
||||
pub fn init<R: Runtime, C: DeserializeOwned>(
|
||||
_app: &AppHandle<R>,
|
||||
api: PluginApi<R, C>,
|
||||
) -> crate::Result<Dialog<R>> {
|
||||
#[cfg(target_os = "android")]
|
||||
let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "DialogPlugin")?;
|
||||
#[cfg(target_os = "ios")]
|
||||
let handle = api.register_ios_plugin(init_plugin_dialog)?;
|
||||
Ok(Dialog(handle))
|
||||
}
|
||||
|
||||
/// Access to the dialog APIs.
|
||||
#[derive(Debug)]
|
||||
pub struct Dialog<R: Runtime>(PluginHandle<R>);
|
||||
|
||||
impl<R: Runtime> Clone for Dialog<R> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> Dialog<R> {
|
||||
pub(crate) fn app_handle(&self) -> &AppHandle<R> {
|
||||
self.0.app()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct FilePickerResponse {
|
||||
files: Vec<FileResponse>,
|
||||
}
|
||||
|
||||
pub fn pick_file<R: Runtime, F: FnOnce(Option<FileResponse>) + Send + 'static>(
|
||||
dialog: FileDialogBuilder<R>,
|
||||
f: F,
|
||||
) {
|
||||
std::thread::spawn(move || {
|
||||
let res = dialog
|
||||
.dialog
|
||||
.0
|
||||
.run_mobile_plugin::<FilePickerResponse>("showFilePicker", dialog.payload(false));
|
||||
if let Ok(response) = res {
|
||||
f(Some(response.files.into_iter().next().unwrap()))
|
||||
} else {
|
||||
f(None)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn pick_files<R: Runtime, F: FnOnce(Option<Vec<FileResponse>>) + Send + 'static>(
|
||||
dialog: FileDialogBuilder<R>,
|
||||
f: F,
|
||||
) {
|
||||
std::thread::spawn(move || {
|
||||
let res = dialog
|
||||
.dialog
|
||||
.0
|
||||
.run_mobile_plugin::<FilePickerResponse>("showFilePicker", dialog.payload(true));
|
||||
if let Ok(response) = res {
|
||||
f(Some(response.files))
|
||||
} else {
|
||||
f(None)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ShowMessageDialogResponse {
|
||||
#[allow(dead_code)]
|
||||
cancelled: bool,
|
||||
value: bool,
|
||||
}
|
||||
|
||||
/// Shows a message dialog
|
||||
pub fn show_message_dialog<R: Runtime, F: FnOnce(bool) + Send + 'static>(
|
||||
dialog: MessageDialogBuilder<R>,
|
||||
f: F,
|
||||
) {
|
||||
std::thread::spawn(move || {
|
||||
let res = dialog
|
||||
.dialog
|
||||
.0
|
||||
.run_mobile_plugin::<ShowMessageDialogResponse>("showMessageDialog", dialog.payload());
|
||||
f(res.map(|r| r.value).unwrap_or_default())
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
/// Types of message, ask and confirm dialogs.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum MessageDialogKind {
|
||||
/// Information dialog.
|
||||
Info,
|
||||
/// Warning dialog.
|
||||
Warning,
|
||||
/// Error dialog.
|
||||
Error,
|
||||
}
|
||||
|
||||
impl Default for MessageDialogKind {
|
||||
fn default() -> Self {
|
||||
Self::Info
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for MessageDialogKind {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
Ok(match s.to_lowercase().as_str() {
|
||||
"info" => MessageDialogKind::Info,
|
||||
"warning" => MessageDialogKind::Warning,
|
||||
"error" => MessageDialogKind::Error,
|
||||
_ => MessageDialogKind::Info,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for MessageDialogKind {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
Self::Info => serializer.serialize_str("info"),
|
||||
Self::Warning => serializer.serialize_str("warning"),
|
||||
Self::Error => serializer.serialize_str("error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user