[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
diff --git a/packages/image_picker/image_picker_android/CHANGELOG.md b/packages/image_picker/image_picker_android/CHANGELOG.md
index 97ccd9d..971cfe0 100644
--- a/packages/image_picker/image_picker_android/CHANGELOG.md
+++ b/packages/image_picker/image_picker_android/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.8.7
+
+* Adds `getMedia` method.
+
## 0.8.6+20
* Bumps androidx.activity:activity from 1.7.0 to 1.7.1.
diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java
index d216309..685534e 100644
--- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java
+++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java
@@ -79,6 +79,7 @@
@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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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);
}
diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java
index 31b2303..b5deb28 100644
--- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java
+++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java
@@ -19,10 +19,16 @@
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 @@
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 @@
@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 @@
}
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);
@@ -326,11 +331,25 @@
}
@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 @@
}
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 @@
@Nullable
@Override
- public Messages.CacheRetrievalResult retrieveLostResults() {
+ public CacheRetrievalResult retrieveLostResults() {
ImagePickerDelegate delegate = getImagePickerDelegate();
if (delegate == null) {
throw new FlutterError(
diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/Messages.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/Messages.java
index 17390ac..8a19cfd 100644
--- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/Messages.java
+++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/Messages.java
@@ -88,6 +88,79 @@
}
}
+ /** 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 @@
}
}
+ /** 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 @@
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 @@
} 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 @@
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 @@
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 @@
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 @@
}
};
- api.pickImages(
- sourceArg, optionsArg, allowMultipleArg, usePhotoPickerArg, resultCallback);
+ api.pickImages(sourceArg, optionsArg, generalOptionsArg, resultCallback);
});
} else {
channel.setMessageHandler(null);
@@ -644,8 +785,7 @@
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 @@
}
};
- 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);
diff --git a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java
index efdbbae..73ee5a0 100644
--- a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java
+++ b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java
@@ -29,7 +29,9 @@
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 @@
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;
@@ -162,6 +166,18 @@
}
@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() {
@@ -632,6 +648,19 @@
}
@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();
diff --git a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java
index cd408c5..b2c281c 100644
--- a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java
+++ b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java
@@ -24,7 +24,9 @@
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 @@
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 @@
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 @@
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 @@
@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));
}
diff --git a/packages/image_picker/image_picker_android/example/lib/main.dart b/packages/image_picker/image_picker_android/example/lib/main.dart
index fa87587..7d58a2a 100755
--- a/packages/image_picker/image_picker_android/example/lib/main.dart
+++ b/packages/image_picker/image_picker_android/example/lib/main.dart
@@ -14,6 +14,7 @@
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 _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 @@
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 @@
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 @@
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 @@
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 @@
}
}
+ 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 @@
}
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 @@
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 @@
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 @@
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 @@
child: FloatingActionButton(
backgroundColor: Colors.red,
onPressed: () {
- isVideo = true;
+ _isVideo = true;
_onImageButtonPressed(ImageSource.gallery, context: context);
},
heroTag: 'video0',
@@ -370,7 +454,7 @@
child: FloatingActionButton(
backgroundColor: Colors.red,
onPressed: () {
- isVideo = true;
+ _isVideo = true;
_onImageButtonPressed(ImageSource.camera, context: context);
},
heroTag: 'video1',
@@ -510,3 +594,7 @@
}
}
}
+
+T? _firstOrNull<T>(List<T> list) {
+ return list.isEmpty ? null : list.first;
+}
diff --git a/packages/image_picker/image_picker_android/example/pubspec.yaml b/packages/image_picker/image_picker_android/example/pubspec.yaml
index 8921a37..1cce21a 100644
--- a/packages/image_picker/image_picker_android/example/pubspec.yaml
+++ b/packages/image_picker/image_picker_android/example/pubspec.yaml
@@ -19,7 +19,8 @@
# 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:
diff --git a/packages/image_picker/image_picker_android/lib/image_picker_android.dart b/packages/image_picker/image_picker_android/lib/image_picker_android.dart
index fbc7fa7..c9e2c87 100644
--- a/packages/image_picker/image_picker_android/lib/image_picker_android.dart
+++ b/packages/image_picker/image_picker_android/lib/image_picker_android.dart
@@ -11,12 +11,17 @@
/// 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 @@
}
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 @@
}
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 @@
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;
}
@@ -198,6 +210,21 @@
}
@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,
CameraDevice preferredCameraDevice = CameraDevice.rear,
@@ -211,6 +238,38 @@
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 @@
: 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 @@
// 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;
}
diff --git a/packages/image_picker/image_picker_android/lib/src/messages.g.dart b/packages/image_picker/image_picker_android/lib/src/messages.g.dart
index a4f15c8..476e80d 100644
--- a/packages/image_picker/image_picker_android/lib/src/messages.g.dart
+++ b/packages/image_picker/image_picker_android/lib/src/messages.g.dart
@@ -26,6 +26,32 @@
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 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 @@
} 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 @@
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 @@
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 @@
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',
diff --git a/packages/image_picker/image_picker_android/pigeons/messages.dart b/packages/image_picker/image_picker_android/pigeons/messages.dart
index 31ff22f..9d264b5 100644
--- a/packages/image_picker/image_picker_android/pigeons/messages.dart
+++ b/packages/image_picker/image_picker_android/pigeons/messages.dart
@@ -13,6 +13,11 @@
),
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 @@
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 @@
/// 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 @@
/// 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)
diff --git a/packages/image_picker/image_picker_android/pubspec.yaml b/packages/image_picker/image_picker_android/pubspec.yaml
index 8c61648..ed7c8db 100755
--- a/packages/image_picker/image_picker_android/pubspec.yaml
+++ b/packages/image_picker/image_picker_android/pubspec.yaml
@@ -3,7 +3,7 @@
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 @@
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:
diff --git a/packages/image_picker/image_picker_android/test/image_picker_android_test.dart b/packages/image_picker/image_picker_android/test/image_picker_android_test.dart
index f17d078..0b0cab4 100644
--- a/packages/image_picker/image_picker_android/test/image_picker_android_test.dart
+++ b/packages/image_picker/image_picker_android/test/image_picker_android_test.dart
@@ -654,6 +654,129 @@
});
});
+ 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 @@
@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>[];
}
diff --git a/packages/image_picker/image_picker_android/test/test_api.g.dart b/packages/image_picker/image_picker_android/test/test_api.g.dart
index dbb6b14..d3b6891 100644
--- a/packages/image_picker/image_picker_android/test/test_api.g.dart
+++ b/packages/image_picker/image_picker_android/test/test_api.g.dart
@@ -23,15 +23,21 @@
} 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 @@
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 @@
/// 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 @@
(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 @@
(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];
});
}
diff --git a/packages/image_picker/image_picker_for_web/CHANGELOG.md b/packages/image_picker/image_picker_for_web/CHANGELOG.md
index 100a9b0..8230dd7 100644
--- a/packages/image_picker/image_picker_for_web/CHANGELOG.md
+++ b/packages/image_picker/image_picker_for_web/CHANGELOG.md
@@ -1,5 +1,6 @@
-## NEXT
+## 2.2.0
+* Adds `getMedia` method.
* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18.
## 2.1.12
diff --git a/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart b/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart
index 9fe40da..256fe34 100644
--- a/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart
+++ b/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart
@@ -87,7 +87,8 @@
));
});
- 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 @@
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 {
diff --git a/packages/image_picker/image_picker_for_web/example/pubspec.yaml b/packages/image_picker/image_picker_for_web/example/pubspec.yaml
index 9c431bd..433a160 100644
--- a/packages/image_picker/image_picker_for_web/example/pubspec.yaml
+++ b/packages/image_picker/image_picker_for_web/example/pubspec.yaml
@@ -10,7 +10,7 @@
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:
diff --git a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart
index bb261f7..fb88c96 100644
--- a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart
+++ b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart
@@ -8,6 +8,7 @@
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 @@
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 @@
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.
///
diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml
index 06a7093..a61a5b8 100644
--- a/packages/image_picker/image_picker_for_web/pubspec.yaml
+++ b/packages/image_picker/image_picker_for_web/pubspec.yaml
@@ -2,7 +2,7 @@
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 @@
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:
diff --git a/packages/image_picker/image_picker_ios/CHANGELOG.md b/packages/image_picker/image_picker_ios/CHANGELOG.md
index 1173ddf..78805ad 100644
--- a/packages/image_picker/image_picker_ios/CHANGELOG.md
+++ b/packages/image_picker/image_picker_ios/CHANGELOG.md
@@ -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.
diff --git a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m
index ede6233..cc22621 100644
--- a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m
+++ b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m
@@ -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]);
diff --git a/packages/image_picker/image_picker_ios/example/lib/main.dart b/packages/image_picker/image_picker_ios/example/lib/main.dart
index 76076a5..0f42b58 100755
--- a/packages/image_picker/image_picker_ios/example/lib/main.dart
+++ b/packages/image_picker/image_picker_ios/example/lib/main.dart
@@ -10,6 +10,7 @@
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 _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 @@
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 @@
}
}
- 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 @@
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 @@
});
}
});
+ } 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 @@
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 @@
}
}
+ 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 @@
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 @@
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 @@
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 @@
child: FloatingActionButton(
backgroundColor: Colors.red,
onPressed: () {
- isVideo = true;
+ _isVideo = true;
_onImageButtonPressed(ImageSource.gallery, context: context);
},
heroTag: 'video0',
@@ -295,7 +377,7 @@
child: FloatingActionButton(
backgroundColor: Colors.red,
onPressed: () {
- isVideo = true;
+ _isVideo = true;
_onImageButtonPressed(ImageSource.camera, context: context);
},
heroTag: 'video1',
@@ -428,3 +510,7 @@
}
}
}
+
+T? _firstOrNull<T>(List<T> list) {
+ return list.isEmpty ? null : list.first;
+}
diff --git a/packages/image_picker/image_picker_ios/example/pubspec.yaml b/packages/image_picker/image_picker_ios/example/pubspec.yaml
index d0bca04..9d08635 100755
--- a/packages/image_picker/image_picker_ios/example/pubspec.yaml
+++ b/packages/image_picker/image_picker_ios/example/pubspec.yaml
@@ -16,7 +16,8 @@
# 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:
diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.h b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.h
index 0016765..212f092 100644
--- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.h
+++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.h
@@ -16,7 +16,10 @@
+ (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
diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.m
index bf712cd..294bbc7 100644
--- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.m
+++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.m
@@ -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
diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m
index 5aadecd..c812e35 100644
--- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m
+++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m
@@ -109,7 +109,13 @@
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 @@
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 @@
}
}
+- (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 @@
}
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 {
diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin_Test.h b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin_Test.h
index f849211..99d3ef6 100644
--- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin_Test.h
+++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin_Test.h
@@ -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 @@
/** 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 -
diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m
index 80e03dd..3476721 100644
--- a/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m
+++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m
@@ -107,9 +107,15 @@
[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 @@
}
}
+/**
+ * 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
diff --git a/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h
index cdde03d..4e2c4b2 100644
--- a/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h
+++ b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h
@@ -1,7 +1,7 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-// Autogenerated from Pigeon (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 @@
};
@class FLTMaxSize;
+@class FLTMediaSelectionOptions;
@class FLTSourceSpecification;
@interface FLTMaxSize : NSObject
@@ -32,6 +33,19 @@
@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 @@
- (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,
diff --git a/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m
index a1d8636..2a24f83 100644
--- a/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m
+++ b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m
@@ -1,7 +1,7 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-// Autogenerated from Pigeon (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 @@
- (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 @@
}
@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 @@
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 @@
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 @@
[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];
+ }
+ }
}
diff --git a/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart b/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart
index 3f76784..02105f9 100644
--- a/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart
+++ b/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart
@@ -176,6 +176,51 @@
}
@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,
CameraDevice preferredCameraDevice = CameraDevice.rear,
diff --git a/packages/image_picker/image_picker_ios/lib/src/messages.g.dart b/packages/image_picker/image_picker_ios/lib/src/messages.g.dart
index 87596b7..91dde82 100644
--- a/packages/image_picker/image_picker_ios/lib/src/messages.g.dart
+++ b/packages/image_picker/image_picker_ios/lib/src/messages.g.dart
@@ -1,7 +1,7 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-// Autogenerated from Pigeon (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 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 @@
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 @@
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 @@
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?>();
+ }
+ }
}
diff --git a/packages/image_picker/image_picker_ios/pigeons/messages.dart b/packages/image_picker/image_picker_ios/pigeons/messages.dart
index d04841b..fb69a6d 100644
--- a/packages/image_picker/image_picker_ios/pigeons/messages.dart
+++ b/packages/image_picker/image_picker_ios/pigeons/messages.dart
@@ -20,6 +20,20 @@
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 @@
@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);
}
diff --git a/packages/image_picker/image_picker_ios/pubspec.yaml b/packages/image_picker/image_picker_ios/pubspec.yaml
index 90d6dff..a924689 100755
--- a/packages/image_picker/image_picker_ios/pubspec.yaml
+++ b/packages/image_picker/image_picker_ios/pubspec.yaml
@@ -2,7 +2,7 @@
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 @@
dependencies:
flutter:
sdk: flutter
- image_picker_platform_interface: ^2.6.1
+ image_picker_platform_interface: ^2.8.0
dev_dependencies:
flutter_test:
diff --git a/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart b/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart
index 2c9d525..da74e31 100644
--- a/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart
+++ b/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart
@@ -72,6 +72,19 @@
}
@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 {
calls.add(_LoggedMethodCall('pickVideo', arguments: <String, dynamic>{
@@ -878,6 +891,227 @@
});
});
+ 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);
diff --git a/packages/image_picker/image_picker_ios/test/test_api.g.dart b/packages/image_picker/image_picker_ios/test/test_api.g.dart
index 4ac6195..6da0400 100644
--- a/packages/image_picker/image_picker_ios/test/test_api.g.dart
+++ b/packages/image_picker/image_picker_ios/test/test_api.g.dart
@@ -1,7 +1,7 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-// Autogenerated from Pigeon (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 @@
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 @@
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 @@
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 @@
});
}
}
+ {
+ 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];
+ });
+ }
+ }
}
}
diff --git a/packages/image_picker/image_picker_linux/CHANGELOG.md b/packages/image_picker/image_picker_linux/CHANGELOG.md
index d3bfbf9..9f14cc7 100644
--- a/packages/image_picker/image_picker_linux/CHANGELOG.md
+++ b/packages/image_picker/image_picker_linux/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.2.1
+
+* Adds `getMedia` method.
+
## 0.2.0
* Implements initial Linux support.
diff --git a/packages/image_picker/image_picker_linux/example/lib/main.dart b/packages/image_picker/image_picker_linux/example/lib/main.dart
index 9e22c71..8f48870 100644
--- a/packages/image_picker/image_picker_linux/example/lib/main.dart
+++ b/packages/image_picker/image_picker_linux/example/lib/main.dart
@@ -9,6 +9,7 @@
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 _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 @@
}
}
- 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 @@
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 @@
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 @@
}
}
+ 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 @@
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);
@@ -248,6 +305,39 @@
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,
+ isMultiImage: true,
);
},
heroTag: 'image1',
@@ -420,3 +510,7 @@
}
}
}
+
+T? _firstOrNull<T>(List<T> list) {
+ return list.isEmpty ? null : list.first;
+}
diff --git a/packages/image_picker/image_picker_linux/example/pubspec.yaml b/packages/image_picker/image_picker_linux/example/pubspec.yaml
index 54beb76..76e8f25 100644
--- a/packages/image_picker/image_picker_linux/example/pubspec.yaml
+++ b/packages/image_picker/image_picker_linux/example/pubspec.yaml
@@ -17,7 +17,8 @@
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ..
- image_picker_platform_interface: ^2.7.0
+ image_picker_platform_interface: ^2.8.0
+ mime: ^1.0.4
video_player: ^2.1.4
dev_dependencies:
diff --git a/packages/image_picker/image_picker_linux/lib/image_picker_linux.dart b/packages/image_picker/image_picker_linux/lib/image_picker_linux.dart
index f932a02..72596ea 100644
--- a/packages/image_picker/image_picker_linux/lib/image_picker_linux.dart
+++ b/packages/image_picker/image_picker_linux/lib/image_picker_linux.dart
@@ -154,4 +154,27 @@
.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;
+ }
}
diff --git a/packages/image_picker/image_picker_linux/pubspec.yaml b/packages/image_picker/image_picker_linux/pubspec.yaml
index dcfd675..9698991 100644
--- a/packages/image_picker/image_picker_linux/pubspec.yaml
+++ b/packages/image_picker/image_picker_linux/pubspec.yaml
@@ -2,7 +2,7 @@
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 @@
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
diff --git a/packages/image_picker/image_picker_linux/test/image_picker_linux_test.dart b/packages/image_picker/image_picker_linux/test/image_picker_linux_test.dart
index 32c3d45..004bfcc 100644
--- a/packages/image_picker/image_picker_linux/test/image_picker_linux_test.dart
+++ b/packages/image_picker/image_picker_linux/test/image_picker_linux_test.dart
@@ -125,6 +125,38 @@
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 {
diff --git a/packages/image_picker/image_picker_macos/CHANGELOG.md b/packages/image_picker/image_picker_macos/CHANGELOG.md
index 94ce98b..bd79a86 100644
--- a/packages/image_picker/image_picker_macos/CHANGELOG.md
+++ b/packages/image_picker/image_picker_macos/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.2.1
+
+* Adds `getMedia` method.
+
## 0.2.0
* Implements initial macOS support.
diff --git a/packages/image_picker/image_picker_macos/example/lib/main.dart b/packages/image_picker/image_picker_macos/example/lib/main.dart
index 9e22c71..8f48870 100644
--- a/packages/image_picker/image_picker_macos/example/lib/main.dart
+++ b/packages/image_picker/image_picker_macos/example/lib/main.dart
@@ -9,6 +9,7 @@
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 _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 @@
}
}
- 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 @@
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 @@
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 @@
}
}
+ 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 @@
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);
@@ -248,6 +305,39 @@
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,
+ isMultiImage: true,
);
},
heroTag: 'image1',
@@ -420,3 +510,7 @@
}
}
}
+
+T? _firstOrNull<T>(List<T> list) {
+ return list.isEmpty ? null : list.first;
+}
diff --git a/packages/image_picker/image_picker_macos/example/pubspec.yaml b/packages/image_picker/image_picker_macos/example/pubspec.yaml
index e76c492..785a2af 100644
--- a/packages/image_picker/image_picker_macos/example/pubspec.yaml
+++ b/packages/image_picker/image_picker_macos/example/pubspec.yaml
@@ -17,7 +17,8 @@
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ..
- image_picker_platform_interface: ^2.7.0
+ image_picker_platform_interface: ^2.8.0
+ mime: ^1.0.4
video_player: ^2.1.4
dev_dependencies:
diff --git a/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart b/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart
index 7a7e927..9e9447a 100644
--- a/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart
+++ b/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart
@@ -159,4 +159,28 @@
.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;
+ }
}
diff --git a/packages/image_picker/image_picker_macos/pubspec.yaml b/packages/image_picker/image_picker_macos/pubspec.yaml
index ef97bd4..9ace885 100644
--- a/packages/image_picker/image_picker_macos/pubspec.yaml
+++ b/packages/image_picker/image_picker_macos/pubspec.yaml
@@ -2,7 +2,7 @@
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 @@
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
diff --git a/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart
index f2b45cf..7e94161 100644
--- a/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart
+++ b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart
@@ -131,6 +131,38 @@
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 {
diff --git a/packages/image_picker/image_picker_windows/CHANGELOG.md b/packages/image_picker/image_picker_windows/CHANGELOG.md
index 2159d87..bd881d1 100644
--- a/packages/image_picker/image_picker_windows/CHANGELOG.md
+++ b/packages/image_picker/image_picker_windows/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.2.1
+
+* Adds `getMedia` method.
+
## 0.2.0
* Updates minimum Flutter version to 3.3.
diff --git a/packages/image_picker/image_picker_windows/example/lib/main.dart b/packages/image_picker/image_picker_windows/example/lib/main.dart
index 9e22c71..8f48870 100644
--- a/packages/image_picker/image_picker_windows/example/lib/main.dart
+++ b/packages/image_picker/image_picker_windows/example/lib/main.dart
@@ -9,6 +9,7 @@
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 _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 @@
}
}
- 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 @@
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 @@
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 @@
}
}
+ 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 @@
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);
@@ -248,6 +305,39 @@
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,
+ isMultiImage: true,
);
},
heroTag: 'image1',
@@ -420,3 +510,7 @@
}
}
}
+
+T? _firstOrNull<T>(List<T> list) {
+ return list.isEmpty ? null : list.first;
+}
diff --git a/packages/image_picker/image_picker_windows/example/pubspec.yaml b/packages/image_picker/image_picker_windows/example/pubspec.yaml
index a645670..6515d50 100644
--- a/packages/image_picker/image_picker_windows/example/pubspec.yaml
+++ b/packages/image_picker/image_picker_windows/example/pubspec.yaml
@@ -10,7 +10,7 @@
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 @@
# 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:
diff --git a/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart b/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart
index ba7ff4d..e9e4146 100644
--- a/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart
+++ b/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart
@@ -183,4 +183,28 @@
.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;
+ }
}
diff --git a/packages/image_picker/image_picker_windows/pubspec.yaml b/packages/image_picker/image_picker_windows/pubspec.yaml
index 2ca2fc5..e16ecbd 100644
--- a/packages/image_picker/image_picker_windows/pubspec.yaml
+++ b/packages/image_picker/image_picker_windows/pubspec.yaml
@@ -2,7 +2,7 @@
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 @@
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
diff --git a/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart b/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart
index d680d78..6da0873 100644
--- a/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart
+++ b/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart
@@ -128,6 +128,41 @@
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>[]);
+ });
+ });
});
}
diff --git a/script/configs/allowed_unpinned_deps.yaml b/script/configs/allowed_unpinned_deps.yaml
index 1cf35c8..d027396 100644
--- a/script/configs/allowed_unpinned_deps.yaml
+++ b/script/configs/allowed_unpinned_deps.yaml
@@ -48,6 +48,7 @@
- logging
- markdown
- meta
+- mime
- path
- shelf
- shelf_static