feat(dialog): add `showFolderPicker` to android

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

@ -47,6 +47,14 @@ class SaveFileDialogOptions {
lateinit var filters: Array<Filter> lateinit var filters: Array<Filter>
} }
@InvokeArg
class FolderPickerOptions {
var title: String? = null
var multiple: Boolean? = null
var recursive: Boolean? = null
var canCreateDirectories: Boolean? = null
}
@TauriPlugin @TauriPlugin
class DialogPlugin(private val activity: Activity): Plugin(activity) { class DialogPlugin(private val activity: Activity): Plugin(activity) {
var filePickerOptions: FilePickerOptions? = null var filePickerOptions: FilePickerOptions? = null
@ -249,4 +257,55 @@ class DialogPlugin(private val activity: Activity): Plugin(activity) {
invoke.reject(message) 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
}
} }

@ -198,6 +198,22 @@ class FilePickerUtils {
} }
return os.toByteArray() 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
}
}
} }
} }

@ -16,9 +16,9 @@ use crate::{
#[derive(Serialize)] #[derive(Serialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum OpenResponse { pub enum OpenResponse {
#[cfg(any(desktop, target_os = "ios"))] #[cfg(any(desktop, target_os = "ios", target_os = "android"))]
Folders(Option<Vec<FilePath>>), Folders(Option<Vec<FilePath>>),
#[cfg(any(desktop, target_os = "ios"))] #[cfg(any(desktop, target_os = "ios", target_os = "android"))]
Folder(Option<FilePath>), Folder(Option<FilePath>),
Files(Option<Vec<FilePath>>), Files(Option<Vec<FilePath>>),
File(Option<FilePath>), File(Option<FilePath>),
@ -132,8 +132,8 @@ pub(crate) async fn open<R: Runtime>(
dialog_builder = dialog_builder.add_filter(filter.name, &extensions); dialog_builder = dialog_builder.add_filter(filter.name, &extensions);
} }
let res = if options.directory { if options.directory {
#[cfg(any(desktop, target_os = "ios"))] #[cfg(any(desktop, target_os = "ios", target_os = "android"))]
{ {
let tauri_scope = window.state::<tauri::scope::Scopes>(); let tauri_scope = window.state::<tauri::scope::Scopes>();
@ -149,9 +149,9 @@ pub(crate) async fn open<R: Runtime>(
} }
} }
} }
OpenResponse::Folders( return Ok(OpenResponse::Folders(
folders.map(|folders| folders.into_iter().map(|p| p.simplified()).collect()), folders.map(|folders| folders.into_iter().map(|p| p.simplified()).collect()),
) ));
} else { } else {
let folder = dialog_builder.blocking_pick_folder(); let folder = dialog_builder.blocking_pick_folder();
if let Some(folder) = &folder { if let Some(folder) = &folder {
@ -162,14 +162,14 @@ pub(crate) async fn open<R: Runtime>(
tauri_scope.allow_directory(&path, options.directory)?; 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::<tauri::scope::Scopes>();
// Handle file selection
if options.multiple {
let tauri_scope = window.state::<tauri::scope::Scopes>();
let files = dialog_builder.blocking_pick_files(); let files = dialog_builder.blocking_pick_files();
if let Some(files) = &files { if let Some(files) = &files {
for file in files { for file in files {
@ -177,16 +177,14 @@ pub(crate) async fn open<R: Runtime>(
if let Some(s) = window.try_fs_scope() { if let Some(s) = window.try_fs_scope() {
s.allow_file(&path)?; s.allow_file(&path)?;
} }
tauri_scope.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 { } else {
let tauri_scope = window.state::<tauri::scope::Scopes>(); let tauri_scope = window.state::<tauri::scope::Scopes>();
let file = dialog_builder.blocking_pick_file(); let file = dialog_builder.blocking_pick_file();
if let Some(file) = &file { if let Some(file) = &file {
if let Ok(path) = file.clone().into_path() { if let Ok(path) = file.clone().into_path() {
if let Some(s) = window.try_fs_scope() { if let Some(s) = window.try_fs_scope() {
@ -195,9 +193,8 @@ pub(crate) async fn open<R: Runtime>(
tauri_scope.allow_file(&path)?; tauri_scope.allow_file(&path)?;
} }
} }
OpenResponse::File(file.map(|f| f.simplified())) Ok(OpenResponse::File(file.map(|f| f.simplified())))
}; }
Ok(res)
} }
#[allow(unused_variables)] #[allow(unused_variables)]

@ -16,9 +16,6 @@ pub enum Error {
#[cfg(mobile)] #[cfg(mobile)]
#[error(transparent)] #[error(transparent)]
PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),
#[cfg(mobile)]
#[error("Folder picker is not implemented on mobile")]
FolderPickerNotImplemented,
#[error(transparent)] #[error(transparent)]
Fs(#[from] tauri_plugin_fs::Error), Fs(#[from] tauri_plugin_fs::Error),
} }

@ -506,7 +506,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// Ok(()) /// Ok(())
/// }); /// });
/// ``` /// ```
#[cfg(any(desktop, target_os = "ios"))] #[cfg(any(desktop, target_os = "ios", target_os = "android"))]
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(any(desktop, target_os = "ios"))] #[cfg(any(desktop, target_os = "ios", target_os = "android"))]
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(any(desktop, target_os = "ios"))] #[cfg(any(desktop, target_os = "ios", target_os = "android"))]
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(any(desktop, target_os = "ios"))] #[cfg(any(desktop, target_os = "ios", target_os = "android"))]
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)
} }

@ -131,7 +131,7 @@ pub fn show_message_dialog<R: Runtime, F: FnOnce(bool) + Send + 'static>(
}); });
} }
#[cfg(target_os = "ios")] #[cfg(mobile)]
pub fn pick_folders<R: Runtime, F: FnOnce(Option<Vec<FilePath>>) + Send + 'static>( pub fn pick_folders<R: Runtime, F: FnOnce(Option<Vec<FilePath>>) + Send + 'static>(
dialog: FileDialogBuilder<R>, dialog: FileDialogBuilder<R>,
f: F, f: F,
@ -149,7 +149,7 @@ pub fn pick_folders<R: Runtime, F: FnOnce(Option<Vec<FilePath>>) + Send + 'stati
}); });
} }
#[cfg(target_os = "ios")] #[cfg(mobile)]
pub fn pick_folder<R: Runtime, F: FnOnce(Option<FilePath>) + Send + 'static>( pub fn pick_folder<R: Runtime, F: FnOnce(Option<FilePath>) + Send + 'static>(
dialog: FileDialogBuilder<R>, dialog: FileDialogBuilder<R>,
f: F, f: F,

Loading…
Cancel
Save