// Copyright 2019-2023 Tauri Programme within The Commons Conservancy // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use std::path::PathBuf; use serde::{Deserialize, Serialize}; use tauri::{command, Manager, Runtime, State, Window}; use tauri_plugin_fs::FsExt; use crate::{Dialog, FileDialogBuilder, FileResponse, MessageDialogKind, Result}; #[derive(Serialize)] #[serde(untagged)] pub enum OpenResponse { #[cfg(desktop)] Folders(Option>), #[cfg(desktop)] Folder(Option), Files(Option>), File(Option), } #[allow(dead_code)] #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DialogFilter { name: String, extensions: Vec, } /// The options for the open dialog API. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct OpenDialogOptions { /// The title of the dialog window. title: Option, /// The filters of the dialog. #[serde(default)] filters: Vec, /// Whether the dialog allows multiple selection or not. #[serde(default)] multiple: bool, /// Whether the dialog is a directory selection (`true` value) or file selection (`false` value). #[serde(default)] directory: bool, /// The initial path of the dialog. default_path: Option, /// If [`Self::directory`] is true, indicates that it will be read recursively later. /// Defines whether subdirectories will be allowed on the scope or not. #[serde(default)] #[cfg_attr(mobile, allow(dead_code))] recursive: bool, /// Whether to allow creating directories in the dialog **macOS Only** can_create_directories: Option, } /// The options for the save dialog API. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(mobile, allow(dead_code))] pub struct SaveDialogOptions { /// The title of the dialog window. title: Option, /// The filters of the dialog. #[serde(default)] filters: Vec, /// The initial path of the dialog. default_path: Option, /// Whether to allow creating directories in the dialog **macOS Only** can_create_directories: Option, } fn set_default_path( mut dialog_builder: FileDialogBuilder, default_path: PathBuf, ) -> FileDialogBuilder { // we need to adjust the separator on Windows: https://github.com/tauri-apps/tauri/issues/8074 let default_path: PathBuf = default_path.components().collect(); if default_path.is_file() || !default_path.exists() { if let (Some(parent), Some(file_name)) = (default_path.parent(), default_path.file_name()) { if parent.components().count() > 0 { dialog_builder = dialog_builder.set_directory(parent); } dialog_builder = dialog_builder.set_file_name(file_name.to_string_lossy()); } else { dialog_builder = dialog_builder.set_directory(default_path); } dialog_builder } else { dialog_builder.set_directory(default_path) } } #[command] pub(crate) async fn open( window: Window, dialog: State<'_, Dialog>, options: OpenDialogOptions, ) -> Result { let mut dialog_builder = dialog.file(); #[cfg(any(windows, target_os = "macos"))] { dialog_builder = dialog_builder.set_parent(&window); } if let Some(title) = options.title { dialog_builder = dialog_builder.set_title(title); } if let Some(default_path) = options.default_path { dialog_builder = set_default_path(dialog_builder, default_path); } if let Some(can) = options.can_create_directories { dialog_builder = dialog_builder.set_can_create_directories(can); } for filter in options.filters { let extensions: Vec<&str> = filter.extensions.iter().map(|s| &**s).collect(); dialog_builder = dialog_builder.add_filter(filter.name, &extensions); } let res = if options.directory { #[cfg(desktop)] { if options.multiple { let folders = dialog_builder.blocking_pick_folders(); if let Some(folders) = &folders { for folder in folders { if let Some(s) = window.try_fs_scope() { s.allow_directory(folder, options.recursive); } } } OpenResponse::Folders(folders.map(|folders| { folders .iter() .map(|p| dunce::simplified(p).to_path_buf()) .collect() })) } else { let folder = dialog_builder.blocking_pick_folder(); if let Some(path) = &folder { if let Some(s) = window.try_fs_scope() { s.allow_directory(path, options.recursive); } } OpenResponse::Folder(folder.map(|p| dunce::simplified(&p).to_path_buf())) } } #[cfg(mobile)] return Err(crate::Error::FolderPickerNotImplemented); } else if options.multiple { let files = dialog_builder.blocking_pick_files(); if let Some(files) = &files { for file in files { if let Some(s) = window.try_fs_scope() { s.allow_file(&file.path); } window .state::() .allow_file(&file.path)?; } } OpenResponse::Files(files.map(|files| { files .into_iter() .map(|mut f| { f.path = dunce::simplified(&f.path).to_path_buf(); f }) .collect() })) } else { let file = dialog_builder.blocking_pick_file(); if let Some(file) = &file { if let Some(s) = window.try_fs_scope() { s.allow_file(&file.path); } window .state::() .allow_file(&file.path)?; } OpenResponse::File(file.map(|mut f| { f.path = dunce::simplified(&f.path).to_path_buf(); f })) }; Ok(res) } #[allow(unused_variables)] #[command] pub(crate) async fn save( window: Window, dialog: State<'_, Dialog>, options: SaveDialogOptions, ) -> Result> { #[cfg(mobile)] return Err(crate::Error::FileSaveDialogNotImplemented); #[cfg(desktop)] { let mut dialog_builder = dialog.file(); #[cfg(any(windows, target_os = "macos"))] { dialog_builder = dialog_builder.set_parent(&window); } if let Some(title) = options.title { dialog_builder = dialog_builder.set_title(title); } if let Some(default_path) = options.default_path { dialog_builder = set_default_path(dialog_builder, default_path); } if let Some(can) = options.can_create_directories { dialog_builder = dialog_builder.set_can_create_directories(can); } for filter in options.filters { let extensions: Vec<&str> = filter.extensions.iter().map(|s| &**s).collect(); dialog_builder = dialog_builder.add_filter(filter.name, &extensions); } let path = dialog_builder.blocking_save_file(); if let Some(p) = &path { if let Some(s) = window.try_fs_scope() { s.allow_file(p); } window.state::().allow_file(p)?; } Ok(path.map(|p| dunce::simplified(&p).to_path_buf())) } } fn message_dialog( #[allow(unused_variables)] window: Window, dialog: State<'_, Dialog>, title: Option, message: String, kind: Option, ok_button_label: Option, cancel_button_label: Option, ) -> bool { let mut builder = dialog.message(message); if let Some(title) = title { builder = builder.title(title); } #[cfg(any(windows, target_os = "macos"))] { builder = builder.parent(&window); } if let Some(kind) = kind { builder = builder.kind(kind); } if let Some(ok) = ok_button_label { builder = builder.ok_button_label(ok); } if let Some(cancel) = cancel_button_label { builder = builder.cancel_button_label(cancel); } builder.blocking_show() } #[command] pub(crate) async fn message( window: Window, dialog: State<'_, Dialog>, title: Option, message: String, kind: Option, ok_button_label: Option, ) -> Result { Ok(message_dialog( window, dialog, title, message, kind, ok_button_label, None, )) } #[command] pub(crate) async fn ask( window: Window, dialog: State<'_, Dialog>, title: Option, message: String, kind: Option, ok_button_label: Option, cancel_button_label: Option, ) -> Result { Ok(message_dialog( window, dialog, title, message, kind, Some(ok_button_label.unwrap_or_else(|| "Yes".into())), Some(cancel_button_label.unwrap_or_else(|| "No".into())), )) } #[command] pub(crate) async fn confirm( window: Window, dialog: State<'_, Dialog>, title: Option, message: String, kind: Option, ok_button_label: Option, cancel_button_label: Option, ) -> Result { Ok(message_dialog( window, dialog, title, message, kind, Some(ok_button_label.unwrap_or_else(|| "Ok".into())), Some(cancel_button_label.unwrap_or_else(|| "Cancel".into())), )) }