diff --git a/CHANGELOG.md b/CHANGELOG.md index 53fd073..be38935 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 * 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.); diff --git a/README.md b/README.md index 19f4ec5..fc76c6c 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,8 @@ A package that allows you to use a native file explorer to pick single or multip ## Currently supported features * 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 **multiple files** optionally, supplying a file extension +* Load path from a **custom format** by providing a list of file extensions (pdf, svg, zip, etc.) +* Load path from **multiple files** optionally, supplying file extensions * Load path from **gallery** * Load path from **audio** * 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) * [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) +5. [Troubleshooting](https://github.com/miguelpruivo/flutter_file_picker/wiki/Troubleshooting) ## Usage Quick simple usage example: @@ -54,10 +55,18 @@ File file = await FilePicker.getFile(); ``` List files = await FilePicker.getMultiFile(); ``` +#### Multiple files with extension filter +``` + List 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. ## Example App ![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 diff --git a/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerPlugin.java b/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerPlugin.java index 9e79e0e..292d579 100644 --- a/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerPlugin.java +++ b/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerPlugin.java @@ -30,7 +30,7 @@ import io.flutter.plugin.common.PluginRegistry.Registrar; public class FilePickerPlugin implements MethodChannel.MethodCallHandler, FlutterPlugin, ActivityAware { 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 implements Application.ActivityLifecycleCallbacks, DefaultLifecycleObserver { diff --git a/example/example_ios.gif b/example/example_ios.gif new file mode 100644 index 0000000..ec2857f Binary files /dev/null and b/example/example_ios.gif differ diff --git a/example/lib/src/file_picker_demo.dart b/example/lib/src/file_picker_demo.dart index 051c29f..84355af 100644 --- a/example/lib/src/file_picker_demo.dart +++ b/example/lib/src/file_picker_demo.dart @@ -29,10 +29,18 @@ class _FilePickerDemoState extends State { try { if (_multiPick) { _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 { _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) { print("Unsupported operation" + e.toString()); @@ -40,7 +48,9 @@ class _FilePickerDemoState extends State { if (!mounted) return; setState(() { _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 { onChanged: (value) => setState(() { _pickingType = value; if (_pickingType != FileType.custom) { - _controller.text = _extension = null; + _controller.text = _extension = ''; } })), ), @@ -99,7 +109,8 @@ class _FilePickerDemoState extends State { maxLength: 15, autovalidate: true, controller: _controller, - decoration: InputDecoration(labelText: 'File extension'), + decoration: + InputDecoration(labelText: 'File extension'), keyboardType: TextInputType.text, textCapitalization: TextCapitalization.none, ) @@ -108,8 +119,10 @@ class _FilePickerDemoState extends State { new ConstrainedBox( constraints: BoxConstraints.tightFor(width: 200.0), child: new SwitchListTile.adaptive( - title: new Text('Pick multiple files', textAlign: TextAlign.right), - onChanged: (bool value) => setState(() => _multiPick = value), + title: new Text('Pick multiple files', + textAlign: TextAlign.right), + onChanged: (bool value) => + setState(() => _multiPick = value), value: _multiPick, ), ), @@ -122,18 +135,28 @@ class _FilePickerDemoState extends State { ), new Builder( 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 ? new Container( padding: const EdgeInsets.only(bottom: 30.0), height: MediaQuery.of(context).size.height * 0.50, child: new Scrollbar( 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) { - final bool isMultiPath = _paths != null && _paths.isNotEmpty; - final String name = 'File $index: ' + (isMultiPath ? _paths.keys.toList()[index] : _fileName ?? '...'); - final path = isMultiPath ? _paths.values.toList()[index].toString() : _path; + final bool isMultiPath = + _paths != null && _paths.isNotEmpty; + final String name = 'File $index: ' + + (isMultiPath + ? _paths.keys.toList()[index] + : _fileName ?? '...'); + final path = isMultiPath + ? _paths.values.toList()[index].toString() + : _path; return new ListTile( title: new Text( @@ -142,7 +165,9 @@ class _FilePickerDemoState extends State { subtitle: new Text(path), ); }, - separatorBuilder: (BuildContext context, int index) => new Divider(), + separatorBuilder: + (BuildContext context, int index) => + new Divider(), )), ) : new Container(), diff --git a/go/plugin.go b/go/plugin.go index d33cb76..bd61a3e 100644 --- a/go/plugin.go +++ b/go/plugin.go @@ -7,7 +7,7 @@ import ( "github.com/pkg/errors" ) -const channelName = "miguelruivo.flutter.plugins.file_picker" +const channelName = "miguelruivo.flutter.plugins.filepicker" type FilePickerPlugin struct{} diff --git a/ios/Classes/FilePickerPlugin.m b/ios/Classes/FilePickerPlugin.m index 1a47da8..8c356f4 100644 --- a/ios/Classes/FilePickerPlugin.m +++ b/ios/Classes/FilePickerPlugin.m @@ -9,14 +9,14 @@ @property (nonatomic) UIDocumentPickerViewController *documentPickerController; @property (nonatomic) UIDocumentInteractionController *interactionController; @property (nonatomic) MPMediaPickerController *audioPickerController; -@property (nonatomic) NSString * fileType; +@property (nonatomic) NSArray * allowedExtensions; @end @implementation FilePickerPlugin + (void)registerWithRegistrar:(NSObject*)registrar { FlutterMethodChannel* channel = [FlutterMethodChannel - methodChannelWithName:@"miguelruivo.flutter.plugins.file_picker" + methodChannelWithName:@"miguelruivo.flutter.plugins.filepicker" binaryMessenger:[registrar messenger]]; UIViewController *viewController = [UIApplication sharedApplication].delegate.window.rootViewController; @@ -45,15 +45,16 @@ } _result = result; - BOOL isMultiplePick = [call.arguments boolValue]; - if(isMultiplePick || [call.method isEqualToString:@"ANY"] || [call.method containsString:@"__CUSTOM"]) { - self.fileType = [FileUtils resolveType:call.method]; - if(self.fileType == nil) { + NSDictionary * arguments = call.arguments; + BOOL isMultiplePick = ((NSNumber*)[arguments valueForKey:@"allowMultipleSelection"]).boolValue; + if(isMultiplePick || [call.method isEqualToString:@"ANY"] || [call.method containsString:@"CUSTOM"]) { + self.allowedExtensions = [FileUtils resolveType:call.method withAllowedExtensions: [arguments valueForKey:@"allowedExtensions"]]; + if(self.allowedExtensions == nil) { _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]); _result = nil; - } else if(self.fileType != nil) { + } else if(self.allowedExtensions != nil) { [self resolvePickDocumentWithMultipleSelection:isMultiplePick]; } } else if([call.method isEqualToString:@"VIDEO"]) { @@ -75,7 +76,7 @@ @try{ self.documentPickerController = [[UIDocumentPickerViewController alloc] - initWithDocumentTypes:@[self.fileType] + initWithDocumentTypes: self.allowedExtensions inMode:UIDocumentPickerModeImport]; } @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); diff --git a/ios/Classes/FileUtils.h b/ios/Classes/FileUtils.h index 6c04ba6..94e125a 100644 --- a/ios/Classes/FileUtils.h +++ b/ios/Classes/FileUtils.h @@ -13,7 +13,7 @@ #endif @interface FileUtils : NSObject -+ (NSString*) resolveType:(NSString*)type; ++ (NSArray*) resolveType:(NSString*)type withAllowedExtensions:(NSArray*)allowedExtensions; + (NSArray*) resolvePath:(NSArray *)urls; @end diff --git a/ios/Classes/FileUtils.m b/ios/Classes/FileUtils.m index 5f941ba..03e32c0 100644 --- a/ios/Classes/FileUtils.m +++ b/ios/Classes/FileUtils.m @@ -9,28 +9,37 @@ @implementation FileUtils -+ (NSString*) resolveType:(NSString*)type { - - 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; - } ++ (NSArray *) resolveType:(NSString*)type withAllowedExtensions:(NSArray*) allowedExtensions { if ([type isEqualToString:@"ANY"]) { - return @"public.item"; + return @[@"public.item"]; } else if ([type isEqualToString:@"IMAGE"]) { - return @"public.image"; + return @[@"public.image"]; } else if ([type isEqualToString:@"VIDEO"]) { - return @"public.movie"; + return @[@"public.movie"]; } 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* utis = [[NSMutableArray alloc] init]; + + for(int i = 0 ; i` 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. /// 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. - static Future> getMultiFilePath({FileType type = FileType.any, List allowedExtensions}) async => + static Future> getMultiFilePath( + {FileType type = FileType.any, + List allowedExtensions}) async => await _getPath(_handleType(type), true, allowedExtensions); /// 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` /// of [allowedExtensions] (e.g. [`pdf`, `svg`, `jpg`].). /// Defaults to `FileType.any` which will display all file types. - static Future getFilePath({FileType type = FileType.any, List allowedExtensions}) async => + static Future getFilePath( + {FileType type = FileType.any, + List allowedExtensions}) async => await _getPath(_handleType(type), false, allowedExtensions); /// 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 /// you are planing to create a `File` for the returned path. - static Future getFile({FileType type = FileType.any, List allowedExtensions}) async { - final String filePath = await _getPath(_handleType(type), false, allowedExtensions); + static Future getFile( + {FileType type = FileType.any, List allowedExtensions}) async { + final String filePath = + await _getPath(_handleType(type), false, allowedExtensions); 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 /// you are planing to create a list of `File`s for the returned paths. - static Future> getMultiFile({FileType type = FileType.any, List allowedExtensions}) async { - final Map paths = await _getPath(_handleType(type), true, allowedExtensions); - return paths != null && paths.isNotEmpty ? paths.values.map((path) => File(path)).toList() : null; + static Future> getMultiFile( + {FileType type = FileType.any, List allowedExtensions}) async { + final Map paths = + await _getPath(_handleType(type), true, allowedExtensions); + return paths != null && paths.isNotEmpty + ? paths.values.map((path) => File(path)).toList() + : null; } - static Future _getPath(String type, bool allowMultipleSelection, List allowedExtensions) async { + static Future _getPath(String type, bool allowMultipleSelection, + List allowedExtensions) async { 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 { dynamic result = await _channel.invokeMethod(type, { @@ -64,14 +77,16 @@ class FilePicker { if (result is String) { result = [result]; } - return Map.fromIterable(result, key: (path) => path.split('/').last, value: (path) => path); + return Map.fromIterable(result, + key: (path) => path.split('/').last, value: (path) => path); } return result; } on PlatformException catch (e) { print('[$_tag] Platform exception: $e'); rethrow; } 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; } } diff --git a/pubspec.yaml b/pubspec.yaml index 6b63c6d..7bcc9f2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ 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. homepage: https://github.com/miguelpruivo/plugins_flutter_file_picker -version: 1.5.1 +version: 1.6.0 dependencies: flutter: