see changelog (v1.4.0)

* Better handling on Android of buggy file managers that return no intent when canceling the file selection even though it returns Activity.RESULT_OK (#111)

* Add file_picker Go support. (#132)

* Add file_picker Go support.

Originally written by @chunhunghan, cleaned up and fixed for MacOS by Geert-Johan Riemer.

Co-authored-by: chunhunghan <chunhunghan@gmail.com>

* Add improved instructions to go/README.md

* removes deprecated Android SDK code and fixes an issue that could prevent some downloaded files from being picked

* adds getMultiFile and prevents UI blocking when picking large remote files

* updates readme file
This commit is contained in:
Miguel Ruivo 2019-08-31 15:14:28 +01:00
parent 3f67653fed
commit f502423ab2
26 changed files with 592 additions and 382 deletions

View File

@ -1,3 +1,14 @@
## 1.4.0
**New features**
* Adds Desktop support throught **[go-flutter](https://github.com/go-flutter-desktop/go-flutter)**, you can see detailed instructions on how to get in runing [here](https://github.com/go-flutter-desktop/hover).
* Adds Desktop example, to run it just do `hover init` and then `hover run` within the plugin's example folder (you must have go and hover installed, check the previous point).
* Similar to `getFile`, now there is also a `getMultiFile` which behaves the same way, but returning a list of files instead.
**Improvements:**
* Updates Android SDK deprecated code.
* Sometimes when a big file was being picked from a remote directory (GDrive for example), the UI could be blocked. Now this shouldn't happen anymore.
## 1.3.8
**Bug fix:** Fixes an issue that could cause a crash when picking files with very long names.

117
README.md
View File

@ -2,103 +2,10 @@
[![Awesome Flutter](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter)
[![Codemagic build status](https://api.codemagic.io/apps/5ce89f4a9b46f5000ca89638/5ce89f4a9b46f5000ca89637/status_badge.svg)](https://codemagic.io/apps/5ce89f4a9b46f5000ca89638/5ce89f4a9b46f5000ca89637/latest_build)
# file_picker
![fluter_file_picker](https://user-images.githubusercontent.com/27860743/64064695-b88dab00-cbfc-11e9-814f-30921b66035f.png)
# File Picker
A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extensions filtering support.
## Installation
First, add *file_picker* as a dependency in [your pubspec.yaml file](https://flutter.io/platform-plugins/).
```
file_picker: ^1.3.8
```
### Android
Add
```
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
```
before `<application>` to your app's `AndroidManifest.xml` file. This is required to access files from external storage.
### iOS
Based on the location of the files that you are willing to pick paths, you may need to add some keys to your iOS app's _Info.plist_ file, located in `<project root>/ios/Runner/Info.plist`:
* **_UIBackgroundModes_** with the **_fetch_** and **_remote-notifications_** keys - Required if you'll be using the `FileType.ANY` or `FileType.CUSTOM`. Describe why your app needs to access background taks, such downloading files (from cloud services). This is called _Required background modes_, with the keys _App download content from network_ and _App downloads content in response to push notifications_ respectively in the visual editor (since both methods aren't actually overriden, not adding this property/keys may only display a warning, but shouldn't prevent its correct usage).
```
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>remote-notification</string>
</array>
```
* **_NSAppleMusicUsageDescription_** - Required if you'll be using the `FileType.AUDIO`. Describe why your app needs permission to access music library. This is called _Privacy - Media Library Usage Description_ in the visual editor.
```
<key>NSAppleMusicUsageDescription</key>
<string>Explain why your app uses music</string>
```
* **_NSPhotoLibraryUsageDescription_** - Required if you'll be using the `FileType.IMAGE` or `FileType.VIDEO`. Describe why your app needs permission for the photo library. This is called _Privacy - Photo Library Usage Description_ in the visual editor.
```
<key>NSPhotoLibraryUsageDescription</key>
<string>Explain why your app uses photo library</string>
```
**Note:** Any iOS version below 11.0, will require an Apple Developer Program account to enable _CloudKit_ and make it possible to use the document picker (which happens when you select `FileType.ALL`, `FileType.CUSTOM` or any other option with `getMultiFilePath()`). You can read more about it [here]( https://developer.apple.com/library/archive/documentation/DataManagement/Conceptual/CloudKitQuickStart/EnablingiCloudandConfiguringCloudKit/EnablingiCloudandConfiguringCloudKit.html).
## Usage
There are only two methods that should be used with this package:
#### `FilePicker.getFilePath()`
Will let you pick a **single** file. This receives two optional parameters: the `fileType` for specifying the type of the picker and a `fileExtension` parameter to filter selectable files. The available filters are:
* `FileType.ANY` - Will let you pick all available files.
* `FileType.CUSTOM` - Will let you pick a single path for the extension matching the `fileExtension` provided.
* `FileType.IMAGE` - Will let you pick a single image file. Opens gallery on iOS.
* `FileType.VIDEO` - WIll let you pick a single video file. Opens gallery on iOS.
* `FileType.AUDIO` - Will let you pick a single audio file. Opens music on iOS. Note that DRM protected files won't provide a path, `null` will be returned instead.
#### `FilePicker.getMultiFilePath()`
Will let you select **multiple** files and retrieve its path at once. Optionally you can provide a `fileExtension` parameter to filter the allowed selectable files.
Will return a `Map<String,String>` with the files name (`key`) and corresponding path (`value`) of all selected files.
Picking multiple paths from iOS gallery (image and video) aren't currently supported.
#### Usages
So, a few example usages can be as follow:
```
// Single file path
String filePath;
filePath = await FilePicker.getFilePath(type: FileType.ANY); // will let you pick one file path, from all extensions
filePath = await FilePicker.getFilePath(type: FileType.CUSTOM, fileExtension: 'svg'); // will filter and only let you pick files with svg extension
// Pick a single file directly
File file = await FilePicker.getFile(type: FileType.ANY); // will return a File object directly from the selected file
// Multi file path
Map<String,String> filesPaths;
filePaths = await FilePicker.getMultiFilePath(); // will let you pick multiple files of any format at once
filePaths = await FilePicker.getMultiFilePath(fileExtension: 'pdf'); // will let you pick multiple pdf files at once
filePaths = await FilePicker.getMultiFilePath(type: FileType.IMAGE); // will let you pick multiple image files at once
List<String> allNames = filePaths.keys; // List of all file names
List<String> allPaths = filePaths.values; // List of all paths
String someFilePath = filePaths['fileName']; // Access a file path directly by its name (matching a key)
```
##### A few side notes
* Using `getMultiFilePath()` on iOS will always use the document picker (aka Files app). This means that multi picks are not currently supported for photo library images/videos or music library files.
* When using `FileType.CUSTOM`, unsupported extensions will throw a `MissingPluginException` that is handled by the plugin.
* On Android, when available, you should avoid using third-party file explorers as those may prevent file extension filtering (behaving as `FileType.ANY`). In this scenario, you will need to validate it on return.
## Currently supported features
* [X] Load paths from **cloud files** (GDrive, Dropbox, iCloud)
* [X] Load path from a **custom format** by providing a file extension (pdf, svg, zip, etc.)
@ -107,17 +14,27 @@ String someFilePath = filePaths['fileName']; // Access a file path directly by i
* [X] Load path from **audio**
* [X] Load path from **video**
* [X] Load path from **any**
* [X] Create a `File` object from **any** selected file
* [X] Create a `File` or `List<File>` objects from **any** selected file(s)
* [X] Supports desktop through **go-flutter** (MacOS, Windows, Linux)
If you have any feature that you want to see in this package, please add it [here](https://github.com/miguelpruivo/plugins_flutter_file_picker/issues/99). 🎉
## Demo App
## Documentation
See the **[File Picker Wiki](https://github.com/miguelpruivo/flutter_file_picker/wiki)** for every detail on about how to install, setup and use it.
1. [Installation](https://github.com/miguelpruivo/plugins_flutter_file_picker/wiki/Installation)
2. [Setup](https://github.com/miguelpruivo/plugins_flutter_file_picker/wiki/Setup)
* [Android](https://github.com/miguelpruivo/plugins_flutter_file_picker/wiki/Setup#android)
* [iOS](https://github.com/miguelpruivo/plugins_flutter_file_picker/wiki/Setup#ios)
* [Desktop (go-flutter)](https://github.com/miguelpruivo/plugins_flutter_file_picker/wiki/Setup/_edit#desktop-go-flutter)
3. [API](https://github.com/miguelpruivo/plugins_flutter_file_picker/wiki/api)
* [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/main.dart)
## Example App
![Demo](https://github.com/miguelpruivo/plugins_flutter_file_picker/blob/master/example/example.gif)
## Example
See example app.
## Getting Started
For help getting started with Flutter, view our online

View File

@ -1,29 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<Objective-C-extensions>
<file>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" />
</file>
<class>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" />
</class>
<extensions>
<pair source="cpp" header="h" fileNamingConvention="NONE" />
<pair source="c" header="h" fileNamingConvention="NONE" />
</extensions>
</Objective-C-extensions>
</code_scheme>
</component>

View File

@ -1,3 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.mr.flutter.plugin.filepicker">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
</manifest>

View File

@ -52,46 +52,58 @@ public class FilePickerPlugin implements MethodCallHandler {
instance = registrar;
instance.addActivityResultListener(new PluginRegistry.ActivityResultListener() {
@Override
public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
public boolean onActivityResult(int requestCode, int resultCode, final Intent data) {
if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) {
if(data.getClipData() != null) {
int count = data.getClipData().getItemCount();
int currentItem = 0;
ArrayList<String> paths = new ArrayList<>();
while(currentItem < count) {
final Uri currentUri = data.getClipData().getItemAt(currentItem).getUri();
String path = FileUtils.getPath(currentUri, instance.context());
if(path == null) {
path = FileUtils.getUriFromRemote(instance.activeContext(), currentUri, result);
}
paths.add(path);
Log.i(TAG, "[MultiFilePick] File #" + currentItem + " - URI: " +currentUri.getPath());
currentItem++;
}
if(paths.size() > 1){
result.success(paths);
} else {
result.success(paths.get(0));
}
} else if (data != null) {
Uri uri = data.getData();
Log.i(TAG, "[SingleFilePick] File URI:" +data.getData().toString());
String fullPath = FileUtils.getPath(uri, instance.context());
new Thread(new Runnable() {
@Override
public void run() {
if (data != null) {
if(data.getClipData() != null) {
int count = data.getClipData().getItemCount();
int currentItem = 0;
ArrayList<String> paths = new ArrayList<>();
while(currentItem < count) {
final Uri currentUri = data.getClipData().getItemAt(currentItem).getUri();
String path = FileUtils.getPath(currentUri, instance.context());
if(path == null) {
path = FileUtils.getUriFromRemote(instance.activeContext(), currentUri, result);
}
paths.add(path);
Log.i(TAG, "[MultiFilePick] File #" + currentItem + " - URI: " +currentUri.getPath());
currentItem++;
}
if(paths.size() > 1){
runOnUiThread(result, paths, true);
} else {
runOnUiThread(result, paths.get(0), true);
}
} else if (data.getData() != null) {
Uri uri = data.getData();
Log.i(TAG, "[SingleFilePick] File URI:" + uri.toString());
String fullPath = FileUtils.getPath(uri, instance.context());
if(fullPath == null) {
fullPath = FileUtils.getUriFromRemote(instance.activeContext(), uri, result);
}
if(fullPath == null) {
fullPath = FileUtils.getUriFromRemote(instance.activeContext(), uri, result);
}
if(fullPath != null) {
Log.i(TAG, "Absolute file path:" + fullPath);
runOnUiThread(result, fullPath, true);
} else {
runOnUiThread(result, "Failed to retrieve path.", false);
}
} else {
runOnUiThread(result, "Unknown activity error, please fill an issue.", false);
}
} else {
runOnUiThread(result, "Unknown activity error, please fill an issue.", false);
}
}
}).start();
return true;
if(fullPath != null) {
Log.i(TAG, "Absolute file path:" + fullPath);
result.success(fullPath);
} else {
result.error(TAG, "Failed to retrieve path." ,null);
}
}
return true;
} else if(requestCode == REQUEST_CODE && resultCode == Activity.RESULT_CANCELED) {
result.success(null);
return true;
@ -115,9 +127,24 @@ public class FilePickerPlugin implements MethodCallHandler {
});
}
private static void runOnUiThread(final Result result, final Object o, final boolean success) {
instance.activity().runOnUiThread(new Runnable() {
@Override
public void run() {
if(success) {
result.success(o);
} else if(o != null) {
result.error(TAG,(String)o, null);
} else {
result.notImplemented();
}
}
});
}
@Override
public void onMethodCall(MethodCall call, Result result) {
this.result = result;
FilePickerPlugin.result = result;
fileType = resolveType(call.method);
isMultipleSelection = (boolean)call.arguments;
@ -177,13 +204,9 @@ public class FilePickerPlugin implements MethodCallHandler {
Intent intent;
if (checkPermission()) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
intent = new Intent(Intent.ACTION_PICK);
} else {
intent = new Intent(Intent.ACTION_GET_CONTENT);
}
Uri uri = Uri.parse(Environment.getExternalStorageDirectory().getPath() + File.separator);
intent = new Intent(Intent.ACTION_GET_CONTENT);
Uri uri = Uri.parse(FileUtils.getExternalPath(instance.activeContext()) + File.separator);
intent.setDataAndType(uri, type);
intent.setType(type);
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, isMultipleSelection);

View File

@ -38,6 +38,13 @@ public class FileUtils {
return null;
}
public static String getExternalPath(Context context) {
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
return context.getExternalFilesDir(null).getAbsolutePath();
}
return context.getFilesDir().getAbsolutePath();
}
@TargetApi(19)
private static String getForApi19(Context context, Uri uri) {
Log.e(TAG, "Getting for API 19 or above" + uri);
@ -50,11 +57,11 @@ public class FileUtils {
final String type = split[0];
if ("primary".equalsIgnoreCase(type)) {
Log.e(TAG, "Primary External Document URI");
return Environment.getExternalStorageDirectory() + "/" + split[1];
return getExternalPath(context) + "/" + split[1];
}
} else if (isDownloadsDocument(uri)) {
Log.e(TAG, "Downloads External Document URI");
final String id = DocumentsContract.getDocumentId(uri);
String id = DocumentsContract.getDocumentId(uri);
if (!TextUtils.isEmpty(id)) {
if (id.startsWith("raw:")) {
@ -65,6 +72,9 @@ public class FileUtils {
"content://downloads/my_downloads",
"content://downloads/all_downloads"
};
if(id.contains(":")){
id = id.split(":")[1];
}
for (String contentUriPrefix : contentUriPrefixesToTry) {
Uri contentUri = ContentUris.withAppendedId(Uri.parse(contentUriPrefix), Long.valueOf(id));
try {
@ -132,6 +142,7 @@ public class FileUtils {
final int index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(index);
}
} catch(Exception ex){
} finally {
if (cursor != null)
cursor.close();

View File

@ -15,7 +15,7 @@ apply plugin: 'com.android.application'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 28
compileSdkVersion 29
lintOptions {
disable 'InvalidPackage'
@ -24,7 +24,7 @@ android {
defaultConfig {
applicationId "com.mr.flutter.plugin.filepickerexample"
minSdkVersion 16
targetSdkVersion 28
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View File

@ -7,8 +7,7 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method.
In most cases you can leave this as-is, but you if you want to provide
@ -18,7 +17,8 @@
android:name="io.flutter.app.FlutterApplication"
android:label="file_picker_example"
android:icon="@mipmap/ic_launcher"
tools:replace="android:label">
tools:replace="android:label"
tools:ignore="GoogleAppIndexingWarning">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"

View File

@ -5,7 +5,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.0'
classpath 'com.android.tools.build:gradle:3.2.1'
}
}
@ -14,6 +14,12 @@ allprojects {
google()
jcenter()
}
gradle.projectsEvaluated {
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
options.compilerArgs << "-Xlint:deprecation"
}
}
}
rootProject.buildDir = '../build'

View File

@ -1,6 +1,6 @@
#Sat Jan 26 11:50:40 EET 2019
#Thu Aug 29 13:24:58 WEST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip

View File

@ -14,9 +14,9 @@ EXTERNAL SOURCES:
:path: ".symlinks/flutter/ios"
SPEC CHECKSUMS:
file_picker: 78c3344d9b2c343bb3090c2f032b796242ebaea7
Flutter: 9d0fac939486c9aba2809b7982dfdbb47a7b0296
file_picker: 408623be2125b79a4539cf703be3d4b3abe5e245
Flutter: 58dd7d1b27887414a370fcccb9e645c08ffd7a6a
PODFILE CHECKSUM: 1e5af4103afd21ca5ead147d7b81d06f494f51a2
COCOAPODS: 1.5.3
COCOAPODS: 1.7.5

View File

@ -54,6 +54,8 @@
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
9E29C2B321AA1B6738D05DCC /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; };
AA58896224B359D6BCEB4ED4 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
D616774FB981784E6589DAC9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -137,6 +139,8 @@
EE3450EDCED914F636FA6BB9 /* Pods */ = {
isa = PBXGroup;
children = (
D616774FB981784E6589DAC9 /* Pods-Runner.debug.xcconfig */,
AA58896224B359D6BCEB4ED4 /* Pods-Runner.release.xcconfig */,
);
name = Pods;
sourceTree = "<group>";
@ -266,7 +270,7 @@
files = (
);
inputPaths = (
"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
"${PODS_ROOT}/../.symlinks/flutter/ios/Flutter.framework",
);
name = "[CP] Embed Pods Frameworks";
@ -275,7 +279,7 @@
);
runOnlyForDeploymentPostprocessing = 1;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */

View File

@ -18,14 +18,19 @@
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>NSAppleMusicUsageDescription</key>
<string>Used to demonstrate file picker plugin</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppleMusicUsageDescription</key>
<string>Used to demonstrate file picker plugin</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Used to demonstrate file picker plugin</string>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
@ -36,11 +41,6 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>remote-notification</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>

View File

@ -1,185 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:file_picker/file_picker.dart';
import 'package:file_picker_example/src/file_picker_demo.dart';
import 'package:flutter/widgets.dart';
void main() => runApp(new FilePickerDemo());
class FilePickerDemo extends StatefulWidget {
@override
_FilePickerDemoState createState() => new _FilePickerDemoState();
}
class _FilePickerDemoState extends State<FilePickerDemo> {
String _fileName;
String _path;
Map<String, String> _paths;
String _extension;
bool _multiPick = false;
bool _hasValidMime = false;
FileType _pickingType;
TextEditingController _controller = new TextEditingController();
@override
void initState() {
super.initState();
_controller.addListener(() => _extension = _controller.text);
}
void _openFileExplorer() async {
if (_pickingType != FileType.CUSTOM || _hasValidMime) {
try {
if (_multiPick) {
_path = null;
_paths = await FilePicker.getMultiFilePath(
type: _pickingType, fileExtension: _extension);
} else {
_paths = null;
_path = await FilePicker.getFilePath(
type: _pickingType, fileExtension: _extension);
}
} on PlatformException catch (e) {
print("Unsupported operation" + e.toString());
}
if (!mounted) return;
setState(() {
_fileName = _path != null
? _path.split('/').last
: _paths != null ? _paths.keys.toString() : '...';
});
}
}
@override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: const Text('File Picker example app'),
),
body: new Center(
child: new Padding(
padding: const EdgeInsets.only(left: 10.0, right: 10.0),
child: new SingleChildScrollView(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Padding(
padding: const EdgeInsets.only(top: 20.0),
child: new DropdownButton(
hint: new Text('LOAD PATH FROM'),
value: _pickingType,
items: <DropdownMenuItem>[
new DropdownMenuItem(
child: new Text('FROM AUDIO'),
value: FileType.AUDIO,
),
new DropdownMenuItem(
child: new Text('FROM IMAGE'),
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;
if (_pickingType != FileType.CUSTOM) {
_controller.text = _extension = '';
}
})),
),
new ConstrainedBox(
constraints: BoxConstraints.tightFor(width: 100.0),
child: _pickingType == FileType.CUSTOM
? new TextFormField(
maxLength: 15,
autovalidate: true,
controller: _controller,
decoration:
InputDecoration(labelText: 'File extension'),
keyboardType: TextInputType.text,
textCapitalization: TextCapitalization.none,
validator: (value) {
RegExp reg = new RegExp(r'[^a-zA-Z0-9]');
if (reg.hasMatch(value)) {
_hasValidMime = false;
return 'Invalid format';
}
_hasValidMime = true;
return null;
},
)
: new Container(),
),
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),
value: _multiPick,
),
),
new Padding(
padding: const EdgeInsets.only(top: 50.0, bottom: 20.0),
child: new RaisedButton(
onPressed: () => _openFileExplorer(),
child: new Text("Open file picker"),
),
),
new Builder(
builder: (BuildContext context) =>
_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,
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;
return new ListTile(
title: new Text(
name,
),
subtitle: new Text(path),
);
},
separatorBuilder:
(BuildContext context, int index) =>
new Divider(),
)),
)
: new Container(),
),
],
),
),
)),
),
);
}
}

View File

@ -0,0 +1,8 @@
import 'package:file_picker_example/src/file_picker_demo.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
void main() {
debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia;
runApp(new FilePickerDemo());
}

View File

@ -0,0 +1,188 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:file_picker/file_picker.dart';
class FilePickerDemo extends StatefulWidget {
@override
_FilePickerDemoState createState() => new _FilePickerDemoState();
}
class _FilePickerDemoState extends State<FilePickerDemo> {
String _fileName;
String _path;
Map<String, String> _paths;
String _extension;
bool _loadingPath = false;
bool _multiPick = false;
bool _hasValidMime = false;
FileType _pickingType;
TextEditingController _controller = new TextEditingController();
@override
void initState() {
super.initState();
_controller.addListener(() => _extension = _controller.text);
}
void _openFileExplorer() async {
if (_pickingType != FileType.CUSTOM || _hasValidMime) {
setState(() => _loadingPath = true);
try {
if (_multiPick) {
_path = null;
_paths = await FilePicker.getMultiFilePath(
type: _pickingType, fileExtension: _extension);
} else {
_paths = null;
_path = await FilePicker.getFilePath(
type: _pickingType, fileExtension: _extension);
}
} on PlatformException catch (e) {
print("Unsupported operation" + e.toString());
}
if (!mounted) return;
setState(() {
_loadingPath = false;
_fileName = _path != null
? _path.split('/').last
: _paths != null ? _paths.keys.toString() : '...';
});
}
}
@override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: const Text('File Picker example app'),
),
body: new Center(
child: new Padding(
padding: const EdgeInsets.only(left: 10.0, right: 10.0),
child: new SingleChildScrollView(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Padding(
padding: const EdgeInsets.only(top: 20.0),
child: new DropdownButton(
hint: new Text('LOAD PATH FROM'),
value: _pickingType,
items: <DropdownMenuItem>[
new DropdownMenuItem(
child: new Text('FROM AUDIO'),
value: FileType.AUDIO,
),
new DropdownMenuItem(
child: new Text('FROM IMAGE'),
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;
if (_pickingType != FileType.CUSTOM) {
_controller.text = _extension = '';
}
})),
),
new ConstrainedBox(
constraints: BoxConstraints.tightFor(width: 100.0),
child: _pickingType == FileType.CUSTOM
? new TextFormField(
maxLength: 15,
autovalidate: true,
controller: _controller,
decoration:
InputDecoration(labelText: 'File extension'),
keyboardType: TextInputType.text,
textCapitalization: TextCapitalization.none,
validator: (value) {
RegExp reg = new RegExp(r'[^a-zA-Z0-9]');
if (reg.hasMatch(value)) {
_hasValidMime = false;
return 'Invalid format';
}
_hasValidMime = true;
return null;
},
)
: new Container(),
),
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),
value: _multiPick,
),
),
new Padding(
padding: const EdgeInsets.only(top: 50.0, bottom: 20.0),
child: new RaisedButton(
onPressed: () => _openFileExplorer(),
child: new Text("Open file picker"),
),
),
new Builder(
builder: (BuildContext context) => _loadingPath
? 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,
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;
return new ListTile(
title: new Text(
name,
),
subtitle: new Text(path),
);
},
separatorBuilder:
(BuildContext context, int index) =>
new Divider(),
)),
)
: new Container(),
),
],
),
),
)),
),
);
}
}

23
go/README.md Normal file
View File

@ -0,0 +1,23 @@
# file_picker
This Go package implements the host-side of the Flutter [file_picker](https://github.com/miguelpruivo/plugins_flutter_file_picker) plugin.
## Usage
Modify your applications `options.go`:
```
package main
import (
... other imports ....
"github.com/miguelpruivo/plugins_flutter_file_picker/go"
)
var options = []flutter.Option{
... other plugins and options ...
flutter.AddPlugin(&file_picker.FilePickerPlugin{}),
}
```

52
go/file_darwin.go Normal file
View File

@ -0,0 +1,52 @@
package file_picker
import (
"fmt"
"os/exec"
"path/filepath"
"strings"
"github.com/pkg/errors"
)
func fileFilter(method string) (string, error) {
switch method {
case "ANY":
return `"public.item"`, nil
case "IMAGE":
return `"public.image"`, nil
case "AUDIO":
return `"public.audio"`, nil
case "VIDEO":
return `"public.movie"`, nil
default:
if strings.HasPrefix(method, "__CUSTOM_") {
resolveType := strings.Split(method, "__CUSTOM_")
return `"` + resolveType[1] + `"`, nil
}
return "", errors.New("unknown method")
}
}
func fileDialog(title string, filter string) (string, error) {
osascript, err := exec.LookPath("osascript")
if err != nil {
return "", err
}
output, err := exec.Command(osascript, "-e", `choose file of type {`+filter+`} with prompt "`+title+`"`).Output()
if err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
fmt.Printf("miguelpruivo/plugins_flutter_file_picker/go: file dialog exited with code %d and output `%s`\n", exitError.ExitCode(), string(output))
return "", nil // user probably canceled or closed the selection window
}
return "", errors.Wrap(err, "failed to open file dialog")
}
trimmedOutput := strings.TrimSpace(string(output))
pathParts := strings.Split(trimmedOutput, ":")
path := string(filepath.Separator) + filepath.Join(pathParts[1:]...)
return path, nil
}

36
go/file_linux.go Normal file
View File

@ -0,0 +1,36 @@
package file_picker
import (
"strings"
"github.com/gen2brain/dlgs"
"github.com/pkg/errors"
)
func fileFilter(method string) (string, error) {
switch method {
case "ANY":
return `*.*`, nil
case "IMAGE":
return `*.png *.jpg *.jpeg`, nil
case "AUDIO":
return `*.mp3`, nil
case "VIDEO":
return `*.webm *.mpeg *.mkv *.mp4 *.avi *.mov *.flv`, nil
default:
if strings.HasPrefix(method, "__CUSTOM_") {
resolveType := strings.Split(method, "__CUSTOM_")
return `*.` + resolveType[1], nil
}
return "", errors.New("unknown method")
}
}
func fileDialog(title string, filter string) (string, error) {
filePath, _, err := dlgs.File(title, filter, false)
if err != nil {
return "", errors.Wrap(err, "failed to open dialog picker")
}
return filePath, nil
}

15
go/file_unsupported.go Normal file
View File

@ -0,0 +1,15 @@
// +build !darwin,!linux,!windows
package file_picker
import (
"github.com/pkg/errors"
)
func fileFilter(method string) (string, error) {
return "", errors.New("platform unsupported")
}
func fileDialog(title string, filter string) (string, error) {
return "", errors.New("platform unsupported")
}

35
go/file_windows.go Normal file
View File

@ -0,0 +1,35 @@
package file_picker
import (
"strings"
"github.com/gen2brain/dlgs"
"github.com/pkg/errors"
)
func fileFilter(method string) (string, error) {
switch method {
case "ANY":
return "*", nil
case "IMAGE":
return "Images (*.jpeg,*.png,*.gif)\x00*.jpg;*.jpeg;*.png;*.gif\x00All Files (*.*)\x00*.*\x00\x00", nil
case "AUDIO":
return "Audios (*.mp3)\x00*.mp3\x00All Files (*.*)\x00*.*\x00\x00", nil
case "VIDEO":
return "Videos (*.webm,*.wmv,*.mpeg,*.mkv,*.mp4,*.avi,*.mov,*.flv)\x00*.webm;*.wmv;*.mpeg;*.mkv;*mp4;*.avi;*.mov;*.flv\x00All Files (*.*)\x00*.*\x00\x00", nil
default:
if strings.HasPrefix(method, "__CUSTOM_") {
resolveType := strings.Split(method, "__CUSTOM_")
return "Files (*." + resolveType[1] + ")\x00*." + resolveType[1] + "\x00All Files (*.*)\x00*.*\x00\x00", nil
}
return "", errors.New("unknown method")
}
}
func fileDialog(title string, filter string) (string, error) {
filePath, _, err := dlgs.File(title, filter, false)
if err != nil {
return "", errors.Wrap(err, "failed to open dialog picker")
}
return filePath, nil
}

9
go/go.mod Normal file
View File

@ -0,0 +1,9 @@
module github.com/miguelpruivo/plugins_flutter_file_picker/go
go 1.12
require (
github.com/gen2brain/dlgs v0.0.0-20180629122906-342edb4c68c1
github.com/go-flutter-desktop/go-flutter v0.27.0
github.com/pkg/errors v0.8.1
)

18
go/go.sum Normal file
View File

@ -0,0 +1,18 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gen2brain/dlgs v0.0.0-20180629122906-342edb4c68c1 h1:16o0LcrCHuKADRSOTYxoXTRQpdVo9BDeABauof+9Em8=
github.com/gen2brain/dlgs v0.0.0-20180629122906-342edb4c68c1/go.mod h1:/eFcjDXaU2THSOOqLxOPETIbHETnamk8FA/hMjhg/gU=
github.com/go-flutter-desktop/go-flutter v0.27.0 h1:XYhKiRjwX/Z73Hpe+TJDBbutFL1mx1wmy9Po9lux1GU=
github.com/go-flutter-desktop/go-flutter v0.27.0/go.mod h1:GZYxHYp7lRnt3imJV1d8EWleMv5q9J4S2ONNEqpPOfo=
github.com/go-gl/glfw v0.0.0-20190519095719-e6da0acd62b1 h1:noz9OnjV5PMOZWNOI+y1cS5rnxuJfpY6leIgQEEdBQw=
github.com/go-gl/glfw v0.0.0-20190519095719-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4=
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=

55
go/plugin.go Normal file
View File

@ -0,0 +1,55 @@
package file_picker
import (
"github.com/gen2brain/dlgs"
"github.com/go-flutter-desktop/go-flutter"
"github.com/go-flutter-desktop/go-flutter/plugin"
"github.com/pkg/errors"
)
const channelName = "file_picker"
type FilePickerPlugin struct{}
var _ flutter.Plugin = &FilePickerPlugin{} // compile-time type check
func (p *FilePickerPlugin) InitPlugin(messenger plugin.BinaryMessenger) error {
channel := plugin.NewMethodChannel(messenger, channelName, plugin.StandardMethodCodec{})
channel.CatchAllHandleFunc(p.handleFilePicker)
return nil
}
func (p *FilePickerPlugin) handleFilePicker(methodCall interface{}) (reply interface{}, err error) {
method := methodCall.(plugin.MethodCall)
filter, err := fileFilter(method.Method)
if err != nil {
return nil, errors.Wrap(err, "failed to get filter")
}
selectMultiple, ok := method.Arguments.(bool)
if !ok {
return nil, errors.Wrap(err, "invalid format for argument, not a bool")
}
if selectMultiple {
filePaths, _, err := dlgs.FileMulti("Select one or more files", filter)
if err != nil {
return nil, errors.Wrap(err, "failed to open dialog picker")
}
// type []string is not supported by StandardMessageCodec
sliceFilePaths := make([]interface{}, len(filePaths))
for i, file := range filePaths {
sliceFilePaths[i] = file
}
return sliceFilePaths, nil
}
filePath, err := fileDialog("Select a file", filter)
if err != nil {
return nil, errors.Wrap(err, "failed to open dialog picker")
}
return filePath, nil
}

View File

@ -47,6 +47,19 @@ class FilePicker {
return filePath != null ? File(filePath) : null;
}
/// Returns a `List<File>` 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<List<File>> getMultiFile(
{FileType type = FileType.ANY, String fileExtension}) async {
final Map<String, String> paths =
await _getPath(_handleType(type, fileExtension), true);
return paths != null && paths.isNotEmpty
? paths.values.map((path) => File(path)).toList()
: null;
}
static Future<dynamic> _getPath(String type, bool multipleSelection) async {
try {
dynamic result = await _channel.invokeMethod(type, multipleSelection);

View File

@ -2,8 +2,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 extensions filtering support.
author: Miguel Ruivo <miguel@miguelruivo.com>
homepage: https://github.com/miguelpruivo/plugins_flutter_file_picker
version: 1.3.8
version: 1.4.0
dependencies:
flutter:
@ -12,10 +11,6 @@ dependencies:
environment:
sdk: ">=2.0.0 <3.0.0"
# For information on the generic Dart part of this file, see the
# following page: https://www.dartlang.org/tools/pub/pubspec
# The following section is specific to Flutter.
flutter:
plugin:
androidPackage: com.mr.flutter.plugin.filepicker