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