feat(dialog): add `showFolderPicker`

pull/2322/head
mo0nbase 6 months ago
parent b63d724e85
commit ad5fae489c
No known key found for this signature in database
GPG Key ID: 3BAB6FEAAFF4E694

@ -0,0 +1,6 @@
---
"dialog": patch
"dialog-js": patch
---
Add support for folder picker on iOS.

@ -38,6 +38,14 @@ struct SaveFileDialogOptions: Decodable {
var defaultPath: String? var defaultPath: String?
} }
struct FolderPickerOptions: Decodable {
var title: String?
var defaultPath: String?
var multiple: Bool?
var recursive: Bool?
var canCreateDirectories: Bool?
}
class DialogPlugin: Plugin { class DialogPlugin: Plugin {
var filePickerController: FilePickerController! var filePickerController: FilePickerController!
@ -168,6 +176,47 @@ class DialogPlugin: Plugin {
} }
} }
@objc public func showFolderPicker(_ invoke: Invoke) throws {
let args = try invoke.parseArgs(FolderPickerOptions.self)
onFilePickerResult = { (event: FilePickerEvent) -> Void in
switch event {
case .selected(let urls):
invoke.resolve(["directories": urls])
case .cancelled:
invoke.resolve(["directories": nil])
case .error(let error):
invoke.reject(error)
}
}
DispatchQueue.main.async {
let picker: UIDocumentPickerViewController
if #available(iOS 14.0, *) {
picker = UIDocumentPickerViewController(forOpeningContentTypes: [.folder])
} else {
picker = UIDocumentPickerViewController(documentTypes: [kUTTypeFolder as String], in: .open)
}
if let title = args.title {
picker.title = title
}
if let defaultPath = args.defaultPath {
picker.directoryURL = URL(string: defaultPath)
}
picker.delegate = self.filePickerController
picker.allowsMultipleSelection = args.multiple ?? false
// Note: canCreateDirectories is only supported on macOS
// recursive is handled at the filesystem access level, not in the picker
picker.modalPresentationStyle = .fullScreen
self.presentViewController(picker)
}
}
private func presentViewController(_ viewControllerToPresent: UIViewController) { private func presentViewController(_ viewControllerToPresent: UIViewController) {
self.manager.viewController?.present(viewControllerToPresent, animated: true, completion: nil) self.manager.viewController?.present(viewControllerToPresent, animated: true, completion: nil)
} }

@ -7,6 +7,7 @@ import MobileCoreServices
import PhotosUI import PhotosUI
import Photos import Photos
import Tauri import Tauri
import UniformTypeIdentifiers
public class FilePickerController: NSObject { public class FilePickerController: NSObject {
var plugin: DialogPlugin var plugin: DialogPlugin
@ -118,6 +119,15 @@ public class FilePickerController: NSObject {
extension FilePickerController: UIDocumentPickerDelegate { extension FilePickerController: UIDocumentPickerDelegate {
public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
// Check if this is a folder picker by examining the URLs
let isFolder = urls.first?.hasDirectoryPath ?? false
if isFolder {
self.plugin.onFilePickerEvent(.selected(urls))
return
}
// Handle regular files
do { do {
let temporaryUrls = try urls.map { try saveTemporaryFile($0) } let temporaryUrls = try urls.map { try saveTemporaryFile($0) }
self.plugin.onFilePickerEvent(.selected(temporaryUrls)) self.plugin.onFilePickerEvent(.selected(temporaryUrls))

@ -16,9 +16,9 @@ use crate::{
#[derive(Serialize)] #[derive(Serialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum OpenResponse { pub enum OpenResponse {
#[cfg(desktop)] #[cfg(any(desktop, target_os = "ios"))]
Folders(Option<Vec<FilePath>>), Folders(Option<Vec<FilePath>>),
#[cfg(desktop)] #[cfg(any(desktop, target_os = "ios"))]
Folder(Option<FilePath>), Folder(Option<FilePath>),
Files(Option<Vec<FilePath>>), Files(Option<Vec<FilePath>>),
File(Option<FilePath>), File(Option<FilePath>),
@ -133,7 +133,7 @@ pub(crate) async fn open<R: Runtime>(
} }
let res = if options.directory { let res = if options.directory {
#[cfg(desktop)] #[cfg(any(desktop, target_os = "ios"))]
{ {
let tauri_scope = window.state::<tauri::scope::Scopes>(); let tauri_scope = window.state::<tauri::scope::Scopes>();
@ -165,7 +165,7 @@ pub(crate) async fn open<R: Runtime>(
OpenResponse::Folder(folder.map(|p| p.simplified())) OpenResponse::Folder(folder.map(|p| p.simplified()))
} }
} }
#[cfg(mobile)] #[cfg(all(mobile, not(target_os = "ios")))]
return Err(crate::Error::FolderPickerNotImplemented); return Err(crate::Error::FolderPickerNotImplemented);
} else if options.multiple { } else if options.multiple {
let tauri_scope = window.state::<tauri::scope::Scopes>(); let tauri_scope = window.state::<tauri::scope::Scopes>();

@ -506,7 +506,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// Ok(()) /// Ok(())
/// }); /// });
/// ``` /// ```
#[cfg(desktop)] #[cfg(any(desktop, target_os = "ios"))]
pub fn pick_folder<F: FnOnce(Option<FilePath>) + Send + 'static>(self, f: F) { pub fn pick_folder<F: FnOnce(Option<FilePath>) + Send + 'static>(self, f: F) {
pick_folder(self, f) pick_folder(self, f)
} }
@ -528,7 +528,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// Ok(()) /// Ok(())
/// }); /// });
/// ``` /// ```
#[cfg(desktop)] #[cfg(any(desktop, target_os = "ios"))]
pub fn pick_folders<F: FnOnce(Option<Vec<FilePath>>) + Send + 'static>(self, f: F) { pub fn pick_folders<F: FnOnce(Option<Vec<FilePath>>) + Send + 'static>(self, f: F) {
pick_folders(self, f) pick_folders(self, f)
} }
@ -611,7 +611,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// // the folder path is `None` if the user closed the dialog /// // the folder path is `None` if the user closed the dialog
/// } /// }
/// ``` /// ```
#[cfg(desktop)] #[cfg(any(desktop, target_os = "ios"))]
pub fn blocking_pick_folder(self) -> Option<FilePath> { pub fn blocking_pick_folder(self) -> Option<FilePath> {
blocking_fn!(self, pick_folder) blocking_fn!(self, pick_folder)
} }
@ -631,7 +631,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// // the folder paths value is `None` if the user closed the dialog /// // the folder paths value is `None` if the user closed the dialog
/// } /// }
/// ``` /// ```
#[cfg(desktop)] #[cfg(any(desktop, target_os = "ios"))]
pub fn blocking_pick_folders(self) -> Option<Vec<FilePath>> { pub fn blocking_pick_folders(self) -> Option<Vec<FilePath>> {
blocking_fn!(self, pick_folders) blocking_fn!(self, pick_folders)
} }

@ -54,6 +54,11 @@ struct SaveFileResponse {
file: FilePath, file: FilePath,
} }
#[derive(Debug, Deserialize)]
struct FolderPickerResponse {
directories: Vec<FilePath>,
}
pub fn pick_file<R: Runtime, F: FnOnce(Option<FilePath>) + Send + 'static>( pub fn pick_file<R: Runtime, F: FnOnce(Option<FilePath>) + Send + 'static>(
dialog: FileDialogBuilder<R>, dialog: FileDialogBuilder<R>,
f: F, f: F,
@ -125,3 +130,39 @@ pub fn show_message_dialog<R: Runtime, F: FnOnce(bool) + Send + 'static>(
f(res.map(|r| r.value).unwrap_or_default()) f(res.map(|r| r.value).unwrap_or_default())
}); });
} }
#[cfg(target_os = "ios")]
pub fn pick_folders<R: Runtime, F: FnOnce(Option<Vec<FilePath>>) + Send + 'static>(
dialog: FileDialogBuilder<R>,
f: F,
) {
std::thread::spawn(move || {
let res = dialog
.dialog
.0
.run_mobile_plugin::<FolderPickerResponse>("showFolderPicker", dialog.payload(true));
if let Ok(response) = res {
f(Some(response.directories))
} else {
f(None)
}
});
}
#[cfg(target_os = "ios")]
pub fn pick_folder<R: Runtime, F: FnOnce(Option<FilePath>) + Send + 'static>(
dialog: FileDialogBuilder<R>,
f: F,
) {
std::thread::spawn(move || {
let res = dialog
.dialog
.0
.run_mobile_plugin::<FolderPickerResponse>("showFolderPicker", dialog.payload(false));
if let Ok(response) = res {
f(Some(response.directories.into_iter().next().unwrap()))
} else {
f(None)
}
});
}

Loading…
Cancel
Save