diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index 4b916949975..d0a667bf018 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.9 + +* Adds `getMedia` and `getMultipleMedia` methods. + ## 0.8.8 * Adds initial support for Windows, macOS, and Linux. diff --git a/packages/image_picker/image_picker/README.md b/packages/image_picker/image_picker/README.md index 2c5aa5c3f1a..33ecc2edee8 100755 --- a/packages/image_picker/image_picker/README.md +++ b/packages/image_picker/image_picker/README.md @@ -59,7 +59,7 @@ When under high memory pressure the Android system may kill the MainActivity of the application using the image_picker. On Android the image_picker makes use of the default `Intent.ACTION_GET_CONTENT` or `MediaStore.ACTION_IMAGE_CAPTURE` intents. This means that while the intent is executing the source application -is moved to the background and becomes eligable for cleanup when the system is +is moved to the background and becomes eligible for cleanup when the system is low on memory. When the intent finishes executing, Android will restart the application. Since the data is never returned to the original call use the `ImagePicker.retrieveLostData()` method to retrieve the lost data. For example: @@ -180,6 +180,10 @@ final XFile? galleryVideo = final XFile? cameraVideo = await picker.pickVideo(source: ImageSource.camera); // Pick multiple images. final List images = await picker.pickMultiImage(); +// Pick singe image or video. +final XFile? media = await picker.pickMedia(); +// Pick multiple images and videos. +final List medias = await picker.pickMultipleMedia(); ``` ## Migrating to 0.8.2+ diff --git a/packages/image_picker/image_picker/example/lib/main.dart b/packages/image_picker/image_picker/example/lib/main.dart index e7c5dae2851..b1431c5c33b 100755 --- a/packages/image_picker/image_picker/example/lib/main.dart +++ b/packages/image_picker/image_picker/example/lib/main.dart @@ -10,6 +10,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:mime/mime.dart'; import 'package:video_player/video_player.dart'; void main() { @@ -38,10 +39,10 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - List? _imageFileList; + List? _mediaFileList; void _setImageFileListFromFile(XFile? value) { - _imageFileList = value == null ? null : [value]; + _mediaFileList = value == null ? null : [value]; } dynamic _pickImageError; @@ -80,8 +81,12 @@ class _MyHomePageState extends State { } } - Future _onImageButtonPressed(ImageSource source, - {required BuildContext context, bool isMultiImage = false}) async { + Future _onImageButtonPressed( + ImageSource source, { + required BuildContext context, + bool isMultiImage = false, + bool isMedia = false, + }) async { if (_controller != null) { await _controller!.setVolume(0.0); } @@ -94,14 +99,42 @@ class _MyHomePageState extends State { await _displayPickImageDialog(context, (double? maxWidth, double? maxHeight, int? quality) async { try { - final List pickedFileList = await _picker.pickMultiImage( + final List pickedFileList = isMedia + ? await _picker.pickMultipleMedia( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ) + : await _picker.pickMultiImage( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); + setState(() { + _mediaFileList = pickedFileList; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } else if (isMedia) { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = []; + final XFile? media = await _picker.pickMedia( maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: quality, ); - setState(() { - _imageFileList = pickedFileList; - }); + if (media != null) { + pickedFileList.add(media); + setState(() { + _mediaFileList = pickedFileList; + }); + } } catch (e) { setState(() { _pickImageError = e; @@ -179,28 +212,34 @@ class _MyHomePageState extends State { if (retrieveError != null) { return retrieveError; } - if (_imageFileList != null) { + if (_mediaFileList != null) { return Semantics( label: 'image_picker_example_picked_images', child: ListView.builder( key: UniqueKey(), itemBuilder: (BuildContext context, int index) { + final String? mime = lookupMimeType(_mediaFileList![index].path); + // Why network for web? // See https://pub.dev/packages/image_picker_for_web#limitations-on-the-web-platform return Semantics( label: 'image_picker_example_picked_image', child: kIsWeb - ? Image.network(_imageFileList![index].path) - : Image.file( - File(_imageFileList![index].path), - errorBuilder: (BuildContext context, Object error, - StackTrace? stackTrace) => - const Center( - child: Text('This image type is not supported')), - ), + ? Image.network(_mediaFileList![index].path) + : (mime == null || mime.startsWith('image/') + ? Image.file( + File(_mediaFileList![index].path), + errorBuilder: (BuildContext context, Object error, + StackTrace? stackTrace) { + return const Center( + child: + Text('This image type is not supported')); + }, + ) + : _buildInlineVideoPlayer(index)), ); }, - itemCount: _imageFileList!.length, + itemCount: _mediaFileList!.length, ), ); } else if (_pickImageError != null) { @@ -216,6 +255,17 @@ class _MyHomePageState extends State { } } + Widget _buildInlineVideoPlayer(int index) { + final VideoPlayerController controller = + VideoPlayerController.file(File(_mediaFileList![index].path)); + const double volume = kIsWeb ? 0.0 : 1.0; + controller.setVolume(volume); + controller.initialize(); + controller.setLooping(true); + controller.play(); + return Center(child: AspectRatioVideo(controller)); + } + Widget _handlePreview() { if (isVideo) { return _previewVideo(); @@ -239,7 +289,7 @@ class _MyHomePageState extends State { if (response.files == null) { _setImageFileListFromFile(response.file); } else { - _imageFileList = response.files; + _mediaFileList = response.files; } }); } @@ -300,6 +350,39 @@ class _MyHomePageState extends State { child: const Icon(Icons.photo), ), ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + isMedia: true, + ); + }, + heroTag: 'multipleMedia', + tooltip: 'Pick Multiple Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMedia: true, + ); + }, + heroTag: 'media', + tooltip: 'Pick Single Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), Padding( padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( diff --git a/packages/image_picker/image_picker/example/lib/readme_excerpts.dart b/packages/image_picker/image_picker/example/lib/readme_excerpts.dart index e32f4fc8415..15c8185ecf6 100644 --- a/packages/image_picker/image_picker/example/lib/readme_excerpts.dart +++ b/packages/image_picker/image_picker/example/lib/readme_excerpts.dart @@ -42,6 +42,10 @@ Future> readmePickExample() async { final XFile? cameraVideo = await picker.pickVideo(source: ImageSource.camera); // Pick multiple images. final List images = await picker.pickMultiImage(); + // Pick singe image or video. + final XFile? media = await picker.pickMedia(); + // Pick multiple images and videos. + final List medias = await picker.pickMultipleMedia(); // #enddocregion Pick // Return everything for the sanity check test. @@ -50,7 +54,9 @@ Future> readmePickExample() async { photo, galleryVideo, cameraVideo, - if (images.isEmpty) null else images.first + if (images.isEmpty) null else images.first, + media, + if (medias.isEmpty) null else medias.first, ]; } diff --git a/packages/image_picker/image_picker/example/pubspec.yaml b/packages/image_picker/image_picker/example/pubspec.yaml index fc28de42011..4fbeb73be3f 100644 --- a/packages/image_picker/image_picker/example/pubspec.yaml +++ b/packages/image_picker/image_picker/example/pubspec.yaml @@ -17,7 +17,8 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - image_picker_platform_interface: ^2.7.0 + image_picker_platform_interface: ^2.8.0 + mime: ^1.0.4 video_player: ^2.1.4 dev_dependencies: diff --git a/packages/image_picker/image_picker/example/test/readme_excerpts_test.dart b/packages/image_picker/image_picker/example/test/readme_excerpts_test.dart index 771d5d419de..512438ce2b5 100644 --- a/packages/image_picker/image_picker/example/test/readme_excerpts_test.dart +++ b/packages/image_picker/image_picker/example/test/readme_excerpts_test.dart @@ -50,6 +50,13 @@ class FakeImagePicker extends ImagePickerPlatform { return [XFile('multiImage')]; } + @override + Future> getMedia({required MediaOptions options}) async { + return options.allowMultiple + ? [XFile('medias'), XFile('medias')] + : [XFile('media')]; + } + @override Future getVideo( {required ImageSource source, diff --git a/packages/image_picker/image_picker/lib/image_picker.dart b/packages/image_picker/image_picker/lib/image_picker.dart index 0eb35b4bce9..a558dbd7d55 100755 --- a/packages/image_picker/image_picker/lib/image_picker.dart +++ b/packages/image_picker/image_picker/lib/image_picker.dart @@ -27,7 +27,7 @@ class ImagePicker { /// Returns a [PickedFile] object wrapping the image that was picked. /// - /// The returned [PickedFile] is intended to be used within a single APP session. Do not save the file path and use it across sessions. + /// The returned [PickedFile] is intended to be used within a single app session. Do not save the file path and use it across sessions. /// /// The `source` argument controls where the image comes from. This can /// be either [ImageSource.camera] or [ImageSource.gallery]. @@ -78,7 +78,7 @@ class ImagePicker { /// Returns a [List] object wrapping the images that were picked. /// - /// The returned [List] is intended to be used within a single APP session. Do not save the file path and use it across sessions. + /// The returned [List] is intended to be used within a single app session. Do not save the file path and use it across sessions. /// /// Where iOS supports HEIC images, Android 8 and below doesn't. Android 9 and above only support HEIC images if used /// in addition to a size modification, of which the usage is explained below. @@ -115,7 +115,7 @@ class ImagePicker { /// Returns a [PickedFile] object wrapping the video that was picked. /// - /// The returned [PickedFile] is intended to be used within a single APP session. Do not save the file path and use it across sessions. + /// The returned [PickedFile] is intended to be used within a single app session. Do not save the file path and use it across sessions. /// /// The [source] argument controls where the video comes from. This can /// be either [ImageSource.camera] or [ImageSource.gallery]. @@ -151,7 +151,7 @@ class ImagePicker { /// Retrieve the lost [PickedFile] when [selectImage] or [selectVideo] 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. - /// Call this method to retrieve the lost data and process the data according to your APP's business logic. + /// Call this method to retrieve the lost data and process the data according to your app's business logic. /// /// Returns a [LostData] object if successfully retrieved the lost data. The [LostData] object can represent either a /// successful image/video selection, or a failure. @@ -168,7 +168,7 @@ class ImagePicker { /// Returns an [XFile] object wrapping the image that was picked. /// - /// The returned [XFile] is intended to be used within a single APP session. Do not save the file path and use it across sessions. + /// The returned [XFile] is intended to be used within a single app session. Do not save the file path and use it across sessions. /// /// The `source` argument controls where the image comes from. This can /// be either [ImageSource.camera] or [ImageSource.gallery]. @@ -217,32 +217,24 @@ class ImagePicker { CameraDevice preferredCameraDevice = CameraDevice.rear, bool requestFullMetadata = true, }) { - if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { - throw ArgumentError.value( - imageQuality, 'imageQuality', 'must be between 0 and 100'); - } - if (maxWidth != null && maxWidth < 0) { - throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative'); - } - if (maxHeight != null && maxHeight < 0) { - throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); - } + final ImagePickerOptions imagePickerOptions = + ImagePickerOptions.createAndValidate( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice, + requestFullMetadata: requestFullMetadata, + ); return platform.getImageFromSource( source: source, - options: ImagePickerOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: imageQuality, - preferredCameraDevice: preferredCameraDevice, - requestFullMetadata: requestFullMetadata, - ), + options: imagePickerOptions, ); } /// Returns a [List] object wrapping the images that were picked. /// - /// The returned [List] is intended to be used within a single APP session. Do not save the file path and use it across sessions. + /// The returned [List] is intended to be used within a single app session. Do not save the file path and use it across sessions. /// /// Where iOS supports HEIC images, Android 8 and below doesn't. Android 9 and above only support HEIC images if used /// in addition to a size modification, of which the usage is explained below. @@ -277,22 +269,121 @@ class ImagePicker { int? imageQuality, bool requestFullMetadata = true, }) { - if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { - throw ArgumentError.value( - imageQuality, 'imageQuality', 'must be between 0 and 100'); - } - if (maxWidth != null && maxWidth < 0) { - throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative'); - } - if (maxHeight != null && maxHeight < 0) { - throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); - } + final ImageOptions imageOptions = ImageOptions.createAndValidate( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + requestFullMetadata: requestFullMetadata, + ); return platform.getMultiImageWithOptions( options: MultiImagePickerOptions( - imageOptions: ImageOptions( + imageOptions: imageOptions, + ), + ); + } + + /// Returns an [XFile] of the image or video that was picked. + /// The image or videos can only come from the gallery. + /// + /// The returned [XFile] is intended to be used within a single app session. + /// Do not save the file path and use it across sessions. + /// + /// Where iOS supports HEIC images, Android 8 and below doesn't. Android 9 and + /// above only support HEIC images if used in addition to a size modification, + /// of which the usage is explained below. + /// + /// This method is not supported in iOS versions lower than 14. + /// + /// If specified, the image will be at most `maxWidth` wide and + /// `maxHeight` tall. Otherwise the image will be returned at it's + /// original width and height. + /// + /// The `imageQuality` argument modifies the quality of the image, ranging from 0-100 + /// where 100 is the original/max quality. If `imageQuality` is null, the image with + /// the original quality will be returned. Compression is only supported for certain + /// image types such as JPEG and on Android PNG and WebP, too. If compression is not + /// supported for the image that is picked, a warning message will be logged. + /// + /// Use `requestFullMetadata` (defaults to `true`) to control how much additional + /// information the plugin tries to get. + /// If `requestFullMetadata` is set to `true`, the plugin tries to get the full + /// image metadata which may require extra permission requests on some platforms, + /// such as `Photo Library Usage` permission on iOS. + /// + /// The method could throw [PlatformException] if the app does not have permission to access + /// the photos gallery, plugin is already in use, temporary file could not be + /// created (iOS only), plugin activity could not be allocated (Android only) + /// or due to an unknown error. + /// + /// If no image or video was picked, the return value is null. + Future pickMedia({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + bool requestFullMetadata = true, + }) async { + final List listMedia = await platform.getMedia( + options: MediaOptions( + imageOptions: ImageOptions.createAndValidate( + maxHeight: maxHeight, maxWidth: maxWidth, + imageQuality: imageQuality, + requestFullMetadata: requestFullMetadata, + ), + allowMultiple: false, + ), + ); + + return listMedia.isNotEmpty ? listMedia.first : null; + } + + /// Returns a [List] with the images and/or videos that were picked. + /// The images and videos come from the gallery. + /// + /// The returned [List] is intended to be used within a single app session. + /// Do not save the file paths and use them across sessions. + /// + /// Where iOS supports HEIC images, Android 8 and below doesn't. Android 9 and + /// above only support HEIC images if used in addition to a size modification, + /// of which the usage is explained below. + /// + /// This method is not supported in iOS versions lower than 14. + /// + /// If specified, the images will be at most `maxWidth` wide and + /// `maxHeight` tall. Otherwise the images will be returned at their + /// original width and height. + /// + /// The `imageQuality` argument modifies the quality of the images, ranging from 0-100 + /// where 100 is the original/max quality. If `imageQuality` is null, the images with + /// the original quality will be returned. Compression is only supported for certain + /// image types such as JPEG and on Android PNG and WebP, too. If compression is not + /// supported for the image that is picked, a warning message will be logged. + /// + /// Use `requestFullMetadata` (defaults to `true`) to control how much additional + /// information the plugin tries to get. + /// If `requestFullMetadata` is set to `true`, the plugin tries to get the full + /// image metadata which may require extra permission requests on some platforms, + /// such as `Photo Library Usage` permission on iOS. + /// + /// The method could throw [PlatformException] if the app does not have permission to access + /// the photos gallery, plugin is already in use, temporary file could not be + /// created (iOS only), plugin activity could not be allocated (Android only) + /// or due to an unknown error. + /// + /// If no images or videos were picked, the return value is an empty list. + Future> pickMultipleMedia({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + bool requestFullMetadata = true, + }) { + return platform.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( maxHeight: maxHeight, + maxWidth: maxWidth, imageQuality: imageQuality, requestFullMetadata: requestFullMetadata, ), @@ -302,7 +393,7 @@ class ImagePicker { /// Returns an [XFile] object wrapping the video that was picked. /// - /// The returned [XFile] is intended to be used within a single APP session. Do not save the file path and use it across sessions. + /// The returned [XFile] is intended to be used within a single app session. Do not save the file path and use it across sessions. /// /// The [source] argument controls where the video comes from. This can /// be either [ImageSource.camera] or [ImageSource.gallery]. @@ -338,7 +429,7 @@ class ImagePicker { /// 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. - /// Call this method to retrieve the lost data and process the data according to your APP's business logic. + /// Call this method to retrieve the lost data and process the data according to your app's business logic. /// /// Returns a [LostDataResponse] object if successfully retrieved the lost data. The [LostDataResponse] object can \ /// represent either a successful image/video selection, or a failure. diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index 8b38ba56cae..69e255c65ba 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera. repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.8 +version: 0.8.9 environment: sdk: ">=2.18.0 <4.0.0" @@ -33,7 +33,7 @@ dependencies: image_picker_ios: ^0.8.6+1 image_picker_linux: ^0.2.0 image_picker_macos: ^0.2.0 - image_picker_platform_interface: ^2.7.0 + image_picker_platform_interface: ^2.8.0 image_picker_windows: ^0.2.0 dev_dependencies: diff --git a/packages/image_picker/image_picker/test/image_picker_test.dart b/packages/image_picker/image_picker/test/image_picker_test.dart index 459a383b5d9..4ff5b4e025d 100644 --- a/packages/image_picker/image_picker/test/image_picker_test.dart +++ b/packages/image_picker/image_picker/test/image_picker_test.dart @@ -588,14 +588,369 @@ void main() { }); }); - test('supportsImageSource calls through to platform', () async { - final ImagePicker picker = ImagePicker(); - when(mockPlatform.supportsImageSource(any)).thenReturn(true); + group('#Media', () { + setUp(() { + when( + mockPlatform.getMedia( + options: anyNamed('options'), + ), + ).thenAnswer((Invocation _) async => []); + }); + + group('#pickMedia', () { + test('passes the width and height arguments correctly', () async { + final ImagePicker picker = ImagePicker(); + await picker.pickMedia(); + await picker.pickMedia( + maxWidth: 10.0, + ); + await picker.pickMedia( + maxHeight: 10.0, + ); + await picker.pickMedia( + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.pickMedia( + maxWidth: 10.0, + imageQuality: 70, + ); + await picker.pickMedia( + maxHeight: 10.0, + imageQuality: 70, + ); + await picker.pickMedia( + maxWidth: 10.0, maxHeight: 20.0, imageQuality: 70); + + verifyInOrder([ + mockPlatform.getMedia( + options: argThat( + isInstanceOf(), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf().having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf().having( + (MediaOptions options) => options.imageOptions.maxHeight, + 'maxHeight', + equals(10.0)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf() + .having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)) + .having( + (MediaOptions options) => + options.imageOptions.maxHeight, + 'maxHeight', + equals(20.0)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf() + .having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)) + .having( + (MediaOptions options) => + options.imageOptions.imageQuality, + 'imageQuality', + equals(70)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf() + .having( + (MediaOptions options) => + options.imageOptions.maxHeight, + 'maxHeight', + equals(10.0)) + .having( + (MediaOptions options) => + options.imageOptions.imageQuality, + 'imageQuality', + equals(70)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf() + .having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)) + .having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxHeight', + equals(10.0)) + .having( + (MediaOptions options) => + options.imageOptions.imageQuality, + 'imageQuality', + equals(70)), + named: 'options', + ), + ), + ]); + }); + + test('does not accept a negative width or height argument', () { + final ImagePicker picker = ImagePicker(); + expect( + () => picker.pickMedia(maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + () => picker.pickMedia(maxHeight: -1.0), + throwsArgumentError, + ); + }); - final bool supported = picker.supportsImageSource(ImageSource.camera); + test('handles an empty image file response gracefully', () async { + final ImagePicker picker = ImagePicker(); - expect(supported, true); - verify(mockPlatform.supportsImageSource(ImageSource.camera)); + expect(await picker.pickMedia(), isNull); + expect(await picker.pickMedia(), isNull); + }); + + test('full metadata argument defaults to true', () async { + final ImagePicker picker = ImagePicker(); + await picker.pickMedia(); + + verify(mockPlatform.getMedia( + options: argThat( + isInstanceOf().having( + (MediaOptions options) => + options.imageOptions.requestFullMetadata, + 'requestFullMetadata', + isTrue), + named: 'options', + ), + )); + }); + + test('passes the full metadata argument correctly', () async { + final ImagePicker picker = ImagePicker(); + await picker.pickMedia( + requestFullMetadata: false, + ); + + verify(mockPlatform.getMedia( + options: argThat( + isInstanceOf().having( + (MediaOptions options) => + options.imageOptions.requestFullMetadata, + 'requestFullMetadata', + isFalse), + named: 'options', + ), + )); + }); + }); + + group('#pickMultipleMedia', () { + test('passes the width and height arguments correctly', () async { + final ImagePicker picker = ImagePicker(); + await picker.pickMultipleMedia(); + await picker.pickMultipleMedia( + maxWidth: 10.0, + ); + await picker.pickMultipleMedia( + maxHeight: 10.0, + ); + await picker.pickMultipleMedia( + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.pickMultipleMedia( + maxWidth: 10.0, + imageQuality: 70, + ); + await picker.pickMultipleMedia( + maxHeight: 10.0, + imageQuality: 70, + ); + await picker.pickMultipleMedia( + maxWidth: 10.0, maxHeight: 20.0, imageQuality: 70); + + verifyInOrder([ + mockPlatform.getMedia( + options: argThat( + isInstanceOf(), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf().having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf().having( + (MediaOptions options) => options.imageOptions.maxHeight, + 'maxHeight', + equals(10.0)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf() + .having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)) + .having( + (MediaOptions options) => + options.imageOptions.maxHeight, + 'maxHeight', + equals(20.0)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf() + .having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)) + .having( + (MediaOptions options) => + options.imageOptions.imageQuality, + 'imageQuality', + equals(70)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf() + .having( + (MediaOptions options) => + options.imageOptions.maxHeight, + 'maxHeight', + equals(10.0)) + .having( + (MediaOptions options) => + options.imageOptions.imageQuality, + 'imageQuality', + equals(70)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf() + .having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)) + .having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxHeight', + equals(10.0)) + .having( + (MediaOptions options) => + options.imageOptions.imageQuality, + 'imageQuality', + equals(70)), + named: 'options', + ), + ), + ]); + }); + + test('does not accept a negative width or height argument', () { + final ImagePicker picker = ImagePicker(); + expect( + () => picker.pickMultipleMedia(maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + () => picker.pickMultipleMedia(maxHeight: -1.0), + throwsArgumentError, + ); + }); + + test('handles an empty image file response gracefully', () async { + final ImagePicker picker = ImagePicker(); + + expect(await picker.pickMultipleMedia(), isEmpty); + expect(await picker.pickMultipleMedia(), isEmpty); + }); + + test('full metadata argument defaults to true', () async { + final ImagePicker picker = ImagePicker(); + await picker.pickMultipleMedia(); + + verify(mockPlatform.getMedia( + options: argThat( + isInstanceOf().having( + (MediaOptions options) => + options.imageOptions.requestFullMetadata, + 'requestFullMetadata', + isTrue), + named: 'options', + ), + )); + }); + + test('passes the full metadata argument correctly', () async { + final ImagePicker picker = ImagePicker(); + await picker.pickMultipleMedia( + requestFullMetadata: false, + ); + + verify(mockPlatform.getMedia( + options: argThat( + isInstanceOf().having( + (MediaOptions options) => + options.imageOptions.requestFullMetadata, + 'requestFullMetadata', + isFalse), + named: 'options', + ), + )); + }); + }); + test('supportsImageSource calls through to platform', () async { + final ImagePicker picker = ImagePicker(); + when(mockPlatform.supportsImageSource(any)).thenReturn(true); + + final bool supported = picker.supportsImageSource(ImageSource.camera); + + expect(supported, true); + verify(mockPlatform.supportsImageSource(ImageSource.camera)); + }); }); }); } diff --git a/packages/image_picker/image_picker/test/image_picker_test.mocks.dart b/packages/image_picker/image_picker/test/image_picker_test.mocks.dart index 7336d7d5027..85c9df08c79 100644 --- a/packages/image_picker/image_picker/test/image_picker_test.mocks.dart +++ b/packages/image_picker/image_picker/test/image_picker_test.mocks.dart @@ -165,6 +165,16 @@ class MockImagePickerPlatform extends _i1.Mock returnValue: _i4.Future?>.value(), ) as _i4.Future?>); @override + _i4.Future> getMedia({required _i2.MediaOptions? options}) => + (super.noSuchMethod( + Invocation.method( + #getMedia, + [], + {#options: options}, + ), + returnValue: _i4.Future>.value(<_i5.XFile>[]), + ) as _i4.Future>); + @override _i4.Future<_i5.XFile?> getVideo({ required _i2.ImageSource? source, _i2.CameraDevice? preferredCameraDevice = _i2.CameraDevice.rear,