see changelog
This commit is contained in:
parent
a218fff854
commit
e7f014215b
|
@ -1,13 +1,33 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.atom/
|
.atom/
|
||||||
.idea
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
.packages
|
.packages
|
||||||
.dart_tool/
|
|
||||||
.pub/
|
.pub/
|
||||||
build/
|
.dart_tool/
|
||||||
ios/.generated/
|
|
||||||
packages
|
|
||||||
pubspec.lock
|
pubspec.lock
|
||||||
.iml
|
|
||||||
|
Podfile
|
||||||
Podfile.lock
|
Podfile.lock
|
||||||
file_picker.iml
|
Pods/
|
||||||
|
.symlinks/
|
||||||
|
**/Flutter/App.framework/
|
||||||
|
**/Flutter/Flutter.framework/
|
||||||
|
**/Flutter/Generated.xcconfig
|
||||||
|
**/Flutter/flutter_assets/
|
||||||
|
ServiceDefinitions.json
|
||||||
|
xcuserdata/
|
||||||
|
|
||||||
|
local.properties
|
||||||
|
.gradle/
|
||||||
|
gradlew
|
||||||
|
gradlew.bat
|
||||||
|
gradle-wrapper.jar
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
GeneratedPluginRegistrant.h
|
||||||
|
GeneratedPluginRegistrant.m
|
||||||
|
GeneratedPluginRegistrant.java
|
||||||
|
build/
|
||||||
|
.flutter-plugins
|
||||||
|
|
17
CHANGELOG.md
17
CHANGELOG.md
|
@ -1,3 +1,20 @@
|
||||||
|
## 1.1.0
|
||||||
|
|
||||||
|
**Breaking changes**
|
||||||
|
* `FileType.PDF` was removed since now it can be used along with custom file types by using the `FileType.CUSTOM` and providing the file extension (e.g. PDF, SVG, ZIP, etc.).
|
||||||
|
* `FileType.CAPTURE` is now `FileType.CAMERA`
|
||||||
|
|
||||||
|
**New features**
|
||||||
|
* Now it is possible to provide a custom file extension to filter file picking options by using `FileType.CUSTOM`
|
||||||
|
|
||||||
|
**Bug fixes and updates**
|
||||||
|
* Fixes file names from cloud on Android. Previously it would always display **Document**
|
||||||
|
* Fixes an issue on iOS where an exception was being thrown after canceling and re-opening the picker.
|
||||||
|
* Fixes an issue where collision could happen with request codes on Android.
|
||||||
|
* Adds public documentation to `file_picker`
|
||||||
|
* Example app updated.
|
||||||
|
* Updates .gitignore
|
||||||
|
|
||||||
## 1.0.3
|
## 1.0.3
|
||||||
|
|
||||||
* Fixes `build.gradle`.
|
* Fixes `build.gradle`.
|
||||||
|
|
14
README.md
14
README.md
|
@ -4,17 +4,17 @@
|
||||||
</a>
|
</a>
|
||||||
# file_picker
|
# file_picker
|
||||||
|
|
||||||
File picker plugin alows you to use a native file explorer to load absolute file path from different types of files.
|
File picker plugin alows you to use a native file explorer to load absolute file path from different file types.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
First, add *file_picker* as a dependency in [your pubspec.yaml file](https://flutter.io/platform-plugins/).
|
First, add *file_picker* as a dependency in [your pubspec.yaml file](https://flutter.io/platform-plugins/).
|
||||||
|
|
||||||
```
|
```
|
||||||
file_picker: ^1.0.2
|
file_picker: ^1.1.0
|
||||||
```
|
```
|
||||||
## Android
|
## Android
|
||||||
Add `<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />` to your app `AndroidManifest.xml` file.
|
Add `<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>` to your app `AndroidManifest.xml` file.
|
||||||
|
|
||||||
## iOS
|
## iOS
|
||||||
Since we are using *image_picker* as a dependency from this plugin to load paths from gallery and camera, we need the following keys to your _Info.plist_ file, located in `<project root>/ios/Runner/Info.plist`:
|
Since we are using *image_picker* as a dependency from this plugin to load paths from gallery and camera, we need the following keys to your _Info.plist_ file, located in `<project root>/ios/Runner/Info.plist`:
|
||||||
|
@ -22,19 +22,19 @@ Since we are using *image_picker* as a dependency from this plugin to load paths
|
||||||
* `NSPhotoLibraryUsageDescription` - describe why your app needs permission for the photo library. This is called _Privacy - Photo Library Usage Description_ in the visual editor.
|
* `NSPhotoLibraryUsageDescription` - describe why your app needs permission for the photo library. This is called _Privacy - Photo Library Usage Description_ in the visual editor.
|
||||||
* `NSCameraUsageDescription` - describe why your app needs access to the camera. This is called _Privacy - Camera Usage Description_ in the visual editor.
|
* `NSCameraUsageDescription` - describe why your app needs access to the camera. This is called _Privacy - Camera Usage Description_ in the visual editor.
|
||||||
* `NSMicrophoneUsageDescription` - describe why your app needs access to the microphone, if you intend to record videos. This is called _Privacy - Microphone Usage Description_ in the visual editor.
|
* `NSMicrophoneUsageDescription` - describe why your app needs access to the microphone, if you intend to record videos. This is called _Privacy - Microphone Usage Description_ in the visual editor.
|
||||||
|
* `UIBackgroundModes` with the `fetch` and `remote-notifications` keys - describe why your app needs to access background taks, such downloading files (from cloud services) when not cached to locate path. 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).
|
||||||
|
|
||||||
## To-do
|
## 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 **PDF**
|
|
||||||
* [X] Load path from **gallery**
|
* [X] Load path from **gallery**
|
||||||
* [X] Load path from **camera**
|
* [X] Load path from **camera**
|
||||||
* [X] Load path from **video**
|
* [X] Load path from **video**
|
||||||
* [X] Load path from **any** type of file (without filtering)
|
* [X] Load path from **any** type of file (without filtering)
|
||||||
* [ ] Load path from a **custom format**
|
* [X] Load path from a **custom format** by providing a file extension (pdf, svg, zip, etc.)
|
||||||
|
|
||||||
## Demo App
|
## Demo App
|
||||||
|
|
||||||
![Demo](https://github.com/miguelpruivo/plugins_flutter_file_picker/blob/master/example/demo.png)
|
![Demo](https://github.com/miguelpruivo/plugins_flutter_file_picker/blob/master/example/example.gif)
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
*.iml
|
*.iml
|
||||||
*.class
|
*.class
|
||||||
.gradle
|
.gradle
|
||||||
|
.idea/
|
||||||
/local.properties
|
/local.properties
|
||||||
/.idea/workspace.xml
|
/.idea/workspace.xml
|
||||||
/.idea/libraries
|
/.idea/libraries
|
||||||
|
|
|
@ -6,11 +6,14 @@ import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.os.Environment;
|
||||||
import android.support.v4.app.ActivityCompat;
|
import android.support.v4.app.ActivityCompat;
|
||||||
import android.support.v4.content.ContextCompat;
|
import android.support.v4.content.ContextCompat;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
import java.io.BufferedOutputStream;
|
import java.io.BufferedOutputStream;
|
||||||
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
@ -25,10 +28,11 @@ import io.flutter.plugin.common.PluginRegistry.Registrar;
|
||||||
/** FilePickerPlugin */
|
/** FilePickerPlugin */
|
||||||
public class FilePickerPlugin implements MethodCallHandler {
|
public class FilePickerPlugin implements MethodCallHandler {
|
||||||
|
|
||||||
private static final int REQUEST_CODE = 43;
|
private static final int REQUEST_CODE = FilePickerPlugin.class.hashCode() + 43;
|
||||||
|
private static final int PERM_CODE = FilePickerPlugin.class.hashCode() + 50;
|
||||||
private static final String TAG = "FilePicker";
|
private static final String TAG = "FilePicker";
|
||||||
|
|
||||||
private static final String permission = Manifest.permission.WRITE_EXTERNAL_STORAGE;
|
private static final String permission = Manifest.permission.WRITE_EXTERNAL_STORAGE;
|
||||||
|
|
||||||
private static Result result;
|
private static Result result;
|
||||||
private static Registrar instance;
|
private static Registrar instance;
|
||||||
private static String fileType;
|
private static String fileType;
|
||||||
|
@ -54,7 +58,7 @@ public class FilePickerPlugin implements MethodCallHandler {
|
||||||
if(fullPath == null)
|
if(fullPath == null)
|
||||||
{
|
{
|
||||||
FileOutputStream fos = null;
|
FileOutputStream fos = null;
|
||||||
cloudFile = instance.activeContext().getCacheDir().getAbsolutePath() + "/Document";
|
cloudFile = instance.activeContext().getCacheDir().getAbsolutePath() + "/" + FileUtils.getFileName(uri, instance.activeContext());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fos = new FileOutputStream(cloudFile);
|
fos = new FileOutputStream(cloudFile);
|
||||||
|
@ -78,7 +82,7 @@ public class FilePickerPlugin implements MethodCallHandler {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i(TAG, "Loaded file from cloud created on:" + cloudFile);
|
Log.i(TAG, "Cloud file loaded and cached on:" + cloudFile);
|
||||||
fullPath = cloudFile;
|
fullPath = cloudFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,7 +98,7 @@ public class FilePickerPlugin implements MethodCallHandler {
|
||||||
instance.addRequestPermissionsResultListener(new PluginRegistry.RequestPermissionsResultListener() {
|
instance.addRequestPermissionsResultListener(new PluginRegistry.RequestPermissionsResultListener() {
|
||||||
@Override
|
@Override
|
||||||
public boolean onRequestPermissionsResult(int requestCode, String[] strings, int[] grantResults) {
|
public boolean onRequestPermissionsResult(int requestCode, String[] strings, int[] grantResults) {
|
||||||
if (requestCode == 0 && grantResults.length > 0
|
if (requestCode == PERM_CODE && grantResults.length > 0
|
||||||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
startFileExplorer(fileType);
|
startFileExplorer(fileType);
|
||||||
return true;
|
return true;
|
||||||
|
@ -128,11 +132,20 @@ public class FilePickerPlugin implements MethodCallHandler {
|
||||||
Activity activity = instance.activity();
|
Activity activity = instance.activity();
|
||||||
Log.i(TAG, "Requesting permission: " + permission);
|
Log.i(TAG, "Requesting permission: " + permission);
|
||||||
String[] perm = { permission };
|
String[] perm = { permission };
|
||||||
ActivityCompat.requestPermissions(activity, perm, 0);
|
ActivityCompat.requestPermissions(activity, perm, PERM_CODE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String resolveType(String type) {
|
private String resolveType(String type) {
|
||||||
|
|
||||||
|
final boolean isCustom = type.contains("__CUSTOM_");
|
||||||
|
|
||||||
|
if(isCustom) {
|
||||||
|
final String extension = type.split("__CUSTOM_")[1].toLowerCase();
|
||||||
|
String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||||
|
Log.i(TAG, "Custom file type: " + mime);
|
||||||
|
return mime;
|
||||||
|
}
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "PDF":
|
case "PDF":
|
||||||
return "application/pdf";
|
return "application/pdf";
|
||||||
|
@ -152,14 +165,19 @@ public class FilePickerPlugin implements MethodCallHandler {
|
||||||
Intent intent;
|
Intent intent;
|
||||||
|
|
||||||
if (checkPermission()) {
|
if (checkPermission()) {
|
||||||
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT){
|
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
||||||
intent = new Intent(Intent.ACTION_PICK);
|
intent = new Intent(Intent.ACTION_PICK);
|
||||||
} else {
|
} else {
|
||||||
intent = new Intent(Intent.ACTION_GET_CONTENT);
|
intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Uri uri = Uri.parse(Environment.getExternalStorageDirectory().getPath() + File.separator);
|
||||||
|
intent.setDataAndType(uri, type);
|
||||||
intent.setType(type);
|
intent.setType(type);
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
|
|
||||||
|
Log.d(TAG, "Intent: " + intent.toString());
|
||||||
|
|
||||||
instance.activity().startActivityForResult(intent, REQUEST_CODE);
|
instance.activity().startActivityForResult(intent, REQUEST_CODE);
|
||||||
} else {
|
} else {
|
||||||
requestPermission();
|
requestPermission();
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
package com.mr.flutter.plugin.filepicker;
|
package com.mr.flutter.plugin.filepicker;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.content.ContentUris;
|
import android.content.ContentUris;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
@ -10,50 +11,42 @@ import android.provider.DocumentsContract;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Credits to NiRRaNjAN from package in.gauriinfotech.commons;.
|
* Credits to NiRRaNjAN from utils extracted of in.gauriinfotech.commons;.
|
||||||
**/
|
**/
|
||||||
public class FileUtils
|
|
||||||
{
|
public class FileUtils {
|
||||||
|
|
||||||
private static final String tag = "FilePathPicker";
|
private static final String tag = "FilePathPicker";
|
||||||
|
|
||||||
public static String getPath(final Uri uri, Context context)
|
public static String getPath(final Uri uri, Context context) {
|
||||||
{
|
|
||||||
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
|
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
|
||||||
if (isKitKat)
|
if (isKitKat) {
|
||||||
{
|
|
||||||
return getForApi19(context, uri);
|
return getForApi19(context, uri);
|
||||||
} else if ("content".equalsIgnoreCase(uri.getScheme()))
|
} else if ("content".equalsIgnoreCase(uri.getScheme())) {
|
||||||
{
|
if (isGooglePhotosUri(uri)) {
|
||||||
if (isGooglePhotosUri(uri))
|
|
||||||
{
|
|
||||||
return uri.getLastPathSegment();
|
return uri.getLastPathSegment();
|
||||||
}
|
}
|
||||||
return getDataColumn(context, uri, null, null);
|
return getDataColumn(context, uri, null, null);
|
||||||
} else if ("file".equalsIgnoreCase(uri.getScheme()))
|
} else if ("file".equalsIgnoreCase(uri.getScheme())) {
|
||||||
{
|
|
||||||
return uri.getPath();
|
return uri.getPath();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(19)
|
@TargetApi(19)
|
||||||
private static String getForApi19(Context context, Uri uri)
|
private static String getForApi19(Context context, Uri uri) {
|
||||||
{
|
|
||||||
Log.e(tag, "+++ API 19 URI :: " + uri);
|
Log.e(tag, "+++ API 19 URI :: " + uri);
|
||||||
if (DocumentsContract.isDocumentUri(context, uri))
|
if (DocumentsContract.isDocumentUri(context, uri)) {
|
||||||
{
|
|
||||||
Log.e(tag, "+++ Document URI");
|
Log.e(tag, "+++ Document URI");
|
||||||
if (isExternalStorageDocument(uri))
|
if (isExternalStorageDocument(uri)) {
|
||||||
{
|
|
||||||
Log.e(tag, "+++ External Document URI");
|
Log.e(tag, "+++ External Document URI");
|
||||||
final String docId = DocumentsContract.getDocumentId(uri);
|
final String docId = DocumentsContract.getDocumentId(uri);
|
||||||
final String[] split = docId.split(":");
|
final String[] split = docId.split(":");
|
||||||
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 Environment.getExternalStorageDirectory() + "/" + split[1];
|
||||||
}
|
}
|
||||||
|
@ -65,13 +58,25 @@ public class FileUtils
|
||||||
if (id.startsWith("raw:")) {
|
if (id.startsWith("raw:")) {
|
||||||
return id.replaceFirst("raw:", "");
|
return id.replaceFirst("raw:", "");
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
final Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
|
String[] contentUriPrefixesToTry = new String[]{
|
||||||
return getDataColumn(context, contentUri, null, null);
|
"content://downloads/public_downloads",
|
||||||
} catch (Exception e) {
|
"content://downloads/my_downloads",
|
||||||
Log.e(tag, "+++ Something went wrong while retrieving document path: " + e.toString());
|
"content://downloads/all_downloads"
|
||||||
|
};
|
||||||
|
for (String contentUriPrefix : contentUriPrefixesToTry) {
|
||||||
|
Uri contentUri = ContentUris.withAppendedId(Uri.parse(contentUriPrefix), Long.valueOf(id));
|
||||||
|
try {
|
||||||
|
String path = getDataColumn(context, contentUri, null, null);
|
||||||
|
if (path != null) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(tag, "+++ Something went wrong while retrieving document path: " + e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else if (isMediaDocument(uri)) {
|
} else if (isMediaDocument(uri)) {
|
||||||
Log.e(tag, "+++ Media Document URI");
|
Log.e(tag, "+++ Media Document URI");
|
||||||
final String docId = DocumentsContract.getDocumentId(uri);
|
final String docId = DocumentsContract.getDocumentId(uri);
|
||||||
|
@ -79,16 +84,13 @@ public class FileUtils
|
||||||
final String type = split[0];
|
final String type = split[0];
|
||||||
|
|
||||||
Uri contentUri = null;
|
Uri contentUri = null;
|
||||||
if ("image".equals(type))
|
if ("image".equals(type)) {
|
||||||
{
|
|
||||||
Log.e(tag, "+++ Image Media Document URI");
|
Log.e(tag, "+++ Image Media Document URI");
|
||||||
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
||||||
} else if ("video".equals(type))
|
} else if ("video".equals(type)) {
|
||||||
{
|
|
||||||
Log.e(tag, "+++ Video Media Document URI");
|
Log.e(tag, "+++ Video Media Document URI");
|
||||||
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
|
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
|
||||||
} else if ("audio".equals(type))
|
} else if ("audio".equals(type)) {
|
||||||
{
|
|
||||||
Log.e(tag, "+++ Audio Media Document URI");
|
Log.e(tag, "+++ Audio Media Document URI");
|
||||||
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
||||||
}
|
}
|
||||||
|
@ -100,15 +102,13 @@ public class FileUtils
|
||||||
|
|
||||||
return getDataColumn(context, contentUri, selection, selectionArgs);
|
return getDataColumn(context, contentUri, selection, selectionArgs);
|
||||||
}
|
}
|
||||||
} else if ("content".equalsIgnoreCase(uri.getScheme()))
|
} else if ("content".equalsIgnoreCase(uri.getScheme())) {
|
||||||
{
|
|
||||||
Log.e(tag, "+++ No DOCUMENT URI :: CONTENT ");
|
Log.e(tag, "+++ No DOCUMENT URI :: CONTENT ");
|
||||||
if (isGooglePhotosUri(uri))
|
if (isGooglePhotosUri(uri))
|
||||||
return uri.getLastPathSegment();
|
return uri.getLastPathSegment();
|
||||||
|
|
||||||
return getDataColumn(context, uri, null, null);
|
return getDataColumn(context, uri, null, null);
|
||||||
} else if ("file".equalsIgnoreCase(uri.getScheme()))
|
} else if ("file".equalsIgnoreCase(uri.getScheme())) {
|
||||||
{
|
|
||||||
Log.e(tag, "+++ No DOCUMENT URI :: FILE ");
|
Log.e(tag, "+++ No DOCUMENT URI :: FILE ");
|
||||||
return uri.getPath();
|
return uri.getPath();
|
||||||
}
|
}
|
||||||
|
@ -116,47 +116,74 @@ public class FileUtils
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getDataColumn(Context context, Uri uri, String selection,
|
private static String getDataColumn(Context context, Uri uri, String selection,
|
||||||
String[] selectionArgs)
|
String[] selectionArgs) {
|
||||||
{
|
|
||||||
Cursor cursor = null;
|
Cursor cursor = null;
|
||||||
final String column = "_data";
|
final String column = "_data";
|
||||||
final String[] projection = {
|
final String[] projection = {
|
||||||
column
|
column
|
||||||
};
|
};
|
||||||
try
|
try {
|
||||||
{
|
|
||||||
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
|
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
|
||||||
null);
|
null);
|
||||||
if (cursor != null && cursor.moveToFirst())
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
{
|
|
||||||
final int index = cursor.getColumnIndexOrThrow(column);
|
final int index = cursor.getColumnIndexOrThrow(column);
|
||||||
return cursor.getString(index);
|
return cursor.getString(index);
|
||||||
}
|
}
|
||||||
} finally
|
} finally {
|
||||||
{
|
|
||||||
if (cursor != null)
|
if (cursor != null)
|
||||||
cursor.close();
|
cursor.close();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isExternalStorageDocument(Uri uri)
|
public static String getFileName(Uri uri, Context context) {
|
||||||
{
|
String result = null;
|
||||||
|
|
||||||
|
//if uri is content
|
||||||
|
if (uri.getScheme() != null && uri.getScheme().equals("content")) {
|
||||||
|
Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
|
||||||
|
try {
|
||||||
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
|
//local filesystem
|
||||||
|
int index = cursor.getColumnIndex("_data");
|
||||||
|
if (index == -1)
|
||||||
|
//google drive
|
||||||
|
index = cursor.getColumnIndex("_display_name");
|
||||||
|
result = cursor.getString(index);
|
||||||
|
if (result != null)
|
||||||
|
uri = Uri.parse(result);
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(uri.getPath() != null) {
|
||||||
|
result = uri.getPath();
|
||||||
|
int cut = result.lastIndexOf('/');
|
||||||
|
if (cut != -1)
|
||||||
|
result = result.substring(cut + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static boolean isExternalStorageDocument(Uri uri) {
|
||||||
return "com.android.externalstorage.documents".equals(uri.getAuthority());
|
return "com.android.externalstorage.documents".equals(uri.getAuthority());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isDownloadsDocument(Uri uri)
|
private static boolean isDownloadsDocument(Uri uri) {
|
||||||
{
|
|
||||||
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
|
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isMediaDocument(Uri uri)
|
private static boolean isMediaDocument(Uri uri) {
|
||||||
{
|
|
||||||
return "com.android.providers.media.documents".equals(uri.getAuthority());
|
return "com.android.providers.media.documents".equals(uri.getAuthority());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isGooglePhotosUri(Uri uri)
|
private static boolean isGooglePhotosUri(Uri uri) {
|
||||||
{
|
|
||||||
return "com.google.android.apps.photos.content".equals(uri.getAuthority());
|
return "com.google.android.apps.photos.content".equals(uri.getAuthority());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
# file_picker_example
|
|
||||||
|
|
||||||
Demonstrates how to use the file_picker plugin.
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
For help getting started with Flutter, view our online
|
|
||||||
[documentation](https://flutter.io/).
|
|
BIN
example/demo.png
BIN
example/demo.png
Binary file not shown.
Before Width: | Height: | Size: 129 KiB |
Binary file not shown.
After Width: | Height: | Size: 156 KiB |
|
@ -38,6 +38,11 @@
|
||||||
<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>
|
||||||
|
|
|
@ -13,20 +13,31 @@ class MyApp extends StatefulWidget {
|
||||||
class _MyAppState extends State<MyApp> {
|
class _MyAppState extends State<MyApp> {
|
||||||
String _fileName = '...';
|
String _fileName = '...';
|
||||||
String _path = '...';
|
String _path = '...';
|
||||||
FileType _pickingType = FileType.ANY;
|
String _extension;
|
||||||
|
bool _hasValidMime = false;
|
||||||
|
FileType _pickingType;
|
||||||
|
TextEditingController _controller = new TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller.addListener(() => _extension = _controller.text);
|
||||||
|
}
|
||||||
|
|
||||||
void _openFileExplorer() async {
|
void _openFileExplorer() async {
|
||||||
try {
|
if (_pickingType != FileType.CUSTOM || _hasValidMime) {
|
||||||
_path = await FilePicker.getFilePath(type: _pickingType);
|
try {
|
||||||
} on PlatformException catch (e) {
|
_path = await FilePicker.getFilePath(type: _pickingType, fileExtension: _extension);
|
||||||
print(e.toString());
|
} on PlatformException catch (e) {
|
||||||
|
print("Unsupported operation" + e.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_fileName = _path != null ? _path.split('/').last : '...';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mounted) return;
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_fileName = _path != null ? _path.split('/').last : '...';
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -36,78 +47,97 @@ class _MyAppState extends State<MyApp> {
|
||||||
appBar: new AppBar(
|
appBar: new AppBar(
|
||||||
title: const Text('Plugin example app'),
|
title: const Text('Plugin example app'),
|
||||||
),
|
),
|
||||||
body: new Center(
|
body: SingleChildScrollView(
|
||||||
|
child: new Center(
|
||||||
|
child: new Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 50.0, left: 10.0, right: 10.0),
|
||||||
child: new Column(
|
child: new Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
new Padding(
|
new Padding(
|
||||||
padding: const EdgeInsets.all(20.0),
|
padding: const EdgeInsets.only(top: 20.0),
|
||||||
child: new DropdownButton(
|
child: new DropdownButton(
|
||||||
hint: new Text('LOAD FILE PATH FROM...'),
|
hint: new Text('LOAD PATH FROM'),
|
||||||
value: _pickingType,
|
value: _pickingType,
|
||||||
items: <DropdownMenuItem>[
|
items: <DropdownMenuItem>[
|
||||||
new DropdownMenuItem(
|
new DropdownMenuItem(
|
||||||
child: new Text('FROM CAMERA'),
|
child: new Text('FROM CAMERA'),
|
||||||
value: FileType.CAPTURE,
|
value: FileType.CAMERA,
|
||||||
|
),
|
||||||
|
new DropdownMenuItem(
|
||||||
|
child: new Text('FROM GALLERY'),
|
||||||
|
value: FileType.IMAGE,
|
||||||
|
),
|
||||||
|
new DropdownMenuItem(
|
||||||
|
child: new Text('FROM VIDEO'),
|
||||||
|
value: FileType.VIDEO,
|
||||||
|
),
|
||||||
|
new DropdownMenuItem(
|
||||||
|
child: new Text('FROM ANY'),
|
||||||
|
value: FileType.ANY,
|
||||||
|
),
|
||||||
|
new DropdownMenuItem(
|
||||||
|
child: new Text('CUSTOM FORMAT'),
|
||||||
|
value: FileType.CUSTOM,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: (value) => setState(() => _pickingType = value)),
|
||||||
|
),
|
||||||
|
new ConstrainedBox(
|
||||||
|
constraints: new BoxConstraints(maxWidth: 150.0),
|
||||||
|
child: _pickingType == FileType.CUSTOM
|
||||||
|
? new TextFormField(
|
||||||
|
maxLength: 20,
|
||||||
|
autovalidate: true,
|
||||||
|
controller: _controller,
|
||||||
|
decoration: InputDecoration(labelText: 'File type'),
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: new Container(),
|
||||||
|
),
|
||||||
|
new Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 50.0, bottom: 20.0),
|
||||||
|
child: new RaisedButton(
|
||||||
|
onPressed: () => _openFileExplorer(),
|
||||||
|
child: new Text("Open file picker"),
|
||||||
),
|
),
|
||||||
new DropdownMenuItem(
|
),
|
||||||
child: new Text('FROM GALLERY'),
|
new Text(
|
||||||
value: FileType.IMAGE,
|
'URI PATH ',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: new TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
new Text(
|
||||||
|
_path ?? '...',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
softWrap: true,
|
||||||
|
textScaleFactor: 0.85,
|
||||||
|
),
|
||||||
|
new Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 10.0),
|
||||||
|
child: new Text(
|
||||||
|
'FILE NAME ',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: new TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
new DropdownMenuItem(
|
),
|
||||||
child: new Text('FROM PDF'),
|
new Text(
|
||||||
value: FileType.PDF,
|
_fileName,
|
||||||
),
|
textAlign: TextAlign.center,
|
||||||
new DropdownMenuItem(
|
),
|
||||||
child: new Text('FROM VIDEO'),
|
],
|
||||||
value: FileType.VIDEO,
|
|
||||||
),
|
|
||||||
new DropdownMenuItem(
|
|
||||||
child: new Text('FROM ANY'),
|
|
||||||
value: FileType.ANY,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(
|
|
||||||
() {
|
|
||||||
_pickingType = value;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
new Padding(
|
)),
|
||||||
padding: const EdgeInsets.all(20.0),
|
),
|
||||||
child: new RaisedButton(
|
|
||||||
onPressed: () => _openFileExplorer(),
|
|
||||||
child: new Text("Open file picker"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
new Text(
|
|
||||||
'URI PATH ',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: new TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
new Text(
|
|
||||||
_path ?? '...',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
softWrap: true,
|
|
||||||
textScaleFactor: 0.85,
|
|
||||||
),
|
|
||||||
new Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 10.0),
|
|
||||||
child: new Text(
|
|
||||||
'FILE NAME ',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: new TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
new Text(
|
|
||||||
_fileName,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,5 +2,5 @@
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
#import <MobileCoreServices/MobileCoreServices.h>
|
#import <MobileCoreServices/MobileCoreServices.h>
|
||||||
|
|
||||||
@interface FilePickerPlugin : NSObject<FlutterPlugin, UIDocumentPickerDelegate, UITabBarDelegate, UINavigationControllerDelegate,UIImagePickerControllerDelegate>
|
@interface FilePickerPlugin : NSObject<FlutterPlugin, UIDocumentPickerDelegate, UITabBarDelegate, UINavigationControllerDelegate, UIImagePickerControllerDelegate>
|
||||||
@end
|
@end
|
||||||
|
|
|
@ -83,7 +83,6 @@ didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls{
|
||||||
|
|
||||||
|
|
||||||
// VideoPicker delegate
|
// VideoPicker delegate
|
||||||
|
|
||||||
- (void) resolvePickVideo{
|
- (void) resolvePickVideo{
|
||||||
|
|
||||||
UIImagePickerController *videoPicker = [[UIImagePickerController alloc] init];
|
UIImagePickerController *videoPicker = [[UIImagePickerController alloc] init];
|
||||||
|
@ -102,7 +101,13 @@ didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls{
|
||||||
_result([videoURL path]);
|
_result([videoURL path]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller {
|
||||||
|
_result = nil;
|
||||||
|
[controller dismissViewControllerAnimated:YES completion:NULL];
|
||||||
|
}
|
||||||
|
|
||||||
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
|
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
|
||||||
|
_result = nil;
|
||||||
[picker dismissViewControllerAnimated:YES completion:NULL];
|
[picker dismissViewControllerAnimated:YES completion:NULL];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
//
|
//
|
||||||
// Created by Miguel Ruivo on 05/12/2018.
|
// Created by Miguel Ruivo on 05/12/2018.
|
||||||
//
|
//
|
||||||
|
#import <MobileCoreServices/MobileCoreServices.h>
|
||||||
@interface FileUtils : NSObject
|
@interface FileUtils : NSObject
|
||||||
+ (NSString*) resolveType:(NSString*)type;
|
+ (NSString*) resolveType:(NSString*)type;
|
||||||
+ (NSString*) resolvePath:(NSArray<NSURL *> *)urls;
|
+ (NSString*) resolvePath:(NSArray<NSURL *> *)urls;
|
||||||
|
|
|
@ -11,6 +11,18 @@
|
||||||
|
|
||||||
+ (NSString*) resolveType:(NSString*)type {
|
+ (NSString*) resolveType:(NSString*)type {
|
||||||
|
|
||||||
|
BOOL isCustom = [type containsString:@"__CUSTOM_"];
|
||||||
|
|
||||||
|
if(isCustom) {
|
||||||
|
type = [type stringByReplacingOccurrencesOfString:@"__CUSTOM_" withString:@""];
|
||||||
|
NSString * format = [NSString stringWithFormat:@"dummy.%@", type];
|
||||||
|
CFStringRef UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)[format pathExtension], NULL);
|
||||||
|
NSString * UTIString = (__bridge NSString *)(UTI);
|
||||||
|
CFRelease(UTI);
|
||||||
|
NSLog(@"Custom file type: %@", UTIString);
|
||||||
|
return [UTIString containsString:@"dyn."] ? nil : UTIString;
|
||||||
|
}
|
||||||
|
|
||||||
if ([type isEqualToString:@"PDF"]) {
|
if ([type isEqualToString:@"PDF"]) {
|
||||||
return @"com.adobe.pdf";
|
return @"com.adobe.pdf";
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,36 +6,55 @@ import 'package:image_picker/image_picker.dart';
|
||||||
/// Supported file types, [ANY] should be used if the file you need isn't listed
|
/// Supported file types, [ANY] should be used if the file you need isn't listed
|
||||||
enum FileType {
|
enum FileType {
|
||||||
ANY,
|
ANY,
|
||||||
PDF,
|
|
||||||
IMAGE,
|
IMAGE,
|
||||||
VIDEO,
|
VIDEO,
|
||||||
CAPTURE,
|
CAMERA,
|
||||||
|
CUSTOM,
|
||||||
}
|
}
|
||||||
|
|
||||||
class FilePicker {
|
class FilePicker {
|
||||||
static const MethodChannel _channel = const MethodChannel('file_picker');
|
static const MethodChannel _channel = const MethodChannel('file_picker');
|
||||||
|
static const String _tag = 'FilePicker';
|
||||||
|
|
||||||
static Future<String> _getPath(String type) async => await _channel.invokeMethod(type);
|
static Future<String> _getPath(String type) async {
|
||||||
|
try {
|
||||||
static Future<String> _getImage(ImageSource type) async {
|
return await _channel.invokeMethod(type);
|
||||||
var image = await ImagePicker.pickImage(source: type);
|
} on PlatformException catch (e) {
|
||||||
|
print("[$_tag] Platform exception: " + e.toString());
|
||||||
return image?.path;
|
} catch (e) {
|
||||||
|
print(
|
||||||
|
"[$_tag] Unsupported operation. This probably have happened because [${type.split('_').last}] is an unsupported file type. You may want to try FileType.ALL instead.");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a [String] with the absolute path for the selected file
|
static Future<String> _getImage(ImageSource type) async {
|
||||||
static Future<String> getFilePath({FileType type = FileType.ANY}) async {
|
try {
|
||||||
|
var image = await ImagePicker.pickImage(source: type);
|
||||||
|
return image?.path;
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
print("[$_tag] Platform exception: " + e.toString());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an absolute file path from the calling platform
|
||||||
|
///
|
||||||
|
/// A [type] must be provided to filter the picking results.
|
||||||
|
/// Can be used a custom file type with `FileType.CUSTOM`. A [fileExtension] must be provided (e.g. PDF, SVG, etc.)
|
||||||
|
/// Defaults to `FileType.ANY` which will display all file types.
|
||||||
|
static Future<String> getFilePath({FileType type = FileType.ANY, String fileExtension}) async {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case FileType.IMAGE:
|
case FileType.IMAGE:
|
||||||
return _getImage(ImageSource.gallery);
|
return _getImage(ImageSource.gallery);
|
||||||
case FileType.CAPTURE:
|
case FileType.CAMERA:
|
||||||
return _getImage(ImageSource.camera);
|
return _getImage(ImageSource.camera);
|
||||||
case FileType.PDF:
|
|
||||||
return _getPath('PDF');
|
|
||||||
case FileType.VIDEO:
|
case FileType.VIDEO:
|
||||||
return _getPath('VIDEO');
|
return _getPath('VIDEO');
|
||||||
case FileType.ANY:
|
case FileType.ANY:
|
||||||
return _getPath('ANY');
|
return _getPath('ANY');
|
||||||
|
case FileType.CUSTOM:
|
||||||
|
return _getPath('__CUSTOM_' + (fileExtension ?? ''));
|
||||||
default:
|
default:
|
||||||
return _getPath('ANY');
|
return _getPath('ANY');
|
||||||
}
|
}
|
||||||
|
|
32
pubspec.yaml
32
pubspec.yaml
|
@ -1,6 +1,6 @@
|
||||||
name: file_picker
|
name: file_picker
|
||||||
description: A plugin that allows you to pick absolute paths from diferent file types.
|
description: A plugin that allows you to pick absolute paths from diferent file types.
|
||||||
version: 1.0.3
|
version: 1.1.0
|
||||||
author: Miguel Ruivo <miguelpruivo@outlook.com>
|
author: Miguel Ruivo <miguelpruivo@outlook.com>
|
||||||
homepage: https://github.com/miguelpruivo/plugins_flutter_file_picker
|
homepage: https://github.com/miguelpruivo/plugins_flutter_file_picker
|
||||||
|
|
||||||
|
@ -23,33 +23,3 @@ flutter:
|
||||||
androidPackage: com.mr.flutter.plugin.filepicker
|
androidPackage: com.mr.flutter.plugin.filepicker
|
||||||
pluginClass: FilePickerPlugin
|
pluginClass: FilePickerPlugin
|
||||||
|
|
||||||
# To add assets to your plugin package, add an assets section, like this:
|
|
||||||
# assets:
|
|
||||||
# - images/a_dot_burr.jpeg
|
|
||||||
# - images/a_dot_ham.jpeg
|
|
||||||
#
|
|
||||||
# For details regarding assets in packages, see
|
|
||||||
# https://flutter.io/assets-and-images/#from-packages
|
|
||||||
#
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
|
||||||
# https://flutter.io/assets-and-images/#resolution-aware.
|
|
||||||
|
|
||||||
# To add custom fonts to your plugin package, add a fonts section here,
|
|
||||||
# in this "flutter" section. Each entry in this list should have a
|
|
||||||
# "family" key with the font family name, and a "fonts" key with a
|
|
||||||
# list giving the asset and other descriptors for the font. For
|
|
||||||
# example:
|
|
||||||
# fonts:
|
|
||||||
# - family: Schyler
|
|
||||||
# fonts:
|
|
||||||
# - asset: fonts/Schyler-Regular.ttf
|
|
||||||
# - asset: fonts/Schyler-Italic.ttf
|
|
||||||
# style: italic
|
|
||||||
# - family: Trajan Pro
|
|
||||||
# fonts:
|
|
||||||
# - asset: fonts/TrajanPro.ttf
|
|
||||||
# - asset: fonts/TrajanPro_Bold.ttf
|
|
||||||
# weight: 700
|
|
||||||
#
|
|
||||||
# For details regarding fonts in packages, see
|
|
||||||
# https://flutter.io/custom-fonts/#from-packages
|
|
||||||
|
|
Loading…
Reference in New Issue