diff --git a/.changeset/popular-games-prove.md b/.changeset/popular-games-prove.md new file mode 100644 index 00000000..82e398ac --- /dev/null +++ b/.changeset/popular-games-prove.md @@ -0,0 +1,5 @@ +--- +'@capawesome/capacitor-file-picker': minor +--- + +feat: add `pickDirectory()` method diff --git a/packages/file-picker/README.md b/packages/file-picker/README.md index 763dcd41..324b270f 100644 --- a/packages/file-picker/README.md +++ b/packages/file-picker/README.md @@ -82,6 +82,7 @@ const requestPermissions = async () => { * [`checkPermissions()`](#checkpermissions) * [`convertHeicToJpeg(...)`](#convertheictojpeg) * [`pickFiles(...)`](#pickfiles) +* [`pickDirectory()`](#pickdirectory) * [`pickImages(...)`](#pickimages) * [`pickMedia(...)`](#pickmedia) * [`pickVideos(...)`](#pickvideos) @@ -151,6 +152,23 @@ Open the file picker that allows the user to select one or more files. -------------------- +### pickDirectory() + +```typescript +pickDirectory() => Promise +``` + +Open a picker dialog that allows the user to select a directory. + +Only available on Android and iOS. + +**Returns:** Promise<PickDirectoryResult> + +**Since:** 6.2.0 + +-------------------- + + ### pickImages(...) ```typescript @@ -333,6 +351,13 @@ Remove all listeners for this plugin. | **`readData`** | boolean | Whether to read the file data. | false | | +#### PickDirectoryResult + +| Prop | Type | Description | Since | +| ---------- | ------------------- | ----------------------------------- | ----- | +| **`path`** | string | The path to the selected directory. | 6.2.0 | + + #### PickMediaOptions | Prop | Type | Description | Default | Since | diff --git a/packages/file-picker/android/src/main/java/io/capawesome/capacitorjs/plugins/filepicker/FilePickerPlugin.java b/packages/file-picker/android/src/main/java/io/capawesome/capacitorjs/plugins/filepicker/FilePickerPlugin.java index 447b5e6c..5b5338e5 100644 --- a/packages/file-picker/android/src/main/java/io/capawesome/capacitorjs/plugins/filepicker/FilePickerPlugin.java +++ b/packages/file-picker/android/src/main/java/io/capawesome/capacitorjs/plugins/filepicker/FilePickerPlugin.java @@ -34,6 +34,8 @@ public class FilePickerPlugin extends Plugin { public static final String ERROR_PICK_FILE_FAILED = "pickFiles failed."; public static final String ERROR_PICK_FILE_CANCELED = "pickFiles canceled."; + public static final String ERROR_PICK_DIRECTORY_FAILED = "pickDirectory failed."; + public static final String ERROR_PICK_DIRECTORY_CANCELED = "pickDirectory canceled."; public static final String PERMISSION_ACCESS_MEDIA_LOCATION = "accessMediaLocation"; public static final String PERMISSION_READ_EXTERNAL_STORAGE = "readExternalStorage"; public static final String TAG = "FilePickerPlugin"; @@ -78,6 +80,18 @@ public void pickFiles(PluginCall call) { } } + @PluginMethod + public void pickDirectory(PluginCall call) { + try { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + startActivityForResult(call, intent, "pickDirectoryResult"); + } catch (Exception ex) { + String message = ex.getMessage(); + Log.e(TAG, message); + call.reject(message); + } + } + @PluginMethod public void pickImages(PluginCall call) { try { @@ -200,6 +214,28 @@ private void pickFilesResult(PluginCall call, ActivityResult result) { } } + @ActivityCallback + private void pickDirectoryResult(PluginCall call, ActivityResult result) { + try { + int resultCode = result.getResultCode(); + switch (resultCode) { + case Activity.RESULT_OK: + JSObject callResult = createPickDirectoryResult(result.getData()); + call.resolve(callResult); + break; + case Activity.RESULT_CANCELED: + call.reject(ERROR_PICK_DIRECTORY_CANCELED); + break; + default: + call.reject(ERROR_PICK_DIRECTORY_FAILED); + } + } catch (Exception ex) { + String message = ex.getMessage(); + Log.e(TAG, message); + call.reject(message); + } + } + private JSObject createPickFilesResult(@Nullable Intent data, boolean readData) { JSObject callResult = new JSObject(); List filesResultList = new ArrayList<>(); @@ -260,4 +296,15 @@ private JSObject createPickFilesResult(@Nullable Intent data, boolean readData) callResult.put("files", JSArray.from(filesResultList.toArray())); return callResult; } + + private JSObject createPickDirectoryResult(@Nullable Intent data) { + JSObject callResult = new JSObject(); + if (data != null) { + Uri uri = data.getData(); + if (uri != null) { + callResult.put("path", implementation.getPathFromUri(uri)); + } + } + return callResult; + } } diff --git a/packages/file-picker/ios/Plugin/FilePicker.swift b/packages/file-picker/ios/Plugin/FilePicker.swift index 5dc8621c..77568ce5 100644 --- a/packages/file-picker/ios/Plugin/FilePicker.swift +++ b/packages/file-picker/ios/Plugin/FilePicker.swift @@ -7,6 +7,7 @@ import MobileCoreServices @objc public class FilePicker: NSObject { private var plugin: FilePickerPlugin? + private var invokedMethod: String? init(_ plugin: FilePickerPlugin?) { super.init() @@ -30,6 +31,7 @@ import MobileCoreServices } public func openDocumentPicker(limit: Int, documentTypes: [String]) { + invokedMethod = "pickFiles" DispatchQueue.main.async { let picker = UIDocumentPickerViewController(documentTypes: documentTypes, in: .import) picker.delegate = self @@ -39,6 +41,24 @@ import MobileCoreServices } } + public func openDirectoryPicker() { + invokedMethod = "pickDirectory" + DispatchQueue.main.async { + if #available(iOS 14.0, *) { + let picker = UIDocumentPickerViewController(forOpeningContentTypes: [.folder]) + picker.delegate = self + picker.modalPresentationStyle = .fullScreen + self.presentViewController(picker) + } else { + let documentTypes = [kUTTypeFolder as String] + let picker = UIDocumentPickerViewController(documentTypes: documentTypes, in: .open) + picker.delegate = self + picker.modalPresentationStyle = .fullScreen + self.presentViewController(picker) + } + } + } + public func openImagePicker(limit: Int, skipTranscoding: Bool, ordered: Bool) { DispatchQueue.main.async { if #available(iOS 14, *) { @@ -257,17 +277,29 @@ import MobileCoreServices extension FilePicker: UIDocumentPickerDelegate { public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { - do { - let temporaryUrls = try urls.map { try saveTemporaryFile($0) } - plugin?.handleDocumentPickerResult(urls: temporaryUrls, error: nil) - } catch { - plugin?.handleDocumentPickerResult(urls: nil, error: self.plugin?.errorTemporaryCopyFailed) + if invokedMethod == "pickFiles" { + do { + let temporaryUrls = try urls.map { try saveTemporaryFile($0) } + plugin?.handleDocumentPickerResult(urls: temporaryUrls, error: nil) + } catch { + plugin?.handleDocumentPickerResult(urls: nil, error: self.plugin?.errorTemporaryCopyFailed) + } + } else if invokedMethod == "pickDirectory" { + plugin?.handleDirectoryPickerResult(path: urls.first?.absoluteString, error: nil) + } else { + return } plugin?.notifyPickerDismissedListener() } public func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { - plugin?.handleDocumentPickerResult(urls: nil, error: nil) + if invokedMethod == "pickFiles" { + plugin?.handleDocumentPickerResult(urls: nil, error: nil) + } else if invokedMethod == "pickDirectory" { + plugin?.handleDirectoryPickerResult(path: nil, error: nil) + } else { + return + } plugin?.notifyPickerDismissedListener() } } diff --git a/packages/file-picker/ios/Plugin/FilePickerPlugin.m b/packages/file-picker/ios/Plugin/FilePickerPlugin.m index 1bfddc05..6052def5 100644 --- a/packages/file-picker/ios/Plugin/FilePickerPlugin.m +++ b/packages/file-picker/ios/Plugin/FilePickerPlugin.m @@ -9,4 +9,5 @@ CAP_PLUGIN_METHOD(pickImages, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(pickMedia, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(pickVideos, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(pickDirectory, CAPPluginReturnPromise); ) diff --git a/packages/file-picker/ios/Plugin/FilePickerPlugin.swift b/packages/file-picker/ios/Plugin/FilePickerPlugin.swift index c33045d7..7fceaa6a 100644 --- a/packages/file-picker/ios/Plugin/FilePickerPlugin.swift +++ b/packages/file-picker/ios/Plugin/FilePickerPlugin.swift @@ -13,6 +13,7 @@ public class FilePickerPlugin: CAPPlugin { public let errorFileNotExist = "File does not exist." public let errorConvertFailed = "File could not be converted." public let errorPickFileCanceled = "pickFiles canceled." + public let errorPickDirectoryCanceled = "pickDirectory canceled." public let errorUnknown = "Unknown error occurred." public let errorTemporaryCopyFailed = "An unknown error occurred while creating a temporary copy of the file." public let errorUnsupportedFileTypeIdentifier = "Unsupported file type identifier." @@ -60,6 +61,11 @@ public class FilePickerPlugin: CAPPlugin { implementation?.openDocumentPicker(limit: limit, documentTypes: documentTypes) } + @objc func pickDirectory(_ call: CAPPluginCall) { + savedCall = call + implementation?.openDirectoryPicker() + } + @objc func pickImages(_ call: CAPPluginCall) { savedCall = call @@ -140,6 +146,23 @@ public class FilePickerPlugin: CAPPlugin { } } + @objc func handleDirectoryPickerResult(path: String?, error: String?) { + guard let savedCall = savedCall else { + return + } + if let error = error { + savedCall.reject(error) + return + } + guard let path = path else { + savedCall.reject(errorPickDirectoryCanceled) + return + } + var result = JSObject() + result["path"] = path + savedCall.resolve(result) + } + private func parseTypesOption(_ types: [String]) -> [String] { var parsedTypes: [String] = [] for (_, type) in types.enumerated() { diff --git a/packages/file-picker/src/definitions.ts b/packages/file-picker/src/definitions.ts index 023f150f..1091e831 100644 --- a/packages/file-picker/src/definitions.ts +++ b/packages/file-picker/src/definitions.ts @@ -23,6 +23,14 @@ export interface FilePickerPlugin { * Open the file picker that allows the user to select one or more files. */ pickFiles(options?: PickFilesOptions): Promise; + /** + * Open a picker dialog that allows the user to select a directory. + * + * Only available on Android and iOS. + * + * @since 6.2.0 + */ + pickDirectory(): Promise; /** * Pick one or more images from the gallery. * @@ -269,6 +277,15 @@ export interface PickMediaOptions { ordered?: boolean; } +export interface PickDirectoryResult { + /** + * The path to the selected directory. + * + * @since 6.2.0 + */ + path: string; +} + /** * @since 0.5.3 */ diff --git a/packages/file-picker/src/web.ts b/packages/file-picker/src/web.ts index 450aae7c..cc1b111a 100644 --- a/packages/file-picker/src/web.ts +++ b/packages/file-picker/src/web.ts @@ -15,6 +15,7 @@ import type { PickVideosResult, PickedFile, RequestPermissionsOptions, + PickDirectoryResult, } from './definitions'; export class FilePickerWeb extends WebPlugin implements FilePickerPlugin { @@ -55,6 +56,10 @@ export class FilePickerWeb extends WebPlugin implements FilePickerPlugin { return result; } + public async pickDirectory(): Promise { + throw this.unimplemented('Not implemented on web.'); + } + public async pickImages( options?: PickImagesOptions, ): Promise {