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

[image_picker_for_web] Support multiple pick. Store name and other local file properties in XFile. #4166

Merged
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions packages/image_picker/image_picker_for_web/AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,4 @@ Aleksandr Yurkovskiy <[email protected]>
Anton Borries <[email protected]>
Alex Li <[email protected]>
Rahul Raj <[email protected]>
Balvinder Singh Gambhir<[email protected]>
5 changes: 5 additions & 0 deletions packages/image_picker/image_picker_for_web/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# 2.1.1

* Implement `pickMultiImage` and `getMultiImage`
* Add support for getting name,length and lastModified from XFile

# 2.1.0

* Implemented `getImage`, `getVideo` and `getFile` methods that return `XFile` instances.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import 'package:integration_test/integration_test.dart';
final String expectedStringContents = "Hello, world!";
final Uint8List bytes = utf8.encode(expectedStringContents) as Uint8List;
final html.File textFile = html.File([bytes], "hello.txt");
final html.File secondTextFile = html.File([bytes], "secondFile.txt");

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
Expand Down Expand Up @@ -67,6 +68,29 @@ void main() {
expect((await file).readAsBytes(), completion(isNotEmpty));
});

testWidgets('Can select multiple files', (WidgetTester tester) async {
final mockInput = html.FileUploadInputElement();

final overrides = ImagePickerPluginTestOverrides()
..createInputElement = ((_, __) => mockInput)
..getMultipleFilesFromInput = ((_) => [textFile, secondTextFile]);

final plugin = ImagePickerPlugin(overrides: overrides);

// Init the pick file dialog...
final files = plugin.getMultiImage();

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

// Now the file should be available
expect(files, completes);
// And readable

expect((await files)?.first.readAsBytes(), completion(isNotEmpty));
expect((await files)?[1].readAsBytes(), completion(isNotEmpty));
});

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

testWidgets('computeCaptureAttribute', (WidgetTester tester) async {
Expand Down
147 changes: 131 additions & 16 deletions packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ final String _kAcceptVideoMimeType = 'video/3gpp,video/x-m4v,video/mp4,video/*';
/// This class implements the `package:image_picker` functionality for the web.
class ImagePickerPlugin extends ImagePickerPlatform {
final ImagePickerPluginTestOverrides? _overrides;

bool get _hasOverrides => _overrides != null;

late html.Element _target;
Expand Down Expand Up @@ -96,6 +97,17 @@ class ImagePickerPlugin extends ImagePickerPlatform {
return _getSelectedFile(input);
}

/// Injects a file input, and returns a list of PickedFile that the user selected locally.
@override
Future<List<PickedFile>?> pickMultiImage(
{double? maxWidth, double? maxHeight, int? imageQuality}) {
html.FileUploadInputElement input =
createInputElement(_kAcceptImageMimeType, null, multiple: true)
as html.FileUploadInputElement;
_injectAndActivate(input);
return _getSelectedFiles(input);
}

/// Returns an [XFile] with the image that was picked.
///
/// The `source` argument controls where the image comes from. This can
Expand Down Expand Up @@ -142,8 +154,33 @@ class ImagePickerPlugin extends ImagePickerPlatform {
return getFile(accept: _kAcceptVideoMimeType, capture: capture);
}

/// Injects a file input, and returns a list of XFile that the user selected locally.
@override
Future<List<XFile>?> getMultiImage(
{double? maxWidth, double? maxHeight, int? imageQuality}) {
return getFiles(accept: _kAcceptImageMimeType);
}

/// Injects a file input with the specified accept+capture attributes, and
/// returns the PickedFile that the user selected locally.
/// returns a list of XFile that the user selected locally.
///
/// `capture` is only supported in mobile browsers.
/// See https://caniuse.com/#feat=html-media-capture
@visibleForTesting
Future<List<XFile>> getFiles({
String? accept,
String? capture,
}) {
html.FileUploadInputElement input =
createInputElement(accept, capture, multiple: true)
as html.FileUploadInputElement;
_injectAndActivate(input);

return _getSelectedXFiles(input);
}

/// Injects a file input with the specified accept+capture attributes, and
/// returns the XFile that the user selected locally.
///
/// `capture` is only supported in mobile browsers.
/// See https://caniuse.com/#feat=html-media-capture
Expand Down Expand Up @@ -178,27 +215,61 @@ class ImagePickerPlugin extends ImagePickerPlatform {
return input.files?.first;
}

List<html.File>? _getFilesFromInput(html.FileUploadInputElement input) {
if (_hasOverrides) {
return _overrides!.getMultipleFilesFromInput(input);
}
return input.files;
}

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

if (file != null) {
return html.Url.createObjectUrl(file);
}
return null;
/// Handles the OnChange event from a FileUploadInputElement object
/// Returns the list of selected files.
List<html.File>? _handleOnChangeEventForMultipleFiles(html.Event event) {
final html.FileUploadInputElement input =
event.target as html.FileUploadInputElement;
return _getFilesFromInput(input);
}

/// Monitors an <input type="file"> and returns the selected file.
Future<PickedFile> _getSelectedFile(html.FileUploadInputElement input) {
final Completer<PickedFile> _completer = Completer<PickedFile>();
// Observe the input until we can return something
input.onChange.first.then((event) {
final objectUrl = _handleOnChangeEvent(event);
if (!_completer.isCompleted && objectUrl != null) {
_completer.complete(PickedFile(objectUrl));
final pickedFile = _handleOnChangeEvent(event);
if (!_completer.isCompleted && pickedFile != null) {
_completer.complete(PickedFile(html.Url.createObjectUrl(pickedFile)));
}
});
input.onError.first.then((event) {
if (!_completer.isCompleted) {
_completer.completeError(event);
}
});
// Note that we don't bother detaching from these streams, since the
// "input" gets re-created in the DOM every time the user needs to
// pick a file.
return _completer.future;
}

Future<List<PickedFile>> _getSelectedFiles(
html.FileUploadInputElement input) {
final Completer<List<PickedFile>> _completer =
Completer<List<PickedFile>>();
// Observe the input until we can return something
input.onChange.first.then((event) {
final files = _handleOnChangeEventForMultipleFiles(event);
if (!_completer.isCompleted && files != null) {
_completer.complete(files
.map((file) => PickedFile(html.Url.createObjectUrl(file)))
.toList());
}
});
input.onError.first.then((event) {
Expand All @@ -216,9 +287,40 @@ class ImagePickerPlugin extends ImagePickerPlatform {
final Completer<XFile> _completer = Completer<XFile>();
// Observe the input until we can return something
input.onChange.first.then((event) {
final objectUrl = _handleOnChangeEvent(event);
if (!_completer.isCompleted && objectUrl != null) {
_completer.complete(XFile(objectUrl));
final file = _handleOnChangeEvent(event);
if (!_completer.isCompleted && file != null) {
_completer.complete(XFile(html.Url.createObjectUrl(file),
name: file.name,
length: file.size,
mimeType: file.type,
lastModified: file.lastModifiedDate));
}
});
input.onError.first.then((event) {
if (!_completer.isCompleted) {
_completer.completeError(event);
}
});
// Note that we don't bother detaching from these streams, since the
// "input" gets re-created in the DOM every time the user needs to
// pick a file.
return _completer.future;
}

Future<List<XFile>> _getSelectedXFiles(html.FileUploadInputElement input) {
final Completer<List<XFile>> _completer = Completer<List<XFile>>();
// Observe the input until we can return something
input.onChange.first.then((event) {
final files = _handleOnChangeEventForMultipleFiles(event);
if (!_completer.isCompleted && files != null) {
_completer.complete(files
.map((file) => XFile(
html.Url.createObjectUrl(file),
name: file.name,
length: file.size,
lastModified: file.lastModifiedDate,
))
.toList());
}
});
input.onError.first.then((event) {
Expand Down Expand Up @@ -248,12 +350,15 @@ class ImagePickerPlugin extends ImagePickerPlatform {
/// Creates an input element that accepts certain file types, and
/// allows to `capture` from the device's cameras (where supported)
@visibleForTesting
html.Element createInputElement(String? accept, String? capture) {
html.Element createInputElement(String? accept, String? capture,
{bool multiple = false}) {
if (_hasOverrides) {
return _overrides!.createInputElement(accept, capture);
}

html.Element element = html.FileUploadInputElement()..accept = accept;
html.Element element = html.FileUploadInputElement()
..accept = accept
..multiple = multiple;

if (capture != null) {
element.setAttribute('capture', capture);
Expand Down Expand Up @@ -284,6 +389,13 @@ typedef OverrideExtractFilesFromInputFunction = html.File Function(
html.Element? input,
);

/// A function that extracts list of files from the file `input` passed in.
@visibleForTesting
typedef OverrideExtractMultipleFilesFromInputFunction = List<html.File>
Function(
html.Element? input,
);

/// Overrides for some of the functionality above.
@visibleForTesting
class ImagePickerPluginTestOverrides {
Expand All @@ -292,4 +404,7 @@ class ImagePickerPluginTestOverrides {

/// Override the extraction of the selected file from an input element.
late OverrideExtractFilesFromInputFunction getFileFromInput;

/// Override the extraction of the selected files from an input element.
late OverrideExtractMultipleFilesFromInputFunction getMultipleFilesFromInput;
}
2 changes: 1 addition & 1 deletion packages/image_picker/image_picker_for_web/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: image_picker_for_web
description: Web platform implementation of image_picker
repository: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker_for_web
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
version: 2.1.0
version: 2.1.1

environment:
sdk: ">=2.12.0 <3.0.0"
Expand Down