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 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
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]>
6 changes: 6 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,9 @@
# 2.1.1

* Implemented `getMultiImage`.
* Initialized the following `XFile` attributes for picked files:
* `name`, `length`, `mimeType` and `lastModified`.

# 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 @@ -11,9 +11,16 @@ import 'package:image_picker_for_web/image_picker_for_web.dart';
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
import 'package:integration_test/integration_test.dart';

final String expectedStringContents = "Hello, world!";
final String expectedStringContents = 'Hello, world!';
final String otherStringContents = 'Hello again, world!';
final Uint8List bytes = utf8.encode(expectedStringContents) as Uint8List;
final html.File textFile = html.File([bytes], "hello.txt");
final Uint8List otherBytes = utf8.encode(otherStringContents) as Uint8List;
final Map<String, dynamic> options = {
'type': 'text/plain',
'lastModified': DateTime.utc(2017, 12, 13).millisecondsSinceEpoch,
};
final html.File textFile = html.File([bytes], 'hello.txt', options);
final html.File secondTextFile = html.File([otherBytes], 'secondFile.txt');

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
Expand All @@ -30,7 +37,7 @@ void main() {

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

final plugin = ImagePickerPlugin(overrides: overrides);

Expand All @@ -51,20 +58,58 @@ void main() {

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

final plugin = ImagePickerPlugin(overrides: overrides);

// Init the pick file dialog...
final file = plugin.getFile();
final image = plugin.getImage(source: ImageSource.camera);

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

// Now the file should be available
expect(file, completes);
expect(image, completes);

// And readable
expect((await file).readAsBytes(), completion(isNotEmpty));
final XFile file = await image;
expect(file.readAsBytes(), completion(isNotEmpty));
expect(file.name, textFile.name);
expect(file.length(), completion(textFile.size));
expect(file.mimeType, textFile.type);
expect(
file.lastModified(),
completion(
DateTime.fromMillisecondsSinceEpoch(textFile.lastModified!),
));
});

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));

// Peek into the second file...
final XFile secondFile = (await files).elementAt(1);
expect(secondFile.readAsBytes(), completion(isNotEmpty));
expect(secondFile.name, secondTextFile.name);
expect(secondFile.length(), completion(secondTextFile.size));
});

// There's no good way of detecting when the user has "aborted" the selection.
Expand Down Expand Up @@ -94,13 +139,35 @@ void main() {

expect(input.attributes, containsPair('accept', 'any'));
expect(input.attributes, isNot(contains('capture')));
expect(input.attributes, isNot(contains('multiple')));
});

testWidgets('accept: any, capture: something', (WidgetTester tester) async {
html.Element input = plugin.createInputElement('any', 'something');

expect(input.attributes, containsPair('accept', 'any'));
expect(input.attributes, containsPair('capture', 'something'));
expect(input.attributes, isNot(contains('multiple')));
});

testWidgets('accept: any, capture: null, multi: true',
(WidgetTester tester) async {
html.Element input =
plugin.createInputElement('any', null, multiple: true);

expect(input.attributes, containsPair('accept', 'any'));
expect(input.attributes, isNot(contains('capture')));
expect(input.attributes, contains('multiple'));
});

testWidgets('accept: any, capture: something, multi: true',
(WidgetTester tester) async {
html.Element input =
plugin.createInputElement('any', 'something', multiple: true);

expect(input.attributes, containsPair('accept', 'any'));
expect(input.attributes, containsPair('capture', 'something'));
expect(input.attributes, contains('multiple'));
});
});
}
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 @@ -115,9 +116,13 @@ class ImagePickerPlugin extends ImagePickerPlatform {
double? maxHeight,
int? imageQuality,
CameraDevice preferredCameraDevice = CameraDevice.rear,
}) {
}) async {
String? capture = computeCaptureAttribute(source, preferredCameraDevice);
return getFile(accept: _kAcceptImageMimeType, capture: capture);
List<XFile> files = await getFiles(
accept: _kAcceptImageMimeType,
capture: capture,
);
return files.first;
}

/// Returns an [XFile] containing the video that was picked.
Expand All @@ -137,25 +142,48 @@ class ImagePickerPlugin extends ImagePickerPlatform {
required ImageSource source,
CameraDevice preferredCameraDevice = CameraDevice.rear,
Duration? maxDuration,
}) {
}) async {
String? capture = computeCaptureAttribute(source, preferredCameraDevice);
return getFile(accept: _kAcceptVideoMimeType, capture: capture);
List<XFile> files = await getFiles(
accept: _kAcceptVideoMimeType,
capture: capture,
);
return files.first;
}

/// 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, multiple: true);
}

/// 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.
///
/// `multiple` can be passed to allow for multiple selection of files. Defaults
/// to false.
///
/// See https://caniuse.com/#feat=html-media-capture
@visibleForTesting
Future<XFile> getFile({
Future<List<XFile>> getFiles({
String? accept,
String? capture,
bool multiple = false,
}) {
html.FileUploadInputElement input =
createInputElement(accept, capture) as html.FileUploadInputElement;
html.FileUploadInputElement input = createInputElement(
accept,
capture,
multiple: multiple,
) as html.FileUploadInputElement;
_injectAndActivate(input);
return _getSelectedXFile(input);

return _getSelectedXFiles(input);
}

// DOM methods
Expand All @@ -171,34 +199,31 @@ class ImagePickerPlugin extends ImagePickerPlatform {
return null;
}

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

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

if (file != null) {
return html.Url.createObjectUrl(file);
}
return null;
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 files = _handleOnChangeEvent(event);
if (!_completer.isCompleted && files != null) {
_completer.complete(PickedFile(
html.Url.createObjectUrl(files.first),
));
}
});
input.onError.first.then((event) {
Expand All @@ -212,13 +237,24 @@ class ImagePickerPlugin extends ImagePickerPlatform {
return _completer.future;
}

Future<XFile> _getSelectedXFile(html.FileUploadInputElement input) {
final Completer<XFile> _completer = Completer<XFile>();
/// Monitors an <input type="file"> and returns the selected file(s).
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 objectUrl = _handleOnChangeEvent(event);
if (!_completer.isCompleted && objectUrl != null) {
_completer.complete(XFile(objectUrl));
final files = _handleOnChangeEvent(event);
if (!_completer.isCompleted && files != null) {
_completer.complete(files
.map((file) => XFile(
html.Url.createObjectUrl(file),
name: file.name,
length: file.size,
lastModified: DateTime.fromMillisecondsSinceEpoch(
file.lastModified ?? DateTime.now().millisecondsSinceEpoch,
),
mimeType: file.type,
))
.toList());
}
});
input.onError.first.then((event) {
Expand Down Expand Up @@ -248,12 +284,18 @@ 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 All @@ -278,18 +320,17 @@ typedef OverrideCreateInputFunction = html.Element Function(
String? capture,
);

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

/// Overrides for some of the functionality above.
@visibleForTesting
class ImagePickerPluginTestOverrides {
/// Override the creation of the input element.
late OverrideCreateInputFunction createInputElement;

/// 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