From 48800d885112bd8d1cc2e0204dd130854caa4796 Mon Sep 17 00:00:00 2001 From: mo0nbase <42557632+Mo0nbase@users.noreply.github.com> Date: Tue, 14 Jan 2025 19:20:24 -0500 Subject: [PATCH] feat(dialog): add `showFolderPicker` to android --- ...log-ios-fix.md => dialog-folder-mobile.md} | 0 .../android/src/main/java/DialogPlugin.kt | 59 +++++++++++++++++++ .../android/src/main/java/FilePickerUtils.kt | 16 +++++ plugins/dialog/src/commands.rs | 31 +++++----- plugins/dialog/src/error.rs | 3 - plugins/dialog/src/lib.rs | 8 +-- plugins/dialog/src/mobile.rs | 4 +- 7 files changed, 95 insertions(+), 26 deletions(-) rename .changes/{dialog-ios-fix.md => dialog-folder-mobile.md} (100%) diff --git a/.changes/dialog-ios-fix.md b/.changes/dialog-folder-mobile.md similarity index 100% rename from .changes/dialog-ios-fix.md rename to .changes/dialog-folder-mobile.md diff --git a/plugins/dialog/android/src/main/java/DialogPlugin.kt b/plugins/dialog/android/src/main/java/DialogPlugin.kt index af0467d8..1aec3735 100644 --- a/plugins/dialog/android/src/main/java/DialogPlugin.kt +++ b/plugins/dialog/android/src/main/java/DialogPlugin.kt @@ -47,6 +47,14 @@ class SaveFileDialogOptions { lateinit var filters: Array } +@InvokeArg +class FolderPickerOptions { + var title: String? = null + var multiple: Boolean? = null + var recursive: Boolean? = null + var canCreateDirectories: Boolean? = null +} + @TauriPlugin class DialogPlugin(private val activity: Activity): Plugin(activity) { var filePickerOptions: FilePickerOptions? = null @@ -249,4 +257,55 @@ class DialogPlugin(private val activity: Activity): Plugin(activity) { invoke.reject(message) } } + + @Command + fun showFolderPicker(invoke: Invoke) { + try { + val args = invoke.parseArgs(FolderPickerOptions::class.java) + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + + if (args.title != null) { + intent.putExtra(Intent.EXTRA_TITLE, args.title) + } + + // Note: Android's document tree picker doesn't support multiple selection natively + // recursive and canCreateDirectories are handled at filesystem level + + startActivityForResult(invoke, intent, "folderPickerResult") + } catch (ex: Exception) { + val message = ex.message ?: "Failed to pick folder" + Logger.error(message) + invoke.reject(message) + } + } + + @ActivityCallback + fun folderPickerResult(invoke: Invoke, result: ActivityResult) { + try { + when (result.resultCode) { + Activity.RESULT_OK -> { + val callResult = createFolderPickerResult(result.data) + invoke.resolve(callResult) + } + Activity.RESULT_CANCELED -> invoke.reject("Folder picker cancelled") + else -> invoke.reject("Failed to pick folder") + } + } catch (ex: java.lang.Exception) { + val message = ex.message ?: "Failed to read folder pick result" + Logger.error(message) + invoke.reject(message) + } + } + + private fun createFolderPickerResult(data: Intent?): JSObject { + val callResult = JSObject() + if (data == null) { + callResult.put("directories", null) + return callResult + } + val uri = data.data + val directories = JSArray.from(arrayOf(uri.toString())) + callResult.put("directories", directories) + return callResult + } } \ No newline at end of file diff --git a/plugins/dialog/android/src/main/java/FilePickerUtils.kt b/plugins/dialog/android/src/main/java/FilePickerUtils.kt index 7029d4c6..33932a44 100644 --- a/plugins/dialog/android/src/main/java/FilePickerUtils.kt +++ b/plugins/dialog/android/src/main/java/FilePickerUtils.kt @@ -198,6 +198,22 @@ class FilePickerUtils { } return os.toByteArray() } + + private fun isTreeUri(uri: Uri): Boolean { + return DocumentsContract.isTreeUri(uri) + } + + fun getTreePathFromUri(context: Context, uri: Uri): String? { + if (!isTreeUri(uri)) return null + + val docId = DocumentsContract.getTreeDocumentId(uri) + val split = docId.split(":") + return if ("primary".equals(split[0], ignoreCase = true)) { + "${Environment.getExternalStorageDirectory()}/${split[1]}" + } else { + null + } + } } } diff --git a/plugins/dialog/src/commands.rs b/plugins/dialog/src/commands.rs index 915fd711..728a855b 100644 --- a/plugins/dialog/src/commands.rs +++ b/plugins/dialog/src/commands.rs @@ -16,9 +16,9 @@ use crate::{ #[derive(Serialize)] #[serde(untagged)] pub enum OpenResponse { - #[cfg(any(desktop, target_os = "ios"))] + #[cfg(any(desktop, target_os = "ios", target_os = "android"))] Folders(Option>), - #[cfg(any(desktop, target_os = "ios"))] + #[cfg(any(desktop, target_os = "ios", target_os = "android"))] Folder(Option), Files(Option>), File(Option), @@ -132,8 +132,8 @@ pub(crate) async fn open( dialog_builder = dialog_builder.add_filter(filter.name, &extensions); } - let res = if options.directory { - #[cfg(any(desktop, target_os = "ios"))] + if options.directory { + #[cfg(any(desktop, target_os = "ios", target_os = "android"))] { let tauri_scope = window.state::(); @@ -149,9 +149,9 @@ pub(crate) async fn open( } } } - OpenResponse::Folders( + return Ok(OpenResponse::Folders( folders.map(|folders| folders.into_iter().map(|p| p.simplified()).collect()), - ) + )); } else { let folder = dialog_builder.blocking_pick_folder(); if let Some(folder) = &folder { @@ -162,14 +162,14 @@ pub(crate) async fn open( tauri_scope.allow_directory(&path, options.directory)?; } } - OpenResponse::Folder(folder.map(|p| p.simplified())) + return Ok(OpenResponse::Folder(folder.map(|p| p.simplified()))); } } - #[cfg(all(mobile, not(target_os = "ios")))] - return Err(crate::Error::FolderPickerNotImplemented); - } else if options.multiple { - let tauri_scope = window.state::(); + } + // Handle file selection + if options.multiple { + let tauri_scope = window.state::(); let files = dialog_builder.blocking_pick_files(); if let Some(files) = &files { for file in files { @@ -177,16 +177,14 @@ pub(crate) async fn open( if let Some(s) = window.try_fs_scope() { s.allow_file(&path)?; } - tauri_scope.allow_file(&path)?; } } } - OpenResponse::Files(files.map(|files| files.into_iter().map(|f| f.simplified()).collect())) + Ok(OpenResponse::Files(files.map(|files| files.into_iter().map(|f| f.simplified()).collect()))) } else { let tauri_scope = window.state::(); let file = dialog_builder.blocking_pick_file(); - if let Some(file) = &file { if let Ok(path) = file.clone().into_path() { if let Some(s) = window.try_fs_scope() { @@ -195,9 +193,8 @@ pub(crate) async fn open( tauri_scope.allow_file(&path)?; } } - OpenResponse::File(file.map(|f| f.simplified())) - }; - Ok(res) + Ok(OpenResponse::File(file.map(|f| f.simplified()))) + } } #[allow(unused_variables)] diff --git a/plugins/dialog/src/error.rs b/plugins/dialog/src/error.rs index 0c3ed5b8..8d46b32d 100644 --- a/plugins/dialog/src/error.rs +++ b/plugins/dialog/src/error.rs @@ -16,9 +16,6 @@ pub enum Error { #[cfg(mobile)] #[error(transparent)] PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), - #[cfg(mobile)] - #[error("Folder picker is not implemented on mobile")] - FolderPickerNotImplemented, #[error(transparent)] Fs(#[from] tauri_plugin_fs::Error), } diff --git a/plugins/dialog/src/lib.rs b/plugins/dialog/src/lib.rs index 5f2aa49a..e489e0d6 100644 --- a/plugins/dialog/src/lib.rs +++ b/plugins/dialog/src/lib.rs @@ -506,7 +506,7 @@ impl FileDialogBuilder { /// Ok(()) /// }); /// ``` - #[cfg(any(desktop, target_os = "ios"))] + #[cfg(any(desktop, target_os = "ios", target_os = "android"))] pub fn pick_folder) + Send + 'static>(self, f: F) { pick_folder(self, f) } @@ -528,7 +528,7 @@ impl FileDialogBuilder { /// Ok(()) /// }); /// ``` - #[cfg(any(desktop, target_os = "ios"))] + #[cfg(any(desktop, target_os = "ios", target_os = "android"))] pub fn pick_folders>) + Send + 'static>(self, f: F) { pick_folders(self, f) } @@ -611,7 +611,7 @@ impl FileDialogBuilder { /// // the folder path is `None` if the user closed the dialog /// } /// ``` - #[cfg(any(desktop, target_os = "ios"))] + #[cfg(any(desktop, target_os = "ios", target_os = "android"))] pub fn blocking_pick_folder(self) -> Option { blocking_fn!(self, pick_folder) } @@ -631,7 +631,7 @@ impl FileDialogBuilder { /// // the folder paths value is `None` if the user closed the dialog /// } /// ``` - #[cfg(any(desktop, target_os = "ios"))] + #[cfg(any(desktop, target_os = "ios", target_os = "android"))] pub fn blocking_pick_folders(self) -> Option> { blocking_fn!(self, pick_folders) } diff --git a/plugins/dialog/src/mobile.rs b/plugins/dialog/src/mobile.rs index a2e4e29c..ebe156e4 100644 --- a/plugins/dialog/src/mobile.rs +++ b/plugins/dialog/src/mobile.rs @@ -131,7 +131,7 @@ pub fn show_message_dialog( }); } -#[cfg(target_os = "ios")] +#[cfg(mobile)] pub fn pick_folders>) + Send + 'static>( dialog: FileDialogBuilder, f: F, @@ -149,7 +149,7 @@ pub fn pick_folders>) + Send + 'stati }); } -#[cfg(target_os = "ios")] +#[cfg(mobile)] pub fn pick_folder) + Send + 'static>( dialog: FileDialogBuilder, f: F,