diff --git a/packages/image_picker/image_picker_for_web/AUTHORS b/packages/image_picker/image_picker_for_web/AUTHORS index 493a0b4ef9c2..d6ad42a677e5 100644 --- a/packages/image_picker/image_picker_for_web/AUTHORS +++ b/packages/image_picker/image_picker_for_web/AUTHORS @@ -64,3 +64,4 @@ Aleksandr Yurkovskiy Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> +Balvinder Singh Gambhir diff --git a/packages/image_picker/image_picker_for_web/CHANGELOG.md b/packages/image_picker/image_picker_for_web/CHANGELOG.md index b0379ad2c07c..f32a5d8e92cd 100644 --- a/packages/image_picker/image_picker_for_web/CHANGELOG.md +++ b/packages/image_picker/image_picker_for_web/CHANGELOG.md @@ -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. diff --git a/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart b/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart index c6d0b3b532ca..c1025a9f07d3 100644 --- a/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart +++ b/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart @@ -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 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(); @@ -30,7 +37,7 @@ void main() { final overrides = ImagePickerPluginTestOverrides() ..createInputElement = ((_, __) => mockInput) - ..getFileFromInput = ((_) => textFile); + ..getMultipleFilesFromInput = ((_) => [textFile]); final plugin = ImagePickerPlugin(overrides: overrides); @@ -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. @@ -94,6 +139,7 @@ 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 { @@ -101,6 +147,27 @@ void main() { 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')); }); }); } diff --git a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart index 08ce801cafbe..b170ee3256ab 100644 --- a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart +++ b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart @@ -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; @@ -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 files = await getFiles( + accept: _kAcceptImageMimeType, + capture: capture, + ); + return files.first; } /// Returns an [XFile] containing the video that was picked. @@ -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 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> 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 getFile({ + Future> 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 @@ -171,24 +199,19 @@ class ImagePickerPlugin extends ImagePickerPlatform { return null; } - html.File? _getFileFromInput(html.FileUploadInputElement input) { + List? _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? _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 and returns the selected file. @@ -196,9 +219,11 @@ class ImagePickerPlugin extends ImagePickerPlatform { final Completer _completer = Completer(); // 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) { @@ -212,13 +237,24 @@ class ImagePickerPlugin extends ImagePickerPlatform { return _completer.future; } - Future _getSelectedXFile(html.FileUploadInputElement input) { - final Completer _completer = Completer(); + /// Monitors an and returns the selected file(s). + Future> _getSelectedXFiles(html.FileUploadInputElement input) { + final Completer> _completer = Completer>(); // 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) { @@ -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); @@ -278,11 +320,10 @@ 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 + Function(html.Element? input); /// Overrides for some of the functionality above. @visibleForTesting @@ -290,6 +331,6 @@ 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; } diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml index d9b9c5e5cb86..b2479285a3ea 100644 --- a/packages/image_picker/image_picker_for_web/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/pubspec.yaml @@ -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"