[image_picker] getMedia platform implementations (#4175)

Adds `getMedia` and `getMultipleMedia` methods to all image_picker
platforms.

~~waiting on https://github.com/flutter/packages/pull/4174~~

precursor to https://github.com/flutter/packages/pull/3892

part of https://github.com/flutter/flutter/issues/89159

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [relevant style guides] and ran the
auto-formatter. (Unlike the flutter/flutter repo, the flutter/packages
repo does use `dart format`.)
- [x] I signed the [CLA].
- [x] The title of the PR starts with the name of the package surrounded
by square brackets, e.g. `[shared_preferences]`
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated `pubspec.yaml` with an appropriate new version according
to the [pub versioning philosophy], or this PR is [exempt from version
changes].
- [x] I updated `CHANGELOG.md` to add a description of the change,
[following repository CHANGELOG style].
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/packages/blob/main/CONTRIBUTING.md
[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene
[relevant style guides]:
https://github.com/flutter/packages/blob/main/CONTRIBUTING.md#style
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes
[Discord]: https://github.com/flutter/flutter/wiki/Chat
[pub versioning philosophy]: https://dart.dev/tools/pub/versioning
[exempt from version changes]:
https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#version-and-changelog-updates
[following repository CHANGELOG style]:
https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#changelog-style
[test-exempt]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#tests
This commit is contained in:
Tarrin Neal
2023-06-14 17:34:05 -07:00
committed by GitHub
parent cef91bc730
commit 7869896018
55 changed files with 2561 additions and 449 deletions

View File

@ -1,3 +1,7 @@
## 0.8.7
* Adds `getMedia` method.
## 0.8.6+20
* Bumps androidx.activity:activity from 1.7.0 to 1.7.1.

View File

@ -79,6 +79,7 @@ public class ImagePickerDelegate
@VisibleForTesting static final int REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA = 2343;
@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_VIDEO_FROM_GALLERY = 2352;
@VisibleForTesting static final int REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA = 2353;
@ -279,6 +280,52 @@ public class ImagePickerDelegate
return result.build();
}
public void chooseMediaFromGallery(
@NonNull Messages.MediaSelectionOptions options,
@NonNull Messages.GeneralOptions generalOptions,
@NonNull Messages.Result<List<String>> result) {
if (!setPendingOptionsAndResult(options.getImageSelectionOptions(), null, result)) {
finishWithAlreadyActiveError(result);
return;
}
launchPickMediaFromGalleryIntent(generalOptions);
}
private void launchPickMediaFromGalleryIntent(Messages.GeneralOptions generalOptions) {
Intent pickMediaIntent;
if (generalOptions.getUsePhotoPicker() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (generalOptions.getAllowMultiple()) {
pickMediaIntent =
new ActivityResultContracts.PickMultipleVisualMedia()
.createIntent(
activity,
new PickVisualMediaRequest.Builder()
.setMediaType(
ActivityResultContracts.PickVisualMedia.ImageAndVideo.INSTANCE)
.build());
} else {
pickMediaIntent =
new ActivityResultContracts.PickVisualMedia()
.createIntent(
activity,
new PickVisualMediaRequest.Builder()
.setMediaType(
ActivityResultContracts.PickVisualMedia.ImageAndVideo.INSTANCE)
.build());
}
} else {
pickMediaIntent = new Intent(Intent.ACTION_GET_CONTENT);
pickMediaIntent.setType("*/*");
String[] mimeTypes = {"video/*", "image/*"};
pickMediaIntent.putExtra("CONTENT_TYPE", mimeTypes);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
pickMediaIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, generalOptions.getAllowMultiple());
}
}
activity.startActivityForResult(pickMediaIntent, REQUEST_CODE_CHOOSE_MEDIA_FROM_GALLERY);
}
public void chooseVideoFromGallery(
@NonNull VideoSelectionOptions options,
boolean usePhotoPicker,
@ -291,9 +338,9 @@ public class ImagePickerDelegate
launchPickVideoFromGalleryIntent(usePhotoPicker);
}
private void launchPickVideoFromGalleryIntent(Boolean useAndroidPhotoPicker) {
private void launchPickVideoFromGalleryIntent(Boolean usePhotoPicker) {
Intent pickVideoIntent;
if (useAndroidPhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (usePhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
pickVideoIntent =
new ActivityResultContracts.PickVisualMedia()
.createIntent(
@ -389,9 +436,9 @@ public class ImagePickerDelegate
launchMultiPickImageFromGalleryIntent(usePhotoPicker);
}
private void launchPickImageFromGalleryIntent(Boolean useAndroidPhotoPicker) {
private void launchPickImageFromGalleryIntent(Boolean usePhotoPicker) {
Intent pickImageIntent;
if (useAndroidPhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (usePhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
pickImageIntent =
new ActivityResultContracts.PickVisualMedia()
.createIntent(
@ -406,9 +453,9 @@ public class ImagePickerDelegate
activity.startActivityForResult(pickImageIntent, REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY);
}
private void launchMultiPickImageFromGalleryIntent(Boolean useAndroidPhotoPicker) {
private void launchMultiPickImageFromGalleryIntent(Boolean usePhotoPicker) {
Intent pickMultiImageIntent;
if (useAndroidPhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (usePhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
pickMultiImageIntent =
new ActivityResultContracts.PickMultipleVisualMedia()
.createIntent(
@ -563,6 +610,9 @@ public class ImagePickerDelegate
case REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA:
handlerRunnable = () -> handleCaptureImageResult(resultCode);
break;
case REQUEST_CODE_CHOOSE_MEDIA_FROM_GALLERY:
handlerRunnable = () -> handleChooseMediaResult(resultCode, data);
break;
case REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY:
handlerRunnable = () -> handleChooseVideoResult(resultCode, data);
break;
@ -589,17 +639,59 @@ public class ImagePickerDelegate
finishWithSuccess(null);
}
private void handleChooseMultiImageResult(int resultCode, Intent intent) {
public class MediaPath {
public MediaPath(@NonNull String path, @Nullable String mimeType) {
this.path = path;
this.mimeType = mimeType;
}
final String path;
final String mimeType;
public @NonNull String getPath() {
return path;
}
public @Nullable String getMimeType() {
return mimeType;
}
}
private void handleChooseMediaResult(int resultCode, Intent intent) {
if (resultCode == Activity.RESULT_OK && intent != null) {
ArrayList<String> paths = new ArrayList<>();
ArrayList<MediaPath> paths = new ArrayList<>();
if (intent.getClipData() != null) {
for (int i = 0; i < intent.getClipData().getItemCount(); i++) {
paths.add(fileUtils.getPathFromUri(activity, intent.getClipData().getItemAt(i).getUri()));
Uri uri = intent.getClipData().getItemAt(i).getUri();
String path = fileUtils.getPathFromUri(activity, uri);
String mimeType = activity.getContentResolver().getType(uri);
paths.add(new MediaPath(path, mimeType));
}
} else {
paths.add(fileUtils.getPathFromUri(activity, intent.getData()));
paths.add(new MediaPath(fileUtils.getPathFromUri(activity, intent.getData()), null));
}
handleMultiImageResult(paths);
handleMediaResult(paths);
return;
}
// User cancelled choosing a picture.
finishWithSuccess(null);
}
private void handleChooseMultiImageResult(int resultCode, Intent intent) {
if (resultCode == Activity.RESULT_OK && intent != null) {
ArrayList<MediaPath> paths = new ArrayList<>();
if (intent.getClipData() != null) {
for (int i = 0; i < intent.getClipData().getItemCount(); i++) {
paths.add(
new MediaPath(
fileUtils.getPathFromUri(activity, intent.getClipData().getItemAt(i).getUri()),
null));
}
} else {
paths.add(new MediaPath(fileUtils.getPathFromUri(activity, intent.getData()), null));
}
handleMediaResult(paths);
return;
}
@ -649,26 +741,6 @@ public class ImagePickerDelegate
finishWithSuccess(null);
}
private void handleMultiImageResult(ArrayList<String> paths) {
ImageSelectionOptions localImageOptions = null;
synchronized (pendingCallStateLock) {
if (pendingCallState != null) {
localImageOptions = pendingCallState.imageOptions;
}
}
if (localImageOptions != null) {
ArrayList<String> finalPath = new ArrayList<>();
for (int i = 0; i < paths.size(); i++) {
String finalImagePath = getResizedImagePath(paths.get(i), localImageOptions);
finalPath.add(i, finalImagePath);
}
finishWithListSuccess(finalPath);
} else {
finishWithListSuccess(paths);
}
}
void handleImageResult(String path, boolean shouldDeleteOriginalIfScaled) {
ImageSelectionOptions localImageOptions = null;
synchronized (pendingCallStateLock) {
@ -679,7 +751,7 @@ public class ImagePickerDelegate
if (localImageOptions != null) {
String finalImagePath = getResizedImagePath(path, localImageOptions);
//delete original file if scaled
// Delete original file if scaled.
if (finalImagePath != null && !finalImagePath.equals(path) && shouldDeleteOriginalIfScaled) {
new File(path).delete();
}
@ -697,7 +769,34 @@ public class ImagePickerDelegate
outputOptions.getQuality().intValue());
}
void handleVideoResult(String path) {
private void handleMediaResult(@NonNull ArrayList<MediaPath> paths) {
ImageSelectionOptions localImageOptions = null;
synchronized (pendingCallStateLock) {
if (pendingCallState != null) {
localImageOptions = pendingCallState.imageOptions;
}
}
ArrayList<String> finalPaths = new ArrayList<>();
if (localImageOptions != null) {
for (int i = 0; i < paths.size(); i++) {
MediaPath path = paths.get(i);
String finalPath = path.path;
if (path.mimeType == null || !path.mimeType.startsWith("video/")) {
finalPath = getResizedImagePath(path.path, localImageOptions);
}
finalPaths.add(finalPath);
}
finishWithListSuccess(finalPaths);
} else {
for (int i = 0; i < paths.size(); i++) {
finalPaths.add(paths.get(i).path);
}
finishWithListSuccess(finalPaths);
}
}
private void handleVideoResult(String path) {
finishWithSuccess(path);
}

View File

@ -19,10 +19,16 @@ import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.PluginRegistry;
import io.flutter.plugins.imagepicker.Messages.CacheRetrievalResult;
import io.flutter.plugins.imagepicker.Messages.FlutterError;
import io.flutter.plugins.imagepicker.Messages.GeneralOptions;
import io.flutter.plugins.imagepicker.Messages.ImagePickerApi;
import io.flutter.plugins.imagepicker.Messages.ImageSelectionOptions;
import io.flutter.plugins.imagepicker.Messages.MediaSelectionOptions;
import io.flutter.plugins.imagepicker.Messages.Result;
import io.flutter.plugins.imagepicker.Messages.SourceCamera;
import io.flutter.plugins.imagepicker.Messages.SourceSpecification;
import io.flutter.plugins.imagepicker.Messages.VideoSelectionOptions;
import java.util.List;
@SuppressWarnings("deprecation")
@ -279,7 +285,7 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic
private void setCameraDevice(
@NonNull ImagePickerDelegate delegate, @NonNull SourceSpecification source) {
Messages.SourceCamera camera = source.getCamera();
SourceCamera camera = source.getCamera();
if (camera != null) {
ImagePickerDelegate.CameraDevice device;
switch (camera) {
@ -298,9 +304,8 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic
@Override
public void pickImages(
@NonNull SourceSpecification source,
@NonNull Messages.ImageSelectionOptions options,
@NonNull Boolean allowMultiple,
@NonNull Boolean usePhotoPicker,
@NonNull ImageSelectionOptions options,
@NonNull GeneralOptions generalOptions,
@NonNull Result<List<String>> result) {
ImagePickerDelegate delegate = getImagePickerDelegate();
if (delegate == null) {
@ -311,12 +316,12 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic
}
setCameraDevice(delegate, source);
if (allowMultiple) {
delegate.chooseMultiImageFromGallery(options, usePhotoPicker, result);
if (generalOptions.getAllowMultiple()) {
delegate.chooseMultiImageFromGallery(options, generalOptions.getUsePhotoPicker(), result);
} else {
switch (source.getType()) {
case GALLERY:
delegate.chooseImageFromGallery(options, usePhotoPicker, result);
delegate.chooseImageFromGallery(options, generalOptions.getUsePhotoPicker(), result);
break;
case CAMERA:
delegate.takeImageWithCamera(options, result);
@ -325,12 +330,26 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic
}
}
@Override
public void pickMedia(
@NonNull MediaSelectionOptions mediaSelectionOptions,
@NonNull GeneralOptions generalOptions,
@NonNull Result<List<String>> result) {
ImagePickerDelegate delegate = getImagePickerDelegate();
if (delegate == null) {
result.error(
new FlutterError(
"no_activity", "image_picker plugin requires a foreground activity.", null));
return;
}
delegate.chooseMediaFromGallery(mediaSelectionOptions, generalOptions, result);
}
@Override
public void pickVideos(
@NonNull SourceSpecification source,
@NonNull Messages.VideoSelectionOptions options,
@NonNull Boolean allowMultiple,
@NonNull Boolean usePhotoPicker,
@NonNull VideoSelectionOptions options,
@NonNull GeneralOptions generalOptions,
@NonNull Result<List<String>> result) {
ImagePickerDelegate delegate = getImagePickerDelegate();
if (delegate == null) {
@ -341,12 +360,12 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic
}
setCameraDevice(delegate, source);
if (allowMultiple) {
if (generalOptions.getAllowMultiple()) {
result.error(new RuntimeException("Multi-video selection is not implemented"));
} else {
switch (source.getType()) {
case GALLERY:
delegate.chooseVideoFromGallery(options, usePhotoPicker, result);
delegate.chooseVideoFromGallery(options, generalOptions.getUsePhotoPicker(), result);
break;
case CAMERA:
delegate.takeVideoWithCamera(options, result);
@ -357,7 +376,7 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic
@Nullable
@Override
public Messages.CacheRetrievalResult retrieveLostResults() {
public CacheRetrievalResult retrieveLostResults() {
ImagePickerDelegate delegate = getImagePickerDelegate();
if (delegate == null) {
throw new FlutterError(

View File

@ -88,6 +88,79 @@ public class Messages {
}
}
/** Generated class from Pigeon that represents data sent in messages. */
public static final class GeneralOptions {
private @NonNull Boolean allowMultiple;
public @NonNull Boolean getAllowMultiple() {
return allowMultiple;
}
public void setAllowMultiple(@NonNull Boolean setterArg) {
if (setterArg == null) {
throw new IllegalStateException("Nonnull field \"allowMultiple\" is null.");
}
this.allowMultiple = setterArg;
}
private @NonNull Boolean usePhotoPicker;
public @NonNull Boolean getUsePhotoPicker() {
return usePhotoPicker;
}
public void setUsePhotoPicker(@NonNull Boolean setterArg) {
if (setterArg == null) {
throw new IllegalStateException("Nonnull field \"usePhotoPicker\" is null.");
}
this.usePhotoPicker = setterArg;
}
/** Constructor is non-public to enforce null safety; use Builder. */
GeneralOptions() {}
public static final class Builder {
private @Nullable Boolean allowMultiple;
public @NonNull Builder setAllowMultiple(@NonNull Boolean setterArg) {
this.allowMultiple = setterArg;
return this;
}
private @Nullable Boolean usePhotoPicker;
public @NonNull Builder setUsePhotoPicker(@NonNull Boolean setterArg) {
this.usePhotoPicker = setterArg;
return this;
}
public @NonNull GeneralOptions build() {
GeneralOptions pigeonReturn = new GeneralOptions();
pigeonReturn.setAllowMultiple(allowMultiple);
pigeonReturn.setUsePhotoPicker(usePhotoPicker);
return pigeonReturn;
}
}
@NonNull
ArrayList<Object> toList() {
ArrayList<Object> toListResult = new ArrayList<Object>(2);
toListResult.add(allowMultiple);
toListResult.add(usePhotoPicker);
return toListResult;
}
static @NonNull GeneralOptions fromList(@NonNull ArrayList<Object> list) {
GeneralOptions pigeonResult = new GeneralOptions();
Object allowMultiple = list.get(0);
pigeonResult.setAllowMultiple((Boolean) allowMultiple);
Object usePhotoPicker = list.get(1);
pigeonResult.setUsePhotoPicker((Boolean) usePhotoPicker);
return pigeonResult;
}
}
/**
* Options for image selection and output.
*
@ -193,6 +266,58 @@ public class Messages {
}
}
/** Generated class from Pigeon that represents data sent in messages. */
public static final class MediaSelectionOptions {
private @NonNull ImageSelectionOptions imageSelectionOptions;
public @NonNull ImageSelectionOptions getImageSelectionOptions() {
return imageSelectionOptions;
}
public void setImageSelectionOptions(@NonNull ImageSelectionOptions setterArg) {
if (setterArg == null) {
throw new IllegalStateException("Nonnull field \"imageSelectionOptions\" is null.");
}
this.imageSelectionOptions = setterArg;
}
/** Constructor is non-public to enforce null safety; use Builder. */
MediaSelectionOptions() {}
public static final class Builder {
private @Nullable ImageSelectionOptions imageSelectionOptions;
public @NonNull Builder setImageSelectionOptions(@NonNull ImageSelectionOptions setterArg) {
this.imageSelectionOptions = setterArg;
return this;
}
public @NonNull MediaSelectionOptions build() {
MediaSelectionOptions pigeonReturn = new MediaSelectionOptions();
pigeonReturn.setImageSelectionOptions(imageSelectionOptions);
return pigeonReturn;
}
}
@NonNull
ArrayList<Object> toList() {
ArrayList<Object> toListResult = new ArrayList<Object>(1);
toListResult.add((imageSelectionOptions == null) ? null : imageSelectionOptions.toList());
return toListResult;
}
static @NonNull MediaSelectionOptions fromList(@NonNull ArrayList<Object> list) {
MediaSelectionOptions pigeonResult = new MediaSelectionOptions();
Object imageSelectionOptions = list.get(0);
pigeonResult.setImageSelectionOptions(
(imageSelectionOptions == null)
? null
: ImageSelectionOptions.fromList((ArrayList<Object>) imageSelectionOptions));
return pigeonResult;
}
}
/**
* Options for image selection and output.
*
@ -523,10 +648,14 @@ public class Messages {
case (byte) 129:
return CacheRetrievalResult.fromList((ArrayList<Object>) readValue(buffer));
case (byte) 130:
return ImageSelectionOptions.fromList((ArrayList<Object>) readValue(buffer));
return GeneralOptions.fromList((ArrayList<Object>) readValue(buffer));
case (byte) 131:
return SourceSpecification.fromList((ArrayList<Object>) readValue(buffer));
return ImageSelectionOptions.fromList((ArrayList<Object>) readValue(buffer));
case (byte) 132:
return MediaSelectionOptions.fromList((ArrayList<Object>) readValue(buffer));
case (byte) 133:
return SourceSpecification.fromList((ArrayList<Object>) readValue(buffer));
case (byte) 134:
return VideoSelectionOptions.fromList((ArrayList<Object>) readValue(buffer));
default:
return super.readValueOfType(type, buffer);
@ -541,14 +670,20 @@ public class Messages {
} else if (value instanceof CacheRetrievalResult) {
stream.write(129);
writeValue(stream, ((CacheRetrievalResult) value).toList());
} else if (value instanceof ImageSelectionOptions) {
} else if (value instanceof GeneralOptions) {
stream.write(130);
writeValue(stream, ((ImageSelectionOptions) value).toList());
} else if (value instanceof SourceSpecification) {
writeValue(stream, ((GeneralOptions) value).toList());
} else if (value instanceof ImageSelectionOptions) {
stream.write(131);
writeValue(stream, ((ImageSelectionOptions) value).toList());
} else if (value instanceof MediaSelectionOptions) {
stream.write(132);
writeValue(stream, ((MediaSelectionOptions) value).toList());
} else if (value instanceof SourceSpecification) {
stream.write(133);
writeValue(stream, ((SourceSpecification) value).toList());
} else if (value instanceof VideoSelectionOptions) {
stream.write(132);
stream.write(134);
writeValue(stream, ((VideoSelectionOptions) value).toList());
} else {
super.writeValue(stream, value);
@ -567,8 +702,7 @@ public class Messages {
void pickImages(
@NonNull SourceSpecification source,
@NonNull ImageSelectionOptions options,
@NonNull Boolean allowMultiple,
@NonNull Boolean usePhotoPicker,
@NonNull GeneralOptions generalOptions,
@NonNull Result<List<String>> result);
/**
* Selects video and returns their paths.
@ -579,8 +713,17 @@ public class Messages {
void pickVideos(
@NonNull SourceSpecification source,
@NonNull VideoSelectionOptions options,
@NonNull Boolean allowMultiple,
@NonNull Boolean usePhotoPicker,
@NonNull GeneralOptions generalOptions,
@NonNull Result<List<String>> result);
/**
* Selects images and videos and returns their paths.
*
* <p>Elements must not be null, by convention. See
* https://github.com/flutter/flutter/issues/97848
*/
void pickMedia(
@NonNull MediaSelectionOptions mediaSelectionOptions,
@NonNull GeneralOptions generalOptions,
@NonNull Result<List<String>> result);
/** Returns results from a previous app session, if any. */
@Nullable
@ -607,8 +750,7 @@ public class Messages {
ArrayList<Object> args = (ArrayList<Object>) message;
SourceSpecification sourceArg = (SourceSpecification) args.get(0);
ImageSelectionOptions optionsArg = (ImageSelectionOptions) args.get(1);
Boolean allowMultipleArg = (Boolean) args.get(2);
Boolean usePhotoPickerArg = (Boolean) args.get(3);
GeneralOptions generalOptionsArg = (GeneralOptions) args.get(2);
Result<List<String>> resultCallback =
new Result<List<String>>() {
public void success(List<String> result) {
@ -622,8 +764,7 @@ public class Messages {
}
};
api.pickImages(
sourceArg, optionsArg, allowMultipleArg, usePhotoPickerArg, resultCallback);
api.pickImages(sourceArg, optionsArg, generalOptionsArg, resultCallback);
});
} else {
channel.setMessageHandler(null);
@ -644,8 +785,7 @@ public class Messages {
ArrayList<Object> args = (ArrayList<Object>) message;
SourceSpecification sourceArg = (SourceSpecification) args.get(0);
VideoSelectionOptions optionsArg = (VideoSelectionOptions) args.get(1);
Boolean allowMultipleArg = (Boolean) args.get(2);
Boolean usePhotoPickerArg = (Boolean) args.get(3);
GeneralOptions generalOptionsArg = (GeneralOptions) args.get(2);
Result<List<String>> resultCallback =
new Result<List<String>>() {
public void success(List<String> result) {
@ -659,8 +799,38 @@ public class Messages {
}
};
api.pickVideos(
sourceArg, optionsArg, allowMultipleArg, usePhotoPickerArg, resultCallback);
api.pickVideos(sourceArg, optionsArg, generalOptionsArg, resultCallback);
});
} else {
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger, "dev.flutter.pigeon.ImagePickerApi.pickMedia", getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
ArrayList<Object> args = (ArrayList<Object>) message;
MediaSelectionOptions mediaSelectionOptionsArg =
(MediaSelectionOptions) args.get(0);
GeneralOptions generalOptionsArg = (GeneralOptions) args.get(1);
Result<List<String>> resultCallback =
new Result<List<String>>() {
public void success(List<String> result) {
wrapped.add(0, result);
reply.reply(wrapped);
}
public void error(Throwable error) {
ArrayList<Object> wrappedError = wrapError(error);
reply.reply(wrappedError);
}
};
api.pickMedia(mediaSelectionOptionsArg, generalOptionsArg, resultCallback);
});
} else {
channel.setMessageHandler(null);

View File

@ -29,7 +29,9 @@ import android.content.pm.PackageManager;
import android.net.Uri;
import androidx.annotation.Nullable;
import io.flutter.plugins.imagepicker.Messages.FlutterError;
import io.flutter.plugins.imagepicker.Messages.GeneralOptions;
import io.flutter.plugins.imagepicker.Messages.ImageSelectionOptions;
import io.flutter.plugins.imagepicker.Messages.MediaSelectionOptions;
import io.flutter.plugins.imagepicker.Messages.VideoSelectionOptions;
import java.io.File;
import java.io.IOException;
@ -61,6 +63,8 @@ public class ImagePickerDelegateTest {
new ImageSelectionOptions.Builder().setQuality((long) 100).setMaxWidth(WIDTH).build();
private static final VideoSelectionOptions DEFAULT_VIDEO_OPTIONS =
new VideoSelectionOptions.Builder().build();
private static final MediaSelectionOptions DEFAULT_MEDIA_OPTIONS =
new MediaSelectionOptions.Builder().setImageSelectionOptions(DEFAULT_IMAGE_OPTIONS).build();
@Mock Activity mockActivity;
@Mock ImageResizer mockImageResizer;
@ -161,6 +165,18 @@ public class ImagePickerDelegateTest {
verifyNoMoreInteractions(mockResult);
}
@Test
public void chooseMediaFromGallery_whenPendingResultExists_finishesWithAlreadyActiveError() {
ImagePickerDelegate delegate =
createDelegateWithPendingResultAndOptions(DEFAULT_IMAGE_OPTIONS, null);
GeneralOptions generalOptions =
new GeneralOptions.Builder().setAllowMultiple(true).setUsePhotoPicker(true).build();
delegate.chooseMediaFromGallery(DEFAULT_MEDIA_OPTIONS, generalOptions, mockResult);
verifyFinishedWithAlreadyActiveError();
verifyNoMoreInteractions(mockResult);
}
@Test
@Config(sdk = 30)
public void chooseImageFromGallery_launchesChooseFromGalleryIntent() {
@ -631,6 +647,19 @@ public class ImagePickerDelegateTest {
assertTrue(isHandled);
}
@Test
public void onActivityResult_whenMediaPickedFromGallery_returnsTrue() {
ImagePickerDelegate delegate = createDelegate();
boolean isHandled =
delegate.onActivityResult(
ImagePickerDelegate.REQUEST_CODE_CHOOSE_MEDIA_FROM_GALLERY,
Activity.RESULT_OK,
mockIntent);
assertTrue(isHandled);
}
@Test
public void onActivityResult_whenVideoPickerFromGallery_returnsTrue() {
ImagePickerDelegate delegate = createDelegate();

View File

@ -24,7 +24,9 @@ import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugins.imagepicker.Messages.FlutterError;
import io.flutter.plugins.imagepicker.Messages.GeneralOptions;
import io.flutter.plugins.imagepicker.Messages.ImageSelectionOptions;
import io.flutter.plugins.imagepicker.Messages.MediaSelectionOptions;
import io.flutter.plugins.imagepicker.Messages.SourceSpecification;
import io.flutter.plugins.imagepicker.Messages.VideoSelectionOptions;
import java.util.List;
@ -40,6 +42,16 @@ public class ImagePickerPluginTest {
new ImageSelectionOptions.Builder().setQuality((long) 100).build();
private static final VideoSelectionOptions DEFAULT_VIDEO_OPTIONS =
new VideoSelectionOptions.Builder().build();
private static final MediaSelectionOptions DEFAULT_MEDIA_OPTIONS =
new MediaSelectionOptions.Builder().setImageSelectionOptions(DEFAULT_IMAGE_OPTIONS).build();
private static final GeneralOptions GENERAL_OPTIONS_ALLOW_MULTIPLE_USE_PHOTO_PICKER =
new GeneralOptions.Builder().setUsePhotoPicker(true).setAllowMultiple(true).build();
private static final GeneralOptions GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_USE_PHOTO_PICKER =
new GeneralOptions.Builder().setUsePhotoPicker(true).setAllowMultiple(false).build();
private static final GeneralOptions GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER =
new GeneralOptions.Builder().setUsePhotoPicker(false).setAllowMultiple(false).build();
private static final GeneralOptions GENERAL_OPTIONS_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER =
new GeneralOptions.Builder().setUsePhotoPicker(false).setAllowMultiple(true).build();
private static final SourceSpecification SOURCE_GALLERY =
new SourceSpecification.Builder().setType(Messages.SourceType.GALLERY).build();
private static final SourceSpecification SOURCE_CAMERA_FRONT =
@ -88,7 +100,10 @@ public class ImagePickerPluginTest {
ImagePickerPlugin imagePickerPluginWithNullActivity =
new ImagePickerPlugin(mockImagePickerDelegate, null);
imagePickerPluginWithNullActivity.pickImages(
SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, false, false, mockResult);
SOURCE_GALLERY,
DEFAULT_IMAGE_OPTIONS,
GENERAL_OPTIONS_ALLOW_MULTIPLE_USE_PHOTO_PICKER,
mockResult);
ArgumentCaptor<FlutterError> errorCaptor = ArgumentCaptor.forClass(FlutterError.class);
verify(mockResult).error(errorCaptor.capture());
@ -103,7 +118,10 @@ public class ImagePickerPluginTest {
ImagePickerPlugin imagePickerPluginWithNullActivity =
new ImagePickerPlugin(mockImagePickerDelegate, null);
imagePickerPluginWithNullActivity.pickVideos(
SOURCE_CAMERA_REAR, DEFAULT_VIDEO_OPTIONS, false, false, mockResult);
SOURCE_CAMERA_REAR,
DEFAULT_VIDEO_OPTIONS,
GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER,
mockResult);
ArgumentCaptor<FlutterError> errorCaptor = ArgumentCaptor.forClass(FlutterError.class);
verify(mockResult).error(errorCaptor.capture());
@ -126,60 +144,126 @@ public class ImagePickerPluginTest {
@Test
public void pickImages_whenSourceIsGallery_invokesChooseImageFromGallery() {
plugin.pickImages(SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, false, false, mockResult);
plugin.pickImages(
SOURCE_GALLERY,
DEFAULT_IMAGE_OPTIONS,
GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER,
mockResult);
verify(mockImagePickerDelegate).chooseImageFromGallery(any(), eq(false), any());
verifyNoInteractions(mockResult);
}
@Test
public void pickImages_whenSourceIsGalleryUsingPhotoPicker_invokesChooseImageFromGallery() {
plugin.pickImages(SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, false, true, mockResult);
plugin.pickImages(
SOURCE_GALLERY,
DEFAULT_IMAGE_OPTIONS,
GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_USE_PHOTO_PICKER,
mockResult);
verify(mockImagePickerDelegate).chooseImageFromGallery(any(), eq(true), any());
verifyNoInteractions(mockResult);
}
@Test
public void pickImages_invokesChooseMultiImageFromGallery() {
plugin.pickImages(SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, true, false, mockResult);
plugin.pickImages(
SOURCE_GALLERY,
DEFAULT_IMAGE_OPTIONS,
GENERAL_OPTIONS_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER,
mockResult);
verify(mockImagePickerDelegate).chooseMultiImageFromGallery(any(), eq(false), any());
verifyNoInteractions(mockResult);
}
@Test
public void pickImages_usingPhotoPicker_invokesChooseMultiImageFromGallery() {
plugin.pickImages(SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, true, true, mockResult);
plugin.pickImages(
SOURCE_GALLERY,
DEFAULT_IMAGE_OPTIONS,
GENERAL_OPTIONS_ALLOW_MULTIPLE_USE_PHOTO_PICKER,
mockResult);
verify(mockImagePickerDelegate).chooseMultiImageFromGallery(any(), eq(true), any());
verifyNoInteractions(mockResult);
}
@Test
public void pickMedia_invokesChooseMediaFromGallery() {
MediaSelectionOptions mediaSelectionOptions =
new MediaSelectionOptions.Builder().setImageSelectionOptions(DEFAULT_IMAGE_OPTIONS).build();
plugin.pickMedia(
mediaSelectionOptions,
GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER,
mockResult);
verify(mockImagePickerDelegate)
.chooseMediaFromGallery(
eq(mediaSelectionOptions),
eq(GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER),
any());
verifyNoInteractions(mockResult);
}
@Test
public void pickMedia_usingPhotoPicker_invokesChooseMediaFromGallery() {
MediaSelectionOptions mediaSelectionOptions =
new MediaSelectionOptions.Builder().setImageSelectionOptions(DEFAULT_IMAGE_OPTIONS).build();
plugin.pickMedia(
mediaSelectionOptions, GENERAL_OPTIONS_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, mockResult);
verify(mockImagePickerDelegate)
.chooseMediaFromGallery(
eq(mediaSelectionOptions),
eq(GENERAL_OPTIONS_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER),
any());
verifyNoInteractions(mockResult);
}
@Test
public void pickImages_whenSourceIsCamera_invokesTakeImageWithCamera() {
plugin.pickImages(SOURCE_CAMERA_REAR, DEFAULT_IMAGE_OPTIONS, false, false, mockResult);
plugin.pickImages(
SOURCE_CAMERA_REAR,
DEFAULT_IMAGE_OPTIONS,
GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER,
mockResult);
verify(mockImagePickerDelegate).takeImageWithCamera(any(), any());
verifyNoInteractions(mockResult);
}
@Test
public void pickImages_whenSourceIsCamera_invokesTakeImageWithCamera_RearCamera() {
plugin.pickImages(SOURCE_CAMERA_REAR, DEFAULT_IMAGE_OPTIONS, false, false, mockResult);
plugin.pickImages(
SOURCE_CAMERA_REAR,
DEFAULT_IMAGE_OPTIONS,
GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER,
mockResult);
verify(mockImagePickerDelegate).setCameraDevice(eq(ImagePickerDelegate.CameraDevice.REAR));
}
@Test
public void pickImages_whenSourceIsCamera_invokesTakeImageWithCamera_FrontCamera() {
plugin.pickImages(SOURCE_CAMERA_FRONT, DEFAULT_IMAGE_OPTIONS, false, false, mockResult);
plugin.pickImages(
SOURCE_CAMERA_FRONT,
DEFAULT_IMAGE_OPTIONS,
GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER,
mockResult);
verify(mockImagePickerDelegate).setCameraDevice(eq(ImagePickerDelegate.CameraDevice.FRONT));
}
@Test
public void pickVideos_whenSourceIsCamera_invokesTakeImageWithCamera_RearCamera() {
plugin.pickVideos(SOURCE_CAMERA_REAR, DEFAULT_VIDEO_OPTIONS, false, false, mockResult);
plugin.pickVideos(
SOURCE_CAMERA_REAR,
DEFAULT_VIDEO_OPTIONS,
GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER,
mockResult);
verify(mockImagePickerDelegate).setCameraDevice(eq(ImagePickerDelegate.CameraDevice.REAR));
}
@Test
public void pickVideos_whenSourceIsCamera_invokesTakeImageWithCamera_FrontCamera() {
plugin.pickVideos(SOURCE_CAMERA_FRONT, DEFAULT_VIDEO_OPTIONS, false, false, mockResult);
plugin.pickVideos(
SOURCE_CAMERA_FRONT,
DEFAULT_VIDEO_OPTIONS,
GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER,
mockResult);
verify(mockImagePickerDelegate).setCameraDevice(eq(ImagePickerDelegate.CameraDevice.FRONT));
}

View File

@ -14,6 +14,7 @@ import 'package:flutter_driver/driver_extension.dart';
import 'package:image_picker_android/image_picker_android.dart';
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
// #enddocregion photo-picker-example
import 'package:mime/mime.dart';
import 'package:video_player/video_player.dart';
void appMain() {
@ -55,14 +56,14 @@ class MyHomePage extends StatefulWidget {
}
class _MyHomePageState extends State<MyHomePage> {
List<XFile>? _imageFileList;
List<XFile>? _mediaFileList;
void _setImageFileListFromFile(XFile? value) {
_imageFileList = value == null ? null : <XFile>[value];
_mediaFileList = value == null ? null : <XFile>[value];
}
dynamic _pickImageError;
bool isVideo = false;
bool _isVideo = false;
VideoPlayerController? _controller;
VideoPlayerController? _toBeDisposed;
@ -77,18 +78,10 @@ class _MyHomePageState extends State<MyHomePage> {
if (file != null && mounted) {
await _disposeVideoController();
late VideoPlayerController controller;
if (kIsWeb) {
controller = VideoPlayerController.network(file.path);
} else {
controller = VideoPlayerController.file(File(file.path));
}
controller = VideoPlayerController.file(File(file.path));
_controller = controller;
// In web, most browsers won't honor a programmatic call to .play
// if the video has a sound track (and is not muted).
// Mute the video so it auto-plays in web!
// This is not needed if the call to .play is the result of user
// interaction (clicking on a "play" button, for example).
const double volume = kIsWeb ? 0.0 : 1.0;
const double volume = 1.0;
await controller.setVolume(volume);
await controller.initialize();
await controller.setLooping(true);
@ -101,12 +94,13 @@ class _MyHomePageState extends State<MyHomePage> {
ImageSource source, {
required BuildContext context,
bool isMultiImage = false,
bool isMedia = false,
}) async {
if (_controller != null) {
await _controller!.setVolume(0.0);
}
if (context.mounted) {
if (isVideo) {
if (_isVideo) {
final XFile? file = await _picker.getVideo(
source: source, maxDuration: const Duration(seconds: 10));
if (file != null && context.mounted) {
@ -117,15 +111,54 @@ class _MyHomePageState extends State<MyHomePage> {
await _displayPickImageDialog(context,
(double? maxWidth, double? maxHeight, int? quality) async {
try {
final List<XFile>? pickedFileList = await _picker.getMultiImage(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
);
final List<XFile>? pickedFileList = isMedia
? await _picker.getMedia(
options: MediaOptions(
allowMultiple: isMultiImage,
imageOptions: ImageOptions(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
)),
)
: await _picker.getMultiImage(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
);
if (pickedFileList != null && context.mounted) {
_showPickedSnackBar(context, pickedFileList);
}
setState(() => _imageFileList = pickedFileList);
setState(() {
_mediaFileList = pickedFileList;
});
} catch (e) {
setState(() {
_pickImageError = e;
});
}
});
} else if (isMedia) {
await _displayPickImageDialog(context,
(double? maxWidth, double? maxHeight, int? quality) async {
try {
final List<XFile> pickedFileList = <XFile>[];
final XFile? media = _firstOrNull(await _picker.getMedia(
options: MediaOptions(
allowMultiple: isMultiImage,
imageOptions: ImageOptions(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
)),
));
if (media != null) {
pickedFileList.add(media);
setState(() {
_mediaFileList = pickedFileList;
});
}
} catch (e) {
setState(() => _pickImageError = e);
}
@ -200,30 +233,37 @@ class _MyHomePageState extends State<MyHomePage> {
if (retrieveError != null) {
return retrieveError;
}
if (_imageFileList != null) {
if (_mediaFileList != null) {
return Semantics(
label: 'image_picker_example_picked_images',
child: ListView.builder(
key: UniqueKey(),
itemBuilder: (BuildContext context, int index) {
final XFile image = _imageFileList![index];
final XFile image = _mediaFileList![index];
final String? mime = lookupMimeType(_mediaFileList![index].path);
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(image.name,
key: const Key('image_picker_example_picked_image_name')),
// Why network for web?
// See https://pub.dev/packages/image_picker#getting-ready-for-the-web-platform
Semantics(
label: 'image_picker_example_picked_image',
child: kIsWeb
? Image.network(image.path)
: Image.file(File(image.path)),
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: _imageFileList!.length,
itemCount: _mediaFileList!.length,
),
);
} else if (_pickImageError != null) {
@ -239,8 +279,19 @@ class _MyHomePageState extends State<MyHomePage> {
}
}
Widget _buildInlineVideoPlayer(int index) {
final VideoPlayerController controller =
VideoPlayerController.file(File(_mediaFileList![index].path));
const double volume = 1.0;
controller.setVolume(volume);
controller.initialize();
controller.setLooping(true);
controller.play();
return Center(child: AspectRatioVideo(controller));
}
Widget _handlePreview() {
if (isVideo) {
if (_isVideo) {
return _previewVideo();
} else {
return _previewImages();
@ -254,15 +305,15 @@ class _MyHomePageState extends State<MyHomePage> {
}
if (response.file != null) {
if (response.type == RetrieveType.video) {
isVideo = true;
_isVideo = true;
await _playVideo(response.file);
} else {
isVideo = false;
_isVideo = false;
setState(() {
if (response.files == null) {
_setImageFileListFromFile(response.file);
} else {
_imageFileList = response.files;
_mediaFileList = response.files;
}
});
}
@ -316,7 +367,7 @@ class _MyHomePageState extends State<MyHomePage> {
child: FloatingActionButton(
key: const Key('image_picker_example_from_gallery'),
onPressed: () {
isVideo = false;
_isVideo = false;
_onImageButtonPressed(ImageSource.gallery, context: context);
},
heroTag: 'image0',
@ -328,7 +379,40 @@ class _MyHomePageState extends State<MyHomePage> {
padding: const EdgeInsets.only(top: 16.0),
child: FloatingActionButton(
onPressed: () {
isVideo = false;
_isVideo = false;
_onImageButtonPressed(
ImageSource.gallery,
context: context,
isMultiImage: true,
isMedia: true,
);
},
heroTag: 'multipleMedia',
tooltip: 'Pick Multiple Media from gallery',
child: const Icon(Icons.photo_library),
),
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: FloatingActionButton(
onPressed: () {
_isVideo = false;
_onImageButtonPressed(
ImageSource.gallery,
context: context,
isMedia: true,
);
},
heroTag: 'media',
tooltip: 'Pick Single Media from gallery',
child: const Icon(Icons.photo_library),
),
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: FloatingActionButton(
onPressed: () {
_isVideo = false;
_onImageButtonPressed(
ImageSource.gallery,
context: context,
@ -344,7 +428,7 @@ class _MyHomePageState extends State<MyHomePage> {
padding: const EdgeInsets.only(top: 16.0),
child: FloatingActionButton(
onPressed: () {
isVideo = false;
_isVideo = false;
_onImageButtonPressed(ImageSource.camera, context: context);
},
heroTag: 'image2',
@ -357,7 +441,7 @@ class _MyHomePageState extends State<MyHomePage> {
child: FloatingActionButton(
backgroundColor: Colors.red,
onPressed: () {
isVideo = true;
_isVideo = true;
_onImageButtonPressed(ImageSource.gallery, context: context);
},
heroTag: 'video0',
@ -370,7 +454,7 @@ class _MyHomePageState extends State<MyHomePage> {
child: FloatingActionButton(
backgroundColor: Colors.red,
onPressed: () {
isVideo = true;
_isVideo = true;
_onImageButtonPressed(ImageSource.camera, context: context);
},
heroTag: 'video1',
@ -510,3 +594,7 @@ class AspectRatioVideoState extends State<AspectRatioVideo> {
}
}
}
T? _firstOrNull<T>(List<T> list) {
return list.isEmpty ? null : list.first;
}

View File

@ -19,7 +19,8 @@ dependencies:
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ../
image_picker_platform_interface: ^2.3.0
image_picker_platform_interface: ^2.8.0
mime: ^1.0.4
video_player: ^2.1.4
dev_dependencies:

View File

@ -11,12 +11,17 @@ import 'src/messages.g.dart';
/// An Android implementation of [ImagePickerPlatform].
class ImagePickerAndroid extends ImagePickerPlatform {
/// Creates a new plugin implemenation instance.
/// Creates a new plugin implementation instance.
ImagePickerAndroid({@visibleForTesting ImagePickerApi? api})
: _hostApi = api ?? ImagePickerApi();
final ImagePickerApi _hostApi;
/// Sets [ImagePickerAndroid] to use Android 13 Photo Picker.
///
/// Currently defaults to false, but the default is subject to change.
bool useAndroidPhotoPicker = false;
/// Registers this class as the default platform implementation.
static void registerWith() {
ImagePickerPlatform.instance = ImagePickerAndroid();
@ -77,13 +82,14 @@ class ImagePickerAndroid extends ImagePickerPlatform {
}
return _hostApi.pickImages(
SourceSpecification(type: SourceType.gallery),
ImageSelectionOptions(
maxWidth: maxWidth,
maxHeight: maxHeight,
quality: imageQuality ?? 100),
/* allowMultiple */ true,
useAndroidPhotoPicker);
SourceSpecification(type: SourceType.gallery),
ImageSelectionOptions(
maxWidth: maxWidth,
maxHeight: maxHeight,
quality: imageQuality ?? 100),
GeneralOptions(
allowMultiple: true, usePhotoPicker: useAndroidPhotoPicker),
);
}
Future<String?> _getImagePath({
@ -108,13 +114,16 @@ class ImagePickerAndroid extends ImagePickerPlatform {
}
final List<String?> paths = await _hostApi.pickImages(
_buildSourceSpec(source, preferredCameraDevice),
ImageSelectionOptions(
maxWidth: maxWidth,
maxHeight: maxHeight,
quality: imageQuality ?? 100),
/* allowMultiple */ false,
useAndroidPhotoPicker);
_buildSourceSpec(source, preferredCameraDevice),
ImageSelectionOptions(
maxWidth: maxWidth,
maxHeight: maxHeight,
quality: imageQuality ?? 100),
GeneralOptions(
allowMultiple: false,
usePhotoPicker: useAndroidPhotoPicker,
),
);
return paths.isEmpty ? null : paths.first;
}
@ -138,10 +147,13 @@ class ImagePickerAndroid extends ImagePickerPlatform {
Duration? maxDuration,
}) async {
final List<String?> paths = await _hostApi.pickVideos(
_buildSourceSpec(source, preferredCameraDevice),
VideoSelectionOptions(maxDurationSeconds: maxDuration?.inSeconds),
/* allowMultiple */ false,
useAndroidPhotoPicker);
_buildSourceSpec(source, preferredCameraDevice),
VideoSelectionOptions(maxDurationSeconds: maxDuration?.inSeconds),
GeneralOptions(
allowMultiple: false,
usePhotoPicker: useAndroidPhotoPicker,
),
);
return paths.isEmpty ? null : paths.first;
}
@ -197,6 +209,21 @@ class ImagePickerAndroid extends ImagePickerPlatform {
return paths.map((dynamic path) => XFile(path as String)).toList();
}
@override
Future<List<XFile>> getMedia({
required MediaOptions options,
}) async {
return (await _hostApi.pickMedia(
_mediaOptionsToMediaSelectionOptions(options),
GeneralOptions(
allowMultiple: options.allowMultiple,
usePhotoPicker: useAndroidPhotoPicker,
),
))
.map((String? path) => XFile(path!))
.toList();
}
@override
Future<XFile?> getVideo({
required ImageSource source,
@ -211,6 +238,38 @@ class ImagePickerAndroid extends ImagePickerPlatform {
return path != null ? XFile(path) : null;
}
MediaSelectionOptions _mediaOptionsToMediaSelectionOptions(
MediaOptions mediaOptions) {
final ImageSelectionOptions imageSelectionOptions =
_imageOptionsToImageSelectionOptionsWithValidator(
mediaOptions.imageOptions);
return MediaSelectionOptions(
imageSelectionOptions: imageSelectionOptions,
);
}
ImageSelectionOptions _imageOptionsToImageSelectionOptionsWithValidator(
ImageOptions? imageOptions) {
final double? maxHeight = imageOptions?.maxHeight;
final double? maxWidth = imageOptions?.maxWidth;
final int? imageQuality = imageOptions?.imageQuality;
if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) {
throw ArgumentError.value(
imageQuality, 'imageQuality', 'must be between 0 and 100');
}
if (maxWidth != null && maxWidth < 0) {
throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative');
}
if (maxHeight != null && maxHeight < 0) {
throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative');
}
return ImageSelectionOptions(
quality: imageQuality ?? 100, maxHeight: maxHeight, maxWidth: maxWidth);
}
@override
Future<LostData> retrieveLostData() async {
final LostDataResponse result = await getLostData();
@ -243,7 +302,7 @@ class ImagePickerAndroid extends ImagePickerPlatform {
: PlatformException(code: error.code, message: error.message);
// Entries are guaranteed not to be null, even though that's not currently
// expressable in Pigeon.
// expressible in Pigeon.
final List<XFile> pickedFileList =
result.paths.map((String? path) => XFile(path!)).toList();
@ -309,9 +368,4 @@ class ImagePickerAndroid extends ImagePickerPlatform {
// ignore: dead_code
return RetrieveType.image;
}
/// Sets [ImagePickerAndroid] to use Android 13 Photo Picker.
///
/// Currently defaults to false, but the default is subject to change.
bool useAndroidPhotoPicker = false;
}

View File

@ -26,6 +26,32 @@ enum CacheRetrievalType {
video,
}
class GeneralOptions {
GeneralOptions({
required this.allowMultiple,
required this.usePhotoPicker,
});
bool allowMultiple;
bool usePhotoPicker;
Object encode() {
return <Object?>[
allowMultiple,
usePhotoPicker,
];
}
static GeneralOptions decode(Object result) {
result as List<Object?>;
return GeneralOptions(
allowMultiple: result[0]! as bool,
usePhotoPicker: result[1]! as bool,
);
}
}
/// Options for image selection and output.
class ImageSelectionOptions {
ImageSelectionOptions({
@ -63,6 +89,28 @@ class ImageSelectionOptions {
}
}
class MediaSelectionOptions {
MediaSelectionOptions({
required this.imageSelectionOptions,
});
ImageSelectionOptions imageSelectionOptions;
Object encode() {
return <Object?>[
imageSelectionOptions.encode(),
];
}
static MediaSelectionOptions decode(Object result) {
result as List<Object?>;
return MediaSelectionOptions(
imageSelectionOptions:
ImageSelectionOptions.decode(result[0]! as List<Object?>),
);
}
}
/// Options for image selection and output.
class VideoSelectionOptions {
VideoSelectionOptions({
@ -192,15 +240,21 @@ class _ImagePickerApiCodec extends StandardMessageCodec {
} else if (value is CacheRetrievalResult) {
buffer.putUint8(129);
writeValue(buffer, value.encode());
} else if (value is ImageSelectionOptions) {
} else if (value is GeneralOptions) {
buffer.putUint8(130);
writeValue(buffer, value.encode());
} else if (value is SourceSpecification) {
} else if (value is ImageSelectionOptions) {
buffer.putUint8(131);
writeValue(buffer, value.encode());
} else if (value is VideoSelectionOptions) {
} else if (value is MediaSelectionOptions) {
buffer.putUint8(132);
writeValue(buffer, value.encode());
} else if (value is SourceSpecification) {
buffer.putUint8(133);
writeValue(buffer, value.encode());
} else if (value is VideoSelectionOptions) {
buffer.putUint8(134);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
}
@ -214,10 +268,14 @@ class _ImagePickerApiCodec extends StandardMessageCodec {
case 129:
return CacheRetrievalResult.decode(readValue(buffer)!);
case 130:
return ImageSelectionOptions.decode(readValue(buffer)!);
return GeneralOptions.decode(readValue(buffer)!);
case 131:
return SourceSpecification.decode(readValue(buffer)!);
return ImageSelectionOptions.decode(readValue(buffer)!);
case 132:
return MediaSelectionOptions.decode(readValue(buffer)!);
case 133:
return SourceSpecification.decode(readValue(buffer)!);
case 134:
return VideoSelectionOptions.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
@ -242,17 +300,13 @@ class ImagePickerApi {
Future<List<String?>> pickImages(
SourceSpecification arg_source,
ImageSelectionOptions arg_options,
bool arg_allowMultiple,
bool arg_usePhotoPicker) async {
GeneralOptions arg_generalOptions) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.ImagePickerApi.pickImages', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel.send(<Object?>[
arg_source,
arg_options,
arg_allowMultiple,
arg_usePhotoPicker
]) as List<Object?>?;
final List<Object?>? replyList = await channel
.send(<Object?>[arg_source, arg_options, arg_generalOptions])
as List<Object?>?;
if (replyList == null) {
throw PlatformException(
code: 'channel-error',
@ -281,17 +335,47 @@ class ImagePickerApi {
Future<List<String?>> pickVideos(
SourceSpecification arg_source,
VideoSelectionOptions arg_options,
bool arg_allowMultiple,
bool arg_usePhotoPicker) async {
GeneralOptions arg_generalOptions) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.ImagePickerApi.pickVideos', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel.send(<Object?>[
arg_source,
arg_options,
arg_allowMultiple,
arg_usePhotoPicker
]) as List<Object?>?;
final List<Object?>? replyList = await channel
.send(<Object?>[arg_source, arg_options, arg_generalOptions])
as List<Object?>?;
if (replyList == null) {
throw PlatformException(
code: 'channel-error',
message: 'Unable to establish connection on channel.',
);
} else if (replyList.length > 1) {
throw PlatformException(
code: replyList[0]! as String,
message: replyList[1] as String?,
details: replyList[2],
);
} else if (replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (replyList[0] as List<Object?>?)!.cast<String?>();
}
}
/// Selects images and videos and returns their paths.
///
/// Elements must not be null, by convention. See
/// https://github.com/flutter/flutter/issues/97848
Future<List<String?>> pickMedia(
MediaSelectionOptions arg_mediaSelectionOptions,
GeneralOptions arg_generalOptions) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.ImagePickerApi.pickMedia', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel
.send(<Object?>[arg_mediaSelectionOptions, arg_generalOptions])
as List<Object?>?;
if (replyList == null) {
throw PlatformException(
code: 'channel-error',

View File

@ -13,6 +13,11 @@ import 'package:pigeon/pigeon.dart';
),
copyrightHeader: 'pigeons/copyright.txt',
))
class GeneralOptions {
GeneralOptions(this.allowMultiple, this.usePhotoPicker);
bool allowMultiple;
bool usePhotoPicker;
}
/// Options for image selection and output.
class ImageSelectionOptions {
@ -30,6 +35,14 @@ class ImageSelectionOptions {
int quality;
}
class MediaSelectionOptions {
MediaSelectionOptions({
required this.imageSelectionOptions,
});
ImageSelectionOptions imageSelectionOptions;
}
/// Options for image selection and output.
class VideoSelectionOptions {
VideoSelectionOptions({this.maxDurationSeconds});
@ -89,8 +102,11 @@ abstract class ImagePickerApi {
/// https://github.com/flutter/flutter/issues/97848
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
@async
List<String?> pickImages(SourceSpecification source,
ImageSelectionOptions options, bool allowMultiple, bool usePhotoPicker);
List<String?> pickImages(
SourceSpecification source,
ImageSelectionOptions options,
GeneralOptions generalOptions,
);
/// Selects video and returns their paths.
///
@ -98,8 +114,21 @@ abstract class ImagePickerApi {
/// https://github.com/flutter/flutter/issues/97848
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
@async
List<String?> pickVideos(SourceSpecification source,
VideoSelectionOptions options, bool allowMultiple, bool usePhotoPicker);
List<String?> pickVideos(
SourceSpecification source,
VideoSelectionOptions options,
GeneralOptions generalOptions,
);
/// Selects images and videos and returns their paths.
///
/// Elements must not be null, by convention. See
/// https://github.com/flutter/flutter/issues/97848
@async
List<String?> pickMedia(
MediaSelectionOptions mediaSelectionOptions,
GeneralOptions generalOptions,
);
/// Returns results from a previous app session, if any.
@TaskQueue(type: TaskQueueType.serialBackgroundThread)

View File

@ -3,7 +3,7 @@ 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.6+20
version: 0.8.7
environment:
sdk: ">=2.18.0 <4.0.0"
@ -22,7 +22,7 @@ dependencies:
flutter:
sdk: flutter
flutter_plugin_android_lifecycle: ^2.0.1
image_picker_platform_interface: ^2.5.0
image_picker_platform_interface: ^2.8.0
dev_dependencies:
flutter_test:

View File

@ -654,6 +654,129 @@ void main() {
});
});
group('#getMedia', () {
test('calls the method correctly', () async {
const List<String> fakePaths = <String>['/foo.jgp', 'bar.jpg'];
api.returnValue = fakePaths;
final List<XFile> files = await picker.getMedia(
options: const MediaOptions(
allowMultiple: true,
),
);
expect(api.lastCall, _LastPickType.image);
expect(files.length, 2);
expect(files[0].path, fakePaths[0]);
expect(files[1].path, fakePaths[1]);
});
test('passes default image options', () async {
await picker.getMedia(
options: const MediaOptions(
allowMultiple: true,
),
);
expect(api.passedImageOptions?.maxWidth, null);
expect(api.passedImageOptions?.maxHeight, null);
expect(api.passedImageOptions?.quality, 100);
});
test('passes image option arguments correctly', () async {
await picker.getMedia(
options: const MediaOptions(
allowMultiple: true,
imageOptions: ImageOptions(
maxWidth: 10.0,
maxHeight: 20.0,
imageQuality: 70,
),
));
expect(api.passedImageOptions?.maxWidth, 10.0);
expect(api.passedImageOptions?.maxHeight, 20.0);
expect(api.passedImageOptions?.quality, 70);
});
test('does not accept a negative width or height argument', () {
expect(
() => picker.getMedia(
options: const MediaOptions(
allowMultiple: true,
imageOptions: ImageOptions(maxWidth: -1.0),
),
),
throwsArgumentError,
);
expect(
() => picker.getMedia(
options: const MediaOptions(
allowMultiple: true,
imageOptions: ImageOptions(maxHeight: -1.0),
),
),
throwsArgumentError,
);
});
test('does not accept an invalid imageQuality argument', () {
expect(
() => picker.getMedia(
options: const MediaOptions(
allowMultiple: true,
imageOptions: ImageOptions(imageQuality: -1),
),
),
throwsArgumentError,
);
expect(
() => picker.getMedia(
options: const MediaOptions(
allowMultiple: true,
imageOptions: ImageOptions(imageQuality: 101),
),
),
throwsArgumentError,
);
});
test('handles an empty path response gracefully', () async {
api.returnValue = <String>[];
expect(
await picker.getMedia(
options: const MediaOptions(
allowMultiple: true,
),
),
<String>[]);
});
test('defaults to not using Android Photo Picker', () async {
await picker.getMedia(
options: const MediaOptions(
allowMultiple: true,
),
);
expect(api.passedPhotoPickerFlag, false);
});
test('allows using Android Photo Picker', () async {
picker.useAndroidPhotoPicker = true;
await picker.getMedia(
options: const MediaOptions(
allowMultiple: true,
),
);
expect(api.passedPhotoPickerFlag, true);
});
});
group('#getImageFromSource', () {
test('calls the method correctly', () async {
const String fakePath = '/foo.jpg';
@ -807,29 +930,41 @@ class _FakeImagePickerApi implements ImagePickerApi {
@override
Future<List<String?>> pickImages(
SourceSpecification source,
ImageSelectionOptions options,
bool allowMultiple,
bool usePhotoPicker) async {
SourceSpecification source,
ImageSelectionOptions options,
GeneralOptions generalOptions,
) async {
lastCall = _LastPickType.image;
passedSource = source;
passedImageOptions = options;
passedAllowMultiple = allowMultiple;
passedPhotoPickerFlag = usePhotoPicker;
passedAllowMultiple = generalOptions.allowMultiple;
passedPhotoPickerFlag = generalOptions.usePhotoPicker;
return returnValue as List<String?>? ?? <String>[];
}
@override
Future<List<String?>> pickMedia(
MediaSelectionOptions options,
GeneralOptions generalOptions,
) async {
lastCall = _LastPickType.image;
passedImageOptions = options.imageSelectionOptions;
passedPhotoPickerFlag = generalOptions.usePhotoPicker;
passedAllowMultiple = generalOptions.allowMultiple;
return returnValue as List<String?>? ?? <String>[];
}
@override
Future<List<String?>> pickVideos(
SourceSpecification source,
VideoSelectionOptions options,
bool allowMultiple,
bool usePhotoPicker) async {
SourceSpecification source,
VideoSelectionOptions options,
GeneralOptions generalOptions,
) async {
lastCall = _LastPickType.video;
passedSource = source;
passedVideoOptions = options;
passedAllowMultiple = allowMultiple;
passedPhotoPickerFlag = usePhotoPicker;
passedAllowMultiple = generalOptions.allowMultiple;
passedPhotoPickerFlag = generalOptions.usePhotoPicker;
return returnValue as List<String?>? ?? <String>[];
}

View File

@ -23,15 +23,21 @@ class _TestHostImagePickerApiCodec extends StandardMessageCodec {
} else if (value is CacheRetrievalResult) {
buffer.putUint8(129);
writeValue(buffer, value.encode());
} else if (value is ImageSelectionOptions) {
} else if (value is GeneralOptions) {
buffer.putUint8(130);
writeValue(buffer, value.encode());
} else if (value is SourceSpecification) {
} else if (value is ImageSelectionOptions) {
buffer.putUint8(131);
writeValue(buffer, value.encode());
} else if (value is VideoSelectionOptions) {
} else if (value is MediaSelectionOptions) {
buffer.putUint8(132);
writeValue(buffer, value.encode());
} else if (value is SourceSpecification) {
buffer.putUint8(133);
writeValue(buffer, value.encode());
} else if (value is VideoSelectionOptions) {
buffer.putUint8(134);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
}
@ -45,10 +51,14 @@ class _TestHostImagePickerApiCodec extends StandardMessageCodec {
case 129:
return CacheRetrievalResult.decode(readValue(buffer)!);
case 130:
return ImageSelectionOptions.decode(readValue(buffer)!);
return GeneralOptions.decode(readValue(buffer)!);
case 131:
return SourceSpecification.decode(readValue(buffer)!);
return ImageSelectionOptions.decode(readValue(buffer)!);
case 132:
return MediaSelectionOptions.decode(readValue(buffer)!);
case 133:
return SourceSpecification.decode(readValue(buffer)!);
case 134:
return VideoSelectionOptions.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
@ -66,14 +76,21 @@ abstract class TestHostImagePickerApi {
/// Elements must not be null, by convention. See
/// https://github.com/flutter/flutter/issues/97848
Future<List<String?>> pickImages(SourceSpecification source,
ImageSelectionOptions options, bool allowMultiple, bool usePhotoPicker);
ImageSelectionOptions options, GeneralOptions generalOptions);
/// Selects video and returns their paths.
///
/// Elements must not be null, by convention. See
/// https://github.com/flutter/flutter/issues/97848
Future<List<String?>> pickVideos(SourceSpecification source,
VideoSelectionOptions options, bool allowMultiple, bool usePhotoPicker);
VideoSelectionOptions options, GeneralOptions generalOptions);
/// Selects images and videos and returns their paths.
///
/// Elements must not be null, by convention. See
/// https://github.com/flutter/flutter/issues/97848
Future<List<String?>> pickMedia(MediaSelectionOptions mediaSelectionOptions,
GeneralOptions generalOptions);
/// Returns results from a previous app session, if any.
CacheRetrievalResult? retrieveLostResults();
@ -102,14 +119,12 @@ abstract class TestHostImagePickerApi {
(args[1] as ImageSelectionOptions?);
assert(arg_options != null,
'Argument for dev.flutter.pigeon.ImagePickerApi.pickImages was null, expected non-null ImageSelectionOptions.');
final bool? arg_allowMultiple = (args[2] as bool?);
assert(arg_allowMultiple != null,
'Argument for dev.flutter.pigeon.ImagePickerApi.pickImages was null, expected non-null bool.');
final bool? arg_usePhotoPicker = (args[3] as bool?);
assert(arg_usePhotoPicker != null,
'Argument for dev.flutter.pigeon.ImagePickerApi.pickImages was null, expected non-null bool.');
final List<String?> output = await api.pickImages(arg_source!,
arg_options!, arg_allowMultiple!, arg_usePhotoPicker!);
final GeneralOptions? arg_generalOptions =
(args[2] as GeneralOptions?);
assert(arg_generalOptions != null,
'Argument for dev.flutter.pigeon.ImagePickerApi.pickImages was null, expected non-null GeneralOptions.');
final List<String?> output = await api.pickImages(
arg_source!, arg_options!, arg_generalOptions!);
return <Object?>[output];
});
}
@ -136,14 +151,40 @@ abstract class TestHostImagePickerApi {
(args[1] as VideoSelectionOptions?);
assert(arg_options != null,
'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideos was null, expected non-null VideoSelectionOptions.');
final bool? arg_allowMultiple = (args[2] as bool?);
assert(arg_allowMultiple != null,
'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideos was null, expected non-null bool.');
final bool? arg_usePhotoPicker = (args[3] as bool?);
assert(arg_usePhotoPicker != null,
'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideos was null, expected non-null bool.');
final List<String?> output = await api.pickVideos(arg_source!,
arg_options!, arg_allowMultiple!, arg_usePhotoPicker!);
final GeneralOptions? arg_generalOptions =
(args[2] as GeneralOptions?);
assert(arg_generalOptions != null,
'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideos was null, expected non-null GeneralOptions.');
final List<String?> output = await api.pickVideos(
arg_source!, arg_options!, arg_generalOptions!);
return <Object?>[output];
});
}
}
{
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.ImagePickerApi.pickMedia', codec,
binaryMessenger: binaryMessenger);
if (api == null) {
_testBinaryMessengerBinding!.defaultBinaryMessenger
.setMockDecodedMessageHandler<Object?>(channel, null);
} else {
_testBinaryMessengerBinding!.defaultBinaryMessenger
.setMockDecodedMessageHandler<Object?>(channel,
(Object? message) async {
assert(message != null,
'Argument for dev.flutter.pigeon.ImagePickerApi.pickMedia was null.');
final List<Object?> args = (message as List<Object?>?)!;
final MediaSelectionOptions? arg_mediaSelectionOptions =
(args[0] as MediaSelectionOptions?);
assert(arg_mediaSelectionOptions != null,
'Argument for dev.flutter.pigeon.ImagePickerApi.pickMedia was null, expected non-null MediaSelectionOptions.');
final GeneralOptions? arg_generalOptions =
(args[1] as GeneralOptions?);
assert(arg_generalOptions != null,
'Argument for dev.flutter.pigeon.ImagePickerApi.pickMedia was null, expected non-null GeneralOptions.');
final List<String?> output = await api.pickMedia(
arg_mediaSelectionOptions!, arg_generalOptions!);
return <Object?>[output];
});
}

View File

@ -1,5 +1,6 @@
## NEXT
## 2.2.0
* Adds `getMedia` method.
* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18.
## 2.1.12

View File

@ -87,7 +87,8 @@ void main() {
));
});
testWidgets('Can select multiple files', (WidgetTester tester) async {
testWidgets('getMultiImage can select multiple files',
(WidgetTester tester) async {
final html.FileUploadInputElement mockInput = html.FileUploadInputElement();
final ImagePickerPluginTestOverrides overrides =
@ -117,6 +118,38 @@ void main() {
expect(secondFile.length(), completion(secondTextFile.size));
});
testWidgets('getMedia can select multiple files',
(WidgetTester tester) async {
final html.FileUploadInputElement mockInput = html.FileUploadInputElement();
final ImagePickerPluginTestOverrides overrides =
ImagePickerPluginTestOverrides()
..createInputElement = ((_, __) => mockInput)
..getMultipleFilesFromInput =
((_) => <html.File>[textFile, secondTextFile]);
final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides);
// Init the pick file dialog...
final Future<List<XFile>> files =
plugin.getMedia(options: const MediaOptions(allowMultiple: true));
// Mock the browser behavior of selecting a file...
mockInput.dispatchEvent(html.Event('change'));
// Now the file should be available
expect(files, completes);
// And readable
expect((await files).first.readAsBytes(), completion(isNotEmpty));
// Peek into the second file...
final XFile secondFile = (await files).elementAt(1);
expect(secondFile.readAsBytes(), completion(isNotEmpty));
expect(secondFile.name, secondTextFile.name);
expect(secondFile.length(), completion(secondTextFile.size));
});
// There's no good way of detecting when the user has "aborted" the selection.
testWidgets('computeCaptureAttribute', (WidgetTester tester) async {

View File

@ -10,7 +10,7 @@ dependencies:
sdk: flutter
image_picker_for_web:
path: ../
image_picker_platform_interface: ^2.2.0
image_picker_platform_interface: ^2.8.0
dev_dependencies:
flutter_driver:

View File

@ -8,6 +8,7 @@ import 'dart:html' as html;
import 'package:flutter/foundation.dart' show visibleForTesting;
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
import 'package:mime/mime.dart' as mime;
import 'src/image_resizer.dart';
@ -166,7 +167,7 @@ class ImagePickerPlugin extends ImagePickerPlatform {
return files.first;
}
/// Injects a file input, and returns a list of XFile that the user selected locally.
/// Injects a file input, and returns a list of XFile images that the user selected locally.
@override
Future<List<XFile>> getMultiImage({
double? maxWidth,
@ -189,6 +190,30 @@ class ImagePickerPlugin extends ImagePickerPlatform {
return Future.wait<XFile>(resized);
}
/// Injects a file input, and returns a list of XFile media that the user selected locally.
@override
Future<List<XFile>> getMedia({
required MediaOptions options,
}) async {
final List<XFile> images = await getFiles(
accept: '$_kAcceptImageMimeType,$_kAcceptVideoMimeType',
multiple: options.allowMultiple,
);
final Iterable<Future<XFile>> resized = images.map((XFile media) {
if (mime.lookupMimeType(media.path)?.startsWith('image/') ?? false) {
return _imageResizer.resizeImageIfNeeded(
media,
options.imageOptions.maxWidth,
options.imageOptions.maxHeight,
options.imageOptions.imageQuality,
);
}
return Future<XFile>.value(media);
});
return Future.wait<XFile>(resized);
}
/// Injects a file input with the specified accept+capture attributes, and
/// returns a list of XFile that the user selected locally.
///

View File

@ -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: 2.1.12
version: 2.2.0
environment:
sdk: ">=2.18.0 <4.0.0"
@ -21,7 +21,8 @@ dependencies:
sdk: flutter
flutter_web_plugins:
sdk: flutter
image_picker_platform_interface: ^2.2.0
image_picker_platform_interface: ^2.8.0
mime: ^1.0.4
dev_dependencies:
flutter_test:

View File

@ -1,4 +1,9 @@
## 0.8.8
* Adds `getMedia` and `getMultipleMedia` methods.
## 0.8.7+4
* Fixes `BuildContext` handling in example.
* Updates metadata unit test to work on iOS 16.2.

View File

@ -182,6 +182,32 @@
[mockUIImagePicker setSourceType:UIImagePickerControllerSourceTypePhotoLibrary]);
}
- (void)testPickMediaShouldUseUIImagePickerControllerOnPreiOS14 {
if (@available(iOS 14, *)) {
return;
}
id mockUIImagePicker = OCMClassMock([UIImagePickerController class]);
id photoLibrary = OCMClassMock([PHPhotoLibrary class]);
OCMStub(ClassMethod([photoLibrary authorizationStatus]))
.andReturn(PHAuthorizationStatusAuthorized);
FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init];
[plugin setImagePickerControllerOverrides:@[ mockUIImagePicker ]];
FLTMediaSelectionOptions *mediaSelectionOptions =
[FLTMediaSelectionOptions makeWithMaxSize:[FLTMaxSize makeWithWidth:@(100) height:@(200)]
imageQuality:@(50)
requestFullMetadata:@YES
allowMultiple:@YES];
[plugin pickMediaWithMediaSelectionOptions:mediaSelectionOptions
completion:^(NSArray<NSString *> *_Nullable result,
FlutterError *_Nullable error){
}];
OCMVerify(times(1),
[mockUIImagePicker setSourceType:UIImagePickerControllerSourceTypePhotoLibrary]);
}
- (void)testPickImageWithoutFullMetadata {
id mockUIImagePicker = OCMClassMock([UIImagePickerController class]);
id photoLibrary = OCMClassMock([PHPhotoLibrary class]);
@ -217,6 +243,28 @@
OCMVerify(times(0), [photoLibrary authorizationStatus]);
}
- (void)testPickMediaWithoutFullMetadata {
id mockUIImagePicker = OCMClassMock([UIImagePickerController class]);
id photoLibrary = OCMClassMock([PHPhotoLibrary class]);
FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init];
[plugin setImagePickerControllerOverrides:@[ mockUIImagePicker ]];
FLTMediaSelectionOptions *mediaSelectionOptions =
[FLTMediaSelectionOptions makeWithMaxSize:[FLTMaxSize makeWithWidth:@(100) height:@(200)]
imageQuality:@(50)
requestFullMetadata:@YES
allowMultiple:@YES];
[plugin pickMediaWithMediaSelectionOptions:mediaSelectionOptions
completion:^(NSArray<NSString *> *_Nullable result,
FlutterError *_Nullable error){
}];
OCMVerify(times(0), [photoLibrary authorizationStatus]);
}
#pragma mark - Test camera devices, no op on simulators
- (void)testPluginPickImageDeviceCancelClickMultipleTimes {
@ -298,6 +346,36 @@
[self waitForExpectationsWithTimeout:30 handler:nil];
}
- (void)testPluginMediaPathHasNoItem {
FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init];
XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"];
plugin.callContext = [[FLTImagePickerMethodCallContext alloc]
initWithResult:^(NSArray<NSString *> *_Nullable result, FlutterError *_Nullable error) {
XCTAssertEqualObjects(result, @[]);
[resultExpectation fulfill];
}];
[plugin sendCallResultWithSavedPathList:@[]];
[self waitForExpectationsWithTimeout:30 handler:nil];
}
- (void)testPluginMediaPathHasItem {
FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init];
NSArray *pathList = @[ @"test" ];
XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"];
plugin.callContext = [[FLTImagePickerMethodCallContext alloc]
initWithResult:^(NSArray<NSString *> *_Nullable result, FlutterError *_Nullable error) {
XCTAssertEqualObjects(result, pathList);
[resultExpectation fulfill];
}];
[plugin sendCallResultWithSavedPathList:pathList];
[self waitForExpectationsWithTimeout:30 handler:nil];
}
- (void)testSendsImageInvalidSourceError API_AVAILABLE(ios(14)) {
id mockPickerViewController = OCMClassMock([PHPickerViewController class]);

View File

@ -10,6 +10,7 @@ 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';
import 'package:video_player/video_player.dart';
void main() {
@ -38,14 +39,14 @@ class MyHomePage extends StatefulWidget {
}
class _MyHomePageState extends State<MyHomePage> {
List<XFile>? _imageFileList;
List<XFile>? _mediaFileList;
void _setImageFileListFromFile(XFile? value) {
_imageFileList = value == null ? null : <XFile>[value];
_mediaFileList = value == null ? null : <XFile>[value];
}
dynamic _pickImageError;
bool isVideo = false;
bool _isVideo = false;
VideoPlayerController? _controller;
VideoPlayerController? _toBeDisposed;
@ -60,18 +61,10 @@ class _MyHomePageState extends State<MyHomePage> {
if (file != null && mounted) {
await _disposeVideoController();
late VideoPlayerController controller;
if (kIsWeb) {
controller = VideoPlayerController.network(file.path);
} else {
controller = VideoPlayerController.file(File(file.path));
}
controller = VideoPlayerController.file(File(file.path));
_controller = controller;
// In web, most browsers won't honor a programmatic call to .play
// if the video has a sound track (and is not muted).
// Mute the video so it auto-plays in web!
// This is not needed if the call to .play is the result of user
// interaction (clicking on a "play" button, for example).
const double volume = kIsWeb ? 0.0 : 1.0;
const double volume = 1.0;
await controller.setVolume(volume);
await controller.initialize();
await controller.setLooping(true);
@ -80,13 +73,17 @@ class _MyHomePageState extends State<MyHomePage> {
}
}
Future<void> _onImageButtonPressed(ImageSource source,
{required BuildContext context, bool isMultiImage = false}) async {
Future<void> _onImageButtonPressed(
ImageSource source, {
required BuildContext context,
bool isMultiImage = false,
bool isMedia = false,
}) async {
if (_controller != null) {
await _controller!.setVolume(0.0);
}
if (context.mounted) {
if (isVideo) {
if (_isVideo) {
final XFile? file = await _picker.getVideo(
source: source, maxDuration: const Duration(seconds: 10));
await _playVideo(file);
@ -94,18 +91,27 @@ class _MyHomePageState extends State<MyHomePage> {
await _displayPickImageDialog(context,
(double? maxWidth, double? maxHeight, int? quality) async {
try {
final List<XFile> pickedFileList =
await _picker.getMultiImageWithOptions(
options: MultiImagePickerOptions(
imageOptions: ImageOptions(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
),
),
);
final List<XFile> pickedFileList = isMedia
? await _picker.getMedia(
options: MediaOptions(
allowMultiple: isMultiImage,
imageOptions: ImageOptions(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
)),
)
: await _picker.getMultiImageWithOptions(
options: MultiImagePickerOptions(
imageOptions: ImageOptions(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
),
),
);
setState(() {
_imageFileList = pickedFileList;
_mediaFileList = pickedFileList;
});
} catch (e) {
setState(() {
@ -113,6 +119,31 @@ class _MyHomePageState extends State<MyHomePage> {
});
}
});
} else if (isMedia) {
await _displayPickImageDialog(context,
(double? maxWidth, double? maxHeight, int? quality) async {
try {
final List<XFile> pickedFileList = <XFile>[];
final XFile? media = _firstOrNull(await _picker.getMedia(
options: MediaOptions(
allowMultiple: isMultiImage,
imageOptions: ImageOptions(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
)),
));
if (media != null) {
pickedFileList.add(media);
setState(() {
_mediaFileList = pickedFileList;
});
}
} catch (e) {
setState(() => _pickImageError = e);
}
});
} else {
await _displayPickImageDialog(context,
(double? maxWidth, double? maxHeight, int? quality) async {
@ -186,22 +217,28 @@ class _MyHomePageState extends State<MyHomePage> {
if (retrieveError != null) {
return retrieveError;
}
if (_imageFileList != null) {
if (_mediaFileList != null) {
return Semantics(
label: 'image_picker_example_picked_images',
child: ListView.builder(
key: UniqueKey(),
itemBuilder: (BuildContext context, int index) {
// Why network for web?
// See https://pub.dev/packages/image_picker#getting-ready-for-the-web-platform
final String? mime = lookupMimeType(_mediaFileList![index].path);
return Semantics(
label: 'image_picker_example_picked_image',
child: kIsWeb
? Image.network(_imageFileList![index].path)
: Image.file(File(_imageFileList![index].path)),
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: _imageFileList!.length,
itemCount: _mediaFileList!.length,
),
);
} else if (_pickImageError != null) {
@ -217,8 +254,19 @@ class _MyHomePageState extends State<MyHomePage> {
}
}
Widget _buildInlineVideoPlayer(int index) {
final VideoPlayerController controller =
VideoPlayerController.file(File(_mediaFileList![index].path));
const double volume = kIsWeb ? 0.0 : 1.0;
controller.setVolume(volume);
controller.initialize();
controller.setLooping(true);
controller.play();
return Center(child: AspectRatioVideo(controller));
}
Widget _handlePreview() {
if (isVideo) {
if (_isVideo) {
return _previewVideo();
} else {
return _previewImages();
@ -240,8 +288,9 @@ class _MyHomePageState extends State<MyHomePage> {
Semantics(
label: 'image_picker_example_from_gallery',
child: FloatingActionButton(
key: const Key('image_picker_example_from_gallery'),
onPressed: () {
isVideo = false;
_isVideo = false;
_onImageButtonPressed(ImageSource.gallery, context: context);
},
heroTag: 'image0',
@ -253,7 +302,40 @@ class _MyHomePageState extends State<MyHomePage> {
padding: const EdgeInsets.only(top: 16.0),
child: FloatingActionButton(
onPressed: () {
isVideo = false;
_isVideo = false;
_onImageButtonPressed(
ImageSource.gallery,
context: context,
isMultiImage: true,
isMedia: true,
);
},
heroTag: 'multipleMedia',
tooltip: 'Pick Multiple Media from gallery',
child: const Icon(Icons.photo_library),
),
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: FloatingActionButton(
onPressed: () {
_isVideo = false;
_onImageButtonPressed(
ImageSource.gallery,
context: context,
isMedia: true,
);
},
heroTag: 'media',
tooltip: 'Pick Single Media from gallery',
child: const Icon(Icons.photo_library),
),
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: FloatingActionButton(
onPressed: () {
_isVideo = false;
_onImageButtonPressed(
ImageSource.gallery,
context: context,
@ -269,7 +351,7 @@ class _MyHomePageState extends State<MyHomePage> {
padding: const EdgeInsets.only(top: 16.0),
child: FloatingActionButton(
onPressed: () {
isVideo = false;
_isVideo = false;
_onImageButtonPressed(ImageSource.camera, context: context);
},
heroTag: 'image2',
@ -282,7 +364,7 @@ class _MyHomePageState extends State<MyHomePage> {
child: FloatingActionButton(
backgroundColor: Colors.red,
onPressed: () {
isVideo = true;
_isVideo = true;
_onImageButtonPressed(ImageSource.gallery, context: context);
},
heroTag: 'video0',
@ -295,7 +377,7 @@ class _MyHomePageState extends State<MyHomePage> {
child: FloatingActionButton(
backgroundColor: Colors.red,
onPressed: () {
isVideo = true;
_isVideo = true;
_onImageButtonPressed(ImageSource.camera, context: context);
},
heroTag: 'video1',
@ -428,3 +510,7 @@ class AspectRatioVideoState extends State<AspectRatioVideo> {
}
}
}
T? _firstOrNull<T>(List<T> list) {
return list.isEmpty ? null : list.first;
}

View File

@ -16,7 +16,8 @@ dependencies:
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ../
image_picker_platform_interface: ^2.6.1
image_picker_platform_interface: ^2.8.0
mime: ^1.0.4
video_player: ^2.1.4
dev_dependencies:

View File

@ -16,7 +16,10 @@ NS_ASSUME_NONNULL_BEGIN
+ (nullable PHAsset *)getAssetFromPHPickerResult:(PHPickerResult *)result API_AVAILABLE(ios(14));
// Save image with correct meta data and extention copied from the original asset.
// Saves video to temporary URL. Returns nil on failure;
+ (NSURL *)saveVideoFromURL:(NSURL *)videoURL;
// Saves image with correct meta data and extention copied from the original asset.
// maxWidth and maxHeight are used only for GIF images.
+ (NSString *)saveImageWithOriginalImageData:(NSData *)originalImageData
image:(UIImage *)image

View File

@ -20,6 +20,20 @@
return fetchResult.firstObject;
}
+ (NSURL *)saveVideoFromURL:(NSURL *)videoURL {
if (![[NSFileManager defaultManager] isReadableFileAtPath:[videoURL path]]) {
return nil;
}
NSString *fileName = [videoURL lastPathComponent];
NSURL *destination = [NSURL fileURLWithPath:[self temporaryFilePath:fileName]];
NSError *error;
[[NSFileManager defaultManager] copyItemAtURL:videoURL toURL:destination error:&error];
if (error) {
return nil;
}
return destination;
}
+ (NSString *)saveImageWithOriginalImageData:(NSData *)originalImageData
image:(UIImage *)image
maxWidth:(NSNumber *)maxWidth

View File

@ -109,7 +109,13 @@ typedef NS_ENUM(NSInteger, ImagePickerClassType) { UIImagePickerClassType, PHPic
PHPickerConfiguration *config =
[[PHPickerConfiguration alloc] initWithPhotoLibrary:PHPhotoLibrary.sharedPhotoLibrary];
config.selectionLimit = context.maxImageCount;
config.filter = [PHPickerFilter imagesFilter];
if (context.includeVideo) {
config.filter = [PHPickerFilter anyFilterMatchingSubfilters:@[
[PHPickerFilter imagesFilter], [PHPickerFilter videosFilter]
]];
} else {
config.filter = [PHPickerFilter imagesFilter];
}
_pickerViewController = [[PHPickerViewController alloc] initWithConfiguration:config];
_pickerViewController.delegate = self;
@ -128,7 +134,12 @@ typedef NS_ENUM(NSInteger, ImagePickerClassType) { UIImagePickerClassType, PHPic
UIImagePickerController *imagePickerController = [self createImagePickerController];
imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext;
imagePickerController.delegate = self;
imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage ];
if (context.includeVideo) {
imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage, (NSString *)kUTTypeMovie ];
} else {
imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage ];
}
self.callContext = context;
switch (source.type) {
@ -206,6 +217,29 @@ typedef NS_ENUM(NSInteger, ImagePickerClassType) { UIImagePickerClassType, PHPic
}
}
- (void)pickMediaWithMediaSelectionOptions:(nonnull FLTMediaSelectionOptions *)mediaSelectionOptions
completion:(nonnull void (^)(NSArray<NSString *> *_Nullable,
FlutterError *_Nullable))completion {
FLTImagePickerMethodCallContext *context =
[[FLTImagePickerMethodCallContext alloc] initWithResult:completion];
context.maxSize = [mediaSelectionOptions maxSize];
context.imageQuality = [mediaSelectionOptions imageQuality];
context.requestFullMetadata = [mediaSelectionOptions requestFullMetadata];
context.includeVideo = YES;
if (![[mediaSelectionOptions allowMultiple] boolValue]) {
context.maxImageCount = 1;
}
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];
}
}
- (void)pickVideoWithSource:(nonnull FLTSourceSpecification *)source
maxDuration:(nullable NSNumber *)maxDurationSeconds
completion:
@ -538,25 +572,16 @@ typedef NS_ENUM(NSInteger, ImagePickerClassType) { UIImagePickerClassType, PHPic
}
if (videoURL != nil) {
if (@available(iOS 13.0, *)) {
NSString *fileName = [videoURL lastPathComponent];
NSURL *destination =
[NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:fileName]];
if ([[NSFileManager defaultManager] isReadableFileAtPath:[videoURL path]]) {
NSError *error;
if (![[videoURL path] isEqualToString:[destination path]]) {
[[NSFileManager defaultManager] copyItemAtURL:videoURL toURL:destination error:&error];
if (error) {
[self sendCallResultWithError:[FlutterError
errorWithCode:@"flutter_image_picker_copy_video_error"
message:@"Could not cache the video file."
details:nil]];
return;
}
}
videoURL = destination;
NSURL *destination = [FLTImagePickerPhotoAssetUtil saveVideoFromURL:videoURL];
if (destination == nil) {
[self sendCallResultWithError:[FlutterError
errorWithCode:@"flutter_image_picker_copy_video_error"
message:@"Could not cache the video file."
details:nil]];
return;
}
videoURL = destination;
}
[self sendCallResultWithSavedPathList:@[ videoURL.path ]];
} else {

View File

@ -11,7 +11,7 @@
NS_ASSUME_NONNULL_BEGIN
/**
* The return hander used for all method calls, which internally adapts the provided result list
* The return handler used for all method calls, which internally adapts the provided result list
* to return either a list or a single element depending on the original call.
*/
typedef void (^FlutterResultAdapter)(NSArray<NSString *> *_Nullable, FlutterError *_Nullable);
@ -49,6 +49,9 @@ typedef void (^FlutterResultAdapter)(NSArray<NSString *> *_Nullable, FlutterErro
/** 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*/
@property(nonatomic, assign) BOOL includeVideo;
@end
#pragma mark -

View File

@ -107,9 +107,15 @@ API_AVAILABLE(ios(14))
[self completeOperationWithPath:nil error:flutterError];
}
}];
} else if ([self.result.itemProvider
// This supports uniform types that conform to UTTypeMovie.
// This includes kUTTypeVideo, kUTTypeMPEG4, public.3gpp, kUTTypeMPEG,
// public.3gpp2, public.avi, kUTTypeQuickTimeMovie.
hasItemConformingToTypeIdentifier:UTTypeMovie.identifier]) {
[self processVideo];
} else {
FlutterError *flutterError = [FlutterError errorWithCode:@"invalid_source"
message:@"Invalid image source."
message:@"Invalid media source."
details:nil];
[self completeOperationWithPath:nil error:flutterError];
}
@ -184,4 +190,41 @@ API_AVAILABLE(ios(14))
}
}
/**
* Processes the video.
*/
- (void)processVideo API_AVAILABLE(ios(14)) {
NSString *typeIdentifier = self.result.itemProvider.registeredTypeIdentifiers.firstObject;
[self.result.itemProvider
loadFileRepresentationForTypeIdentifier:typeIdentifier
completionHandler:^(NSURL *_Nullable videoURL,
NSError *_Nullable error) {
if (error != nil) {
FlutterError *flutterError =
[FlutterError errorWithCode:@"invalid_image"
message:error.localizedDescription
details:error.domain];
[self completeOperationWithPath:nil error:flutterError];
return;
}
NSURL *destination =
[FLTImagePickerPhotoAssetUtil saveVideoFromURL:videoURL];
if (destination == nil) {
[self
completeOperationWithPath:nil
error:[FlutterError
errorWithCode:
@"flutter_image_picker_copy_"
@"video_error"
message:@"Could not cache "
@"the video file."
details:nil]];
return;
}
[self completeOperationWithPath:[destination path] error:nil];
}];
}
@end

View File

@ -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 (v9.2.4), do not edit directly.
// Autogenerated from Pigeon (v9.2.5), do not edit directly.
// See also: https://pub.dev/packages/pigeon
#import <Foundation/Foundation.h>
@ -24,6 +24,7 @@ typedef NS_ENUM(NSUInteger, FLTSourceType) {
};
@class FLTMaxSize;
@class FLTMediaSelectionOptions;
@class FLTSourceSpecification;
@interface FLTMaxSize : NSObject
@ -32,6 +33,19 @@ typedef NS_ENUM(NSUInteger, FLTSourceType) {
@property(nonatomic, strong, nullable) NSNumber *height;
@end
@interface FLTMediaSelectionOptions : NSObject
/// `init` unavailable to enforce nonnull fields, see the `make` class method.
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)makeWithMaxSize:(FLTMaxSize *)maxSize
imageQuality:(nullable NSNumber *)imageQuality
requestFullMetadata:(NSNumber *)requestFullMetadata
allowMultiple:(NSNumber *)allowMultiple;
@property(nonatomic, strong) FLTMaxSize *maxSize;
@property(nonatomic, strong, nullable) NSNumber *imageQuality;
@property(nonatomic, strong) NSNumber *requestFullMetadata;
@property(nonatomic, strong) NSNumber *allowMultiple;
@end
@interface FLTSourceSpecification : NSObject
/// `init` unavailable to enforce nonnull fields, see the `make` class method.
- (instancetype)init NS_UNAVAILABLE;
@ -57,6 +71,10 @@ NSObject<FlutterMessageCodec> *FLTImagePickerApiGetCodec(void);
- (void)pickVideoWithSource:(FLTSourceSpecification *)source
maxDuration:(nullable NSNumber *)maxDurationSeconds
completion:(void (^)(NSString *_Nullable, FlutterError *_Nullable))completion;
/// Selects images and videos and returns their paths.
- (void)pickMediaWithMediaSelectionOptions:(FLTMediaSelectionOptions *)mediaSelectionOptions
completion:(void (^)(NSArray<NSString *> *_Nullable,
FlutterError *_Nullable))completion;
@end
extern void FLTImagePickerApiSetup(id<FlutterBinaryMessenger> binaryMessenger,

View File

@ -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 (v9.2.4), do not edit directly.
// Autogenerated from Pigeon (v9.2.5), do not edit directly.
// See also: https://pub.dev/packages/pigeon
#import "messages.g.h"
@ -30,6 +30,12 @@ static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) {
- (NSArray *)toList;
@end
@interface FLTMediaSelectionOptions ()
+ (FLTMediaSelectionOptions *)fromList:(NSArray *)list;
+ (nullable FLTMediaSelectionOptions *)nullableFromList:(NSArray *)list;
- (NSArray *)toList;
@end
@interface FLTSourceSpecification ()
+ (FLTSourceSpecification *)fromList:(NSArray *)list;
+ (nullable FLTSourceSpecification *)nullableFromList:(NSArray *)list;
@ -60,6 +66,42 @@ static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) {
}
@end
@implementation FLTMediaSelectionOptions
+ (instancetype)makeWithMaxSize:(FLTMaxSize *)maxSize
imageQuality:(nullable NSNumber *)imageQuality
requestFullMetadata:(NSNumber *)requestFullMetadata
allowMultiple:(NSNumber *)allowMultiple {
FLTMediaSelectionOptions *pigeonResult = [[FLTMediaSelectionOptions alloc] init];
pigeonResult.maxSize = maxSize;
pigeonResult.imageQuality = imageQuality;
pigeonResult.requestFullMetadata = requestFullMetadata;
pigeonResult.allowMultiple = allowMultiple;
return pigeonResult;
}
+ (FLTMediaSelectionOptions *)fromList:(NSArray *)list {
FLTMediaSelectionOptions *pigeonResult = [[FLTMediaSelectionOptions alloc] init];
pigeonResult.maxSize = [FLTMaxSize nullableFromList:(GetNullableObjectAtIndex(list, 0))];
NSAssert(pigeonResult.maxSize != nil, @"");
pigeonResult.imageQuality = GetNullableObjectAtIndex(list, 1);
pigeonResult.requestFullMetadata = GetNullableObjectAtIndex(list, 2);
NSAssert(pigeonResult.requestFullMetadata != nil, @"");
pigeonResult.allowMultiple = GetNullableObjectAtIndex(list, 3);
NSAssert(pigeonResult.allowMultiple != nil, @"");
return pigeonResult;
}
+ (nullable FLTMediaSelectionOptions *)nullableFromList:(NSArray *)list {
return (list) ? [FLTMediaSelectionOptions fromList:list] : nil;
}
- (NSArray *)toList {
return @[
(self.maxSize ? [self.maxSize toList] : [NSNull null]),
(self.imageQuality ?: [NSNull null]),
(self.requestFullMetadata ?: [NSNull null]),
(self.allowMultiple ?: [NSNull null]),
];
}
@end
@implementation FLTSourceSpecification
+ (instancetype)makeWithType:(FLTSourceType)type camera:(FLTSourceCamera)camera {
FLTSourceSpecification *pigeonResult = [[FLTSourceSpecification alloc] init];
@ -92,6 +134,8 @@ static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) {
case 128:
return [FLTMaxSize fromList:[self readValue]];
case 129:
return [FLTMediaSelectionOptions fromList:[self readValue]];
case 130:
return [FLTSourceSpecification fromList:[self readValue]];
default:
return [super readValueOfType:type];
@ -106,9 +150,12 @@ static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) {
if ([value isKindOfClass:[FLTMaxSize class]]) {
[self writeByte:128];
[self writeValue:[value toList]];
} else if ([value isKindOfClass:[FLTSourceSpecification class]]) {
} else if ([value isKindOfClass:[FLTMediaSelectionOptions class]]) {
[self writeByte:129];
[self writeValue:[value toList]];
} else if ([value isKindOfClass:[FLTSourceSpecification class]]) {
[self writeByte:130];
[self writeValue:[value toList]];
} else {
[super writeValue:value];
}
@ -220,4 +267,28 @@ void FLTImagePickerApiSetup(id<FlutterBinaryMessenger> binaryMessenger,
[channel setMessageHandler:nil];
}
}
/// Selects images and videos and returns their paths.
{
FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc]
initWithName:@"dev.flutter.pigeon.ImagePickerApi.pickMedia"
binaryMessenger:binaryMessenger
codec:FLTImagePickerApiGetCodec()];
if (api) {
NSCAssert([api respondsToSelector:@selector(pickMediaWithMediaSelectionOptions:completion:)],
@"FLTImagePickerApi api (%@) doesn't respond to "
@"@selector(pickMediaWithMediaSelectionOptions:completion:)",
api);
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
NSArray *args = message;
FLTMediaSelectionOptions *arg_mediaSelectionOptions = GetNullableObjectAtIndex(args, 0);
[api pickMediaWithMediaSelectionOptions:arg_mediaSelectionOptions
completion:^(NSArray<NSString *> *_Nullable output,
FlutterError *_Nullable error) {
callback(wrapResult(output, error));
}];
}];
} else {
[channel setMessageHandler:nil];
}
}
}

View File

@ -175,6 +175,51 @@ class ImagePickerIOS extends ImagePickerPlatform {
);
}
@override
Future<List<XFile>> getMedia({
required MediaOptions options,
}) async {
final MediaSelectionOptions mediaSelectionOptions =
_mediaOptionsToMediaSelectionOptions(options);
return (await _hostApi.pickMedia(mediaSelectionOptions))
.map((String? path) => XFile(path!))
.toList();
}
MaxSize _imageOptionsToMaxSizeWithValidation(ImageOptions imageOptions) {
final double? maxHeight = imageOptions.maxHeight;
final double? maxWidth = imageOptions.maxWidth;
final int? imageQuality = imageOptions.imageQuality;
if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) {
throw ArgumentError.value(
imageQuality, 'imageQuality', 'must be between 0 and 100');
}
if (maxWidth != null && maxWidth < 0) {
throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative');
}
if (maxHeight != null && maxHeight < 0) {
throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative');
}
return MaxSize(width: maxWidth, height: maxHeight);
}
MediaSelectionOptions _mediaOptionsToMediaSelectionOptions(
MediaOptions mediaOptions) {
final MaxSize maxSize =
_imageOptionsToMaxSizeWithValidation(mediaOptions.imageOptions);
return MediaSelectionOptions(
maxSize: maxSize,
imageQuality: mediaOptions.imageOptions.imageQuality,
requestFullMetadata: mediaOptions.imageOptions.requestFullMetadata,
allowMultiple: mediaOptions.allowMultiple,
);
}
@override
Future<PickedFile?> pickVideo({
required ImageSource source,

View File

@ -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 (v9.2.4), do not edit directly.
// Autogenerated from Pigeon (v9.2.5), 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
@ -47,6 +47,42 @@ class MaxSize {
}
}
class MediaSelectionOptions {
MediaSelectionOptions({
required this.maxSize,
this.imageQuality,
required this.requestFullMetadata,
required this.allowMultiple,
});
MaxSize maxSize;
int? imageQuality;
bool requestFullMetadata;
bool allowMultiple;
Object encode() {
return <Object?>[
maxSize.encode(),
imageQuality,
requestFullMetadata,
allowMultiple,
];
}
static MediaSelectionOptions decode(Object result) {
result as List<Object?>;
return MediaSelectionOptions(
maxSize: MaxSize.decode(result[0]! as List<Object?>),
imageQuality: result[1] as int?,
requestFullMetadata: result[2]! as bool,
allowMultiple: result[3]! as bool,
);
}
}
class SourceSpecification {
SourceSpecification({
required this.type,
@ -80,9 +116,12 @@ class _ImagePickerApiCodec extends StandardMessageCodec {
if (value is MaxSize) {
buffer.putUint8(128);
writeValue(buffer, value.encode());
} else if (value is SourceSpecification) {
} else if (value is MediaSelectionOptions) {
buffer.putUint8(129);
writeValue(buffer, value.encode());
} else if (value is SourceSpecification) {
buffer.putUint8(130);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
}
@ -94,6 +133,8 @@ class _ImagePickerApiCodec extends StandardMessageCodec {
case 128:
return MaxSize.decode(readValue(buffer)!);
case 129:
return MediaSelectionOptions.decode(readValue(buffer)!);
case 130:
return SourceSpecification.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
@ -184,4 +225,33 @@ class ImagePickerApi {
return (replyList[0] as String?);
}
}
/// Selects images and videos and returns their paths.
Future<List<String?>> pickMedia(
MediaSelectionOptions arg_mediaSelectionOptions) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.ImagePickerApi.pickMedia', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel
.send(<Object?>[arg_mediaSelectionOptions]) as List<Object?>?;
if (replyList == null) {
throw PlatformException(
code: 'channel-error',
message: 'Unable to establish connection on channel.',
);
} else if (replyList.length > 1) {
throw PlatformException(
code: replyList[0]! as String,
message: replyList[1] as String?,
details: replyList[2],
);
} else if (replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (replyList[0] as List<Object?>?)!.cast<String?>();
}
}
}

View File

@ -20,6 +20,20 @@ class MaxSize {
double? height;
}
class MediaSelectionOptions {
MediaSelectionOptions({
required this.maxSize,
this.imageQuality,
required this.requestFullMetadata,
required this.allowMultiple,
});
MaxSize maxSize;
int? imageQuality;
bool requestFullMetadata;
bool allowMultiple;
}
// Corresponds to `CameraDevice` from the platform interface package.
enum SourceCamera { rear, front }
@ -45,4 +59,9 @@ abstract class ImagePickerApi {
@async
@ObjCSelector('pickVideoWithSource:maxDuration:')
String? pickVideo(SourceSpecification source, int? maxDurationSeconds);
/// Selects images and videos and returns their paths.
@async
@ObjCSelector('pickMediaWithMediaSelectionOptions:')
List<String?> pickMedia(MediaSelectionOptions mediaSelectionOptions);
}

View File

@ -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.7+4
version: 0.8.8
environment:
sdk: ">=2.18.0 <4.0.0"
@ -19,7 +19,7 @@ flutter:
dependencies:
flutter:
sdk: flutter
image_picker_platform_interface: ^2.6.1
image_picker_platform_interface: ^2.8.0
dev_dependencies:
flutter_test:

View File

@ -71,6 +71,19 @@ class _ApiLogger implements TestHostImagePickerApi {
return returnValue as List<String?>?;
}
@override
Future<List<String?>> pickMedia(
MediaSelectionOptions mediaSelectionOptions) async {
calls.add(_LoggedMethodCall('pickMedia', arguments: <String, dynamic>{
'maxWidth': mediaSelectionOptions.maxSize.width,
'maxHeight': mediaSelectionOptions.maxSize.height,
'imageQuality': mediaSelectionOptions.imageQuality,
'requestFullMetadata': mediaSelectionOptions.requestFullMetadata,
'allowMultiple': mediaSelectionOptions.allowMultiple,
}));
return returnValue as List<String?>;
}
@override
Future<String?> pickVideo(
SourceSpecification source, int? maxDurationSeconds) async {
@ -878,6 +891,227 @@ void main() {
});
});
group('#getMedia', () {
test('calls the method correctly', () async {
log.returnValue = <String>['0', '1'];
await picker.getMedia(options: const MediaOptions(allowMultiple: true));
expect(
log.calls,
<_LoggedMethodCall>[
const _LoggedMethodCall('pickMedia', arguments: <String, dynamic>{
'maxWidth': null,
'maxHeight': null,
'imageQuality': null,
'requestFullMetadata': true,
'allowMultiple': true
}),
],
);
});
test('passes the width and height arguments correctly', () async {
log.returnValue = <String>['0', '1'];
await picker.getMedia(options: const MediaOptions(allowMultiple: true));
await picker.getMedia(
options: MediaOptions(
allowMultiple: true,
imageOptions: ImageOptions.createAndValidate(
maxWidth: 10.0,
),
));
await picker.getMedia(
options: MediaOptions(
allowMultiple: true,
imageOptions: ImageOptions.createAndValidate(
maxHeight: 10.0,
),
));
await picker.getMedia(
options: MediaOptions(
allowMultiple: true,
imageOptions: ImageOptions.createAndValidate(
maxWidth: 10.0,
maxHeight: 20.0,
),
));
await picker.getMedia(
options: MediaOptions(
allowMultiple: true,
imageOptions: ImageOptions.createAndValidate(
maxWidth: 10.0,
imageQuality: 70,
),
));
await picker.getMedia(
options: MediaOptions(
allowMultiple: true,
imageOptions: ImageOptions.createAndValidate(
maxHeight: 10.0,
imageQuality: 70,
),
));
await picker.getMedia(
options: MediaOptions(
allowMultiple: true,
imageOptions: ImageOptions.createAndValidate(
maxWidth: 10.0,
maxHeight: 20.0,
imageQuality: 70,
),
));
expect(
log.calls,
<_LoggedMethodCall>[
const _LoggedMethodCall('pickMedia', arguments: <String, dynamic>{
'maxWidth': null,
'maxHeight': null,
'imageQuality': null,
'requestFullMetadata': true,
'allowMultiple': true
}),
const _LoggedMethodCall('pickMedia', arguments: <String, dynamic>{
'maxWidth': 10.0,
'maxHeight': null,
'imageQuality': null,
'requestFullMetadata': true,
'allowMultiple': true
}),
const _LoggedMethodCall('pickMedia', arguments: <String, dynamic>{
'maxWidth': null,
'maxHeight': 10.0,
'imageQuality': null,
'requestFullMetadata': true,
'allowMultiple': true
}),
const _LoggedMethodCall('pickMedia', arguments: <String, dynamic>{
'maxWidth': 10.0,
'maxHeight': 20.0,
'imageQuality': null,
'requestFullMetadata': true,
'allowMultiple': true
}),
const _LoggedMethodCall('pickMedia', arguments: <String, dynamic>{
'maxWidth': 10.0,
'maxHeight': null,
'imageQuality': 70,
'requestFullMetadata': true,
'allowMultiple': true
}),
const _LoggedMethodCall('pickMedia', arguments: <String, dynamic>{
'maxWidth': null,
'maxHeight': 10.0,
'imageQuality': 70,
'requestFullMetadata': true,
'allowMultiple': true
}),
const _LoggedMethodCall('pickMedia', arguments: <String, dynamic>{
'maxWidth': 10.0,
'maxHeight': 20.0,
'imageQuality': 70,
'requestFullMetadata': true,
'allowMultiple': true
}),
],
);
});
test('passes request metadata argument correctly', () async {
log.returnValue = <String>['0', '1'];
await picker.getMedia(
options: const MediaOptions(
allowMultiple: true,
imageOptions: ImageOptions(requestFullMetadata: false),
));
expect(
log.calls,
<_LoggedMethodCall>[
const _LoggedMethodCall('pickMedia', arguments: <String, dynamic>{
'maxWidth': null,
'maxHeight': null,
'imageQuality': null,
'requestFullMetadata': false,
'allowMultiple': true
}),
],
);
});
test('passes allowMultiple argument correctly', () async {
log.returnValue = <String>['0', '1'];
await picker.getMedia(
options: const MediaOptions(
allowMultiple: false,
));
expect(
log.calls,
<_LoggedMethodCall>[
const _LoggedMethodCall('pickMedia', arguments: <String, dynamic>{
'maxWidth': null,
'maxHeight': null,
'imageQuality': null,
'requestFullMetadata': true,
'allowMultiple': false
}),
],
);
});
test('does not accept a negative width or height argument', () {
log.returnValue = <String>['0', '1'];
expect(
() => picker.getMedia(
options: MediaOptions(
allowMultiple: true,
imageOptions: ImageOptions.createAndValidate(maxWidth: -1.0),
)),
throwsArgumentError,
);
expect(
() => picker.getMedia(
options: MediaOptions(
allowMultiple: true,
imageOptions: ImageOptions.createAndValidate(maxHeight: -1.0),
)),
throwsArgumentError,
);
});
test('does not accept a invalid imageQuality argument', () {
log.returnValue = <String>['0', '1'];
expect(
() => picker.getMedia(
options: MediaOptions(
allowMultiple: true,
imageOptions: ImageOptions.createAndValidate(imageQuality: -1),
)),
throwsArgumentError,
);
expect(
() => picker.getMedia(
options: MediaOptions(
allowMultiple: true,
imageOptions: ImageOptions.createAndValidate(imageQuality: 101),
)),
throwsArgumentError,
);
});
test('handles a empty path response gracefully', () async {
log.returnValue = <String>[];
expect(
await picker.getMedia(
options: const MediaOptions(allowMultiple: true)),
<String>[]);
});
});
group('#getVideo', () {
test('passes the image source argument correctly', () async {
await picker.getVideo(source: ImageSource.camera);

View File

@ -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 (v9.2.4), do not edit directly.
// Autogenerated from Pigeon (v9.2.5), 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
// ignore_for_file: avoid_relative_lib_imports
@ -20,9 +20,12 @@ class _TestHostImagePickerApiCodec extends StandardMessageCodec {
if (value is MaxSize) {
buffer.putUint8(128);
writeValue(buffer, value.encode());
} else if (value is SourceSpecification) {
} else if (value is MediaSelectionOptions) {
buffer.putUint8(129);
writeValue(buffer, value.encode());
} else if (value is SourceSpecification) {
buffer.putUint8(130);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
}
@ -34,6 +37,8 @@ class _TestHostImagePickerApiCodec extends StandardMessageCodec {
case 128:
return MaxSize.decode(readValue(buffer)!);
case 129:
return MediaSelectionOptions.decode(readValue(buffer)!);
case 130:
return SourceSpecification.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
@ -55,6 +60,9 @@ abstract class TestHostImagePickerApi {
Future<String?> pickVideo(
SourceSpecification source, int? maxDurationSeconds);
/// Selects images and videos and returns their paths.
Future<List<String?>> pickMedia(MediaSelectionOptions mediaSelectionOptions);
static void setup(TestHostImagePickerApi? api,
{BinaryMessenger? binaryMessenger}) {
{
@ -140,5 +148,29 @@ abstract class TestHostImagePickerApi {
});
}
}
{
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.ImagePickerApi.pickMedia', codec,
binaryMessenger: binaryMessenger);
if (api == null) {
_testBinaryMessengerBinding!.defaultBinaryMessenger
.setMockDecodedMessageHandler<Object?>(channel, null);
} else {
_testBinaryMessengerBinding!.defaultBinaryMessenger
.setMockDecodedMessageHandler<Object?>(channel,
(Object? message) async {
assert(message != null,
'Argument for dev.flutter.pigeon.ImagePickerApi.pickMedia was null.');
final List<Object?> args = (message as List<Object?>?)!;
final MediaSelectionOptions? arg_mediaSelectionOptions =
(args[0] as MediaSelectionOptions?);
assert(arg_mediaSelectionOptions != null,
'Argument for dev.flutter.pigeon.ImagePickerApi.pickMedia was null, expected non-null MediaSelectionOptions.');
final List<String?> output =
await api.pickMedia(arg_mediaSelectionOptions!);
return <Object?>[output];
});
}
}
}
}

View File

@ -1,3 +1,7 @@
## 0.2.1
* Adds `getMedia` method.
## 0.2.0
* Implements initial Linux support.

View File

@ -9,6 +9,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
import 'package:mime/mime.dart';
import 'package:video_player/video_player.dart';
void main() {
@ -37,11 +38,11 @@ class MyHomePage extends StatefulWidget {
}
class _MyHomePageState extends State<MyHomePage> {
List<XFile>? _imageFileList;
List<XFile>? _mediaFileList;
// This must be called from within a setState() callback
void _setImageFileListFromFile(XFile? value) {
_imageFileList = value == null ? null : <XFile>[value];
_mediaFileList = value == null ? null : <XFile>[value];
}
dynamic _pickImageError;
@ -70,52 +71,12 @@ class _MyHomePageState extends State<MyHomePage> {
}
}
Future<void> _handleMultiImagePicked(BuildContext context) async {
await _displayPickImageDialog(context,
(double? maxWidth, double? maxHeight, int? quality) async {
try {
final List<XFile>? pickedFileList = await _picker.getMultiImage(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
);
setState(() {
_imageFileList = pickedFileList;
});
} catch (e) {
setState(() {
_pickImageError = e;
});
}
});
}
Future<void> _handleSingleImagePicked(
BuildContext context, ImageSource source) async {
await _displayPickImageDialog(context,
(double? maxWidth, double? maxHeight, int? quality) async {
try {
final XFile? pickedFile = await _picker.getImageFromSource(
source: source,
options: ImagePickerOptions(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
),
);
setState(() {
_setImageFileListFromFile(pickedFile);
});
} catch (e) {
setState(() {
_pickImageError = e;
});
}
});
}
Future<void> _onImageButtonPressed(ImageSource source,
{required BuildContext context, bool isMultiImage = false}) async {
Future<void> _onImageButtonPressed(
ImageSource source, {
required BuildContext context,
bool isMultiImage = false,
bool isMedia = false,
}) async {
if (_controller != null) {
await _controller!.setVolume(0.0);
}
@ -125,9 +86,83 @@ class _MyHomePageState extends State<MyHomePage> {
source: source, maxDuration: const Duration(seconds: 10));
await _playVideo(file);
} else if (isMultiImage) {
await _handleMultiImagePicked(context);
await _displayPickImageDialog(context,
(double? maxWidth, double? maxHeight, int? quality) async {
try {
final List<XFile> pickedFileList = isMedia
? await _picker.getMedia(
options: MediaOptions(
allowMultiple: isMultiImage,
imageOptions: ImageOptions(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
)),
)
: await _picker.getMultiImageWithOptions(
options: MultiImagePickerOptions(
imageOptions: ImageOptions(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
),
),
);
setState(() {
_mediaFileList = pickedFileList;
});
} catch (e) {
setState(() {
_pickImageError = e;
});
}
});
} else if (isMedia) {
await _displayPickImageDialog(context,
(double? maxWidth, double? maxHeight, int? quality) async {
try {
final List<XFile> pickedFileList = <XFile>[];
final XFile? media = _firstOrNull(await _picker.getMedia(
options: MediaOptions(
allowMultiple: isMultiImage,
imageOptions: ImageOptions(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
)),
));
if (media != null) {
pickedFileList.add(media);
setState(() {
_mediaFileList = pickedFileList;
});
}
} catch (e) {
setState(() => _pickImageError = e);
}
});
} else {
await _handleSingleImagePicked(context, source);
await _displayPickImageDialog(context,
(double? maxWidth, double? maxHeight, int? quality) async {
try {
final XFile? pickedFile = await _picker.getImageFromSource(
source: source,
options: ImagePickerOptions(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
),
);
setState(() {
_setImageFileListFromFile(pickedFile);
});
} catch (e) {
setState(() {
_pickImageError = e;
});
}
});
}
}
}
@ -180,18 +215,28 @@ class _MyHomePageState extends State<MyHomePage> {
if (retrieveError != null) {
return retrieveError;
}
if (_imageFileList != null) {
if (_mediaFileList != null) {
return Semantics(
label: 'image_picker_example_picked_images',
child: ListView.builder(
key: UniqueKey(),
itemBuilder: (BuildContext context, int index) {
final String? mime = lookupMimeType(_mediaFileList![index].path);
return Semantics(
label: 'image_picker_example_picked_image',
child: Image.file(File(_imageFileList![index].path)),
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: _imageFileList!.length,
itemCount: _mediaFileList!.length,
),
);
} else if (_pickImageError != null) {
@ -207,6 +252,17 @@ class _MyHomePageState extends State<MyHomePage> {
}
}
Widget _buildInlineVideoPlayer(int index) {
final VideoPlayerController controller =
VideoPlayerController.file(File(_mediaFileList![index].path));
const double volume = 1.0;
controller.setVolume(volume);
controller.initialize();
controller.setLooping(true);
controller.play();
return Center(child: AspectRatioVideo(controller));
}
Widget _handlePreview() {
if (_isVideo) {
return _previewVideo();
@ -230,6 +286,7 @@ class _MyHomePageState extends State<MyHomePage> {
Semantics(
label: 'image_picker_example_from_gallery',
child: FloatingActionButton(
key: const Key('image_picker_example_from_gallery'),
onPressed: () {
_isVideo = false;
_onImageButtonPressed(ImageSource.gallery, context: context);
@ -239,6 +296,39 @@ class _MyHomePageState extends State<MyHomePage> {
child: const Icon(Icons.photo),
),
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: FloatingActionButton(
onPressed: () {
_isVideo = false;
_onImageButtonPressed(
ImageSource.gallery,
context: context,
isMultiImage: true,
isMedia: true,
);
},
heroTag: 'multipleMedia',
tooltip: 'Pick Multiple Media from gallery',
child: const Icon(Icons.photo_library),
),
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: FloatingActionButton(
onPressed: () {
_isVideo = false;
_onImageButtonPressed(
ImageSource.gallery,
context: context,
isMedia: true,
);
},
heroTag: 'media',
tooltip: 'Pick Single Media from gallery',
child: const Icon(Icons.photo_library),
),
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: FloatingActionButton(
@ -420,3 +510,7 @@ class AspectRatioVideoState extends State<AspectRatioVideo> {
}
}
}
T? _firstOrNull<T>(List<T> list) {
return list.isEmpty ? null : list.first;
}

View File

@ -17,7 +17,8 @@ dependencies:
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ..
image_picker_platform_interface: ^2.7.0
image_picker_platform_interface: ^2.8.0
mime: ^1.0.4
video_player: ^2.1.4
dev_dependencies:

View File

@ -154,4 +154,27 @@ class ImagePickerLinux extends CameraDelegatingImagePickerPlatform {
.openFiles(acceptedTypeGroups: <XTypeGroup>[typeGroup]);
return files;
}
// `maxWidth`, `maxHeight`, and `imageQuality` arguments are not currently
// supported. If any of these arguments are supplied, they will be silently
// ignored.
@override
Future<List<XFile>> getMedia({required MediaOptions options}) async {
const XTypeGroup typeGroup = XTypeGroup(
label: 'images and videos', extensions: <String>['image/*', 'video/*']);
List<XFile> files;
if (options.allowMultiple) {
files = await fileSelector
.openFiles(acceptedTypeGroups: <XTypeGroup>[typeGroup]);
} else {
final XFile? file = await fileSelector
.openFile(acceptedTypeGroups: <XTypeGroup>[typeGroup]);
files = <XFile>[
if (file != null) file,
];
}
return files;
}
}

View File

@ -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.0
version: 0.2.1
environment:
sdk: ">=2.18.0 <4.0.0"
@ -20,7 +20,7 @@ dependencies:
file_selector_platform_interface: ^2.2.0
flutter:
sdk: flutter
image_picker_platform_interface: ^2.7.0
image_picker_platform_interface: ^2.8.0
dev_dependencies:
build_runner: ^2.1.5

View File

@ -125,6 +125,38 @@ void main() {
plugin.getVideo(source: ImageSource.camera), throwsStateError);
});
});
group('media', () {
test('getMedia passes the accepted type groups correctly', () async {
await plugin.getMedia(options: const MediaOptions(allowMultiple: true));
final VerificationResult result = verify(
mockFileSelectorPlatform.openFiles(
acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups')));
expect(capturedTypeGroups(result)[0].extensions,
<String>['image/*', 'video/*']);
});
test('multiple media handles an empty path response gracefully', () async {
expect(
await plugin.getMedia(
options: const MediaOptions(
allowMultiple: true,
),
),
<String>[]);
});
test('single media handles an empty path response gracefully', () async {
expect(
await plugin.getMedia(
options: const MediaOptions(
allowMultiple: false,
),
),
<String>[]);
});
});
}
class FakeCameraDelegate extends ImagePickerCameraDelegate {

View File

@ -1,3 +1,7 @@
## 0.2.1
* Adds `getMedia` method.
## 0.2.0
* Implements initial macOS support.

View File

@ -9,6 +9,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
import 'package:mime/mime.dart';
import 'package:video_player/video_player.dart';
void main() {
@ -37,11 +38,11 @@ class MyHomePage extends StatefulWidget {
}
class _MyHomePageState extends State<MyHomePage> {
List<XFile>? _imageFileList;
List<XFile>? _mediaFileList;
// This must be called from within a setState() callback
void _setImageFileListFromFile(XFile? value) {
_imageFileList = value == null ? null : <XFile>[value];
_mediaFileList = value == null ? null : <XFile>[value];
}
dynamic _pickImageError;
@ -70,52 +71,12 @@ class _MyHomePageState extends State<MyHomePage> {
}
}
Future<void> _handleMultiImagePicked(BuildContext context) async {
await _displayPickImageDialog(context,
(double? maxWidth, double? maxHeight, int? quality) async {
try {
final List<XFile>? pickedFileList = await _picker.getMultiImage(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
);
setState(() {
_imageFileList = pickedFileList;
});
} catch (e) {
setState(() {
_pickImageError = e;
});
}
});
}
Future<void> _handleSingleImagePicked(
BuildContext context, ImageSource source) async {
await _displayPickImageDialog(context,
(double? maxWidth, double? maxHeight, int? quality) async {
try {
final XFile? pickedFile = await _picker.getImageFromSource(
source: source,
options: ImagePickerOptions(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
),
);
setState(() {
_setImageFileListFromFile(pickedFile);
});
} catch (e) {
setState(() {
_pickImageError = e;
});
}
});
}
Future<void> _onImageButtonPressed(ImageSource source,
{required BuildContext context, bool isMultiImage = false}) async {
Future<void> _onImageButtonPressed(
ImageSource source, {
required BuildContext context,
bool isMultiImage = false,
bool isMedia = false,
}) async {
if (_controller != null) {
await _controller!.setVolume(0.0);
}
@ -125,9 +86,83 @@ class _MyHomePageState extends State<MyHomePage> {
source: source, maxDuration: const Duration(seconds: 10));
await _playVideo(file);
} else if (isMultiImage) {
await _handleMultiImagePicked(context);
await _displayPickImageDialog(context,
(double? maxWidth, double? maxHeight, int? quality) async {
try {
final List<XFile> pickedFileList = isMedia
? await _picker.getMedia(
options: MediaOptions(
allowMultiple: isMultiImage,
imageOptions: ImageOptions(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
)),
)
: await _picker.getMultiImageWithOptions(
options: MultiImagePickerOptions(
imageOptions: ImageOptions(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
),
),
);
setState(() {
_mediaFileList = pickedFileList;
});
} catch (e) {
setState(() {
_pickImageError = e;
});
}
});
} else if (isMedia) {
await _displayPickImageDialog(context,
(double? maxWidth, double? maxHeight, int? quality) async {
try {
final List<XFile> pickedFileList = <XFile>[];
final XFile? media = _firstOrNull(await _picker.getMedia(
options: MediaOptions(
allowMultiple: isMultiImage,
imageOptions: ImageOptions(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
)),
));
if (media != null) {
pickedFileList.add(media);
setState(() {
_mediaFileList = pickedFileList;
});
}
} catch (e) {
setState(() => _pickImageError = e);
}
});
} else {
await _handleSingleImagePicked(context, source);
await _displayPickImageDialog(context,
(double? maxWidth, double? maxHeight, int? quality) async {
try {
final XFile? pickedFile = await _picker.getImageFromSource(
source: source,
options: ImagePickerOptions(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
),
);
setState(() {
_setImageFileListFromFile(pickedFile);
});
} catch (e) {
setState(() {
_pickImageError = e;
});
}
});
}
}
}
@ -180,18 +215,28 @@ class _MyHomePageState extends State<MyHomePage> {
if (retrieveError != null) {
return retrieveError;
}
if (_imageFileList != null) {
if (_mediaFileList != null) {
return Semantics(
label: 'image_picker_example_picked_images',
child: ListView.builder(
key: UniqueKey(),
itemBuilder: (BuildContext context, int index) {
final String? mime = lookupMimeType(_mediaFileList![index].path);
return Semantics(
label: 'image_picker_example_picked_image',
child: Image.file(File(_imageFileList![index].path)),
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: _imageFileList!.length,
itemCount: _mediaFileList!.length,
),
);
} else if (_pickImageError != null) {
@ -207,6 +252,17 @@ class _MyHomePageState extends State<MyHomePage> {
}
}
Widget _buildInlineVideoPlayer(int index) {
final VideoPlayerController controller =
VideoPlayerController.file(File(_mediaFileList![index].path));
const double volume = 1.0;
controller.setVolume(volume);
controller.initialize();
controller.setLooping(true);
controller.play();
return Center(child: AspectRatioVideo(controller));
}
Widget _handlePreview() {
if (_isVideo) {
return _previewVideo();
@ -230,6 +286,7 @@ class _MyHomePageState extends State<MyHomePage> {
Semantics(
label: 'image_picker_example_from_gallery',
child: FloatingActionButton(
key: const Key('image_picker_example_from_gallery'),
onPressed: () {
_isVideo = false;
_onImageButtonPressed(ImageSource.gallery, context: context);
@ -239,6 +296,39 @@ class _MyHomePageState extends State<MyHomePage> {
child: const Icon(Icons.photo),
),
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: FloatingActionButton(
onPressed: () {
_isVideo = false;
_onImageButtonPressed(
ImageSource.gallery,
context: context,
isMultiImage: true,
isMedia: true,
);
},
heroTag: 'multipleMedia',
tooltip: 'Pick Multiple Media from gallery',
child: const Icon(Icons.photo_library),
),
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: FloatingActionButton(
onPressed: () {
_isVideo = false;
_onImageButtonPressed(
ImageSource.gallery,
context: context,
isMedia: true,
);
},
heroTag: 'media',
tooltip: 'Pick Single Media from gallery',
child: const Icon(Icons.photo_library),
),
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: FloatingActionButton(
@ -420,3 +510,7 @@ class AspectRatioVideoState extends State<AspectRatioVideo> {
}
}
}
T? _firstOrNull<T>(List<T> list) {
return list.isEmpty ? null : list.first;
}

View File

@ -17,7 +17,8 @@ dependencies:
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ..
image_picker_platform_interface: ^2.7.0
image_picker_platform_interface: ^2.8.0
mime: ^1.0.4
video_player: ^2.1.4
dev_dependencies:

View File

@ -159,4 +159,28 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform {
.openFiles(acceptedTypeGroups: <XTypeGroup>[typeGroup]);
return files;
}
// `maxWidth`, `maxHeight`, and `imageQuality` arguments are not currently
// supported. If any of these arguments are supplied, they will be silently
// ignored.
@override
Future<List<XFile>> getMedia({required MediaOptions options}) async {
const XTypeGroup typeGroup = XTypeGroup(
label: 'images and videos',
extensions: <String>['public.image', 'public.movie']);
List<XFile> files;
if (options.allowMultiple) {
files = await fileSelector
.openFiles(acceptedTypeGroups: <XTypeGroup>[typeGroup]);
} else {
final XFile? file = await fileSelector
.openFile(acceptedTypeGroups: <XTypeGroup>[typeGroup]);
files = <XFile>[
if (file != null) file,
];
}
return files;
}
}

View File

@ -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.0
version: 0.2.1
environment:
sdk: ">=2.18.0 <4.0.0"
@ -20,7 +20,7 @@ dependencies:
file_selector_platform_interface: ^2.3.0
flutter:
sdk: flutter
image_picker_platform_interface: ^2.7.0
image_picker_platform_interface: ^2.8.0
dev_dependencies:
build_runner: ^2.1.5

View File

@ -131,6 +131,38 @@ void main() {
plugin.getVideo(source: ImageSource.camera), throwsStateError);
});
});
group('media', () {
test('getMedia passes the accepted type groups correctly', () async {
await plugin.getMedia(options: const MediaOptions(allowMultiple: true));
final VerificationResult result = verify(
mockFileSelectorPlatform.openFiles(
acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups')));
expect(capturedTypeGroups(result)[0].extensions,
<String>['public.image', 'public.movie']);
});
test('multiple media handles an empty path response gracefully', () async {
expect(
await plugin.getMedia(
options: const MediaOptions(
allowMultiple: true,
),
),
<String>[]);
});
test('single media handles an empty path response gracefully', () async {
expect(
await plugin.getMedia(
options: const MediaOptions(
allowMultiple: false,
),
),
<String>[]);
});
});
}
class FakeCameraDelegate extends ImagePickerCameraDelegate {

View File

@ -1,3 +1,7 @@
## 0.2.1
* Adds `getMedia` method.
## 0.2.0
* Updates minimum Flutter version to 3.3.

View File

@ -9,6 +9,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
import 'package:mime/mime.dart';
import 'package:video_player/video_player.dart';
void main() {
@ -37,11 +38,11 @@ class MyHomePage extends StatefulWidget {
}
class _MyHomePageState extends State<MyHomePage> {
List<XFile>? _imageFileList;
List<XFile>? _mediaFileList;
// This must be called from within a setState() callback
void _setImageFileListFromFile(XFile? value) {
_imageFileList = value == null ? null : <XFile>[value];
_mediaFileList = value == null ? null : <XFile>[value];
}
dynamic _pickImageError;
@ -70,52 +71,12 @@ class _MyHomePageState extends State<MyHomePage> {
}
}
Future<void> _handleMultiImagePicked(BuildContext context) async {
await _displayPickImageDialog(context,
(double? maxWidth, double? maxHeight, int? quality) async {
try {
final List<XFile>? pickedFileList = await _picker.getMultiImage(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
);
setState(() {
_imageFileList = pickedFileList;
});
} catch (e) {
setState(() {
_pickImageError = e;
});
}
});
}
Future<void> _handleSingleImagePicked(
BuildContext context, ImageSource source) async {
await _displayPickImageDialog(context,
(double? maxWidth, double? maxHeight, int? quality) async {
try {
final XFile? pickedFile = await _picker.getImageFromSource(
source: source,
options: ImagePickerOptions(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
),
);
setState(() {
_setImageFileListFromFile(pickedFile);
});
} catch (e) {
setState(() {
_pickImageError = e;
});
}
});
}
Future<void> _onImageButtonPressed(ImageSource source,
{required BuildContext context, bool isMultiImage = false}) async {
Future<void> _onImageButtonPressed(
ImageSource source, {
required BuildContext context,
bool isMultiImage = false,
bool isMedia = false,
}) async {
if (_controller != null) {
await _controller!.setVolume(0.0);
}
@ -125,9 +86,83 @@ class _MyHomePageState extends State<MyHomePage> {
source: source, maxDuration: const Duration(seconds: 10));
await _playVideo(file);
} else if (isMultiImage) {
await _handleMultiImagePicked(context);
await _displayPickImageDialog(context,
(double? maxWidth, double? maxHeight, int? quality) async {
try {
final List<XFile> pickedFileList = isMedia
? await _picker.getMedia(
options: MediaOptions(
allowMultiple: isMultiImage,
imageOptions: ImageOptions(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
)),
)
: await _picker.getMultiImageWithOptions(
options: MultiImagePickerOptions(
imageOptions: ImageOptions(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
),
),
);
setState(() {
_mediaFileList = pickedFileList;
});
} catch (e) {
setState(() {
_pickImageError = e;
});
}
});
} else if (isMedia) {
await _displayPickImageDialog(context,
(double? maxWidth, double? maxHeight, int? quality) async {
try {
final List<XFile> pickedFileList = <XFile>[];
final XFile? media = _firstOrNull(await _picker.getMedia(
options: MediaOptions(
allowMultiple: isMultiImage,
imageOptions: ImageOptions(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
)),
));
if (media != null) {
pickedFileList.add(media);
setState(() {
_mediaFileList = pickedFileList;
});
}
} catch (e) {
setState(() => _pickImageError = e);
}
});
} else {
await _handleSingleImagePicked(context, source);
await _displayPickImageDialog(context,
(double? maxWidth, double? maxHeight, int? quality) async {
try {
final XFile? pickedFile = await _picker.getImageFromSource(
source: source,
options: ImagePickerOptions(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
),
);
setState(() {
_setImageFileListFromFile(pickedFile);
});
} catch (e) {
setState(() {
_pickImageError = e;
});
}
});
}
}
}
@ -180,18 +215,28 @@ class _MyHomePageState extends State<MyHomePage> {
if (retrieveError != null) {
return retrieveError;
}
if (_imageFileList != null) {
if (_mediaFileList != null) {
return Semantics(
label: 'image_picker_example_picked_images',
child: ListView.builder(
key: UniqueKey(),
itemBuilder: (BuildContext context, int index) {
final String? mime = lookupMimeType(_mediaFileList![index].path);
return Semantics(
label: 'image_picker_example_picked_image',
child: Image.file(File(_imageFileList![index].path)),
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: _imageFileList!.length,
itemCount: _mediaFileList!.length,
),
);
} else if (_pickImageError != null) {
@ -207,6 +252,17 @@ class _MyHomePageState extends State<MyHomePage> {
}
}
Widget _buildInlineVideoPlayer(int index) {
final VideoPlayerController controller =
VideoPlayerController.file(File(_mediaFileList![index].path));
const double volume = 1.0;
controller.setVolume(volume);
controller.initialize();
controller.setLooping(true);
controller.play();
return Center(child: AspectRatioVideo(controller));
}
Widget _handlePreview() {
if (_isVideo) {
return _previewVideo();
@ -230,6 +286,7 @@ class _MyHomePageState extends State<MyHomePage> {
Semantics(
label: 'image_picker_example_from_gallery',
child: FloatingActionButton(
key: const Key('image_picker_example_from_gallery'),
onPressed: () {
_isVideo = false;
_onImageButtonPressed(ImageSource.gallery, context: context);
@ -239,6 +296,39 @@ class _MyHomePageState extends State<MyHomePage> {
child: const Icon(Icons.photo),
),
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: FloatingActionButton(
onPressed: () {
_isVideo = false;
_onImageButtonPressed(
ImageSource.gallery,
context: context,
isMultiImage: true,
isMedia: true,
);
},
heroTag: 'multipleMedia',
tooltip: 'Pick Multiple Media from gallery',
child: const Icon(Icons.photo_library),
),
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: FloatingActionButton(
onPressed: () {
_isVideo = false;
_onImageButtonPressed(
ImageSource.gallery,
context: context,
isMedia: true,
);
},
heroTag: 'media',
tooltip: 'Pick Single Media from gallery',
child: const Icon(Icons.photo_library),
),
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: FloatingActionButton(
@ -420,3 +510,7 @@ class AspectRatioVideoState extends State<AspectRatioVideo> {
}
}
}
T? _firstOrNull<T>(List<T> list) {
return list.isEmpty ? null : list.first;
}

View File

@ -10,7 +10,7 @@ environment:
dependencies:
flutter:
sdk: flutter
image_picker_platform_interface: ^2.7.0
image_picker_platform_interface: ^2.8.0
image_picker_windows:
# When depending on this package from a real application you should use:
# image_picker_windows: ^x.y.z
@ -18,6 +18,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: ..
mime: ^1.0.4
video_player: ^2.1.4
dev_dependencies:

View File

@ -183,4 +183,28 @@ class ImagePickerWindows extends CameraDelegatingImagePickerPlatform {
.openFiles(acceptedTypeGroups: <XTypeGroup>[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.
@override
Future<List<XFile>> getMedia({required MediaOptions options}) async {
const XTypeGroup typeGroup = XTypeGroup(
label: 'images and videos',
extensions: <String>[...imageFormats, ...videoFormats]);
List<XFile> files;
if (options.allowMultiple) {
files = await fileSelector
.openFiles(acceptedTypeGroups: <XTypeGroup>[typeGroup]);
} else {
final XFile? file = await fileSelector
.openFile(acceptedTypeGroups: <XTypeGroup>[typeGroup]);
files = <XFile>[
if (file != null) file,
];
}
return files;
}
}

View File

@ -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.0
version: 0.2.1
environment:
sdk: ">=2.18.0 <4.0.0"
@ -20,7 +20,7 @@ dependencies:
file_selector_windows: ^0.9.0
flutter:
sdk: flutter
image_picker_platform_interface: ^2.7.0
image_picker_platform_interface: ^2.8.0
dev_dependencies:
build_runner: ^2.1.5

View File

@ -128,6 +128,41 @@ void main() {
plugin.getVideo(source: ImageSource.camera), throwsStateError);
});
});
group('media', () {
test('getMedia passes the accepted type groups correctly', () async {
await plugin.getMedia(options: const MediaOptions(allowMultiple: true));
final VerificationResult result = verify(
mockFileSelectorPlatform.openFiles(
acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups')));
expect(capturedTypeGroups(result)[0].extensions, <String>[
...ImagePickerWindows.imageFormats,
...ImagePickerWindows.videoFormats
]);
});
test('multiple media handles an empty path response gracefully',
() async {
expect(
await plugin.getMedia(
options: const MediaOptions(
allowMultiple: true,
),
),
<String>[]);
});
test('single media handles an empty path response gracefully', () async {
expect(
await plugin.getMedia(
options: const MediaOptions(
allowMultiple: false,
),
),
<String>[]);
});
});
});
}

View File

@ -48,6 +48,7 @@
- logging
- markdown
- meta
- mime
- path
- shelf
- shelf_static