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:
parent
3f67653fed
commit
f502423ab2
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -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
|
## 1.3.8
|
||||||
|
|
||||||
**Bug fix:** Fixes an issue that could cause a crash when picking files with very long names.
|
**Bug fix:** Fixes an issue that could cause a crash when picking files with very long names.
|
||||||
|
|
117
README.md
117
README.md
|
@ -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)
|
[![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)
|
[![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.
|
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
|
## Currently supported features
|
||||||
* [X] Load paths from **cloud files** (GDrive, Dropbox, iCloud)
|
* [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.)
|
* [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 **audio**
|
||||||
* [X] Load path from **video**
|
* [X] Load path from **video**
|
||||||
* [X] Load path from **any**
|
* [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). 🎉
|
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)
|
![Demo](https://github.com/miguelpruivo/plugins_flutter_file_picker/blob/master/example/example.gif)
|
||||||
|
|
||||||
## Example
|
|
||||||
See example app.
|
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
For help getting started with Flutter, view our online
|
For help getting started with Flutter, view our online
|
||||||
|
|
|
@ -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>
|
|
|
@ -1,3 +1,4 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.mr.flutter.plugin.filepicker">
|
package="com.mr.flutter.plugin.filepicker">
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -52,46 +52,58 @@ public class FilePickerPlugin implements MethodCallHandler {
|
||||||
instance = registrar;
|
instance = registrar;
|
||||||
instance.addActivityResultListener(new PluginRegistry.ActivityResultListener() {
|
instance.addActivityResultListener(new PluginRegistry.ActivityResultListener() {
|
||||||
@Override
|
@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 (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) {
|
||||||
|
|
||||||
if(data.getClipData() != null) {
|
new Thread(new Runnable() {
|
||||||
int count = data.getClipData().getItemCount();
|
@Override
|
||||||
int currentItem = 0;
|
public void run() {
|
||||||
ArrayList<String> paths = new ArrayList<>();
|
if (data != null) {
|
||||||
while(currentItem < count) {
|
if(data.getClipData() != null) {
|
||||||
final Uri currentUri = data.getClipData().getItemAt(currentItem).getUri();
|
int count = data.getClipData().getItemCount();
|
||||||
String path = FileUtils.getPath(currentUri, instance.context());
|
int currentItem = 0;
|
||||||
if(path == null) {
|
ArrayList<String> paths = new ArrayList<>();
|
||||||
path = FileUtils.getUriFromRemote(instance.activeContext(), currentUri, result);
|
while(currentItem < count) {
|
||||||
}
|
final Uri currentUri = data.getClipData().getItemAt(currentItem).getUri();
|
||||||
paths.add(path);
|
String path = FileUtils.getPath(currentUri, instance.context());
|
||||||
Log.i(TAG, "[MultiFilePick] File #" + currentItem + " - URI: " +currentUri.getPath());
|
if(path == null) {
|
||||||
currentItem++;
|
path = FileUtils.getUriFromRemote(instance.activeContext(), currentUri, result);
|
||||||
}
|
}
|
||||||
if(paths.size() > 1){
|
paths.add(path);
|
||||||
result.success(paths);
|
Log.i(TAG, "[MultiFilePick] File #" + currentItem + " - URI: " +currentUri.getPath());
|
||||||
} else {
|
currentItem++;
|
||||||
result.success(paths.get(0));
|
}
|
||||||
}
|
if(paths.size() > 1){
|
||||||
} else if (data != null) {
|
runOnUiThread(result, paths, true);
|
||||||
Uri uri = data.getData();
|
} else {
|
||||||
Log.i(TAG, "[SingleFilePick] File URI:" +data.getData().toString());
|
runOnUiThread(result, paths.get(0), true);
|
||||||
String fullPath = FileUtils.getPath(uri, instance.context());
|
}
|
||||||
|
} 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) {
|
if(fullPath == null) {
|
||||||
fullPath = FileUtils.getUriFromRemote(instance.activeContext(), uri, result);
|
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) {
|
} else if(requestCode == REQUEST_CODE && resultCode == Activity.RESULT_CANCELED) {
|
||||||
result.success(null);
|
result.success(null);
|
||||||
return true;
|
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
|
@Override
|
||||||
public void onMethodCall(MethodCall call, Result result) {
|
public void onMethodCall(MethodCall call, Result result) {
|
||||||
this.result = result;
|
FilePickerPlugin.result = result;
|
||||||
fileType = resolveType(call.method);
|
fileType = resolveType(call.method);
|
||||||
isMultipleSelection = (boolean)call.arguments;
|
isMultipleSelection = (boolean)call.arguments;
|
||||||
|
|
||||||
|
@ -177,13 +204,9 @@ public class FilePickerPlugin implements MethodCallHandler {
|
||||||
Intent intent;
|
Intent intent;
|
||||||
|
|
||||||
if (checkPermission()) {
|
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.setDataAndType(uri, type);
|
||||||
intent.setType(type);
|
intent.setType(type);
|
||||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, isMultipleSelection);
|
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, isMultipleSelection);
|
||||||
|
|
|
@ -38,6 +38,13 @@ public class FileUtils {
|
||||||
return null;
|
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)
|
@TargetApi(19)
|
||||||
private static String getForApi19(Context context, Uri uri) {
|
private static String getForApi19(Context context, Uri uri) {
|
||||||
Log.e(TAG, "Getting for API 19 or above" + uri);
|
Log.e(TAG, "Getting for API 19 or above" + uri);
|
||||||
|
@ -50,11 +57,11 @@ public class FileUtils {
|
||||||
final String type = split[0];
|
final String type = split[0];
|
||||||
if ("primary".equalsIgnoreCase(type)) {
|
if ("primary".equalsIgnoreCase(type)) {
|
||||||
Log.e(TAG, "Primary External Document URI");
|
Log.e(TAG, "Primary External Document URI");
|
||||||
return Environment.getExternalStorageDirectory() + "/" + split[1];
|
return getExternalPath(context) + "/" + split[1];
|
||||||
}
|
}
|
||||||
} else if (isDownloadsDocument(uri)) {
|
} else if (isDownloadsDocument(uri)) {
|
||||||
Log.e(TAG, "Downloads External Document URI");
|
Log.e(TAG, "Downloads External Document URI");
|
||||||
final String id = DocumentsContract.getDocumentId(uri);
|
String id = DocumentsContract.getDocumentId(uri);
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(id)) {
|
if (!TextUtils.isEmpty(id)) {
|
||||||
if (id.startsWith("raw:")) {
|
if (id.startsWith("raw:")) {
|
||||||
|
@ -65,6 +72,9 @@ public class FileUtils {
|
||||||
"content://downloads/my_downloads",
|
"content://downloads/my_downloads",
|
||||||
"content://downloads/all_downloads"
|
"content://downloads/all_downloads"
|
||||||
};
|
};
|
||||||
|
if(id.contains(":")){
|
||||||
|
id = id.split(":")[1];
|
||||||
|
}
|
||||||
for (String contentUriPrefix : contentUriPrefixesToTry) {
|
for (String contentUriPrefix : contentUriPrefixesToTry) {
|
||||||
Uri contentUri = ContentUris.withAppendedId(Uri.parse(contentUriPrefix), Long.valueOf(id));
|
Uri contentUri = ContentUris.withAppendedId(Uri.parse(contentUriPrefix), Long.valueOf(id));
|
||||||
try {
|
try {
|
||||||
|
@ -132,6 +142,7 @@ public class FileUtils {
|
||||||
final int index = cursor.getColumnIndexOrThrow(column);
|
final int index = cursor.getColumnIndexOrThrow(column);
|
||||||
return cursor.getString(index);
|
return cursor.getString(index);
|
||||||
}
|
}
|
||||||
|
} catch(Exception ex){
|
||||||
} finally {
|
} finally {
|
||||||
if (cursor != null)
|
if (cursor != null)
|
||||||
cursor.close();
|
cursor.close();
|
||||||
|
|
|
@ -15,7 +15,7 @@ apply plugin: 'com.android.application'
|
||||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 28
|
compileSdkVersion 29
|
||||||
|
|
||||||
lintOptions {
|
lintOptions {
|
||||||
disable 'InvalidPackage'
|
disable 'InvalidPackage'
|
||||||
|
@ -24,7 +24,7 @@ android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.mr.flutter.plugin.filepickerexample"
|
applicationId "com.mr.flutter.plugin.filepickerexample"
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 28
|
targetSdkVersion 29
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "1.0"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
|
@ -7,8 +7,7 @@
|
||||||
to allow setting breakpoints, to provide hot reload, etc.
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
-->
|
-->
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<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
|
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||||
In most cases you can leave this as-is, but you if you want to provide
|
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:name="io.flutter.app.FlutterApplication"
|
||||||
android:label="file_picker_example"
|
android:label="file_picker_example"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
tools:replace="android:label">
|
tools:replace="android:label"
|
||||||
|
tools:ignore="GoogleAppIndexingWarning">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
|
|
|
@ -5,7 +5,7 @@ buildscript {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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()
|
google()
|
||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
|
gradle.projectsEvaluated {
|
||||||
|
tasks.withType(JavaCompile) {
|
||||||
|
options.encoding = 'UTF-8'
|
||||||
|
options.compilerArgs << "-Xlint:deprecation"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.buildDir = '../build'
|
rootProject.buildDir = '../build'
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
#Sat Jan 26 11:50:40 EET 2019
|
#Thu Aug 29 13:24:58 WEST 2019
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
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
|
||||||
|
|
|
@ -14,9 +14,9 @@ EXTERNAL SOURCES:
|
||||||
:path: ".symlinks/flutter/ios"
|
:path: ".symlinks/flutter/ios"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
file_picker: 78c3344d9b2c343bb3090c2f032b796242ebaea7
|
file_picker: 408623be2125b79a4539cf703be3d4b3abe5e245
|
||||||
Flutter: 9d0fac939486c9aba2809b7982dfdbb47a7b0296
|
Flutter: 58dd7d1b27887414a370fcccb9e645c08ffd7a6a
|
||||||
|
|
||||||
PODFILE CHECKSUM: 1e5af4103afd21ca5ead147d7b81d06f494f51a2
|
PODFILE CHECKSUM: 1e5af4103afd21ca5ead147d7b81d06f494f51a2
|
||||||
|
|
||||||
COCOAPODS: 1.5.3
|
COCOAPODS: 1.7.5
|
||||||
|
|
|
@ -54,6 +54,8 @@
|
||||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
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>"; };
|
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; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
@ -137,6 +139,8 @@
|
||||||
EE3450EDCED914F636FA6BB9 /* Pods */ = {
|
EE3450EDCED914F636FA6BB9 /* Pods */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D616774FB981784E6589DAC9 /* Pods-Runner.debug.xcconfig */,
|
||||||
|
AA58896224B359D6BCEB4ED4 /* Pods-Runner.release.xcconfig */,
|
||||||
);
|
);
|
||||||
name = Pods;
|
name = Pods;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -266,7 +270,7 @@
|
||||||
files = (
|
files = (
|
||||||
);
|
);
|
||||||
inputPaths = (
|
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",
|
"${PODS_ROOT}/../.symlinks/flutter/ios/Flutter.framework",
|
||||||
);
|
);
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
|
@ -275,7 +279,7 @@
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 1;
|
runOnlyForDeploymentPostprocessing = 1;
|
||||||
shellPath = /bin/sh;
|
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;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
|
@ -18,14 +18,19 @@
|
||||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>NSAppleMusicUsageDescription</key>
|
|
||||||
<string>Used to demonstrate file picker plugin</string>
|
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>NSAppleMusicUsageDescription</key>
|
||||||
|
<string>Used to demonstrate file picker plugin</string>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>Used to demonstrate file picker plugin</string>
|
<string>Used to demonstrate file picker plugin</string>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>fetch</string>
|
||||||
|
<string>remote-notification</string>
|
||||||
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
|
@ -36,11 +41,6 @@
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UIBackgroundModes</key>
|
|
||||||
<array>
|
|
||||||
<string>fetch</string>
|
|
||||||
<string>remote-notification</string>
|
|
||||||
</array>
|
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
|
|
@ -1,185 +1,4 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:file_picker_example/src/file_picker_demo.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
|
||||||
|
|
||||||
void main() => runApp(new FilePickerDemo());
|
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(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
|
@ -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(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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{}),
|
||||||
|
}
|
||||||
|
```
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
|
@ -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=
|
|
@ -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
|
||||||
|
}
|
|
@ -47,6 +47,19 @@ class FilePicker {
|
||||||
return filePath != null ? File(filePath) : null;
|
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 {
|
static Future<dynamic> _getPath(String type, bool multipleSelection) async {
|
||||||
try {
|
try {
|
||||||
dynamic result = await _channel.invokeMethod(type, multipleSelection);
|
dynamic result = await _channel.invokeMethod(type, multipleSelection);
|
||||||
|
|
|
@ -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.
|
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>
|
author: Miguel Ruivo <miguel@miguelruivo.com>
|
||||||
homepage: https://github.com/miguelpruivo/plugins_flutter_file_picker
|
homepage: https://github.com/miguelpruivo/plugins_flutter_file_picker
|
||||||
version: 1.3.8
|
version: 1.4.0
|
||||||
|
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
|
@ -12,10 +11,6 @@ dependencies:
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.0.0 <3.0.0"
|
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:
|
flutter:
|
||||||
plugin:
|
plugin:
|
||||||
androidPackage: com.mr.flutter.plugin.filepicker
|
androidPackage: com.mr.flutter.plugin.filepicker
|
||||||
|
|
Loading…
Reference in New Issue