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
* 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
* 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<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.
## 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

View File

@ -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 {

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 {
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<FilePickerDemo> {
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<FilePickerDemo> {
onChanged: (value) => setState(() {
_pickingType = value;
if (_pickingType != FileType.custom) {
_controller.text = _extension = null;
_controller.text = _extension = '';
}
})),
),
@ -99,7 +109,8 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
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<FilePickerDemo> {
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<FilePickerDemo> {
),
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<FilePickerDemo> {
subtitle: new Text(path),
);
},
separatorBuilder: (BuildContext context, int index) => new Divider(),
separatorBuilder:
(BuildContext context, int index) =>
new Divider(),
)),
)
: new Container(),

View File

@ -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{}

View File

@ -9,14 +9,14 @@
@property (nonatomic) UIDocumentPickerViewController *documentPickerController;
@property (nonatomic) UIDocumentInteractionController *interactionController;
@property (nonatomic) MPMediaPickerController *audioPickerController;
@property (nonatomic) NSString * fileType;
@property (nonatomic) NSArray<NSString *> * allowedExtensions;
@end
@implementation FilePickerPlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)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);

View File

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

View File

@ -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<NSString*> *) resolveType:(NSString*)type withAllowedExtensions:(NSArray<NSString*>*) 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<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 {
return nil;
}

View File

@ -13,7 +13,8 @@ enum FileType {
class 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';
/// 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.
/// 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<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);
/// 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<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);
/// 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<File> getFile({FileType type = FileType.any, List<String> allowedExtensions}) async {
final String filePath = await _getPath(_handleType(type), false, allowedExtensions);
static Future<File> getFile(
{FileType type = FileType.any, List<String> 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<List<File>> getMultiFile({FileType type = FileType.any, List<String> allowedExtensions}) async {
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<List<File>> getMultiFile(
{FileType type = FileType.any, List<String> allowedExtensions}) async {
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)) {
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<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;
} 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;
}
}

View File

@ -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: