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}