From a33ece96ed65b6a7bc7a8302c3871f5aceff93ac Mon Sep 17 00:00:00 2001 From: Miguel Ruivo Date: Wed, 6 Mar 2019 01:16:35 +0000 Subject: [PATCH] adds multiple file selection option on iOS --- example/lib/main.dart | 155 ++++++++++++++++++--------------- ios/Classes/FilePickerPlugin.m | 22 +++-- ios/Classes/FileUtils.h | 2 +- ios/Classes/FileUtils.m | 7 +- lib/file_picker.dart | 55 +++++++----- 5 files changed, 136 insertions(+), 105 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 5d85465..ac84eba 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -11,9 +11,11 @@ class FilePickerDemo extends StatefulWidget { } class _FilePickerDemoState extends State { - String _fileName = '...'; - String _path = '...'; + String _fileName; + String _path; + Map _paths; String _extension; + bool _multiPick = false; bool _hasValidMime = false; FileType _pickingType; TextEditingController _controller = new TextEditingController(); @@ -27,7 +29,12 @@ class _FilePickerDemoState extends State { void _openFileExplorer() async { if (_pickingType != FileType.CUSTOM || _hasValidMime) { try { - _path = await FilePicker.getFilePath(type: _pickingType, fileExtension: _extension); + if (_multiPick) { + _paths = await FilePicker.getMultiFilePath(fileExtension: _extension); + print("cenas"); + } else { + _path = await FilePicker.getFilePath(type: _pickingType, fileExtension: _extension); + } } on PlatformException catch (e) { print("Unsupported operation" + e.toString()); } @@ -35,7 +42,7 @@ class _FilePickerDemoState extends State { if (!mounted) return; setState(() { - _fileName = _path != null ? _path.split('/').last : '...'; + _fileName = _path != null ? _path.split('/').last : _paths != null ? _paths.keys.toString() : '...'; }); } } @@ -45,51 +52,47 @@ class _FilePickerDemoState extends State { return new MaterialApp( home: new Scaffold( appBar: new AppBar( - title: const Text('Plugin example app'), + title: const Text('File Picker example app'), ), body: SingleChildScrollView( child: new Center( child: new Padding( padding: const EdgeInsets.only(top: 50.0, left: 10.0, right: 10.0), - child: new Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - new Padding( - padding: const EdgeInsets.only(top: 20.0), - child: new DropdownButton( - hint: new Text('LOAD PATH FROM'), - value: _pickingType, - items: [ - // new DropdownMenuItem( - // child: new Text('FROM CAMERA'), - // value: FileType.CAMERA, - // ), - new DropdownMenuItem( - child: new Text('FROM AUDIO'), - value: FileType.AUDIO, - ), - new DropdownMenuItem( - child: new Text('FROM GALLERY'), - value: FileType.IMAGE, - ), - new DropdownMenuItem( - child: new Text('FROM VIDEO'), - value: FileType.VIDEO, - ), - new DropdownMenuItem( - child: new Text('FROM ANY'), - value: FileType.ANY, - ), - new DropdownMenuItem( - child: new Text('CUSTOM FORMAT'), - value: FileType.CUSTOM, - ), - ], - onChanged: (value) => setState(() => _pickingType = value)), - ), - new ConstrainedBox( - constraints: new BoxConstraints(maxWidth: 150.0), - child: _pickingType == FileType.CUSTOM + child: new ConstrainedBox( + constraints: new BoxConstraints(maxWidth: 200.0), + child: new Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + new Padding( + padding: const EdgeInsets.only(top: 20.0), + child: new DropdownButton( + hint: new Text('LOAD PATH FROM'), + value: _pickingType, + items: [ + new DropdownMenuItem( + child: new Text('FROM AUDIO'), + value: FileType.AUDIO, + ), + new DropdownMenuItem( + child: new Text('FROM GALLERY'), + value: FileType.IMAGE, + ), + new DropdownMenuItem( + child: new Text('FROM VIDEO'), + value: FileType.VIDEO, + ), + new DropdownMenuItem( + child: new Text('FROM ANY'), + value: FileType.ANY, + ), + new DropdownMenuItem( + child: new Text('CUSTOM FORMAT'), + value: FileType.CUSTOM, + ), + ], + onChanged: (value) => setState(() => _pickingType = value)), + ), + _pickingType == FileType.CUSTOM ? new TextFormField( maxLength: 20, autovalidate: true, @@ -107,38 +110,46 @@ class _FilePickerDemoState extends State { }, ) : new Container(), - ), - new Padding( - padding: const EdgeInsets.only(top: 50.0, bottom: 20.0), - child: new RaisedButton( - onPressed: () => _openFileExplorer(), - child: new Text("Open file picker"), + new Visibility( + visible: _pickingType == FileType.ANY || _pickingType == FileType.CUSTOM, + child: new SwitchListTile.adaptive( + title: new Text('Pick multiple files', textAlign: TextAlign.right), + onChanged: (bool value) => setState(() => _multiPick = value), + value: _multiPick, + ), ), - ), - new Text( - 'URI PATH ', - textAlign: TextAlign.center, - style: new TextStyle(fontWeight: FontWeight.bold), - ), - new Text( - _path ?? '...', - textAlign: TextAlign.center, - softWrap: true, - textScaleFactor: 0.85, - ), - new Padding( - padding: const EdgeInsets.only(top: 10.0), - child: new Text( - 'FILE NAME ', + new Padding( + padding: const EdgeInsets.only(top: 50.0, bottom: 20.0), + child: new RaisedButton( + onPressed: () => _openFileExplorer(), + child: new Text("Open file picker"), + ), + ), + new Text( + 'URI PATH ', textAlign: TextAlign.center, style: new TextStyle(fontWeight: FontWeight.bold), ), - ), - new Text( - _fileName, - textAlign: TextAlign.center, - ), - ], + new Text( + _path ?? _paths?.values?.map((path) => path + '\n\n').toString() ?? '...', + textAlign: TextAlign.center, + softWrap: true, + textScaleFactor: 0.85, + ), + new Padding( + padding: const EdgeInsets.only(top: 10.0), + child: new Text( + 'FILE NAME ', + textAlign: TextAlign.center, + style: new TextStyle(fontWeight: FontWeight.bold), + ), + ), + new Text( + _fileName ?? '...', + textAlign: TextAlign.center, + ), + ], + ), ), )), ), diff --git a/ios/Classes/FilePickerPlugin.m b/ios/Classes/FilePickerPlugin.m index bfb6ff7..4dbcf4b 100644 --- a/ios/Classes/FilePickerPlugin.m +++ b/ios/Classes/FilePickerPlugin.m @@ -7,8 +7,8 @@ @property (nonatomic) UIViewController *viewController; @property (nonatomic) UIImagePickerController *galleryPickerController; @property (nonatomic) UIDocumentPickerViewController *pickerController; -@property (nonatomic) MPMediaPickerController *audioPickerController; @property (nonatomic) UIDocumentInteractionController *interactionController; +@property (nonatomic) MPMediaPickerController *audioPickerController; @property (nonatomic) NSString * fileType; @end @@ -60,7 +60,7 @@ if(self.fileType == nil){ result(FlutterMethodNotImplemented); } else { - [self resolvePickDocument]; + [self resolvePickDocumentWithMultipleSelection:call.arguments]; } } @@ -68,17 +68,18 @@ #pragma mark - Resolvers -- (void)resolvePickDocument { +- (void)resolvePickDocumentWithMultipleSelection:(BOOL)allowsMultipleSelection { self.pickerController = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[self.fileType] inMode:UIDocumentPickerModeImport]; if (@available(iOS 11.0, *)) { - self.pickerController.allowsMultipleSelection = NO; - } else { - // Fallback on earlier versions + self.pickerController.allowsMultipleSelection = allowsMultipleSelection; + } else if(allowsMultipleSelection) { + NSLog(@"Multiple file selection is only supported on iOS 11 and above. Single selection will be used."); } + self.pickerController.delegate = self; self.pickerController.modalPresentationStyle = UIModalPresentationCurrentContext; self.galleryPickerController.allowsEditing = NO; @@ -124,7 +125,14 @@ didPickDocumentsAtURLs:(NSArray *)urls{ [self.pickerController dismissViewControllerAnimated:YES completion:nil]; - _result([FileUtils resolvePath:urls]); + NSArray * result = [FileUtils resolvePath:urls]; + + if([result count] > 1) { + _result(result); + } else { + _result([result objectAtIndex:0]); + } + } diff --git a/ios/Classes/FileUtils.h b/ios/Classes/FileUtils.h index a02ba06..5943cdf 100644 --- a/ios/Classes/FileUtils.h +++ b/ios/Classes/FileUtils.h @@ -7,7 +7,7 @@ #import @interface FileUtils : NSObject + (NSString*) resolveType:(NSString*)type; -+ (NSString*) resolvePath:(NSArray *)urls; ++ (NSArray*) resolvePath:(NSArray *)urls; @end diff --git a/ios/Classes/FileUtils.m b/ios/Classes/FileUtils.m index 805ebca..519af99 100644 --- a/ios/Classes/FileUtils.m +++ b/ios/Classes/FileUtils.m @@ -30,15 +30,16 @@ } } - -+ (NSString*) resolvePath:(NSArray *)urls{ ++ (NSMutableArray*) resolvePath:(NSArray *)urls{ NSString * uri; + NSMutableArray * paths = [[NSMutableArray alloc] init]; for (NSURL *url in urls) { uri = (NSString *)[url path]; + [paths addObject:uri]; } - return uri; + return paths; } @end diff --git a/lib/file_picker.dart b/lib/file_picker.dart index 4527d49..833fbcf 100644 --- a/lib/file_picker.dart +++ b/lib/file_picker.dart @@ -1,15 +1,14 @@ import 'dart:async'; import 'package:flutter/services.dart'; -// import 'package:image_picker/image_picker.dart'; -/// Supported file types, [ANY] should be used if the file you need isn't listed +String _kCustomType = '__CUSTOM_'; + enum FileType { ANY, IMAGE, VIDEO, AUDIO, - // CAMERA, CUSTOM, } @@ -17,27 +16,34 @@ class FilePicker { static const MethodChannel _channel = const MethodChannel('file_picker'); static const String _tag = 'FilePicker'; - static Future _getPath(String type) async { + static Future _getPath(String type, [bool multipleSelection = false]) async { try { - return await _channel.invokeMethod(type); + dynamic result = await _channel.invokeMethod(type, multipleSelection); + if (multipleSelection) { + if (result is String) { + result = [result]; + } + return Map.fromIterable(result, key: (path) => path.split('/').last, value: (path) => path); + } + return result; } on PlatformException catch (e) { print("[$_tag] Platform exception: " + e.toString()); } catch (e) { + print(e.toString()); print( "[$_tag] Unsupported operation. This probably have happened because [${type.split('_').last}] is an unsupported file type. You may want to try FileType.ALL instead."); } return null; } - // static Future _getImage(ImageSource type) async { - // try { - // var image = await ImagePicker.pickImage(source: type); - // return image?.path; - // } on PlatformException catch (e) { - // print("[$_tag] Platform exception: " + e.toString()); - // } - // return null; - // } + /// Returns an iterable `Map` where the `key` is the name of the file + /// and the `value` the path. + /// + /// A [fileExtension] can be provided to filter the picking results. + /// If provided, it will be use the `FileType.CUSTOM` for that [fileExtension]. + /// If not, `FileType.ANY` will be used and any combination of files can be multi picked at once. + static Future> getMultiFilePath({String fileExtension}) async => + await _getPath(fileExtension != null ? (_kCustomType + fileExtension) : 'ANY', true); /// Returns an absolute file path from the calling platform /// @@ -45,21 +51,26 @@ class FilePicker { /// Can be used a custom file type with `FileType.CUSTOM`. A [fileExtension] must be provided (e.g. PDF, SVG, etc.) /// Defaults to `FileType.ANY` which will display all file types. static Future getFilePath({FileType type = FileType.ANY, String fileExtension}) async { + var path; switch (type) { case FileType.IMAGE: - return _getPath('IMAGE'); - // case FileType.CAMERA: - // return _getImage(ImageSource.camera); + path = _getPath('IMAGE'); + break; case FileType.AUDIO: - return _getPath('AUDIO'); + path = _getPath('AUDIO'); + break; case FileType.VIDEO: - return _getPath('VIDEO'); + path = _getPath('VIDEO'); + break; case FileType.ANY: - return _getPath('ANY'); + path = _getPath('ANY'); + break; case FileType.CUSTOM: - return _getPath('__CUSTOM_' + (fileExtension ?? '')); + path = _getPath(_kCustomType + (fileExtension ?? '')); + break; default: - return _getPath('ANY'); + break; } + return await path; } }