Allow picking directory path

Adds support for picking directory paths on both iOS & Android through getDirectoryPath() method.
This commit is contained in:
Miguel Ruivo 2020-06-04 22:49:39 +01:00
parent 2214e87c4a
commit b1f8cd6064
12 changed files with 190 additions and 97 deletions

View File

@ -1,3 +1,6 @@
## 1.10.0
**Android & iOS:** Adds `getDirectoryPath()` method that allows you to select and pick directory paths. Android, requires SDK 21 or above for this to work, and iOS requires iOS 11 or above.
## 1.9.0+1 ## 1.9.0+1
Adds a temporary workaround on Android where it can trigger `onRequestPermissionsResult` twice, related to Flutter issue [49365](https://github.com/flutter/flutter/issues/49365) for anyone affected in Flutter versions below 1.14.6. Adds a temporary workaround on Android where it can trigger `onRequestPermissionsResult` twice, related to Flutter issue [49365](https://github.com/flutter/flutter/issues/49365) for anyone affected in Flutter versions below 1.14.6.

View File

@ -25,6 +25,7 @@ A package that allows you to use a native file explorer to pick single or multip
* Load path from **audio** only * Load path from **audio** only
* Load path from **image** only * Load path from **image** only
* Load path from **video** only * Load path from **video** only
* Load path from **directory**
* Load path from **any** * Load path from **any**
* Create a `File` or `List<File>` objects from **any** selected file(s) * Create a `File` or `List<File>` objects from **any** selected file(s)
* Supports desktop through **go-flutter** (MacOS, Windows, Linux) * Supports desktop through **go-flutter** (MacOS, Windows, Linux)

View File

@ -5,7 +5,9 @@ import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Environment; import android.os.Environment;
import android.provider.DocumentsContract;
import android.util.Log; import android.util.Log;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
@ -85,12 +87,17 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener
finishWithSuccess(paths.get(0)); finishWithSuccess(paths.get(0));
} }
} else if (data.getData() != null) { } else if (data.getData() != null) {
final Uri uri = data.getData(); Uri uri = data.getData();
String fullPath;
if (type.equals("dir") && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
uri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri));
}
Log.i(FilePickerDelegate.TAG, "[SingleFilePick] File URI:" + uri.toString()); Log.i(FilePickerDelegate.TAG, "[SingleFilePick] File URI:" + uri.toString());
String fullPath = FileUtils.getPath(uri, FilePickerDelegate.this.activity); fullPath = FileUtils.getPath(uri, FilePickerDelegate.this.activity);
if (fullPath == null) { if (fullPath == null) {
fullPath = FileUtils.getUriFromRemote(FilePickerDelegate.this.activity, uri); fullPath = type.equals("dir") ? FileUtils.getFullPathFromTreeUri(uri, activity) : FileUtils.getUriFromRemote(FilePickerDelegate.this.activity, uri);
} }
if (fullPath != null) { if (fullPath != null) {
@ -99,6 +106,7 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener
} else { } else {
finishWithError("unknown_path", "Failed to retrieve path."); finishWithError("unknown_path", "Failed to retrieve path.");
} }
} else { } else {
finishWithError("unknown_activity", "Unknown activity error, please fill an issue."); finishWithError("unknown_activity", "Unknown activity error, please fill an issue.");
} }
@ -160,20 +168,24 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener
return; return;
} }
intent = new Intent(Intent.ACTION_GET_CONTENT); if (type.equals("dir")) {
final Uri uri = Uri.parse(Environment.getExternalStorageDirectory().getPath() + File.separator); intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
Log.d(TAG, "Selected type " + type); } else {
intent.setDataAndType(uri, this.type); intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType(this.type); final Uri uri = Uri.parse(Environment.getExternalStorageDirectory().getPath() + File.separator);
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, this.isMultipleSelection); Log.d(TAG, "Selected type " + type);
intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setDataAndType(uri, this.type);
intent.setType(this.type);
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, this.isMultipleSelection);
intent.addCategory(Intent.CATEGORY_OPENABLE);
if (type.contains(",")) { if (type.contains(",")) {
allowedExtensions = type.split(","); allowedExtensions = type.split(",");
} }
if (allowedExtensions != null) { if (allowedExtensions != null) {
intent.putExtra(Intent.EXTRA_MIME_TYPES, allowedExtensions); intent.putExtra(Intent.EXTRA_MIME_TYPES, allowedExtensions);
}
} }
if (intent.resolveActivity(this.activity.getPackageManager()) != null) { if (intent.resolveActivity(this.activity.getPackageManager()) != null) {

View File

@ -152,13 +152,16 @@ public class FilePickerPlugin implements MethodChannel.MethodCallHandler, Flutte
} }
fileType = FilePickerPlugin.resolveType(call.method); fileType = FilePickerPlugin.resolveType(call.method);
isMultipleSelection = (boolean) arguments.get("allowMultipleSelection"); String[] allowedExtensions = null;
final String[] allowedExtensions = FileUtils.getMimeTypes((ArrayList<String>) arguments.get("allowedExtensions"));
if (fileType == null) { if (fileType == null) {
result.notImplemented(); result.notImplemented();
} else if (fileType == "custom" && (allowedExtensions == null || allowedExtensions.length == 0)) { } else if (fileType != "dir") {
isMultipleSelection = (boolean) arguments.get("allowMultipleSelection");
allowedExtensions = FileUtils.getMimeTypes((ArrayList<String>) arguments.get("allowedExtensions"));
}
if (fileType == "custom" && (allowedExtensions == null || allowedExtensions.length == 0)) {
result.error(TAG, "Unsupported filter. Make sure that you are only using the extension without the dot, (ie., jpg instead of .jpg). This could also have happened because you are using an unsupported file extension. If the problem persists, you may want to consider using FileType.all instead.", null); result.error(TAG, "Unsupported filter. Make sure that you are only using the extension without the dot, (ie., jpg instead of .jpg). This could also have happened because you are using an unsupported file extension. If the problem persists, you may want to consider using FileType.all instead.", null);
} else { } else {
this.delegate.startFileExplorer(fileType, isMultipleSelection, allowedExtensions, result); this.delegate.startFileExplorer(fileType, isMultipleSelection, allowedExtensions, result);
@ -168,7 +171,6 @@ public class FilePickerPlugin implements MethodChannel.MethodCallHandler, Flutte
private static String resolveType(final String type) { private static String resolveType(final String type) {
switch (type) { switch (type) {
case "audio": case "audio":
return "audio/*"; return "audio/*";
@ -181,6 +183,8 @@ public class FilePickerPlugin implements MethodChannel.MethodCallHandler, Flutte
case "any": case "any":
case "custom": case "custom":
return "*/*"; return "*/*";
case "dir":
return "dir";
default: default:
return null; return null;
} }

View File

@ -1,5 +1,6 @@
package com.mr.flutter.plugin.filepicker; package com.mr.flutter.plugin.filepicker;
import android.annotation.SuppressLint;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.ContentUris; import android.content.ContentUris;
import android.content.Context; import android.content.Context;
@ -7,23 +8,29 @@ import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Environment; import android.os.Environment;
import android.os.storage.StorageManager;
import android.provider.DocumentsContract; import android.provider.DocumentsContract;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.webkit.MimeTypeMap; import android.webkit.MimeTypeMap;
import androidx.annotation.Nullable;
import java.io.BufferedOutputStream; import java.io.BufferedOutputStream;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Random; import java.util.Random;
public class FileUtils { public class FileUtils {
private static final String TAG = "FilePickerUtils"; private static final String TAG = "FilePickerUtils";
private static final String PRIMARY_VOLUME_NAME = "primary";
public static String getPath(final Uri uri, final Context context) { public static String getPath(final Uri uri, final Context context) {
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
@ -53,7 +60,7 @@ public class FileUtils {
final String type = split[0]; final String type = split[0];
if ("primary".equalsIgnoreCase(type)) { if ("primary".equalsIgnoreCase(type)) {
Log.e(TAG, "Primary External Document URI"); Log.e(TAG, "Primary External Document URI");
return Environment.getExternalStorageDirectory() + "/" + split[1]; return Environment.getExternalStorageDirectory() + (split.length > 1 ? ("/" + split[1]) : "");
} }
} else if (isDownloadsDocument(uri)) { } else if (isDownloadsDocument(uri)) {
Log.e(TAG, "Downloads External Document URI"); Log.e(TAG, "Downloads External Document URI");
@ -270,6 +277,78 @@ public class FileUtils {
return externalFile; return externalFile;
} }
@Nullable
public static String getFullPathFromTreeUri(@Nullable final Uri treeUri, Context con) {
if (treeUri == null) return null;
String volumePath = getVolumePath(getVolumeIdFromTreeUri(treeUri), con);
if (volumePath == null) return File.separator;
if (volumePath.endsWith(File.separator))
volumePath = volumePath.substring(0, volumePath.length() - 1);
String documentPath = getDocumentPathFromTreeUri(treeUri);
if (documentPath.endsWith(File.separator))
documentPath = documentPath.substring(0, documentPath.length() - 1);
if (documentPath.length() > 0) {
if (documentPath.startsWith(File.separator))
return volumePath + documentPath;
else
return volumePath + File.separator + documentPath;
} else return volumePath;
}
@SuppressLint("ObsoleteSdkInt")
private static String getVolumePath(final String volumeId, Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return null;
try {
StorageManager mStorageManager =
(StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
Class<?> storageVolumeClazz = Class.forName("android.os.storage.StorageVolume");
Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList");
Method getUuid = storageVolumeClazz.getMethod("getUuid");
Method getPath = storageVolumeClazz.getMethod("getPath");
Method isPrimary = storageVolumeClazz.getMethod("isPrimary");
Object result = getVolumeList.invoke(mStorageManager);
final int length = Array.getLength(result);
for (int i = 0; i < length; i++) {
Object storageVolumeElement = Array.get(result, i);
String uuid = (String) getUuid.invoke(storageVolumeElement);
Boolean primary = (Boolean) isPrimary.invoke(storageVolumeElement);
// primary volume?
if (primary && PRIMARY_VOLUME_NAME.equals(volumeId))
return (String) getPath.invoke(storageVolumeElement);
// other volumes?
if (uuid != null && uuid.equals(volumeId))
return (String) getPath.invoke(storageVolumeElement);
}
// not found.
return null;
} catch (Exception ex) {
return null;
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static String getVolumeIdFromTreeUri(final Uri treeUri) {
final String docId = DocumentsContract.getTreeDocumentId(treeUri);
final String[] split = docId.split(":");
if (split.length > 0) return split[0];
else return null;
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static String getDocumentPathFromTreeUri(final Uri treeUri) {
final String docId = DocumentsContract.getTreeDocumentId(treeUri);
final String[] split = docId.split(":");
if ((split.length >= 2) && (split[1] != null)) return split[1];
else return File.separator;
}
private static boolean isDropBoxUri(final Uri uri) { private static boolean isDropBoxUri(final Uri uri) {
return "com.dropbox.android.FileCache".equals(uri.getAuthority()); return "com.dropbox.android.FileCache".equals(uri.getAuthority());
} }

View File

@ -71,7 +71,7 @@ SPEC CHECKSUMS:
file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1 file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1
FLAnimatedImage: 4a0b56255d9b05f18b6dd7ee06871be5d3b89e31 FLAnimatedImage: 4a0b56255d9b05f18b6dd7ee06871be5d3b89e31
Flutter: 0e3d915762c693b495b44d77113d4970485de6ec Flutter: 0e3d915762c693b495b44d77113d4970485de6ec
flutter_plugin_android_lifecycle: 47de533a02850f070f5696a623995e93eddcdb9b flutter_plugin_android_lifecycle: dc0b544e129eebb77a6bfb1239d4d1c673a60a35
SDWebImage: 97351f6582ceca541ea294ba66a1fcb342a331c2 SDWebImage: 97351f6582ceca541ea294ba66a1fcb342a331c2
SDWebImageFLPlugin: 6c2295fb1242d44467c6c87dc5db6b0a13228fd8 SDWebImageFLPlugin: 6c2295fb1242d44467c6c87dc5db6b0a13228fd8

View File

@ -35,7 +35,6 @@
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = "<group>"; };
4FE42FA345519DC2CF8F7CAB /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4FE42FA345519DC2CF8F7CAB /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
620D3249E16C66CF31F39A1D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; }; 620D3249E16C66CF31F39A1D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
@ -43,7 +42,6 @@
7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; }; 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
@ -67,9 +65,7 @@
9740EEB11CF90186004384FC /* Flutter */ = { 9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
3B80C3931E831B6300D905FE /* App.framework */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEBA1CF902C7004384FC /* Flutter.framework */,
9740EEB21CF90195004384FC /* Debug.xcconfig */, 9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */,

View File

@ -31,17 +31,11 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
if (_multiPick) { if (_multiPick) {
_path = null; _path = null;
_paths = await FilePicker.getMultiFilePath( _paths = await FilePicker.getMultiFilePath(
type: _pickingType, type: _pickingType, allowedExtensions: (_extension?.isNotEmpty ?? false) ? _extension?.replaceAll(' ', '')?.split(',') : null);
allowedExtensions: (_extension?.isNotEmpty ?? false)
? _extension?.replaceAll(' ', '')?.split(',')
: null);
} else { } else {
_paths = null; _paths = null;
_path = await FilePicker.getFilePath( _path = await FilePicker.getFilePath(
type: _pickingType, type: _pickingType, allowedExtensions: (_extension?.isNotEmpty ?? false) ? _extension?.replaceAll(' ', '')?.split(',') : null);
allowedExtensions: (_extension?.isNotEmpty ?? false)
? _extension?.replaceAll(' ', '')?.split(',')
: null);
} }
} on PlatformException catch (e) { } on PlatformException catch (e) {
print("Unsupported operation" + e.toString()); print("Unsupported operation" + e.toString());
@ -49,9 +43,7 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_loadingPath = false; _loadingPath = false;
_fileName = _path != null _fileName = _path != null ? _path.split('/').last : _paths != null ? _paths.keys.toString() : '...';
? _path.split('/').last
: _paths != null ? _paths.keys.toString() : '...';
}); });
} }
@ -60,14 +52,18 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
_scaffoldKey.currentState.showSnackBar( _scaffoldKey.currentState.showSnackBar(
SnackBar( SnackBar(
backgroundColor: result ? Colors.green : Colors.red, backgroundColor: result ? Colors.green : Colors.red,
content: Text((result content: Text((result ? 'Temporary files removed with success.' : 'Failed to clean temporary files')),
? 'Temporary files removed with success.'
: 'Failed to clean temporary files')),
), ),
); );
}); });
} }
void _selectFolder() {
FilePicker.getDirectoryPath().then((value) {
setState(() => _path = value);
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new MaterialApp( return new MaterialApp(
@ -128,8 +124,7 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
maxLength: 15, maxLength: 15,
autovalidate: true, autovalidate: true,
controller: _controller, controller: _controller,
decoration: decoration: InputDecoration(labelText: 'File extension'),
InputDecoration(labelText: 'File extension'),
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
textCapitalization: TextCapitalization.none, textCapitalization: TextCapitalization.none,
) )
@ -138,10 +133,8 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
new ConstrainedBox( new ConstrainedBox(
constraints: BoxConstraints.tightFor(width: 200.0), constraints: BoxConstraints.tightFor(width: 200.0),
child: new SwitchListTile.adaptive( child: new SwitchListTile.adaptive(
title: new Text('Pick multiple files', title: new Text('Pick multiple files', textAlign: TextAlign.right),
textAlign: TextAlign.right), onChanged: (bool value) => setState(() => _multiPick = value),
onChanged: (bool value) =>
setState(() => _multiPick = value),
value: _multiPick, value: _multiPick,
), ),
), ),
@ -153,6 +146,10 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
onPressed: () => _openFileExplorer(), onPressed: () => _openFileExplorer(),
child: new Text("Open file picker"), child: new Text("Open file picker"),
), ),
new RaisedButton(
onPressed: () => _selectFolder(),
child: new Text("Pick folder"),
),
new RaisedButton( new RaisedButton(
onPressed: () => _clearCachedFiles(), onPressed: () => _clearCachedFiles(),
child: new Text("Clear temporary files"), child: new Text("Clear temporary files"),
@ -162,28 +159,18 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
), ),
new Builder( new Builder(
builder: (BuildContext context) => _loadingPath builder: (BuildContext context) => _loadingPath
? Padding( ? Padding(padding: const EdgeInsets.only(bottom: 10.0), child: const CircularProgressIndicator())
padding: const EdgeInsets.only(bottom: 10.0),
child: const CircularProgressIndicator())
: _path != null || _paths != null : _path != null || _paths != null
? new Container( ? new Container(
padding: const EdgeInsets.only(bottom: 30.0), padding: const EdgeInsets.only(bottom: 30.0),
height: MediaQuery.of(context).size.height * 0.50, height: MediaQuery.of(context).size.height * 0.50,
child: new Scrollbar( child: new Scrollbar(
child: new ListView.separated( child: new ListView.separated(
itemCount: _paths != null && _paths.isNotEmpty itemCount: _paths != null && _paths.isNotEmpty ? _paths.length : 1,
? _paths.length
: 1,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
final bool isMultiPath = final bool isMultiPath = _paths != null && _paths.isNotEmpty;
_paths != null && _paths.isNotEmpty; final String name = 'File $index: ' + (isMultiPath ? _paths.keys.toList()[index] : _fileName ?? '...');
final String name = 'File $index: ' + final path = isMultiPath ? _paths.values.toList()[index].toString() : _path;
(isMultiPath
? _paths.keys.toList()[index]
: _fileName ?? '...');
final path = isMultiPath
? _paths.values.toList()[index].toString()
: _path;
return new ListTile( return new ListTile(
title: new Text( title: new Text(
@ -192,9 +179,7 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
subtitle: new Text(path), subtitle: new Text(path),
); );
}, },
separatorBuilder: separatorBuilder: (BuildContext context, int index) => new Divider(),
(BuildContext context, int index) =>
new Divider(),
)), )),
) )
: new Container(), : new Container(),

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -54,6 +54,11 @@
return; return;
} }
if([call.method isEqualToString:@"dir"]) {
[self resolvePickDocumentWithMultiPick:NO pickDirectory:YES];
return;
}
NSDictionary * arguments = call.arguments; NSDictionary * arguments = call.arguments;
BOOL isMultiplePick = ((NSNumber*)[arguments valueForKey:@"allowMultipleSelection"]).boolValue; BOOL isMultiplePick = ((NSNumber*)[arguments valueForKey:@"allowMultipleSelection"]).boolValue;
if([call.method isEqualToString:@"any"] || [call.method containsString:@"custom"]) { if([call.method isEqualToString:@"any"] || [call.method containsString:@"custom"]) {
@ -64,7 +69,7 @@
details:nil]); details:nil]);
_result = nil; _result = nil;
} else if(self.allowedExtensions != nil) { } else if(self.allowedExtensions != nil) {
[self resolvePickDocumentWithMultipleSelection:isMultiplePick]; [self resolvePickDocumentWithMultiPick:isMultiplePick pickDirectory:NO];
} }
} else if([call.method isEqualToString:@"video"] || [call.method isEqualToString:@"image"] || [call.method isEqualToString:@"media"]) { } else if([call.method isEqualToString:@"video"] || [call.method isEqualToString:@"image"] || [call.method isEqualToString:@"media"]) {
[self resolvePickMedia:[FileUtils resolveMediaType:call.method] withMultiPick:isMultiplePick]; [self resolvePickMedia:[FileUtils resolveMediaType:call.method] withMultiPick:isMultiplePick];
@ -78,13 +83,12 @@
} }
#pragma mark - Resolvers #pragma mark - Resolvers
- (void)resolvePickDocumentWithMultiPick:(BOOL)allowsMultipleSelection pickDirectory:(BOOL)isDirectory {
- (void)resolvePickDocumentWithMultipleSelection:(BOOL)allowsMultipleSelection {
@try{ @try{
self.documentPickerController = [[UIDocumentPickerViewController alloc] self.documentPickerController = [[UIDocumentPickerViewController alloc]
initWithDocumentTypes: self.allowedExtensions initWithDocumentTypes: isDirectory ? @[@"public.folder"] : self.allowedExtensions
inMode:UIDocumentPickerModeImport]; inMode: isDirectory ? UIDocumentPickerModeOpen : UIDocumentPickerModeImport];
} @catch (NSException * e) { } @catch (NSException * e) {
Log(@"Couldn't launch documents file picker. Probably due to iOS version being below 11.0 and not having the iCloud entitlement. If so, just make sure to enable it for your app in Xcode. Exception was: %@", e); Log(@"Couldn't launch documents file picker. Probably due to iOS version being below 11.0 and not having the iCloud entitlement. If so, just make sure to enable it for your app in Xcode. Exception was: %@", e);
_result = nil; _result = nil;

View File

@ -15,8 +15,7 @@ enum FileType {
class FilePicker { class FilePicker {
FilePicker._(); FilePicker._();
static const MethodChannel _channel = static const MethodChannel _channel = const MethodChannel('miguelruivo.flutter.plugins.filepicker');
const MethodChannel('miguelruivo.flutter.plugins.filepicker');
static const String _tag = 'FilePicker'; static const String _tag = 'FilePicker';
/// Returns an iterable `Map<String,String>` where the `key` is the name of the file /// Returns an iterable `Map<String,String>` where the `key` is the name of the file
@ -25,9 +24,7 @@ class FilePicker {
/// A `List` with [allowedExtensions] can be provided to filter the allowed files to picked. /// A `List` with [allowedExtensions] can be provided to filter the allowed files to picked.
/// If provided, make sure you select `FileType.custom` as type. /// If provided, make sure you select `FileType.custom` as type.
/// Defaults to `FileType.any`, which allows any combination of files to be multi selected at once. /// Defaults to `FileType.any`, which allows any combination of files to be multi selected at once.
static Future<Map<String, String>> getMultiFilePath( static Future<Map<String, String>> getMultiFilePath({FileType type = FileType.any, List<String> allowedExtensions}) async =>
{FileType type = FileType.any,
List<String> allowedExtensions}) async =>
await _getPath(describeEnum(type), true, allowedExtensions); await _getPath(describeEnum(type), true, allowedExtensions);
/// Returns an absolute file path from the calling platform. /// Returns an absolute file path from the calling platform.
@ -35,19 +32,15 @@ class FilePicker {
/// Extension filters are allowed with `FileType.custom`, when used, make sure to provide a `List` /// Extension filters are allowed with `FileType.custom`, when used, make sure to provide a `List`
/// of [allowedExtensions] (e.g. [`pdf`, `svg`, `jpg`].). /// of [allowedExtensions] (e.g. [`pdf`, `svg`, `jpg`].).
/// Defaults to `FileType.any` which will display all file types. /// Defaults to `FileType.any` which will display all file types.
static Future<String> getFilePath( static Future<String> getFilePath({FileType type = FileType.any, List<String> allowedExtensions}) async =>
{FileType type = FileType.any,
List<String> allowedExtensions}) async =>
await _getPath(describeEnum(type), false, allowedExtensions); await _getPath(describeEnum(type), false, allowedExtensions);
/// Returns a `File` object from the selected file path. /// Returns a `File` object from the selected file path.
/// ///
/// This is an utility method that does the same of `getFilePath()` but saving some boilerplate if /// This is an utility method that does the same of `getFilePath()` but saving some boilerplate if
/// you are planing to create a `File` for the returned path. /// you are planing to create a `File` for the returned path.
static Future<File> getFile( static Future<File> getFile({FileType type = FileType.any, List<String> allowedExtensions}) async {
{FileType type = FileType.any, List<String> allowedExtensions}) async { final String filePath = await _getPath(describeEnum(type), false, allowedExtensions);
final String filePath =
await _getPath(describeEnum(type), false, allowedExtensions);
return filePath != null ? File(filePath) : null; return filePath != null ? File(filePath) : null;
} }
@ -66,20 +59,30 @@ class FilePicker {
/// ///
/// This is an utility method that does the same of `getMultiFilePath()` but saving some boilerplate if /// This is an utility method that does the same of `getMultiFilePath()` but saving some boilerplate if
/// you are planing to create a list of `File`s for the returned paths. /// you are planing to create a list of `File`s for the returned paths.
static Future<List<File>> getMultiFile( static Future<List<File>> getMultiFile({FileType type = FileType.any, List<String> allowedExtensions}) async {
{FileType type = FileType.any, List<String> allowedExtensions}) async { final Map<String, String> paths = await _getPath(describeEnum(type), true, allowedExtensions);
final Map<String, String> paths = return paths != null && paths.isNotEmpty ? paths.values.map((path) => File(path)).toList() : null;
await _getPath(describeEnum(type), true, allowedExtensions);
return paths != null && paths.isNotEmpty
? paths.values.map((path) => File(path)).toList()
: null;
} }
static Future<dynamic> _getPath(String type, bool allowMultipleSelection, /// Selects a directory and returns its absolute path.
List<String> allowedExtensions) async { ///
/// On Android, this requires to be running on SDK 21 or above, else won't work.
/// Returns `null` if folder path couldn't be resolved.
static Future<String> getDirectoryPath() async {
try {
return await _channel.invokeMethod('dir');
} on PlatformException catch (ex) {
if (ex.code == "unknown_path") {
print(
'[$_tag] Could not resolve directory path. Maybe it\'s a protected one or unsupported (such as Downloads folder). If you are on Android, make sure that you are on SDK 21 or above.');
}
return null;
}
}
static Future<dynamic> _getPath(String type, bool allowMultipleSelection, List<String> allowedExtensions) async {
if (type != 'custom' && (allowedExtensions?.isNotEmpty ?? false)) { if (type != 'custom' && (allowedExtensions?.isNotEmpty ?? false)) {
throw Exception( throw Exception('If you are using a custom extension filter, please use the FileType.custom instead.');
'If you are using a custom extension filter, please use the FileType.custom instead.');
} }
try { try {
dynamic result = await _channel.invokeMethod(type, { dynamic result = await _channel.invokeMethod(type, {
@ -90,16 +93,14 @@ class FilePicker {
if (result is String) { if (result is String) {
result = [result]; result = [result];
} }
return Map<String, String>.fromIterable(result, return Map<String, String>.fromIterable(result, key: (path) => path.split('/').last, value: (path) => path);
key: (path) => path.split('/').last, value: (path) => path);
} }
return result; return result;
} on PlatformException catch (e) { } on PlatformException catch (e) {
print('[$_tag] Platform exception: $e'); print('[$_tag] Platform exception: $e');
rethrow; rethrow;
} catch (e) { } catch (e) {
print( print('[$_tag] Unsupported operation. Method not found. The exception thrown was: $e');
'[$_tag] Unsupported operation. Method not found. The exception thrown was: $e');
rethrow; rethrow;
} }
} }

View File

@ -1,7 +1,7 @@
name: file_picker name: file_picker
description: A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extension filtering support. description: A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extension filtering support.
homepage: https://github.com/miguelpruivo/plugins_flutter_file_picker homepage: https://github.com/miguelpruivo/plugins_flutter_file_picker
version: 1.9.0+1 version: 1.10.0
dependencies: dependencies:
flutter: flutter: