tauri_plugin_advanced_file_manager/dialog/
mod.rs

1// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5//! Native system dialogs for opening and saving files along with message dialogs.
6
7use serde::{Deserialize, Serialize};
8use tauri::{
9    plugin::{Builder, TauriPlugin},
10    Manager, Runtime,
11};
12
13use std::{
14    path::{Path, PathBuf},
15    sync::mpsc::sync_channel,
16};
17
18pub use models::*;
19
20// 使用主模块的 FilePath
21pub use crate::FilePath;
22#[cfg(desktop)]
23pub(crate) mod desktop;
24#[cfg(mobile)]
25pub(crate) mod mobile;
26
27pub(crate) mod commands;
28mod error;
29mod models;
30
31pub use error::{Error, Result};
32
33#[cfg(desktop)]
34use desktop::*;
35#[cfg(mobile)]
36use mobile::*;
37
38#[cfg(desktop)]
39pub use desktop::Dialog;
40#[cfg(mobile)]
41pub use mobile::Dialog;
42
43#[derive(Debug, Serialize, Deserialize, Clone)]
44#[serde(rename_all = "lowercase")]
45pub enum PickerMode {
46    Document,
47    Media,
48    Image,
49    Video,
50}
51
52pub(crate) const OK: &str = "Ok";
53pub(crate) const CANCEL: &str = "Cancel";
54pub(crate) const YES: &str = "Yes";
55pub(crate) const NO: &str = "No";
56
57macro_rules! blocking_fn {
58    ($self:ident, $fn:ident) => {{
59        let (tx, rx) = sync_channel(0);
60        let cb = move |response| {
61            tx.send(response).unwrap();
62        };
63        $self.$fn(cb);
64        rx.recv().unwrap()
65    }};
66}
67
68/// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the dialog APIs.
69pub trait DialogExt<R: Runtime> {
70    fn dialog(&self) -> &Dialog<R>;
71}
72
73impl<R: Runtime, T: Manager<R>> crate::DialogExt<R> for T {
74    fn dialog(&self) -> &Dialog<R> {
75        self.state::<Dialog<R>>().inner()
76    }
77}
78
79impl<R: Runtime> Dialog<R> {
80    /// Create a new messaging dialog builder.
81    /// The dialog can optionally ask the user for confirmation or include an OK button.
82    ///
83    /// # Examples
84    ///
85    /// - Message dialog:
86    ///
87    /// ```
88    /// use tauri_plugin_advanced_file_manager::DialogExt;
89    ///
90    /// tauri::Builder::default()
91    ///   .setup(|app| {
92    ///     app
93    ///       .dialog()
94    ///       .message("Tauri is Awesome!")
95    ///       .show(|_| {
96    ///         println!("dialog closed");
97    ///       });
98    ///     Ok(())
99    ///   });
100    /// ```
101    ///
102    /// - Ask dialog:
103    ///
104    /// ```
105    /// use tauri_plugin_advanced_file_manager::{DialogExt, MessageDialogButtons};
106    ///
107    /// tauri::Builder::default()
108    ///   .setup(|app| {
109    ///     app.dialog()
110    ///       .message("Are you sure?")
111    ///       .buttons(MessageDialogButtons::OkCancelCustom("Yes".to_string(), "No".to_string()))
112    ///       .show(|yes| {
113    ///         println!("user said {}", if yes { "yes" } else { "no" });
114    ///       });
115    ///     Ok(())
116    ///   });
117    /// ```
118    ///
119    /// - Message dialog with OK button:
120    ///
121    /// ```
122    /// use tauri_plugin_advanced_file_manager::{DialogExt, MessageDialogButtons};
123    ///
124    /// tauri::Builder::default()
125    ///   .setup(|app| {
126    ///     app.dialog()
127    ///       .message("Job completed successfully")
128    ///       .buttons(MessageDialogButtons::Ok)
129    ///       .show(|_| {
130    ///         println!("dialog closed");
131    ///       });
132    ///     Ok(())
133    ///   });
134    /// ```
135    ///
136    /// # `show` vs `blocking_show`
137    ///
138    /// The dialog builder includes two separate APIs for rendering the dialog: `show` and `blocking_show`.
139    /// The `show` function is asynchronous and takes a closure to be executed when the dialog is closed.
140    /// To block the current thread until the user acted on the dialog, you can use `blocking_show`,
141    /// but note that it cannot be executed on the main thread as it will freeze your application.
142    ///
143    /// ```
144    /// use tauri_plugin_advanced_file_manager::{DialogExt, MessageDialogButtons};
145    ///
146    /// tauri::Builder::default()
147    ///   .setup(|app| {
148    ///     let handle = app.handle().clone();
149    ///     std::thread::spawn(move || {
150    ///       let yes = handle.dialog()
151    ///         .message("Are you sure?")
152    ///         .buttons(MessageDialogButtons::OkCancelCustom("Yes".to_string(), "No".to_string()))
153    ///         .blocking_show();
154    ///     });
155    ///
156    ///     Ok(())
157    ///   });
158    /// ```
159    pub fn message(&self, message: impl Into<String>) -> MessageDialogBuilder<R> {
160        MessageDialogBuilder::new(
161            self.clone(),
162            self.app_handle().package_info().name.clone(),
163            message,
164        )
165    }
166
167    /// Creates a new builder for dialogs that lets the user select file(s) or folder(s).
168    pub fn file(&self) -> FileDialogBuilder<R> {
169        FileDialogBuilder::new(self.clone())
170    }
171}
172
173/// Initializes the plugin.
174pub fn init<R: Runtime>() -> TauriPlugin<R> {
175    #[allow(unused_mut)]
176    let mut builder = Builder::new("dialog");
177
178    // Dialogs are implemented natively on Android
179    #[cfg(not(target_os = "android"))]
180    {
181        builder = builder.js_init_script(include_str!("init-iife.js").to_string());
182    }
183
184    builder
185        .invoke_handler(tauri::generate_handler![
186            commands::dialog_open,
187            commands::save,
188            commands::message,
189            commands::ask,
190            commands::confirm
191        ])
192        .setup(|app, api| {
193            #[cfg(mobile)]
194            let dialog = mobile::init(app, api)?;
195            #[cfg(desktop)]
196            let dialog = desktop::init(app, api)?;
197            app.manage(dialog);
198            Ok(())
199        })
200        .build()
201}
202
203/// A builder for message dialogs.
204pub struct MessageDialogBuilder<R: Runtime> {
205    #[allow(dead_code)]
206    pub(crate) dialog: Dialog<R>,
207    pub(crate) title: String,
208    pub(crate) message: String,
209    pub(crate) kind: MessageDialogKind,
210    pub(crate) buttons: MessageDialogButtons,
211    #[cfg(desktop)]
212    pub(crate) parent: Option<desktop::WindowHandle>,
213}
214
215/// Payload for the message dialog mobile API.
216#[cfg(mobile)]
217#[derive(Serialize)]
218#[serde(rename_all = "camelCase")]
219pub(crate) struct MessageDialogPayload<'a> {
220    title: &'a String,
221    message: &'a String,
222    kind: &'a MessageDialogKind,
223    ok_button_label: Option<&'a str>,
224    no_button_label: Option<&'a str>,
225    cancel_button_label: Option<&'a str>,
226}
227
228// raw window handle :(
229unsafe impl<R: Runtime> Send for MessageDialogBuilder<R> {}
230
231impl<R: Runtime> MessageDialogBuilder<R> {
232    /// Creates a new message dialog builder.
233    pub fn new(dialog: Dialog<R>, title: impl Into<String>, message: impl Into<String>) -> Self {
234        Self {
235            dialog,
236            title: title.into(),
237            message: message.into(),
238            kind: Default::default(),
239            buttons: Default::default(),
240            #[cfg(desktop)]
241            parent: None,
242        }
243    }
244
245    #[cfg(mobile)]
246    pub(crate) fn payload(&self) -> MessageDialogPayload<'_> {
247        let (ok_button_label, no_button_label, cancel_button_label) = match &self.buttons {
248            MessageDialogButtons::Ok => (Some(OK), None, None),
249            MessageDialogButtons::OkCancel => (Some(OK), None, Some(CANCEL)),
250            MessageDialogButtons::YesNo => (Some(YES), Some(NO), None),
251            MessageDialogButtons::YesNoCancel => (Some(YES), Some(NO), Some(CANCEL)),
252            MessageDialogButtons::OkCustom(ok) => (Some(ok.as_str()), None, None),
253            MessageDialogButtons::OkCancelCustom(ok, cancel) => {
254                (Some(ok.as_str()), None, Some(cancel.as_str()))
255            }
256            MessageDialogButtons::YesNoCancelCustom(yes, no, cancel) => {
257                (Some(yes.as_str()), Some(no.as_str()), Some(cancel.as_str()))
258            }
259        };
260        MessageDialogPayload {
261            title: &self.title,
262            message: &self.message,
263            kind: &self.kind,
264            ok_button_label,
265            no_button_label,
266            cancel_button_label,
267        }
268    }
269
270    /// Sets the dialog title.
271    pub fn title(mut self, title: impl Into<String>) -> Self {
272        self.title = title.into();
273        self
274    }
275
276    /// Set parent windows explicitly (optional)
277    #[cfg(desktop)]
278    pub fn parent<W: raw_window_handle::HasWindowHandle + raw_window_handle::HasDisplayHandle>(
279        mut self,
280        parent: &W,
281    ) -> Self {
282        if let (Ok(window_handle), Ok(display_handle)) =
283            (parent.window_handle(), parent.display_handle())
284        {
285            self.parent.replace(desktop::WindowHandle::new(
286                window_handle.as_raw(),
287                display_handle.as_raw(),
288            ));
289        }
290        self
291    }
292
293    /// Sets the dialog buttons.
294    pub fn buttons(mut self, buttons: MessageDialogButtons) -> Self {
295        self.buttons = buttons;
296        self
297    }
298
299    /// Set type of a dialog.
300    ///
301    /// Depending on the system it can result in type specific icon to show up,
302    /// the will inform user it message is a error, warning or just information.
303    pub fn kind(mut self, kind: MessageDialogKind) -> Self {
304        self.kind = kind;
305        self
306    }
307
308    /// Shows a message dialog
309    ///
310    /// Returns `true` if the user pressed the OK/Yes button,
311    pub fn show<F: FnOnce(bool) + Send + 'static>(self, f: F) {
312        let ok_label = match &self.buttons {
313            MessageDialogButtons::OkCustom(ok) => Some(ok.clone()),
314            MessageDialogButtons::OkCancelCustom(ok, _) => Some(ok.clone()),
315            MessageDialogButtons::YesNoCancelCustom(yes, _, _) => Some(yes.clone()),
316            _ => None,
317        };
318
319        show_message_dialog(self, move |res| {
320            let sucess = match res {
321                MessageDialogResult::Ok | MessageDialogResult::Yes => true,
322                MessageDialogResult::Custom(s) => {
323                    ok_label.map_or(s == OK, |ok_label| ok_label == s)
324                }
325                _ => false,
326            };
327
328            f(sucess)
329        })
330    }
331
332    /// Shows a message dialog and returns the button that was pressed.
333    ///
334    /// Returns a [`MessageDialogResult`] enum that indicates which button was pressed.
335    pub fn show_with_result<F: FnOnce(MessageDialogResult) + Send + 'static>(self, f: F) {
336        show_message_dialog(self, f)
337    }
338
339    /// Shows a message dialog.
340    ///
341    /// Returns `true` if the user pressed the OK/Yes button,
342    ///
343    /// This is a blocking operation,
344    /// and should *NOT* be used when running on the main thread context.
345    pub fn blocking_show(self) -> bool {
346        blocking_fn!(self, show)
347    }
348
349    /// Shows a message dialog and returns the button that was pressed.
350    ///
351    /// Returns a [`MessageDialogResult`] enum that indicates which button was pressed.
352    ///
353    /// This is a blocking operation,
354    /// and should *NOT* be used when running on the main thread context.
355    pub fn blocking_show_with_result(self) -> MessageDialogResult {
356        blocking_fn!(self, show_with_result)
357    }
358}
359#[derive(Debug, Serialize)]
360pub(crate) struct Filter {
361    pub name: String,
362    pub extensions: Vec<String>,
363}
364
365/// The file dialog builder.
366///
367/// Constructs file picker dialogs that can select single/multiple files or directories.
368#[derive(Debug)]
369pub struct FileDialogBuilder<R: Runtime> {
370    #[allow(dead_code)]
371    pub(crate) dialog: Dialog<R>,
372    pub(crate) filters: Vec<Filter>,
373    pub(crate) starting_directory: Option<PathBuf>,
374    pub(crate) file_name: Option<String>,
375    pub(crate) title: Option<String>,
376    pub(crate) can_create_directories: Option<bool>,
377    pub(crate) picker_mode: Option<PickerMode>,
378    #[cfg(desktop)]
379    pub(crate) parent: Option<desktop::WindowHandle>,
380}
381
382#[cfg(mobile)]
383#[derive(Serialize)]
384#[serde(rename_all = "camelCase")]
385pub(crate) struct FileDialogPayload<'a> {
386    file_name: &'a Option<String>,
387    filters: &'a Vec<Filter>,
388    multiple: bool,
389    picker_mode: &'a Option<PickerMode>,
390}
391
392// raw window handle :(
393unsafe impl<R: Runtime> Send for FileDialogBuilder<R> {}
394
395impl<R: Runtime> FileDialogBuilder<R> {
396    /// Gets the default file dialog builder.
397    pub fn new(dialog: Dialog<R>) -> Self {
398        Self {
399            dialog,
400            filters: Vec::new(),
401            starting_directory: None,
402            file_name: None,
403            title: None,
404            can_create_directories: None,
405            picker_mode: None,
406            #[cfg(desktop)]
407            parent: None,
408        }
409    }
410
411    #[cfg(mobile)]
412    pub(crate) fn payload(&self, multiple: bool) -> FileDialogPayload<'_> {
413        FileDialogPayload {
414            file_name: &self.file_name,
415            filters: &self.filters,
416            multiple,
417            picker_mode: &self.picker_mode,
418        }
419    }
420
421    /// Add file extension filter. Takes in the name of the filter, and list of extensions
422    #[must_use]
423    pub fn add_filter(mut self, name: impl Into<String>, extensions: &[&str]) -> Self {
424        self.filters.push(Filter {
425            name: name.into(),
426            extensions: extensions.iter().map(|e| e.to_string()).collect(),
427        });
428        self
429    }
430
431    /// Set starting directory of the dialog.
432    #[must_use]
433    pub fn set_directory<P: AsRef<Path>>(mut self, directory: P) -> Self {
434        self.starting_directory.replace(directory.as_ref().into());
435        self
436    }
437
438    /// Set starting file name of the dialog.
439    #[must_use]
440    pub fn set_file_name(mut self, file_name: impl Into<String>) -> Self {
441        self.file_name.replace(file_name.into());
442        self
443    }
444
445    /// Sets the parent window of the dialog.
446    #[cfg(desktop)]
447    #[must_use]
448    pub fn set_parent<
449        W: raw_window_handle::HasWindowHandle + raw_window_handle::HasDisplayHandle,
450    >(
451        mut self,
452        parent: &W,
453    ) -> Self {
454        if let (Ok(window_handle), Ok(display_handle)) =
455            (parent.window_handle(), parent.display_handle())
456        {
457            self.parent.replace(desktop::WindowHandle::new(
458                window_handle.as_raw(),
459                display_handle.as_raw(),
460            ));
461        }
462        self
463    }
464
465    /// Set the title of the dialog.
466    #[must_use]
467    pub fn set_title(mut self, title: impl Into<String>) -> Self {
468        self.title.replace(title.into());
469        self
470    }
471
472    /// Set whether it should be possible to create new directories in the dialog. Enabled by default. **macOS only**.
473    pub fn set_can_create_directories(mut self, can: bool) -> Self {
474        self.can_create_directories.replace(can);
475        self
476    }
477
478    /// Set the picker mode of the dialog.
479    /// This is meant for mobile platforms (iOS and Android) which have distinct file and media pickers.
480    /// On desktop, this option is ignored.
481    /// If not provided, the dialog will automatically choose the best mode based on the MIME types of the filters.
482    pub fn set_picker_mode(mut self, mode: PickerMode) -> Self {
483        self.picker_mode.replace(mode);
484        self
485    }
486
487    /// Shows the dialog to select a single file.
488    ///
489    /// This is not a blocking operation,
490    /// and should be used when running on the main thread to avoid deadlocks with the event loop.
491    ///
492    /// See [`Self::blocking_pick_file`] for a blocking version for use in other contexts.
493    ///
494    /// # Examples
495    ///
496    /// ```
497    /// use tauri_plugin_advanced_file_manager::DialogExt;
498    /// tauri::Builder::default()
499    ///   .setup(|app| {
500    ///     app.dialog().file().pick_file(|file_path| {
501    ///       // do something with the optional file path here
502    ///       // the file path is `None` if the user closed the dialog
503    ///     });
504    ///     Ok(())
505    ///   });
506    /// ```
507    pub fn pick_file<F: FnOnce(Option<FilePath>) + Send + 'static>(self, f: F) {
508        pick_file(self, f)
509    }
510
511    /// Shows the dialog to select multiple files.
512    ///
513    /// This is not a blocking operation,
514    /// and should be used when running on the main thread to avoid deadlocks with the event loop.
515    ///
516    /// See [`Self::blocking_pick_files`] for a blocking version for use in other contexts.
517    ///
518    /// # Reading the files
519    ///
520    /// The file paths cannot be read directly on Android as they are behind a content URI.
521    /// The recommended way to read the files is using the [`fs`](https://v2.tauri.app/plugin/file-system/) plugin:
522    ///
523    /// ```
524    /// use tauri_plugin_advanced_file_manager::DialogExt;
525    /// use tauri_plugin_advanced_file_manager::FsExt;
526    /// tauri::Builder::default()
527    ///   .setup(|app| {
528    ///     let handle = app.handle().clone();
529    ///     app.dialog().file().pick_file(move |file_path| {
530    ///       let Some(path) = file_path else { return };
531    ///       let Ok(contents) = handle.fs().read_to_string(path) else {
532    ///         eprintln!("failed to read file, <todo add error handling!>");
533    ///         return;
534    ///       };
535    ///     });
536    ///     Ok(())
537    ///   });
538    /// ```
539    ///
540    /// See <https://developer.android.com/guide/topics/providers/content-provider-basics> for more information.
541    ///
542    /// # Examples
543    ///
544    /// ```
545    /// use tauri_plugin_advanced_file_manager::DialogExt;
546    /// tauri::Builder::default()
547    ///   .setup(|app| {
548    ///     app.dialog().file().pick_files(|file_paths| {
549    ///       // do something with the optional file paths here
550    ///       // the file paths value is `None` if the user closed the dialog
551    ///     });
552    ///     Ok(())
553    ///   });
554    /// ```
555    pub fn pick_files<F: FnOnce(Option<Vec<FilePath>>) + Send + 'static>(self, f: F) {
556        pick_files(self, f)
557    }
558
559    /// Shows the dialog to select a single folder.
560    ///
561    /// This is not a blocking operation,
562    /// and should be used when running on the main thread to avoid deadlocks with the event loop.
563    ///
564    /// See [`Self::blocking_pick_folder`] for a blocking version for use in other contexts.
565    ///
566    /// # Examples
567    ///
568    /// ```
569    /// use tauri_plugin_advanced_file_manager::DialogExt;
570    /// tauri::Builder::default()
571    ///   .setup(|app| {
572    ///     app.dialog().file().pick_folder(|folder_path| {
573    ///       // do something with the optional folder path here
574    ///       // the folder path is `None` if the user closed the dialog
575    ///     });
576    ///     Ok(())
577    ///   });
578    /// ```
579    #[cfg(desktop)]
580    pub fn pick_folder<F: FnOnce(Option<FilePath>) + Send + 'static>(self, f: F) {
581        pick_folder(self, f)
582    }
583
584    /// Shows the dialog to select multiple folders.
585    ///
586    /// This is not a blocking operation,
587    /// and should be used when running on the main thread to avoid deadlocks with the event loop.
588    ///
589    /// See [`Self::blocking_pick_folders`] for a blocking version for use in other contexts.
590    ///
591    /// # Examples
592    ///
593    /// ```
594    /// use tauri_plugin_advanced_file_manager::DialogExt;
595    /// tauri::Builder::default()
596    ///   .setup(|app| {
597    ///     app.dialog().file().pick_folders(|file_paths| {
598    ///       // do something with the optional folder paths here
599    ///       // the folder paths value is `None` if the user closed the dialog
600    ///     });
601    ///     Ok(())
602    ///   });
603    /// ```
604    #[cfg(desktop)]
605    pub fn pick_folders<F: FnOnce(Option<Vec<FilePath>>) + Send + 'static>(self, f: F) {
606        pick_folders(self, f)
607    }
608
609    /// Shows the dialog to save a file.
610    ///
611    /// This is not a blocking operation,
612    /// and should be used when running on the main thread to avoid deadlocks with the event loop.
613    ///
614    /// See [`Self::blocking_save_file`] for a blocking version for use in other contexts.
615    ///
616    /// # Examples
617    ///
618    /// ```
619    /// use tauri_plugin_advanced_file_manager::DialogExt;
620    /// tauri::Builder::default()
621    ///   .setup(|app| {
622    ///     app.dialog().file().save_file(|file_path| {
623    ///       // do something with the optional file path here
624    ///       // the file path is `None` if the user closed the dialog
625    ///     });
626    ///     Ok(())
627    ///   });
628    /// ```
629    pub fn save_file<F: FnOnce(Option<FilePath>) + Send + 'static>(self, f: F) {
630        save_file(self, f)
631    }
632}
633
634/// Blocking APIs.
635impl<R: Runtime> FileDialogBuilder<R> {
636    /// Shows the dialog to select a single file.
637    ///
638    /// This is a blocking operation,
639    /// and should *NOT* be used when running on the main thread.
640    ///
641    /// See [`Self::pick_file`] for a non-blocking version for use in main-thread contexts.
642    ///
643    /// # Examples
644    ///
645    /// ```
646    /// use tauri_plugin_advanced_file_manager::DialogExt;
647    /// #[tauri::command]
648    /// async fn my_command(app: tauri::AppHandle) {
649    ///   let file_path = app.dialog().file().blocking_pick_file();
650    ///   // do something with the optional file path here
651    ///   // the file path is `None` if the user closed the dialog
652    /// }
653    /// ```
654    pub fn blocking_pick_file(self) -> Option<FilePath> {
655        blocking_fn!(self, pick_file)
656    }
657
658    /// Shows the dialog to select multiple files.
659    ///
660    /// This is a blocking operation,
661    /// and should *NOT* be used when running on the main thread.
662    ///
663    /// See [`Self::pick_files`] for a non-blocking version for use in main-thread contexts.
664    ///
665    /// # Examples
666    ///
667    /// ```
668    /// use tauri_plugin_advanced_file_manager::DialogExt;
669    /// #[tauri::command]
670    /// async fn my_command(app: tauri::AppHandle) {
671    ///   let file_path = app.dialog().file().blocking_pick_files();
672    ///   // do something with the optional file paths here
673    ///   // the file paths value is `None` if the user closed the dialog
674    /// }
675    /// ```
676    pub fn blocking_pick_files(self) -> Option<Vec<FilePath>> {
677        blocking_fn!(self, pick_files)
678    }
679
680    /// Shows the dialog to select a single folder.
681    ///
682    /// This is a blocking operation,
683    /// and should *NOT* be used when running on the main thread.
684    ///
685    /// See [`Self::pick_folder`] for a non-blocking version for use in main-thread contexts.
686    ///
687    /// # Examples
688    ///
689    /// ```
690    /// use tauri_plugin_advanced_file_manager::DialogExt;
691    /// #[tauri::command]
692    /// async fn my_command(app: tauri::AppHandle) {
693    ///   let folder_path = app.dialog().file().blocking_pick_folder();
694    ///   // do something with the optional folder path here
695    ///   // the folder path is `None` if the user closed the dialog
696    /// }
697    /// ```
698    #[cfg(desktop)]
699    pub fn blocking_pick_folder(self) -> Option<FilePath> {
700        blocking_fn!(self, pick_folder)
701    }
702
703    /// Shows the dialog to select multiple folders.
704    ///
705    /// This is a blocking operation,
706    /// and should *NOT* be used when running on the main thread.
707    ///
708    /// See [`Self::pick_folders`] for a non-blocking version for use in main-thread contexts.
709    ///
710    /// # Examples
711    ///
712    /// ```
713    /// use tauri_plugin_advanced_file_manager::DialogExt;
714    /// #[tauri::command]
715    /// async fn my_command(app: tauri::AppHandle) {
716    ///   let folder_paths = app.dialog().file().blocking_pick_folders();
717    ///   // do something with the optional folder paths here
718    ///   // the folder paths value is `None` if the user closed the dialog
719    /// }
720    /// ```
721    #[cfg(desktop)]
722    pub fn blocking_pick_folders(self) -> Option<Vec<FilePath>> {
723        blocking_fn!(self, pick_folders)
724    }
725
726    /// Shows the dialog to save a file.
727    ///
728    /// This is a blocking operation,
729    /// and should *NOT* be used when running on the main thread.
730    ///
731    /// See [`Self::save_file`] for a non-blocking version for use in main-thread contexts.
732    ///
733    /// # Examples
734    ///
735    /// ```
736    /// use tauri_plugin_advanced_file_manager::DialogExt;
737    /// #[tauri::command]
738    /// async fn my_command(app: tauri::AppHandle) {
739    ///   let file_path = app.dialog().file().blocking_save_file();
740    ///   // do something with the optional file path here
741    ///   // the file path is `None` if the user closed the dialog
742    /// }
743    /// ```
744    pub fn blocking_save_file(self) -> Option<FilePath> {
745        blocking_fn!(self, save_file)
746    }
747}