Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

[image_picker] Select any file with image_picker. #2856

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/image_picker/image_picker/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.6.8

* Add `getFile(allowedExtensions)` method so users can select any type of file (web only).

## 0.6.7+2

* iOS: Fixes unpresentable album/image picker if window's root view controller is already presenting other view controller.
Expand Down
56 changes: 56 additions & 0 deletions packages/image_picker/image_picker/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import 'dart:async';
import 'dart:io';
import 'package:intl/intl.dart' show NumberFormat;

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
Expand Down Expand Up @@ -65,6 +66,16 @@ class _MyHomePageState extends State<MyHomePage> {
}
}

void _onSelectTextFilePressed({BuildContext context}) async {
if (_controller != null) {
await _controller.setVolume(0.0);
}
final PickedFile file = await _picker.getFile(
allowedExtensions: ['txt', 'json'],
);
return _displayTextFileContents(file, context: context);
}

void _onImageButtonPressed(ImageSource source, {BuildContext context}) async {
if (_controller != null) {
await _controller.setVolume(0.0);
Expand Down Expand Up @@ -270,6 +281,19 @@ class _MyHomePageState extends State<MyHomePage> {
child: const Icon(Icons.videocam),
),
),
if (kIsWeb)
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: FloatingActionButton(
backgroundColor: Colors.green,
onPressed: () {
_onSelectTextFilePressed(context: context);
},
heroTag: 'file',
tooltip: 'Select a file (.txt|.json)',
child: const Icon(Icons.picture_as_pdf),
),
),
],
),
);
Expand All @@ -284,6 +308,38 @@ class _MyHomePageState extends State<MyHomePage> {
return null;
}

Future<void> _displayTextFileContents(
PickedFile file, {
BuildContext context,
}) async {
final fileContents = await file.readAsString();
final size = NumberFormat.compact().format(await file.length());
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('${file.name} (${size}B)'),
content: Scrollbar(
child: SingleChildScrollView(
child: Text(
fileContents,
style: TextStyle(fontFamily: "monospace"),
),
),
),
actions: [
FlatButton(
child: const Text('CLOSE'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}

Future<void> _displayPickImageDialog(
BuildContext context, OnPickImageCallback onPick) async {
return showDialog(
Expand Down
3 changes: 2 additions & 1 deletion packages/image_picker/image_picker/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ description: Demonstrates how to use the image_picker plugin.
author: Flutter Team <[email protected]>

dependencies:
intl: ^0.16.1
video_player: ^0.10.3
flutter:
sdk: flutter
Expand All @@ -21,5 +22,5 @@ flutter:
uses-material-design: true

environment:
sdk: ">=2.0.0-dev.28.0 <3.0.0"
sdk: ">=2.2.2 <3.0.0"
flutter: ">=1.10.0 <2.0.0"
12 changes: 12 additions & 0 deletions packages/image_picker/image_picker/lib/image_picker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,18 @@ class ImagePicker {
);
}

/// Returns an arbitrary [PickedFile].
///
/// The [allowedExtensions] argument controls what files may be selected, for example:
/// \['pdf', 'doc'] to allow only to pick .pdf or .doc files.
///
/// When empty, the plugin lets the user pick any file.
Future<PickedFile> getFile({
List<String> allowedExtensions = const [],
}) {
return platform.pickArbitraryFile(allowedExtensions: allowedExtensions);
}

/// Retrieve the lost image file when [pickImage] or [pickVideo] failed because the MainActivity is destroyed. (Android only)
///
/// Image or video can be lost if the MainActivity is destroyed. And there is no guarantee that the MainActivity is always alive.
Expand Down
4 changes: 2 additions & 2 deletions packages/image_picker/image_picker/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: image_picker
description: Flutter plugin for selecting images from the Android and iOS image
library, and taking new pictures with the camera.
homepage: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker
version: 0.6.7+2
version: 0.6.8

flutter:
plugin:
Expand All @@ -17,7 +17,7 @@ dependencies:
flutter:
sdk: flutter
flutter_plugin_android_lifecycle: ^1.0.2
image_picker_platform_interface: ^1.1.0
image_picker_platform_interface: ^1.2.0

dev_dependencies:
video_player: ^0.10.3
Expand Down
9 changes: 7 additions & 2 deletions packages/image_picker/image_picker_for_web/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
# 0.1.0+1
## 0.1.1

* Implement `pickArbitraryFile` platform method.
* Initialize `name` and `size` of the picked file.

## 0.1.0+1

* Remove `android` directory.

# 0.1.0
## 0.1.0

* Initial open-source release.
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class ImagePickerPlugin extends ImagePickerPlatform {
CameraDevice preferredCameraDevice = CameraDevice.rear,
}) {
String capture = computeCaptureAttribute(source, preferredCameraDevice);
return pickFile(accept: _kAcceptImageMimeType, capture: capture);
return pickFileFromBrowser(accept: _kAcceptImageMimeType, capture: capture);
}

@override
Expand All @@ -50,7 +50,15 @@ class ImagePickerPlugin extends ImagePickerPlatform {
Duration maxDuration,
}) {
String capture = computeCaptureAttribute(source, preferredCameraDevice);
return pickFile(accept: _kAcceptVideoMimeType, capture: capture);
return pickFileFromBrowser(accept: _kAcceptVideoMimeType, capture: capture);
}

@override
Future<PickedFile> pickArbitraryFile({
List<String> allowedExtensions = const [],
}) {
String accept = computeAcceptAttribute(allowedExtensions);
return pickFileFromBrowser(accept: accept, capture: null);
}

/// Injects a file input with the specified accept+capture attributes, and
Expand All @@ -59,7 +67,7 @@ class ImagePickerPlugin extends ImagePickerPlatform {
/// `capture` is only supported in mobile browsers.
/// See https://caniuse.com/#feat=html-media-capture
@visibleForTesting
Future<PickedFile> pickFile({
Future<PickedFile> pickFileFromBrowser({
String accept,
String capture,
}) {
Expand All @@ -81,6 +89,18 @@ class ImagePickerPlugin extends ImagePickerPlatform {
return null;
}

/// Converts a List of file extensions into the accept attribute.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept
@visibleForTesting
String computeAcceptAttribute(List<String> extensions) {
return extensions?.where((e) => e?.trim()?.isNotEmpty ?? false)?.map((e) {
String ext = e.trim();
return ext.startsWith('\.') ? ext : '.$ext';
})?.join(',') ??
'';
}

html.File _getFileFromInput(html.FileUploadInputElement input) {
if (_hasOverrides) {
return _overrides.getFileFromInput(input);
Expand All @@ -90,12 +110,15 @@ class ImagePickerPlugin extends ImagePickerPlatform {

/// Handles the OnChange event from a FileUploadInputElement object
/// Returns the objectURL of the selected file.
String _handleOnChangeEvent(html.Event event) {
_HtmlFileInfo _handleOnChangeEvent(html.Event event) {
final html.FileUploadInputElement input = event?.target;
final html.File file = _getFileFromInput(input);

if (file != null) {
return html.Url.createObjectUrl(file);
return _HtmlFileInfo()
..url = html.Url.createObjectUrl(file)
..length = file.size
..name = file.name;
}
return null;
}
Expand All @@ -105,9 +128,13 @@ class ImagePickerPlugin extends ImagePickerPlatform {
final Completer<PickedFile> _completer = Completer<PickedFile>();
// Observe the input until we can return something
input.onChange.first.then((event) {
final objectUrl = _handleOnChangeEvent(event);
final fileInfo = _handleOnChangeEvent(event);
if (!_completer.isCompleted) {
_completer.complete(PickedFile(objectUrl));
_completer.complete(PickedFile(
fileInfo.url,
name: fileInfo.name,
length: fileInfo.length,
));
}
});
input.onError.first.then((event) {
Expand Down Expand Up @@ -142,7 +169,10 @@ class ImagePickerPlugin extends ImagePickerPlatform {
return _overrides.createInputElement(accept, capture);
}

html.Element element = html.FileUploadInputElement()..accept = accept;
final element = html.FileUploadInputElement();
if (accept?.isNotEmpty ?? false) {
element.accept = accept;
}

if (capture != null) {
element.setAttribute('capture', capture);
Expand All @@ -159,6 +189,14 @@ class ImagePickerPlugin extends ImagePickerPlatform {
}
}

/// A simple PODO to contain the information that we need
/// from the selected html.File
class _HtmlFileInfo {
int length;
String url;
String name;
}

// Some tools to override behavior for unit-testing
/// A function that creates a file input with the passed in `accept` and `capture` attributes.
@visibleForTesting
Expand Down
4 changes: 2 additions & 2 deletions packages/image_picker/image_picker_for_web/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ homepage: https://github.com/flutter/plugins/tree/master/packages/image_picker/i
# 0.1.y+z is compatible with 1.0.0, if you land a breaking change bump
# the version to 2.0.0.
# See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0
version: 0.1.0+1
version: 0.1.1

flutter:
plugin:
Expand All @@ -14,7 +14,7 @@ flutter:
fileName: image_picker_for_web.dart

dependencies:
image_picker_platform_interface: ^1.1.0
image_picker_platform_interface: ^1.2.0
flutter:
sdk: flutter
flutter_web_plugins:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:image_picker_for_web/image_picker_for_web.dart';
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';

final String expectedStringContents = "Hello, world!";
final String expectedStringContents = 'Hello, world!';
final expectedSize = expectedStringContents.length;
final Uint8List bytes = utf8.encode(expectedStringContents);
final html.File textFile = html.File([bytes], "hello.txt");
final html.File textFile = html.File([bytes], 'hello.txt');

void main() {
// Under test...
Expand All @@ -34,19 +35,55 @@ void main() {
final plugin = ImagePickerPlugin(overrides: overrides);

// Init the pick file dialog...
final file = plugin.pickFile();
final file = plugin.pickFileFromBrowser();

// Mock the browser behavior of selecting a file...
mockInput.dispatchEvent(html.Event('change'));

// Now the file should be available
expect(file, completes);
// And readable
expect((await file).readAsBytes(), completion(isNotEmpty));
final pickedFile = await file;
expect(pickedFile.readAsBytes(), completion(isNotEmpty));
expect(pickedFile.length(), completion(equals(expectedSize)));
expect(pickedFile.name, 'hello.txt');
});

// There's no good way of detecting when the user has "aborted" the selection.

test('computeAcceptAttribute', () {
expect(
plugin.computeAcceptAttribute(null),
'',
);
expect(
plugin.computeAcceptAttribute([]),
'',
);
expect(
plugin.computeAcceptAttribute([null, '', ' ']),
'',
reason: 'Should remove null/empty values and end up with an empty string',
);
expect(
plugin.computeAcceptAttribute(['jpg', 'png', 'bmp']),
'.jpg,.png,.bmp',
);
expect(
plugin.computeAcceptAttribute(['.jpg', '.png', '.bmp']),
'.jpg,.png,.bmp',
);
expect(
plugin.computeAcceptAttribute(['.jpg', 'png', '.bmp']),
'.jpg,.png,.bmp',
);
expect(
plugin.computeAcceptAttribute([null, '.jpg', '', 'png', ' ', '.bmp']),
'.jpg,.png,.bmp',
reason: 'Should strip out null/empty values from the list of extensions',
);
});

test('computeCaptureAttribute', () {
expect(
plugin.computeCaptureAttribute(ImageSource.gallery, CameraDevice.front),
Expand All @@ -67,6 +104,12 @@ void main() {
});

group('createInputElement', () {
test('accept: empty string', () {
html.Element input = plugin.createInputElement('', null);

expect(input.attributes, isNot(contains('accept')));
});

test('accept: any, capture: null', () {
html.Element input = plugin.createInputElement('any', null);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 1.2.0

* Add `pickArbitraryFile` method to the platform interface. This allows users to pick any file type, not just images or videos.
* Add `name` and `length()` methods to PickedFile.

## 1.1.0

* Introduce PickedFile type for the new API.
Expand Down
Loading