mirror of
https://github.com/flutter/packages.git
synced 2025-06-29 14:18:54 +08:00
[image_picker] getMedia platform implementations (#4175)
Adds `getMedia` and `getMultipleMedia` methods to all image_picker platforms. ~~waiting on https://github.com/flutter/packages/pull/4174~~ precursor to https://github.com/flutter/packages/pull/3892 part of https://github.com/flutter/flutter/issues/89159 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [relevant style guides] and ran the auto-formatter. (Unlike the flutter/flutter repo, the flutter/packages repo does use `dart format`.) - [x] I signed the [CLA]. - [x] The title of the PR starts with the name of the package surrounded by square brackets, e.g. `[shared_preferences]` - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated `pubspec.yaml` with an appropriate new version according to the [pub versioning philosophy], or this PR is [exempt from version changes]. - [x] I updated `CHANGELOG.md` to add a description of the change, [following repository CHANGELOG style]. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/packages/blob/main/CONTRIBUTING.md [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [relevant style guides]: https://github.com/flutter/packages/blob/main/CONTRIBUTING.md#style [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/wiki/Chat [pub versioning philosophy]: https://dart.dev/tools/pub/versioning [exempt from version changes]: https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#version-and-changelog-updates [following repository CHANGELOG style]: https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#changelog-style [test-exempt]: https://github.com/flutter/flutter/wiki/Tree-hygiene#tests
This commit is contained in:
@ -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.
|
||||
|
@ -79,6 +79,7 @@ public class ImagePickerDelegate
|
||||
@VisibleForTesting static final int REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA = 2343;
|
||||
@VisibleForTesting static final int REQUEST_CAMERA_IMAGE_PERMISSION = 2345;
|
||||
@VisibleForTesting static final int REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY = 2346;
|
||||
@VisibleForTesting static final int REQUEST_CODE_CHOOSE_MEDIA_FROM_GALLERY = 2347;
|
||||
|
||||
@VisibleForTesting static final int REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY = 2352;
|
||||
@VisibleForTesting static final int REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA = 2353;
|
||||
@ -279,6 +280,52 @@ public class ImagePickerDelegate
|
||||
return result.build();
|
||||
}
|
||||
|
||||
public void chooseMediaFromGallery(
|
||||
@NonNull Messages.MediaSelectionOptions options,
|
||||
@NonNull Messages.GeneralOptions generalOptions,
|
||||
@NonNull Messages.Result<List<String>> result) {
|
||||
if (!setPendingOptionsAndResult(options.getImageSelectionOptions(), null, result)) {
|
||||
finishWithAlreadyActiveError(result);
|
||||
return;
|
||||
}
|
||||
|
||||
launchPickMediaFromGalleryIntent(generalOptions);
|
||||
}
|
||||
|
||||
private void launchPickMediaFromGalleryIntent(Messages.GeneralOptions generalOptions) {
|
||||
Intent pickMediaIntent;
|
||||
if (generalOptions.getUsePhotoPicker() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
if (generalOptions.getAllowMultiple()) {
|
||||
pickMediaIntent =
|
||||
new ActivityResultContracts.PickMultipleVisualMedia()
|
||||
.createIntent(
|
||||
activity,
|
||||
new PickVisualMediaRequest.Builder()
|
||||
.setMediaType(
|
||||
ActivityResultContracts.PickVisualMedia.ImageAndVideo.INSTANCE)
|
||||
.build());
|
||||
} else {
|
||||
pickMediaIntent =
|
||||
new ActivityResultContracts.PickVisualMedia()
|
||||
.createIntent(
|
||||
activity,
|
||||
new PickVisualMediaRequest.Builder()
|
||||
.setMediaType(
|
||||
ActivityResultContracts.PickVisualMedia.ImageAndVideo.INSTANCE)
|
||||
.build());
|
||||
}
|
||||
} else {
|
||||
pickMediaIntent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
pickMediaIntent.setType("*/*");
|
||||
String[] mimeTypes = {"video/*", "image/*"};
|
||||
pickMediaIntent.putExtra("CONTENT_TYPE", mimeTypes);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
pickMediaIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, generalOptions.getAllowMultiple());
|
||||
}
|
||||
}
|
||||
activity.startActivityForResult(pickMediaIntent, REQUEST_CODE_CHOOSE_MEDIA_FROM_GALLERY);
|
||||
}
|
||||
|
||||
public void chooseVideoFromGallery(
|
||||
@NonNull VideoSelectionOptions options,
|
||||
boolean usePhotoPicker,
|
||||
@ -291,9 +338,9 @@ public class ImagePickerDelegate
|
||||
launchPickVideoFromGalleryIntent(usePhotoPicker);
|
||||
}
|
||||
|
||||
private void launchPickVideoFromGalleryIntent(Boolean useAndroidPhotoPicker) {
|
||||
private void launchPickVideoFromGalleryIntent(Boolean usePhotoPicker) {
|
||||
Intent pickVideoIntent;
|
||||
if (useAndroidPhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
if (usePhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
pickVideoIntent =
|
||||
new ActivityResultContracts.PickVisualMedia()
|
||||
.createIntent(
|
||||
@ -389,9 +436,9 @@ public class ImagePickerDelegate
|
||||
launchMultiPickImageFromGalleryIntent(usePhotoPicker);
|
||||
}
|
||||
|
||||
private void launchPickImageFromGalleryIntent(Boolean useAndroidPhotoPicker) {
|
||||
private void launchPickImageFromGalleryIntent(Boolean usePhotoPicker) {
|
||||
Intent pickImageIntent;
|
||||
if (useAndroidPhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
if (usePhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
pickImageIntent =
|
||||
new ActivityResultContracts.PickVisualMedia()
|
||||
.createIntent(
|
||||
@ -406,9 +453,9 @@ public class ImagePickerDelegate
|
||||
activity.startActivityForResult(pickImageIntent, REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY);
|
||||
}
|
||||
|
||||
private void launchMultiPickImageFromGalleryIntent(Boolean useAndroidPhotoPicker) {
|
||||
private void launchMultiPickImageFromGalleryIntent(Boolean usePhotoPicker) {
|
||||
Intent pickMultiImageIntent;
|
||||
if (useAndroidPhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
if (usePhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
pickMultiImageIntent =
|
||||
new ActivityResultContracts.PickMultipleVisualMedia()
|
||||
.createIntent(
|
||||
@ -563,6 +610,9 @@ public class ImagePickerDelegate
|
||||
case REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA:
|
||||
handlerRunnable = () -> handleCaptureImageResult(resultCode);
|
||||
break;
|
||||
case REQUEST_CODE_CHOOSE_MEDIA_FROM_GALLERY:
|
||||
handlerRunnable = () -> handleChooseMediaResult(resultCode, data);
|
||||
break;
|
||||
case REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY:
|
||||
handlerRunnable = () -> handleChooseVideoResult(resultCode, data);
|
||||
break;
|
||||
@ -589,17 +639,59 @@ public class ImagePickerDelegate
|
||||
finishWithSuccess(null);
|
||||
}
|
||||
|
||||
private void handleChooseMultiImageResult(int resultCode, Intent intent) {
|
||||
public class MediaPath {
|
||||
public MediaPath(@NonNull String path, @Nullable String mimeType) {
|
||||
this.path = path;
|
||||
this.mimeType = mimeType;
|
||||
}
|
||||
|
||||
final String path;
|
||||
final String mimeType;
|
||||
|
||||
public @NonNull String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public @Nullable String getMimeType() {
|
||||
return mimeType;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleChooseMediaResult(int resultCode, Intent intent) {
|
||||
if (resultCode == Activity.RESULT_OK && intent != null) {
|
||||
ArrayList<String> paths = new ArrayList<>();
|
||||
ArrayList<MediaPath> paths = new ArrayList<>();
|
||||
if (intent.getClipData() != null) {
|
||||
for (int i = 0; i < intent.getClipData().getItemCount(); i++) {
|
||||
paths.add(fileUtils.getPathFromUri(activity, intent.getClipData().getItemAt(i).getUri()));
|
||||
Uri uri = intent.getClipData().getItemAt(i).getUri();
|
||||
String path = fileUtils.getPathFromUri(activity, uri);
|
||||
String mimeType = activity.getContentResolver().getType(uri);
|
||||
paths.add(new MediaPath(path, mimeType));
|
||||
}
|
||||
} else {
|
||||
paths.add(fileUtils.getPathFromUri(activity, intent.getData()));
|
||||
paths.add(new MediaPath(fileUtils.getPathFromUri(activity, intent.getData()), null));
|
||||
}
|
||||
handleMultiImageResult(paths);
|
||||
handleMediaResult(paths);
|
||||
return;
|
||||
}
|
||||
|
||||
// User cancelled choosing a picture.
|
||||
finishWithSuccess(null);
|
||||
}
|
||||
|
||||
private void handleChooseMultiImageResult(int resultCode, Intent intent) {
|
||||
if (resultCode == Activity.RESULT_OK && intent != null) {
|
||||
ArrayList<MediaPath> paths = new ArrayList<>();
|
||||
if (intent.getClipData() != null) {
|
||||
for (int i = 0; i < intent.getClipData().getItemCount(); i++) {
|
||||
paths.add(
|
||||
new MediaPath(
|
||||
fileUtils.getPathFromUri(activity, intent.getClipData().getItemAt(i).getUri()),
|
||||
null));
|
||||
}
|
||||
} else {
|
||||
paths.add(new MediaPath(fileUtils.getPathFromUri(activity, intent.getData()), null));
|
||||
}
|
||||
handleMediaResult(paths);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -649,26 +741,6 @@ public class ImagePickerDelegate
|
||||
finishWithSuccess(null);
|
||||
}
|
||||
|
||||
private void handleMultiImageResult(ArrayList<String> paths) {
|
||||
ImageSelectionOptions localImageOptions = null;
|
||||
synchronized (pendingCallStateLock) {
|
||||
if (pendingCallState != null) {
|
||||
localImageOptions = pendingCallState.imageOptions;
|
||||
}
|
||||
}
|
||||
|
||||
if (localImageOptions != null) {
|
||||
ArrayList<String> finalPath = new ArrayList<>();
|
||||
for (int i = 0; i < paths.size(); i++) {
|
||||
String finalImagePath = getResizedImagePath(paths.get(i), localImageOptions);
|
||||
finalPath.add(i, finalImagePath);
|
||||
}
|
||||
finishWithListSuccess(finalPath);
|
||||
} else {
|
||||
finishWithListSuccess(paths);
|
||||
}
|
||||
}
|
||||
|
||||
void handleImageResult(String path, boolean shouldDeleteOriginalIfScaled) {
|
||||
ImageSelectionOptions localImageOptions = null;
|
||||
synchronized (pendingCallStateLock) {
|
||||
@ -679,7 +751,7 @@ public class ImagePickerDelegate
|
||||
|
||||
if (localImageOptions != null) {
|
||||
String finalImagePath = getResizedImagePath(path, localImageOptions);
|
||||
//delete original file if scaled
|
||||
// Delete original file if scaled.
|
||||
if (finalImagePath != null && !finalImagePath.equals(path) && shouldDeleteOriginalIfScaled) {
|
||||
new File(path).delete();
|
||||
}
|
||||
@ -697,7 +769,34 @@ public class ImagePickerDelegate
|
||||
outputOptions.getQuality().intValue());
|
||||
}
|
||||
|
||||
void handleVideoResult(String path) {
|
||||
private void handleMediaResult(@NonNull ArrayList<MediaPath> paths) {
|
||||
ImageSelectionOptions localImageOptions = null;
|
||||
synchronized (pendingCallStateLock) {
|
||||
if (pendingCallState != null) {
|
||||
localImageOptions = pendingCallState.imageOptions;
|
||||
}
|
||||
}
|
||||
|
||||
ArrayList<String> finalPaths = new ArrayList<>();
|
||||
if (localImageOptions != null) {
|
||||
for (int i = 0; i < paths.size(); i++) {
|
||||
MediaPath path = paths.get(i);
|
||||
String finalPath = path.path;
|
||||
if (path.mimeType == null || !path.mimeType.startsWith("video/")) {
|
||||
finalPath = getResizedImagePath(path.path, localImageOptions);
|
||||
}
|
||||
finalPaths.add(finalPath);
|
||||
}
|
||||
finishWithListSuccess(finalPaths);
|
||||
} else {
|
||||
for (int i = 0; i < paths.size(); i++) {
|
||||
finalPaths.add(paths.get(i).path);
|
||||
}
|
||||
finishWithListSuccess(finalPaths);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleVideoResult(String path) {
|
||||
finishWithSuccess(path);
|
||||
}
|
||||
|
||||
|
@ -19,10 +19,16 @@ import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
|
||||
import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter;
|
||||
import io.flutter.plugin.common.BinaryMessenger;
|
||||
import io.flutter.plugin.common.PluginRegistry;
|
||||
import io.flutter.plugins.imagepicker.Messages.CacheRetrievalResult;
|
||||
import io.flutter.plugins.imagepicker.Messages.FlutterError;
|
||||
import io.flutter.plugins.imagepicker.Messages.GeneralOptions;
|
||||
import io.flutter.plugins.imagepicker.Messages.ImagePickerApi;
|
||||
import io.flutter.plugins.imagepicker.Messages.ImageSelectionOptions;
|
||||
import io.flutter.plugins.imagepicker.Messages.MediaSelectionOptions;
|
||||
import io.flutter.plugins.imagepicker.Messages.Result;
|
||||
import io.flutter.plugins.imagepicker.Messages.SourceCamera;
|
||||
import io.flutter.plugins.imagepicker.Messages.SourceSpecification;
|
||||
import io.flutter.plugins.imagepicker.Messages.VideoSelectionOptions;
|
||||
import java.util.List;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@ -279,7 +285,7 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic
|
||||
|
||||
private void setCameraDevice(
|
||||
@NonNull ImagePickerDelegate delegate, @NonNull SourceSpecification source) {
|
||||
Messages.SourceCamera camera = source.getCamera();
|
||||
SourceCamera camera = source.getCamera();
|
||||
if (camera != null) {
|
||||
ImagePickerDelegate.CameraDevice device;
|
||||
switch (camera) {
|
||||
@ -298,9 +304,8 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic
|
||||
@Override
|
||||
public void pickImages(
|
||||
@NonNull SourceSpecification source,
|
||||
@NonNull Messages.ImageSelectionOptions options,
|
||||
@NonNull Boolean allowMultiple,
|
||||
@NonNull Boolean usePhotoPicker,
|
||||
@NonNull ImageSelectionOptions options,
|
||||
@NonNull GeneralOptions generalOptions,
|
||||
@NonNull Result<List<String>> result) {
|
||||
ImagePickerDelegate delegate = getImagePickerDelegate();
|
||||
if (delegate == null) {
|
||||
@ -311,12 +316,12 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic
|
||||
}
|
||||
|
||||
setCameraDevice(delegate, source);
|
||||
if (allowMultiple) {
|
||||
delegate.chooseMultiImageFromGallery(options, usePhotoPicker, result);
|
||||
if (generalOptions.getAllowMultiple()) {
|
||||
delegate.chooseMultiImageFromGallery(options, generalOptions.getUsePhotoPicker(), result);
|
||||
} else {
|
||||
switch (source.getType()) {
|
||||
case GALLERY:
|
||||
delegate.chooseImageFromGallery(options, usePhotoPicker, result);
|
||||
delegate.chooseImageFromGallery(options, generalOptions.getUsePhotoPicker(), result);
|
||||
break;
|
||||
case CAMERA:
|
||||
delegate.takeImageWithCamera(options, result);
|
||||
@ -325,12 +330,26 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pickMedia(
|
||||
@NonNull MediaSelectionOptions mediaSelectionOptions,
|
||||
@NonNull GeneralOptions generalOptions,
|
||||
@NonNull Result<List<String>> result) {
|
||||
ImagePickerDelegate delegate = getImagePickerDelegate();
|
||||
if (delegate == null) {
|
||||
result.error(
|
||||
new FlutterError(
|
||||
"no_activity", "image_picker plugin requires a foreground activity.", null));
|
||||
return;
|
||||
}
|
||||
delegate.chooseMediaFromGallery(mediaSelectionOptions, generalOptions, result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pickVideos(
|
||||
@NonNull SourceSpecification source,
|
||||
@NonNull Messages.VideoSelectionOptions options,
|
||||
@NonNull Boolean allowMultiple,
|
||||
@NonNull Boolean usePhotoPicker,
|
||||
@NonNull VideoSelectionOptions options,
|
||||
@NonNull GeneralOptions generalOptions,
|
||||
@NonNull Result<List<String>> result) {
|
||||
ImagePickerDelegate delegate = getImagePickerDelegate();
|
||||
if (delegate == null) {
|
||||
@ -341,12 +360,12 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic
|
||||
}
|
||||
|
||||
setCameraDevice(delegate, source);
|
||||
if (allowMultiple) {
|
||||
if (generalOptions.getAllowMultiple()) {
|
||||
result.error(new RuntimeException("Multi-video selection is not implemented"));
|
||||
} else {
|
||||
switch (source.getType()) {
|
||||
case GALLERY:
|
||||
delegate.chooseVideoFromGallery(options, usePhotoPicker, result);
|
||||
delegate.chooseVideoFromGallery(options, generalOptions.getUsePhotoPicker(), result);
|
||||
break;
|
||||
case CAMERA:
|
||||
delegate.takeVideoWithCamera(options, result);
|
||||
@ -357,7 +376,7 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Messages.CacheRetrievalResult retrieveLostResults() {
|
||||
public CacheRetrievalResult retrieveLostResults() {
|
||||
ImagePickerDelegate delegate = getImagePickerDelegate();
|
||||
if (delegate == null) {
|
||||
throw new FlutterError(
|
||||
|
@ -88,6 +88,79 @@ public class Messages {
|
||||
}
|
||||
}
|
||||
|
||||
/** Generated class from Pigeon that represents data sent in messages. */
|
||||
public static final class GeneralOptions {
|
||||
private @NonNull Boolean allowMultiple;
|
||||
|
||||
public @NonNull Boolean getAllowMultiple() {
|
||||
return allowMultiple;
|
||||
}
|
||||
|
||||
public void setAllowMultiple(@NonNull Boolean setterArg) {
|
||||
if (setterArg == null) {
|
||||
throw new IllegalStateException("Nonnull field \"allowMultiple\" is null.");
|
||||
}
|
||||
this.allowMultiple = setterArg;
|
||||
}
|
||||
|
||||
private @NonNull Boolean usePhotoPicker;
|
||||
|
||||
public @NonNull Boolean getUsePhotoPicker() {
|
||||
return usePhotoPicker;
|
||||
}
|
||||
|
||||
public void setUsePhotoPicker(@NonNull Boolean setterArg) {
|
||||
if (setterArg == null) {
|
||||
throw new IllegalStateException("Nonnull field \"usePhotoPicker\" is null.");
|
||||
}
|
||||
this.usePhotoPicker = setterArg;
|
||||
}
|
||||
|
||||
/** Constructor is non-public to enforce null safety; use Builder. */
|
||||
GeneralOptions() {}
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
private @Nullable Boolean allowMultiple;
|
||||
|
||||
public @NonNull Builder setAllowMultiple(@NonNull Boolean setterArg) {
|
||||
this.allowMultiple = setterArg;
|
||||
return this;
|
||||
}
|
||||
|
||||
private @Nullable Boolean usePhotoPicker;
|
||||
|
||||
public @NonNull Builder setUsePhotoPicker(@NonNull Boolean setterArg) {
|
||||
this.usePhotoPicker = setterArg;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull GeneralOptions build() {
|
||||
GeneralOptions pigeonReturn = new GeneralOptions();
|
||||
pigeonReturn.setAllowMultiple(allowMultiple);
|
||||
pigeonReturn.setUsePhotoPicker(usePhotoPicker);
|
||||
return pigeonReturn;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
ArrayList<Object> toList() {
|
||||
ArrayList<Object> toListResult = new ArrayList<Object>(2);
|
||||
toListResult.add(allowMultiple);
|
||||
toListResult.add(usePhotoPicker);
|
||||
return toListResult;
|
||||
}
|
||||
|
||||
static @NonNull GeneralOptions fromList(@NonNull ArrayList<Object> list) {
|
||||
GeneralOptions pigeonResult = new GeneralOptions();
|
||||
Object allowMultiple = list.get(0);
|
||||
pigeonResult.setAllowMultiple((Boolean) allowMultiple);
|
||||
Object usePhotoPicker = list.get(1);
|
||||
pigeonResult.setUsePhotoPicker((Boolean) usePhotoPicker);
|
||||
return pigeonResult;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for image selection and output.
|
||||
*
|
||||
@ -193,6 +266,58 @@ public class Messages {
|
||||
}
|
||||
}
|
||||
|
||||
/** Generated class from Pigeon that represents data sent in messages. */
|
||||
public static final class MediaSelectionOptions {
|
||||
private @NonNull ImageSelectionOptions imageSelectionOptions;
|
||||
|
||||
public @NonNull ImageSelectionOptions getImageSelectionOptions() {
|
||||
return imageSelectionOptions;
|
||||
}
|
||||
|
||||
public void setImageSelectionOptions(@NonNull ImageSelectionOptions setterArg) {
|
||||
if (setterArg == null) {
|
||||
throw new IllegalStateException("Nonnull field \"imageSelectionOptions\" is null.");
|
||||
}
|
||||
this.imageSelectionOptions = setterArg;
|
||||
}
|
||||
|
||||
/** Constructor is non-public to enforce null safety; use Builder. */
|
||||
MediaSelectionOptions() {}
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
private @Nullable ImageSelectionOptions imageSelectionOptions;
|
||||
|
||||
public @NonNull Builder setImageSelectionOptions(@NonNull ImageSelectionOptions setterArg) {
|
||||
this.imageSelectionOptions = setterArg;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull MediaSelectionOptions build() {
|
||||
MediaSelectionOptions pigeonReturn = new MediaSelectionOptions();
|
||||
pigeonReturn.setImageSelectionOptions(imageSelectionOptions);
|
||||
return pigeonReturn;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
ArrayList<Object> toList() {
|
||||
ArrayList<Object> toListResult = new ArrayList<Object>(1);
|
||||
toListResult.add((imageSelectionOptions == null) ? null : imageSelectionOptions.toList());
|
||||
return toListResult;
|
||||
}
|
||||
|
||||
static @NonNull MediaSelectionOptions fromList(@NonNull ArrayList<Object> list) {
|
||||
MediaSelectionOptions pigeonResult = new MediaSelectionOptions();
|
||||
Object imageSelectionOptions = list.get(0);
|
||||
pigeonResult.setImageSelectionOptions(
|
||||
(imageSelectionOptions == null)
|
||||
? null
|
||||
: ImageSelectionOptions.fromList((ArrayList<Object>) imageSelectionOptions));
|
||||
return pigeonResult;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for image selection and output.
|
||||
*
|
||||
@ -523,10 +648,14 @@ public class Messages {
|
||||
case (byte) 129:
|
||||
return CacheRetrievalResult.fromList((ArrayList<Object>) readValue(buffer));
|
||||
case (byte) 130:
|
||||
return ImageSelectionOptions.fromList((ArrayList<Object>) readValue(buffer));
|
||||
return GeneralOptions.fromList((ArrayList<Object>) readValue(buffer));
|
||||
case (byte) 131:
|
||||
return SourceSpecification.fromList((ArrayList<Object>) readValue(buffer));
|
||||
return ImageSelectionOptions.fromList((ArrayList<Object>) readValue(buffer));
|
||||
case (byte) 132:
|
||||
return MediaSelectionOptions.fromList((ArrayList<Object>) readValue(buffer));
|
||||
case (byte) 133:
|
||||
return SourceSpecification.fromList((ArrayList<Object>) readValue(buffer));
|
||||
case (byte) 134:
|
||||
return VideoSelectionOptions.fromList((ArrayList<Object>) readValue(buffer));
|
||||
default:
|
||||
return super.readValueOfType(type, buffer);
|
||||
@ -541,14 +670,20 @@ public class Messages {
|
||||
} else if (value instanceof CacheRetrievalResult) {
|
||||
stream.write(129);
|
||||
writeValue(stream, ((CacheRetrievalResult) value).toList());
|
||||
} else if (value instanceof ImageSelectionOptions) {
|
||||
} else if (value instanceof GeneralOptions) {
|
||||
stream.write(130);
|
||||
writeValue(stream, ((ImageSelectionOptions) value).toList());
|
||||
} else if (value instanceof SourceSpecification) {
|
||||
writeValue(stream, ((GeneralOptions) value).toList());
|
||||
} else if (value instanceof ImageSelectionOptions) {
|
||||
stream.write(131);
|
||||
writeValue(stream, ((ImageSelectionOptions) value).toList());
|
||||
} else if (value instanceof MediaSelectionOptions) {
|
||||
stream.write(132);
|
||||
writeValue(stream, ((MediaSelectionOptions) value).toList());
|
||||
} else if (value instanceof SourceSpecification) {
|
||||
stream.write(133);
|
||||
writeValue(stream, ((SourceSpecification) value).toList());
|
||||
} else if (value instanceof VideoSelectionOptions) {
|
||||
stream.write(132);
|
||||
stream.write(134);
|
||||
writeValue(stream, ((VideoSelectionOptions) value).toList());
|
||||
} else {
|
||||
super.writeValue(stream, value);
|
||||
@ -567,8 +702,7 @@ public class Messages {
|
||||
void pickImages(
|
||||
@NonNull SourceSpecification source,
|
||||
@NonNull ImageSelectionOptions options,
|
||||
@NonNull Boolean allowMultiple,
|
||||
@NonNull Boolean usePhotoPicker,
|
||||
@NonNull GeneralOptions generalOptions,
|
||||
@NonNull Result<List<String>> result);
|
||||
/**
|
||||
* Selects video and returns their paths.
|
||||
@ -579,8 +713,17 @@ public class Messages {
|
||||
void pickVideos(
|
||||
@NonNull SourceSpecification source,
|
||||
@NonNull VideoSelectionOptions options,
|
||||
@NonNull Boolean allowMultiple,
|
||||
@NonNull Boolean usePhotoPicker,
|
||||
@NonNull GeneralOptions generalOptions,
|
||||
@NonNull Result<List<String>> result);
|
||||
/**
|
||||
* Selects images and videos and returns their paths.
|
||||
*
|
||||
* <p>Elements must not be null, by convention. See
|
||||
* https://github.com/flutter/flutter/issues/97848
|
||||
*/
|
||||
void pickMedia(
|
||||
@NonNull MediaSelectionOptions mediaSelectionOptions,
|
||||
@NonNull GeneralOptions generalOptions,
|
||||
@NonNull Result<List<String>> result);
|
||||
/** Returns results from a previous app session, if any. */
|
||||
@Nullable
|
||||
@ -607,8 +750,7 @@ public class Messages {
|
||||
ArrayList<Object> args = (ArrayList<Object>) message;
|
||||
SourceSpecification sourceArg = (SourceSpecification) args.get(0);
|
||||
ImageSelectionOptions optionsArg = (ImageSelectionOptions) args.get(1);
|
||||
Boolean allowMultipleArg = (Boolean) args.get(2);
|
||||
Boolean usePhotoPickerArg = (Boolean) args.get(3);
|
||||
GeneralOptions generalOptionsArg = (GeneralOptions) args.get(2);
|
||||
Result<List<String>> resultCallback =
|
||||
new Result<List<String>>() {
|
||||
public void success(List<String> result) {
|
||||
@ -622,8 +764,7 @@ public class Messages {
|
||||
}
|
||||
};
|
||||
|
||||
api.pickImages(
|
||||
sourceArg, optionsArg, allowMultipleArg, usePhotoPickerArg, resultCallback);
|
||||
api.pickImages(sourceArg, optionsArg, generalOptionsArg, resultCallback);
|
||||
});
|
||||
} else {
|
||||
channel.setMessageHandler(null);
|
||||
@ -644,8 +785,7 @@ public class Messages {
|
||||
ArrayList<Object> args = (ArrayList<Object>) message;
|
||||
SourceSpecification sourceArg = (SourceSpecification) args.get(0);
|
||||
VideoSelectionOptions optionsArg = (VideoSelectionOptions) args.get(1);
|
||||
Boolean allowMultipleArg = (Boolean) args.get(2);
|
||||
Boolean usePhotoPickerArg = (Boolean) args.get(3);
|
||||
GeneralOptions generalOptionsArg = (GeneralOptions) args.get(2);
|
||||
Result<List<String>> resultCallback =
|
||||
new Result<List<String>>() {
|
||||
public void success(List<String> result) {
|
||||
@ -659,8 +799,38 @@ public class Messages {
|
||||
}
|
||||
};
|
||||
|
||||
api.pickVideos(
|
||||
sourceArg, optionsArg, allowMultipleArg, usePhotoPickerArg, resultCallback);
|
||||
api.pickVideos(sourceArg, optionsArg, generalOptionsArg, resultCallback);
|
||||
});
|
||||
} else {
|
||||
channel.setMessageHandler(null);
|
||||
}
|
||||
}
|
||||
{
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(
|
||||
binaryMessenger, "dev.flutter.pigeon.ImagePickerApi.pickMedia", getCodec());
|
||||
if (api != null) {
|
||||
channel.setMessageHandler(
|
||||
(message, reply) -> {
|
||||
ArrayList<Object> wrapped = new ArrayList<Object>();
|
||||
ArrayList<Object> args = (ArrayList<Object>) message;
|
||||
MediaSelectionOptions mediaSelectionOptionsArg =
|
||||
(MediaSelectionOptions) args.get(0);
|
||||
GeneralOptions generalOptionsArg = (GeneralOptions) args.get(1);
|
||||
Result<List<String>> resultCallback =
|
||||
new Result<List<String>>() {
|
||||
public void success(List<String> result) {
|
||||
wrapped.add(0, result);
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
|
||||
public void error(Throwable error) {
|
||||
ArrayList<Object> wrappedError = wrapError(error);
|
||||
reply.reply(wrappedError);
|
||||
}
|
||||
};
|
||||
|
||||
api.pickMedia(mediaSelectionOptionsArg, generalOptionsArg, resultCallback);
|
||||
});
|
||||
} else {
|
||||
channel.setMessageHandler(null);
|
||||
|
@ -29,7 +29,9 @@ import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import io.flutter.plugins.imagepicker.Messages.FlutterError;
|
||||
import io.flutter.plugins.imagepicker.Messages.GeneralOptions;
|
||||
import io.flutter.plugins.imagepicker.Messages.ImageSelectionOptions;
|
||||
import io.flutter.plugins.imagepicker.Messages.MediaSelectionOptions;
|
||||
import io.flutter.plugins.imagepicker.Messages.VideoSelectionOptions;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@ -61,6 +63,8 @@ public class ImagePickerDelegateTest {
|
||||
new ImageSelectionOptions.Builder().setQuality((long) 100).setMaxWidth(WIDTH).build();
|
||||
private static final VideoSelectionOptions DEFAULT_VIDEO_OPTIONS =
|
||||
new VideoSelectionOptions.Builder().build();
|
||||
private static final MediaSelectionOptions DEFAULT_MEDIA_OPTIONS =
|
||||
new MediaSelectionOptions.Builder().setImageSelectionOptions(DEFAULT_IMAGE_OPTIONS).build();
|
||||
|
||||
@Mock Activity mockActivity;
|
||||
@Mock ImageResizer mockImageResizer;
|
||||
@ -161,6 +165,18 @@ public class ImagePickerDelegateTest {
|
||||
verifyNoMoreInteractions(mockResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chooseMediaFromGallery_whenPendingResultExists_finishesWithAlreadyActiveError() {
|
||||
ImagePickerDelegate delegate =
|
||||
createDelegateWithPendingResultAndOptions(DEFAULT_IMAGE_OPTIONS, null);
|
||||
GeneralOptions generalOptions =
|
||||
new GeneralOptions.Builder().setAllowMultiple(true).setUsePhotoPicker(true).build();
|
||||
delegate.chooseMediaFromGallery(DEFAULT_MEDIA_OPTIONS, generalOptions, mockResult);
|
||||
|
||||
verifyFinishedWithAlreadyActiveError();
|
||||
verifyNoMoreInteractions(mockResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = 30)
|
||||
public void chooseImageFromGallery_launchesChooseFromGalleryIntent() {
|
||||
@ -631,6 +647,19 @@ public class ImagePickerDelegateTest {
|
||||
assertTrue(isHandled);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onActivityResult_whenMediaPickedFromGallery_returnsTrue() {
|
||||
ImagePickerDelegate delegate = createDelegate();
|
||||
|
||||
boolean isHandled =
|
||||
delegate.onActivityResult(
|
||||
ImagePickerDelegate.REQUEST_CODE_CHOOSE_MEDIA_FROM_GALLERY,
|
||||
Activity.RESULT_OK,
|
||||
mockIntent);
|
||||
|
||||
assertTrue(isHandled);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onActivityResult_whenVideoPickerFromGallery_returnsTrue() {
|
||||
ImagePickerDelegate delegate = createDelegate();
|
||||
|
@ -24,7 +24,9 @@ import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
|
||||
import io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference;
|
||||
import io.flutter.plugin.common.BinaryMessenger;
|
||||
import io.flutter.plugins.imagepicker.Messages.FlutterError;
|
||||
import io.flutter.plugins.imagepicker.Messages.GeneralOptions;
|
||||
import io.flutter.plugins.imagepicker.Messages.ImageSelectionOptions;
|
||||
import io.flutter.plugins.imagepicker.Messages.MediaSelectionOptions;
|
||||
import io.flutter.plugins.imagepicker.Messages.SourceSpecification;
|
||||
import io.flutter.plugins.imagepicker.Messages.VideoSelectionOptions;
|
||||
import java.util.List;
|
||||
@ -40,6 +42,16 @@ public class ImagePickerPluginTest {
|
||||
new ImageSelectionOptions.Builder().setQuality((long) 100).build();
|
||||
private static final VideoSelectionOptions DEFAULT_VIDEO_OPTIONS =
|
||||
new VideoSelectionOptions.Builder().build();
|
||||
private static final MediaSelectionOptions DEFAULT_MEDIA_OPTIONS =
|
||||
new MediaSelectionOptions.Builder().setImageSelectionOptions(DEFAULT_IMAGE_OPTIONS).build();
|
||||
private static final GeneralOptions GENERAL_OPTIONS_ALLOW_MULTIPLE_USE_PHOTO_PICKER =
|
||||
new GeneralOptions.Builder().setUsePhotoPicker(true).setAllowMultiple(true).build();
|
||||
private static final GeneralOptions GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_USE_PHOTO_PICKER =
|
||||
new GeneralOptions.Builder().setUsePhotoPicker(true).setAllowMultiple(false).build();
|
||||
private static final GeneralOptions GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER =
|
||||
new GeneralOptions.Builder().setUsePhotoPicker(false).setAllowMultiple(false).build();
|
||||
private static final GeneralOptions GENERAL_OPTIONS_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER =
|
||||
new GeneralOptions.Builder().setUsePhotoPicker(false).setAllowMultiple(true).build();
|
||||
private static final SourceSpecification SOURCE_GALLERY =
|
||||
new SourceSpecification.Builder().setType(Messages.SourceType.GALLERY).build();
|
||||
private static final SourceSpecification SOURCE_CAMERA_FRONT =
|
||||
@ -88,7 +100,10 @@ public class ImagePickerPluginTest {
|
||||
ImagePickerPlugin imagePickerPluginWithNullActivity =
|
||||
new ImagePickerPlugin(mockImagePickerDelegate, null);
|
||||
imagePickerPluginWithNullActivity.pickImages(
|
||||
SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, false, false, mockResult);
|
||||
SOURCE_GALLERY,
|
||||
DEFAULT_IMAGE_OPTIONS,
|
||||
GENERAL_OPTIONS_ALLOW_MULTIPLE_USE_PHOTO_PICKER,
|
||||
mockResult);
|
||||
|
||||
ArgumentCaptor<FlutterError> errorCaptor = ArgumentCaptor.forClass(FlutterError.class);
|
||||
verify(mockResult).error(errorCaptor.capture());
|
||||
@ -103,7 +118,10 @@ public class ImagePickerPluginTest {
|
||||
ImagePickerPlugin imagePickerPluginWithNullActivity =
|
||||
new ImagePickerPlugin(mockImagePickerDelegate, null);
|
||||
imagePickerPluginWithNullActivity.pickVideos(
|
||||
SOURCE_CAMERA_REAR, DEFAULT_VIDEO_OPTIONS, false, false, mockResult);
|
||||
SOURCE_CAMERA_REAR,
|
||||
DEFAULT_VIDEO_OPTIONS,
|
||||
GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER,
|
||||
mockResult);
|
||||
|
||||
ArgumentCaptor<FlutterError> errorCaptor = ArgumentCaptor.forClass(FlutterError.class);
|
||||
verify(mockResult).error(errorCaptor.capture());
|
||||
@ -126,60 +144,126 @@ public class ImagePickerPluginTest {
|
||||
|
||||
@Test
|
||||
public void pickImages_whenSourceIsGallery_invokesChooseImageFromGallery() {
|
||||
plugin.pickImages(SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, false, false, mockResult);
|
||||
plugin.pickImages(
|
||||
SOURCE_GALLERY,
|
||||
DEFAULT_IMAGE_OPTIONS,
|
||||
GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER,
|
||||
mockResult);
|
||||
verify(mockImagePickerDelegate).chooseImageFromGallery(any(), eq(false), any());
|
||||
verifyNoInteractions(mockResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pickImages_whenSourceIsGalleryUsingPhotoPicker_invokesChooseImageFromGallery() {
|
||||
plugin.pickImages(SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, false, true, mockResult);
|
||||
plugin.pickImages(
|
||||
SOURCE_GALLERY,
|
||||
DEFAULT_IMAGE_OPTIONS,
|
||||
GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_USE_PHOTO_PICKER,
|
||||
mockResult);
|
||||
verify(mockImagePickerDelegate).chooseImageFromGallery(any(), eq(true), any());
|
||||
verifyNoInteractions(mockResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pickImages_invokesChooseMultiImageFromGallery() {
|
||||
plugin.pickImages(SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, true, false, mockResult);
|
||||
plugin.pickImages(
|
||||
SOURCE_GALLERY,
|
||||
DEFAULT_IMAGE_OPTIONS,
|
||||
GENERAL_OPTIONS_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER,
|
||||
mockResult);
|
||||
verify(mockImagePickerDelegate).chooseMultiImageFromGallery(any(), eq(false), any());
|
||||
verifyNoInteractions(mockResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pickImages_usingPhotoPicker_invokesChooseMultiImageFromGallery() {
|
||||
plugin.pickImages(SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, true, true, mockResult);
|
||||
plugin.pickImages(
|
||||
SOURCE_GALLERY,
|
||||
DEFAULT_IMAGE_OPTIONS,
|
||||
GENERAL_OPTIONS_ALLOW_MULTIPLE_USE_PHOTO_PICKER,
|
||||
mockResult);
|
||||
verify(mockImagePickerDelegate).chooseMultiImageFromGallery(any(), eq(true), any());
|
||||
verifyNoInteractions(mockResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pickMedia_invokesChooseMediaFromGallery() {
|
||||
MediaSelectionOptions mediaSelectionOptions =
|
||||
new MediaSelectionOptions.Builder().setImageSelectionOptions(DEFAULT_IMAGE_OPTIONS).build();
|
||||
plugin.pickMedia(
|
||||
mediaSelectionOptions,
|
||||
GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER,
|
||||
mockResult);
|
||||
verify(mockImagePickerDelegate)
|
||||
.chooseMediaFromGallery(
|
||||
eq(mediaSelectionOptions),
|
||||
eq(GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER),
|
||||
any());
|
||||
verifyNoInteractions(mockResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pickMedia_usingPhotoPicker_invokesChooseMediaFromGallery() {
|
||||
MediaSelectionOptions mediaSelectionOptions =
|
||||
new MediaSelectionOptions.Builder().setImageSelectionOptions(DEFAULT_IMAGE_OPTIONS).build();
|
||||
plugin.pickMedia(
|
||||
mediaSelectionOptions, GENERAL_OPTIONS_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, mockResult);
|
||||
verify(mockImagePickerDelegate)
|
||||
.chooseMediaFromGallery(
|
||||
eq(mediaSelectionOptions),
|
||||
eq(GENERAL_OPTIONS_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER),
|
||||
any());
|
||||
verifyNoInteractions(mockResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pickImages_whenSourceIsCamera_invokesTakeImageWithCamera() {
|
||||
plugin.pickImages(SOURCE_CAMERA_REAR, DEFAULT_IMAGE_OPTIONS, false, false, mockResult);
|
||||
plugin.pickImages(
|
||||
SOURCE_CAMERA_REAR,
|
||||
DEFAULT_IMAGE_OPTIONS,
|
||||
GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER,
|
||||
mockResult);
|
||||
verify(mockImagePickerDelegate).takeImageWithCamera(any(), any());
|
||||
verifyNoInteractions(mockResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pickImages_whenSourceIsCamera_invokesTakeImageWithCamera_RearCamera() {
|
||||
plugin.pickImages(SOURCE_CAMERA_REAR, DEFAULT_IMAGE_OPTIONS, false, false, mockResult);
|
||||
plugin.pickImages(
|
||||
SOURCE_CAMERA_REAR,
|
||||
DEFAULT_IMAGE_OPTIONS,
|
||||
GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER,
|
||||
mockResult);
|
||||
verify(mockImagePickerDelegate).setCameraDevice(eq(ImagePickerDelegate.CameraDevice.REAR));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pickImages_whenSourceIsCamera_invokesTakeImageWithCamera_FrontCamera() {
|
||||
plugin.pickImages(SOURCE_CAMERA_FRONT, DEFAULT_IMAGE_OPTIONS, false, false, mockResult);
|
||||
plugin.pickImages(
|
||||
SOURCE_CAMERA_FRONT,
|
||||
DEFAULT_IMAGE_OPTIONS,
|
||||
GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER,
|
||||
mockResult);
|
||||
verify(mockImagePickerDelegate).setCameraDevice(eq(ImagePickerDelegate.CameraDevice.FRONT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pickVideos_whenSourceIsCamera_invokesTakeImageWithCamera_RearCamera() {
|
||||
plugin.pickVideos(SOURCE_CAMERA_REAR, DEFAULT_VIDEO_OPTIONS, false, false, mockResult);
|
||||
plugin.pickVideos(
|
||||
SOURCE_CAMERA_REAR,
|
||||
DEFAULT_VIDEO_OPTIONS,
|
||||
GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER,
|
||||
mockResult);
|
||||
verify(mockImagePickerDelegate).setCameraDevice(eq(ImagePickerDelegate.CameraDevice.REAR));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pickVideos_whenSourceIsCamera_invokesTakeImageWithCamera_FrontCamera() {
|
||||
plugin.pickVideos(SOURCE_CAMERA_FRONT, DEFAULT_VIDEO_OPTIONS, false, false, mockResult);
|
||||
plugin.pickVideos(
|
||||
SOURCE_CAMERA_FRONT,
|
||||
DEFAULT_VIDEO_OPTIONS,
|
||||
GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER,
|
||||
mockResult);
|
||||
verify(mockImagePickerDelegate).setCameraDevice(eq(ImagePickerDelegate.CameraDevice.FRONT));
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,7 @@ import 'package:flutter_driver/driver_extension.dart';
|
||||
import 'package:image_picker_android/image_picker_android.dart';
|
||||
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
|
||||
// #enddocregion photo-picker-example
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
void appMain() {
|
||||
@ -55,14 +56,14 @@ class MyHomePage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
List<XFile>? _imageFileList;
|
||||
List<XFile>? _mediaFileList;
|
||||
|
||||
void _setImageFileListFromFile(XFile? value) {
|
||||
_imageFileList = value == null ? null : <XFile>[value];
|
||||
_mediaFileList = value == null ? null : <XFile>[value];
|
||||
}
|
||||
|
||||
dynamic _pickImageError;
|
||||
bool isVideo = false;
|
||||
bool _isVideo = false;
|
||||
|
||||
VideoPlayerController? _controller;
|
||||
VideoPlayerController? _toBeDisposed;
|
||||
@ -77,18 +78,10 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
if (file != null && mounted) {
|
||||
await _disposeVideoController();
|
||||
late VideoPlayerController controller;
|
||||
if (kIsWeb) {
|
||||
controller = VideoPlayerController.network(file.path);
|
||||
} else {
|
||||
controller = VideoPlayerController.file(File(file.path));
|
||||
}
|
||||
|
||||
controller = VideoPlayerController.file(File(file.path));
|
||||
_controller = controller;
|
||||
// In web, most browsers won't honor a programmatic call to .play
|
||||
// if the video has a sound track (and is not muted).
|
||||
// Mute the video so it auto-plays in web!
|
||||
// This is not needed if the call to .play is the result of user
|
||||
// interaction (clicking on a "play" button, for example).
|
||||
const double volume = kIsWeb ? 0.0 : 1.0;
|
||||
const double volume = 1.0;
|
||||
await controller.setVolume(volume);
|
||||
await controller.initialize();
|
||||
await controller.setLooping(true);
|
||||
@ -101,12 +94,13 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
ImageSource source, {
|
||||
required BuildContext context,
|
||||
bool isMultiImage = false,
|
||||
bool isMedia = false,
|
||||
}) async {
|
||||
if (_controller != null) {
|
||||
await _controller!.setVolume(0.0);
|
||||
}
|
||||
if (context.mounted) {
|
||||
if (isVideo) {
|
||||
if (_isVideo) {
|
||||
final XFile? file = await _picker.getVideo(
|
||||
source: source, maxDuration: const Duration(seconds: 10));
|
||||
if (file != null && context.mounted) {
|
||||
@ -117,15 +111,54 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
await _displayPickImageDialog(context,
|
||||
(double? maxWidth, double? maxHeight, int? quality) async {
|
||||
try {
|
||||
final List<XFile>? pickedFileList = await _picker.getMultiImage(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
imageQuality: quality,
|
||||
);
|
||||
final List<XFile>? pickedFileList = isMedia
|
||||
? await _picker.getMedia(
|
||||
options: MediaOptions(
|
||||
allowMultiple: isMultiImage,
|
||||
imageOptions: ImageOptions(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
imageQuality: quality,
|
||||
)),
|
||||
)
|
||||
: await _picker.getMultiImage(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
imageQuality: quality,
|
||||
);
|
||||
if (pickedFileList != null && context.mounted) {
|
||||
_showPickedSnackBar(context, pickedFileList);
|
||||
}
|
||||
setState(() => _imageFileList = pickedFileList);
|
||||
setState(() {
|
||||
_mediaFileList = pickedFileList;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_pickImageError = e;
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (isMedia) {
|
||||
await _displayPickImageDialog(context,
|
||||
(double? maxWidth, double? maxHeight, int? quality) async {
|
||||
try {
|
||||
final List<XFile> pickedFileList = <XFile>[];
|
||||
final XFile? media = _firstOrNull(await _picker.getMedia(
|
||||
options: MediaOptions(
|
||||
allowMultiple: isMultiImage,
|
||||
imageOptions: ImageOptions(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
imageQuality: quality,
|
||||
)),
|
||||
));
|
||||
|
||||
if (media != null) {
|
||||
pickedFileList.add(media);
|
||||
setState(() {
|
||||
_mediaFileList = pickedFileList;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() => _pickImageError = e);
|
||||
}
|
||||
@ -200,30 +233,37 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
if (retrieveError != null) {
|
||||
return retrieveError;
|
||||
}
|
||||
if (_imageFileList != null) {
|
||||
if (_mediaFileList != null) {
|
||||
return Semantics(
|
||||
label: 'image_picker_example_picked_images',
|
||||
child: ListView.builder(
|
||||
key: UniqueKey(),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final XFile image = _imageFileList![index];
|
||||
final XFile image = _mediaFileList![index];
|
||||
final String? mime = lookupMimeType(_mediaFileList![index].path);
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(image.name,
|
||||
key: const Key('image_picker_example_picked_image_name')),
|
||||
// Why network for web?
|
||||
// See https://pub.dev/packages/image_picker#getting-ready-for-the-web-platform
|
||||
Semantics(
|
||||
label: 'image_picker_example_picked_image',
|
||||
child: kIsWeb
|
||||
? Image.network(image.path)
|
||||
: Image.file(File(image.path)),
|
||||
child: mime == null || mime.startsWith('image/')
|
||||
? Image.file(
|
||||
File(_mediaFileList![index].path),
|
||||
errorBuilder: (BuildContext context, Object error,
|
||||
StackTrace? stackTrace) {
|
||||
return const Center(
|
||||
child:
|
||||
Text('This image type is not supported'));
|
||||
},
|
||||
)
|
||||
: _buildInlineVideoPlayer(index),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
itemCount: _imageFileList!.length,
|
||||
itemCount: _mediaFileList!.length,
|
||||
),
|
||||
);
|
||||
} else if (_pickImageError != null) {
|
||||
@ -239,8 +279,19 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildInlineVideoPlayer(int index) {
|
||||
final VideoPlayerController controller =
|
||||
VideoPlayerController.file(File(_mediaFileList![index].path));
|
||||
const double volume = 1.0;
|
||||
controller.setVolume(volume);
|
||||
controller.initialize();
|
||||
controller.setLooping(true);
|
||||
controller.play();
|
||||
return Center(child: AspectRatioVideo(controller));
|
||||
}
|
||||
|
||||
Widget _handlePreview() {
|
||||
if (isVideo) {
|
||||
if (_isVideo) {
|
||||
return _previewVideo();
|
||||
} else {
|
||||
return _previewImages();
|
||||
@ -254,15 +305,15 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
}
|
||||
if (response.file != null) {
|
||||
if (response.type == RetrieveType.video) {
|
||||
isVideo = true;
|
||||
_isVideo = true;
|
||||
await _playVideo(response.file);
|
||||
} else {
|
||||
isVideo = false;
|
||||
_isVideo = false;
|
||||
setState(() {
|
||||
if (response.files == null) {
|
||||
_setImageFileListFromFile(response.file);
|
||||
} else {
|
||||
_imageFileList = response.files;
|
||||
_mediaFileList = response.files;
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -316,7 +367,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
child: FloatingActionButton(
|
||||
key: const Key('image_picker_example_from_gallery'),
|
||||
onPressed: () {
|
||||
isVideo = false;
|
||||
_isVideo = false;
|
||||
_onImageButtonPressed(ImageSource.gallery, context: context);
|
||||
},
|
||||
heroTag: 'image0',
|
||||
@ -328,7 +379,40 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () {
|
||||
isVideo = false;
|
||||
_isVideo = false;
|
||||
_onImageButtonPressed(
|
||||
ImageSource.gallery,
|
||||
context: context,
|
||||
isMultiImage: true,
|
||||
isMedia: true,
|
||||
);
|
||||
},
|
||||
heroTag: 'multipleMedia',
|
||||
tooltip: 'Pick Multiple Media from gallery',
|
||||
child: const Icon(Icons.photo_library),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () {
|
||||
_isVideo = false;
|
||||
_onImageButtonPressed(
|
||||
ImageSource.gallery,
|
||||
context: context,
|
||||
isMedia: true,
|
||||
);
|
||||
},
|
||||
heroTag: 'media',
|
||||
tooltip: 'Pick Single Media from gallery',
|
||||
child: const Icon(Icons.photo_library),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () {
|
||||
_isVideo = false;
|
||||
_onImageButtonPressed(
|
||||
ImageSource.gallery,
|
||||
context: context,
|
||||
@ -344,7 +428,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () {
|
||||
isVideo = false;
|
||||
_isVideo = false;
|
||||
_onImageButtonPressed(ImageSource.camera, context: context);
|
||||
},
|
||||
heroTag: 'image2',
|
||||
@ -357,7 +441,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
child: FloatingActionButton(
|
||||
backgroundColor: Colors.red,
|
||||
onPressed: () {
|
||||
isVideo = true;
|
||||
_isVideo = true;
|
||||
_onImageButtonPressed(ImageSource.gallery, context: context);
|
||||
},
|
||||
heroTag: 'video0',
|
||||
@ -370,7 +454,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
child: FloatingActionButton(
|
||||
backgroundColor: Colors.red,
|
||||
onPressed: () {
|
||||
isVideo = true;
|
||||
_isVideo = true;
|
||||
_onImageButtonPressed(ImageSource.camera, context: context);
|
||||
},
|
||||
heroTag: 'video1',
|
||||
@ -510,3 +594,7 @@ class AspectRatioVideoState extends State<AspectRatioVideo> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
T? _firstOrNull<T>(List<T> list) {
|
||||
return list.isEmpty ? null : list.first;
|
||||
}
|
||||
|
@ -19,7 +19,8 @@ dependencies:
|
||||
# The example app is bundled with the plugin so we use a path dependency on
|
||||
# the parent directory to use the current plugin's version.
|
||||
path: ../
|
||||
image_picker_platform_interface: ^2.3.0
|
||||
image_picker_platform_interface: ^2.8.0
|
||||
mime: ^1.0.4
|
||||
video_player: ^2.1.4
|
||||
|
||||
dev_dependencies:
|
||||
|
@ -11,12 +11,17 @@ import 'src/messages.g.dart';
|
||||
|
||||
/// An Android implementation of [ImagePickerPlatform].
|
||||
class ImagePickerAndroid extends ImagePickerPlatform {
|
||||
/// Creates a new plugin implemenation instance.
|
||||
/// Creates a new plugin implementation instance.
|
||||
ImagePickerAndroid({@visibleForTesting ImagePickerApi? api})
|
||||
: _hostApi = api ?? ImagePickerApi();
|
||||
|
||||
final ImagePickerApi _hostApi;
|
||||
|
||||
/// Sets [ImagePickerAndroid] to use Android 13 Photo Picker.
|
||||
///
|
||||
/// Currently defaults to false, but the default is subject to change.
|
||||
bool useAndroidPhotoPicker = false;
|
||||
|
||||
/// Registers this class as the default platform implementation.
|
||||
static void registerWith() {
|
||||
ImagePickerPlatform.instance = ImagePickerAndroid();
|
||||
@ -77,13 +82,14 @@ class ImagePickerAndroid extends ImagePickerPlatform {
|
||||
}
|
||||
|
||||
return _hostApi.pickImages(
|
||||
SourceSpecification(type: SourceType.gallery),
|
||||
ImageSelectionOptions(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
quality: imageQuality ?? 100),
|
||||
/* allowMultiple */ true,
|
||||
useAndroidPhotoPicker);
|
||||
SourceSpecification(type: SourceType.gallery),
|
||||
ImageSelectionOptions(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
quality: imageQuality ?? 100),
|
||||
GeneralOptions(
|
||||
allowMultiple: true, usePhotoPicker: useAndroidPhotoPicker),
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> _getImagePath({
|
||||
@ -108,13 +114,16 @@ class ImagePickerAndroid extends ImagePickerPlatform {
|
||||
}
|
||||
|
||||
final List<String?> paths = await _hostApi.pickImages(
|
||||
_buildSourceSpec(source, preferredCameraDevice),
|
||||
ImageSelectionOptions(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
quality: imageQuality ?? 100),
|
||||
/* allowMultiple */ false,
|
||||
useAndroidPhotoPicker);
|
||||
_buildSourceSpec(source, preferredCameraDevice),
|
||||
ImageSelectionOptions(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
quality: imageQuality ?? 100),
|
||||
GeneralOptions(
|
||||
allowMultiple: false,
|
||||
usePhotoPicker: useAndroidPhotoPicker,
|
||||
),
|
||||
);
|
||||
return paths.isEmpty ? null : paths.first;
|
||||
}
|
||||
|
||||
@ -138,10 +147,13 @@ class ImagePickerAndroid extends ImagePickerPlatform {
|
||||
Duration? maxDuration,
|
||||
}) async {
|
||||
final List<String?> paths = await _hostApi.pickVideos(
|
||||
_buildSourceSpec(source, preferredCameraDevice),
|
||||
VideoSelectionOptions(maxDurationSeconds: maxDuration?.inSeconds),
|
||||
/* allowMultiple */ false,
|
||||
useAndroidPhotoPicker);
|
||||
_buildSourceSpec(source, preferredCameraDevice),
|
||||
VideoSelectionOptions(maxDurationSeconds: maxDuration?.inSeconds),
|
||||
GeneralOptions(
|
||||
allowMultiple: false,
|
||||
usePhotoPicker: useAndroidPhotoPicker,
|
||||
),
|
||||
);
|
||||
return paths.isEmpty ? null : paths.first;
|
||||
}
|
||||
|
||||
@ -197,6 +209,21 @@ class ImagePickerAndroid extends ImagePickerPlatform {
|
||||
return paths.map((dynamic path) => XFile(path as String)).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<XFile>> getMedia({
|
||||
required MediaOptions options,
|
||||
}) async {
|
||||
return (await _hostApi.pickMedia(
|
||||
_mediaOptionsToMediaSelectionOptions(options),
|
||||
GeneralOptions(
|
||||
allowMultiple: options.allowMultiple,
|
||||
usePhotoPicker: useAndroidPhotoPicker,
|
||||
),
|
||||
))
|
||||
.map((String? path) => XFile(path!))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<XFile?> getVideo({
|
||||
required ImageSource source,
|
||||
@ -211,6 +238,38 @@ class ImagePickerAndroid extends ImagePickerPlatform {
|
||||
return path != null ? XFile(path) : null;
|
||||
}
|
||||
|
||||
MediaSelectionOptions _mediaOptionsToMediaSelectionOptions(
|
||||
MediaOptions mediaOptions) {
|
||||
final ImageSelectionOptions imageSelectionOptions =
|
||||
_imageOptionsToImageSelectionOptionsWithValidator(
|
||||
mediaOptions.imageOptions);
|
||||
return MediaSelectionOptions(
|
||||
imageSelectionOptions: imageSelectionOptions,
|
||||
);
|
||||
}
|
||||
|
||||
ImageSelectionOptions _imageOptionsToImageSelectionOptionsWithValidator(
|
||||
ImageOptions? imageOptions) {
|
||||
final double? maxHeight = imageOptions?.maxHeight;
|
||||
final double? maxWidth = imageOptions?.maxWidth;
|
||||
final int? imageQuality = imageOptions?.imageQuality;
|
||||
|
||||
if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) {
|
||||
throw ArgumentError.value(
|
||||
imageQuality, 'imageQuality', 'must be between 0 and 100');
|
||||
}
|
||||
|
||||
if (maxWidth != null && maxWidth < 0) {
|
||||
throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative');
|
||||
}
|
||||
|
||||
if (maxHeight != null && maxHeight < 0) {
|
||||
throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative');
|
||||
}
|
||||
return ImageSelectionOptions(
|
||||
quality: imageQuality ?? 100, maxHeight: maxHeight, maxWidth: maxWidth);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LostData> retrieveLostData() async {
|
||||
final LostDataResponse result = await getLostData();
|
||||
@ -243,7 +302,7 @@ class ImagePickerAndroid extends ImagePickerPlatform {
|
||||
: PlatformException(code: error.code, message: error.message);
|
||||
|
||||
// Entries are guaranteed not to be null, even though that's not currently
|
||||
// expressable in Pigeon.
|
||||
// expressible in Pigeon.
|
||||
final List<XFile> pickedFileList =
|
||||
result.paths.map((String? path) => XFile(path!)).toList();
|
||||
|
||||
@ -309,9 +368,4 @@ class ImagePickerAndroid extends ImagePickerPlatform {
|
||||
// ignore: dead_code
|
||||
return RetrieveType.image;
|
||||
}
|
||||
|
||||
/// Sets [ImagePickerAndroid] to use Android 13 Photo Picker.
|
||||
///
|
||||
/// Currently defaults to false, but the default is subject to change.
|
||||
bool useAndroidPhotoPicker = false;
|
||||
}
|
||||
|
@ -26,6 +26,32 @@ enum CacheRetrievalType {
|
||||
video,
|
||||
}
|
||||
|
||||
class GeneralOptions {
|
||||
GeneralOptions({
|
||||
required this.allowMultiple,
|
||||
required this.usePhotoPicker,
|
||||
});
|
||||
|
||||
bool allowMultiple;
|
||||
|
||||
bool usePhotoPicker;
|
||||
|
||||
Object encode() {
|
||||
return <Object?>[
|
||||
allowMultiple,
|
||||
usePhotoPicker,
|
||||
];
|
||||
}
|
||||
|
||||
static GeneralOptions decode(Object result) {
|
||||
result as List<Object?>;
|
||||
return GeneralOptions(
|
||||
allowMultiple: result[0]! as bool,
|
||||
usePhotoPicker: result[1]! as bool,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Options for image selection and output.
|
||||
class ImageSelectionOptions {
|
||||
ImageSelectionOptions({
|
||||
@ -63,6 +89,28 @@ class ImageSelectionOptions {
|
||||
}
|
||||
}
|
||||
|
||||
class MediaSelectionOptions {
|
||||
MediaSelectionOptions({
|
||||
required this.imageSelectionOptions,
|
||||
});
|
||||
|
||||
ImageSelectionOptions imageSelectionOptions;
|
||||
|
||||
Object encode() {
|
||||
return <Object?>[
|
||||
imageSelectionOptions.encode(),
|
||||
];
|
||||
}
|
||||
|
||||
static MediaSelectionOptions decode(Object result) {
|
||||
result as List<Object?>;
|
||||
return MediaSelectionOptions(
|
||||
imageSelectionOptions:
|
||||
ImageSelectionOptions.decode(result[0]! as List<Object?>),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Options for image selection and output.
|
||||
class VideoSelectionOptions {
|
||||
VideoSelectionOptions({
|
||||
@ -192,15 +240,21 @@ class _ImagePickerApiCodec extends StandardMessageCodec {
|
||||
} else if (value is CacheRetrievalResult) {
|
||||
buffer.putUint8(129);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is ImageSelectionOptions) {
|
||||
} else if (value is GeneralOptions) {
|
||||
buffer.putUint8(130);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is SourceSpecification) {
|
||||
} else if (value is ImageSelectionOptions) {
|
||||
buffer.putUint8(131);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is VideoSelectionOptions) {
|
||||
} else if (value is MediaSelectionOptions) {
|
||||
buffer.putUint8(132);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is SourceSpecification) {
|
||||
buffer.putUint8(133);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is VideoSelectionOptions) {
|
||||
buffer.putUint8(134);
|
||||
writeValue(buffer, value.encode());
|
||||
} else {
|
||||
super.writeValue(buffer, value);
|
||||
}
|
||||
@ -214,10 +268,14 @@ class _ImagePickerApiCodec extends StandardMessageCodec {
|
||||
case 129:
|
||||
return CacheRetrievalResult.decode(readValue(buffer)!);
|
||||
case 130:
|
||||
return ImageSelectionOptions.decode(readValue(buffer)!);
|
||||
return GeneralOptions.decode(readValue(buffer)!);
|
||||
case 131:
|
||||
return SourceSpecification.decode(readValue(buffer)!);
|
||||
return ImageSelectionOptions.decode(readValue(buffer)!);
|
||||
case 132:
|
||||
return MediaSelectionOptions.decode(readValue(buffer)!);
|
||||
case 133:
|
||||
return SourceSpecification.decode(readValue(buffer)!);
|
||||
case 134:
|
||||
return VideoSelectionOptions.decode(readValue(buffer)!);
|
||||
default:
|
||||
return super.readValueOfType(type, buffer);
|
||||
@ -242,17 +300,13 @@ class ImagePickerApi {
|
||||
Future<List<String?>> pickImages(
|
||||
SourceSpecification arg_source,
|
||||
ImageSelectionOptions arg_options,
|
||||
bool arg_allowMultiple,
|
||||
bool arg_usePhotoPicker) async {
|
||||
GeneralOptions arg_generalOptions) async {
|
||||
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.ImagePickerApi.pickImages', codec,
|
||||
binaryMessenger: _binaryMessenger);
|
||||
final List<Object?>? replyList = await channel.send(<Object?>[
|
||||
arg_source,
|
||||
arg_options,
|
||||
arg_allowMultiple,
|
||||
arg_usePhotoPicker
|
||||
]) as List<Object?>?;
|
||||
final List<Object?>? replyList = await channel
|
||||
.send(<Object?>[arg_source, arg_options, arg_generalOptions])
|
||||
as List<Object?>?;
|
||||
if (replyList == null) {
|
||||
throw PlatformException(
|
||||
code: 'channel-error',
|
||||
@ -281,17 +335,47 @@ class ImagePickerApi {
|
||||
Future<List<String?>> pickVideos(
|
||||
SourceSpecification arg_source,
|
||||
VideoSelectionOptions arg_options,
|
||||
bool arg_allowMultiple,
|
||||
bool arg_usePhotoPicker) async {
|
||||
GeneralOptions arg_generalOptions) async {
|
||||
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.ImagePickerApi.pickVideos', codec,
|
||||
binaryMessenger: _binaryMessenger);
|
||||
final List<Object?>? replyList = await channel.send(<Object?>[
|
||||
arg_source,
|
||||
arg_options,
|
||||
arg_allowMultiple,
|
||||
arg_usePhotoPicker
|
||||
]) as List<Object?>?;
|
||||
final List<Object?>? replyList = await channel
|
||||
.send(<Object?>[arg_source, arg_options, arg_generalOptions])
|
||||
as List<Object?>?;
|
||||
if (replyList == null) {
|
||||
throw PlatformException(
|
||||
code: 'channel-error',
|
||||
message: 'Unable to establish connection on channel.',
|
||||
);
|
||||
} else if (replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: replyList[0]! as String,
|
||||
message: replyList[1] as String?,
|
||||
details: replyList[2],
|
||||
);
|
||||
} else if (replyList[0] == null) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
} else {
|
||||
return (replyList[0] as List<Object?>?)!.cast<String?>();
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects images and videos and returns their paths.
|
||||
///
|
||||
/// Elements must not be null, by convention. See
|
||||
/// https://github.com/flutter/flutter/issues/97848
|
||||
Future<List<String?>> pickMedia(
|
||||
MediaSelectionOptions arg_mediaSelectionOptions,
|
||||
GeneralOptions arg_generalOptions) async {
|
||||
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.ImagePickerApi.pickMedia', codec,
|
||||
binaryMessenger: _binaryMessenger);
|
||||
final List<Object?>? replyList = await channel
|
||||
.send(<Object?>[arg_mediaSelectionOptions, arg_generalOptions])
|
||||
as List<Object?>?;
|
||||
if (replyList == null) {
|
||||
throw PlatformException(
|
||||
code: 'channel-error',
|
||||
|
@ -13,6 +13,11 @@ import 'package:pigeon/pigeon.dart';
|
||||
),
|
||||
copyrightHeader: 'pigeons/copyright.txt',
|
||||
))
|
||||
class GeneralOptions {
|
||||
GeneralOptions(this.allowMultiple, this.usePhotoPicker);
|
||||
bool allowMultiple;
|
||||
bool usePhotoPicker;
|
||||
}
|
||||
|
||||
/// Options for image selection and output.
|
||||
class ImageSelectionOptions {
|
||||
@ -30,6 +35,14 @@ class ImageSelectionOptions {
|
||||
int quality;
|
||||
}
|
||||
|
||||
class MediaSelectionOptions {
|
||||
MediaSelectionOptions({
|
||||
required this.imageSelectionOptions,
|
||||
});
|
||||
|
||||
ImageSelectionOptions imageSelectionOptions;
|
||||
}
|
||||
|
||||
/// Options for image selection and output.
|
||||
class VideoSelectionOptions {
|
||||
VideoSelectionOptions({this.maxDurationSeconds});
|
||||
@ -89,8 +102,11 @@ abstract class ImagePickerApi {
|
||||
/// https://github.com/flutter/flutter/issues/97848
|
||||
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
|
||||
@async
|
||||
List<String?> pickImages(SourceSpecification source,
|
||||
ImageSelectionOptions options, bool allowMultiple, bool usePhotoPicker);
|
||||
List<String?> pickImages(
|
||||
SourceSpecification source,
|
||||
ImageSelectionOptions options,
|
||||
GeneralOptions generalOptions,
|
||||
);
|
||||
|
||||
/// Selects video and returns their paths.
|
||||
///
|
||||
@ -98,8 +114,21 @@ abstract class ImagePickerApi {
|
||||
/// https://github.com/flutter/flutter/issues/97848
|
||||
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
|
||||
@async
|
||||
List<String?> pickVideos(SourceSpecification source,
|
||||
VideoSelectionOptions options, bool allowMultiple, bool usePhotoPicker);
|
||||
List<String?> pickVideos(
|
||||
SourceSpecification source,
|
||||
VideoSelectionOptions options,
|
||||
GeneralOptions generalOptions,
|
||||
);
|
||||
|
||||
/// Selects images and videos and returns their paths.
|
||||
///
|
||||
/// Elements must not be null, by convention. See
|
||||
/// https://github.com/flutter/flutter/issues/97848
|
||||
@async
|
||||
List<String?> pickMedia(
|
||||
MediaSelectionOptions mediaSelectionOptions,
|
||||
GeneralOptions generalOptions,
|
||||
);
|
||||
|
||||
/// Returns results from a previous app session, if any.
|
||||
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
|
||||
|
@ -3,7 +3,7 @@ description: Android implementation of the image_picker plugin.
|
||||
repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_android
|
||||
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
|
||||
|
||||
version: 0.8.6+20
|
||||
version: 0.8.7
|
||||
|
||||
environment:
|
||||
sdk: ">=2.18.0 <4.0.0"
|
||||
@ -22,7 +22,7 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_plugin_android_lifecycle: ^2.0.1
|
||||
image_picker_platform_interface: ^2.5.0
|
||||
image_picker_platform_interface: ^2.8.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -654,6 +654,129 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('#getMedia', () {
|
||||
test('calls the method correctly', () async {
|
||||
const List<String> fakePaths = <String>['/foo.jgp', 'bar.jpg'];
|
||||
api.returnValue = fakePaths;
|
||||
|
||||
final List<XFile> files = await picker.getMedia(
|
||||
options: const MediaOptions(
|
||||
allowMultiple: true,
|
||||
),
|
||||
);
|
||||
|
||||
expect(api.lastCall, _LastPickType.image);
|
||||
expect(files.length, 2);
|
||||
expect(files[0].path, fakePaths[0]);
|
||||
expect(files[1].path, fakePaths[1]);
|
||||
});
|
||||
|
||||
test('passes default image options', () async {
|
||||
await picker.getMedia(
|
||||
options: const MediaOptions(
|
||||
allowMultiple: true,
|
||||
),
|
||||
);
|
||||
|
||||
expect(api.passedImageOptions?.maxWidth, null);
|
||||
expect(api.passedImageOptions?.maxHeight, null);
|
||||
expect(api.passedImageOptions?.quality, 100);
|
||||
});
|
||||
|
||||
test('passes image option arguments correctly', () async {
|
||||
await picker.getMedia(
|
||||
options: const MediaOptions(
|
||||
allowMultiple: true,
|
||||
imageOptions: ImageOptions(
|
||||
maxWidth: 10.0,
|
||||
maxHeight: 20.0,
|
||||
imageQuality: 70,
|
||||
),
|
||||
));
|
||||
|
||||
expect(api.passedImageOptions?.maxWidth, 10.0);
|
||||
expect(api.passedImageOptions?.maxHeight, 20.0);
|
||||
expect(api.passedImageOptions?.quality, 70);
|
||||
});
|
||||
|
||||
test('does not accept a negative width or height argument', () {
|
||||
expect(
|
||||
() => picker.getMedia(
|
||||
options: const MediaOptions(
|
||||
allowMultiple: true,
|
||||
imageOptions: ImageOptions(maxWidth: -1.0),
|
||||
),
|
||||
),
|
||||
throwsArgumentError,
|
||||
);
|
||||
|
||||
expect(
|
||||
() => picker.getMedia(
|
||||
options: const MediaOptions(
|
||||
allowMultiple: true,
|
||||
imageOptions: ImageOptions(maxHeight: -1.0),
|
||||
),
|
||||
),
|
||||
throwsArgumentError,
|
||||
);
|
||||
});
|
||||
|
||||
test('does not accept an invalid imageQuality argument', () {
|
||||
expect(
|
||||
() => picker.getMedia(
|
||||
options: const MediaOptions(
|
||||
allowMultiple: true,
|
||||
imageOptions: ImageOptions(imageQuality: -1),
|
||||
),
|
||||
),
|
||||
throwsArgumentError,
|
||||
);
|
||||
|
||||
expect(
|
||||
() => picker.getMedia(
|
||||
options: const MediaOptions(
|
||||
allowMultiple: true,
|
||||
imageOptions: ImageOptions(imageQuality: 101),
|
||||
),
|
||||
),
|
||||
throwsArgumentError,
|
||||
);
|
||||
});
|
||||
|
||||
test('handles an empty path response gracefully', () async {
|
||||
api.returnValue = <String>[];
|
||||
|
||||
expect(
|
||||
await picker.getMedia(
|
||||
options: const MediaOptions(
|
||||
allowMultiple: true,
|
||||
),
|
||||
),
|
||||
<String>[]);
|
||||
});
|
||||
|
||||
test('defaults to not using Android Photo Picker', () async {
|
||||
await picker.getMedia(
|
||||
options: const MediaOptions(
|
||||
allowMultiple: true,
|
||||
),
|
||||
);
|
||||
|
||||
expect(api.passedPhotoPickerFlag, false);
|
||||
});
|
||||
|
||||
test('allows using Android Photo Picker', () async {
|
||||
picker.useAndroidPhotoPicker = true;
|
||||
await picker.getMedia(
|
||||
options: const MediaOptions(
|
||||
allowMultiple: true,
|
||||
),
|
||||
);
|
||||
|
||||
expect(api.passedPhotoPickerFlag, true);
|
||||
});
|
||||
});
|
||||
|
||||
group('#getImageFromSource', () {
|
||||
test('calls the method correctly', () async {
|
||||
const String fakePath = '/foo.jpg';
|
||||
@ -807,29 +930,41 @@ class _FakeImagePickerApi implements ImagePickerApi {
|
||||
|
||||
@override
|
||||
Future<List<String?>> pickImages(
|
||||
SourceSpecification source,
|
||||
ImageSelectionOptions options,
|
||||
bool allowMultiple,
|
||||
bool usePhotoPicker) async {
|
||||
SourceSpecification source,
|
||||
ImageSelectionOptions options,
|
||||
GeneralOptions generalOptions,
|
||||
) async {
|
||||
lastCall = _LastPickType.image;
|
||||
passedSource = source;
|
||||
passedImageOptions = options;
|
||||
passedAllowMultiple = allowMultiple;
|
||||
passedPhotoPickerFlag = usePhotoPicker;
|
||||
passedAllowMultiple = generalOptions.allowMultiple;
|
||||
passedPhotoPickerFlag = generalOptions.usePhotoPicker;
|
||||
return returnValue as List<String?>? ?? <String>[];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<String?>> pickMedia(
|
||||
MediaSelectionOptions options,
|
||||
GeneralOptions generalOptions,
|
||||
) async {
|
||||
lastCall = _LastPickType.image;
|
||||
passedImageOptions = options.imageSelectionOptions;
|
||||
passedPhotoPickerFlag = generalOptions.usePhotoPicker;
|
||||
passedAllowMultiple = generalOptions.allowMultiple;
|
||||
return returnValue as List<String?>? ?? <String>[];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<String?>> pickVideos(
|
||||
SourceSpecification source,
|
||||
VideoSelectionOptions options,
|
||||
bool allowMultiple,
|
||||
bool usePhotoPicker) async {
|
||||
SourceSpecification source,
|
||||
VideoSelectionOptions options,
|
||||
GeneralOptions generalOptions,
|
||||
) async {
|
||||
lastCall = _LastPickType.video;
|
||||
passedSource = source;
|
||||
passedVideoOptions = options;
|
||||
passedAllowMultiple = allowMultiple;
|
||||
passedPhotoPickerFlag = usePhotoPicker;
|
||||
passedAllowMultiple = generalOptions.allowMultiple;
|
||||
passedPhotoPickerFlag = generalOptions.usePhotoPicker;
|
||||
return returnValue as List<String?>? ?? <String>[];
|
||||
}
|
||||
|
||||
|
@ -23,15 +23,21 @@ class _TestHostImagePickerApiCodec extends StandardMessageCodec {
|
||||
} else if (value is CacheRetrievalResult) {
|
||||
buffer.putUint8(129);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is ImageSelectionOptions) {
|
||||
} else if (value is GeneralOptions) {
|
||||
buffer.putUint8(130);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is SourceSpecification) {
|
||||
} else if (value is ImageSelectionOptions) {
|
||||
buffer.putUint8(131);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is VideoSelectionOptions) {
|
||||
} else if (value is MediaSelectionOptions) {
|
||||
buffer.putUint8(132);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is SourceSpecification) {
|
||||
buffer.putUint8(133);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is VideoSelectionOptions) {
|
||||
buffer.putUint8(134);
|
||||
writeValue(buffer, value.encode());
|
||||
} else {
|
||||
super.writeValue(buffer, value);
|
||||
}
|
||||
@ -45,10 +51,14 @@ class _TestHostImagePickerApiCodec extends StandardMessageCodec {
|
||||
case 129:
|
||||
return CacheRetrievalResult.decode(readValue(buffer)!);
|
||||
case 130:
|
||||
return ImageSelectionOptions.decode(readValue(buffer)!);
|
||||
return GeneralOptions.decode(readValue(buffer)!);
|
||||
case 131:
|
||||
return SourceSpecification.decode(readValue(buffer)!);
|
||||
return ImageSelectionOptions.decode(readValue(buffer)!);
|
||||
case 132:
|
||||
return MediaSelectionOptions.decode(readValue(buffer)!);
|
||||
case 133:
|
||||
return SourceSpecification.decode(readValue(buffer)!);
|
||||
case 134:
|
||||
return VideoSelectionOptions.decode(readValue(buffer)!);
|
||||
default:
|
||||
return super.readValueOfType(type, buffer);
|
||||
@ -66,14 +76,21 @@ abstract class TestHostImagePickerApi {
|
||||
/// Elements must not be null, by convention. See
|
||||
/// https://github.com/flutter/flutter/issues/97848
|
||||
Future<List<String?>> pickImages(SourceSpecification source,
|
||||
ImageSelectionOptions options, bool allowMultiple, bool usePhotoPicker);
|
||||
ImageSelectionOptions options, GeneralOptions generalOptions);
|
||||
|
||||
/// Selects video and returns their paths.
|
||||
///
|
||||
/// Elements must not be null, by convention. See
|
||||
/// https://github.com/flutter/flutter/issues/97848
|
||||
Future<List<String?>> pickVideos(SourceSpecification source,
|
||||
VideoSelectionOptions options, bool allowMultiple, bool usePhotoPicker);
|
||||
VideoSelectionOptions options, GeneralOptions generalOptions);
|
||||
|
||||
/// Selects images and videos and returns their paths.
|
||||
///
|
||||
/// Elements must not be null, by convention. See
|
||||
/// https://github.com/flutter/flutter/issues/97848
|
||||
Future<List<String?>> pickMedia(MediaSelectionOptions mediaSelectionOptions,
|
||||
GeneralOptions generalOptions);
|
||||
|
||||
/// Returns results from a previous app session, if any.
|
||||
CacheRetrievalResult? retrieveLostResults();
|
||||
@ -102,14 +119,12 @@ abstract class TestHostImagePickerApi {
|
||||
(args[1] as ImageSelectionOptions?);
|
||||
assert(arg_options != null,
|
||||
'Argument for dev.flutter.pigeon.ImagePickerApi.pickImages was null, expected non-null ImageSelectionOptions.');
|
||||
final bool? arg_allowMultiple = (args[2] as bool?);
|
||||
assert(arg_allowMultiple != null,
|
||||
'Argument for dev.flutter.pigeon.ImagePickerApi.pickImages was null, expected non-null bool.');
|
||||
final bool? arg_usePhotoPicker = (args[3] as bool?);
|
||||
assert(arg_usePhotoPicker != null,
|
||||
'Argument for dev.flutter.pigeon.ImagePickerApi.pickImages was null, expected non-null bool.');
|
||||
final List<String?> output = await api.pickImages(arg_source!,
|
||||
arg_options!, arg_allowMultiple!, arg_usePhotoPicker!);
|
||||
final GeneralOptions? arg_generalOptions =
|
||||
(args[2] as GeneralOptions?);
|
||||
assert(arg_generalOptions != null,
|
||||
'Argument for dev.flutter.pigeon.ImagePickerApi.pickImages was null, expected non-null GeneralOptions.');
|
||||
final List<String?> output = await api.pickImages(
|
||||
arg_source!, arg_options!, arg_generalOptions!);
|
||||
return <Object?>[output];
|
||||
});
|
||||
}
|
||||
@ -136,14 +151,40 @@ abstract class TestHostImagePickerApi {
|
||||
(args[1] as VideoSelectionOptions?);
|
||||
assert(arg_options != null,
|
||||
'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideos was null, expected non-null VideoSelectionOptions.');
|
||||
final bool? arg_allowMultiple = (args[2] as bool?);
|
||||
assert(arg_allowMultiple != null,
|
||||
'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideos was null, expected non-null bool.');
|
||||
final bool? arg_usePhotoPicker = (args[3] as bool?);
|
||||
assert(arg_usePhotoPicker != null,
|
||||
'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideos was null, expected non-null bool.');
|
||||
final List<String?> output = await api.pickVideos(arg_source!,
|
||||
arg_options!, arg_allowMultiple!, arg_usePhotoPicker!);
|
||||
final GeneralOptions? arg_generalOptions =
|
||||
(args[2] as GeneralOptions?);
|
||||
assert(arg_generalOptions != null,
|
||||
'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideos was null, expected non-null GeneralOptions.');
|
||||
final List<String?> output = await api.pickVideos(
|
||||
arg_source!, arg_options!, arg_generalOptions!);
|
||||
return <Object?>[output];
|
||||
});
|
||||
}
|
||||
}
|
||||
{
|
||||
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.ImagePickerApi.pickMedia', codec,
|
||||
binaryMessenger: binaryMessenger);
|
||||
if (api == null) {
|
||||
_testBinaryMessengerBinding!.defaultBinaryMessenger
|
||||
.setMockDecodedMessageHandler<Object?>(channel, null);
|
||||
} else {
|
||||
_testBinaryMessengerBinding!.defaultBinaryMessenger
|
||||
.setMockDecodedMessageHandler<Object?>(channel,
|
||||
(Object? message) async {
|
||||
assert(message != null,
|
||||
'Argument for dev.flutter.pigeon.ImagePickerApi.pickMedia was null.');
|
||||
final List<Object?> args = (message as List<Object?>?)!;
|
||||
final MediaSelectionOptions? arg_mediaSelectionOptions =
|
||||
(args[0] as MediaSelectionOptions?);
|
||||
assert(arg_mediaSelectionOptions != null,
|
||||
'Argument for dev.flutter.pigeon.ImagePickerApi.pickMedia was null, expected non-null MediaSelectionOptions.');
|
||||
final GeneralOptions? arg_generalOptions =
|
||||
(args[1] as GeneralOptions?);
|
||||
assert(arg_generalOptions != null,
|
||||
'Argument for dev.flutter.pigeon.ImagePickerApi.pickMedia was null, expected non-null GeneralOptions.');
|
||||
final List<String?> output = await api.pickMedia(
|
||||
arg_mediaSelectionOptions!, arg_generalOptions!);
|
||||
return <Object?>[output];
|
||||
});
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -87,7 +87,8 @@ void main() {
|
||||
));
|
||||
});
|
||||
|
||||
testWidgets('Can select multiple files', (WidgetTester tester) async {
|
||||
testWidgets('getMultiImage can select multiple files',
|
||||
(WidgetTester tester) async {
|
||||
final html.FileUploadInputElement mockInput = html.FileUploadInputElement();
|
||||
|
||||
final ImagePickerPluginTestOverrides overrides =
|
||||
@ -117,6 +118,38 @@ void main() {
|
||||
expect(secondFile.length(), completion(secondTextFile.size));
|
||||
});
|
||||
|
||||
testWidgets('getMedia can select multiple files',
|
||||
(WidgetTester tester) async {
|
||||
final html.FileUploadInputElement mockInput = html.FileUploadInputElement();
|
||||
|
||||
final ImagePickerPluginTestOverrides overrides =
|
||||
ImagePickerPluginTestOverrides()
|
||||
..createInputElement = ((_, __) => mockInput)
|
||||
..getMultipleFilesFromInput =
|
||||
((_) => <html.File>[textFile, secondTextFile]);
|
||||
|
||||
final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides);
|
||||
|
||||
// Init the pick file dialog...
|
||||
final Future<List<XFile>> files =
|
||||
plugin.getMedia(options: const MediaOptions(allowMultiple: true));
|
||||
|
||||
// Mock the browser behavior of selecting a file...
|
||||
mockInput.dispatchEvent(html.Event('change'));
|
||||
|
||||
// Now the file should be available
|
||||
expect(files, completes);
|
||||
|
||||
// And readable
|
||||
expect((await files).first.readAsBytes(), completion(isNotEmpty));
|
||||
|
||||
// Peek into the second file...
|
||||
final XFile secondFile = (await files).elementAt(1);
|
||||
expect(secondFile.readAsBytes(), completion(isNotEmpty));
|
||||
expect(secondFile.name, secondTextFile.name);
|
||||
expect(secondFile.length(), completion(secondTextFile.size));
|
||||
});
|
||||
|
||||
// There's no good way of detecting when the user has "aborted" the selection.
|
||||
|
||||
testWidgets('computeCaptureAttribute', (WidgetTester tester) async {
|
||||
|
@ -10,7 +10,7 @@ dependencies:
|
||||
sdk: flutter
|
||||
image_picker_for_web:
|
||||
path: ../
|
||||
image_picker_platform_interface: ^2.2.0
|
||||
image_picker_platform_interface: ^2.8.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_driver:
|
||||
|
@ -8,6 +8,7 @@ import 'dart:html' as html;
|
||||
import 'package:flutter/foundation.dart' show visibleForTesting;
|
||||
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
|
||||
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
|
||||
import 'package:mime/mime.dart' as mime;
|
||||
|
||||
import 'src/image_resizer.dart';
|
||||
|
||||
@ -166,7 +167,7 @@ class ImagePickerPlugin extends ImagePickerPlatform {
|
||||
return files.first;
|
||||
}
|
||||
|
||||
/// Injects a file input, and returns a list of XFile that the user selected locally.
|
||||
/// Injects a file input, and returns a list of XFile images that the user selected locally.
|
||||
@override
|
||||
Future<List<XFile>> getMultiImage({
|
||||
double? maxWidth,
|
||||
@ -189,6 +190,30 @@ class ImagePickerPlugin extends ImagePickerPlatform {
|
||||
return Future.wait<XFile>(resized);
|
||||
}
|
||||
|
||||
/// Injects a file input, and returns a list of XFile media that the user selected locally.
|
||||
@override
|
||||
Future<List<XFile>> getMedia({
|
||||
required MediaOptions options,
|
||||
}) async {
|
||||
final List<XFile> images = await getFiles(
|
||||
accept: '$_kAcceptImageMimeType,$_kAcceptVideoMimeType',
|
||||
multiple: options.allowMultiple,
|
||||
);
|
||||
final Iterable<Future<XFile>> resized = images.map((XFile media) {
|
||||
if (mime.lookupMimeType(media.path)?.startsWith('image/') ?? false) {
|
||||
return _imageResizer.resizeImageIfNeeded(
|
||||
media,
|
||||
options.imageOptions.maxWidth,
|
||||
options.imageOptions.maxHeight,
|
||||
options.imageOptions.imageQuality,
|
||||
);
|
||||
}
|
||||
return Future<XFile>.value(media);
|
||||
});
|
||||
|
||||
return Future.wait<XFile>(resized);
|
||||
}
|
||||
|
||||
/// Injects a file input with the specified accept+capture attributes, and
|
||||
/// returns a list of XFile that the user selected locally.
|
||||
///
|
||||
|
@ -2,7 +2,7 @@ name: image_picker_for_web
|
||||
description: Web platform implementation of image_picker
|
||||
repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_for_web
|
||||
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
|
||||
version: 2.1.12
|
||||
version: 2.2.0
|
||||
|
||||
environment:
|
||||
sdk: ">=2.18.0 <4.0.0"
|
||||
@ -21,7 +21,8 @@ dependencies:
|
||||
sdk: flutter
|
||||
flutter_web_plugins:
|
||||
sdk: flutter
|
||||
image_picker_platform_interface: ^2.2.0
|
||||
image_picker_platform_interface: ^2.8.0
|
||||
mime: ^1.0.4
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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]);
|
||||
|
||||
|
@ -10,6 +10,7 @@ import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
void main() {
|
||||
@ -38,14 +39,14 @@ class MyHomePage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
List<XFile>? _imageFileList;
|
||||
List<XFile>? _mediaFileList;
|
||||
|
||||
void _setImageFileListFromFile(XFile? value) {
|
||||
_imageFileList = value == null ? null : <XFile>[value];
|
||||
_mediaFileList = value == null ? null : <XFile>[value];
|
||||
}
|
||||
|
||||
dynamic _pickImageError;
|
||||
bool isVideo = false;
|
||||
bool _isVideo = false;
|
||||
|
||||
VideoPlayerController? _controller;
|
||||
VideoPlayerController? _toBeDisposed;
|
||||
@ -60,18 +61,10 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
if (file != null && mounted) {
|
||||
await _disposeVideoController();
|
||||
late VideoPlayerController controller;
|
||||
if (kIsWeb) {
|
||||
controller = VideoPlayerController.network(file.path);
|
||||
} else {
|
||||
controller = VideoPlayerController.file(File(file.path));
|
||||
}
|
||||
|
||||
controller = VideoPlayerController.file(File(file.path));
|
||||
_controller = controller;
|
||||
// In web, most browsers won't honor a programmatic call to .play
|
||||
// if the video has a sound track (and is not muted).
|
||||
// Mute the video so it auto-plays in web!
|
||||
// This is not needed if the call to .play is the result of user
|
||||
// interaction (clicking on a "play" button, for example).
|
||||
const double volume = kIsWeb ? 0.0 : 1.0;
|
||||
const double volume = 1.0;
|
||||
await controller.setVolume(volume);
|
||||
await controller.initialize();
|
||||
await controller.setLooping(true);
|
||||
@ -80,13 +73,17 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onImageButtonPressed(ImageSource source,
|
||||
{required BuildContext context, bool isMultiImage = false}) async {
|
||||
Future<void> _onImageButtonPressed(
|
||||
ImageSource source, {
|
||||
required BuildContext context,
|
||||
bool isMultiImage = false,
|
||||
bool isMedia = false,
|
||||
}) async {
|
||||
if (_controller != null) {
|
||||
await _controller!.setVolume(0.0);
|
||||
}
|
||||
if (context.mounted) {
|
||||
if (isVideo) {
|
||||
if (_isVideo) {
|
||||
final XFile? file = await _picker.getVideo(
|
||||
source: source, maxDuration: const Duration(seconds: 10));
|
||||
await _playVideo(file);
|
||||
@ -94,18 +91,27 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
await _displayPickImageDialog(context,
|
||||
(double? maxWidth, double? maxHeight, int? quality) async {
|
||||
try {
|
||||
final List<XFile> pickedFileList =
|
||||
await _picker.getMultiImageWithOptions(
|
||||
options: MultiImagePickerOptions(
|
||||
imageOptions: ImageOptions(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
imageQuality: quality,
|
||||
),
|
||||
),
|
||||
);
|
||||
final List<XFile> pickedFileList = isMedia
|
||||
? await _picker.getMedia(
|
||||
options: MediaOptions(
|
||||
allowMultiple: isMultiImage,
|
||||
imageOptions: ImageOptions(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
imageQuality: quality,
|
||||
)),
|
||||
)
|
||||
: await _picker.getMultiImageWithOptions(
|
||||
options: MultiImagePickerOptions(
|
||||
imageOptions: ImageOptions(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
imageQuality: quality,
|
||||
),
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_imageFileList = pickedFileList;
|
||||
_mediaFileList = pickedFileList;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
@ -113,6 +119,31 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (isMedia) {
|
||||
await _displayPickImageDialog(context,
|
||||
(double? maxWidth, double? maxHeight, int? quality) async {
|
||||
try {
|
||||
final List<XFile> pickedFileList = <XFile>[];
|
||||
final XFile? media = _firstOrNull(await _picker.getMedia(
|
||||
options: MediaOptions(
|
||||
allowMultiple: isMultiImage,
|
||||
imageOptions: ImageOptions(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
imageQuality: quality,
|
||||
)),
|
||||
));
|
||||
|
||||
if (media != null) {
|
||||
pickedFileList.add(media);
|
||||
setState(() {
|
||||
_mediaFileList = pickedFileList;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() => _pickImageError = e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await _displayPickImageDialog(context,
|
||||
(double? maxWidth, double? maxHeight, int? quality) async {
|
||||
@ -186,22 +217,28 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
if (retrieveError != null) {
|
||||
return retrieveError;
|
||||
}
|
||||
if (_imageFileList != null) {
|
||||
if (_mediaFileList != null) {
|
||||
return Semantics(
|
||||
label: 'image_picker_example_picked_images',
|
||||
child: ListView.builder(
|
||||
key: UniqueKey(),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
// Why network for web?
|
||||
// See https://pub.dev/packages/image_picker#getting-ready-for-the-web-platform
|
||||
final String? mime = lookupMimeType(_mediaFileList![index].path);
|
||||
return Semantics(
|
||||
label: 'image_picker_example_picked_image',
|
||||
child: kIsWeb
|
||||
? Image.network(_imageFileList![index].path)
|
||||
: Image.file(File(_imageFileList![index].path)),
|
||||
child: mime == null || mime.startsWith('image/')
|
||||
? Image.file(
|
||||
File(_mediaFileList![index].path),
|
||||
errorBuilder: (BuildContext context, Object error,
|
||||
StackTrace? stackTrace) {
|
||||
return const Center(
|
||||
child: Text('This image type is not supported'));
|
||||
},
|
||||
)
|
||||
: _buildInlineVideoPlayer(index),
|
||||
);
|
||||
},
|
||||
itemCount: _imageFileList!.length,
|
||||
itemCount: _mediaFileList!.length,
|
||||
),
|
||||
);
|
||||
} else if (_pickImageError != null) {
|
||||
@ -217,8 +254,19 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildInlineVideoPlayer(int index) {
|
||||
final VideoPlayerController controller =
|
||||
VideoPlayerController.file(File(_mediaFileList![index].path));
|
||||
const double volume = kIsWeb ? 0.0 : 1.0;
|
||||
controller.setVolume(volume);
|
||||
controller.initialize();
|
||||
controller.setLooping(true);
|
||||
controller.play();
|
||||
return Center(child: AspectRatioVideo(controller));
|
||||
}
|
||||
|
||||
Widget _handlePreview() {
|
||||
if (isVideo) {
|
||||
if (_isVideo) {
|
||||
return _previewVideo();
|
||||
} else {
|
||||
return _previewImages();
|
||||
@ -240,8 +288,9 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
Semantics(
|
||||
label: 'image_picker_example_from_gallery',
|
||||
child: FloatingActionButton(
|
||||
key: const Key('image_picker_example_from_gallery'),
|
||||
onPressed: () {
|
||||
isVideo = false;
|
||||
_isVideo = false;
|
||||
_onImageButtonPressed(ImageSource.gallery, context: context);
|
||||
},
|
||||
heroTag: 'image0',
|
||||
@ -253,7 +302,40 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () {
|
||||
isVideo = false;
|
||||
_isVideo = false;
|
||||
_onImageButtonPressed(
|
||||
ImageSource.gallery,
|
||||
context: context,
|
||||
isMultiImage: true,
|
||||
isMedia: true,
|
||||
);
|
||||
},
|
||||
heroTag: 'multipleMedia',
|
||||
tooltip: 'Pick Multiple Media from gallery',
|
||||
child: const Icon(Icons.photo_library),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () {
|
||||
_isVideo = false;
|
||||
_onImageButtonPressed(
|
||||
ImageSource.gallery,
|
||||
context: context,
|
||||
isMedia: true,
|
||||
);
|
||||
},
|
||||
heroTag: 'media',
|
||||
tooltip: 'Pick Single Media from gallery',
|
||||
child: const Icon(Icons.photo_library),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () {
|
||||
_isVideo = false;
|
||||
_onImageButtonPressed(
|
||||
ImageSource.gallery,
|
||||
context: context,
|
||||
@ -269,7 +351,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () {
|
||||
isVideo = false;
|
||||
_isVideo = false;
|
||||
_onImageButtonPressed(ImageSource.camera, context: context);
|
||||
},
|
||||
heroTag: 'image2',
|
||||
@ -282,7 +364,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
child: FloatingActionButton(
|
||||
backgroundColor: Colors.red,
|
||||
onPressed: () {
|
||||
isVideo = true;
|
||||
_isVideo = true;
|
||||
_onImageButtonPressed(ImageSource.gallery, context: context);
|
||||
},
|
||||
heroTag: 'video0',
|
||||
@ -295,7 +377,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
child: FloatingActionButton(
|
||||
backgroundColor: Colors.red,
|
||||
onPressed: () {
|
||||
isVideo = true;
|
||||
_isVideo = true;
|
||||
_onImageButtonPressed(ImageSource.camera, context: context);
|
||||
},
|
||||
heroTag: 'video1',
|
||||
@ -428,3 +510,7 @@ class AspectRatioVideoState extends State<AspectRatioVideo> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
T? _firstOrNull<T>(List<T> list) {
|
||||
return list.isEmpty ? null : list.first;
|
||||
}
|
||||
|
@ -16,7 +16,8 @@ dependencies:
|
||||
# The example app is bundled with the plugin so we use a path dependency on
|
||||
# the parent directory to use the current plugin's version.
|
||||
path: ../
|
||||
image_picker_platform_interface: ^2.6.1
|
||||
image_picker_platform_interface: ^2.8.0
|
||||
mime: ^1.0.4
|
||||
video_player: ^2.1.4
|
||||
|
||||
dev_dependencies:
|
||||
|
@ -16,7 +16,10 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
+ (nullable PHAsset *)getAssetFromPHPickerResult:(PHPickerResult *)result API_AVAILABLE(ios(14));
|
||||
|
||||
// Save image with correct meta data and extention copied from the original asset.
|
||||
// Saves video to temporary URL. Returns nil on failure;
|
||||
+ (NSURL *)saveVideoFromURL:(NSURL *)videoURL;
|
||||
|
||||
// Saves image with correct meta data and extention copied from the original asset.
|
||||
// maxWidth and maxHeight are used only for GIF images.
|
||||
+ (NSString *)saveImageWithOriginalImageData:(NSData *)originalImageData
|
||||
image:(UIImage *)image
|
||||
|
@ -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
|
||||
|
@ -109,7 +109,13 @@ typedef NS_ENUM(NSInteger, ImagePickerClassType) { UIImagePickerClassType, PHPic
|
||||
PHPickerConfiguration *config =
|
||||
[[PHPickerConfiguration alloc] initWithPhotoLibrary:PHPhotoLibrary.sharedPhotoLibrary];
|
||||
config.selectionLimit = context.maxImageCount;
|
||||
config.filter = [PHPickerFilter imagesFilter];
|
||||
if (context.includeVideo) {
|
||||
config.filter = [PHPickerFilter anyFilterMatchingSubfilters:@[
|
||||
[PHPickerFilter imagesFilter], [PHPickerFilter videosFilter]
|
||||
]];
|
||||
} else {
|
||||
config.filter = [PHPickerFilter imagesFilter];
|
||||
}
|
||||
|
||||
_pickerViewController = [[PHPickerViewController alloc] initWithConfiguration:config];
|
||||
_pickerViewController.delegate = self;
|
||||
@ -128,7 +134,12 @@ typedef NS_ENUM(NSInteger, ImagePickerClassType) { UIImagePickerClassType, PHPic
|
||||
UIImagePickerController *imagePickerController = [self createImagePickerController];
|
||||
imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext;
|
||||
imagePickerController.delegate = self;
|
||||
imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage ];
|
||||
if (context.includeVideo) {
|
||||
imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage, (NSString *)kUTTypeMovie ];
|
||||
|
||||
} else {
|
||||
imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage ];
|
||||
}
|
||||
self.callContext = context;
|
||||
|
||||
switch (source.type) {
|
||||
@ -206,6 +217,29 @@ typedef NS_ENUM(NSInteger, ImagePickerClassType) { UIImagePickerClassType, PHPic
|
||||
}
|
||||
}
|
||||
|
||||
- (void)pickMediaWithMediaSelectionOptions:(nonnull FLTMediaSelectionOptions *)mediaSelectionOptions
|
||||
completion:(nonnull void (^)(NSArray<NSString *> *_Nullable,
|
||||
FlutterError *_Nullable))completion {
|
||||
FLTImagePickerMethodCallContext *context =
|
||||
[[FLTImagePickerMethodCallContext alloc] initWithResult:completion];
|
||||
context.maxSize = [mediaSelectionOptions maxSize];
|
||||
context.imageQuality = [mediaSelectionOptions imageQuality];
|
||||
context.requestFullMetadata = [mediaSelectionOptions requestFullMetadata];
|
||||
context.includeVideo = YES;
|
||||
if (![[mediaSelectionOptions allowMultiple] boolValue]) {
|
||||
context.maxImageCount = 1;
|
||||
}
|
||||
|
||||
if (@available(iOS 14, *)) {
|
||||
[self launchPHPickerWithContext:context];
|
||||
} else {
|
||||
// Camera is ignored for gallery mode, so the value here is arbitrary.
|
||||
[self launchUIImagePickerWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeGallery
|
||||
camera:FLTSourceCameraRear]
|
||||
context:context];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)pickVideoWithSource:(nonnull FLTSourceSpecification *)source
|
||||
maxDuration:(nullable NSNumber *)maxDurationSeconds
|
||||
completion:
|
||||
@ -538,25 +572,16 @@ typedef NS_ENUM(NSInteger, ImagePickerClassType) { UIImagePickerClassType, PHPic
|
||||
}
|
||||
if (videoURL != nil) {
|
||||
if (@available(iOS 13.0, *)) {
|
||||
NSString *fileName = [videoURL lastPathComponent];
|
||||
NSURL *destination =
|
||||
[NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:fileName]];
|
||||
|
||||
if ([[NSFileManager defaultManager] isReadableFileAtPath:[videoURL path]]) {
|
||||
NSError *error;
|
||||
if (![[videoURL path] isEqualToString:[destination path]]) {
|
||||
[[NSFileManager defaultManager] copyItemAtURL:videoURL toURL:destination error:&error];
|
||||
|
||||
if (error) {
|
||||
[self sendCallResultWithError:[FlutterError
|
||||
errorWithCode:@"flutter_image_picker_copy_video_error"
|
||||
message:@"Could not cache the video file."
|
||||
details:nil]];
|
||||
return;
|
||||
}
|
||||
}
|
||||
videoURL = destination;
|
||||
NSURL *destination = [FLTImagePickerPhotoAssetUtil saveVideoFromURL:videoURL];
|
||||
if (destination == nil) {
|
||||
[self sendCallResultWithError:[FlutterError
|
||||
errorWithCode:@"flutter_image_picker_copy_video_error"
|
||||
message:@"Could not cache the video file."
|
||||
details:nil]];
|
||||
return;
|
||||
}
|
||||
|
||||
videoURL = destination;
|
||||
}
|
||||
[self sendCallResultWithSavedPathList:@[ videoURL.path ]];
|
||||
} else {
|
||||
|
@ -11,7 +11,7 @@
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
* The return hander used for all method calls, which internally adapts the provided result list
|
||||
* The return handler used for all method calls, which internally adapts the provided result list
|
||||
* to return either a list or a single element depending on the original call.
|
||||
*/
|
||||
typedef void (^FlutterResultAdapter)(NSArray<NSString *> *_Nullable, FlutterError *_Nullable);
|
||||
@ -49,6 +49,9 @@ typedef void (^FlutterResultAdapter)(NSArray<NSString *> *_Nullable, FlutterErro
|
||||
/** Whether the image should be picked with full metadata (requires gallery permissions) */
|
||||
@property(nonatomic, assign) BOOL requestFullMetadata;
|
||||
|
||||
/** Whether the picker should include videos in the list*/
|
||||
@property(nonatomic, assign) BOOL includeVideo;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
@ -107,9 +107,15 @@ API_AVAILABLE(ios(14))
|
||||
[self completeOperationWithPath:nil error:flutterError];
|
||||
}
|
||||
}];
|
||||
} else if ([self.result.itemProvider
|
||||
// This supports uniform types that conform to UTTypeMovie.
|
||||
// This includes kUTTypeVideo, kUTTypeMPEG4, public.3gpp, kUTTypeMPEG,
|
||||
// public.3gpp2, public.avi, kUTTypeQuickTimeMovie.
|
||||
hasItemConformingToTypeIdentifier:UTTypeMovie.identifier]) {
|
||||
[self processVideo];
|
||||
} else {
|
||||
FlutterError *flutterError = [FlutterError errorWithCode:@"invalid_source"
|
||||
message:@"Invalid image source."
|
||||
message:@"Invalid media source."
|
||||
details:nil];
|
||||
[self completeOperationWithPath:nil error:flutterError];
|
||||
}
|
||||
@ -184,4 +190,41 @@ API_AVAILABLE(ios(14))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the video.
|
||||
*/
|
||||
- (void)processVideo API_AVAILABLE(ios(14)) {
|
||||
NSString *typeIdentifier = self.result.itemProvider.registeredTypeIdentifiers.firstObject;
|
||||
[self.result.itemProvider
|
||||
loadFileRepresentationForTypeIdentifier:typeIdentifier
|
||||
completionHandler:^(NSURL *_Nullable videoURL,
|
||||
NSError *_Nullable error) {
|
||||
if (error != nil) {
|
||||
FlutterError *flutterError =
|
||||
[FlutterError errorWithCode:@"invalid_image"
|
||||
message:error.localizedDescription
|
||||
details:error.domain];
|
||||
[self completeOperationWithPath:nil error:flutterError];
|
||||
return;
|
||||
}
|
||||
|
||||
NSURL *destination =
|
||||
[FLTImagePickerPhotoAssetUtil saveVideoFromURL:videoURL];
|
||||
if (destination == nil) {
|
||||
[self
|
||||
completeOperationWithPath:nil
|
||||
error:[FlutterError
|
||||
errorWithCode:
|
||||
@"flutter_image_picker_copy_"
|
||||
@"video_error"
|
||||
message:@"Could not cache "
|
||||
@"the video file."
|
||||
details:nil]];
|
||||
return;
|
||||
}
|
||||
|
||||
[self completeOperationWithPath:[destination path] error:nil];
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
// Autogenerated from Pigeon (v9.2.4), do not edit directly.
|
||||
// Autogenerated from Pigeon (v9.2.5), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
@ -24,6 +24,7 @@ typedef NS_ENUM(NSUInteger, FLTSourceType) {
|
||||
};
|
||||
|
||||
@class FLTMaxSize;
|
||||
@class FLTMediaSelectionOptions;
|
||||
@class FLTSourceSpecification;
|
||||
|
||||
@interface FLTMaxSize : NSObject
|
||||
@ -32,6 +33,19 @@ typedef NS_ENUM(NSUInteger, FLTSourceType) {
|
||||
@property(nonatomic, strong, nullable) NSNumber *height;
|
||||
@end
|
||||
|
||||
@interface FLTMediaSelectionOptions : NSObject
|
||||
/// `init` unavailable to enforce nonnull fields, see the `make` class method.
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
+ (instancetype)makeWithMaxSize:(FLTMaxSize *)maxSize
|
||||
imageQuality:(nullable NSNumber *)imageQuality
|
||||
requestFullMetadata:(NSNumber *)requestFullMetadata
|
||||
allowMultiple:(NSNumber *)allowMultiple;
|
||||
@property(nonatomic, strong) FLTMaxSize *maxSize;
|
||||
@property(nonatomic, strong, nullable) NSNumber *imageQuality;
|
||||
@property(nonatomic, strong) NSNumber *requestFullMetadata;
|
||||
@property(nonatomic, strong) NSNumber *allowMultiple;
|
||||
@end
|
||||
|
||||
@interface FLTSourceSpecification : NSObject
|
||||
/// `init` unavailable to enforce nonnull fields, see the `make` class method.
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
@ -57,6 +71,10 @@ NSObject<FlutterMessageCodec> *FLTImagePickerApiGetCodec(void);
|
||||
- (void)pickVideoWithSource:(FLTSourceSpecification *)source
|
||||
maxDuration:(nullable NSNumber *)maxDurationSeconds
|
||||
completion:(void (^)(NSString *_Nullable, FlutterError *_Nullable))completion;
|
||||
/// Selects images and videos and returns their paths.
|
||||
- (void)pickMediaWithMediaSelectionOptions:(FLTMediaSelectionOptions *)mediaSelectionOptions
|
||||
completion:(void (^)(NSArray<NSString *> *_Nullable,
|
||||
FlutterError *_Nullable))completion;
|
||||
@end
|
||||
|
||||
extern void FLTImagePickerApiSetup(id<FlutterBinaryMessenger> binaryMessenger,
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
// Autogenerated from Pigeon (v9.2.4), do not edit directly.
|
||||
// Autogenerated from Pigeon (v9.2.5), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
|
||||
#import "messages.g.h"
|
||||
@ -30,6 +30,12 @@ static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) {
|
||||
- (NSArray *)toList;
|
||||
@end
|
||||
|
||||
@interface FLTMediaSelectionOptions ()
|
||||
+ (FLTMediaSelectionOptions *)fromList:(NSArray *)list;
|
||||
+ (nullable FLTMediaSelectionOptions *)nullableFromList:(NSArray *)list;
|
||||
- (NSArray *)toList;
|
||||
@end
|
||||
|
||||
@interface FLTSourceSpecification ()
|
||||
+ (FLTSourceSpecification *)fromList:(NSArray *)list;
|
||||
+ (nullable FLTSourceSpecification *)nullableFromList:(NSArray *)list;
|
||||
@ -60,6 +66,42 @@ static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) {
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation FLTMediaSelectionOptions
|
||||
+ (instancetype)makeWithMaxSize:(FLTMaxSize *)maxSize
|
||||
imageQuality:(nullable NSNumber *)imageQuality
|
||||
requestFullMetadata:(NSNumber *)requestFullMetadata
|
||||
allowMultiple:(NSNumber *)allowMultiple {
|
||||
FLTMediaSelectionOptions *pigeonResult = [[FLTMediaSelectionOptions alloc] init];
|
||||
pigeonResult.maxSize = maxSize;
|
||||
pigeonResult.imageQuality = imageQuality;
|
||||
pigeonResult.requestFullMetadata = requestFullMetadata;
|
||||
pigeonResult.allowMultiple = allowMultiple;
|
||||
return pigeonResult;
|
||||
}
|
||||
+ (FLTMediaSelectionOptions *)fromList:(NSArray *)list {
|
||||
FLTMediaSelectionOptions *pigeonResult = [[FLTMediaSelectionOptions alloc] init];
|
||||
pigeonResult.maxSize = [FLTMaxSize nullableFromList:(GetNullableObjectAtIndex(list, 0))];
|
||||
NSAssert(pigeonResult.maxSize != nil, @"");
|
||||
pigeonResult.imageQuality = GetNullableObjectAtIndex(list, 1);
|
||||
pigeonResult.requestFullMetadata = GetNullableObjectAtIndex(list, 2);
|
||||
NSAssert(pigeonResult.requestFullMetadata != nil, @"");
|
||||
pigeonResult.allowMultiple = GetNullableObjectAtIndex(list, 3);
|
||||
NSAssert(pigeonResult.allowMultiple != nil, @"");
|
||||
return pigeonResult;
|
||||
}
|
||||
+ (nullable FLTMediaSelectionOptions *)nullableFromList:(NSArray *)list {
|
||||
return (list) ? [FLTMediaSelectionOptions fromList:list] : nil;
|
||||
}
|
||||
- (NSArray *)toList {
|
||||
return @[
|
||||
(self.maxSize ? [self.maxSize toList] : [NSNull null]),
|
||||
(self.imageQuality ?: [NSNull null]),
|
||||
(self.requestFullMetadata ?: [NSNull null]),
|
||||
(self.allowMultiple ?: [NSNull null]),
|
||||
];
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation FLTSourceSpecification
|
||||
+ (instancetype)makeWithType:(FLTSourceType)type camera:(FLTSourceCamera)camera {
|
||||
FLTSourceSpecification *pigeonResult = [[FLTSourceSpecification alloc] init];
|
||||
@ -92,6 +134,8 @@ static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) {
|
||||
case 128:
|
||||
return [FLTMaxSize fromList:[self readValue]];
|
||||
case 129:
|
||||
return [FLTMediaSelectionOptions fromList:[self readValue]];
|
||||
case 130:
|
||||
return [FLTSourceSpecification fromList:[self readValue]];
|
||||
default:
|
||||
return [super readValueOfType:type];
|
||||
@ -106,9 +150,12 @@ static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) {
|
||||
if ([value isKindOfClass:[FLTMaxSize class]]) {
|
||||
[self writeByte:128];
|
||||
[self writeValue:[value toList]];
|
||||
} else if ([value isKindOfClass:[FLTSourceSpecification class]]) {
|
||||
} else if ([value isKindOfClass:[FLTMediaSelectionOptions class]]) {
|
||||
[self writeByte:129];
|
||||
[self writeValue:[value toList]];
|
||||
} else if ([value isKindOfClass:[FLTSourceSpecification class]]) {
|
||||
[self writeByte:130];
|
||||
[self writeValue:[value toList]];
|
||||
} else {
|
||||
[super writeValue:value];
|
||||
}
|
||||
@ -220,4 +267,28 @@ void FLTImagePickerApiSetup(id<FlutterBinaryMessenger> binaryMessenger,
|
||||
[channel setMessageHandler:nil];
|
||||
}
|
||||
}
|
||||
/// Selects images and videos and returns their paths.
|
||||
{
|
||||
FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc]
|
||||
initWithName:@"dev.flutter.pigeon.ImagePickerApi.pickMedia"
|
||||
binaryMessenger:binaryMessenger
|
||||
codec:FLTImagePickerApiGetCodec()];
|
||||
if (api) {
|
||||
NSCAssert([api respondsToSelector:@selector(pickMediaWithMediaSelectionOptions:completion:)],
|
||||
@"FLTImagePickerApi api (%@) doesn't respond to "
|
||||
@"@selector(pickMediaWithMediaSelectionOptions:completion:)",
|
||||
api);
|
||||
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
|
||||
NSArray *args = message;
|
||||
FLTMediaSelectionOptions *arg_mediaSelectionOptions = GetNullableObjectAtIndex(args, 0);
|
||||
[api pickMediaWithMediaSelectionOptions:arg_mediaSelectionOptions
|
||||
completion:^(NSArray<NSString *> *_Nullable output,
|
||||
FlutterError *_Nullable error) {
|
||||
callback(wrapResult(output, error));
|
||||
}];
|
||||
}];
|
||||
} else {
|
||||
[channel setMessageHandler:nil];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -175,6 +175,51 @@ class ImagePickerIOS extends ImagePickerPlatform {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<XFile>> getMedia({
|
||||
required MediaOptions options,
|
||||
}) async {
|
||||
final MediaSelectionOptions mediaSelectionOptions =
|
||||
_mediaOptionsToMediaSelectionOptions(options);
|
||||
|
||||
return (await _hostApi.pickMedia(mediaSelectionOptions))
|
||||
.map((String? path) => XFile(path!))
|
||||
.toList();
|
||||
}
|
||||
|
||||
MaxSize _imageOptionsToMaxSizeWithValidation(ImageOptions imageOptions) {
|
||||
final double? maxHeight = imageOptions.maxHeight;
|
||||
final double? maxWidth = imageOptions.maxWidth;
|
||||
final int? imageQuality = imageOptions.imageQuality;
|
||||
|
||||
if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) {
|
||||
throw ArgumentError.value(
|
||||
imageQuality, 'imageQuality', 'must be between 0 and 100');
|
||||
}
|
||||
|
||||
if (maxWidth != null && maxWidth < 0) {
|
||||
throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative');
|
||||
}
|
||||
|
||||
if (maxHeight != null && maxHeight < 0) {
|
||||
throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative');
|
||||
}
|
||||
|
||||
return MaxSize(width: maxWidth, height: maxHeight);
|
||||
}
|
||||
|
||||
MediaSelectionOptions _mediaOptionsToMediaSelectionOptions(
|
||||
MediaOptions mediaOptions) {
|
||||
final MaxSize maxSize =
|
||||
_imageOptionsToMaxSizeWithValidation(mediaOptions.imageOptions);
|
||||
return MediaSelectionOptions(
|
||||
maxSize: maxSize,
|
||||
imageQuality: mediaOptions.imageOptions.imageQuality,
|
||||
requestFullMetadata: mediaOptions.imageOptions.requestFullMetadata,
|
||||
allowMultiple: mediaOptions.allowMultiple,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PickedFile?> pickVideo({
|
||||
required ImageSource source,
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
// Autogenerated from Pigeon (v9.2.4), do not edit directly.
|
||||
// Autogenerated from Pigeon (v9.2.5), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import
|
||||
|
||||
@ -47,6 +47,42 @@ class MaxSize {
|
||||
}
|
||||
}
|
||||
|
||||
class MediaSelectionOptions {
|
||||
MediaSelectionOptions({
|
||||
required this.maxSize,
|
||||
this.imageQuality,
|
||||
required this.requestFullMetadata,
|
||||
required this.allowMultiple,
|
||||
});
|
||||
|
||||
MaxSize maxSize;
|
||||
|
||||
int? imageQuality;
|
||||
|
||||
bool requestFullMetadata;
|
||||
|
||||
bool allowMultiple;
|
||||
|
||||
Object encode() {
|
||||
return <Object?>[
|
||||
maxSize.encode(),
|
||||
imageQuality,
|
||||
requestFullMetadata,
|
||||
allowMultiple,
|
||||
];
|
||||
}
|
||||
|
||||
static MediaSelectionOptions decode(Object result) {
|
||||
result as List<Object?>;
|
||||
return MediaSelectionOptions(
|
||||
maxSize: MaxSize.decode(result[0]! as List<Object?>),
|
||||
imageQuality: result[1] as int?,
|
||||
requestFullMetadata: result[2]! as bool,
|
||||
allowMultiple: result[3]! as bool,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SourceSpecification {
|
||||
SourceSpecification({
|
||||
required this.type,
|
||||
@ -80,9 +116,12 @@ class _ImagePickerApiCodec extends StandardMessageCodec {
|
||||
if (value is MaxSize) {
|
||||
buffer.putUint8(128);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is SourceSpecification) {
|
||||
} else if (value is MediaSelectionOptions) {
|
||||
buffer.putUint8(129);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is SourceSpecification) {
|
||||
buffer.putUint8(130);
|
||||
writeValue(buffer, value.encode());
|
||||
} else {
|
||||
super.writeValue(buffer, value);
|
||||
}
|
||||
@ -94,6 +133,8 @@ class _ImagePickerApiCodec extends StandardMessageCodec {
|
||||
case 128:
|
||||
return MaxSize.decode(readValue(buffer)!);
|
||||
case 129:
|
||||
return MediaSelectionOptions.decode(readValue(buffer)!);
|
||||
case 130:
|
||||
return SourceSpecification.decode(readValue(buffer)!);
|
||||
default:
|
||||
return super.readValueOfType(type, buffer);
|
||||
@ -184,4 +225,33 @@ class ImagePickerApi {
|
||||
return (replyList[0] as String?);
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects images and videos and returns their paths.
|
||||
Future<List<String?>> pickMedia(
|
||||
MediaSelectionOptions arg_mediaSelectionOptions) async {
|
||||
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.ImagePickerApi.pickMedia', codec,
|
||||
binaryMessenger: _binaryMessenger);
|
||||
final List<Object?>? replyList = await channel
|
||||
.send(<Object?>[arg_mediaSelectionOptions]) as List<Object?>?;
|
||||
if (replyList == null) {
|
||||
throw PlatformException(
|
||||
code: 'channel-error',
|
||||
message: 'Unable to establish connection on channel.',
|
||||
);
|
||||
} else if (replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: replyList[0]! as String,
|
||||
message: replyList[1] as String?,
|
||||
details: replyList[2],
|
||||
);
|
||||
} else if (replyList[0] == null) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
} else {
|
||||
return (replyList[0] as List<Object?>?)!.cast<String?>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,20 @@ class MaxSize {
|
||||
double? height;
|
||||
}
|
||||
|
||||
class MediaSelectionOptions {
|
||||
MediaSelectionOptions({
|
||||
required this.maxSize,
|
||||
this.imageQuality,
|
||||
required this.requestFullMetadata,
|
||||
required this.allowMultiple,
|
||||
});
|
||||
|
||||
MaxSize maxSize;
|
||||
int? imageQuality;
|
||||
bool requestFullMetadata;
|
||||
bool allowMultiple;
|
||||
}
|
||||
|
||||
// Corresponds to `CameraDevice` from the platform interface package.
|
||||
enum SourceCamera { rear, front }
|
||||
|
||||
@ -45,4 +59,9 @@ abstract class ImagePickerApi {
|
||||
@async
|
||||
@ObjCSelector('pickVideoWithSource:maxDuration:')
|
||||
String? pickVideo(SourceSpecification source, int? maxDurationSeconds);
|
||||
|
||||
/// Selects images and videos and returns their paths.
|
||||
@async
|
||||
@ObjCSelector('pickMediaWithMediaSelectionOptions:')
|
||||
List<String?> pickMedia(MediaSelectionOptions mediaSelectionOptions);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ name: image_picker_ios
|
||||
description: iOS implementation of the image_picker plugin.
|
||||
repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_ios
|
||||
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
|
||||
version: 0.8.7+4
|
||||
version: 0.8.8
|
||||
|
||||
environment:
|
||||
sdk: ">=2.18.0 <4.0.0"
|
||||
@ -19,7 +19,7 @@ flutter:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
image_picker_platform_interface: ^2.6.1
|
||||
image_picker_platform_interface: ^2.8.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -71,6 +71,19 @@ class _ApiLogger implements TestHostImagePickerApi {
|
||||
return returnValue as List<String?>?;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<String?>> pickMedia(
|
||||
MediaSelectionOptions mediaSelectionOptions) async {
|
||||
calls.add(_LoggedMethodCall('pickMedia', arguments: <String, dynamic>{
|
||||
'maxWidth': mediaSelectionOptions.maxSize.width,
|
||||
'maxHeight': mediaSelectionOptions.maxSize.height,
|
||||
'imageQuality': mediaSelectionOptions.imageQuality,
|
||||
'requestFullMetadata': mediaSelectionOptions.requestFullMetadata,
|
||||
'allowMultiple': mediaSelectionOptions.allowMultiple,
|
||||
}));
|
||||
return returnValue as List<String?>;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> pickVideo(
|
||||
SourceSpecification source, int? maxDurationSeconds) async {
|
||||
@ -878,6 +891,227 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('#getMedia', () {
|
||||
test('calls the method correctly', () async {
|
||||
log.returnValue = <String>['0', '1'];
|
||||
await picker.getMedia(options: const MediaOptions(allowMultiple: true));
|
||||
|
||||
expect(
|
||||
log.calls,
|
||||
<_LoggedMethodCall>[
|
||||
const _LoggedMethodCall('pickMedia', arguments: <String, dynamic>{
|
||||
'maxWidth': null,
|
||||
'maxHeight': null,
|
||||
'imageQuality': null,
|
||||
'requestFullMetadata': true,
|
||||
'allowMultiple': true
|
||||
}),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('passes the width and height arguments correctly', () async {
|
||||
log.returnValue = <String>['0', '1'];
|
||||
await picker.getMedia(options: const MediaOptions(allowMultiple: true));
|
||||
await picker.getMedia(
|
||||
options: MediaOptions(
|
||||
allowMultiple: true,
|
||||
imageOptions: ImageOptions.createAndValidate(
|
||||
maxWidth: 10.0,
|
||||
),
|
||||
));
|
||||
await picker.getMedia(
|
||||
options: MediaOptions(
|
||||
allowMultiple: true,
|
||||
imageOptions: ImageOptions.createAndValidate(
|
||||
maxHeight: 10.0,
|
||||
),
|
||||
));
|
||||
await picker.getMedia(
|
||||
options: MediaOptions(
|
||||
allowMultiple: true,
|
||||
imageOptions: ImageOptions.createAndValidate(
|
||||
maxWidth: 10.0,
|
||||
maxHeight: 20.0,
|
||||
),
|
||||
));
|
||||
await picker.getMedia(
|
||||
options: MediaOptions(
|
||||
allowMultiple: true,
|
||||
imageOptions: ImageOptions.createAndValidate(
|
||||
maxWidth: 10.0,
|
||||
imageQuality: 70,
|
||||
),
|
||||
));
|
||||
await picker.getMedia(
|
||||
options: MediaOptions(
|
||||
allowMultiple: true,
|
||||
imageOptions: ImageOptions.createAndValidate(
|
||||
maxHeight: 10.0,
|
||||
imageQuality: 70,
|
||||
),
|
||||
));
|
||||
await picker.getMedia(
|
||||
options: MediaOptions(
|
||||
allowMultiple: true,
|
||||
imageOptions: ImageOptions.createAndValidate(
|
||||
maxWidth: 10.0,
|
||||
maxHeight: 20.0,
|
||||
imageQuality: 70,
|
||||
),
|
||||
));
|
||||
|
||||
expect(
|
||||
log.calls,
|
||||
<_LoggedMethodCall>[
|
||||
const _LoggedMethodCall('pickMedia', arguments: <String, dynamic>{
|
||||
'maxWidth': null,
|
||||
'maxHeight': null,
|
||||
'imageQuality': null,
|
||||
'requestFullMetadata': true,
|
||||
'allowMultiple': true
|
||||
}),
|
||||
const _LoggedMethodCall('pickMedia', arguments: <String, dynamic>{
|
||||
'maxWidth': 10.0,
|
||||
'maxHeight': null,
|
||||
'imageQuality': null,
|
||||
'requestFullMetadata': true,
|
||||
'allowMultiple': true
|
||||
}),
|
||||
const _LoggedMethodCall('pickMedia', arguments: <String, dynamic>{
|
||||
'maxWidth': null,
|
||||
'maxHeight': 10.0,
|
||||
'imageQuality': null,
|
||||
'requestFullMetadata': true,
|
||||
'allowMultiple': true
|
||||
}),
|
||||
const _LoggedMethodCall('pickMedia', arguments: <String, dynamic>{
|
||||
'maxWidth': 10.0,
|
||||
'maxHeight': 20.0,
|
||||
'imageQuality': null,
|
||||
'requestFullMetadata': true,
|
||||
'allowMultiple': true
|
||||
}),
|
||||
const _LoggedMethodCall('pickMedia', arguments: <String, dynamic>{
|
||||
'maxWidth': 10.0,
|
||||
'maxHeight': null,
|
||||
'imageQuality': 70,
|
||||
'requestFullMetadata': true,
|
||||
'allowMultiple': true
|
||||
}),
|
||||
const _LoggedMethodCall('pickMedia', arguments: <String, dynamic>{
|
||||
'maxWidth': null,
|
||||
'maxHeight': 10.0,
|
||||
'imageQuality': 70,
|
||||
'requestFullMetadata': true,
|
||||
'allowMultiple': true
|
||||
}),
|
||||
const _LoggedMethodCall('pickMedia', arguments: <String, dynamic>{
|
||||
'maxWidth': 10.0,
|
||||
'maxHeight': 20.0,
|
||||
'imageQuality': 70,
|
||||
'requestFullMetadata': true,
|
||||
'allowMultiple': true
|
||||
}),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('passes request metadata argument correctly', () async {
|
||||
log.returnValue = <String>['0', '1'];
|
||||
await picker.getMedia(
|
||||
options: const MediaOptions(
|
||||
allowMultiple: true,
|
||||
imageOptions: ImageOptions(requestFullMetadata: false),
|
||||
));
|
||||
|
||||
expect(
|
||||
log.calls,
|
||||
<_LoggedMethodCall>[
|
||||
const _LoggedMethodCall('pickMedia', arguments: <String, dynamic>{
|
||||
'maxWidth': null,
|
||||
'maxHeight': null,
|
||||
'imageQuality': null,
|
||||
'requestFullMetadata': false,
|
||||
'allowMultiple': true
|
||||
}),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('passes allowMultiple argument correctly', () async {
|
||||
log.returnValue = <String>['0', '1'];
|
||||
await picker.getMedia(
|
||||
options: const MediaOptions(
|
||||
allowMultiple: false,
|
||||
));
|
||||
|
||||
expect(
|
||||
log.calls,
|
||||
<_LoggedMethodCall>[
|
||||
const _LoggedMethodCall('pickMedia', arguments: <String, dynamic>{
|
||||
'maxWidth': null,
|
||||
'maxHeight': null,
|
||||
'imageQuality': null,
|
||||
'requestFullMetadata': true,
|
||||
'allowMultiple': false
|
||||
}),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('does not accept a negative width or height argument', () {
|
||||
log.returnValue = <String>['0', '1'];
|
||||
expect(
|
||||
() => picker.getMedia(
|
||||
options: MediaOptions(
|
||||
allowMultiple: true,
|
||||
imageOptions: ImageOptions.createAndValidate(maxWidth: -1.0),
|
||||
)),
|
||||
throwsArgumentError,
|
||||
);
|
||||
|
||||
expect(
|
||||
() => picker.getMedia(
|
||||
options: MediaOptions(
|
||||
allowMultiple: true,
|
||||
imageOptions: ImageOptions.createAndValidate(maxHeight: -1.0),
|
||||
)),
|
||||
throwsArgumentError,
|
||||
);
|
||||
});
|
||||
|
||||
test('does not accept a invalid imageQuality argument', () {
|
||||
log.returnValue = <String>['0', '1'];
|
||||
expect(
|
||||
() => picker.getMedia(
|
||||
options: MediaOptions(
|
||||
allowMultiple: true,
|
||||
imageOptions: ImageOptions.createAndValidate(imageQuality: -1),
|
||||
)),
|
||||
throwsArgumentError,
|
||||
);
|
||||
|
||||
expect(
|
||||
() => picker.getMedia(
|
||||
options: MediaOptions(
|
||||
allowMultiple: true,
|
||||
imageOptions: ImageOptions.createAndValidate(imageQuality: 101),
|
||||
)),
|
||||
throwsArgumentError,
|
||||
);
|
||||
});
|
||||
|
||||
test('handles a empty path response gracefully', () async {
|
||||
log.returnValue = <String>[];
|
||||
|
||||
expect(
|
||||
await picker.getMedia(
|
||||
options: const MediaOptions(allowMultiple: true)),
|
||||
<String>[]);
|
||||
});
|
||||
});
|
||||
|
||||
group('#getVideo', () {
|
||||
test('passes the image source argument correctly', () async {
|
||||
await picker.getVideo(source: ImageSource.camera);
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
// Autogenerated from Pigeon (v9.2.4), do not edit directly.
|
||||
// Autogenerated from Pigeon (v9.2.5), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import
|
||||
// ignore_for_file: avoid_relative_lib_imports
|
||||
@ -20,9 +20,12 @@ class _TestHostImagePickerApiCodec extends StandardMessageCodec {
|
||||
if (value is MaxSize) {
|
||||
buffer.putUint8(128);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is SourceSpecification) {
|
||||
} else if (value is MediaSelectionOptions) {
|
||||
buffer.putUint8(129);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is SourceSpecification) {
|
||||
buffer.putUint8(130);
|
||||
writeValue(buffer, value.encode());
|
||||
} else {
|
||||
super.writeValue(buffer, value);
|
||||
}
|
||||
@ -34,6 +37,8 @@ class _TestHostImagePickerApiCodec extends StandardMessageCodec {
|
||||
case 128:
|
||||
return MaxSize.decode(readValue(buffer)!);
|
||||
case 129:
|
||||
return MediaSelectionOptions.decode(readValue(buffer)!);
|
||||
case 130:
|
||||
return SourceSpecification.decode(readValue(buffer)!);
|
||||
default:
|
||||
return super.readValueOfType(type, buffer);
|
||||
@ -55,6 +60,9 @@ abstract class TestHostImagePickerApi {
|
||||
Future<String?> pickVideo(
|
||||
SourceSpecification source, int? maxDurationSeconds);
|
||||
|
||||
/// Selects images and videos and returns their paths.
|
||||
Future<List<String?>> pickMedia(MediaSelectionOptions mediaSelectionOptions);
|
||||
|
||||
static void setup(TestHostImagePickerApi? api,
|
||||
{BinaryMessenger? binaryMessenger}) {
|
||||
{
|
||||
@ -140,5 +148,29 @@ abstract class TestHostImagePickerApi {
|
||||
});
|
||||
}
|
||||
}
|
||||
{
|
||||
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.ImagePickerApi.pickMedia', codec,
|
||||
binaryMessenger: binaryMessenger);
|
||||
if (api == null) {
|
||||
_testBinaryMessengerBinding!.defaultBinaryMessenger
|
||||
.setMockDecodedMessageHandler<Object?>(channel, null);
|
||||
} else {
|
||||
_testBinaryMessengerBinding!.defaultBinaryMessenger
|
||||
.setMockDecodedMessageHandler<Object?>(channel,
|
||||
(Object? message) async {
|
||||
assert(message != null,
|
||||
'Argument for dev.flutter.pigeon.ImagePickerApi.pickMedia was null.');
|
||||
final List<Object?> args = (message as List<Object?>?)!;
|
||||
final MediaSelectionOptions? arg_mediaSelectionOptions =
|
||||
(args[0] as MediaSelectionOptions?);
|
||||
assert(arg_mediaSelectionOptions != null,
|
||||
'Argument for dev.flutter.pigeon.ImagePickerApi.pickMedia was null, expected non-null MediaSelectionOptions.');
|
||||
final List<String?> output =
|
||||
await api.pickMedia(arg_mediaSelectionOptions!);
|
||||
return <Object?>[output];
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,7 @@
|
||||
## 0.2.1
|
||||
|
||||
* Adds `getMedia` method.
|
||||
|
||||
## 0.2.0
|
||||
|
||||
* Implements initial Linux support.
|
||||
|
@ -9,6 +9,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
void main() {
|
||||
@ -37,11 +38,11 @@ class MyHomePage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
List<XFile>? _imageFileList;
|
||||
List<XFile>? _mediaFileList;
|
||||
|
||||
// This must be called from within a setState() callback
|
||||
void _setImageFileListFromFile(XFile? value) {
|
||||
_imageFileList = value == null ? null : <XFile>[value];
|
||||
_mediaFileList = value == null ? null : <XFile>[value];
|
||||
}
|
||||
|
||||
dynamic _pickImageError;
|
||||
@ -70,52 +71,12 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleMultiImagePicked(BuildContext context) async {
|
||||
await _displayPickImageDialog(context,
|
||||
(double? maxWidth, double? maxHeight, int? quality) async {
|
||||
try {
|
||||
final List<XFile>? pickedFileList = await _picker.getMultiImage(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
imageQuality: quality,
|
||||
);
|
||||
setState(() {
|
||||
_imageFileList = pickedFileList;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_pickImageError = e;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _handleSingleImagePicked(
|
||||
BuildContext context, ImageSource source) async {
|
||||
await _displayPickImageDialog(context,
|
||||
(double? maxWidth, double? maxHeight, int? quality) async {
|
||||
try {
|
||||
final XFile? pickedFile = await _picker.getImageFromSource(
|
||||
source: source,
|
||||
options: ImagePickerOptions(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
imageQuality: quality,
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_setImageFileListFromFile(pickedFile);
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_pickImageError = e;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onImageButtonPressed(ImageSource source,
|
||||
{required BuildContext context, bool isMultiImage = false}) async {
|
||||
Future<void> _onImageButtonPressed(
|
||||
ImageSource source, {
|
||||
required BuildContext context,
|
||||
bool isMultiImage = false,
|
||||
bool isMedia = false,
|
||||
}) async {
|
||||
if (_controller != null) {
|
||||
await _controller!.setVolume(0.0);
|
||||
}
|
||||
@ -125,9 +86,83 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
source: source, maxDuration: const Duration(seconds: 10));
|
||||
await _playVideo(file);
|
||||
} else if (isMultiImage) {
|
||||
await _handleMultiImagePicked(context);
|
||||
await _displayPickImageDialog(context,
|
||||
(double? maxWidth, double? maxHeight, int? quality) async {
|
||||
try {
|
||||
final List<XFile> pickedFileList = isMedia
|
||||
? await _picker.getMedia(
|
||||
options: MediaOptions(
|
||||
allowMultiple: isMultiImage,
|
||||
imageOptions: ImageOptions(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
imageQuality: quality,
|
||||
)),
|
||||
)
|
||||
: await _picker.getMultiImageWithOptions(
|
||||
options: MultiImagePickerOptions(
|
||||
imageOptions: ImageOptions(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
imageQuality: quality,
|
||||
),
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_mediaFileList = pickedFileList;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_pickImageError = e;
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (isMedia) {
|
||||
await _displayPickImageDialog(context,
|
||||
(double? maxWidth, double? maxHeight, int? quality) async {
|
||||
try {
|
||||
final List<XFile> pickedFileList = <XFile>[];
|
||||
final XFile? media = _firstOrNull(await _picker.getMedia(
|
||||
options: MediaOptions(
|
||||
allowMultiple: isMultiImage,
|
||||
imageOptions: ImageOptions(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
imageQuality: quality,
|
||||
)),
|
||||
));
|
||||
|
||||
if (media != null) {
|
||||
pickedFileList.add(media);
|
||||
setState(() {
|
||||
_mediaFileList = pickedFileList;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() => _pickImageError = e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await _handleSingleImagePicked(context, source);
|
||||
await _displayPickImageDialog(context,
|
||||
(double? maxWidth, double? maxHeight, int? quality) async {
|
||||
try {
|
||||
final XFile? pickedFile = await _picker.getImageFromSource(
|
||||
source: source,
|
||||
options: ImagePickerOptions(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
imageQuality: quality,
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_setImageFileListFromFile(pickedFile);
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_pickImageError = e;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -180,18 +215,28 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
if (retrieveError != null) {
|
||||
return retrieveError;
|
||||
}
|
||||
if (_imageFileList != null) {
|
||||
if (_mediaFileList != null) {
|
||||
return Semantics(
|
||||
label: 'image_picker_example_picked_images',
|
||||
child: ListView.builder(
|
||||
key: UniqueKey(),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final String? mime = lookupMimeType(_mediaFileList![index].path);
|
||||
return Semantics(
|
||||
label: 'image_picker_example_picked_image',
|
||||
child: Image.file(File(_imageFileList![index].path)),
|
||||
child: mime == null || mime.startsWith('image/')
|
||||
? Image.file(
|
||||
File(_mediaFileList![index].path),
|
||||
errorBuilder: (BuildContext context, Object error,
|
||||
StackTrace? stackTrace) {
|
||||
return const Center(
|
||||
child: Text('This image type is not supported'));
|
||||
},
|
||||
)
|
||||
: _buildInlineVideoPlayer(index),
|
||||
);
|
||||
},
|
||||
itemCount: _imageFileList!.length,
|
||||
itemCount: _mediaFileList!.length,
|
||||
),
|
||||
);
|
||||
} else if (_pickImageError != null) {
|
||||
@ -207,6 +252,17 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildInlineVideoPlayer(int index) {
|
||||
final VideoPlayerController controller =
|
||||
VideoPlayerController.file(File(_mediaFileList![index].path));
|
||||
const double volume = 1.0;
|
||||
controller.setVolume(volume);
|
||||
controller.initialize();
|
||||
controller.setLooping(true);
|
||||
controller.play();
|
||||
return Center(child: AspectRatioVideo(controller));
|
||||
}
|
||||
|
||||
Widget _handlePreview() {
|
||||
if (_isVideo) {
|
||||
return _previewVideo();
|
||||
@ -230,6 +286,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
Semantics(
|
||||
label: 'image_picker_example_from_gallery',
|
||||
child: FloatingActionButton(
|
||||
key: const Key('image_picker_example_from_gallery'),
|
||||
onPressed: () {
|
||||
_isVideo = false;
|
||||
_onImageButtonPressed(ImageSource.gallery, context: context);
|
||||
@ -239,6 +296,39 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
child: const Icon(Icons.photo),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () {
|
||||
_isVideo = false;
|
||||
_onImageButtonPressed(
|
||||
ImageSource.gallery,
|
||||
context: context,
|
||||
isMultiImage: true,
|
||||
isMedia: true,
|
||||
);
|
||||
},
|
||||
heroTag: 'multipleMedia',
|
||||
tooltip: 'Pick Multiple Media from gallery',
|
||||
child: const Icon(Icons.photo_library),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () {
|
||||
_isVideo = false;
|
||||
_onImageButtonPressed(
|
||||
ImageSource.gallery,
|
||||
context: context,
|
||||
isMedia: true,
|
||||
);
|
||||
},
|
||||
heroTag: 'media',
|
||||
tooltip: 'Pick Single Media from gallery',
|
||||
child: const Icon(Icons.photo_library),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: FloatingActionButton(
|
||||
@ -420,3 +510,7 @@ class AspectRatioVideoState extends State<AspectRatioVideo> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
T? _firstOrNull<T>(List<T> list) {
|
||||
return list.isEmpty ? null : list.first;
|
||||
}
|
||||
|
@ -17,7 +17,8 @@ dependencies:
|
||||
# The example app is bundled with the plugin so we use a path dependency on
|
||||
# the parent directory to use the current plugin's version.
|
||||
path: ..
|
||||
image_picker_platform_interface: ^2.7.0
|
||||
image_picker_platform_interface: ^2.8.0
|
||||
mime: ^1.0.4
|
||||
video_player: ^2.1.4
|
||||
|
||||
dev_dependencies:
|
||||
|
@ -154,4 +154,27 @@ class ImagePickerLinux extends CameraDelegatingImagePickerPlatform {
|
||||
.openFiles(acceptedTypeGroups: <XTypeGroup>[typeGroup]);
|
||||
return files;
|
||||
}
|
||||
|
||||
// `maxWidth`, `maxHeight`, and `imageQuality` arguments are not currently
|
||||
// supported. If any of these arguments are supplied, they will be silently
|
||||
// ignored.
|
||||
@override
|
||||
Future<List<XFile>> getMedia({required MediaOptions options}) async {
|
||||
const XTypeGroup typeGroup = XTypeGroup(
|
||||
label: 'images and videos', extensions: <String>['image/*', 'video/*']);
|
||||
|
||||
List<XFile> files;
|
||||
|
||||
if (options.allowMultiple) {
|
||||
files = await fileSelector
|
||||
.openFiles(acceptedTypeGroups: <XTypeGroup>[typeGroup]);
|
||||
} else {
|
||||
final XFile? file = await fileSelector
|
||||
.openFile(acceptedTypeGroups: <XTypeGroup>[typeGroup]);
|
||||
files = <XFile>[
|
||||
if (file != null) file,
|
||||
];
|
||||
}
|
||||
return files;
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ name: image_picker_linux
|
||||
description: Linux platform implementation of image_picker
|
||||
repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_linux
|
||||
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
|
||||
version: 0.2.0
|
||||
version: 0.2.1
|
||||
|
||||
environment:
|
||||
sdk: ">=2.18.0 <4.0.0"
|
||||
@ -20,7 +20,7 @@ dependencies:
|
||||
file_selector_platform_interface: ^2.2.0
|
||||
flutter:
|
||||
sdk: flutter
|
||||
image_picker_platform_interface: ^2.7.0
|
||||
image_picker_platform_interface: ^2.8.0
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.1.5
|
||||
|
@ -125,6 +125,38 @@ void main() {
|
||||
plugin.getVideo(source: ImageSource.camera), throwsStateError);
|
||||
});
|
||||
});
|
||||
|
||||
group('media', () {
|
||||
test('getMedia passes the accepted type groups correctly', () async {
|
||||
await plugin.getMedia(options: const MediaOptions(allowMultiple: true));
|
||||
|
||||
final VerificationResult result = verify(
|
||||
mockFileSelectorPlatform.openFiles(
|
||||
acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups')));
|
||||
expect(capturedTypeGroups(result)[0].extensions,
|
||||
<String>['image/*', 'video/*']);
|
||||
});
|
||||
|
||||
test('multiple media handles an empty path response gracefully', () async {
|
||||
expect(
|
||||
await plugin.getMedia(
|
||||
options: const MediaOptions(
|
||||
allowMultiple: true,
|
||||
),
|
||||
),
|
||||
<String>[]);
|
||||
});
|
||||
|
||||
test('single media handles an empty path response gracefully', () async {
|
||||
expect(
|
||||
await plugin.getMedia(
|
||||
options: const MediaOptions(
|
||||
allowMultiple: false,
|
||||
),
|
||||
),
|
||||
<String>[]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class FakeCameraDelegate extends ImagePickerCameraDelegate {
|
||||
|
@ -1,3 +1,7 @@
|
||||
## 0.2.1
|
||||
|
||||
* Adds `getMedia` method.
|
||||
|
||||
## 0.2.0
|
||||
|
||||
* Implements initial macOS support.
|
||||
|
@ -9,6 +9,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
void main() {
|
||||
@ -37,11 +38,11 @@ class MyHomePage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
List<XFile>? _imageFileList;
|
||||
List<XFile>? _mediaFileList;
|
||||
|
||||
// This must be called from within a setState() callback
|
||||
void _setImageFileListFromFile(XFile? value) {
|
||||
_imageFileList = value == null ? null : <XFile>[value];
|
||||
_mediaFileList = value == null ? null : <XFile>[value];
|
||||
}
|
||||
|
||||
dynamic _pickImageError;
|
||||
@ -70,52 +71,12 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleMultiImagePicked(BuildContext context) async {
|
||||
await _displayPickImageDialog(context,
|
||||
(double? maxWidth, double? maxHeight, int? quality) async {
|
||||
try {
|
||||
final List<XFile>? pickedFileList = await _picker.getMultiImage(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
imageQuality: quality,
|
||||
);
|
||||
setState(() {
|
||||
_imageFileList = pickedFileList;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_pickImageError = e;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _handleSingleImagePicked(
|
||||
BuildContext context, ImageSource source) async {
|
||||
await _displayPickImageDialog(context,
|
||||
(double? maxWidth, double? maxHeight, int? quality) async {
|
||||
try {
|
||||
final XFile? pickedFile = await _picker.getImageFromSource(
|
||||
source: source,
|
||||
options: ImagePickerOptions(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
imageQuality: quality,
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_setImageFileListFromFile(pickedFile);
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_pickImageError = e;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onImageButtonPressed(ImageSource source,
|
||||
{required BuildContext context, bool isMultiImage = false}) async {
|
||||
Future<void> _onImageButtonPressed(
|
||||
ImageSource source, {
|
||||
required BuildContext context,
|
||||
bool isMultiImage = false,
|
||||
bool isMedia = false,
|
||||
}) async {
|
||||
if (_controller != null) {
|
||||
await _controller!.setVolume(0.0);
|
||||
}
|
||||
@ -125,9 +86,83 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
source: source, maxDuration: const Duration(seconds: 10));
|
||||
await _playVideo(file);
|
||||
} else if (isMultiImage) {
|
||||
await _handleMultiImagePicked(context);
|
||||
await _displayPickImageDialog(context,
|
||||
(double? maxWidth, double? maxHeight, int? quality) async {
|
||||
try {
|
||||
final List<XFile> pickedFileList = isMedia
|
||||
? await _picker.getMedia(
|
||||
options: MediaOptions(
|
||||
allowMultiple: isMultiImage,
|
||||
imageOptions: ImageOptions(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
imageQuality: quality,
|
||||
)),
|
||||
)
|
||||
: await _picker.getMultiImageWithOptions(
|
||||
options: MultiImagePickerOptions(
|
||||
imageOptions: ImageOptions(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
imageQuality: quality,
|
||||
),
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_mediaFileList = pickedFileList;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_pickImageError = e;
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (isMedia) {
|
||||
await _displayPickImageDialog(context,
|
||||
(double? maxWidth, double? maxHeight, int? quality) async {
|
||||
try {
|
||||
final List<XFile> pickedFileList = <XFile>[];
|
||||
final XFile? media = _firstOrNull(await _picker.getMedia(
|
||||
options: MediaOptions(
|
||||
allowMultiple: isMultiImage,
|
||||
imageOptions: ImageOptions(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
imageQuality: quality,
|
||||
)),
|
||||
));
|
||||
|
||||
if (media != null) {
|
||||
pickedFileList.add(media);
|
||||
setState(() {
|
||||
_mediaFileList = pickedFileList;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() => _pickImageError = e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await _handleSingleImagePicked(context, source);
|
||||
await _displayPickImageDialog(context,
|
||||
(double? maxWidth, double? maxHeight, int? quality) async {
|
||||
try {
|
||||
final XFile? pickedFile = await _picker.getImageFromSource(
|
||||
source: source,
|
||||
options: ImagePickerOptions(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
imageQuality: quality,
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_setImageFileListFromFile(pickedFile);
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_pickImageError = e;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -180,18 +215,28 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
if (retrieveError != null) {
|
||||
return retrieveError;
|
||||
}
|
||||
if (_imageFileList != null) {
|
||||
if (_mediaFileList != null) {
|
||||
return Semantics(
|
||||
label: 'image_picker_example_picked_images',
|
||||
child: ListView.builder(
|
||||
key: UniqueKey(),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final String? mime = lookupMimeType(_mediaFileList![index].path);
|
||||
return Semantics(
|
||||
label: 'image_picker_example_picked_image',
|
||||
child: Image.file(File(_imageFileList![index].path)),
|
||||
child: mime == null || mime.startsWith('image/')
|
||||
? Image.file(
|
||||
File(_mediaFileList![index].path),
|
||||
errorBuilder: (BuildContext context, Object error,
|
||||
StackTrace? stackTrace) {
|
||||
return const Center(
|
||||
child: Text('This image type is not supported'));
|
||||
},
|
||||
)
|
||||
: _buildInlineVideoPlayer(index),
|
||||
);
|
||||
},
|
||||
itemCount: _imageFileList!.length,
|
||||
itemCount: _mediaFileList!.length,
|
||||
),
|
||||
);
|
||||
} else if (_pickImageError != null) {
|
||||
@ -207,6 +252,17 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildInlineVideoPlayer(int index) {
|
||||
final VideoPlayerController controller =
|
||||
VideoPlayerController.file(File(_mediaFileList![index].path));
|
||||
const double volume = 1.0;
|
||||
controller.setVolume(volume);
|
||||
controller.initialize();
|
||||
controller.setLooping(true);
|
||||
controller.play();
|
||||
return Center(child: AspectRatioVideo(controller));
|
||||
}
|
||||
|
||||
Widget _handlePreview() {
|
||||
if (_isVideo) {
|
||||
return _previewVideo();
|
||||
@ -230,6 +286,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
Semantics(
|
||||
label: 'image_picker_example_from_gallery',
|
||||
child: FloatingActionButton(
|
||||
key: const Key('image_picker_example_from_gallery'),
|
||||
onPressed: () {
|
||||
_isVideo = false;
|
||||
_onImageButtonPressed(ImageSource.gallery, context: context);
|
||||
@ -239,6 +296,39 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
child: const Icon(Icons.photo),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () {
|
||||
_isVideo = false;
|
||||
_onImageButtonPressed(
|
||||
ImageSource.gallery,
|
||||
context: context,
|
||||
isMultiImage: true,
|
||||
isMedia: true,
|
||||
);
|
||||
},
|
||||
heroTag: 'multipleMedia',
|
||||
tooltip: 'Pick Multiple Media from gallery',
|
||||
child: const Icon(Icons.photo_library),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () {
|
||||
_isVideo = false;
|
||||
_onImageButtonPressed(
|
||||
ImageSource.gallery,
|
||||
context: context,
|
||||
isMedia: true,
|
||||
);
|
||||
},
|
||||
heroTag: 'media',
|
||||
tooltip: 'Pick Single Media from gallery',
|
||||
child: const Icon(Icons.photo_library),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: FloatingActionButton(
|
||||
@ -420,3 +510,7 @@ class AspectRatioVideoState extends State<AspectRatioVideo> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
T? _firstOrNull<T>(List<T> list) {
|
||||
return list.isEmpty ? null : list.first;
|
||||
}
|
||||
|
@ -17,7 +17,8 @@ dependencies:
|
||||
# The example app is bundled with the plugin so we use a path dependency on
|
||||
# the parent directory to use the current plugin's version.
|
||||
path: ..
|
||||
image_picker_platform_interface: ^2.7.0
|
||||
image_picker_platform_interface: ^2.8.0
|
||||
mime: ^1.0.4
|
||||
video_player: ^2.1.4
|
||||
|
||||
dev_dependencies:
|
||||
|
@ -159,4 +159,28 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform {
|
||||
.openFiles(acceptedTypeGroups: <XTypeGroup>[typeGroup]);
|
||||
return files;
|
||||
}
|
||||
|
||||
// `maxWidth`, `maxHeight`, and `imageQuality` arguments are not currently
|
||||
// supported. If any of these arguments are supplied, they will be silently
|
||||
// ignored.
|
||||
@override
|
||||
Future<List<XFile>> getMedia({required MediaOptions options}) async {
|
||||
const XTypeGroup typeGroup = XTypeGroup(
|
||||
label: 'images and videos',
|
||||
extensions: <String>['public.image', 'public.movie']);
|
||||
|
||||
List<XFile> files;
|
||||
|
||||
if (options.allowMultiple) {
|
||||
files = await fileSelector
|
||||
.openFiles(acceptedTypeGroups: <XTypeGroup>[typeGroup]);
|
||||
} else {
|
||||
final XFile? file = await fileSelector
|
||||
.openFile(acceptedTypeGroups: <XTypeGroup>[typeGroup]);
|
||||
files = <XFile>[
|
||||
if (file != null) file,
|
||||
];
|
||||
}
|
||||
return files;
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ name: image_picker_macos
|
||||
description: macOS platform implementation of image_picker
|
||||
repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_macos
|
||||
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
|
||||
version: 0.2.0
|
||||
version: 0.2.1
|
||||
|
||||
environment:
|
||||
sdk: ">=2.18.0 <4.0.0"
|
||||
@ -20,7 +20,7 @@ dependencies:
|
||||
file_selector_platform_interface: ^2.3.0
|
||||
flutter:
|
||||
sdk: flutter
|
||||
image_picker_platform_interface: ^2.7.0
|
||||
image_picker_platform_interface: ^2.8.0
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.1.5
|
||||
|
@ -131,6 +131,38 @@ void main() {
|
||||
plugin.getVideo(source: ImageSource.camera), throwsStateError);
|
||||
});
|
||||
});
|
||||
|
||||
group('media', () {
|
||||
test('getMedia passes the accepted type groups correctly', () async {
|
||||
await plugin.getMedia(options: const MediaOptions(allowMultiple: true));
|
||||
|
||||
final VerificationResult result = verify(
|
||||
mockFileSelectorPlatform.openFiles(
|
||||
acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups')));
|
||||
expect(capturedTypeGroups(result)[0].extensions,
|
||||
<String>['public.image', 'public.movie']);
|
||||
});
|
||||
|
||||
test('multiple media handles an empty path response gracefully', () async {
|
||||
expect(
|
||||
await plugin.getMedia(
|
||||
options: const MediaOptions(
|
||||
allowMultiple: true,
|
||||
),
|
||||
),
|
||||
<String>[]);
|
||||
});
|
||||
|
||||
test('single media handles an empty path response gracefully', () async {
|
||||
expect(
|
||||
await plugin.getMedia(
|
||||
options: const MediaOptions(
|
||||
allowMultiple: false,
|
||||
),
|
||||
),
|
||||
<String>[]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class FakeCameraDelegate extends ImagePickerCameraDelegate {
|
||||
|
@ -1,3 +1,7 @@
|
||||
## 0.2.1
|
||||
|
||||
* Adds `getMedia` method.
|
||||
|
||||
## 0.2.0
|
||||
|
||||
* Updates minimum Flutter version to 3.3.
|
||||
|
@ -9,6 +9,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
void main() {
|
||||
@ -37,11 +38,11 @@ class MyHomePage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
List<XFile>? _imageFileList;
|
||||
List<XFile>? _mediaFileList;
|
||||
|
||||
// This must be called from within a setState() callback
|
||||
void _setImageFileListFromFile(XFile? value) {
|
||||
_imageFileList = value == null ? null : <XFile>[value];
|
||||
_mediaFileList = value == null ? null : <XFile>[value];
|
||||
}
|
||||
|
||||
dynamic _pickImageError;
|
||||
@ -70,52 +71,12 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleMultiImagePicked(BuildContext context) async {
|
||||
await _displayPickImageDialog(context,
|
||||
(double? maxWidth, double? maxHeight, int? quality) async {
|
||||
try {
|
||||
final List<XFile>? pickedFileList = await _picker.getMultiImage(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
imageQuality: quality,
|
||||
);
|
||||
setState(() {
|
||||
_imageFileList = pickedFileList;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_pickImageError = e;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _handleSingleImagePicked(
|
||||
BuildContext context, ImageSource source) async {
|
||||
await _displayPickImageDialog(context,
|
||||
(double? maxWidth, double? maxHeight, int? quality) async {
|
||||
try {
|
||||
final XFile? pickedFile = await _picker.getImageFromSource(
|
||||
source: source,
|
||||
options: ImagePickerOptions(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
imageQuality: quality,
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_setImageFileListFromFile(pickedFile);
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_pickImageError = e;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onImageButtonPressed(ImageSource source,
|
||||
{required BuildContext context, bool isMultiImage = false}) async {
|
||||
Future<void> _onImageButtonPressed(
|
||||
ImageSource source, {
|
||||
required BuildContext context,
|
||||
bool isMultiImage = false,
|
||||
bool isMedia = false,
|
||||
}) async {
|
||||
if (_controller != null) {
|
||||
await _controller!.setVolume(0.0);
|
||||
}
|
||||
@ -125,9 +86,83 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
source: source, maxDuration: const Duration(seconds: 10));
|
||||
await _playVideo(file);
|
||||
} else if (isMultiImage) {
|
||||
await _handleMultiImagePicked(context);
|
||||
await _displayPickImageDialog(context,
|
||||
(double? maxWidth, double? maxHeight, int? quality) async {
|
||||
try {
|
||||
final List<XFile> pickedFileList = isMedia
|
||||
? await _picker.getMedia(
|
||||
options: MediaOptions(
|
||||
allowMultiple: isMultiImage,
|
||||
imageOptions: ImageOptions(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
imageQuality: quality,
|
||||
)),
|
||||
)
|
||||
: await _picker.getMultiImageWithOptions(
|
||||
options: MultiImagePickerOptions(
|
||||
imageOptions: ImageOptions(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
imageQuality: quality,
|
||||
),
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_mediaFileList = pickedFileList;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_pickImageError = e;
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (isMedia) {
|
||||
await _displayPickImageDialog(context,
|
||||
(double? maxWidth, double? maxHeight, int? quality) async {
|
||||
try {
|
||||
final List<XFile> pickedFileList = <XFile>[];
|
||||
final XFile? media = _firstOrNull(await _picker.getMedia(
|
||||
options: MediaOptions(
|
||||
allowMultiple: isMultiImage,
|
||||
imageOptions: ImageOptions(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
imageQuality: quality,
|
||||
)),
|
||||
));
|
||||
|
||||
if (media != null) {
|
||||
pickedFileList.add(media);
|
||||
setState(() {
|
||||
_mediaFileList = pickedFileList;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() => _pickImageError = e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await _handleSingleImagePicked(context, source);
|
||||
await _displayPickImageDialog(context,
|
||||
(double? maxWidth, double? maxHeight, int? quality) async {
|
||||
try {
|
||||
final XFile? pickedFile = await _picker.getImageFromSource(
|
||||
source: source,
|
||||
options: ImagePickerOptions(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
imageQuality: quality,
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_setImageFileListFromFile(pickedFile);
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_pickImageError = e;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -180,18 +215,28 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
if (retrieveError != null) {
|
||||
return retrieveError;
|
||||
}
|
||||
if (_imageFileList != null) {
|
||||
if (_mediaFileList != null) {
|
||||
return Semantics(
|
||||
label: 'image_picker_example_picked_images',
|
||||
child: ListView.builder(
|
||||
key: UniqueKey(),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final String? mime = lookupMimeType(_mediaFileList![index].path);
|
||||
return Semantics(
|
||||
label: 'image_picker_example_picked_image',
|
||||
child: Image.file(File(_imageFileList![index].path)),
|
||||
child: mime == null || mime.startsWith('image/')
|
||||
? Image.file(
|
||||
File(_mediaFileList![index].path),
|
||||
errorBuilder: (BuildContext context, Object error,
|
||||
StackTrace? stackTrace) {
|
||||
return const Center(
|
||||
child: Text('This image type is not supported'));
|
||||
},
|
||||
)
|
||||
: _buildInlineVideoPlayer(index),
|
||||
);
|
||||
},
|
||||
itemCount: _imageFileList!.length,
|
||||
itemCount: _mediaFileList!.length,
|
||||
),
|
||||
);
|
||||
} else if (_pickImageError != null) {
|
||||
@ -207,6 +252,17 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildInlineVideoPlayer(int index) {
|
||||
final VideoPlayerController controller =
|
||||
VideoPlayerController.file(File(_mediaFileList![index].path));
|
||||
const double volume = 1.0;
|
||||
controller.setVolume(volume);
|
||||
controller.initialize();
|
||||
controller.setLooping(true);
|
||||
controller.play();
|
||||
return Center(child: AspectRatioVideo(controller));
|
||||
}
|
||||
|
||||
Widget _handlePreview() {
|
||||
if (_isVideo) {
|
||||
return _previewVideo();
|
||||
@ -230,6 +286,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
Semantics(
|
||||
label: 'image_picker_example_from_gallery',
|
||||
child: FloatingActionButton(
|
||||
key: const Key('image_picker_example_from_gallery'),
|
||||
onPressed: () {
|
||||
_isVideo = false;
|
||||
_onImageButtonPressed(ImageSource.gallery, context: context);
|
||||
@ -239,6 +296,39 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
child: const Icon(Icons.photo),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () {
|
||||
_isVideo = false;
|
||||
_onImageButtonPressed(
|
||||
ImageSource.gallery,
|
||||
context: context,
|
||||
isMultiImage: true,
|
||||
isMedia: true,
|
||||
);
|
||||
},
|
||||
heroTag: 'multipleMedia',
|
||||
tooltip: 'Pick Multiple Media from gallery',
|
||||
child: const Icon(Icons.photo_library),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () {
|
||||
_isVideo = false;
|
||||
_onImageButtonPressed(
|
||||
ImageSource.gallery,
|
||||
context: context,
|
||||
isMedia: true,
|
||||
);
|
||||
},
|
||||
heroTag: 'media',
|
||||
tooltip: 'Pick Single Media from gallery',
|
||||
child: const Icon(Icons.photo_library),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: FloatingActionButton(
|
||||
@ -420,3 +510,7 @@ class AspectRatioVideoState extends State<AspectRatioVideo> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
T? _firstOrNull<T>(List<T> list) {
|
||||
return list.isEmpty ? null : list.first;
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
image_picker_platform_interface: ^2.7.0
|
||||
image_picker_platform_interface: ^2.8.0
|
||||
image_picker_windows:
|
||||
# When depending on this package from a real application you should use:
|
||||
# image_picker_windows: ^x.y.z
|
||||
@ -18,6 +18,7 @@ dependencies:
|
||||
# The example app is bundled with the plugin so we use a path dependency on
|
||||
# the parent directory to use the current plugin's version.
|
||||
path: ..
|
||||
mime: ^1.0.4
|
||||
video_player: ^2.1.4
|
||||
|
||||
dev_dependencies:
|
||||
|
@ -183,4 +183,28 @@ class ImagePickerWindows extends CameraDelegatingImagePickerPlatform {
|
||||
.openFiles(acceptedTypeGroups: <XTypeGroup>[typeGroup]);
|
||||
return files;
|
||||
}
|
||||
|
||||
// `maxWidth`, `maxHeight`, and `imageQuality` arguments are not
|
||||
// supported on Windows. If any of these arguments is supplied,
|
||||
// they will be silently ignored by the Windows version of the plugin.
|
||||
@override
|
||||
Future<List<XFile>> getMedia({required MediaOptions options}) async {
|
||||
const XTypeGroup typeGroup = XTypeGroup(
|
||||
label: 'images and videos',
|
||||
extensions: <String>[...imageFormats, ...videoFormats]);
|
||||
|
||||
List<XFile> files;
|
||||
|
||||
if (options.allowMultiple) {
|
||||
files = await fileSelector
|
||||
.openFiles(acceptedTypeGroups: <XTypeGroup>[typeGroup]);
|
||||
} else {
|
||||
final XFile? file = await fileSelector
|
||||
.openFile(acceptedTypeGroups: <XTypeGroup>[typeGroup]);
|
||||
files = <XFile>[
|
||||
if (file != null) file,
|
||||
];
|
||||
}
|
||||
return files;
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ name: image_picker_windows
|
||||
description: Windows platform implementation of image_picker
|
||||
repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_windows
|
||||
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
|
||||
version: 0.2.0
|
||||
version: 0.2.1
|
||||
|
||||
environment:
|
||||
sdk: ">=2.18.0 <4.0.0"
|
||||
@ -20,7 +20,7 @@ dependencies:
|
||||
file_selector_windows: ^0.9.0
|
||||
flutter:
|
||||
sdk: flutter
|
||||
image_picker_platform_interface: ^2.7.0
|
||||
image_picker_platform_interface: ^2.8.0
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.1.5
|
||||
|
@ -128,6 +128,41 @@ void main() {
|
||||
plugin.getVideo(source: ImageSource.camera), throwsStateError);
|
||||
});
|
||||
});
|
||||
|
||||
group('media', () {
|
||||
test('getMedia passes the accepted type groups correctly', () async {
|
||||
await plugin.getMedia(options: const MediaOptions(allowMultiple: true));
|
||||
|
||||
final VerificationResult result = verify(
|
||||
mockFileSelectorPlatform.openFiles(
|
||||
acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups')));
|
||||
expect(capturedTypeGroups(result)[0].extensions, <String>[
|
||||
...ImagePickerWindows.imageFormats,
|
||||
...ImagePickerWindows.videoFormats
|
||||
]);
|
||||
});
|
||||
|
||||
test('multiple media handles an empty path response gracefully',
|
||||
() async {
|
||||
expect(
|
||||
await plugin.getMedia(
|
||||
options: const MediaOptions(
|
||||
allowMultiple: true,
|
||||
),
|
||||
),
|
||||
<String>[]);
|
||||
});
|
||||
|
||||
test('single media handles an empty path response gracefully', () async {
|
||||
expect(
|
||||
await plugin.getMedia(
|
||||
options: const MediaOptions(
|
||||
allowMultiple: false,
|
||||
),
|
||||
),
|
||||
<String>[]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -48,6 +48,7 @@
|
||||
- logging
|
||||
- markdown
|
||||
- meta
|
||||
- mime
|
||||
- path
|
||||
- shelf
|
||||
- shelf_static
|
||||
|
Reference in New Issue
Block a user