diff --git a/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java b/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java index b89cdb8..27b70fa 100644 --- a/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java +++ b/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java @@ -18,6 +18,7 @@ import androidx.core.app.ActivityCompat; import java.io.File; import java.util.ArrayList; +import java.util.Arrays; import io.flutter.plugin.common.EventChannel; import io.flutter.plugin.common.MethodChannel; @@ -100,11 +101,9 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener Log.i(FilePickerDelegate.TAG, "[MultiFilePick] File #" + currentItem + " - URI: " + currentUri.getPath()); currentItem++; } - if (paths.size() > 1) { - finishWithSuccess(paths); - } else { - finishWithSuccess(paths.get(0)); - } + + finishWithSuccess(paths); + } else if (data.getData() != null) { Uri uri = data.getData(); String fullPath; @@ -125,7 +124,7 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener if (fullPath != null) { Log.i(FilePickerDelegate.TAG, "Absolute file path:" + fullPath); - finishWithSuccess(fullPath); + finishWithSuccess(Arrays.asList(fullPath)); } else { finishWithError("unknown_path", "Failed to retrieve path."); } diff --git a/file_picker/example/README.md b/file_picker/example/README.md new file mode 100644 index 0000000..a135626 --- /dev/null +++ b/file_picker/example/README.md @@ -0,0 +1,16 @@ +# example + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/file_picker/example/android/app/build.gradle b/file_picker/example/android/app/build.gradle index ae8e6ae..97913c1 100644 --- a/file_picker/example/android/app/build.gradle +++ b/file_picker/example/android/app/build.gradle @@ -22,7 +22,7 @@ android { } defaultConfig { - applicationId "com.mr.flutter.plugin.filepickerexample" + applicationId "com.mr.flutter.plugin.filepicker.example" minSdkVersion 16 targetSdkVersion 29 versionCode 1 diff --git a/file_picker/example/android/app/src/debug/AndroidManifest.xml b/file_picker/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..66063d7 --- /dev/null +++ b/file_picker/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/file_picker/example/android/app/src/main/AndroidManifest.xml b/file_picker/example/android/app/src/main/AndroidManifest.xml index 91966c1..6365b83 100644 --- a/file_picker/example/android/app/src/main/AndroidManifest.xml +++ b/file_picker/example/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ + + diff --git a/file_picker/example/ios/Flutter/.last_build_id b/file_picker/example/ios/Flutter/.last_build_id new file mode 100644 index 0000000..0b85c88 --- /dev/null +++ b/file_picker/example/ios/Flutter/.last_build_id @@ -0,0 +1 @@ +0915ff87e81a3bfb122df4ced418a2b0 \ No newline at end of file diff --git a/file_picker/example/ios/Runner.xcodeproj/project.pbxproj b/file_picker/example/ios/Runner.xcodeproj/project.pbxproj index 97a44e0..039ac22 100644 --- a/file_picker/example/ios/Runner.xcodeproj/project.pbxproj +++ b/file_picker/example/ios/Runner.xcodeproj/project.pbxproj @@ -310,7 +310,6 @@ /* Begin XCBuildConfiguration section */ 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -366,7 +365,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; diff --git a/file_picker/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/file_picker/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/file_picker/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/file_picker/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/file_picker/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/file_picker/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/file_picker/example/ios/Runner/AppDelegate.swift b/file_picker/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/file_picker/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/file_picker/example/ios/Runner/Runner-Bridging-Header.h b/file_picker/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/file_picker/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/file_picker/example/lib/generated_plugin_registrant.dart b/file_picker/example/lib/generated_plugin_registrant.dart new file mode 100644 index 0000000..daa72e8 --- /dev/null +++ b/file_picker/example/lib/generated_plugin_registrant.dart @@ -0,0 +1,16 @@ +// +// Generated file. Do not edit. +// + +// ignore: unused_import +import 'dart:ui'; + +import 'package:file_picker/src/file_picker_web.dart'; + +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + +// ignore: public_member_api_docs +void registerPlugins(PluginRegistry registry) { + FilePickerWeb.registerWith(registry.registrarFor(FilePickerWeb)); + registry.registerMessageHandler(); +} diff --git a/file_picker/example/lib/src/file_picker_demo.dart b/file_picker/example/lib/src/file_picker_demo.dart index 47ac026..99f61ee 100644 --- a/file_picker/example/lib/src/file_picker_demo.dart +++ b/file_picker/example/lib/src/file_picker_demo.dart @@ -5,19 +5,18 @@ import 'package:file_picker/file_picker.dart'; class FilePickerDemo extends StatefulWidget { @override - _FilePickerDemoState createState() => new _FilePickerDemoState(); + _FilePickerDemoState createState() => _FilePickerDemoState(); } class _FilePickerDemoState extends State { final GlobalKey _scaffoldKey = GlobalKey(); String _fileName; - String _path; - Map _paths; + List _paths; String _extension; bool _loadingPath = false; bool _multiPick = false; FileType _pickingType = FileType.any; - TextEditingController _controller = new TextEditingController(); + TextEditingController _controller = TextEditingController(); @override void initState() { @@ -28,97 +27,84 @@ class _FilePickerDemoState extends State { void _openFileExplorer() async { setState(() => _loadingPath = true); try { - if (_multiPick) { - _path = null; - _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?.isNotEmpty ?? false) - ? _extension?.replaceAll(' ', '')?.split(',') - : null, - ); - } + _paths = (await FilePicker.instance.pickFiles( + type: _pickingType, + allowMultiple: _multiPick, + allowedExtensions: (_extension?.isNotEmpty ?? false) ? _extension?.replaceAll(' ', '')?.split(',') : null, + )) + ?.files; } on PlatformException catch (e) { print("Unsupported operation" + e.toString()); + } catch (ex) { + print(ex); } if (!mounted) return; setState(() { _loadingPath = false; - _fileName = _path != null - ? _path.split('/').last - : _paths != null ? _paths.keys.toString() : '...'; + _fileName = _paths != null ? _paths.map((e) => e.name).toString() : '...'; }); } void _clearCachedFiles() { - FilePicker.clearTemporaryFiles().then((result) { + FilePicker.instance.clearTemporaryFiles().then((result) { _scaffoldKey.currentState.showSnackBar( SnackBar( backgroundColor: result ? Colors.green : Colors.red, - content: Text((result - ? 'Temporary files removed with success.' - : 'Failed to clean temporary files')), + content: Text((result ? 'Temporary files removed with success.' : 'Failed to clean temporary files')), ), ); }); } void _selectFolder() { - FilePicker.getDirectoryPath().then((value) { - setState(() => _path = value); + FilePicker.instance.getDirectoryPath().then((value) { + setState(() => _paths = [value]); }); } @override Widget build(BuildContext context) { - return new MaterialApp( - home: new Scaffold( + return MaterialApp( + home: Scaffold( key: _scaffoldKey, - appBar: new AppBar( + appBar: AppBar( title: const Text('File Picker example app'), ), - body: new Center( - child: new Padding( + body: Center( + child: Padding( padding: const EdgeInsets.only(left: 10.0, right: 10.0), - child: new SingleChildScrollView( - child: new Column( + child: SingleChildScrollView( + child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - new Padding( + Padding( padding: const EdgeInsets.only(top: 20.0), - child: new DropdownButton( - hint: new Text('LOAD PATH FROM'), + child: DropdownButton( + hint: Text('LOAD PATH FROM'), value: _pickingType, items: [ - new DropdownMenuItem( - child: new Text('FROM AUDIO'), + DropdownMenuItem( + child: Text('FROM AUDIO'), value: FileType.audio, ), - new DropdownMenuItem( - child: new Text('FROM IMAGE'), + DropdownMenuItem( + child: Text('FROM IMAGE'), value: FileType.image, ), - new DropdownMenuItem( - child: new Text('FROM VIDEO'), + DropdownMenuItem( + child: Text('FROM VIDEO'), value: FileType.video, ), - new DropdownMenuItem( - child: new Text('FROM MEDIA'), + DropdownMenuItem( + child: Text('FROM MEDIA'), value: FileType.media, ), - new DropdownMenuItem( - child: new Text('FROM ANY'), + DropdownMenuItem( + child: Text('FROM ANY'), value: FileType.any, ), - new DropdownMenuItem( - child: new Text('CUSTOM FORMAT'), + DropdownMenuItem( + child: Text('CUSTOM FORMAT'), value: FileType.custom, ), ], @@ -129,87 +115,76 @@ class _FilePickerDemoState extends State { } })), ), - new ConstrainedBox( - constraints: BoxConstraints.tightFor(width: 100.0), + ConstrainedBox( + constraints: const BoxConstraints.tightFor(width: 100.0), child: _pickingType == FileType.custom - ? new TextFormField( + ? TextFormField( maxLength: 15, autovalidate: true, controller: _controller, - decoration: - InputDecoration(labelText: 'File extension'), + decoration: InputDecoration(labelText: 'File extension'), keyboardType: TextInputType.text, textCapitalization: TextCapitalization.none, ) - : new Container(), + : const SizedBox(), ), - 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), + ConstrainedBox( + constraints: const BoxConstraints.tightFor(width: 200.0), + child: SwitchListTile.adaptive( + title: Text('Pick multiple files', textAlign: TextAlign.right), + onChanged: (bool value) => setState(() => _multiPick = value), value: _multiPick, ), ), - new Padding( + Padding( padding: const EdgeInsets.only(top: 50.0, bottom: 20.0), child: Column( children: [ - new RaisedButton( + RaisedButton( onPressed: () => _openFileExplorer(), - child: new Text("Open file picker"), + child: Text("Open file picker"), ), - new RaisedButton( + RaisedButton( onPressed: () => _selectFolder(), - child: new Text("Pick folder"), + child: Text("Pick folder"), ), - new RaisedButton( + RaisedButton( onPressed: () => _clearCachedFiles(), - child: new Text("Clear temporary files"), + child: Text("Clear temporary files"), ), ], ), ), - new Builder( + Builder( builder: (BuildContext context) => _loadingPath ? Padding( padding: const EdgeInsets.only(bottom: 10.0), - child: const CircularProgressIndicator()) - : _path != null || _paths != null - ? new Container( + child: const CircularProgressIndicator(), + ) + : _paths != null + ? 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, + child: Scrollbar( + child: ListView.separated( + 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.map((e) => e.name).toList()[index] : _fileName ?? '...'); + final path = _paths.map((e) => e.path).toList()[index].toString(); - return new ListTile( - title: new Text( + return ListTile( + title: Text( name, ), - subtitle: new Text(path), + subtitle: Text(path), ); }, - separatorBuilder: - (BuildContext context, int index) => - new Divider(), + separatorBuilder: (BuildContext context, int index) => const Divider(), )), ) - : new Container(), + : const SizedBox(), ), ], ), diff --git a/file_picker/example/test/widget_test.dart b/file_picker/example/test/widget_test.dart new file mode 100644 index 0000000..747db1d --- /dev/null +++ b/file_picker/example/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:example/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/file_picker/example/web/favicon.png b/file_picker/example/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/file_picker/example/web/favicon.png differ diff --git a/file_picker/example/web/icons/Icon-192.png b/file_picker/example/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/file_picker/example/web/icons/Icon-192.png differ diff --git a/file_picker/example/web/icons/Icon-512.png b/file_picker/example/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/file_picker/example/web/icons/Icon-512.png differ diff --git a/file_picker/example/web/index.html b/file_picker/example/web/index.html new file mode 100644 index 0000000..9b7a438 --- /dev/null +++ b/file_picker/example/web/index.html @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + example + + + + + + + + diff --git a/file_picker/example/web/manifest.json b/file_picker/example/web/manifest.json new file mode 100644 index 0000000..c638001 --- /dev/null +++ b/file_picker/example/web/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "example", + "short_name": "example", + "start_url": ".", + "display": "minimal-ui", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/file_picker/ios/Classes/FilePickerPlugin.m b/file_picker/ios/Classes/FilePickerPlugin.m index ab65a33..72738aa 100644 --- a/file_picker/ios/Classes/FilePickerPlugin.m +++ b/file_picker/ios/Classes/FilePickerPlugin.m @@ -253,7 +253,7 @@ - (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url{ [self.documentPickerController dismissViewControllerAnimated:YES completion:nil]; NSString * path = (NSString *)[url path]; - _result(path); + _result(@[path]); _result = nil; } @@ -267,12 +267,7 @@ didPickDocumentsAtURLs:(NSArray *)urls{ [self.documentPickerController dismissViewControllerAnimated:YES completion:nil]; NSArray * result = [FileUtils resolvePath:urls]; - - if([result count] > 1) { - _result(result); - } else { - _result([result objectAtIndex:0]); - } + _result(result); _result = nil; } @@ -323,7 +318,7 @@ didPickDocumentsAtURLs:(NSArray *)urls{ return; } - _result([pickedVideoUrl != nil ? pickedVideoUrl : pickedImageUrl path]); + _result(@[[pickedVideoUrl != nil ? pickedVideoUrl : pickedImageUrl path]]); _result = nil; } diff --git a/file_picker/lib/file_picker.dart b/file_picker/lib/file_picker.dart index 96c63de..7a75eb1 100644 --- a/file_picker/lib/file_picker.dart +++ b/file_picker/lib/file_picker.dart @@ -1,131 +1,5 @@ -import 'dart:async'; -import 'dart:io'; +library file_picker; -import 'package:file_picker_platform_interface/file_picker_platform_interface.dart'; -import 'package:file_picker_platform_interface/method_channel_file_picker.dart'; - -export 'package:file_picker_platform_interface/file_picker_platform_interface.dart' - show FileType; - -final MethodChannelFilePicker _filePickerPlatform = FilePickerPlatform.instance; - -class FilePicker { - FilePicker._(); - - /// Returns an absolute file path from the calling platform. - /// - /// Extension filters are allowed with [FileType.custom], when used, make sure to provide a [List] - /// of `allowedExtensions` (e.g. [`pdf`, `svg`, `jpg`].). - /// - /// If you want to track picking status, for example, because some files may take some time to be - /// cached (particularly those picked from cloud providers), you may want to set [onFileLoading] handler - /// that will give you the current status of picking. - /// - /// If you plan on picking images/videos and don't want them to be compressed automatically by OS, - /// you should set `allowCompression` to [false]. Calling this on Android won't have any effect, as - /// it already provides you the original file (or integral copy). - /// - /// Defaults to [FileType.any] which will display all file types. - static Future getFilePath({ - FileType type = FileType.any, - List allowedExtensions, - Function(FilePickerStatus) onFileLoading, - bool allowCompression, - }) async => - await _filePickerPlatform.getFiles( - type: type, - allowedExtensions: allowedExtensions, - onFileLoading: onFileLoading, - allowCompression: allowCompression, - ); - - /// Returns an iterable [Map] where the `key` is the name of the file - /// and the `value` the path. - /// - /// 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 you want to track picking status, for example, because some files may take some time to be - /// cached (particularly those picked from cloud providers), you may want to set `onFileLoading` handler - /// that will give you the current status of picking. - /// - /// If you plan on picking images/videos and don't want them to be compressed automatically by OS, - /// you should set `allowCompression` to [false]. Calling this on Android won't have any effect, as - /// it already provides you the original file (or integral copy). - /// - /// 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, - Function(FilePickerStatus) onFileLoading, - bool allowCompression, - }) async => - await _filePickerPlatform.getFiles( - type: type, - allowMultiple: true, - allowedExtensions: allowedExtensions, - onFileLoading: onFileLoading, - allowCompression: allowCompression, - ); - - /// 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, - Function(FilePickerStatus) onFileLoading, - bool allowCompression, - }) async { - final String filePath = await _filePickerPlatform.getFiles( - type: type, - allowedExtensions: allowedExtensions, - onFileLoading: onFileLoading, - allowCompression: allowCompression, - ); - return filePath != null ? File(filePath) : null; - } - - /// Returns a [List] object from the selected files paths. - /// - /// 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, - Function(FilePickerStatus) onFileLoading, - bool allowCompression, - }) async { - final Map paths = await _filePickerPlatform.getFiles( - type: type, - allowMultiple: true, - allowedExtensions: allowedExtensions, - onFileLoading: onFileLoading, - allowCompression: allowCompression, - ); - - return paths != null && paths.isNotEmpty - ? paths.values.map((path) => File(path)).toList() - : null; - } - - /// Selects a directory and returns its absolute path. - /// - /// 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 getDirectoryPath() async { - return _filePickerPlatform.getDirectoryPath(); - } - - /// Asks the underlying platform to remove any temporary files created by this plugin. - /// - /// This typically relates to cached files that are stored in the cache directory of - /// each platform and it isn't required to invoke this as the system should take care - /// of it whenever needed. However, this will force the cleanup if you want to manage those on your own. - /// - /// Returns [true] if the files were removed with success, [false] otherwise. - static Future clearTemporaryFiles() async { - return _filePickerPlatform.clearTemporaryFiles(); - } -} +export './src/file_picker.dart'; +export './src/platform_file.dart'; +export './src/file_picker_result.dart'; diff --git a/file_picker/lib/src/file_picker.dart b/file_picker/lib/src/file_picker.dart new file mode 100644 index 0000000..4876dec --- /dev/null +++ b/file_picker/lib/src/file_picker.dart @@ -0,0 +1,75 @@ +import 'dart:async'; + +import 'package:file_picker/file_picker.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'file_picker_io.dart'; +import 'file_picker_result.dart'; + +enum FileType { + any, + media, + image, + video, + audio, + custom, +} + +enum FilePickerStatus { + picking, + done, +} + +/// The interface that implementations of file_picker must implement. +/// +/// Platform implementations should extend this class rather than implement it as `file_picker` +/// does not consider newly added methods to be breaking changes. Extending this class +/// (using `extends`) ensures that the subclass will get the default implementation, while +/// platform implementations that `implements` this interface will be broken by newly added +/// [FilePicker] methods. +abstract class FilePicker extends PlatformInterface { + FilePicker() : super(token: _token); + + static final Object _token = Object(); + + static FilePicker _instance = FilePickerIO(); + + static FilePicker get instance => _instance; + + static set instance(FilePicker instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + /// Retrieves the file(s) from the underlying platform + /// + /// Default [type] set to `FileType.any` with [allowMultiple] set to `false` + /// Optionally, [allowedExtensions] might be provided (e.g. [`pdf`, `svg`, `jpg`].). + /// + /// If you want to track picking status, for example, because some files may take some time to be + /// cached (particularly those picked from cloud providers), you may want to set [onFileLoading] handler + /// that will give you the current status of picking. + Future pickFiles({ + FileType type = FileType.any, + List allowedExtensions, + Function(FilePickerStatus) onFileLoading, + bool allowCompression, + bool allowMultiple = false, + }) async => + throw UnimplementedError('pickFiles() has not been implemented.'); + + /// Asks the underlying platform to remove any temporary files created by this plugin. + /// + /// This typically relates to cached files that are stored in the cache directory of + /// each platform and it isn't required to invoke this as the system should take care + /// of it whenever needed. However, this will force the cleanup if you want to manage those on your own. + /// + /// Returns `true` if the files were removed with success, `false` otherwise. + Future clearTemporaryFiles() async => throw UnimplementedError('clearTemporaryFiles() has not been implemented.'); + + /// Selects a directory and returns its absolute path. + /// + /// 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. + Future getDirectoryPath() async => throw UnimplementedError('getDirectoryPath() has not been implemented.'); +} diff --git a/file_picker/lib/src/file_picker_io.dart b/file_picker/lib/src/file_picker_io.dart new file mode 100644 index 0000000..64493ab --- /dev/null +++ b/file_picker/lib/src/file_picker_io.dart @@ -0,0 +1,86 @@ +import 'dart:async'; + +import 'package:file_picker/file_picker.dart'; +import 'package:file_picker/src/platform_file.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'file_picker_result.dart'; + +const MethodChannel _channel = MethodChannel('miguelruivo.flutter.plugins.filepicker'); +const EventChannel _eventChannel = EventChannel('miguelruivo.flutter.plugins.filepickerevent'); + +/// An implementation of [FilePicker] that uses method channels. +class FilePickerIO extends FilePicker { + static const String _tag = 'MethodChannelFilePicker'; + static StreamSubscription _eventSubscription; + + @override + Future pickFiles({ + FileType type = FileType.any, + List allowedExtensions, + Function(FilePickerStatus) onFileLoading, + bool allowCompression, + bool allowMultiple = false, + }) => + _getPath(type, allowMultiple, allowCompression, allowedExtensions, onFileLoading); + + @override + Future clearTemporaryFiles() async => _channel.invokeMethod('clear'); + + @override + Future getDirectoryPath() async { + try { + String result = await _channel.invokeMethod('dir', {}); + if (result != null) { + return PlatformFile(path: result, isDirectory: true); + } + } 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; + } + + Future _getPath( + FileType fileType, + bool allowMultipleSelection, + bool allowCompression, + List allowedExtensions, + Function(FilePickerStatus) onFileLoading, + ) async { + final String type = describeEnum(fileType); + if (type != 'custom' && (allowedExtensions?.isNotEmpty ?? false)) { + throw Exception('If you are using a custom extension filter, please use the FileType.custom instead.'); + } + try { + _eventSubscription?.cancel(); + if (onFileLoading != null) { + _eventSubscription = _eventChannel.receiveBroadcastStream().listen( + (data) => onFileLoading((data as bool) ? FilePickerStatus.picking : FilePickerStatus.done), + onError: (error) => throw Exception(error), + ); + } + + final List result = await _channel.invokeListMethod(type, { + 'allowMultipleSelection': allowMultipleSelection, + 'allowedExtensions': allowedExtensions, + 'allowCompression': allowCompression, + }); + + if (result == null) { + return null; + } + + return FilePickerResult(result.map((file) => PlatformFile(name: file.split('/').last, path: file)).toList()); + } on PlatformException catch (e) { + print('[$_tag] Platform exception: $e'); + rethrow; + } catch (e) { + print('[$_tag] Unsupported operation. Method not found. The exception thrown was: $e'); + rethrow; + } + } +} diff --git a/file_picker/lib/src/file_picker_result.dart b/file_picker/lib/src/file_picker_result.dart new file mode 100644 index 0000000..b3b5cfc --- /dev/null +++ b/file_picker/lib/src/file_picker_result.dart @@ -0,0 +1,26 @@ +import 'package:file_picker/src/platform_file.dart'; +import 'package:flutter/foundation.dart'; + +class FilePickerResult { + const FilePickerResult(this.files); + + /// Picked files. + final List files; + + /// If this pick contains only a single resource. + bool get isSinglePick => files.length == 1; + + /// The length of picked files. + int get count => files.length; + + /// A `List` containing all paths from picked files. + /// + /// This may or not be available and will typically reference cached copies of + /// original files (which can be accessed through its URI property). + /// + /// Only available on IO. Throws `UnsupportedError` on Web. + List get paths => files.map((file) => kIsWeb ? throw UnsupportedError('Unsupported on Web') : file.path).toList(); + + /// A `List` containing all names from picked files with its extensions. + List get names => files.map((file) => file.name).toList(); +} diff --git a/file_picker/lib/src/file_picker_web.dart b/file_picker/lib/src/file_picker_web.dart new file mode 100644 index 0000000..c4a336e --- /dev/null +++ b/file_picker/lib/src/file_picker_web.dart @@ -0,0 +1,85 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:html' as html; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + +import 'file_picker_result.dart'; +import 'platform_file.dart'; + +class FilePickerWeb extends FilePicker { + FilePickerWeb._(); + static final FilePickerWeb platform = FilePickerWeb._(); + + static void registerWith(Registrar registrar) { + FilePicker.instance = platform; + } + + @override + Future pickFiles({ + FileType type = FileType.any, + List allowedExtensions, + bool allowMultiple = false, + Function(FilePickerStatus) onFileLoading, + bool allowCompression, + }) async { + final Completer> filesCompleter = Completer>(); + + String accept = _fileType(type, allowedExtensions); + html.InputElement uploadInput = html.FileUploadInputElement(); + uploadInput.draggable = true; + uploadInput.multiple = allowMultiple; + uploadInput.accept = accept; + uploadInput.click(); + + uploadInput.onChange.listen((e) { + final files = uploadInput.files; + final reader = html.FileReader(); + + List pickedFiles = []; + + reader.onLoadEnd.listen((e) { + pickedFiles.add( + PlatformFile( + name: uploadInput.value.replaceAll('\\', '/'), + bytes: Base64Decoder().convert(reader.result.toString().split(",").last), + ), + ); + + if (pickedFiles.length >= files.length) { + filesCompleter.complete(pickedFiles); + } + }); + + files.forEach((element) { + reader.readAsDataUrl(element); + }); + }); + return FilePickerResult(await filesCompleter.future); + } + + static String _fileType(FileType type, List allowedExtensions) { + switch (type) { + case FileType.any: + return ''; + + case FileType.audio: + return 'audio/*'; + + case FileType.image: + return 'image/*'; + + case FileType.video: + return 'video/*'; + + case FileType.media: + return 'video/*|image/*'; + + case FileType.custom: + return allowedExtensions.fold('', (prev, next) => '${prev.isEmpty ? '' : '$prev,'} .$next'); + break; + } + return ''; + } +} diff --git a/file_picker/lib/src/platform_file.dart b/file_picker/lib/src/platform_file.dart new file mode 100644 index 0000000..cbefcb6 --- /dev/null +++ b/file_picker/lib/src/platform_file.dart @@ -0,0 +1,40 @@ +import 'dart:typed_data'; + +class PlatformFile { + PlatformFile({ + this.path, + this.uri, + this.name, + this.bytes, + this.isDirectory = false, + }); + + /// The absolute path for this file instance. + /// + /// Typically whis will reflect a copy cached file and not the original source, + /// also, it's not guaranteed that this path is always available as some files + /// can be protected by OS. + /// + /// Available on IO only. On Web is always `null`. + final String path; + + /// The URI (Universal Resource Identifier) for this file. + /// + /// This is the original file resource identifier and can be used to + /// manipulate the original file (read, write, delete). + /// + /// Available on IO only. On Web is always `null`. + final String uri; + + /// File name including its extension. + final String name; + + /// Byte data for this file. + final Uint8List bytes; + + /// Whether this file references a directory or not. + final bool isDirectory; + + /// File extension for this file. + String get extension => name.split('/').last; +} diff --git a/file_picker/pubspec.yaml b/file_picker/pubspec.yaml index 2940e87..22e4485 100644 --- a/file_picker/pubspec.yaml +++ b/file_picker/pubspec.yaml @@ -1,13 +1,16 @@ 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.13.3 +version: 2.0.0 dependencies: flutter: sdk: flutter + flutter_web_plugins: + sdk: flutter + flutter_plugin_android_lifecycle: ^1.0.6 - file_picker_platform_interface: ^1.3.1 + plugin_platform_interface: ^1.0.1 environment: sdk: ">=2.0.0 <3.0.0" @@ -22,4 +25,5 @@ flutter: ios: pluginClass: FilePickerPlugin web: - default_package: file_picker_web + pluginClass: FilePickerWeb + fileName: src/file_picker_web.dart diff --git a/file_picker_platform_interface/CHANGELOG.md b/file_picker_platform_interface/CHANGELOG.md index 75f0c6d..978f2d6 100644 --- a/file_picker_platform_interface/CHANGELOG.md +++ b/file_picker_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## [2.0.0] - Deprecates interface + +Deprecates interface in favor of standalone [file_picker](https://pub.dev/packages/file_picker) for all platforms. + ## [1.3.1] - Rollback `allowCompression` Removes `allowCompression` from interface as it should only be used from `file_picker` (Android & iOS). diff --git a/file_picker_platform_interface/README.md b/file_picker_platform_interface/README.md index 9b8f456..68d2f37 100644 --- a/file_picker_platform_interface/README.md +++ b/file_picker_platform_interface/README.md @@ -1,3 +1,7 @@ +# MUST READ! + +The interface is deprectated in favor of standalone [file_picker](https://pub.dev/packages/file_picker) for all platforms where an interface is integrated. This should be the one used as this package is not longer mantained. + # file_picker_platform_interface A common platform interface for the [`file_picker`][1] plugin. @@ -6,6 +10,7 @@ This interface allows platform-specific implementations of the `file_picker` plugin, as well as the plugin itself, to ensure they are supporting the same interface. + # Usage To implement a new platform-specific implementation of `file_picker`, extend diff --git a/file_picker_platform_interface/pubspec.yaml b/file_picker_platform_interface/pubspec.yaml index 74bf79f..ccf6174 100644 --- a/file_picker_platform_interface/pubspec.yaml +++ b/file_picker_platform_interface/pubspec.yaml @@ -1,7 +1,7 @@ name: file_picker_platform_interface description: A common platform interface for the file_picker plugin that must be used to share commom features homepage: https://github.com/miguelpruivo/plugins_flutter_file_picker/file_picker_platform_interface -version: 1.3.1 +version: 2.0.0 environment: sdk: ">=2.1.0 <3.0.0" diff --git a/file_picker_web/CHANGELOG.md b/file_picker_web/CHANGELOG.md index 0bd9387..ea47c14 100644 --- a/file_picker_web/CHANGELOG.md +++ b/file_picker_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.0 + +Deprecates plugin in favor of standalone [file_picker](https://pub.dev/packages/file_picker) for all platforms. + ## 1.0.2+1 Fix custom filter String creation. diff --git a/file_picker_web/README.md b/file_picker_web/README.md index 9223183..30cd96e 100644 --- a/file_picker_web/README.md +++ b/file_picker_web/README.md @@ -1,3 +1,7 @@ +# MUST READ! + +The web standalone plugin is deprectated in favor of standalone [file_picker](https://pub.dev/packages/file_picker) for all platforms. This should be the one used as this package is not longer mantained. + # file_picker_web The web implementation of [`file_picker`][1]. diff --git a/file_picker_web/pubspec.yaml b/file_picker_web/pubspec.yaml index 03def2f..9001480 100644 --- a/file_picker_web/pubspec.yaml +++ b/file_picker_web/pubspec.yaml @@ -1,14 +1,14 @@ name: file_picker_web description: Web platform implementation of file_picker. Provides a way to pick files with filter support for Web. homepage: https://github.com/miguelpruivo/flutter_file_picker/tree/master/file_picker_web -version: 1.0.2+1 +version: 2.0.0 environment: sdk: ">=2.7.0 <3.0.0" flutter: ">=1.10.0" dependencies: - file_picker_platform_interface: ^1.3.1 + file_picker_platform_interface: ^2.0.0 flutter: sdk: flutter flutter_web_plugins: