Skip to content

Commit 65be884

Browse files
[image_picker] Add the ability to pick multiple videos (#9775)
Adds `pickMultiVideo`, to allow picking multiple videos. This fills the gap in our current support matrix, which allows single or multiple images, single or multiple image-or-video, but only single video-only. Other changes: - While updating the example apps to have a new button for manual testing, I did some cleanup (capitalization fixes, inconsistent use of icons, button order), and also reduced drift between the different versions (e.g., someone had added a snackbar to Android that showed the selected paths, so I replicated that change to the other implementations). - On iOS, pickVideo had never been updated to the new UI-or-PH codepaths that all the other implementations used. pickMultiVideo was easily implementable with the new codepaths with only minor changes, so rather than leave pickVideo inconsistent I updated it as well. Fixes flutter/flutter#102283 ## Pre-Review Checklist **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
1 parent b26262b commit 65be884

File tree

7 files changed

+136
-33
lines changed

7 files changed

+136
-33
lines changed

packages/image_picker/image_picker/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
## NEXT
1+
## 1.2.0
22

3+
* Adds `pickMultiVideo` to allow selecting multiple videos from the gallery.
34
* Updates minimum supported SDK version to Flutter 3.27/Dart 3.6.
45

56
## 1.1.2

packages/image_picker/image_picker/example/lib/main.dart

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class _MyHomePageState extends State<MyHomePage> {
6161
Future<void> _playVideo(XFile? file) async {
6262
if (file != null && mounted) {
6363
await _disposeVideoController();
64-
late VideoPlayerController controller;
64+
final VideoPlayerController controller;
6565
if (kIsWeb) {
6666
controller = VideoPlayerController.networkUrl(Uri.parse(file.path));
6767
} else {
@@ -85,18 +85,25 @@ class _MyHomePageState extends State<MyHomePage> {
8585
Future<void> _onImageButtonPressed(
8686
ImageSource source, {
8787
required BuildContext context,
88-
bool isMultiImage = false,
88+
bool allowMultiple = false,
8989
bool isMedia = false,
9090
}) async {
9191
if (_controller != null) {
9292
await _controller!.setVolume(0.0);
9393
}
9494
if (context.mounted) {
9595
if (isVideo) {
96-
final XFile? file = await _picker.pickVideo(
97-
source: source, maxDuration: const Duration(seconds: 10));
98-
await _playVideo(file);
99-
} else if (isMultiImage) {
96+
final List<XFile> files;
97+
if (allowMultiple) {
98+
files = await _picker.pickMultiVideo();
99+
} else {
100+
final XFile? file = await _picker.pickVideo(
101+
source: source, maxDuration: const Duration(seconds: 10));
102+
files = <XFile>[if (file != null) file];
103+
}
104+
// Just play the first file, to keep the example simple.
105+
await _playVideo(files.firstOrNull);
106+
} else if (allowMultiple) {
100107
await _displayPickImageDialog(context, true, (double? maxWidth,
101108
double? maxHeight, int? quality, int? limit) async {
102109
try {
@@ -349,7 +356,7 @@ class _MyHomePageState extends State<MyHomePage> {
349356
_onImageButtonPressed(ImageSource.gallery, context: context);
350357
},
351358
heroTag: 'image0',
352-
tooltip: 'Pick Image from gallery',
359+
tooltip: 'Pick image from gallery',
353360
child: const Icon(Icons.photo),
354361
),
355362
),
@@ -361,12 +368,11 @@ class _MyHomePageState extends State<MyHomePage> {
361368
_onImageButtonPressed(
362369
ImageSource.gallery,
363370
context: context,
364-
isMultiImage: true,
365-
isMedia: true,
371+
allowMultiple: true,
366372
);
367373
},
368-
heroTag: 'multipleMedia',
369-
tooltip: 'Pick Multiple Media from gallery',
374+
heroTag: 'image1',
375+
tooltip: 'Pick multiple images',
370376
child: const Icon(Icons.photo_library),
371377
),
372378
),
@@ -382,8 +388,8 @@ class _MyHomePageState extends State<MyHomePage> {
382388
);
383389
},
384390
heroTag: 'media',
385-
tooltip: 'Pick Single Media from gallery',
386-
child: const Icon(Icons.photo_library),
391+
tooltip: 'Pick item from gallery',
392+
child: const Icon(Icons.photo_outlined),
387393
),
388394
),
389395
Padding(
@@ -394,12 +400,13 @@ class _MyHomePageState extends State<MyHomePage> {
394400
_onImageButtonPressed(
395401
ImageSource.gallery,
396402
context: context,
397-
isMultiImage: true,
403+
allowMultiple: true,
404+
isMedia: true,
398405
);
399406
},
400-
heroTag: 'image1',
401-
tooltip: 'Pick Multiple Image from gallery',
402-
child: const Icon(Icons.photo_library),
407+
heroTag: 'multipleMedia',
408+
tooltip: 'Pick multiple items',
409+
child: const Icon(Icons.photo_library_outlined),
403410
),
404411
),
405412
if (_picker.supportsImageSource(ImageSource.camera))
@@ -411,7 +418,7 @@ class _MyHomePageState extends State<MyHomePage> {
411418
_onImageButtonPressed(ImageSource.camera, context: context);
412419
},
413420
heroTag: 'image2',
414-
tooltip: 'Take a Photo',
421+
tooltip: 'Take a photo',
415422
child: const Icon(Icons.camera_alt),
416423
),
417424
),
@@ -423,8 +430,22 @@ class _MyHomePageState extends State<MyHomePage> {
423430
isVideo = true;
424431
_onImageButtonPressed(ImageSource.gallery, context: context);
425432
},
426-
heroTag: 'video0',
427-
tooltip: 'Pick Video from gallery',
433+
heroTag: 'video',
434+
tooltip: 'Pick video from gallery',
435+
child: const Icon(Icons.video_file),
436+
),
437+
),
438+
Padding(
439+
padding: const EdgeInsets.only(top: 16.0),
440+
child: FloatingActionButton(
441+
backgroundColor: Colors.red,
442+
onPressed: () {
443+
isVideo = true;
444+
_onImageButtonPressed(ImageSource.gallery,
445+
context: context, allowMultiple: true);
446+
},
447+
heroTag: 'multiVideo',
448+
tooltip: 'Pick multiple videos',
428449
child: const Icon(Icons.video_library),
429450
),
430451
),
@@ -437,8 +458,8 @@ class _MyHomePageState extends State<MyHomePage> {
437458
isVideo = true;
438459
_onImageButtonPressed(ImageSource.camera, context: context);
439460
},
440-
heroTag: 'video1',
441-
tooltip: 'Take a Video',
461+
heroTag: 'takeVideo',
462+
tooltip: 'Take a video',
442463
child: const Icon(Icons.videocam),
443464
),
444465
),

packages/image_picker/image_picker/example/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ dependencies:
1717
# The example app is bundled with the plugin so we use a path dependency on
1818
# the parent directory to use the current plugin's version.
1919
path: ../
20-
image_picker_platform_interface: ^2.10.0
20+
image_picker_platform_interface: ^2.11.0
2121
mime: ^1.0.4
2222
video_player: ^2.7.0
2323

packages/image_picker/image_picker/lib/image_picker.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,34 @@ class ImagePicker {
298298
);
299299
}
300300

301+
/// Returns a [List<XFile>] of the videos that were picked.
302+
///
303+
/// The returned [List<XFile>] is intended to be used within a single app
304+
/// session. Do not save the file path and use it across sessions.
305+
///
306+
/// The videos come from the gallery.
307+
///
308+
/// The [maxDuration] argument specifies the maximum duration of the captured
309+
/// videos. If no [maxDuration] is specified, the maximum duration will be
310+
/// infinite. This value may be ignored by platforms that cannot support it.
311+
///
312+
/// The `limit` parameter modifies the maximum number of videos that can be
313+
/// selected. This value may be ignored by platforms that cannot support it.
314+
///
315+
/// The method can throw a [PlatformException] if the video selection process
316+
/// fails.
317+
Future<List<XFile>> pickMultiVideo({
318+
Duration? maxDuration,
319+
int? limit,
320+
}) {
321+
return platform.getMultiVideoWithOptions(
322+
options: MultiVideoPickerOptions(
323+
maxDuration: maxDuration,
324+
limit: limit,
325+
),
326+
);
327+
}
328+
301329
/// Retrieve the lost [XFile] when [pickImage], [pickMultiImage] or [pickVideo] failed because the MainActivity
302330
/// is destroyed. (Android only)
303331
///

packages/image_picker/image_picker/pubspec.yaml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ description: Flutter plugin for selecting images from the Android and iOS image
33
library, and taking new pictures with the camera.
44
repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker
55
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
6-
version: 1.1.2
6+
version: 1.2.0
77

88
environment:
99
sdk: ^3.6.0
@@ -28,13 +28,13 @@ flutter:
2828
dependencies:
2929
flutter:
3030
sdk: flutter
31-
image_picker_android: ^0.8.7
32-
image_picker_for_web: ">=2.2.0 <4.0.0"
33-
image_picker_ios: ^0.8.8
34-
image_picker_linux: ^0.2.1
35-
image_picker_macos: ^0.2.1
36-
image_picker_platform_interface: ^2.10.0
37-
image_picker_windows: ^0.2.1
31+
image_picker_android: ^0.8.13
32+
image_picker_for_web: ^3.1.0
33+
image_picker_ios: ^0.8.13
34+
image_picker_linux: ^0.2.2
35+
image_picker_macos: ^0.2.2
36+
image_picker_platform_interface: ^2.11.0
37+
image_picker_windows: ^0.2.2
3838

3939
dev_dependencies:
4040
build_runner: ^2.1.10

packages/image_picker/image_picker/test/image_picker_test.dart

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,45 @@ void main() {
397397
expect(response.exception!.message, 'test_error_message');
398398
});
399399
});
400+
401+
group('#pickMultiVideo', () {
402+
setUp(() {
403+
when(mockPlatform.getMultiVideoWithOptions(
404+
options: anyNamed('options'),
405+
)).thenAnswer((Invocation _) async => <XFile>[]);
406+
});
407+
408+
test('passes the arguments correctly', () async {
409+
final ImagePicker picker = ImagePicker();
410+
await picker.pickMultiVideo();
411+
await picker.pickMultiVideo(maxDuration: const Duration(seconds: 10));
412+
await picker.pickMultiVideo(limit: 5);
413+
414+
verifyInOrder(<Object>[
415+
mockPlatform.getMultiVideoWithOptions(
416+
options: argThat(
417+
isInstanceOf<MultiVideoPickerOptions>(),
418+
named: 'options',
419+
)),
420+
mockPlatform.getMultiVideoWithOptions(
421+
options: argThat(
422+
isInstanceOf<MultiVideoPickerOptions>().having(
423+
(MultiVideoPickerOptions options) => options.maxDuration,
424+
'maxDuration',
425+
equals(const Duration(seconds: 10))),
426+
named: 'options',
427+
)),
428+
mockPlatform.getMultiVideoWithOptions(
429+
options: argThat(
430+
isInstanceOf<MultiVideoPickerOptions>().having(
431+
(MultiVideoPickerOptions options) => options.limit,
432+
'limit',
433+
equals(5)),
434+
named: 'options',
435+
)),
436+
]);
437+
});
438+
});
400439
});
401440

402441
group('#Multi images', () {

packages/image_picker/image_picker/test/image_picker_test.mocks.dart

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Mocks generated by Mockito 5.4.4 from annotations
1+
// Mocks generated by Mockito 5.4.6 from annotations
22
// in image_picker/test/image_picker_test.dart.
33
// Do not manually edit this file.
44

@@ -19,6 +19,7 @@ import 'package:mockito/mockito.dart' as _i1;
1919
// ignore_for_file: deprecated_member_use_from_same_package
2020
// ignore_for_file: implementation_imports
2121
// ignore_for_file: invalid_use_of_visible_for_testing_member
22+
// ignore_for_file: must_be_immutable
2223
// ignore_for_file: prefer_const_constructors
2324
// ignore_for_file: unnecessary_parenthesis
2425
// ignore_for_file: camel_case_types
@@ -248,6 +249,19 @@ class MockImagePickerPlatform extends _i1.Mock
248249
returnValue: _i4.Future<List<_i5.XFile>>.value(<_i5.XFile>[]),
249250
) as _i4.Future<List<_i5.XFile>>);
250251

252+
@override
253+
_i4.Future<List<_i5.XFile>> getMultiVideoWithOptions(
254+
{_i2.MultiVideoPickerOptions? options =
255+
const _i2.MultiVideoPickerOptions()}) =>
256+
(super.noSuchMethod(
257+
Invocation.method(
258+
#getMultiVideoWithOptions,
259+
[],
260+
{#options: options},
261+
),
262+
returnValue: _i4.Future<List<_i5.XFile>>.value(<_i5.XFile>[]),
263+
) as _i4.Future<List<_i5.XFile>>);
264+
251265
@override
252266
bool supportsImageSource(_i2.ImageSource? source) => (super.noSuchMethod(
253267
Invocation.method(

0 commit comments

Comments
 (0)