Adds multiple file extensions support (iOS) and updates channel name

This commit is contained in:
Miguel Ruivo 2020-04-05 23:06:48 +01:00
parent 2378cd5422
commit 0741603151
11 changed files with 121 additions and 57 deletions

View File

@ -1,3 +1,8 @@
## 1.6.0
* Adds multiple file extension filter support. From now on, you _must_ provide a `List` of extensions with type `FileType.custom` when restricting types while pikcing.
* Other minor improvements;
## 1.5.1 ## 1.5.1
* iOS: Fixes an issue that could result in a crash when selecting files (with repeated taps) from 3rd party remote providers (Google Drive, Dropbox etc.); * iOS: Fixes an issue that could result in a crash when selecting files (with repeated taps) from 3rd party remote providers (Google Drive, Dropbox etc.);

View File

@ -19,8 +19,8 @@ A package that allows you to use a native file explorer to pick single or multip
## Currently supported features ## Currently supported features
* Load paths from **cloud files** (GDrive, Dropbox, iCloud) * Load paths from **cloud files** (GDrive, Dropbox, iCloud)
* Load path from a **custom format** by providing a file extension (pdf, svg, zip, etc.) * Load path from a **custom format** by providing a list of file extensions (pdf, svg, zip, etc.)
* Load path from **multiple files** optionally, supplying a file extension * Load path from **multiple files** optionally, supplying file extensions
* Load path from **gallery** * Load path from **gallery**
* Load path from **audio** * Load path from **audio**
* Load path from **video** * Load path from **video**
@ -42,6 +42,7 @@ See the **[File Picker Wiki](https://github.com/miguelpruivo/flutter_file_picker
* [Filters](https://github.com/miguelpruivo/plugins_flutter_file_picker/wiki/API#filters) * [Filters](https://github.com/miguelpruivo/plugins_flutter_file_picker/wiki/API#filters)
* [Methods](https://github.com/miguelpruivo/plugins_flutter_file_picker/wiki/API#methods) * [Methods](https://github.com/miguelpruivo/plugins_flutter_file_picker/wiki/API#methods)
4. [Example App](https://github.com/miguelpruivo/flutter_file_picker/blob/master/example/lib/src/file_picker_demo.dart) 4. [Example App](https://github.com/miguelpruivo/flutter_file_picker/blob/master/example/lib/src/file_picker_demo.dart)
5. [Troubleshooting](https://github.com/miguelpruivo/flutter_file_picker/wiki/Troubleshooting)
## Usage ## Usage
Quick simple usage example: Quick simple usage example:
@ -54,10 +55,18 @@ File file = await FilePicker.getFile();
``` ```
List<File> files = await FilePicker.getMultiFile(); List<File> files = await FilePicker.getMultiFile();
``` ```
#### Multiple files with extension filter
```
List<File> files = await FilePicker.getMultiFile(
type: FileType.custom,
allowedExtensions: ['jpg', 'pdf', 'doc'],
);
```
For full usage details refer to the **[Wiki](https://github.com/miguelpruivo/flutter_file_picker/wiki)** above. For full usage details refer to the **[Wiki](https://github.com/miguelpruivo/flutter_file_picker/wiki)** above.
## Example App ## Example App
![Demo](https://github.com/miguelpruivo/plugins_flutter_file_picker/blob/master/example/example.gif) ![Demo](https://github.com/miguelpruivo/plugins_flutter_file_picker/blob/master/example/example.gif)
![DemoMultiFilters](https://github.com/miguelpruivo/plugins_flutter_file_picker/blob/master/example/example_ios.gif)
## Getting Started ## Getting Started

View File

@ -30,7 +30,7 @@ import io.flutter.plugin.common.PluginRegistry.Registrar;
public class FilePickerPlugin implements MethodChannel.MethodCallHandler, FlutterPlugin, ActivityAware { public class FilePickerPlugin implements MethodChannel.MethodCallHandler, FlutterPlugin, ActivityAware {
private static final String TAG = "FilePicker"; private static final String TAG = "FilePicker";
private static final String CHANNEL = "miguelruivo.flutter.plugins.file_picker"; private static final String CHANNEL = "miguelruivo.flutter.plugins.filepicker";
private class LifeCycleObserver private class LifeCycleObserver
implements Application.ActivityLifecycleCallbacks, DefaultLifecycleObserver { implements Application.ActivityLifecycleCallbacks, DefaultLifecycleObserver {

BIN
example/example_ios.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@ -29,10 +29,18 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
try { try {
if (_multiPick) { if (_multiPick) {
_path = null; _path = null;
_paths = await FilePicker.getMultiFilePath(type: _pickingType, allowedExtensions: _extension?.replaceAll(' ', '')?.split(',')); _paths = await FilePicker.getMultiFilePath(
type: _pickingType,
allowedExtensions: (_extension?.isNotEmpty ?? false)
? _extension?.replaceAll(' ', '')?.split(',')
: null);
} else { } else {
_paths = null; _paths = null;
_path = await FilePicker.getFilePath(type: _pickingType, allowedExtensions: _extension?.replaceAll(' ', '')?.split(',')); _path = await FilePicker.getFilePath(
type: _pickingType,
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());
@ -40,7 +48,9 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_loadingPath = false; _loadingPath = false;
_fileName = _path != null ? _path.split('/').last : _paths != null ? _paths.keys.toString() : '...'; _fileName = _path != null
? _path.split('/').last
: _paths != null ? _paths.keys.toString() : '...';
}); });
} }
@ -88,7 +98,7 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
onChanged: (value) => setState(() { onChanged: (value) => setState(() {
_pickingType = value; _pickingType = value;
if (_pickingType != FileType.custom) { if (_pickingType != FileType.custom) {
_controller.text = _extension = null; _controller.text = _extension = '';
} }
})), })),
), ),
@ -99,7 +109,8 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
maxLength: 15, maxLength: 15,
autovalidate: true, autovalidate: true,
controller: _controller, controller: _controller,
decoration: InputDecoration(labelText: 'File extension'), decoration:
InputDecoration(labelText: 'File extension'),
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
textCapitalization: TextCapitalization.none, textCapitalization: TextCapitalization.none,
) )
@ -108,8 +119,10 @@ 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', textAlign: TextAlign.right), title: new Text('Pick multiple files',
onChanged: (bool value) => setState(() => _multiPick = value), textAlign: TextAlign.right),
onChanged: (bool value) =>
setState(() => _multiPick = value),
value: _multiPick, value: _multiPick,
), ),
), ),
@ -122,18 +135,28 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
), ),
new Builder( new Builder(
builder: (BuildContext context) => _loadingPath builder: (BuildContext context) => _loadingPath
? Padding(padding: const EdgeInsets.only(bottom: 10.0), child: const CircularProgressIndicator()) ? Padding(
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 ? _paths.length : 1, itemCount: _paths != null && _paths.isNotEmpty
? _paths.length
: 1,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
final bool isMultiPath = _paths != null && _paths.isNotEmpty; final bool isMultiPath =
final String name = 'File $index: ' + (isMultiPath ? _paths.keys.toList()[index] : _fileName ?? '...'); _paths != null && _paths.isNotEmpty;
final path = isMultiPath ? _paths.values.toList()[index].toString() : _path; final String name = 'File $index: ' +
(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(
@ -142,7 +165,9 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
subtitle: new Text(path), subtitle: new Text(path),
); );
}, },
separatorBuilder: (BuildContext context, int index) => new Divider(), separatorBuilder:
(BuildContext context, int index) =>
new Divider(),
)), )),
) )
: new Container(), : new Container(),

View File

@ -7,7 +7,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
const channelName = "miguelruivo.flutter.plugins.file_picker" const channelName = "miguelruivo.flutter.plugins.filepicker"
type FilePickerPlugin struct{} type FilePickerPlugin struct{}

View File

@ -9,14 +9,14 @@
@property (nonatomic) UIDocumentPickerViewController *documentPickerController; @property (nonatomic) UIDocumentPickerViewController *documentPickerController;
@property (nonatomic) UIDocumentInteractionController *interactionController; @property (nonatomic) UIDocumentInteractionController *interactionController;
@property (nonatomic) MPMediaPickerController *audioPickerController; @property (nonatomic) MPMediaPickerController *audioPickerController;
@property (nonatomic) NSString * fileType; @property (nonatomic) NSArray<NSString *> * allowedExtensions;
@end @end
@implementation FilePickerPlugin @implementation FilePickerPlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar { + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
FlutterMethodChannel* channel = [FlutterMethodChannel FlutterMethodChannel* channel = [FlutterMethodChannel
methodChannelWithName:@"miguelruivo.flutter.plugins.file_picker" methodChannelWithName:@"miguelruivo.flutter.plugins.filepicker"
binaryMessenger:[registrar messenger]]; binaryMessenger:[registrar messenger]];
UIViewController *viewController = [UIApplication sharedApplication].delegate.window.rootViewController; UIViewController *viewController = [UIApplication sharedApplication].delegate.window.rootViewController;
@ -45,15 +45,16 @@
} }
_result = result; _result = result;
BOOL isMultiplePick = [call.arguments boolValue]; NSDictionary * arguments = call.arguments;
if(isMultiplePick || [call.method isEqualToString:@"ANY"] || [call.method containsString:@"__CUSTOM"]) { BOOL isMultiplePick = ((NSNumber*)[arguments valueForKey:@"allowMultipleSelection"]).boolValue;
self.fileType = [FileUtils resolveType:call.method]; if(isMultiplePick || [call.method isEqualToString:@"ANY"] || [call.method containsString:@"CUSTOM"]) {
if(self.fileType == nil) { self.allowedExtensions = [FileUtils resolveType:call.method withAllowedExtensions: [arguments valueForKey:@"allowedExtensions"]];
if(self.allowedExtensions == nil) {
_result([FlutterError errorWithCode:@"Unsupported file extension" _result([FlutterError errorWithCode:@"Unsupported file extension"
message:@"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." message:@"If you are providing extension filters make sure that you are only using FileType.custom and the extension are provided 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."
details:nil]); details:nil]);
_result = nil; _result = nil;
} else if(self.fileType != nil) { } else if(self.allowedExtensions != nil) {
[self resolvePickDocumentWithMultipleSelection:isMultiplePick]; [self resolvePickDocumentWithMultipleSelection:isMultiplePick];
} }
} else if([call.method isEqualToString:@"VIDEO"]) { } else if([call.method isEqualToString:@"VIDEO"]) {
@ -75,7 +76,7 @@
@try{ @try{
self.documentPickerController = [[UIDocumentPickerViewController alloc] self.documentPickerController = [[UIDocumentPickerViewController alloc]
initWithDocumentTypes:@[self.fileType] initWithDocumentTypes: self.allowedExtensions
inMode:UIDocumentPickerModeImport]; inMode: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);

View File

@ -13,7 +13,7 @@
#endif #endif
@interface FileUtils : NSObject @interface FileUtils : NSObject
+ (NSString*) resolveType:(NSString*)type; + (NSArray<NSString*>*) resolveType:(NSString*)type withAllowedExtensions:(NSArray<NSString*>*)allowedExtensions;
+ (NSArray*) resolvePath:(NSArray<NSURL *> *)urls; + (NSArray*) resolvePath:(NSArray<NSURL *> *)urls;
@end @end

View File

@ -9,28 +9,37 @@
@implementation FileUtils @implementation FileUtils
+ (NSString*) resolveType:(NSString*)type { + (NSArray<NSString*> *) resolveType:(NSString*)type withAllowedExtensions:(NSArray<NSString*>*) allowedExtensions {
BOOL isCustom = [type containsString:@"__CUSTOM_"];
if(isCustom) {
type = [type stringByReplacingOccurrencesOfString:@"__CUSTOM_" withString:@""];
NSString * format = [NSString stringWithFormat:@"dummy.%@", type];
CFStringRef UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)[format pathExtension], NULL);
NSString * UTIString = (__bridge NSString *)(UTI);
CFRelease(UTI);
Log(@"Custom file type: %@", UTIString);
return [UTIString containsString:@"dyn."] ? nil : UTIString;
}
if ([type isEqualToString:@"ANY"]) { if ([type isEqualToString:@"ANY"]) {
return @"public.item"; return @[@"public.item"];
} else if ([type isEqualToString:@"IMAGE"]) { } else if ([type isEqualToString:@"IMAGE"]) {
return @"public.image"; return @[@"public.image"];
} else if ([type isEqualToString:@"VIDEO"]) { } else if ([type isEqualToString:@"VIDEO"]) {
return @"public.movie"; return @[@"public.movie"];
} else if ([type isEqualToString:@"AUDIO"]) { } else if ([type isEqualToString:@"AUDIO"]) {
return @"public.audio"; return @[@"public.audio"];
} else if ([type isEqualToString:@"CUSTOM"]) {
if(allowedExtensions == (id)[NSNull null] || allowedExtensions.count == 0) {
return nil;
}
NSMutableArray<NSString*>* utis = [[NSMutableArray<NSString*> alloc] init];
for(int i = 0 ; i<allowedExtensions.count ; i++){
NSString * format = [NSString stringWithFormat:@"dummy.%@", allowedExtensions[i]];
CFStringRef UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)[format pathExtension], NULL);
NSString * UTIString = (__bridge NSString *)(UTI);
CFRelease(UTI);
if([UTIString containsString:@"dyn."]){
Log(@"[Skipping type] Unsupported file type: %@", UTIString);
continue;
} else{
Log(@"Custom file type supported: %@", UTIString);
[utis addObject: UTIString];
}
}
return utis;
} else { } else {
return nil; return nil;
} }

View File

@ -13,7 +13,8 @@ enum FileType {
class FilePicker { class FilePicker {
FilePicker._(); FilePicker._();
static const MethodChannel _channel = const MethodChannel('miguelruivo.flutter.plugins.file_picker'); static const MethodChannel _channel =
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
@ -22,7 +23,9 @@ 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({FileType type = FileType.any, List<String> allowedExtensions}) async => static Future<Map<String, String>> getMultiFilePath(
{FileType type = FileType.any,
List<String> allowedExtensions}) async =>
await _getPath(_handleType(type), true, allowedExtensions); await _getPath(_handleType(type), true, allowedExtensions);
/// Returns an absolute file path from the calling platform. /// Returns an absolute file path from the calling platform.
@ -30,15 +33,19 @@ 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({FileType type = FileType.any, List<String> allowedExtensions}) async => static Future<String> getFilePath(
{FileType type = FileType.any,
List<String> allowedExtensions}) async =>
await _getPath(_handleType(type), false, allowedExtensions); await _getPath(_handleType(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({FileType type = FileType.any, List<String> allowedExtensions}) async { static Future<File> getFile(
final String filePath = await _getPath(_handleType(type), false, allowedExtensions); {FileType type = FileType.any, List<String> allowedExtensions}) async {
final String filePath =
await _getPath(_handleType(type), false, allowedExtensions);
return filePath != null ? File(filePath) : null; return filePath != null ? File(filePath) : null;
} }
@ -46,14 +53,20 @@ 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({FileType type = FileType.any, List<String> allowedExtensions}) async { static Future<List<File>> getMultiFile(
final Map<String, String> paths = await _getPath(_handleType(type), true, allowedExtensions); {FileType type = FileType.any, List<String> allowedExtensions}) async {
return paths != null && paths.isNotEmpty ? paths.values.map((path) => File(path)).toList() : null; final Map<String, String> paths =
await _getPath(_handleType(type), true, allowedExtensions);
return paths != null && paths.isNotEmpty
? paths.values.map((path) => File(path)).toList()
: null;
} }
static Future<dynamic> _getPath(String type, bool allowMultipleSelection, List<String> allowedExtensions) async { 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('If you are using a custom extension filter, please use the FileType.custom instead.'); throw Exception(
'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, {
@ -64,14 +77,16 @@ class FilePicker {
if (result is String) { if (result is String) {
result = [result]; result = [result];
} }
return Map<String, String>.fromIterable(result, key: (path) => path.split('/').last, value: (path) => path); return Map<String, String>.fromIterable(result,
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('[$_tag] Unsupported operation. Method not found. The exception thrown was: $e'); print(
'[$_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.5.1 version: 1.6.0
dependencies: dependencies:
flutter: flutter: