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