diff --git a/packages/image_picker/image_picker_android/CHANGELOG.md b/packages/image_picker/image_picker_android/CHANGELOG.md index be0ed08b451..ac27246ea3a 100644 --- a/packages/image_picker/image_picker_android/CHANGELOG.md +++ b/packages/image_picker/image_picker_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.13 + +* Adds support for `getMultiVideoWithOptions`. + ## 0.8.12+25 * Updates kotlin version to 2.2.0 to enable gradle 8.11 support. diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java index c0fe90327b9..c76d6b63670 100644 --- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java @@ -81,6 +81,7 @@ public class ImagePickerDelegate @VisibleForTesting static final int REQUEST_CAMERA_IMAGE_PERMISSION = 2345; @VisibleForTesting static final int REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY = 2346; @VisibleForTesting static final int REQUEST_CODE_CHOOSE_MEDIA_FROM_GALLERY = 2347; + @VisibleForTesting static final int REQUEST_CODE_CHOOSE_MULTI_VIDEO_FROM_GALLERY = 2348; @VisibleForTesting static final int REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY = 2352; @VisibleForTesting static final int REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA = 2353; @@ -474,6 +475,38 @@ private void launchMultiPickImageFromGalleryIntent(Boolean usePhotoPicker, int l pickMultiImageIntent, REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY); } + public void chooseMultiVideoFromGallery( + @NonNull VideoSelectionOptions options, + boolean usePhotoPicker, + int limit, + @NonNull Messages.Result> result) { + if (!setPendingOptionsAndResult(null, options, result)) { + finishWithAlreadyActiveError(result); + return; + } + + launchMultiPickVideoFromGalleryIntent(usePhotoPicker, limit); + } + + private void launchMultiPickVideoFromGalleryIntent(Boolean usePhotoPicker, int limit) { + Intent pickMultiVideoIntent; + if (usePhotoPicker) { + pickMultiVideoIntent = + new ActivityResultContracts.PickMultipleVisualMedia(limit) + .createIntent( + activity, + new PickVisualMediaRequest.Builder() + .setMediaType(ActivityResultContracts.PickVisualMedia.VideoOnly.INSTANCE) + .build()); + } else { + pickMultiVideoIntent = new Intent(Intent.ACTION_GET_CONTENT); + pickMultiVideoIntent.setType("video/*"); + pickMultiVideoIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + } + activity.startActivityForResult( + pickMultiVideoIntent, REQUEST_CODE_CHOOSE_MULTI_VIDEO_FROM_GALLERY); + } + public void takeImageWithCamera( @NonNull ImageSelectionOptions options, @NonNull Messages.Result> result) { if (!setPendingOptionsAndResult(options, null, result)) { @@ -617,6 +650,9 @@ public boolean onActivityResult( case REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY: handlerRunnable = () -> handleChooseMultiImageResult(resultCode, data); break; + case REQUEST_CODE_CHOOSE_MULTI_VIDEO_FROM_GALLERY: + handlerRunnable = () -> handleChooseMultiVideoResult(resultCode, data); + break; case REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA: handlerRunnable = () -> handleCaptureImageResult(resultCode); break; @@ -696,6 +732,24 @@ private void handleChooseImageResult(int resultCode, Intent data) { finishWithSuccess(null); } + private void handleChooseMultiVideoResult(int resultCode, Intent intent) { + if (resultCode == Activity.RESULT_OK && intent != null) { + ArrayList paths = getPathsFromIntent(intent, false); + // If there's no valid Uri, return an error + if (paths == null) { + finishWithError( + "missing_valid_video_uri", "Cannot find at least one of the selected videos."); + return; + } + + handleMediaResult(paths); + return; + } + + // User cancelled choosing a video. + finishWithSuccess(null); + } + public class MediaPath { public MediaPath(@NonNull String path, @Nullable String mimeType) { this.path = path; diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java index 264921db4f4..c082da62ce1 100644 --- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java @@ -339,7 +339,9 @@ public void pickVideos( setCameraDevice(delegate, source); if (generalOptions.getAllowMultiple()) { - result.error(new RuntimeException("Multi-video selection is not implemented")); + int limit = ImagePickerUtils.getLimitFromOption(generalOptions); + delegate.chooseMultiVideoFromGallery( + options, generalOptions.getUsePhotoPicker(), limit, result); } else { switch (source.getType()) { case GALLERY: diff --git a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java index dc1f9dd502f..94239955fdd 100644 --- a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java +++ b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java @@ -299,6 +299,41 @@ public void pickVideos_whenSourceIsCamera_invokesTakeImageWithCamera_FrontCamera verify(mockImagePickerDelegate).setCameraDevice(eq(ImagePickerDelegate.CameraDevice.FRONT)); } + @Test + public void pickVideos_invokesChooseMultiVideoFromGallery() { + plugin.pickVideos( + SOURCE_GALLERY, + DEFAULT_VIDEO_OPTIONS, + GENERAL_OPTIONS_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, + mockResult); + verify(mockImagePickerDelegate) + .chooseMultiVideoFromGallery(any(), eq(false), eq(Integer.MAX_VALUE), any()); + verifyNoInteractions(mockResult); + } + + @Test + public void pickVideos_usingPhotoPicker_invokesChooseMultiVideoFromGallery() { + plugin.pickVideos( + SOURCE_GALLERY, + DEFAULT_VIDEO_OPTIONS, + GENERAL_OPTIONS_ALLOW_MULTIPLE_USE_PHOTO_PICKER, + mockResult); + verify(mockImagePickerDelegate) + .chooseMultiVideoFromGallery(any(), eq(true), eq(Integer.MAX_VALUE), any()); + verifyNoInteractions(mockResult); + } + + @Test + public void pickVideos_withLimit5_invokesChooseMultiVideoFromGallery() { + plugin.pickVideos( + SOURCE_GALLERY, + DEFAULT_VIDEO_OPTIONS, + GENERAL_OPTIONS_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER_WITH_LIMIT, + mockResult); + verify(mockImagePickerDelegate).chooseMultiVideoFromGallery(any(), eq(false), eq(5), any()); + verifyNoInteractions(mockResult); + } + @Test public void onConstructor_whenContextTypeIsActivity_shouldNotCrash() { new ImagePickerPlugin(mockImagePickerDelegate, mockActivity); diff --git a/packages/image_picker/image_picker_android/example/lib/main.dart b/packages/image_picker/image_picker_android/example/lib/main.dart index c28c7729df9..dea02fa2320 100755 --- a/packages/image_picker/image_picker_android/example/lib/main.dart +++ b/packages/image_picker/image_picker_android/example/lib/main.dart @@ -79,12 +79,10 @@ class _MyHomePageState extends State { Future _playVideo(XFile? file) async { if (file != null && mounted) { await _disposeVideoController(); - late VideoPlayerController controller; - - controller = VideoPlayerController.file(File(file.path)); + final VideoPlayerController controller = + VideoPlayerController.file(File(file.path)); _controller = controller; - const double volume = 1.0; - await controller.setVolume(volume); + await controller.setVolume(1.0); await controller.initialize(); await controller.setLooping(true); await controller.play(); @@ -95,7 +93,7 @@ class _MyHomePageState extends State { Future _onImageButtonPressed( ImageSource source, { required BuildContext context, - bool isMultiImage = false, + bool allowMultiple = false, bool isMedia = false, }) async { if (_controller != null) { @@ -103,13 +101,20 @@ class _MyHomePageState extends State { } if (context.mounted) { if (_isVideo) { - final XFile? file = await _picker.getVideo( - source: source, maxDuration: const Duration(seconds: 10)); - if (file != null && context.mounted) { - _showPickedSnackBar(context, [file]); + final List files; + if (allowMultiple) { + files = await _picker.getMultiVideoWithOptions(); + } else { + final XFile? file = await _picker.getVideo( + source: source, maxDuration: const Duration(seconds: 10)); + files = [if (file != null) file]; + } + if (files.isNotEmpty && context.mounted) { + _showPickedSnackBar(context, files); + // Just play the first file, to keep the example simple. + await _playVideo(files.first); } - await _playVideo(file); - } else if (isMultiImage) { + } else if (allowMultiple) { await _displayPickImageDialog(context, true, (double? maxWidth, double? maxHeight, int? quality, int? limit) async { try { @@ -121,7 +126,7 @@ class _MyHomePageState extends State { final List pickedFileList = isMedia ? await _picker.getMedia( options: MediaOptions( - allowMultiple: isMultiImage, + allowMultiple: allowMultiple, imageOptions: imageOptions, limit: limit, ), @@ -151,7 +156,7 @@ class _MyHomePageState extends State { final List pickedFileList = []; final XFile? media = _firstOrNull(await _picker.getMedia( options: MediaOptions( - allowMultiple: isMultiImage, + allowMultiple: allowMultiple, imageOptions: ImageOptions( maxWidth: maxWidth, maxHeight: maxHeight, @@ -290,8 +295,7 @@ class _MyHomePageState extends State { Widget _buildInlineVideoPlayer(int index) { final VideoPlayerController controller = VideoPlayerController.file(File(_mediaFileList![index].path)); - const double volume = 1.0; - controller.setVolume(volume); + controller.setVolume(1.0); controller.initialize(); controller.setLooping(true); controller.play(); @@ -314,7 +318,7 @@ class _MyHomePageState extends State { if (response.file != null) { if (response.type == RetrieveType.video) { _isVideo = true; - await _playVideo(response.file); + await _playVideo(response.files?.firstOrNull); } else { _isVideo = false; setState(() { @@ -380,8 +384,8 @@ class _MyHomePageState extends State { _onImageButtonPressed(ImageSource.gallery, context: context); }, heroTag: 'image0', - tooltip: 'Pick Image from gallery', - label: const Text('Pick Image from gallery'), + tooltip: 'Pick image from gallery', + label: const Text('Pick image from gallery'), icon: const Icon(Icons.photo), ), ), @@ -393,13 +397,12 @@ class _MyHomePageState extends State { _onImageButtonPressed( ImageSource.gallery, context: context, - isMultiImage: true, - isMedia: true, + allowMultiple: true, ); }, - heroTag: 'multipleMedia', - tooltip: 'Pick Multiple Media from gallery', - label: const Text('Pick Multiple Media from gallery'), + heroTag: 'image1', + tooltip: 'Pick multiple images', + label: const Text('Pick multiple images'), icon: const Icon(Icons.photo_library), ), ), @@ -415,9 +418,9 @@ class _MyHomePageState extends State { ); }, heroTag: 'media', - tooltip: 'Pick Single Media from gallery', - label: const Text('Pick Single Media from gallery'), - icon: const Icon(Icons.photo_library), + tooltip: 'Pick item from gallery', + label: const Text('Pick item from gallery'), + icon: const Icon(Icons.photo_outlined), ), ), Padding( @@ -428,13 +431,14 @@ class _MyHomePageState extends State { _onImageButtonPressed( ImageSource.gallery, context: context, - isMultiImage: true, + allowMultiple: true, + isMedia: true, ); }, - heroTag: 'image1', - tooltip: 'Pick Multiple Image from gallery', - label: const Text('Pick Multiple Image from gallery'), - icon: const Icon(Icons.photo_library), + heroTag: 'multipleMedia', + tooltip: 'Pick multiple items', + label: const Text('Pick multiple items'), + icon: const Icon(Icons.photo_library_outlined), ), ), Padding( @@ -445,8 +449,8 @@ class _MyHomePageState extends State { _onImageButtonPressed(ImageSource.camera, context: context); }, heroTag: 'image2', - tooltip: 'Take a Photo', - label: const Text('Take a Photo'), + tooltip: 'Take a photo', + label: const Text('Take a photo'), icon: const Icon(Icons.camera_alt), ), ), @@ -458,9 +462,24 @@ class _MyHomePageState extends State { _isVideo = true; _onImageButtonPressed(ImageSource.gallery, context: context); }, - heroTag: 'video0', - tooltip: 'Pick Video from gallery', - label: const Text('Pick Video from gallery'), + heroTag: 'video', + tooltip: 'Pick video from gallery', + label: const Text('Pick video from gallery'), + icon: const Icon(Icons.video_file), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton.extended( + backgroundColor: Colors.red, + onPressed: () { + _isVideo = true; + _onImageButtonPressed(ImageSource.gallery, + context: context, allowMultiple: true); + }, + heroTag: 'multiVideo', + tooltip: 'Pick multiple videos', + label: const Text('Pick multiple videos'), icon: const Icon(Icons.video_library), ), ), @@ -472,9 +491,9 @@ class _MyHomePageState extends State { _isVideo = true; _onImageButtonPressed(ImageSource.camera, context: context); }, - heroTag: 'video1', - tooltip: 'Take a Video', - label: const Text('Take a Video'), + heroTag: 'takeVideo', + tooltip: 'Take a video', + label: const Text('Take a video'), icon: const Icon(Icons.videocam), ), ), diff --git a/packages/image_picker/image_picker_android/example/pubspec.yaml b/packages/image_picker/image_picker_android/example/pubspec.yaml index dbaed90f770..9df61214efc 100644 --- a/packages/image_picker/image_picker_android/example/pubspec.yaml +++ b/packages/image_picker/image_picker_android/example/pubspec.yaml @@ -19,7 +19,7 @@ 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.10.0 + image_picker_platform_interface: ^2.11.0 mime: ^2.0.0 video_player: ^2.1.4 diff --git a/packages/image_picker/image_picker_android/lib/image_picker_android.dart b/packages/image_picker/image_picker_android/lib/image_picker_android.dart index 5fa164b2d0f..1e19561e7d5 100644 --- a/packages/image_picker/image_picker_android/lib/image_picker_android.dart +++ b/packages/image_picker/image_picker_android/lib/image_picker_android.dart @@ -261,6 +261,27 @@ class ImagePickerAndroid extends ImagePickerPlatform { return path != null ? XFile(path) : null; } + @override + Future> getMultiVideoWithOptions({ + MultiVideoPickerOptions options = const MultiVideoPickerOptions(), + }) async { + final List paths = await _hostApi.pickVideos( + SourceSpecification(type: SourceType.gallery), + VideoSelectionOptions(maxDurationSeconds: options.maxDuration?.inSeconds), + GeneralOptions( + allowMultiple: true, + usePhotoPicker: useAndroidPhotoPicker, + limit: options.limit, + ), + ); + + if (paths.isEmpty) { + return []; + } + + return paths.map((String path) => XFile(path)).toList(); + } + MediaSelectionOptions _mediaOptionsToMediaSelectionOptions( MediaOptions mediaOptions) { final ImageSelectionOptions imageSelectionOptions = diff --git a/packages/image_picker/image_picker_android/pubspec.yaml b/packages/image_picker/image_picker_android/pubspec.yaml index 7af8063fdf2..b6cade1ae3b 100755 --- a/packages/image_picker/image_picker_android/pubspec.yaml +++ b/packages/image_picker/image_picker_android/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker_android description: Android implementation of the image_picker plugin. repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.12+25 +version: 0.8.13 environment: sdk: ^3.6.0 @@ -21,7 +21,7 @@ dependencies: flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.1 - image_picker_platform_interface: ^2.10.0 + image_picker_platform_interface: ^2.11.0 dev_dependencies: flutter_test: diff --git a/packages/image_picker/image_picker_android/test/image_picker_android_test.dart b/packages/image_picker/image_picker_android/test/image_picker_android_test.dart index 75dc312226b..b0d84854418 100644 --- a/packages/image_picker/image_picker_android/test/image_picker_android_test.dart +++ b/packages/image_picker/image_picker_android/test/image_picker_android_test.dart @@ -602,6 +602,32 @@ void main() { }); }); + group('#getMultiVideoWithOptions', () { + test('calls the method correctly', () async { + const List fakePaths = ['/foo.mp4', 'bar.mp4']; + api.returnValue = fakePaths; + final List result = await picker.getMultiVideoWithOptions(); + + expect(result.length, 2); + expect(result[0].path, fakePaths[0]); + expect(api.lastCall, _LastPickType.video); + expect(api.passedAllowMultiple, true); + }); + + test('passes the arguments correctly', () async { + api.returnValue = []; + await picker.getMultiVideoWithOptions( + options: const MultiVideoPickerOptions( + maxDuration: Duration(seconds: 10), + limit: 5, + )); + + expect(api.passedSource?.type, SourceType.gallery); + expect(api.passedVideoOptions?.maxDurationSeconds, 10); + expect(api.limit, 5); + }); + }); + group('#getLostData', () { test('getLostData get success response', () async { api.returnValue = CacheRetrievalResult( @@ -1005,6 +1031,7 @@ class _FakeImagePickerApi implements ImagePickerApi { passedVideoOptions = options; passedAllowMultiple = generalOptions.allowMultiple; passedPhotoPickerFlag = generalOptions.usePhotoPicker; + limit = generalOptions.limit; return returnValue as List? ?? []; } diff --git a/packages/image_picker/image_picker_for_web/CHANGELOG.md b/packages/image_picker/image_picker_for_web/CHANGELOG.md index 09da6c33550..04fffb8f67b 100644 --- a/packages/image_picker/image_picker_for_web/CHANGELOG.md +++ b/packages/image_picker/image_picker_for_web/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 3.1.0 +* Adds support for `getMultiVideoWithOptions`. * Updates minimum supported SDK version to Flutter 3.27/Dart 3.6. ## 3.0.6 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 a467a189537..3d265b6632d 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 @@ -146,6 +146,39 @@ void main() { expect(secondFile.length(), completion(secondTextFile.size)); }); + testWidgets('getMultiVideoWithOptions can select multiple files', ( + WidgetTester _, + ) async { + final web.HTMLInputElement mockInput = web.HTMLInputElement() + ..type = 'file'; + + final ImagePickerPluginTestOverrides overrides = + ImagePickerPluginTestOverrides() + ..createInputElement = ((_, __) => mockInput) + ..getMultipleFilesFromInput = + ((_) => [textFile, secondTextFile]); + + final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides); + + // Init the pick file dialog... + final Future> files = plugin.getMultiVideoWithOptions(); + + // Mock the browser behavior of selecting a file... + mockInput.dispatchEvent(web.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)); + }); + group('cancel event', () { late web.HTMLInputElement mockInput; late ImagePickerPluginTestOverrides overrides; @@ -211,6 +244,16 @@ void main() { expect(file, completes); expect(await file, isNull); }); + + testWidgets('getMultiVideoWithOptions - returns empty list', ( + WidgetTester _, + ) async { + final Future?> files = plugin.getMultiVideoWithOptions(); + mockCancel(); + + expect(files, completes); + expect(await files, isEmpty); + }); }); testWidgets('computeCaptureAttribute', (WidgetTester tester) async { diff --git a/packages/image_picker/image_picker_for_web/example/pubspec.yaml b/packages/image_picker/image_picker_for_web/example/pubspec.yaml index eb008ca43c2..ff13bae933c 100644 --- a/packages/image_picker/image_picker_for_web/example/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/example/pubspec.yaml @@ -10,7 +10,7 @@ dependencies: sdk: flutter image_picker_for_web: path: ../ - image_picker_platform_interface: ^2.8.0 + image_picker_platform_interface: ^2.11.0 web: ^1.0.0 dev_dependencies: 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 0241e0750a7..3b515babeb3 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 @@ -114,6 +114,17 @@ class ImagePickerPlugin extends ImagePickerPlatform { return files.isEmpty ? null : files.first; } + @override + Future> getMultiVideoWithOptions({ + MultiVideoPickerOptions options = const MultiVideoPickerOptions(), + }) async { + final List files = await getFiles( + accept: _kAcceptVideoMimeType, + multiple: true, + ); + return files; + } + /// Injects a file input, and returns a list of XFile media that the user selected locally. @override Future> getMedia({ diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml index e2bbb034406..98e86decbb3 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/packages/tree/main/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: 3.0.6 +version: 3.1.0 environment: sdk: ^3.6.0 @@ -21,7 +21,7 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - image_picker_platform_interface: ^2.9.0 + image_picker_platform_interface: ^2.11.0 mime: ">=1.0.4 <3.0.0" web: ">=0.5.1 <2.0.0" diff --git a/packages/image_picker/image_picker_ios/CHANGELOG.md b/packages/image_picker/image_picker_ios/CHANGELOG.md index de2edfd411f..6d446840b53 100644 --- a/packages/image_picker/image_picker_ios/CHANGELOG.md +++ b/packages/image_picker/image_picker_ios/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.8.13 +* Adds support for `getMultiVideoWithOptions`. * Updates minimum supported SDK version to Flutter 3.27/Dart 3.6. ## 0.8.12+2 diff --git a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m index eb6bcd29b84..99bdbb30cf1 100644 --- a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m @@ -310,6 +310,18 @@ - (void)testPickingVideoWithDuration { XCTAssertEqual(controller.videoMaximumDuration, 95); } +- (void)testPickingMultiVideoWithDuration { + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; + + [plugin + pickMultiVideoWithMaxDuration:@(95) + limit:nil + completion:^(NSArray *result, FlutterError *_Nullable error){ + }]; + + XCTAssertEqual(plugin.callContext.maxDuration, 95); +} + - (void)testViewController { UIWindow *window = [UIWindow new]; MockViewController *vc1 = [MockViewController new]; @@ -603,7 +615,7 @@ - (void)testPickMultiImageWithLimit { completion:^(NSArray *_Nullable result, FlutterError *_Nullable error){ }]; - XCTAssertEqual(plugin.callContext.maxImageCount, 2); + XCTAssertEqual(plugin.callContext.maxItemCount, 2); } - (void)testPickMediaWithLimitAllowsMultiple { @@ -620,7 +632,7 @@ - (void)testPickMediaWithLimitAllowsMultiple { FlutterError *_Nullable error){ }]; - XCTAssertEqual(plugin.callContext.maxImageCount, 2); + XCTAssertEqual(plugin.callContext.maxItemCount, 2); } - (void)testPickMediaWithLimitMultipleNotAllowed { @@ -637,7 +649,7 @@ - (void)testPickMediaWithLimitMultipleNotAllowed { FlutterError *_Nullable error){ }]; - XCTAssertEqual(plugin.callContext.maxImageCount, 1); + XCTAssertEqual(plugin.callContext.maxItemCount, 1); } - (void)testPickMultiImageWithoutLimit { @@ -649,7 +661,7 @@ - (void)testPickMultiImageWithoutLimit { completion:^(NSArray *_Nullable result, FlutterError *_Nullable error){ }]; - XCTAssertEqual(plugin.callContext.maxImageCount, 0); + XCTAssertEqual(plugin.callContext.maxItemCount, 0); } - (void)testPickMediaWithoutLimitAllowsMultiple { @@ -666,7 +678,27 @@ - (void)testPickMediaWithoutLimitAllowsMultiple { FlutterError *_Nullable error){ }]; - XCTAssertEqual(plugin.callContext.maxImageCount, 0); + XCTAssertEqual(plugin.callContext.maxItemCount, 0); +} + +- (void)testPickMultiVideoWithLimit { + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; + [plugin pickMultiVideoWithMaxDuration:nil + limit:@(2) + completion:^(NSArray *_Nullable result, + FlutterError *_Nullable error){ + }]; + XCTAssertEqual(plugin.callContext.maxItemCount, 2); +} + +- (void)testPickMultiVideoWithoutLimit { + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; + [plugin pickMultiVideoWithMaxDuration:nil + limit:nil + completion:^(NSArray *_Nullable result, + FlutterError *_Nullable error){ + }]; + XCTAssertEqual(plugin.callContext.maxItemCount, 0); } @end diff --git a/packages/image_picker/image_picker_ios/example/lib/main.dart b/packages/image_picker/image_picker_ios/example/lib/main.dart index e91b87e60d6..99a73e99ffe 100755 --- a/packages/image_picker/image_picker_ios/example/lib/main.dart +++ b/packages/image_picker/image_picker_ios/example/lib/main.dart @@ -7,7 +7,6 @@ import 'dart:async'; import 'dart:io'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; import 'package:mime/mime.dart'; @@ -61,12 +60,10 @@ class _MyHomePageState extends State { Future _playVideo(XFile? file) async { if (file != null && mounted) { await _disposeVideoController(); - late VideoPlayerController controller; - - controller = VideoPlayerController.file(File(file.path)); + final VideoPlayerController controller = + VideoPlayerController.file(File(file.path)); _controller = controller; - const double volume = 1.0; - await controller.setVolume(volume); + await controller.setVolume(1.0); await controller.initialize(); await controller.setLooping(true); await controller.play(); @@ -77,7 +74,7 @@ class _MyHomePageState extends State { Future _onImageButtonPressed( ImageSource source, { required BuildContext context, - bool isMultiImage = false, + bool allowMultiple = false, bool isMedia = false, }) async { if (_controller != null) { @@ -85,35 +82,45 @@ class _MyHomePageState extends State { } if (context.mounted) { if (_isVideo) { - final XFile? file = await _picker.getVideo( - source: source, maxDuration: const Duration(seconds: 10)); - await _playVideo(file); - } else if (isMultiImage) { + final List files; + if (allowMultiple) { + files = await _picker.getMultiVideoWithOptions(); + } else { + final XFile? file = await _picker.getVideo( + source: source, maxDuration: const Duration(seconds: 10)); + files = [if (file != null) file]; + } + if (files.isNotEmpty && context.mounted) { + _showPickedSnackBar(context, files); + // Just play the first file, to keep the example simple. + await _playVideo(files.first); + } + } else if (allowMultiple) { await _displayPickImageDialog(context, true, (double? maxWidth, double? maxHeight, int? quality, int? limit) async { try { + final ImageOptions imageOptions = ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); final List pickedFileList = isMedia ? await _picker.getMedia( options: MediaOptions( - allowMultiple: isMultiImage, - imageOptions: ImageOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ), + allowMultiple: allowMultiple, + imageOptions: imageOptions, limit: limit, ), ) : await _picker.getMultiImageWithOptions( options: MultiImagePickerOptions( - imageOptions: ImageOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ), + imageOptions: imageOptions, limit: limit, ), ); + if (pickedFileList.isNotEmpty && context.mounted) { + _showPickedSnackBar(context, pickedFileList); + } setState(() { _mediaFileList = pickedFileList; }); @@ -130,13 +137,12 @@ class _MyHomePageState extends State { final List pickedFileList = []; final XFile? media = _firstOrNull(await _picker.getMedia( options: MediaOptions( - allowMultiple: isMultiImage, - imageOptions: ImageOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ), - ), + allowMultiple: allowMultiple, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), )); if (media != null) { @@ -161,13 +167,12 @@ class _MyHomePageState extends State { imageQuality: quality, ), ); - setState(() { - _setImageFileListFromFile(pickedFile); - }); + if (pickedFile != null && context.mounted) { + _showPickedSnackBar(context, [pickedFile]); + } + setState(() => _setImageFileListFromFile(pickedFile)); } catch (e) { - setState(() { - _pickImageError = e; - }); + setState(() => _pickImageError = e); } }); } @@ -228,12 +233,13 @@ class _MyHomePageState extends State { child: ListView.builder( key: UniqueKey(), itemBuilder: (BuildContext context, int index) { - final String? mime = lookupMimeType(_mediaFileList![index].path); + final XFile image = _mediaFileList![index]; + final String? mime = lookupMimeType(image.path); return Semantics( label: 'image_picker_example_picked_image', child: mime == null || mime.startsWith('image/') ? Image.file( - File(_mediaFileList![index].path), + File(image.path), errorBuilder: (BuildContext context, Object error, StackTrace? stackTrace) { return const Center( @@ -262,8 +268,7 @@ 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.setVolume(1.0); controller.initialize(); controller.setLooping(true); controller.play(); @@ -290,7 +295,6 @@ class _MyHomePageState extends State { ), floatingActionButton: Column( mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.end, children: [ Semantics( label: 'image_picker_example_from_gallery', @@ -301,8 +305,8 @@ class _MyHomePageState extends State { _onImageButtonPressed(ImageSource.gallery, context: context); }, heroTag: 'image0', - tooltip: 'Pick Image from gallery', - label: const Text('Pick Image from gallery'), + tooltip: 'Pick image from gallery', + label: const Text('Pick image from gallery'), icon: const Icon(Icons.photo), ), ), @@ -314,13 +318,12 @@ class _MyHomePageState extends State { _onImageButtonPressed( ImageSource.gallery, context: context, - isMultiImage: true, - isMedia: true, + allowMultiple: true, ); }, - heroTag: 'multipleMedia', - tooltip: 'Pick Multiple Media from gallery', - label: const Text('Pick Multiple Media from gallery'), + heroTag: 'image1', + tooltip: 'Pick multiple images', + label: const Text('Pick multiple images'), icon: const Icon(Icons.photo_library), ), ), @@ -336,9 +339,9 @@ class _MyHomePageState extends State { ); }, heroTag: 'media', - tooltip: 'Pick Single Media from gallery', - label: const Text('Pick Single Media from gallery'), - icon: const Icon(Icons.photo_library), + tooltip: 'Pick item from gallery', + label: const Text('Pick item from gallery'), + icon: const Icon(Icons.photo_outlined), ), ), Padding( @@ -349,13 +352,14 @@ class _MyHomePageState extends State { _onImageButtonPressed( ImageSource.gallery, context: context, - isMultiImage: true, + allowMultiple: true, + isMedia: true, ); }, - heroTag: 'image1', - tooltip: 'Pick Multiple Image from gallery', - label: const Text('Pick Multiple Image from gallery'), - icon: const Icon(Icons.photo_library), + heroTag: 'multipleMedia', + tooltip: 'Pick multiple items', + label: const Text('Pick multiple items'), + icon: const Icon(Icons.photo_library_outlined), ), ), Padding( @@ -366,8 +370,8 @@ class _MyHomePageState extends State { _onImageButtonPressed(ImageSource.camera, context: context); }, heroTag: 'image2', - tooltip: 'Take a Photo', - label: const Text('Take a Photo'), + tooltip: 'Take a photo', + label: const Text('Take a photo'), icon: const Icon(Icons.camera_alt), ), ), @@ -379,9 +383,24 @@ class _MyHomePageState extends State { _isVideo = true; _onImageButtonPressed(ImageSource.gallery, context: context); }, - heroTag: 'video0', - tooltip: 'Pick Video from gallery', - label: const Text('Pick Video from gallery'), + heroTag: 'video', + tooltip: 'Pick video from gallery', + label: const Text('Pick video from gallery'), + icon: const Icon(Icons.video_file), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton.extended( + backgroundColor: Colors.red, + onPressed: () { + _isVideo = true; + _onImageButtonPressed(ImageSource.gallery, + context: context, allowMultiple: true); + }, + heroTag: 'multiVideo', + tooltip: 'Pick multiple videos', + label: const Text('Pick multiple videos'), icon: const Icon(Icons.video_library), ), ), @@ -393,9 +412,9 @@ class _MyHomePageState extends State { _isVideo = true; _onImageButtonPressed(ImageSource.camera, context: context); }, - heroTag: 'video1', - tooltip: 'Take a Video', - label: const Text('Take a Video'), + heroTag: 'takeVideo', + tooltip: 'Take a video', + label: const Text('Take a video'), icon: const Icon(Icons.videocam), ), ), @@ -480,6 +499,13 @@ class _MyHomePageState extends State { ); }); } + + void _showPickedSnackBar(BuildContext context, List files) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('Picked: ${files.map((XFile it) => it.name).join(',')}'), + duration: const Duration(seconds: 2), + )); + } } typedef OnPickImageCallback = void Function( diff --git a/packages/image_picker/image_picker_ios/example/pubspec.yaml b/packages/image_picker/image_picker_ios/example/pubspec.yaml index ea8242982e0..1d1c431aa68 100755 --- a/packages/image_picker/image_picker_ios/example/pubspec.yaml +++ b/packages/image_picker/image_picker_ios/example/pubspec.yaml @@ -16,7 +16,7 @@ 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.10.0 + image_picker_platform_interface: ^2.11.0 mime: ^2.0.0 video_player: ^2.1.4 diff --git a/packages/image_picker/image_picker_ios/ios/image_picker_ios/Sources/image_picker_ios/FLTImagePickerPlugin.m b/packages/image_picker/image_picker_ios/ios/image_picker_ios/Sources/image_picker_ios/FLTImagePickerPlugin.m index fd88b8c4696..d5ed37d73ad 100644 --- a/packages/image_picker/image_picker_ios/ios/image_picker_ios/Sources/image_picker_ios/FLTImagePickerPlugin.m +++ b/packages/image_picker/image_picker_ios/ios/image_picker_ios/Sources/image_picker_ios/FLTImagePickerPlugin.m @@ -98,14 +98,15 @@ - (void)launchPHPickerWithContext:(nonnull FLTImagePickerMethodCallContext *)con API_AVAILABLE(ios(14)) { PHPickerConfiguration *config = [[PHPickerConfiguration alloc] initWithPhotoLibrary:PHPhotoLibrary.sharedPhotoLibrary]; - config.selectionLimit = context.maxImageCount; + config.selectionLimit = context.maxItemCount; + NSMutableArray *filters = [[NSMutableArray alloc] init]; + if (context.includeImages) { + [filters addObject:[PHPickerFilter imagesFilter]]; + } if (context.includeVideo) { - config.filter = [PHPickerFilter anyFilterMatchingSubfilters:@[ - [PHPickerFilter imagesFilter], [PHPickerFilter videosFilter] - ]]; - } else { - config.filter = [PHPickerFilter imagesFilter]; + [filters addObject:[PHPickerFilter videosFilter]]; } + config.filter = [PHPickerFilter anyFilterMatchingSubfilters:filters]; PHPickerViewController *pickerViewController = [[PHPickerViewController alloc] initWithConfiguration:config]; @@ -121,12 +122,19 @@ - (void)launchUIImagePickerWithSource:(nonnull FLTSourceSpecification *)source UIImagePickerController *imagePickerController = [self createImagePickerController]; imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext; imagePickerController.delegate = self; + NSMutableArray *mediaTypes = [[NSMutableArray alloc] init]; + if (context.includeImages) { + [mediaTypes addObject:(NSString *)kUTTypeImage]; + } if (context.includeVideo) { - imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage, (NSString *)kUTTypeMovie ]; - - } else { - imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage ]; + [mediaTypes addObject:(NSString *)kUTTypeMovie]; + imagePickerController.videoQuality = UIImagePickerControllerQualityTypeHigh; + } + imagePickerController.mediaTypes = mediaTypes; + if (context.maxDuration != 0.0) { + imagePickerController.videoMaximumDuration = context.maxDuration; } + self.callContext = context; switch (source.type) { @@ -167,9 +175,10 @@ - (void)pickImageWithSource:(nonnull FLTSourceSpecification *)source } completion(paths.firstObject, error); }]; + context.includeImages = YES; context.maxSize = maxSize; context.imageQuality = imageQuality; - context.maxImageCount = 1; + context.maxItemCount = 1; context.requestFullMetadata = fullMetadata; if (source.type == FLTSourceTypeGallery) { // Capture is not possible with PHPicker @@ -192,10 +201,11 @@ - (void)pickMultiImageWithMaxSize:(nonnull FLTMaxSize *)maxSize [self cancelInProgressCall]; FLTImagePickerMethodCallContext *context = [[FLTImagePickerMethodCallContext alloc] initWithResult:completion]; + context.includeImages = YES; context.maxSize = maxSize; context.imageQuality = imageQuality; context.requestFullMetadata = fullMetadata; - context.maxImageCount = limit.intValue; + context.maxItemCount = limit.intValue; if (@available(iOS 14, *)) { [self launchPHPickerWithContext:context]; @@ -216,12 +226,13 @@ - (void)pickMediaWithMediaSelectionOptions:(nonnull FLTMediaSelectionOptions *)m context.maxSize = [mediaSelectionOptions maxSize]; context.imageQuality = [mediaSelectionOptions imageQuality]; context.requestFullMetadata = [mediaSelectionOptions requestFullMetadata]; + context.includeImages = YES; context.includeVideo = YES; NSNumber *limit = [mediaSelectionOptions limit]; if (!mediaSelectionOptions.allowMultiple) { - context.maxImageCount = 1; + context.maxItemCount = 1; } else if (limit != nil) { - context.maxImageCount = limit.intValue; + context.maxItemCount = limit.intValue; } if (@available(iOS 14, *)) { @@ -248,37 +259,39 @@ - (void)pickVideoWithSource:(nonnull FLTSourceSpecification *)source } completion(paths.firstObject, error); }]; - context.maxImageCount = 1; + context.includeVideo = YES; + context.maxItemCount = 1; + context.maxDuration = maxDurationSeconds.doubleValue; - UIImagePickerController *imagePickerController = [self createImagePickerController]; - imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext; - imagePickerController.delegate = self; - imagePickerController.mediaTypes = @[ - (NSString *)kUTTypeMovie, (NSString *)kUTTypeAVIMovie, (NSString *)kUTTypeVideo, - (NSString *)kUTTypeMPEG4 - ]; - imagePickerController.videoQuality = UIImagePickerControllerQualityTypeHigh; - - if (maxDurationSeconds) { - NSTimeInterval max = [maxDurationSeconds doubleValue]; - imagePickerController.videoMaximumDuration = max; + if (source.type == FLTSourceTypeGallery) { // Capture is not possible with PHPicker + if (@available(iOS 14, *)) { + [self launchPHPickerWithContext:context]; + } else { + [self launchUIImagePickerWithSource:source context:context]; + } + } else { + [self launchUIImagePickerWithSource:source context:context]; } +} - self.callContext = context; +- (void)pickMultiVideoWithMaxDuration:(nullable NSNumber *)maxDurationSeconds + limit:(nullable NSNumber *)limit + completion:(nonnull void (^)(NSArray *_Nullable, + FlutterError *_Nullable))completion { + [self cancelInProgressCall]; + FLTImagePickerMethodCallContext *context = + [[FLTImagePickerMethodCallContext alloc] initWithResult:completion]; + context.includeVideo = YES; + context.maxItemCount = limit.intValue; + context.maxDuration = maxDurationSeconds.doubleValue; - switch (source.type) { - case FLTSourceTypeCamera: - [self checkCameraAuthorizationWithImagePicker:imagePickerController - camera:[self cameraDeviceForSource:source]]; - break; - case FLTSourceTypeGallery: - [self checkPhotoAuthorizationWithImagePicker:imagePickerController]; - break; - default: - [self sendCallResultWithError:[FlutterError errorWithCode:@"invalid_source" - message:@"Invalid video source." - details:nil]]; - break; + if (@available(iOS 14, *)) { + [self launchPHPickerWithContext:context]; + } else { + // Camera is ignored for gallery mode, so the value here is arbitrary. + [self launchUIImagePickerWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeGallery + camera:FLTSourceCameraRear] + context:context]; } } diff --git a/packages/image_picker/image_picker_ios/ios/image_picker_ios/Sources/image_picker_ios/include/image_picker_ios/FLTImagePickerPlugin_Test.h b/packages/image_picker/image_picker_ios/ios/image_picker_ios/Sources/image_picker_ios/include/image_picker_ios/FLTImagePickerPlugin_Test.h index 845cdcf878a..55d04c6f3f3 100644 --- a/packages/image_picker/image_picker_ios/ios/image_picker_ios/Sources/image_picker_ios/include/image_picker_ios/FLTImagePickerPlugin_Test.h +++ b/packages/image_picker/image_picker_ios/ios/image_picker_ios/Sources/image_picker_ios/include/image_picker_ios/FLTImagePickerPlugin_Test.h @@ -33,13 +33,19 @@ typedef void (^FlutterResultAdapter)(NSArray *_Nullable, FlutterErro /// If nil, no resampling is done. @property(nonatomic, strong, nullable) NSNumber *imageQuality; -/// Maximum number of images to select. 0 indicates no maximum. -@property(nonatomic, assign) int maxImageCount; +/// Maximum number of items to select. 0 indicates no maximum. +@property(nonatomic, assign) int maxItemCount; -/// Whether the image should be picked with full metadata (requires gallery permissions) +/// Whether the image should be picked with full metadata (requires gallery permissions). @property(nonatomic, assign) BOOL requestFullMetadata; -/// Whether the picker should include videos in the list*/ +/// Maximum duration for videos. 0 indicates no maximum. +@property(nonatomic, assign) NSTimeInterval maxDuration; + +/// Whether the picker should include images in the list. +@property(nonatomic, assign) BOOL includeImages; + +/// Whether the picker should include videos in the list. @property(nonatomic, assign) BOOL includeVideo; @end diff --git a/packages/image_picker/image_picker_ios/ios/image_picker_ios/Sources/image_picker_ios/include/image_picker_ios/messages.g.h b/packages/image_picker/image_picker_ios/ios/image_picker_ios/Sources/image_picker_ios/include/image_picker_ios/messages.g.h index ccf84379506..bda30017db9 100644 --- a/packages/image_picker/image_picker_ios/ios/image_picker_ios/Sources/image_picker_ios/include/image_picker_ios/messages.g.h +++ b/packages/image_picker/image_picker_ios/ios/image_picker_ios/Sources/image_picker_ios/include/image_picker_ios/messages.g.h @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.4.1), do not edit directly. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. // See also: https://pub.dev/packages/pigeon #import @@ -86,6 +86,10 @@ NSObject *FLTGetMessagesCodec(void); - (void)pickVideoWithSource:(FLTSourceSpecification *)source maxDuration:(nullable NSNumber *)maxDurationSeconds completion:(void (^)(NSString *_Nullable, FlutterError *_Nullable))completion; +- (void)pickMultiVideoWithMaxDuration:(nullable NSNumber *)maxDurationSeconds + limit:(nullable NSNumber *)limit + completion:(void (^)(NSArray *_Nullable, + FlutterError *_Nullable))completion; /// Selects images and videos and returns their paths. - (void)pickMediaWithMediaSelectionOptions:(FLTMediaSelectionOptions *)mediaSelectionOptions completion:(void (^)(NSArray *_Nullable, diff --git a/packages/image_picker/image_picker_ios/ios/image_picker_ios/Sources/image_picker_ios/messages.g.m b/packages/image_picker/image_picker_ios/ios/image_picker_ios/Sources/image_picker_ios/messages.g.m index f2d8395caff..8f766fb7fef 100644 --- a/packages/image_picker/image_picker_ios/ios/image_picker_ios/Sources/image_picker_ios/messages.g.m +++ b/packages/image_picker/image_picker_ios/ios/image_picker_ios/Sources/image_picker_ios/messages.g.m @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.4.1), do not edit directly. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. // See also: https://pub.dev/packages/pigeon #import "./include/image_picker_ios/messages.g.h" @@ -337,6 +337,34 @@ void SetUpFLTImagePickerApiWithSuffix(id binaryMessenger [channel setMessageHandler:nil]; } } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.image_picker_ios." + @"ImagePickerApi.pickMultiVideo", + messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FLTGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(pickMultiVideoWithMaxDuration:limit:completion:)], + @"FLTImagePickerApi api (%@) doesn't respond to " + @"@selector(pickMultiVideoWithMaxDuration:limit:completion:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_maxDurationSeconds = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_limit = GetNullableObjectAtIndex(args, 1); + [api pickMultiVideoWithMaxDuration:arg_maxDurationSeconds + limit:arg_limit + completion:^(NSArray *_Nullable output, + FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } /// Selects images and videos and returns their paths. { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] diff --git a/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart b/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart index ea188cb4ad6..ee9243fc000 100644 --- a/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart +++ b/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart @@ -322,4 +322,14 @@ class ImagePickerIOS extends ImagePickerPlatform { ); return path != null ? XFile(path) : null; } + + @override + Future> getMultiVideoWithOptions({ + MultiVideoPickerOptions options = const MultiVideoPickerOptions(), + }) async { + return (await _hostApi.pickMultiVideo( + options.maxDuration?.inSeconds, options.limit)) + .map((String path) => XFile(path)) + .toList(); + } } diff --git a/packages/image_picker/image_picker_ios/lib/src/messages.g.dart b/packages/image_picker/image_picker_ios/lib/src/messages.g.dart index 63a245a1a67..14112c77c44 100644 --- a/packages/image_picker/image_picker_ios/lib/src/messages.g.dart +++ b/packages/image_picker/image_picker_ios/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.4.1), do not edit directly. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -277,6 +277,36 @@ class ImagePickerApi { } } + Future> pickMultiVideo( + int? maxDurationSeconds, int? limit) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.image_picker_ios.ImagePickerApi.pickMultiVideo$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = await pigeonVar_channel + .send([maxDurationSeconds, limit]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as List?)!.cast(); + } + } + /// Selects images and videos and returns their paths. Future> pickMedia( MediaSelectionOptions mediaSelectionOptions) async { diff --git a/packages/image_picker/image_picker_ios/pigeons/messages.dart b/packages/image_picker/image_picker_ios/pigeons/messages.dart index c8dd34fa569..45b98c2bcf2 100644 --- a/packages/image_picker/image_picker_ios/pigeons/messages.dart +++ b/packages/image_picker/image_picker_ios/pigeons/messages.dart @@ -63,6 +63,9 @@ abstract class ImagePickerApi { @async @ObjCSelector('pickVideoWithSource:maxDuration:') String? pickVideo(SourceSpecification source, int? maxDurationSeconds); + @async + @ObjCSelector('pickMultiVideoWithMaxDuration:limit:') + List pickMultiVideo(int? maxDurationSeconds, int? limit); /// Selects images and videos and returns their paths. @async diff --git a/packages/image_picker/image_picker_ios/pubspec.yaml b/packages/image_picker/image_picker_ios/pubspec.yaml index 8bae216660d..84b8d372b1f 100755 --- a/packages/image_picker/image_picker_ios/pubspec.yaml +++ b/packages/image_picker/image_picker_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker_ios description: iOS implementation of the image_picker plugin. repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.12+2 +version: 0.8.13 environment: sdk: ^3.6.0 @@ -19,7 +19,7 @@ flutter: dependencies: flutter: sdk: flutter - image_picker_platform_interface: ^2.10.0 + image_picker_platform_interface: ^2.11.0 dev_dependencies: flutter_test: diff --git a/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart b/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart index c84b2554301..c9eb37415ab 100644 --- a/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart +++ b/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart @@ -97,6 +97,16 @@ class _ApiLogger implements TestHostImagePickerApi { })); return returnValue as String?; } + + @override + Future> pickMultiVideo( + int? maxDurationSeconds, int? limit) async { + calls.add(_LoggedMethodCall('pickMultiVideo', arguments: { + 'maxDuration': maxDurationSeconds, + 'limit': limit, + })); + return returnValue as List; + } } void main() { @@ -1292,6 +1302,44 @@ void main() { }); }); + group('#getMultiVideoWithOptions', () { + test('calls the method correctly', () async { + log.returnValue = ['/foo.mp4', 'bar.mp4']; + await picker.getMultiVideoWithOptions(); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMultiVideo', + arguments: { + 'maxDuration': null, + 'limit': null, + }), + ], + ); + }); + + test('passes the arguments correctly', () async { + log.returnValue = []; + await picker.getMultiVideoWithOptions( + options: const MultiVideoPickerOptions( + maxDuration: Duration(seconds: 10), + limit: 5, + )); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMultiVideo', + arguments: { + 'maxDuration': 10, + 'limit': 5, + }), + ], + ); + }); + }); + group('#getImageFromSource', () { test('passes the image source argument correctly', () async { await picker.getImageFromSource(source: ImageSource.camera); diff --git a/packages/image_picker/image_picker_ios/test/test_api.g.dart b/packages/image_picker/image_picker_ios/test/test_api.g.dart index 46dc4e4e85d..caa53c799e2 100644 --- a/packages/image_picker/image_picker_ios/test/test_api.g.dart +++ b/packages/image_picker/image_picker_ios/test/test_api.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.4.1), do not edit directly. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import, no_leading_underscores_for_local_identifiers // ignore_for_file: avoid_relative_lib_imports @@ -75,6 +75,8 @@ abstract class TestHostImagePickerApi { Future pickVideo( SourceSpecification source, int? maxDurationSeconds); + Future> pickMultiVideo(int? maxDurationSeconds, int? limit); + /// Selects images and videos and returns their paths. Future> pickMedia(MediaSelectionOptions mediaSelectionOptions); @@ -199,6 +201,38 @@ abstract class TestHostImagePickerApi { }); } } + { + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.image_picker_ios.ImagePickerApi.pickMultiVideo$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.image_picker_ios.ImagePickerApi.pickMultiVideo was null.'); + final List args = (message as List?)!; + final int? arg_maxDurationSeconds = (args[0] as int?); + final int? arg_limit = (args[1] as int?); + try { + final List output = + await api.pickMultiVideo(arg_maxDurationSeconds, arg_limit); + return [output]; + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } { final BasicMessageChannel< Object?> pigeonVar_channel = BasicMessageChannel< diff --git a/packages/image_picker/image_picker_linux/CHANGELOG.md b/packages/image_picker/image_picker_linux/CHANGELOG.md index 9902a5e91af..95207a3befa 100644 --- a/packages/image_picker/image_picker_linux/CHANGELOG.md +++ b/packages/image_picker/image_picker_linux/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.2.2 +* Adds support for `getMultiVideoWithOptions`. * Updates minimum supported SDK version to Flutter 3.27/Dart 3.6. ## 0.2.1+2 diff --git a/packages/image_picker/image_picker_linux/example/lib/main.dart b/packages/image_picker/image_picker_linux/example/lib/main.dart index acec8f8b360..b62d3913397 100644 --- a/packages/image_picker/image_picker_linux/example/lib/main.dart +++ b/packages/image_picker/image_picker_linux/example/lib/main.dart @@ -74,7 +74,7 @@ class _MyHomePageState extends State { Future _onImageButtonPressed( ImageSource source, { required BuildContext context, - bool isMultiImage = false, + bool allowMultiple = false, bool isMedia = false, }) async { if (_controller != null) { @@ -82,32 +82,43 @@ class _MyHomePageState extends State { } if (context.mounted) { if (_isVideo) { - final XFile? file = await _picker.getVideo( - source: source, maxDuration: const Duration(seconds: 10)); - await _playVideo(file); - } else if (isMultiImage) { + final List files; + if (allowMultiple) { + files = await _picker.getMultiVideoWithOptions(); + } else { + final XFile? file = await _picker.getVideo( + source: source, maxDuration: const Duration(seconds: 10)); + files = [if (file != null) file]; + } + if (files.isNotEmpty && context.mounted) { + _showPickedSnackBar(context, files); + // Just play the first file, to keep the example simple. + await _playVideo(files.first); + } + } else if (allowMultiple) { await _displayPickImageDialog(context, (double? maxWidth, double? maxHeight, int? quality) async { try { + final ImageOptions imageOptions = ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); final List pickedFileList = isMedia ? await _picker.getMedia( options: MediaOptions( - allowMultiple: isMultiImage, - imageOptions: ImageOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - )), + allowMultiple: allowMultiple, + imageOptions: imageOptions, + ), ) : await _picker.getMultiImageWithOptions( options: MultiImagePickerOptions( - imageOptions: ImageOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ), + imageOptions: imageOptions, ), ); + if (pickedFileList.isNotEmpty && context.mounted) { + _showPickedSnackBar(context, pickedFileList); + } setState(() { _mediaFileList = pickedFileList; }); @@ -124,7 +135,7 @@ class _MyHomePageState extends State { final List pickedFileList = []; final XFile? media = _firstOrNull(await _picker.getMedia( options: MediaOptions( - allowMultiple: isMultiImage, + allowMultiple: allowMultiple, imageOptions: ImageOptions( maxWidth: maxWidth, maxHeight: maxHeight, @@ -154,13 +165,12 @@ class _MyHomePageState extends State { imageQuality: quality, ), ); - setState(() { - _setImageFileListFromFile(pickedFile); - }); + if (pickedFile != null && context.mounted) { + _showPickedSnackBar(context, [pickedFile]); + } + setState(() => _setImageFileListFromFile(pickedFile)); } catch (e) { - setState(() { - _pickImageError = e; - }); + setState(() => _pickImageError = e); } }); } @@ -221,19 +231,28 @@ class _MyHomePageState extends State { child: ListView.builder( key: UniqueKey(), itemBuilder: (BuildContext context, int index) { + final XFile image = _mediaFileList![index]; final String? mime = lookupMimeType(_mediaFileList![index].path); - return Semantics( - label: 'image_picker_example_picked_image', - child: 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), + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(image.name, + key: const Key('image_picker_example_picked_image_name')), + Semantics( + label: 'image_picker_example_picked_image', + child: 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: _mediaFileList!.length, @@ -255,8 +274,7 @@ class _MyHomePageState extends State { Widget _buildInlineVideoPlayer(int index) { final VideoPlayerController controller = VideoPlayerController.file(File(_mediaFileList![index].path)); - const double volume = 1.0; - controller.setVolume(volume); + controller.setVolume(1.0); controller.initialize(); controller.setLooping(true); controller.play(); @@ -293,8 +311,8 @@ class _MyHomePageState extends State { _onImageButtonPressed(ImageSource.gallery, context: context); }, heroTag: 'image0', - tooltip: 'Pick Image from gallery', - label: const Text('Pick Image from gallery'), + tooltip: 'Pick image from gallery', + label: const Text('Pick image from gallery'), icon: const Icon(Icons.photo), ), ), @@ -306,13 +324,12 @@ class _MyHomePageState extends State { _onImageButtonPressed( ImageSource.gallery, context: context, - isMultiImage: true, - isMedia: true, + allowMultiple: true, ); }, - heroTag: 'multipleMedia', - tooltip: 'Pick Multiple Media from gallery', - label: const Text('Pick Multiple Media from gallery'), + heroTag: 'image1', + tooltip: 'Pick multiple images', + label: const Text('Pick multiple images'), icon: const Icon(Icons.photo_library), ), ), @@ -328,9 +345,9 @@ class _MyHomePageState extends State { ); }, heroTag: 'media', - tooltip: 'Pick Single Media from gallery', - label: const Text('Pick Single Media from gallery'), - icon: const Icon(Icons.photo_library), + tooltip: 'Pick item from gallery', + label: const Text('Pick item from gallery'), + icon: const Icon(Icons.photo_outlined), ), ), Padding( @@ -341,13 +358,14 @@ class _MyHomePageState extends State { _onImageButtonPressed( ImageSource.gallery, context: context, - isMultiImage: true, + allowMultiple: true, + isMedia: true, ); }, - heroTag: 'image1', - tooltip: 'Pick Multiple Image from gallery', - label: const Text('Pick Multiple Image from gallery'), - icon: const Icon(Icons.photo_library), + heroTag: 'multipleMedia', + tooltip: 'Pick multiple items', + label: const Text('Pick multiple items'), + icon: const Icon(Icons.photo_library_outlined), ), ), if (_picker.supportsImageSource(ImageSource.camera)) @@ -359,8 +377,8 @@ class _MyHomePageState extends State { _onImageButtonPressed(ImageSource.camera, context: context); }, heroTag: 'image2', - tooltip: 'Take a Photo', - label: const Text('Take a Photo'), + tooltip: 'Take a photo', + label: const Text('Take a photo'), icon: const Icon(Icons.camera_alt), ), ), @@ -372,9 +390,24 @@ class _MyHomePageState extends State { _isVideo = true; _onImageButtonPressed(ImageSource.gallery, context: context); }, - heroTag: 'video0', - tooltip: 'Pick Video from gallery', - label: const Text('Pick Video from gallery'), + heroTag: 'video', + tooltip: 'Pick video from gallery', + label: const Text('Pick video from gallery'), + icon: const Icon(Icons.video_file), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton.extended( + backgroundColor: Colors.red, + onPressed: () { + _isVideo = true; + _onImageButtonPressed(ImageSource.gallery, + context: context, allowMultiple: true); + }, + heroTag: 'multiVideo', + tooltip: 'Pick multiple videos', + label: const Text('Pick multiple videos'), icon: const Icon(Icons.video_library), ), ), @@ -387,9 +420,9 @@ class _MyHomePageState extends State { _isVideo = true; _onImageButtonPressed(ImageSource.camera, context: context); }, - heroTag: 'video1', - tooltip: 'Take a Video', - label: const Text('Take a Video'), + heroTag: 'takeVideo', + tooltip: 'Take a video', + label: const Text('Take a video'), icon: const Icon(Icons.videocam), ), ), @@ -464,6 +497,13 @@ class _MyHomePageState extends State { ); }); } + + void _showPickedSnackBar(BuildContext context, List files) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('Picked: ${files.map((XFile it) => it.name).join(',')}'), + duration: const Duration(seconds: 2), + )); + } } typedef OnPickImageCallback = void Function( diff --git a/packages/image_picker/image_picker_linux/example/pubspec.yaml b/packages/image_picker/image_picker_linux/example/pubspec.yaml index 62b9960ec20..b1626f31057 100644 --- a/packages/image_picker/image_picker_linux/example/pubspec.yaml +++ b/packages/image_picker/image_picker_linux/example/pubspec.yaml @@ -17,7 +17,7 @@ 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.8.0 + image_picker_platform_interface: ^2.11.0 mime: ^2.0.0 video_player: ^2.1.4 diff --git a/packages/image_picker/image_picker_linux/lib/image_picker_linux.dart b/packages/image_picker/image_picker_linux/lib/image_picker_linux.dart index 207338989b8..deacebaf4e7 100644 --- a/packages/image_picker/image_picker_linux/lib/image_picker_linux.dart +++ b/packages/image_picker/image_picker_linux/lib/image_picker_linux.dart @@ -155,6 +155,17 @@ class ImagePickerLinux extends CameraDelegatingImagePickerPlatform { return files; } + @override + Future> getMultiVideoWithOptions( + {MultiVideoPickerOptions options = + const MultiVideoPickerOptions()}) async { + const XTypeGroup typeGroup = + XTypeGroup(label: 'Videos', mimeTypes: ['video/*']); + final List files = await fileSelector + .openFiles(acceptedTypeGroups: [typeGroup]); + return files; + } + // `maxWidth`, `maxHeight`, and `imageQuality` arguments are not currently // supported. If any of these arguments are supplied, they will be silently // ignored. diff --git a/packages/image_picker/image_picker_linux/pubspec.yaml b/packages/image_picker/image_picker_linux/pubspec.yaml index 4d3180f7323..ba1d4c4d71f 100644 --- a/packages/image_picker/image_picker_linux/pubspec.yaml +++ b/packages/image_picker/image_picker_linux/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker_linux description: Linux platform implementation of image_picker repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_linux issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.2.1+2 +version: 0.2.2 environment: sdk: ^3.6.0 @@ -20,7 +20,7 @@ dependencies: file_selector_platform_interface: ^2.2.0 flutter: sdk: flutter - image_picker_platform_interface: ^2.8.0 + image_picker_platform_interface: ^2.11.0 dev_dependencies: build_runner: ^2.1.5 diff --git a/packages/image_picker/image_picker_linux/test/image_picker_linux_test.dart b/packages/image_picker/image_picker_linux/test/image_picker_linux_test.dart index b8a92eef168..c9fd87868c6 100644 --- a/packages/image_picker/image_picker_linux/test/image_picker_linux_test.dart +++ b/packages/image_picker/image_picker_linux/test/image_picker_linux_test.dart @@ -124,6 +124,16 @@ void main() { await expectLater( plugin.getVideo(source: ImageSource.camera), throwsStateError); }); + + test('getMultiVideoWithOptions passes the accepted type groups correctly', + () async { + await plugin.getMultiVideoWithOptions(); + + final VerificationResult result = verify( + mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].mimeTypes, ['video/*']); + }); }); group('media', () { diff --git a/packages/image_picker/image_picker_macos/CHANGELOG.md b/packages/image_picker/image_picker_macos/CHANGELOG.md index 2e404db2c53..ab17993adcb 100644 --- a/packages/image_picker/image_picker_macos/CHANGELOG.md +++ b/packages/image_picker/image_picker_macos/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.2.2 +* Adds support for `getMultiVideoWithOptions`. * Updates minimum supported SDK version to Flutter 3.27/Dart 3.6. ## 0.2.1+2 diff --git a/packages/image_picker/image_picker_macos/example/lib/main.dart b/packages/image_picker/image_picker_macos/example/lib/main.dart index acec8f8b360..b62d3913397 100644 --- a/packages/image_picker/image_picker_macos/example/lib/main.dart +++ b/packages/image_picker/image_picker_macos/example/lib/main.dart @@ -74,7 +74,7 @@ class _MyHomePageState extends State { Future _onImageButtonPressed( ImageSource source, { required BuildContext context, - bool isMultiImage = false, + bool allowMultiple = false, bool isMedia = false, }) async { if (_controller != null) { @@ -82,32 +82,43 @@ class _MyHomePageState extends State { } if (context.mounted) { if (_isVideo) { - final XFile? file = await _picker.getVideo( - source: source, maxDuration: const Duration(seconds: 10)); - await _playVideo(file); - } else if (isMultiImage) { + final List files; + if (allowMultiple) { + files = await _picker.getMultiVideoWithOptions(); + } else { + final XFile? file = await _picker.getVideo( + source: source, maxDuration: const Duration(seconds: 10)); + files = [if (file != null) file]; + } + if (files.isNotEmpty && context.mounted) { + _showPickedSnackBar(context, files); + // Just play the first file, to keep the example simple. + await _playVideo(files.first); + } + } else if (allowMultiple) { await _displayPickImageDialog(context, (double? maxWidth, double? maxHeight, int? quality) async { try { + final ImageOptions imageOptions = ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); final List pickedFileList = isMedia ? await _picker.getMedia( options: MediaOptions( - allowMultiple: isMultiImage, - imageOptions: ImageOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - )), + allowMultiple: allowMultiple, + imageOptions: imageOptions, + ), ) : await _picker.getMultiImageWithOptions( options: MultiImagePickerOptions( - imageOptions: ImageOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ), + imageOptions: imageOptions, ), ); + if (pickedFileList.isNotEmpty && context.mounted) { + _showPickedSnackBar(context, pickedFileList); + } setState(() { _mediaFileList = pickedFileList; }); @@ -124,7 +135,7 @@ class _MyHomePageState extends State { final List pickedFileList = []; final XFile? media = _firstOrNull(await _picker.getMedia( options: MediaOptions( - allowMultiple: isMultiImage, + allowMultiple: allowMultiple, imageOptions: ImageOptions( maxWidth: maxWidth, maxHeight: maxHeight, @@ -154,13 +165,12 @@ class _MyHomePageState extends State { imageQuality: quality, ), ); - setState(() { - _setImageFileListFromFile(pickedFile); - }); + if (pickedFile != null && context.mounted) { + _showPickedSnackBar(context, [pickedFile]); + } + setState(() => _setImageFileListFromFile(pickedFile)); } catch (e) { - setState(() { - _pickImageError = e; - }); + setState(() => _pickImageError = e); } }); } @@ -221,19 +231,28 @@ class _MyHomePageState extends State { child: ListView.builder( key: UniqueKey(), itemBuilder: (BuildContext context, int index) { + final XFile image = _mediaFileList![index]; final String? mime = lookupMimeType(_mediaFileList![index].path); - return Semantics( - label: 'image_picker_example_picked_image', - child: 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), + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(image.name, + key: const Key('image_picker_example_picked_image_name')), + Semantics( + label: 'image_picker_example_picked_image', + child: 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: _mediaFileList!.length, @@ -255,8 +274,7 @@ class _MyHomePageState extends State { Widget _buildInlineVideoPlayer(int index) { final VideoPlayerController controller = VideoPlayerController.file(File(_mediaFileList![index].path)); - const double volume = 1.0; - controller.setVolume(volume); + controller.setVolume(1.0); controller.initialize(); controller.setLooping(true); controller.play(); @@ -293,8 +311,8 @@ class _MyHomePageState extends State { _onImageButtonPressed(ImageSource.gallery, context: context); }, heroTag: 'image0', - tooltip: 'Pick Image from gallery', - label: const Text('Pick Image from gallery'), + tooltip: 'Pick image from gallery', + label: const Text('Pick image from gallery'), icon: const Icon(Icons.photo), ), ), @@ -306,13 +324,12 @@ class _MyHomePageState extends State { _onImageButtonPressed( ImageSource.gallery, context: context, - isMultiImage: true, - isMedia: true, + allowMultiple: true, ); }, - heroTag: 'multipleMedia', - tooltip: 'Pick Multiple Media from gallery', - label: const Text('Pick Multiple Media from gallery'), + heroTag: 'image1', + tooltip: 'Pick multiple images', + label: const Text('Pick multiple images'), icon: const Icon(Icons.photo_library), ), ), @@ -328,9 +345,9 @@ class _MyHomePageState extends State { ); }, heroTag: 'media', - tooltip: 'Pick Single Media from gallery', - label: const Text('Pick Single Media from gallery'), - icon: const Icon(Icons.photo_library), + tooltip: 'Pick item from gallery', + label: const Text('Pick item from gallery'), + icon: const Icon(Icons.photo_outlined), ), ), Padding( @@ -341,13 +358,14 @@ class _MyHomePageState extends State { _onImageButtonPressed( ImageSource.gallery, context: context, - isMultiImage: true, + allowMultiple: true, + isMedia: true, ); }, - heroTag: 'image1', - tooltip: 'Pick Multiple Image from gallery', - label: const Text('Pick Multiple Image from gallery'), - icon: const Icon(Icons.photo_library), + heroTag: 'multipleMedia', + tooltip: 'Pick multiple items', + label: const Text('Pick multiple items'), + icon: const Icon(Icons.photo_library_outlined), ), ), if (_picker.supportsImageSource(ImageSource.camera)) @@ -359,8 +377,8 @@ class _MyHomePageState extends State { _onImageButtonPressed(ImageSource.camera, context: context); }, heroTag: 'image2', - tooltip: 'Take a Photo', - label: const Text('Take a Photo'), + tooltip: 'Take a photo', + label: const Text('Take a photo'), icon: const Icon(Icons.camera_alt), ), ), @@ -372,9 +390,24 @@ class _MyHomePageState extends State { _isVideo = true; _onImageButtonPressed(ImageSource.gallery, context: context); }, - heroTag: 'video0', - tooltip: 'Pick Video from gallery', - label: const Text('Pick Video from gallery'), + heroTag: 'video', + tooltip: 'Pick video from gallery', + label: const Text('Pick video from gallery'), + icon: const Icon(Icons.video_file), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton.extended( + backgroundColor: Colors.red, + onPressed: () { + _isVideo = true; + _onImageButtonPressed(ImageSource.gallery, + context: context, allowMultiple: true); + }, + heroTag: 'multiVideo', + tooltip: 'Pick multiple videos', + label: const Text('Pick multiple videos'), icon: const Icon(Icons.video_library), ), ), @@ -387,9 +420,9 @@ class _MyHomePageState extends State { _isVideo = true; _onImageButtonPressed(ImageSource.camera, context: context); }, - heroTag: 'video1', - tooltip: 'Take a Video', - label: const Text('Take a Video'), + heroTag: 'takeVideo', + tooltip: 'Take a video', + label: const Text('Take a video'), icon: const Icon(Icons.videocam), ), ), @@ -464,6 +497,13 @@ class _MyHomePageState extends State { ); }); } + + void _showPickedSnackBar(BuildContext context, List files) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('Picked: ${files.map((XFile it) => it.name).join(',')}'), + duration: const Duration(seconds: 2), + )); + } } typedef OnPickImageCallback = void Function( diff --git a/packages/image_picker/image_picker_macos/example/pubspec.yaml b/packages/image_picker/image_picker_macos/example/pubspec.yaml index 3d5514c1d3f..4a61fe59517 100644 --- a/packages/image_picker/image_picker_macos/example/pubspec.yaml +++ b/packages/image_picker/image_picker_macos/example/pubspec.yaml @@ -17,7 +17,7 @@ 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.8.0 + image_picker_platform_interface: ^2.11.0 mime: ^2.0.0 video_player: ^2.1.4 diff --git a/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart b/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart index 9e9447a5710..9ca69f40a09 100644 --- a/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart +++ b/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart @@ -160,6 +160,20 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform { return files; } + @override + Future> getMultiVideoWithOptions( + {MultiVideoPickerOptions options = + const MultiVideoPickerOptions()}) async { + // TODO(stuartmorgan): Add a native implementation that can use + // PHPickerViewController on macOS 13+, with this as a fallback for + // older OS versions: https://github.com/flutter/flutter/issues/125829. + const XTypeGroup typeGroup = + XTypeGroup(uniformTypeIdentifiers: ['public.movie']); + final List files = await fileSelector + .openFiles(acceptedTypeGroups: [typeGroup]); + return files; + } + // `maxWidth`, `maxHeight`, and `imageQuality` arguments are not currently // supported. If any of these arguments are supplied, they will be silently // ignored. diff --git a/packages/image_picker/image_picker_macos/pubspec.yaml b/packages/image_picker/image_picker_macos/pubspec.yaml index b9d7cdb1605..a9322ac481b 100644 --- a/packages/image_picker/image_picker_macos/pubspec.yaml +++ b/packages/image_picker/image_picker_macos/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker_macos description: macOS platform implementation of image_picker repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_macos issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.2.1+2 +version: 0.2.2 environment: sdk: ^3.6.0 @@ -20,7 +20,7 @@ dependencies: file_selector_platform_interface: ^2.3.0 flutter: sdk: flutter - image_picker_platform_interface: ^2.8.0 + image_picker_platform_interface: ^2.11.0 dev_dependencies: build_runner: ^2.1.5 diff --git a/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart index 7e94161d4a4..ee8b473017a 100644 --- a/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart +++ b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart @@ -130,6 +130,17 @@ void main() { await expectLater( plugin.getVideo(source: ImageSource.camera), throwsStateError); }); + + test('getMultiVideoWithOptions passes the accepted type groups correctly', + () async { + await plugin.getMultiVideoWithOptions(); + + final VerificationResult result = verify( + mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].uniformTypeIdentifiers, + ['public.movie']); + }); }); group('media', () { diff --git a/packages/image_picker/image_picker_windows/CHANGELOG.md b/packages/image_picker/image_picker_windows/CHANGELOG.md index e11526a4462..80b3c931b49 100644 --- a/packages/image_picker/image_picker_windows/CHANGELOG.md +++ b/packages/image_picker/image_picker_windows/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.2.2 +* Adds support for `getMultiVideoWithOptions`. * Updates minimum supported SDK version to Flutter 3.27/Dart 3.6. ## 0.2.1+1 diff --git a/packages/image_picker/image_picker_windows/example/lib/main.dart b/packages/image_picker/image_picker_windows/example/lib/main.dart index acec8f8b360..b62d3913397 100644 --- a/packages/image_picker/image_picker_windows/example/lib/main.dart +++ b/packages/image_picker/image_picker_windows/example/lib/main.dart @@ -74,7 +74,7 @@ class _MyHomePageState extends State { Future _onImageButtonPressed( ImageSource source, { required BuildContext context, - bool isMultiImage = false, + bool allowMultiple = false, bool isMedia = false, }) async { if (_controller != null) { @@ -82,32 +82,43 @@ class _MyHomePageState extends State { } if (context.mounted) { if (_isVideo) { - final XFile? file = await _picker.getVideo( - source: source, maxDuration: const Duration(seconds: 10)); - await _playVideo(file); - } else if (isMultiImage) { + final List files; + if (allowMultiple) { + files = await _picker.getMultiVideoWithOptions(); + } else { + final XFile? file = await _picker.getVideo( + source: source, maxDuration: const Duration(seconds: 10)); + files = [if (file != null) file]; + } + if (files.isNotEmpty && context.mounted) { + _showPickedSnackBar(context, files); + // Just play the first file, to keep the example simple. + await _playVideo(files.first); + } + } else if (allowMultiple) { await _displayPickImageDialog(context, (double? maxWidth, double? maxHeight, int? quality) async { try { + final ImageOptions imageOptions = ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); final List pickedFileList = isMedia ? await _picker.getMedia( options: MediaOptions( - allowMultiple: isMultiImage, - imageOptions: ImageOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - )), + allowMultiple: allowMultiple, + imageOptions: imageOptions, + ), ) : await _picker.getMultiImageWithOptions( options: MultiImagePickerOptions( - imageOptions: ImageOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ), + imageOptions: imageOptions, ), ); + if (pickedFileList.isNotEmpty && context.mounted) { + _showPickedSnackBar(context, pickedFileList); + } setState(() { _mediaFileList = pickedFileList; }); @@ -124,7 +135,7 @@ class _MyHomePageState extends State { final List pickedFileList = []; final XFile? media = _firstOrNull(await _picker.getMedia( options: MediaOptions( - allowMultiple: isMultiImage, + allowMultiple: allowMultiple, imageOptions: ImageOptions( maxWidth: maxWidth, maxHeight: maxHeight, @@ -154,13 +165,12 @@ class _MyHomePageState extends State { imageQuality: quality, ), ); - setState(() { - _setImageFileListFromFile(pickedFile); - }); + if (pickedFile != null && context.mounted) { + _showPickedSnackBar(context, [pickedFile]); + } + setState(() => _setImageFileListFromFile(pickedFile)); } catch (e) { - setState(() { - _pickImageError = e; - }); + setState(() => _pickImageError = e); } }); } @@ -221,19 +231,28 @@ class _MyHomePageState extends State { child: ListView.builder( key: UniqueKey(), itemBuilder: (BuildContext context, int index) { + final XFile image = _mediaFileList![index]; final String? mime = lookupMimeType(_mediaFileList![index].path); - return Semantics( - label: 'image_picker_example_picked_image', - child: 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), + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(image.name, + key: const Key('image_picker_example_picked_image_name')), + Semantics( + label: 'image_picker_example_picked_image', + child: 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: _mediaFileList!.length, @@ -255,8 +274,7 @@ class _MyHomePageState extends State { Widget _buildInlineVideoPlayer(int index) { final VideoPlayerController controller = VideoPlayerController.file(File(_mediaFileList![index].path)); - const double volume = 1.0; - controller.setVolume(volume); + controller.setVolume(1.0); controller.initialize(); controller.setLooping(true); controller.play(); @@ -293,8 +311,8 @@ class _MyHomePageState extends State { _onImageButtonPressed(ImageSource.gallery, context: context); }, heroTag: 'image0', - tooltip: 'Pick Image from gallery', - label: const Text('Pick Image from gallery'), + tooltip: 'Pick image from gallery', + label: const Text('Pick image from gallery'), icon: const Icon(Icons.photo), ), ), @@ -306,13 +324,12 @@ class _MyHomePageState extends State { _onImageButtonPressed( ImageSource.gallery, context: context, - isMultiImage: true, - isMedia: true, + allowMultiple: true, ); }, - heroTag: 'multipleMedia', - tooltip: 'Pick Multiple Media from gallery', - label: const Text('Pick Multiple Media from gallery'), + heroTag: 'image1', + tooltip: 'Pick multiple images', + label: const Text('Pick multiple images'), icon: const Icon(Icons.photo_library), ), ), @@ -328,9 +345,9 @@ class _MyHomePageState extends State { ); }, heroTag: 'media', - tooltip: 'Pick Single Media from gallery', - label: const Text('Pick Single Media from gallery'), - icon: const Icon(Icons.photo_library), + tooltip: 'Pick item from gallery', + label: const Text('Pick item from gallery'), + icon: const Icon(Icons.photo_outlined), ), ), Padding( @@ -341,13 +358,14 @@ class _MyHomePageState extends State { _onImageButtonPressed( ImageSource.gallery, context: context, - isMultiImage: true, + allowMultiple: true, + isMedia: true, ); }, - heroTag: 'image1', - tooltip: 'Pick Multiple Image from gallery', - label: const Text('Pick Multiple Image from gallery'), - icon: const Icon(Icons.photo_library), + heroTag: 'multipleMedia', + tooltip: 'Pick multiple items', + label: const Text('Pick multiple items'), + icon: const Icon(Icons.photo_library_outlined), ), ), if (_picker.supportsImageSource(ImageSource.camera)) @@ -359,8 +377,8 @@ class _MyHomePageState extends State { _onImageButtonPressed(ImageSource.camera, context: context); }, heroTag: 'image2', - tooltip: 'Take a Photo', - label: const Text('Take a Photo'), + tooltip: 'Take a photo', + label: const Text('Take a photo'), icon: const Icon(Icons.camera_alt), ), ), @@ -372,9 +390,24 @@ class _MyHomePageState extends State { _isVideo = true; _onImageButtonPressed(ImageSource.gallery, context: context); }, - heroTag: 'video0', - tooltip: 'Pick Video from gallery', - label: const Text('Pick Video from gallery'), + heroTag: 'video', + tooltip: 'Pick video from gallery', + label: const Text('Pick video from gallery'), + icon: const Icon(Icons.video_file), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton.extended( + backgroundColor: Colors.red, + onPressed: () { + _isVideo = true; + _onImageButtonPressed(ImageSource.gallery, + context: context, allowMultiple: true); + }, + heroTag: 'multiVideo', + tooltip: 'Pick multiple videos', + label: const Text('Pick multiple videos'), icon: const Icon(Icons.video_library), ), ), @@ -387,9 +420,9 @@ class _MyHomePageState extends State { _isVideo = true; _onImageButtonPressed(ImageSource.camera, context: context); }, - heroTag: 'video1', - tooltip: 'Take a Video', - label: const Text('Take a Video'), + heroTag: 'takeVideo', + tooltip: 'Take a video', + label: const Text('Take a video'), icon: const Icon(Icons.videocam), ), ), @@ -464,6 +497,13 @@ class _MyHomePageState extends State { ); }); } + + void _showPickedSnackBar(BuildContext context, List files) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('Picked: ${files.map((XFile it) => it.name).join(',')}'), + duration: const Duration(seconds: 2), + )); + } } typedef OnPickImageCallback = void Function( diff --git a/packages/image_picker/image_picker_windows/example/pubspec.yaml b/packages/image_picker/image_picker_windows/example/pubspec.yaml index b09c095f886..3368e8d5640 100644 --- a/packages/image_picker/image_picker_windows/example/pubspec.yaml +++ b/packages/image_picker/image_picker_windows/example/pubspec.yaml @@ -10,7 +10,7 @@ environment: dependencies: flutter: sdk: flutter - image_picker_platform_interface: ^2.8.0 + image_picker_platform_interface: ^2.11.0 image_picker_windows: # When depending on this package from a real application you should use: # image_picker_windows: ^x.y.z diff --git a/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart b/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart index e9e414628c9..5fc3b063509 100644 --- a/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart +++ b/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart @@ -184,6 +184,17 @@ class ImagePickerWindows extends CameraDelegatingImagePickerPlatform { return files; } + @override + Future> getMultiVideoWithOptions( + {MultiVideoPickerOptions options = + const MultiVideoPickerOptions()}) async { + const XTypeGroup typeGroup = + XTypeGroup(label: 'Videos', extensions: videoFormats); + final List files = await fileSelector + .openFiles(acceptedTypeGroups: [typeGroup]); + return files; + } + // `maxWidth`, `maxHeight`, and `imageQuality` arguments are not // supported on Windows. If any of these arguments is supplied, // they will be silently ignored by the Windows version of the plugin. diff --git a/packages/image_picker/image_picker_windows/pubspec.yaml b/packages/image_picker/image_picker_windows/pubspec.yaml index 18637c1b8b6..92e3a75717e 100644 --- a/packages/image_picker/image_picker_windows/pubspec.yaml +++ b/packages/image_picker/image_picker_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker_windows description: Windows platform implementation of image_picker repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.2.1+1 +version: 0.2.2 environment: sdk: ^3.6.0 @@ -20,7 +20,7 @@ dependencies: file_selector_windows: ^0.9.0 flutter: sdk: flutter - image_picker_platform_interface: ^2.8.0 + image_picker_platform_interface: ^2.11.0 dev_dependencies: build_runner: ^2.1.5 diff --git a/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart b/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart index 6da0873af5b..98b4646374d 100644 --- a/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart +++ b/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart @@ -127,6 +127,17 @@ void main() { await expectLater( plugin.getVideo(source: ImageSource.camera), throwsStateError); }); + + test('getMultiVideoWithOptions passes the accepted type groups correctly', + () async { + await plugin.getMultiVideoWithOptions(); + + final VerificationResult result = verify( + mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].extensions, + ImagePickerWindows.videoFormats); + }); }); group('media', () {