Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/image_picker/image_picker/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## NEXT
## 1.2.0

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

## 1.1.2
Expand Down
65 changes: 43 additions & 22 deletions packages/image_picker/image_picker/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class _MyHomePageState extends State<MyHomePage> {
Future<void> _playVideo(XFile? file) async {
if (file != null && mounted) {
await _disposeVideoController();
late VideoPlayerController controller;
final VideoPlayerController controller;
if (kIsWeb) {
controller = VideoPlayerController.networkUrl(Uri.parse(file.path));
} else {
Expand All @@ -85,18 +85,25 @@ class _MyHomePageState extends State<MyHomePage> {
Future<void> _onImageButtonPressed(
ImageSource source, {
required BuildContext context,
bool isMultiImage = false,
bool allowMultiple = false,
bool isMedia = false,
}) async {
if (_controller != null) {
await _controller!.setVolume(0.0);
}
if (context.mounted) {
if (isVideo) {
final XFile? file = await _picker.pickVideo(
source: source, maxDuration: const Duration(seconds: 10));
await _playVideo(file);
} else if (isMultiImage) {
final List<XFile> files;
if (allowMultiple) {
files = await _picker.pickMultiVideo();
} else {
final XFile? file = await _picker.pickVideo(
source: source, maxDuration: const Duration(seconds: 10));
files = <XFile>[if (file != null) file];
}
// Just play the first file, to keep the example simple.
await _playVideo(files.firstOrNull);
} else if (allowMultiple) {
await _displayPickImageDialog(context, true, (double? maxWidth,
double? maxHeight, int? quality, int? limit) async {
try {
Expand Down Expand Up @@ -349,7 +356,7 @@ class _MyHomePageState extends State<MyHomePage> {
_onImageButtonPressed(ImageSource.gallery, context: context);
},
heroTag: 'image0',
tooltip: 'Pick Image from gallery',
tooltip: 'Pick image from gallery',
child: const Icon(Icons.photo),
),
),
Expand All @@ -361,12 +368,11 @@ class _MyHomePageState extends State<MyHomePage> {
_onImageButtonPressed(
ImageSource.gallery,
context: context,
isMultiImage: true,
isMedia: true,
allowMultiple: true,
);
},
heroTag: 'multipleMedia',
tooltip: 'Pick Multiple Media from gallery',
heroTag: 'image1',
tooltip: 'Pick multiple images',
child: const Icon(Icons.photo_library),
),
),
Expand All @@ -382,8 +388,8 @@ class _MyHomePageState extends State<MyHomePage> {
);
},
heroTag: 'media',
tooltip: 'Pick Single Media from gallery',
child: const Icon(Icons.photo_library),
tooltip: 'Pick item from gallery',
child: const Icon(Icons.photo_outlined),
),
),
Padding(
Expand All @@ -394,12 +400,13 @@ class _MyHomePageState extends State<MyHomePage> {
_onImageButtonPressed(
ImageSource.gallery,
context: context,
isMultiImage: true,
allowMultiple: true,
isMedia: true,
);
},
heroTag: 'image1',
tooltip: 'Pick Multiple Image from gallery',
child: const Icon(Icons.photo_library),
heroTag: 'multipleMedia',
tooltip: 'Pick multiple items',
child: const Icon(Icons.photo_library_outlined),
),
),
if (_picker.supportsImageSource(ImageSource.camera))
Expand All @@ -411,7 +418,7 @@ class _MyHomePageState extends State<MyHomePage> {
_onImageButtonPressed(ImageSource.camera, context: context);
},
heroTag: 'image2',
tooltip: 'Take a Photo',
tooltip: 'Take a photo',
child: const Icon(Icons.camera_alt),
),
),
Expand All @@ -423,8 +430,22 @@ class _MyHomePageState extends State<MyHomePage> {
isVideo = true;
_onImageButtonPressed(ImageSource.gallery, context: context);
},
heroTag: 'video0',
tooltip: 'Pick Video from gallery',
heroTag: 'video',
tooltip: 'Pick video from gallery',
child: const Icon(Icons.video_file),
),
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: FloatingActionButton(
backgroundColor: Colors.red,
onPressed: () {
isVideo = true;
_onImageButtonPressed(ImageSource.gallery,
context: context, allowMultiple: true);
},
heroTag: 'multiVideo',
tooltip: 'Pick multiple videos',
child: const Icon(Icons.video_library),
),
),
Expand All @@ -437,8 +458,8 @@ class _MyHomePageState extends State<MyHomePage> {
isVideo = true;
_onImageButtonPressed(ImageSource.camera, context: context);
},
heroTag: 'video1',
tooltip: 'Take a Video',
heroTag: 'takeVideo',
tooltip: 'Take a video',
child: const Icon(Icons.videocam),
),
),
Expand Down
10 changes: 10 additions & 0 deletions packages/image_picker/image_picker/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,13 @@ dev_dependencies:

flutter:
uses-material-design: true
# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE.
# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins
dependency_overrides:
image_picker_android: {path: ../../../../packages/image_picker/image_picker_android}
image_picker_for_web: {path: ../../../../packages/image_picker/image_picker_for_web}
image_picker_ios: {path: ../../../../packages/image_picker/image_picker_ios}
image_picker_linux: {path: ../../../../packages/image_picker/image_picker_linux}
image_picker_macos: {path: ../../../../packages/image_picker/image_picker_macos}
image_picker_platform_interface: {path: ../../../../packages/image_picker/image_picker_platform_interface}
image_picker_windows: {path: ../../../../packages/image_picker/image_picker_windows}
28 changes: 28 additions & 0 deletions packages/image_picker/image_picker/lib/image_picker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,34 @@ class ImagePicker {
);
}

/// Returns a [List<XFile>] of the videos that were picked.
///
/// The returned [List<XFile>] is intended to be used within a single app
/// session. Do not save the file path and use it across sessions.
///
/// The videos come from the gallery.
///
/// The [maxDuration] argument specifies the maximum duration of the captured
/// videos. If no [maxDuration] is specified, the maximum duration will be
/// infinite. This value may be ignored by platforms that cannot support it.
///
/// The `limit` parameter modifies the maximum number of videos that can be
/// selected. This value may be ignored by platforms that cannot support it.
///
/// The method can throw a [PlatformException] if the video selection process
/// fails.
Future<List<XFile>> pickMultiVideo({
Duration? maxDuration,
int? limit,
}) {
return platform.getMultiVideoWithOptions(
options: MultiVideoPickerOptions(
maxDuration: maxDuration,
limit: limit,
),
);
}

/// Retrieve the lost [XFile] when [pickImage], [pickMultiImage] or [pickVideo] failed because the MainActivity
/// is destroyed. (Android only)
///
Expand Down
12 changes: 11 additions & 1 deletion packages/image_picker/image_picker/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: Flutter plugin for selecting images from the Android and iOS image
library, and taking new pictures with the camera.
repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
version: 1.1.2
version: 1.2.0

environment:
sdk: ^3.6.0
Expand Down Expand Up @@ -49,3 +49,13 @@ topics:
- image-picker
- files
- file-selection
# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE.
# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins
dependency_overrides:
image_picker_android: {path: ../../../packages/image_picker/image_picker_android}
image_picker_for_web: {path: ../../../packages/image_picker/image_picker_for_web}
image_picker_ios: {path: ../../../packages/image_picker/image_picker_ios}
image_picker_linux: {path: ../../../packages/image_picker/image_picker_linux}
image_picker_macos: {path: ../../../packages/image_picker/image_picker_macos}
image_picker_platform_interface: {path: ../../../packages/image_picker/image_picker_platform_interface}
image_picker_windows: {path: ../../../packages/image_picker/image_picker_windows}
39 changes: 39 additions & 0 deletions packages/image_picker/image_picker/test/image_picker_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,45 @@ void main() {
expect(response.exception!.message, 'test_error_message');
});
});

group('#pickMultiVideo', () {
setUp(() {
when(mockPlatform.getMultiVideoWithOptions(
options: anyNamed('options'),
)).thenAnswer((Invocation _) async => <XFile>[]);
});

test('passes the arguments correctly', () async {
final ImagePicker picker = ImagePicker();
await picker.pickMultiVideo();
await picker.pickMultiVideo(maxDuration: const Duration(seconds: 10));
await picker.pickMultiVideo(limit: 5);

verifyInOrder(<Object>[
mockPlatform.getMultiVideoWithOptions(
options: argThat(
isInstanceOf<MultiVideoPickerOptions>(),
named: 'options',
)),
mockPlatform.getMultiVideoWithOptions(
options: argThat(
isInstanceOf<MultiVideoPickerOptions>().having(
(MultiVideoPickerOptions options) => options.maxDuration,
'maxDuration',
equals(const Duration(seconds: 10))),
named: 'options',
)),
mockPlatform.getMultiVideoWithOptions(
options: argThat(
isInstanceOf<MultiVideoPickerOptions>().having(
(MultiVideoPickerOptions options) => options.limit,
'limit',
equals(5)),
named: 'options',
)),
]);
});
});
});

group('#Multi images', () {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Mocks generated by Mockito 5.4.4 from annotations
// Mocks generated by Mockito 5.4.6 from annotations
// in image_picker/test/image_picker_test.dart.
// Do not manually edit this file.

Expand All @@ -19,6 +19,7 @@ import 'package:mockito/mockito.dart' as _i1;
// ignore_for_file: deprecated_member_use_from_same_package
// ignore_for_file: implementation_imports
// ignore_for_file: invalid_use_of_visible_for_testing_member
// ignore_for_file: must_be_immutable
// ignore_for_file: prefer_const_constructors
// ignore_for_file: unnecessary_parenthesis
// ignore_for_file: camel_case_types
Expand Down Expand Up @@ -248,6 +249,19 @@ class MockImagePickerPlatform extends _i1.Mock
returnValue: _i4.Future<List<_i5.XFile>>.value(<_i5.XFile>[]),
) as _i4.Future<List<_i5.XFile>>);

@override
_i4.Future<List<_i5.XFile>> getMultiVideoWithOptions(
{_i2.MultiVideoPickerOptions? options =
const _i2.MultiVideoPickerOptions()}) =>
(super.noSuchMethod(
Invocation.method(
#getMultiVideoWithOptions,
[],
{#options: options},
),
returnValue: _i4.Future<List<_i5.XFile>>.value(<_i5.XFile>[]),
) as _i4.Future<List<_i5.XFile>>);

@override
bool supportsImageSource(_i2.ImageSource? source) => (super.noSuchMethod(
Invocation.method(
Expand Down
4 changes: 4 additions & 0 deletions packages/image_picker/image_picker_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.8.13

* Adds support for `getMultiVideo`.

## 0.8.12+25

* Updates kotlin version to 2.2.0 to enable gradle 8.11 support.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<List<String>> 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<List<String>> result) {
if (!setPendingOptionsAndResult(options, null, result)) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<MediaPath> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading