From fcb1fe48e19ba40e0ddeb23cb7708a30cbf4209b Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Thu, 14 May 2020 17:16:54 -0700 Subject: [PATCH 01/15] Introduce the PickedFile class to have a more platform agnostic return. Borrowed from Rody Davis' file_access package. Co-authored-by: Rody Davis --- .../lib/src/types/picked_file/base.dart | 56 +++++++++++++++++++ .../lib/src/types/picked_file/html.dart | 33 +++++++++++ .../lib/src/types/picked_file/io.dart | 35 ++++++++++++ .../lib/src/types/picked_file/lost_data.dart | 49 ++++++++++++++++ .../src/types/picked_file/picked_file.dart | 4 ++ .../src/types/picked_file/unsupported.dart | 14 +++++ 6 files changed, 191 insertions(+) create mode 100644 packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/base.dart create mode 100644 packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/html.dart create mode 100644 packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart create mode 100644 packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/lost_data.dart create mode 100644 packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/picked_file.dart create mode 100644 packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/unsupported.dart diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/base.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/base.dart new file mode 100644 index 000000000000..01900fb71da6 --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/base.dart @@ -0,0 +1,56 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:meta/meta.dart'; + +/// The interface for a PickedFile. +/// +/// A PickedFile is a container that wraps the path of a selected +/// file by the user and (in some platforms, like web) the bytes +/// with the contents of the file. +/// +/// This class is a very limited subset of dart:io [File], so all +/// the methods should seem familiar. +@immutable +abstract class PickedFileBase { + /// Construct a PickedFile + PickedFileBase(String path); + + /// Get the path of the picked file. + /// + /// This should only be used as a backwards-compatibility clutch + /// for mobile apps, or cosmetic reasons only (to show the user + /// the path they've picked). + /// + /// Accessing the data contained in the picked file by its path + /// is platform-dependant (and won't work on web), so use the + /// byte getters in the PickedFile instance instead. + String get path { + throw UnimplementedError('.path has not been implemented.'); + } + + /// Synchronously read the entire file contents as a string using the given [Encoding]. + /// + /// Throws Exception if the operation fails. + String readAsStringSync({Encoding encoding = utf8}) { + throw UnimplementedError('readAsStringSync() has not been implemented.'); + } + + /// Synchronously read the entire file contents as a list of bytes. + /// + /// Throws Exception if the operation fails. + Uint8List readAsBytesSync() { + throw UnimplementedError('readAsBytesSync() has not been implemented.'); + } + + /// Create a new independent [Stream] for the contents of this file. + /// + /// If `start` is present, the file will be read from byte-offset `start`. Otherwise from the beginning (index 0). + /// + /// If `end` is present, only up to byte-index `end` will be read. Otherwise, until end of file. + /// + /// In order to make sure that system resources are freed, the stream must be read to completion or the subscription on the stream must be cancelled. + Stream openRead([int start, int end]) { + throw UnimplementedError('openRead() has not been implemented.'); + } +} diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/html.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/html.dart new file mode 100644 index 000000000000..7b211c6bdff8 --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/html.dart @@ -0,0 +1,33 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import './base.dart'; + +/// A PickedFile that works on web. +/// +/// It wraps the bytes of a selected file. +class PickedFile extends PickedFileBase { + final String path; + final Uint8List _bytes; + + /// Construct a PickedFile object, from its `bytes`. + PickedFile(this.path, {Uint8List bytes}) + : _bytes = bytes, + super(path); + + @override + String readAsStringSync({Encoding encoding = utf8}) { + return encoding.decode(_bytes); + } + + @override + Uint8List readAsBytesSync() { + return UnmodifiableUint8ListView(_bytes); + } + + @override + Stream openRead([int start, int end]) { + return Stream.fromIterable( + [_bytes.sublist(start ?? 0, end ?? _bytes.length)]); + } +} diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart new file mode 100644 index 000000000000..7c229913c3e5 --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart @@ -0,0 +1,35 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import './base.dart'; + +/// A PickedFile backed by a dart:io File. +class PickedFile extends PickedFileBase { + final File _file; + + /// Construct a PickedFile object backed by a dart:io File. + PickedFile(String path) + : _file = File(path), + super(path); + + @override + String get path { + return _file.path; + } + + @override + String readAsStringSync({Encoding encoding = utf8}) { + return _file.readAsStringSync(encoding: encoding); + } + + @override + Uint8List readAsBytesSync() { + return _file.readAsBytesSync(); + } + + @override + Stream openRead([int start, int end]) { + return _file.openRead(start ?? 0, end); + } +} diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/lost_data.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/lost_data.dart new file mode 100644 index 000000000000..b94e69de219e --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/lost_data.dart @@ -0,0 +1,49 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:image_picker_platform_interface/src/types/types.dart'; + +/// The response object of [ImagePicker.retrieveLostData]. +/// +/// Only applies to Android. +/// See also: +/// * [ImagePicker.retrieveLostData] for more details on retrieving lost data. +class LostData { + /// Creates an instance with the given [file], [exception], and [type]. Any of + /// the params may be null, but this is never considered to be empty. + LostData({this.file, this.exception, this.type}); + + /// Initializes an instance with all member params set to null and considered + /// to be empty. + LostData.empty() + : file = null, + exception = null, + type = null, + _empty = true; + + /// Whether it is an empty response. + /// + /// An empty response should have [file], [exception] and [type] to be null. + bool get isEmpty => _empty; + + /// The file that was lost in a previous [pickImage] or [pickVideo] call due to MainActivity being destroyed. + /// + /// Can be null if [exception] exists. + final PickedFile file; + + /// The exception of the last [pickImage] or [pickVideo]. + /// + /// If the last [pickImage] or [pickVideo] threw some exception before the MainActivity destruction, this variable keeps that + /// exception. + /// You should handle this exception as if the [pickImage] or [pickVideo] got an exception when the MainActivity was not destroyed. + /// + /// Note that it is not the exception that caused the destruction of the MainActivity. + final PlatformException exception; + + /// Can either be [RetrieveType.image] or [RetrieveType.video]; + final RetrieveType type; + + bool _empty = false; +} diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/picked_file.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/picked_file.dart new file mode 100644 index 000000000000..b2a614ccb304 --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/picked_file.dart @@ -0,0 +1,4 @@ +export 'lost_data.dart'; +export 'unsupported.dart' + if (dart.library.html) 'html.dart' + if (dart.library.io) 'io.dart'; diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/unsupported.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/unsupported.dart new file mode 100644 index 000000000000..bc10a4890c8d --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/unsupported.dart @@ -0,0 +1,14 @@ +import './base.dart'; + +/// A PickedFile is a cross-platform, simplified File abstraction. +/// +/// It wraps the bytes of a selected file, and its (platform-dependant) path. +class PickedFile extends PickedFileBase { + /// Construct a PickedFile object, from its `bytes`. + /// + /// Optionally, you may pass a `path`. See caveats in [PickedFileBase.path]. + PickedFile(String path) : super(path) { + throw UnimplementedError( + 'PickedFile is not available in your current platform.'); + } +} From f25265f1b0a29b276a4f3c21df47e29a7e8b7639 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Thu, 14 May 2020 17:21:04 -0700 Subject: [PATCH 02/15] Modify the platform interface to return PickedFiles. --- .../CHANGELOG.md | 4 + .../method_channel_image_picker.dart | 68 ++++++++++++++++ .../image_picker_platform.dart | 77 ++++++++++++++++++- .../lib/src/types/lost_data_response.dart | 1 + .../lib/src/types/types.dart | 1 + .../pubspec.yaml | 4 +- 6 files changed, 152 insertions(+), 3 deletions(-) diff --git a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md index 7708c34ffe8c..0a238bcd51bf 100644 --- a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md +++ b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.1.0 + +* Introduce PickedFile type for the new API. + ## 1.0.1 * Update lower bound of dart dependency to 2.1.0. diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart index 4d960517b73b..a9dadbf68df4 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart @@ -19,6 +19,24 @@ class MethodChannelImagePicker extends ImagePickerPlatform { @visibleForTesting MethodChannel get channel => _channel; + @override + Future pickImage({ + @required ImageSource source, + double maxWidth, + double maxHeight, + int imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) async { + String path = await pickImagePath( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice, + ); + return path != null ? PickedFile(path) : null; + } + @override Future pickImagePath({ @required ImageSource source, @@ -53,6 +71,20 @@ class MethodChannelImagePicker extends ImagePickerPlatform { ); } + @override + Future pickVideo({ + @required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration maxDuration, + }) async { + String path = await pickVideoPath( + source: source, + maxDuration: maxDuration, + preferredCameraDevice: preferredCameraDevice, + ); + return path != null ? PickedFile(path) : null; + } + @override Future pickVideoPath({ @required ImageSource source, @@ -70,6 +102,42 @@ class MethodChannelImagePicker extends ImagePickerPlatform { ); } + @override + Future retrieveLostData() async { + final Map result = + await _channel.invokeMapMethod('retrieve'); + + if (result == null) { + return LostData.empty(); + } + + assert(result.containsKey('path') ^ result.containsKey('errorCode')); + + final String type = result['type']; + assert(type == kTypeImage || type == kTypeVideo); + + RetrieveType retrieveType; + if (type == kTypeImage) { + retrieveType = RetrieveType.image; + } else if (type == kTypeVideo) { + retrieveType = RetrieveType.video; + } + + PlatformException exception; + if (result.containsKey('errorCode')) { + exception = PlatformException( + code: result['errorCode'], message: result['errorMessage']); + } + + final String path = result['path']; + + return LostData( + file: path != null ? PickedFile(path) : null, + exception: exception, + type: retrieveType, + ); + } + @override Future retrieveLostDataAsDartIoFile() async { final Map result = diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart b/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart index 66e74dd95636..94be4c2f2ab1 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart @@ -60,6 +60,7 @@ abstract class ImagePickerPlatform extends PlatformInterface { /// /// In Android, the MainActivity can be destroyed for various reasons. If that happens, the result will be lost /// in this call. You can then call [retrieveLostDataAsDartIoFile] when your app relaunches to retrieve the lost data. + @Deprecated('Use pickImage instead.') Future pickImagePath({ @required ImageSource source, double maxWidth, @@ -84,6 +85,7 @@ abstract class ImagePickerPlatform extends PlatformInterface { /// /// In Android, the MainActivity can be destroyed for various fo reasons. If that happens, the result will be lost /// in this call. You can then call [retrieveLostDataAsDartIoFile] when your app relaunches to retrieve the lost data. + @Deprecated('Use pickVideo instead.') Future pickVideoPath({ @required ImageSource source, CameraDevice preferredCameraDevice = CameraDevice.rear, @@ -92,7 +94,7 @@ abstract class ImagePickerPlatform extends PlatformInterface { throw UnimplementedError('pickVideoPath() has not been implemented.'); } - /// Retrieve the lost image file when [pickImage] or [pickVideo] failed because the MainActivity is destroyed. (Android only) + /// Retrieve the lost image file when [pickImagePath] or [pickVideoPath] failed because the MainActivity is destroyed. (Android only) /// /// Image or video can be lost if the MainActivity is destroyed. And there is no guarantee that the MainActivity is always alive. /// Call this method to retrieve the lost data and process the data according to your APP's business logic. @@ -105,8 +107,81 @@ abstract class ImagePickerPlatform extends PlatformInterface { /// See also: /// * [LostDataResponse], for what's included in the response. /// * [Android Activity Lifecycle](https://developer.android.com/reference/android/app/Activity.html), for more information on MainActivity destruction. + @Deprecated('Use retrieveLostData instead.') Future retrieveLostDataAsDartIoFile() { throw UnimplementedError( 'retrieveLostDataAsDartIoFile() has not been implemented.'); } + + // Next version of the API. + + /// Returns a [PickedFile] with the image that was picked. + /// + /// The `source` argument controls where the image comes from. This can + /// be either [ImageSource.camera] or [ImageSource.gallery]. + /// + /// If specified, the image will be at most `maxWidth` wide and + /// `maxHeight` tall. Otherwise the image will be returned at it's + /// original width and height. + /// + /// The `imageQuality` argument modifies the quality of the image, ranging from 0-100 + /// where 100 is the original/max quality. If `imageQuality` is null, the image with + /// the original quality will be returned. Compression is only supportted for certain + /// image types such as JPEG. If compression is not supported for the image that is picked, + /// an warning message will be logged. + /// + /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. + /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. + /// Defaults to [CameraDevice.rear]. + /// + /// In Android, the MainActivity can be destroyed for various reasons. If that happens, the result will be lost + /// in this call. You can then call [retrieveLostData] when your app relaunches to retrieve the lost data. + Future pickImage({ + @required ImageSource source, + double maxWidth, + double maxHeight, + int imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) { + throw UnimplementedError('pickImage() has not been implemented.'); + } + + /// Returns a [PickedFile] containing the video that was picked. + /// + /// The [source] argument controls where the video comes from. This can + /// be either [ImageSource.camera] or [ImageSource.gallery]. + /// + /// The [maxDuration] argument specifies the maximum duration of the captured video. If no [maxDuration] is specified, + /// the maximum duration will be infinite. + /// + /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. + /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. + /// Defaults to [CameraDevice.rear]. + /// + /// In Android, the MainActivity can be destroyed for various fo reasons. If that happens, the result will be lost + /// in this call. You can then call [retrieveLostData] when your app relaunches to retrieve the lost data. + Future pickVideo({ + @required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration maxDuration, + }) { + throw UnimplementedError('pickVideo() has not been implemented.'); + } + + /// Retrieve the lost [PickedFile] file when [pickImage] or [pickVideo] failed because the MainActivity is destroyed. (Android only) + /// + /// Image or video can be lost if the MainActivity is destroyed. And there is no guarantee that the MainActivity is always alive. + /// Call this method to retrieve the lost data and process the data according to your APP's business logic. + /// + /// Returns a [LostData] object if successfully retrieved the lost data. The [LostData] object can represent either a + /// successful image/video selection, or a failure. + /// + /// Calling this on a non-Android platform will throw [UnimplementedError] exception. + /// + /// See also: + /// * [LostData], for what's included in the response. + /// * [Android Activity Lifecycle](https://developer.android.com/reference/android/app/Activity.html), for more information on MainActivity destruction. + Future retrieveLostData() { + throw UnimplementedError('retrieveLostData() has not been implemented.'); + } } diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart index 53e2decd123f..d82618b23cd1 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart @@ -12,6 +12,7 @@ import 'package:image_picker_platform_interface/src/types/types.dart'; /// Only applies to Android. /// See also: /// * [ImagePicker.retrieveLostData] for more details on retrieving lost data. +@Deprecated('Use methods that return a LostData object instead.') class LostDataResponse { /// Creates an instance with the given [file], [exception], and [type]. Any of /// the params may be null, but this is never considered to be empty. diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart index 98418109dc09..9c44fae1aa9d 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart @@ -2,6 +2,7 @@ export 'camera_device.dart'; export 'image_source.dart'; export 'lost_data_response.dart'; export 'retrieve_type.dart'; +export 'picked_file/picked_file.dart'; /// Denotes that an image is being picked. const String kTypeImage = 'image'; diff --git a/packages/image_picker/image_picker_platform_interface/pubspec.yaml b/packages/image_picker/image_picker_platform_interface/pubspec.yaml index a4ea5d1f959a..e4e4a7ad1c8e 100644 --- a/packages/image_picker/image_picker_platform_interface/pubspec.yaml +++ b/packages/image_picker/image_picker_platform_interface/pubspec.yaml @@ -3,7 +3,7 @@ description: A common platform interface for the image_picker plugin. homepage: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker_platform_interface # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 1.0.1 +version: 1.1.0 dependencies: flutter: @@ -18,5 +18,5 @@ dev_dependencies: pedantic: ^1.8.0+1 environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=2.5.0 <3.0.0" flutter: ">=1.10.0 <2.0.0" From 239bf7436bfe3130d9aa54d3b4306aaff10ac553 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Thu, 14 May 2020 18:03:09 -0700 Subject: [PATCH 03/15] Add new API to the image_picker, and mark old methods as deprecated. Update example. --- .../image_picker/example/lib/main.dart | 27 ++++-- .../image_picker/lib/image_picker.dart | 87 +++++++++++++++++++ .../image_picker/image_picker/pubspec.yaml | 3 +- 3 files changed, 109 insertions(+), 8 deletions(-) diff --git a/packages/image_picker/image_picker/example/lib/main.dart b/packages/image_picker/image_picker/example/lib/main.dart index 29781f7df449..ca2ed3126a37 100755 --- a/packages/image_picker/image_picker/example/lib/main.dart +++ b/packages/image_picker/image_picker/example/lib/main.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/src/widgets/basic.dart'; import 'package:flutter/src/widgets/container.dart'; @@ -37,20 +38,27 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - File _imageFile; + PickedFile _imageFile; dynamic _pickImageError; bool isVideo = false; VideoPlayerController _controller; String _retrieveDataError; + final ImagePicker _picker = ImagePicker(); final TextEditingController maxWidthController = TextEditingController(); final TextEditingController maxHeightController = TextEditingController(); final TextEditingController qualityController = TextEditingController(); - Future _playVideo(File file) async { + Future _playVideo(PickedFile file) async { if (file != null && mounted) { await _disposeVideoController(); - _controller = VideoPlayerController.file(file); + if (kIsWeb) { + // Need VideoPlayerController.memory! + // _controller = VideoPlayerController.memory(file.readAsBytesSync()); + return; // noop + } else { + _controller = VideoPlayerController.file(File(file.path)); + } await _controller.setVolume(1.0); await _controller.initialize(); await _controller.setLooping(true); @@ -64,14 +72,14 @@ class _MyHomePageState extends State { await _controller.setVolume(0.0); } if (isVideo) { - final File file = await ImagePicker.pickVideo( + final PickedFile file = await _picker.getVideo( source: source, maxDuration: const Duration(seconds: 10)); await _playVideo(file); } else { await _displayPickImageDialog(context, (double maxWidth, double maxHeight, int quality) async { try { - _imageFile = await ImagePicker.pickImage( + _imageFile = await _picker.getImage( source: source, maxWidth: maxWidth, maxHeight: maxHeight, @@ -132,7 +140,12 @@ class _MyHomePageState extends State { return retrieveError; } if (_imageFile != null) { - return Image.file(_imageFile); + if (kIsWeb) { + return Image.memory(_imageFile.readAsBytesSync()); + } else { + // This would also work from memory as well... + return Image.file(File(_imageFile.path)); + } } else if (_pickImageError != null) { return Text( 'Pick image error: $_pickImageError', @@ -147,7 +160,7 @@ class _MyHomePageState extends State { } Future retrieveLostData() async { - final LostDataResponse response = await ImagePicker.retrieveLostData(); + final LostData response = await _picker.getLostData(); if (response.isEmpty) { return; } diff --git a/packages/image_picker/image_picker/lib/image_picker.dart b/packages/image_picker/image_picker/lib/image_picker.dart index 0dd9cac8d346..110365751ea2 100755 --- a/packages/image_picker/image_picker/lib/image_picker.dart +++ b/packages/image_picker/image_picker/lib/image_picker.dart @@ -15,7 +15,9 @@ export 'package:image_picker_platform_interface/image_picker_platform_interface. kTypeVideo, ImageSource, CameraDevice, + LostData, LostDataResponse, + PickedFile, RetrieveType; /// Provides an easy way to pick an image/video from the image library, @@ -47,6 +49,7 @@ class ImagePicker { /// /// In Android, the MainActivity can be destroyed for various reasons. If that happens, the result will be lost /// in this call. You can then call [retrieveLostData] when your app relaunches to retrieve the lost data. + @Deprecated('Use imagePicker.getImage() method instead.') static Future pickImage( {@required ImageSource source, double maxWidth, @@ -64,6 +67,44 @@ class ImagePicker { return path == null ? null : File(path); } + /// Returns a [PickedFile] object wrapping the image that was picked. + /// + /// The returned [PickedFile] is intended to be used within a single APP session. Do not save the file path and use it across sessions. + /// + /// The `source` argument controls where the image comes from. This can + /// be either [ImageSource.camera] or [ImageSource.gallery]. + /// + /// If specified, the image will be at most `maxWidth` wide and + /// `maxHeight` tall. Otherwise the image will be returned at it's + /// original width and height. + /// The `imageQuality` argument modifies the quality of the image, ranging from 0-100 + /// where 100 is the original/max quality. If `imageQuality` is null, the image with + /// the original quality will be returned. Compression is only supportted for certain + /// image types such as JPEG. If compression is not supported for the image that is picked, + /// an warning message will be logged. + /// + /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. + /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. + /// Defaults to [CameraDevice.rear]. + /// + /// In Android, the MainActivity can be destroyed for various reasons. If that happens, the result will be lost + /// in this call. You can then call [getLostData] when your app relaunches to retrieve the lost data. + Future getImage({ + @required ImageSource source, + double maxWidth, + double maxHeight, + int imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) { + return platform.pickImage( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice, + ); + } + /// Returns a [File] object pointing to the video that was picked. /// /// The returned [File] is intended to be used within a single APP session. Do not save the file path and use it across sessions. @@ -80,6 +121,7 @@ class ImagePicker { /// /// In Android, the MainActivity can be destroyed for various fo reasons. If that happens, the result will be lost /// in this call. You can then call [retrieveLostData] when your app relaunches to retrieve the lost data. + @Deprecated('Use imagePicker.getVideo() method instead.') static Future pickVideo( {@required ImageSource source, CameraDevice preferredCameraDevice = CameraDevice.rear, @@ -93,6 +135,34 @@ class ImagePicker { return path == null ? null : File(path); } + /// Returns a [PickedFile] object wrapping the video that was picked. + /// + /// The returned [PickedFile] is intended to be used within a single APP session. Do not save the file path and use it across sessions. + /// + /// The [source] argument controls where the video comes from. This can + /// be either [ImageSource.camera] or [ImageSource.gallery]. + /// + /// The [maxDuration] argument specifies the maximum duration of the captured video. If no [maxDuration] is specified, + /// the maximum duration will be infinite. + /// + /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. + /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. + /// Defaults to [CameraDevice.rear]. + /// + /// In Android, the MainActivity can be destroyed for various fo reasons. If that happens, the result will be lost + /// in this call. You can then call [getLostData] when your app relaunches to retrieve the lost data. + Future getVideo({ + @required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration maxDuration, + }) { + return platform.pickVideo( + source: source, + preferredCameraDevice: preferredCameraDevice, + maxDuration: maxDuration, + ); + } + /// Retrieve the lost image file when [pickImage] or [pickVideo] failed because the MainActivity is destroyed. (Android only) /// /// Image or video can be lost if the MainActivity is destroyed. And there is no guarantee that the MainActivity is always alive. @@ -109,4 +179,21 @@ class ImagePicker { static Future retrieveLostData() { return platform.retrieveLostDataAsDartIoFile(); } + + /// Retrieve the lost [PickedFile] when [selectImage] or [selectVideo] failed because the MainActivity is destroyed. (Android only) + /// + /// Image or video can be lost if the MainActivity is destroyed. And there is no guarantee that the MainActivity is always alive. + /// Call this method to retrieve the lost data and process the data according to your APP's business logic. + /// + /// Returns a [LostData] object if successfully retrieved the lost data. The [LostData] object can represent either a + /// successful image/video selection, or a failure. + /// + /// Calling this on a non-Android platform will throw [UnimplementedError] exception. + /// + /// See also: + /// * [LostData], for what's included in the response. + /// * [Android Activity Lifecycle](https://developer.android.com/reference/android/app/Activity.html), for more information on MainActivity destruction. + Future getLostData() { + return platform.retrieveLostData(); + } } diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index e9161748db56..e98e7014add6 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -17,7 +17,8 @@ dependencies: flutter: sdk: flutter flutter_plugin_android_lifecycle: ^1.0.2 - image_picker_platform_interface: ^1.0.0 + image_picker_platform_interface: + path: ../image_picker_platform_interface dev_dependencies: video_player: ^0.10.3 From 7fdc348425da49599ece576eca9b44025bd16ec2 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Fri, 15 May 2020 15:26:29 -0700 Subject: [PATCH 04/15] Add image_picker_for_web plugin. --- .../image_picker_for_web/CHANGELOG.md | 3 + .../image_picker/image_picker_for_web/LICENSE | 27 ++++ .../image_picker_for_web/README.md | 25 +++ .../image_picker_for_web/android/.gitignore | 8 + .../image_picker_for_web/android/build.gradle | 33 ++++ .../android/gradle.properties | 2 + .../gradle/wrapper/gradle-wrapper.properties | 5 + .../android/settings.gradle | 1 + .../android/src/main/AndroidManifest.xml | 3 + .../ImagePickerWebPlugin.java | 28 ++++ .../ios/image_picker_for_web.podspec | 20 +++ .../lib/image_picker_for_web.dart | 144 ++++++++++++++++++ .../image_picker_for_web/pubspec.yaml | 34 +++++ .../test/url_launcher_web_test.dart | 103 +++++++++++++ 14 files changed, 436 insertions(+) create mode 100644 packages/image_picker/image_picker_for_web/CHANGELOG.md create mode 100644 packages/image_picker/image_picker_for_web/LICENSE create mode 100644 packages/image_picker/image_picker_for_web/README.md create mode 100644 packages/image_picker/image_picker_for_web/android/.gitignore create mode 100644 packages/image_picker/image_picker_for_web/android/build.gradle create mode 100644 packages/image_picker/image_picker_for_web/android/gradle.properties create mode 100644 packages/image_picker/image_picker_for_web/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 packages/image_picker/image_picker_for_web/android/settings.gradle create mode 100644 packages/image_picker/image_picker_for_web/android/src/main/AndroidManifest.xml create mode 100644 packages/image_picker/image_picker_for_web/android/src/main/java/io/flutter/image_picker_for_web/ImagePickerWebPlugin.java create mode 100644 packages/image_picker/image_picker_for_web/ios/image_picker_for_web.podspec create mode 100644 packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart create mode 100644 packages/image_picker/image_picker_for_web/pubspec.yaml create mode 100644 packages/image_picker/image_picker_for_web/test/url_launcher_web_test.dart diff --git a/packages/image_picker/image_picker_for_web/CHANGELOG.md b/packages/image_picker/image_picker_for_web/CHANGELOG.md new file mode 100644 index 000000000000..18ff7e526b11 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/CHANGELOG.md @@ -0,0 +1,3 @@ +# 0.1.0 + +* Initial open-source release. diff --git a/packages/image_picker/image_picker_for_web/LICENSE b/packages/image_picker/image_picker_for_web/LICENSE new file mode 100644 index 000000000000..0c382ce171cc --- /dev/null +++ b/packages/image_picker/image_picker_for_web/LICENSE @@ -0,0 +1,27 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/image_picker/image_picker_for_web/README.md b/packages/image_picker/image_picker_for_web/README.md new file mode 100644 index 000000000000..3582b604f637 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/README.md @@ -0,0 +1,25 @@ +# image_picker_for_web + +The web implementation of [`image_picker`][1]. + +## Usage + +### Import the package + +This package is the endorsed implementation of `image_picker` for the web platform since version `0.6.7`, so it gets automatically added to your dependencies by depending on `image_picker: ^0.6.7`. + +No modifications to your pubspec.yaml should be required in a recent enough version of Flutter (`>=1.12.13+hotfix.4`): + +```yaml +... +dependencies: + ... + image_picker: ^0.6.7 + ... +... +``` + +### Use the plugin +You should be able to use `package:image_picker` as normal. + +[1]: ../image_picker/image_picker diff --git a/packages/image_picker/image_picker_for_web/android/.gitignore b/packages/image_picker/image_picker_for_web/android/.gitignore new file mode 100644 index 000000000000..c6cbe562a427 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/android/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures diff --git a/packages/image_picker/image_picker_for_web/android/build.gradle b/packages/image_picker/image_picker_for_web/android/build.gradle new file mode 100644 index 000000000000..6d8d50eb7b6d --- /dev/null +++ b/packages/image_picker/image_picker_for_web/android/build.gradle @@ -0,0 +1,33 @@ +group 'io.flutter.image_picker_for_web' +version '1.0' + +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.5.0' + } +} + +rootProject.allprojects { + repositories { + google() + jcenter() + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 28 + + defaultConfig { + minSdkVersion 16 + } + lintOptions { + disable 'InvalidPackage' + } +} diff --git a/packages/image_picker/image_picker_for_web/android/gradle.properties b/packages/image_picker/image_picker_for_web/android/gradle.properties new file mode 100644 index 000000000000..7be3d8b46841 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx1536M +android.enableR8=true diff --git a/packages/image_picker/image_picker_for_web/android/gradle/wrapper/gradle-wrapper.properties b/packages/image_picker/image_picker_for_web/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..019065d1d650 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip diff --git a/packages/image_picker/image_picker_for_web/android/settings.gradle b/packages/image_picker/image_picker_for_web/android/settings.gradle new file mode 100644 index 000000000000..07e3728d1fe7 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'image_picker_for_web' diff --git a/packages/image_picker/image_picker_for_web/android/src/main/AndroidManifest.xml b/packages/image_picker/image_picker_for_web/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..b6f6992b3fb9 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/packages/image_picker/image_picker_for_web/android/src/main/java/io/flutter/image_picker_for_web/ImagePickerWebPlugin.java b/packages/image_picker/image_picker_for_web/android/src/main/java/io/flutter/image_picker_for_web/ImagePickerWebPlugin.java new file mode 100644 index 000000000000..18b5bf21144b --- /dev/null +++ b/packages/image_picker/image_picker_for_web/android/src/main/java/io/flutter/image_picker_for_web/ImagePickerWebPlugin.java @@ -0,0 +1,28 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.image_picker_for_web; + +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.plugin.common.PluginRegistry.Registrar; + +/** ImagePickerWebPlugin */ +public class ImagePickerWebPlugin implements FlutterPlugin { + @Override + public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) {} + + // This static function is optional and equivalent to onAttachedToEngine. It supports the old + // pre-Flutter-1.12 Android projects. You are encouraged to continue supporting + // plugin registration via this function while apps migrate to use the new Android APIs + // post-flutter-1.12 via https://flutter.dev/go/android-project-migration. + // + // It is encouraged to share logic between onAttachedToEngine and registerWith to keep + // them functionally equivalent. Only one of onAttachedToEngine or registerWith will be called + // depending on the user's project. onAttachedToEngine or registerWith must both be defined + // in the same class. + public static void registerWith(Registrar registrar) {} + + @Override + public void onDetachedFromEngine(FlutterPluginBinding binding) {} +} diff --git a/packages/image_picker/image_picker_for_web/ios/image_picker_for_web.podspec b/packages/image_picker/image_picker_for_web/ios/image_picker_for_web.podspec new file mode 100644 index 000000000000..23fb795d1cc2 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/ios/image_picker_for_web.podspec @@ -0,0 +1,20 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'image_picker_for_web' + s.version = '0.0.1' + s.summary = 'No-op implementation of image_picker_for_web plugin to avoid build issues on iOS' + s.description = <<-DESC +temp fake image_picker_for_web plugin + DESC + s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker_for_web' + s.license = { :file => '../LICENSE' } + s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + + s.ios.deployment_target = '8.0' +end diff --git a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart new file mode 100644 index 000000000000..cfaf814c65c1 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart @@ -0,0 +1,144 @@ +import 'dart:async'; +import 'dart:html' as html; + +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:meta/meta.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; + +final String _kImagePickerInputsDomId = '__image_picker_web-file-input'; +final String _kAcceptImageMimeType = 'image/*'; +final String _kAcceptVideoMimeType = 'video/*'; + +/// The web implementation of [ImagePickerPlatform]. +/// +/// This class implements the `package:image_picker` functionality for the web. +class ImagePickerPlugin extends ImagePickerPlatform { + html.Element _target; + + /// A constructor that allows tests to override the window object used by the plugin. + ImagePickerPlugin({@visibleForTesting html.Element target}) + : _target = target { + if (_target == null) { + _target = _initTarget(_kImagePickerInputsDomId); + } + } + + /// Registers this class as the default instance of [ImagePickerPlatform]. + static void registerWith(Registrar registrar) { + ImagePickerPlatform.instance = ImagePickerPlugin(); + } + + @override + Future pickImage({ + @required ImageSource source, + double maxWidth, + double maxHeight, + int imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) { + String capture = _computeCaptureAttribute(source, preferredCameraDevice); + return _pickFile(accept: _kAcceptImageMimeType, capture: capture); + } + + @override + Future pickVideo({ + @required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration maxDuration, + }) { + String capture = _computeCaptureAttribute(source, preferredCameraDevice); + return _pickFile(accept: _kAcceptVideoMimeType, capture: capture); + } + + /// Injects a file input with the specified accept+capture attributes, and + /// returns the PickedFile that the user selected locally. + /// + /// `capture` is only supported in mobile browsers. + /// See https://caniuse.com/#feat=html-media-capture + Future _pickFile({ + String accept, + String capture, + }) { + html.FileUploadInputElement input = _createInputElement(accept, capture); + _injectAndActivate(input); + return _getSelectedFile(input); + } + + // DOM methods + + /// Converts plugin configuration into a proper value for the `capture` attribute. + String _computeCaptureAttribute(ImageSource source, CameraDevice device) { + String capture; + if (source == ImageSource.camera) { + capture = device == CameraDevice.front ? 'user' : 'environment'; + } + return capture; + } + + /// Handles the OnChange event from a FileUploadInputElement object + /// Returns the objectURL of the selected file. + String _handleOnChangeEvent(html.Event event) { + // load the file... + final html.FileUploadInputElement input = event.target; + final html.File file = input.files[0]; + + if (file != null) { + return html.Url.createObjectUrl(file); + } + return null; + } + + /// Monitors an and returns the selected file. + Future _getSelectedFile(html.FileUploadInputElement input) async { + // Observe the input until we can return something + final Completer _completer = Completer(); + input.onChange.listen((html.Event event) async { + final objectUrl = _handleOnChangeEvent(event); + _completer.complete(PickedFile(objectUrl)); + }); + input.onError // What other events signal failure? + .listen((html.Event event) { + _completer.completeError(event); + }); + + return _completer.future; + } + + /// Initializes a DOM container where we can host input elements. + html.Element _initTarget(String id) { + var target = html.querySelector('#${id}'); + if (target == null) { + final html.Element targetElement = + html.Element.tag('flt-image-picker-inputs')..id = id; + + html.querySelector('body').children.add(targetElement); + target = targetElement; + } + return target; + } + + /// Creates an input element that accepts certain file types, and + /// allows to `capture` from the device's cameras (where supported) + html.Element _createInputElement(String accept, String capture) { + html.Element element; + + if (capture != null) { + // Capture is not supported by dart:html :/ + element = html.Element.html( + '', + validator: html.NodeValidatorBuilder() + ..allowElement('input', attributes: ['type', 'accept', 'capture'])); + } else { + element = html.FileUploadInputElement()..accept = accept; + } + + return element; + } + + /// Injects the file input element, and clicks on it + void _injectAndActivate(html.Element element) { + _target.children.clear(); + _target.children.add(element); + element.click(); + } +} diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml new file mode 100644 index 000000000000..1ec71443bc21 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/pubspec.yaml @@ -0,0 +1,34 @@ +name: image_picker_for_web +description: Web platform implementation of image_picker +homepage: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker_for_web +# 0.1.y+z is compatible with 1.0.0, if you land a breaking change bump +# the version to 2.0.0. +# See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 +version: 0.1.0 + +flutter: + plugin: + platforms: + web: + pluginClass: ImagePickerPlugin + fileName: image_picker_for_web.dart + +dependencies: + image_picker_platform_interface: + path: ../image_picker_platform_interface + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + meta: ^1.1.7 + js: ^0.6.0 + +dev_dependencies: + flutter_test: + sdk: flutter + pedantic: ^1.8.0 + mockito: ^4.1.1 + +environment: + sdk: ">=2.5.0 <3.0.0" + flutter: ">=1.10.0 <2.0.0" diff --git a/packages/image_picker/image_picker_for_web/test/url_launcher_web_test.dart b/packages/image_picker/image_picker_for_web/test/url_launcher_web_test.dart new file mode 100644 index 000000000000..b6cf8b70460e --- /dev/null +++ b/packages/image_picker/image_picker_for_web/test/url_launcher_web_test.dart @@ -0,0 +1,103 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@TestOn('chrome') // Uses web-only Flutter SDK + +import 'dart:html' as html; +import 'package:flutter_test/flutter_test.dart'; +import 'package:url_launcher_web/url_launcher_web.dart'; +import 'package:url_launcher_web/src/navigator.dart' as navigator; +import 'package:mockito/mockito.dart'; + +class MockWindow extends Mock implements html.Window {} + +void main() { + group('$UrlLauncherPlugin', () { + MockWindow mockWindow = MockWindow(); + UrlLauncherPlugin plugin = UrlLauncherPlugin(window: mockWindow); + + group('canLaunch', () { + test('"http" URLs -> true', () { + expect(plugin.canLaunch('http://google.com'), completion(isTrue)); + }); + + test('"https" URLs -> true', () { + expect(plugin.canLaunch('https://google.com'), completion(isTrue)); + }); + + test('"mailto" URLs -> true', () { + expect( + plugin.canLaunch('mailto:name@mydomain.com'), completion(isTrue)); + }); + + test('"tel" URLs -> false', () { + expect(plugin.canLaunch('tel:5551234567'), completion(isFalse)); + }); + }); + + group('launch', () { + setUp(() { + // Simulate that window.open does something. + when(mockWindow.open('https://www.google.com', '')) + .thenReturn(MockWindow()); + when(mockWindow.open('mailto:name@mydomain.com', '')) + .thenReturn(MockWindow()); + }); + + test('launching a URL returns true', () { + expect( + plugin.launch( + 'https://www.google.com', + useSafariVC: null, + useWebView: null, + universalLinksOnly: null, + enableDomStorage: null, + enableJavaScript: null, + headers: null, + ), + completion(isTrue)); + }); + + test('launching a "mailto" returns true', () { + expect( + plugin.launch( + 'mailto:name@mydomain.com', + useSafariVC: null, + useWebView: null, + universalLinksOnly: null, + enableDomStorage: null, + enableJavaScript: null, + headers: null, + ), + completion(isTrue)); + }); + }); + + group('openNewWindow', () { + bool _standalone; + + setUp(() { + _standalone = navigator.standalone; + }); + + tearDown(() { + navigator.standalone = _standalone; + }); + + test('the window that is launched is a new window', () { + plugin.openNewWindow('https://www.google.com'); + + verify(mockWindow.open('https://www.google.com', '')); + }); + + test('the window that is launched is in the same window', () { + navigator.standalone = true; + + plugin.openNewWindow('https://www.google.com'); + + verify(mockWindow.open('https://www.google.com', '_top')); + }); + }); + }); +} From c0583224bbd63dc075a82751afbf148913a92a9c Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Fri, 15 May 2020 15:29:10 -0700 Subject: [PATCH 05/15] Make API Async, so web can use objectUrls internally, instead of bytes. --- .../lib/src/types/picked_file/base.dart | 8 ++--- .../lib/src/types/picked_file/html.dart | 32 +++++++++++++------ .../lib/src/types/picked_file/io.dart | 8 ++--- .../pubspec.yaml | 1 + 4 files changed, 31 insertions(+), 18 deletions(-) diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/base.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/base.dart index 01900fb71da6..08afe6f1e41e 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/base.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/base.dart @@ -32,15 +32,15 @@ abstract class PickedFileBase { /// Synchronously read the entire file contents as a string using the given [Encoding]. /// /// Throws Exception if the operation fails. - String readAsStringSync({Encoding encoding = utf8}) { - throw UnimplementedError('readAsStringSync() has not been implemented.'); + Future readAsString({Encoding encoding = utf8}) { + throw UnimplementedError('readAsString() has not been implemented.'); } /// Synchronously read the entire file contents as a list of bytes. /// /// Throws Exception if the operation fails. - Uint8List readAsBytesSync() { - throw UnimplementedError('readAsBytesSync() has not been implemented.'); + Future readAsBytes() { + throw UnimplementedError('readAsBytes() has not been implemented.'); } /// Create a new independent [Stream] for the contents of this file. diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/html.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/html.dart index 7b211c6bdff8..0faf531f3f75 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/html.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/html.dart @@ -1,6 +1,8 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:http/http.dart' as http show readBytes; + import './base.dart'; /// A PickedFile that works on web. @@ -8,26 +10,36 @@ import './base.dart'; /// It wraps the bytes of a selected file. class PickedFile extends PickedFileBase { final String path; - final Uint8List _bytes; + final Uint8List _initBytes; - /// Construct a PickedFile object, from its `bytes`. + /// Construct a PickedFile object from its ObjectUrl. + /// + /// Optionally, this can be initialized with `bytes` + /// so no http requests are performed to retrieve files later. PickedFile(this.path, {Uint8List bytes}) - : _bytes = bytes, + : _initBytes = bytes, super(path); + Future get _bytes async { + if (_initBytes != null) { + return Future.value(UnmodifiableUint8ListView(_initBytes)); + } + return http.readBytes(path); + } + @override - String readAsStringSync({Encoding encoding = utf8}) { - return encoding.decode(_bytes); + Future readAsString({Encoding encoding = utf8}) async { + return encoding.decode(await _bytes); } @override - Uint8List readAsBytesSync() { - return UnmodifiableUint8ListView(_bytes); + Future readAsBytes() async { + return Future.value(await _bytes); } @override - Stream openRead([int start, int end]) { - return Stream.fromIterable( - [_bytes.sublist(start ?? 0, end ?? _bytes.length)]); + Stream openRead([int start, int end]) async* { + final bytes = await _bytes; + yield bytes.sublist(start ?? 0, end ?? bytes.length); } } diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart index 7c229913c3e5..6fbde4943290 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart @@ -19,13 +19,13 @@ class PickedFile extends PickedFileBase { } @override - String readAsStringSync({Encoding encoding = utf8}) { - return _file.readAsStringSync(encoding: encoding); + Future readAsString({Encoding encoding = utf8}) { + return _file.readAsString(encoding: encoding); } @override - Uint8List readAsBytesSync() { - return _file.readAsBytesSync(); + Future readAsBytes() { + return _file.readAsBytes(); } @override diff --git a/packages/image_picker/image_picker_platform_interface/pubspec.yaml b/packages/image_picker/image_picker_platform_interface/pubspec.yaml index e4e4a7ad1c8e..946cf80c5187 100644 --- a/packages/image_picker/image_picker_platform_interface/pubspec.yaml +++ b/packages/image_picker/image_picker_platform_interface/pubspec.yaml @@ -9,6 +9,7 @@ dependencies: flutter: sdk: flutter meta: ^1.1.8 + http: ^0.12.1 plugin_platform_interface: ^1.0.2 dev_dependencies: From 2298b20c3b712074729e5e40009ecdd6c615d1e0 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Fri, 15 May 2020 15:30:17 -0700 Subject: [PATCH 06/15] Modify core image_picker so it uses the new APIs, and add web example --- .../image_picker/example/lib/main.dart | 28 ++++++++------- .../image_picker/example/pubspec.yaml | 2 +- .../image_picker/example/web/favicon.png | Bin 0 -> 917 bytes .../example/web/icons/Icon-192.png | Bin 0 -> 5292 bytes .../example/web/icons/Icon-512.png | Bin 0 -> 8252 bytes .../image_picker/example/web/index.html | 33 ++++++++++++++++++ .../image_picker/example/web/manifest.json | 23 ++++++++++++ .../image_picker/image_picker/pubspec.yaml | 4 +++ 8 files changed, 77 insertions(+), 13 deletions(-) create mode 100644 packages/image_picker/image_picker/example/web/favicon.png create mode 100644 packages/image_picker/image_picker/example/web/icons/Icon-192.png create mode 100644 packages/image_picker/image_picker/example/web/icons/Icon-512.png create mode 100644 packages/image_picker/image_picker/example/web/index.html create mode 100644 packages/image_picker/image_picker/example/web/manifest.json diff --git a/packages/image_picker/image_picker/example/lib/main.dart b/packages/image_picker/image_picker/example/lib/main.dart index ca2ed3126a37..f76be2986db3 100755 --- a/packages/image_picker/image_picker/example/lib/main.dart +++ b/packages/image_picker/image_picker/example/lib/main.dart @@ -53,9 +53,7 @@ class _MyHomePageState extends State { if (file != null && mounted) { await _disposeVideoController(); if (kIsWeb) { - // Need VideoPlayerController.memory! - // _controller = VideoPlayerController.memory(file.readAsBytesSync()); - return; // noop + _controller = VideoPlayerController.network(file.path); } else { _controller = VideoPlayerController.file(File(file.path)); } @@ -79,14 +77,19 @@ class _MyHomePageState extends State { await _displayPickImageDialog(context, (double maxWidth, double maxHeight, int quality) async { try { - _imageFile = await _picker.getImage( - source: source, - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality); - setState(() {}); + final pickedFile = await _picker.getImage( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); + setState(() { + _imageFile = pickedFile; + }); } catch (e) { - _pickImageError = e; + setState(() { + _pickImageError = e; + }); } }); } @@ -141,7 +144,8 @@ class _MyHomePageState extends State { } if (_imageFile != null) { if (kIsWeb) { - return Image.memory(_imageFile.readAsBytesSync()); + return Image.network(_imageFile.path); + // Or from memory... } else { // This would also work from memory as well... return Image.file(File(_imageFile.path)); @@ -186,7 +190,7 @@ class _MyHomePageState extends State { title: Text(widget.title), ), body: Center( - child: Platform.isAndroid + child: !kIsWeb && defaultTargetPlatform == TargetPlatform.android ? FutureBuilder( future: retrieveLostData(), builder: (BuildContext context, AsyncSnapshot snapshot) { diff --git a/packages/image_picker/image_picker/example/pubspec.yaml b/packages/image_picker/image_picker/example/pubspec.yaml index d089161839bc..f3171aa7ccf8 100755 --- a/packages/image_picker/image_picker/example/pubspec.yaml +++ b/packages/image_picker/image_picker/example/pubspec.yaml @@ -6,9 +6,9 @@ dependencies: video_player: ^0.10.3 flutter: sdk: flutter + flutter_plugin_android_lifecycle: ^1.0.2 image_picker: path: ../ - flutter_plugin_android_lifecycle: ^1.0.2 dev_dependencies: flutter_driver: diff --git a/packages/image_picker/image_picker/example/web/favicon.png b/packages/image_picker/image_picker/example/web/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8aaa46ac1ae21512746f852a42ba87e4165dfdd1 GIT binary patch literal 917 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|I14-?iy0X7 zltGxWVyS%@P(fs7NJL45ua8x7ey(0(N`6wRUPW#JP&EUCO@$SZnVVXYs8ErclUHn2 zVXFjIVFhG^g!Ppaz)DK8ZIvQ?0~DO|i&7O#^-S~(l1AfjnEK zjFOT9D}DX)@^Za$W4-*MbbUihOG|wNBYh(yU7!lx;>x^|#0uTKVr7USFmqf|i<65o z3raHc^AtelCMM;Vme?vOfh>Xph&xL%(-1c06+^uR^q@XSM&D4+Kp$>4P^%3{)XKjo zGZknv$b36P8?Z_gF{nK@`XI}Z90TzwSQO}0J1!f2c(B=V`5aP@1P1a|PZ!4!3&Gl8 zTYqUsf!gYFyJnXpu0!n&N*SYAX-%d(5gVjrHJWqXQshj@!Zm{!01WsQrH~9=kTxW#6SvuapgMqt>$=j#%eyGrQzr zP{L-3gsMA^$I1&gsBAEL+vxi1*Igl=8#8`5?A-T5=z-sk46WA1IUT)AIZHx1rdUrf zVJrJn<74DDw`j)Ki#gt}mIT-Q`XRa2-jQXQoI%w`nb|XblvzK${ZzlV)m-XcwC(od z71_OEC5Bt9GEXosOXaPTYOia#R4ID2TiU~`zVMl08TV_C%DnU4^+HE>9(CE4D6?Fz oujB08i7adh9xk7*FX66dWH6F5TM;?E2b5PlUHx3vIVCg!0Dx9vYXATM literal 0 HcmV?d00001 diff --git a/packages/image_picker/image_picker/example/web/icons/Icon-192.png b/packages/image_picker/image_picker/example/web/icons/Icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..b749bfef07473333cf1dd31e9eed89862a5d52aa GIT binary patch literal 5292 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 literal 0 HcmV?d00001 diff --git a/packages/image_picker/image_picker/example/web/icons/Icon-512.png b/packages/image_picker/image_picker/example/web/icons/Icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..88cfd48dff1169879ba46840804b412fe02fefd6 GIT binary patch literal 8252 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 literal 0 HcmV?d00001 diff --git a/packages/image_picker/image_picker/example/web/index.html b/packages/image_picker/image_picker/example/web/index.html new file mode 100644 index 000000000000..787bbc72f6b1 --- /dev/null +++ b/packages/image_picker/image_picker/example/web/index.html @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + url_launcher web example + + + + + + + + diff --git a/packages/image_picker/image_picker/example/web/manifest.json b/packages/image_picker/image_picker/example/web/manifest.json new file mode 100644 index 000000000000..7d9c25627ebd --- /dev/null +++ b/packages/image_picker/image_picker/example/web/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "image_picker example", + "short_name": "image_picker", + "start_url": ".", + "display": "minimal-ui", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "An example of the image_picker on the web.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index e98e7014add6..c968964618f6 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -12,6 +12,8 @@ flutter: pluginClass: ImagePickerPlugin ios: pluginClass: FLTImagePickerPlugin + web: + default_package: image_picker_for_web dependencies: flutter: @@ -19,6 +21,8 @@ dependencies: flutter_plugin_android_lifecycle: ^1.0.2 image_picker_platform_interface: path: ../image_picker_platform_interface + image_picker_for_web: + path: ../image_picker_for_web dev_dependencies: video_player: ^0.10.3 From 3bd68e1e3eefc08456c69c86012cd8cc6d779135 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Mon, 18 May 2020 14:15:24 -0700 Subject: [PATCH 07/15] [image_picker] Ignore deprecated use analyzer warnings --- packages/image_picker/image_picker/lib/image_picker.dart | 2 ++ packages/image_picker/image_picker/test/image_picker_test.dart | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/image_picker/image_picker/lib/image_picker.dart b/packages/image_picker/image_picker/lib/image_picker.dart index 110365751ea2..7dd7e8e52469 100755 --- a/packages/image_picker/image_picker/lib/image_picker.dart +++ b/packages/image_picker/image_picker/lib/image_picker.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: deprecated_member_use, deprecated_member_use_from_same_package + import 'dart:async'; import 'dart:io'; diff --git a/packages/image_picker/image_picker/test/image_picker_test.dart b/packages/image_picker/image_picker/test/image_picker_test.dart index 8db71adcf778..8d4e068a261c 100644 --- a/packages/image_picker/image_picker/test/image_picker_test.dart +++ b/packages/image_picker/image_picker/test/image_picker_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: deprecated_member_use, deprecated_member_use_from_same_package + import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:image_picker/image_picker.dart'; From eea41191cfb66d7ef90ae62ef01ce11c37f87225 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Mon, 18 May 2020 14:15:54 -0700 Subject: [PATCH 08/15] [image_picker_platform_interface] Ignore deprecated use analyzer warnings --- .../lib/src/method_channel/method_channel_image_picker.dart | 3 +++ .../test/method_channel_image_picker_test.dart | 2 ++ 2 files changed, 5 insertions(+) diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart index a9dadbf68df4..71704b63ced4 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart @@ -139,10 +139,12 @@ class MethodChannelImagePicker extends ImagePickerPlatform { } @override + // ignore: deprecated_member_use_from_same_package Future retrieveLostDataAsDartIoFile() async { final Map result = await _channel.invokeMapMethod('retrieve'); if (result == null) { + // ignore: deprecated_member_use_from_same_package return LostDataResponse.empty(); } assert(result.containsKey('path') ^ result.containsKey('errorCode')); @@ -165,6 +167,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { final String path = result['path']; + // ignore: deprecated_member_use_from_same_package return LostDataResponse( file: path == null ? null : File(path), exception: exception, diff --git a/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart b/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart index 701379b84aae..e6968b90f36c 100644 --- a/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart +++ b/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart @@ -299,6 +299,7 @@ void main() { 'path': '/example/path', }; }); + // ignore: deprecated_member_use_from_same_package final LostDataResponse response = await picker.retrieveLostDataAsDartIoFile(); expect(response.type, RetrieveType.image); @@ -313,6 +314,7 @@ void main() { 'errorMessage': 'test_error_message', }; }); + // ignore: deprecated_member_use_from_same_package final LostDataResponse response = await picker.retrieveLostDataAsDartIoFile(); expect(response.type, RetrieveType.video); From 9573eaf79cd17fb8b2982d978c0d57935d6265c0 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Mon, 18 May 2020 14:17:04 -0700 Subject: [PATCH 09/15] [image_picker_for_web] Remove copypaste from test file. --- .../test/image_picker_for_web_test.dart | 15 +++ .../test/url_launcher_web_test.dart | 103 ------------------ 2 files changed, 15 insertions(+), 103 deletions(-) create mode 100644 packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart delete mode 100644 packages/image_picker/image_picker_for_web/test/url_launcher_web_test.dart diff --git a/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart b/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart new file mode 100644 index 000000000000..678dba4a5a30 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart @@ -0,0 +1,15 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@TestOn('chrome') // Uses web-only Flutter SDK + +import 'dart:html' as html; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +class MockWindow extends Mock implements html.Window {} + +void main() { + +} diff --git a/packages/image_picker/image_picker_for_web/test/url_launcher_web_test.dart b/packages/image_picker/image_picker_for_web/test/url_launcher_web_test.dart deleted file mode 100644 index b6cf8b70460e..000000000000 --- a/packages/image_picker/image_picker_for_web/test/url_launcher_web_test.dart +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -@TestOn('chrome') // Uses web-only Flutter SDK - -import 'dart:html' as html; -import 'package:flutter_test/flutter_test.dart'; -import 'package:url_launcher_web/url_launcher_web.dart'; -import 'package:url_launcher_web/src/navigator.dart' as navigator; -import 'package:mockito/mockito.dart'; - -class MockWindow extends Mock implements html.Window {} - -void main() { - group('$UrlLauncherPlugin', () { - MockWindow mockWindow = MockWindow(); - UrlLauncherPlugin plugin = UrlLauncherPlugin(window: mockWindow); - - group('canLaunch', () { - test('"http" URLs -> true', () { - expect(plugin.canLaunch('http://google.com'), completion(isTrue)); - }); - - test('"https" URLs -> true', () { - expect(plugin.canLaunch('https://google.com'), completion(isTrue)); - }); - - test('"mailto" URLs -> true', () { - expect( - plugin.canLaunch('mailto:name@mydomain.com'), completion(isTrue)); - }); - - test('"tel" URLs -> false', () { - expect(plugin.canLaunch('tel:5551234567'), completion(isFalse)); - }); - }); - - group('launch', () { - setUp(() { - // Simulate that window.open does something. - when(mockWindow.open('https://www.google.com', '')) - .thenReturn(MockWindow()); - when(mockWindow.open('mailto:name@mydomain.com', '')) - .thenReturn(MockWindow()); - }); - - test('launching a URL returns true', () { - expect( - plugin.launch( - 'https://www.google.com', - useSafariVC: null, - useWebView: null, - universalLinksOnly: null, - enableDomStorage: null, - enableJavaScript: null, - headers: null, - ), - completion(isTrue)); - }); - - test('launching a "mailto" returns true', () { - expect( - plugin.launch( - 'mailto:name@mydomain.com', - useSafariVC: null, - useWebView: null, - universalLinksOnly: null, - enableDomStorage: null, - enableJavaScript: null, - headers: null, - ), - completion(isTrue)); - }); - }); - - group('openNewWindow', () { - bool _standalone; - - setUp(() { - _standalone = navigator.standalone; - }); - - tearDown(() { - navigator.standalone = _standalone; - }); - - test('the window that is launched is a new window', () { - plugin.openNewWindow('https://www.google.com'); - - verify(mockWindow.open('https://www.google.com', '')); - }); - - test('the window that is launched is in the same window', () { - navigator.standalone = true; - - plugin.openNewWindow('https://www.google.com'); - - verify(mockWindow.open('https://www.google.com', '_top')); - }); - }); - }); -} From 2e323c12bcc30e2ee5ed29d611a0f8f7351fbd86 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Mon, 18 May 2020 14:29:18 -0700 Subject: [PATCH 10/15] [image_picker] Bump changelog and version --- packages/image_picker/image_picker/CHANGELOG.md | 4 ++++ packages/image_picker/image_picker/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index c557f4ac0a89..408a0e43edf3 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.7 + +* Utilize the new platform_interface package. + ## 0.6.6+4 * Fix bug, sometimes double click cancel button will crash. diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index c968964618f6..3708fae435f2 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker description: Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera. homepage: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker -version: 0.6.6+4 +version: 0.6.7 flutter: plugin: From e426c9a088173cb3813899d710671069bd2b4f0a Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Mon, 18 May 2020 17:10:49 -0700 Subject: [PATCH 11/15] Add pick file unit test --- .../lib/image_picker_for_web.dart | 23 ++++--- .../test/image_picker_for_web_test.dart | 64 ++++++++++++++++++- 2 files changed, 78 insertions(+), 9 deletions(-) diff --git a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart index cfaf814c65c1..68664ae7b6d4 100644 --- a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart +++ b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart @@ -13,14 +13,15 @@ final String _kAcceptVideoMimeType = 'video/*'; /// /// This class implements the `package:image_picker` functionality for the web. class ImagePickerPlugin extends ImagePickerPlatform { + final Function _overrideCreateInput; + bool get _shouldOverrideInput => _overrideCreateInput != null; + html.Element _target; - /// A constructor that allows tests to override the window object used by the plugin. - ImagePickerPlugin({@visibleForTesting html.Element target}) - : _target = target { - if (_target == null) { - _target = _initTarget(_kImagePickerInputsDomId); - } + /// A constructor that allows tests to override the function that creates file inputs. + ImagePickerPlugin({@visibleForTesting Function overrideCreateInput}) + : _overrideCreateInput = overrideCreateInput { + _target = _initTarget(_kImagePickerInputsDomId); } /// Registers this class as the default instance of [ImagePickerPlatform]. @@ -122,6 +123,10 @@ class ImagePickerPlugin extends ImagePickerPlatform { html.Element _createInputElement(String accept, String capture) { html.Element element; + if (_shouldOverrideInput) { + return _overrideCreateInput(accept, capture); + } + if (capture != null) { // Capture is not supported by dart:html :/ element = html.Element.html( @@ -137,8 +142,10 @@ class ImagePickerPlugin extends ImagePickerPlatform { /// Injects the file input element, and clicks on it void _injectAndActivate(html.Element element) { - _target.children.clear(); - _target.children.add(element); + if (!_shouldOverrideInput) { + _target.children.clear(); + _target.children.add(element); + } element.click(); } } diff --git a/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart b/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart index 678dba4a5a30..0bc6adfe775c 100644 --- a/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart +++ b/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart @@ -4,12 +4,74 @@ @TestOn('chrome') // Uses web-only Flutter SDK +import 'dart:async'; +import 'dart:convert'; import 'dart:html' as html; +import 'dart:typed_data'; + import 'package:flutter_test/flutter_test.dart'; +import 'package:image_picker_for_web/image_picker_for_web.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; import 'package:mockito/mockito.dart'; -class MockWindow extends Mock implements html.Window {} +final String expectedStringContents = "Hello, world!"; +final Uint8List bytes = utf8.encode(expectedStringContents); +final html.File textFile = html.File([bytes], "hello.txt"); + +class MockFileInput extends Mock implements html.FileUploadInputElement {} + +class MockOnChangeEvent extends Mock implements html.Event { + @override + MockFileInput target; +} + +class MockElementStream extends Mock + implements html.ElementStream { + final StreamController controller = StreamController(); + @override + StreamSubscription listen(void onData(T event), + {Function onError, void onDone(), bool cancelOnError}) { + return controller.stream.listen(onData, + onError: onError, onDone: onDone, cancelOnError: cancelOnError); + } +} void main() { + MockFileInput mockInput = MockFileInput(); + MockElementStream mockStream = MockElementStream(); + MockElementStream mockErrorStream = MockElementStream(); + MockOnChangeEvent mockEvent = MockOnChangeEvent()..target = mockInput; + + // Under test... + ImagePickerPlugin plugin = + ImagePickerPlugin(overrideCreateInput: (_, __) => mockInput); + + setUp(() { + // Make the mockInput behave like a proper input... + when(mockInput.onChange).thenAnswer((_) => mockStream); + when(mockInput.onError).thenAnswer((_) => mockErrorStream); + }); + + tearDown(() { + reset(mockInput); + }); + + // Pick a file... + test('Can select a file, happy case', () async { + // Init the pick file dialog... + final file = plugin.pickImage( + source: ImageSource.gallery, + ); + + // Mock the browser behavior of selecting a file... + when(mockInput.files).thenReturn([textFile]); + mockStream.controller.add(mockEvent); + + // Now the file should be selected + expect(file, completes); + // And readable + expect((await file).readAsString(), completion(expectedStringContents)); + }); + // Creates the correct DOM for the input... } From d95c9edb75654055b886fd3504df6865d936c5d8 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Wed, 20 May 2020 16:52:39 -0700 Subject: [PATCH 12/15] [image_picker_platform_interface] Add unit tests Run with flutter test / flutter test --platform chrome --- .../lib/src/types/picked_file/io.dart | 2 +- .../test/assets/hello.txt | 1 + .../method_channel_image_picker_test.dart | 2 +- .../new_method_channel_image_picker_test.dart | 345 ++++++++++++++++++ .../test/picked_file_html_test.dart | 39 ++ .../test/picked_file_io_test.dart | 39 ++ 6 files changed, 426 insertions(+), 2 deletions(-) create mode 100644 packages/image_picker/image_picker_platform_interface/test/assets/hello.txt create mode 100644 packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart create mode 100644 packages/image_picker/image_picker_platform_interface/test/picked_file_html_test.dart create mode 100644 packages/image_picker/image_picker_platform_interface/test/picked_file_io_test.dart diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart index 6fbde4943290..cd96f4ba89bc 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart @@ -30,6 +30,6 @@ class PickedFile extends PickedFileBase { @override Stream openRead([int start, int end]) { - return _file.openRead(start ?? 0, end); + return _file.openRead(start ?? 0, end).map((chunk) => Uint8List.fromList(chunk)); } } diff --git a/packages/image_picker/image_picker_platform_interface/test/assets/hello.txt b/packages/image_picker/image_picker_platform_interface/test/assets/hello.txt new file mode 100644 index 000000000000..5dd01c177f5d --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/test/assets/hello.txt @@ -0,0 +1 @@ +Hello, world! \ No newline at end of file diff --git a/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart b/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart index e6968b90f36c..ddaad3d32f41 100644 --- a/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart +++ b/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart @@ -340,6 +340,6 @@ void main() { }); expect(picker.retrieveLostDataAsDartIoFile(), throwsAssertionError); }); - }); + }, skip: isBrowser); }); } diff --git a/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart b/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart new file mode 100644 index 000000000000..714f9b4b8061 --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart @@ -0,0 +1,345 @@ +// Copyright 2019 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. + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:image_picker_platform_interface/src/method_channel/method_channel_image_picker.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$MethodChannelImagePicker', () { + MethodChannelImagePicker picker = MethodChannelImagePicker(); + + final List log = []; + + setUp(() { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return ''; + }); + + log.clear(); + }); + + group('#pickImage', () { + test('passes the image source argument correctly', () async { + await picker.pickImage(source: ImageSource.camera); + await picker.pickImage(source: ImageSource.gallery); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 1, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + await picker.pickImage(source: ImageSource.camera); + await picker.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + ); + await picker.pickImage( + source: ImageSource.camera, + maxHeight: 10.0, + ); + await picker.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.pickImage( + source: ImageSource.camera, maxWidth: 10.0, imageQuality: 70,); + await picker.pickImage( + source: ImageSource.camera, maxHeight: 10.0, imageQuality: 70,); + await picker.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70,); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'cameraDevice': 0 + }), + ], + ); + }); + + test('does not accept a negative width or height argument', () { + expect( + () => + picker.pickImage(source: ImageSource.camera, maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + () => + picker.pickImage(source: ImageSource.camera, maxHeight: -1.0), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + picker.channel + .setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.pickImage(source: ImageSource.gallery), isNull); + expect(await picker.pickImage(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.pickImage(source: ImageSource.camera); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.pickImage( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 1, + }), + ], + ); + }); + }); + + group('#pickVideoPath', () { + test('passes the image source argument correctly', () async { + await picker.pickVideo(source: ImageSource.camera); + await picker.pickVideo(source: ImageSource.gallery); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'cameraDevice': 0, + 'maxDuration': null, + }), + isMethodCall('pickVideo', arguments: { + 'source': 1, + 'cameraDevice': 0, + 'maxDuration': null, + }), + ], + ); + }); + + test('passes the duration argument correctly', () async { + await picker.pickVideo(source: ImageSource.camera); + await picker.pickVideo( + source: ImageSource.camera, + maxDuration: const Duration(seconds: 10),); + await picker.pickVideo( + source: ImageSource.camera, + maxDuration: const Duration(minutes: 1),); + await picker.pickVideo( + source: ImageSource.camera, maxDuration: const Duration(hours: 1),); + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': null, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 10, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 60, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 3600, + 'cameraDevice': 0, + }), + ], + ); + }); + + test('handles a null video path response gracefully', () async { + picker.channel + .setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.pickVideo(source: ImageSource.gallery), isNull); + expect(await picker.pickVideo(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.pickVideo(source: ImageSource.camera); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'cameraDevice': 0, + 'maxDuration': null, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.pickVideo( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front,); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': null, + 'cameraDevice': 1, + }), + ], + ); + }); + }); + + group('#retrieveLostData', () { + test('retrieveLostData get success response', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'image', + 'path': '/example/path', + }; + }); + // ignore: deprecated_member_use_from_same_package + final LostData response = + await picker.retrieveLostData(); + expect(response.type, RetrieveType.image); + expect(response.file.path, '/example/path'); + }); + + test('retrieveLostData get error response', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'video', + 'errorCode': 'test_error_code', + 'errorMessage': 'test_error_message', + }; + }); + // ignore: deprecated_member_use_from_same_package + final LostData response = + await picker.retrieveLostData(); + expect(response.type, RetrieveType.video); + expect(response.exception.code, 'test_error_code'); + expect(response.exception.message, 'test_error_message'); + }); + + test('retrieveLostData get null response', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return null; + }); + expect((await picker.retrieveLostData()).isEmpty, true); + }); + + test('retrieveLostData get both path and error should throw', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'video', + 'errorCode': 'test_error_code', + 'errorMessage': 'test_error_message', + 'path': '/example/path', + }; + }); + expect(picker.retrieveLostData(), throwsAssertionError); + }); + }); + }); +} diff --git a/packages/image_picker/image_picker_platform_interface/test/picked_file_html_test.dart b/packages/image_picker/image_picker_platform_interface/test/picked_file_html_test.dart new file mode 100644 index 000000000000..01af03b1abbc --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/test/picked_file_html_test.dart @@ -0,0 +1,39 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@TestOn('chrome') // Uses web-only Flutter SDK + +import 'dart:convert'; +import 'dart:html' as html; +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; + +final String expectedStringContents = 'Hello, world!'; +final Uint8List bytes = utf8.encode(expectedStringContents); +final html.File textFile = html.File([bytes], 'hello.txt'); +final String textFileUrl = html.Url.createObjectUrl(textFile); + +void main() { + + group('Create with an objectUrl', () { + final pickedFile = PickedFile(textFileUrl); + + test('Can be read as a string', () async { + expect(await pickedFile.readAsString(), equals(expectedStringContents)); + }); + test('Can be read as bytes', () async { + expect(await pickedFile.readAsBytes(), equals(bytes)); + }); + + test('Can be read as a stream', () async { + expect(await pickedFile.openRead().first, equals(bytes)); + }); + + test('Stream can be sliced', () async { + expect(await pickedFile.openRead(2, 5).first, equals(bytes.sublist(2, 5))); + }); + }); +} diff --git a/packages/image_picker/image_picker_platform_interface/test/picked_file_io_test.dart b/packages/image_picker/image_picker_platform_interface/test/picked_file_io_test.dart new file mode 100644 index 000000000000..aa9986cce91f --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/test/picked_file_io_test.dart @@ -0,0 +1,39 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@TestOn('vm') // Uses dart:io + +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; + +final String expectedStringContents = 'Hello, world!'; +final Uint8List bytes = utf8.encode(expectedStringContents); +final File textFile = File('./assets/hello.txt'); +final String textFilePath = textFile.path; + +void main() { + + group('Create with an objectUrl', () { + final pickedFile = PickedFile(textFilePath); + + test('Can be read as a string', () async { + expect(await pickedFile.readAsString(), equals(expectedStringContents)); + }); + test('Can be read as bytes', () async { + expect(await pickedFile.readAsBytes(), equals(bytes)); + }); + + test('Can be read as a stream', () async { + expect(await pickedFile.openRead().first, equals(bytes)); + }); + + test('Stream can be sliced', () async { + expect(await pickedFile.openRead(2, 5).first, equals(bytes.sublist(2, 5))); + }); + }); +} From 42106130f78bfb3cebe88c9671d37efdbe3a82df Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Wed, 20 May 2020 16:58:53 -0700 Subject: [PATCH 13/15] [image_picker_platform_interface] dartfmt -w . --- .../lib/src/types/picked_file/io.dart | 4 +- .../new_method_channel_image_picker_test.dart | 50 +++++++++++-------- .../test/picked_file_html_test.dart | 4 +- .../test/picked_file_io_test.dart | 4 +- 4 files changed, 36 insertions(+), 26 deletions(-) diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart index cd96f4ba89bc..dd64558bf044 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart @@ -30,6 +30,8 @@ class PickedFile extends PickedFileBase { @override Stream openRead([int start, int end]) { - return _file.openRead(start ?? 0, end).map((chunk) => Uint8List.fromList(chunk)); + return _file + .openRead(start ?? 0, end) + .map((chunk) => Uint8List.fromList(chunk)); } } diff --git a/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart b/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart index 714f9b4b8061..e7abe37e4838 100644 --- a/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart +++ b/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart @@ -67,14 +67,21 @@ void main() { maxHeight: 20.0, ); await picker.pickImage( - source: ImageSource.camera, maxWidth: 10.0, imageQuality: 70,); + source: ImageSource.camera, + maxWidth: 10.0, + imageQuality: 70, + ); await picker.pickImage( - source: ImageSource.camera, maxHeight: 10.0, imageQuality: 70,); + source: ImageSource.camera, + maxHeight: 10.0, + imageQuality: 70, + ); await picker.pickImage( - source: ImageSource.camera, - maxWidth: 10.0, - maxHeight: 20.0, - imageQuality: 70,); + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ); expect( log, @@ -134,14 +141,12 @@ void main() { test('does not accept a negative width or height argument', () { expect( - () => - picker.pickImage(source: ImageSource.camera, maxWidth: -1.0), + () => picker.pickImage(source: ImageSource.camera, maxWidth: -1.0), throwsArgumentError, ); expect( - () => - picker.pickImage(source: ImageSource.camera, maxHeight: -1.0), + () => picker.pickImage(source: ImageSource.camera, maxHeight: -1.0), throwsArgumentError, ); }); @@ -216,13 +221,17 @@ void main() { test('passes the duration argument correctly', () async { await picker.pickVideo(source: ImageSource.camera); await picker.pickVideo( - source: ImageSource.camera, - maxDuration: const Duration(seconds: 10),); + source: ImageSource.camera, + maxDuration: const Duration(seconds: 10), + ); await picker.pickVideo( - source: ImageSource.camera, - maxDuration: const Duration(minutes: 1),); + source: ImageSource.camera, + maxDuration: const Duration(minutes: 1), + ); await picker.pickVideo( - source: ImageSource.camera, maxDuration: const Duration(hours: 1),); + source: ImageSource.camera, + maxDuration: const Duration(hours: 1), + ); expect( log, [ @@ -275,8 +284,9 @@ void main() { test('camera position can set to front', () async { await picker.pickVideo( - source: ImageSource.camera, - preferredCameraDevice: CameraDevice.front,); + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front, + ); expect( log, @@ -300,8 +310,7 @@ void main() { }; }); // ignore: deprecated_member_use_from_same_package - final LostData response = - await picker.retrieveLostData(); + final LostData response = await picker.retrieveLostData(); expect(response.type, RetrieveType.image); expect(response.file.path, '/example/path'); }); @@ -315,8 +324,7 @@ void main() { }; }); // ignore: deprecated_member_use_from_same_package - final LostData response = - await picker.retrieveLostData(); + final LostData response = await picker.retrieveLostData(); expect(response.type, RetrieveType.video); expect(response.exception.code, 'test_error_code'); expect(response.exception.message, 'test_error_message'); diff --git a/packages/image_picker/image_picker_platform_interface/test/picked_file_html_test.dart b/packages/image_picker/image_picker_platform_interface/test/picked_file_html_test.dart index 01af03b1abbc..49d84ff88f88 100644 --- a/packages/image_picker/image_picker_platform_interface/test/picked_file_html_test.dart +++ b/packages/image_picker/image_picker_platform_interface/test/picked_file_html_test.dart @@ -17,7 +17,6 @@ final html.File textFile = html.File([bytes], 'hello.txt'); final String textFileUrl = html.Url.createObjectUrl(textFile); void main() { - group('Create with an objectUrl', () { final pickedFile = PickedFile(textFileUrl); @@ -33,7 +32,8 @@ void main() { }); test('Stream can be sliced', () async { - expect(await pickedFile.openRead(2, 5).first, equals(bytes.sublist(2, 5))); + expect( + await pickedFile.openRead(2, 5).first, equals(bytes.sublist(2, 5))); }); }); } diff --git a/packages/image_picker/image_picker_platform_interface/test/picked_file_io_test.dart b/packages/image_picker/image_picker_platform_interface/test/picked_file_io_test.dart index aa9986cce91f..94ff759a2fb2 100644 --- a/packages/image_picker/image_picker_platform_interface/test/picked_file_io_test.dart +++ b/packages/image_picker/image_picker_platform_interface/test/picked_file_io_test.dart @@ -17,7 +17,6 @@ final File textFile = File('./assets/hello.txt'); final String textFilePath = textFile.path; void main() { - group('Create with an objectUrl', () { final pickedFile = PickedFile(textFilePath); @@ -33,7 +32,8 @@ void main() { }); test('Stream can be sliced', () async { - expect(await pickedFile.openRead(2, 5).first, equals(bytes.sublist(2, 5))); + expect( + await pickedFile.openRead(2, 5).first, equals(bytes.sublist(2, 5))); }); }); } From 7d975c2012c18ab018a8f67c1b87c5cd22805467 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Wed, 20 May 2020 18:00:46 -0700 Subject: [PATCH 14/15] [image_picker_for_web] Add tests for web plugin. --- .../lib/image_picker_for_web.dart | 22 ++++-- .../test/image_picker_for_web_test.dart | 74 ++++++++++++++----- 2 files changed, 70 insertions(+), 26 deletions(-) diff --git a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart index 68664ae7b6d4..940866fcbc71 100644 --- a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart +++ b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart @@ -7,6 +7,7 @@ import 'package:image_picker_platform_interface/image_picker_platform_interface. final String _kImagePickerInputsDomId = '__image_picker_web-file-input'; final String _kAcceptImageMimeType = 'image/*'; +// This may not be enough for Safari. final String _kAcceptVideoMimeType = 'video/*'; /// The web implementation of [ImagePickerPlatform]. @@ -37,8 +38,8 @@ class ImagePickerPlugin extends ImagePickerPlatform { int imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, }) { - String capture = _computeCaptureAttribute(source, preferredCameraDevice); - return _pickFile(accept: _kAcceptImageMimeType, capture: capture); + String capture = computeCaptureAttribute(source, preferredCameraDevice); + return pickFile(accept: _kAcceptImageMimeType, capture: capture); } @override @@ -47,8 +48,8 @@ class ImagePickerPlugin extends ImagePickerPlatform { CameraDevice preferredCameraDevice = CameraDevice.rear, Duration maxDuration, }) { - String capture = _computeCaptureAttribute(source, preferredCameraDevice); - return _pickFile(accept: _kAcceptVideoMimeType, capture: capture); + String capture = computeCaptureAttribute(source, preferredCameraDevice); + return pickFile(accept: _kAcceptVideoMimeType, capture: capture); } /// Injects a file input with the specified accept+capture attributes, and @@ -56,11 +57,12 @@ class ImagePickerPlugin extends ImagePickerPlatform { /// /// `capture` is only supported in mobile browsers. /// See https://caniuse.com/#feat=html-media-capture - Future _pickFile({ + @visibleForTesting + Future pickFile({ String accept, String capture, }) { - html.FileUploadInputElement input = _createInputElement(accept, capture); + html.FileUploadInputElement input = createInputElement(accept, capture); _injectAndActivate(input); return _getSelectedFile(input); } @@ -68,7 +70,10 @@ class ImagePickerPlugin extends ImagePickerPlatform { // DOM methods /// Converts plugin configuration into a proper value for the `capture` attribute. - String _computeCaptureAttribute(ImageSource source, CameraDevice device) { + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#capture + @visibleForTesting + String computeCaptureAttribute(ImageSource source, CameraDevice device) { String capture; if (source == ImageSource.camera) { capture = device == CameraDevice.front ? 'user' : 'environment'; @@ -120,7 +125,8 @@ class ImagePickerPlugin extends ImagePickerPlatform { /// Creates an input element that accepts certain file types, and /// allows to `capture` from the device's cameras (where supported) - html.Element _createInputElement(String accept, String capture) { + @visibleForTesting + html.Element createInputElement(String accept, String capture) { html.Element element; if (_shouldOverrideInput) { diff --git a/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart b/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart index 0bc6adfe775c..eaff00a7d40e 100644 --- a/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart +++ b/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@TestOn('chrome') // Uses web-only Flutter SDK +@TestOn('chrome') // Uses dart:html import 'dart:async'; import 'dart:convert'; @@ -37,41 +37,79 @@ class MockElementStream extends Mock } void main() { - MockFileInput mockInput = MockFileInput(); - MockElementStream mockStream = MockElementStream(); - MockElementStream mockErrorStream = MockElementStream(); - MockOnChangeEvent mockEvent = MockOnChangeEvent()..target = mockInput; + // Mock the "pick file" browser behavior. + MockFileInput mockInput; + MockElementStream mockStream; + MockElementStream mockErrorStream; + MockOnChangeEvent mockEvent; // Under test... - ImagePickerPlugin plugin = - ImagePickerPlugin(overrideCreateInput: (_, __) => mockInput); + ImagePickerPlugin plugin; setUp(() { + mockInput = MockFileInput(); + mockStream = MockElementStream(); + mockErrorStream = MockElementStream(); + mockEvent = MockOnChangeEvent()..target = mockInput; + // Make the mockInput behave like a proper input... when(mockInput.onChange).thenAnswer((_) => mockStream); when(mockInput.onError).thenAnswer((_) => mockErrorStream); - }); - tearDown(() { - reset(mockInput); + plugin = ImagePickerPlugin(overrideCreateInput: (_, __) => mockInput); }); - // Pick a file... - test('Can select a file, happy case', () async { + test('Can select a file', () async { // Init the pick file dialog... - final file = plugin.pickImage( - source: ImageSource.gallery, - ); + final file = plugin.pickFile(); // Mock the browser behavior of selecting a file... when(mockInput.files).thenReturn([textFile]); mockStream.controller.add(mockEvent); - // Now the file should be selected + // Now the file should be available expect(file, completes); // And readable - expect((await file).readAsString(), completion(expectedStringContents)); + expect((await file).readAsBytes(), completion(isNotEmpty)); + }); + + // There's no good way of detecting when the user has "aborted" the selection. + + test('computeCaptureAttribute', () { + expect( + plugin.computeCaptureAttribute(ImageSource.gallery, CameraDevice.front), + isNull, + ); + expect( + plugin.computeCaptureAttribute(ImageSource.gallery, CameraDevice.rear), + isNull, + ); + expect( + plugin.computeCaptureAttribute(ImageSource.camera, CameraDevice.front), + 'user', + ); + expect( + plugin.computeCaptureAttribute(ImageSource.camera, CameraDevice.rear), + 'environment', + ); }); - // Creates the correct DOM for the input... + group('createInputElement', () { + setUp(() { + plugin = ImagePickerPlugin(); + }); + test('accept: any, capture: null', () { + html.Element input = plugin.createInputElement('any', null); + + expect(input.attributes, containsPair('accept', 'any')); + expect(input.attributes, isNot(contains('capture'))); + }); + + test('accept: any, capture: something', () { + html.Element input = plugin.createInputElement('any', 'something'); + + expect(input.attributes, containsPair('accept', 'any')); + expect(input.attributes, containsPair('capture', 'something')); + }); + }); } From bcab62dd7c55a76e013f4565b4520672d572e43c Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Wed, 20 May 2020 18:33:10 -0700 Subject: [PATCH 15/15] [image_picker_for_web] Update README.md --- .../image_picker_for_web/README.md | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/packages/image_picker/image_picker_for_web/README.md b/packages/image_picker/image_picker_for_web/README.md index 3582b604f637..e94ef805cace 100644 --- a/packages/image_picker/image_picker_for_web/README.md +++ b/packages/image_picker/image_picker_for_web/README.md @@ -2,6 +2,42 @@ The web implementation of [`image_picker`][1]. +## Browser Support + +Since Web Browsers don't offer direct access to their users' file system, the web version of the +plugin attempts to approximate those APIs as much as possible. + +### URL.createObjectURL() + +The `PickedFile` object in web is backed by [`URL.createObjectUrl` Web API](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL), +which is reasonably well supported across all browsers: + +![Data on support for the bloburls feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/bloburls.png) + +However, the returned `path` attribute of the `PickedFile` points to a `network` resource, and not a +local path in your users' drive. See **Use the plugin** below for some examples on how to use this +return value in a cross-platform way. + +### input file "accept" + +In order to filter only video/image content, some browsers offer an [`accept` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept) in their `input type="file"` form elements: + +![Data on support for the input-file-accept feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/input-file-accept.png) + +This feature is just a convenience for users, **not validation**. + +Users can override this setting on their browsers. You must validate in your app (or server) +that the user has picked the file type that you can handle. + +### input file "capture" + +In order to "take a photo", some mobile browsers offer a [`capture` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/capture): + +![Data on support for the html-media-capture feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/html-media-capture.png) + +Each browser may implement `capture` any way they please, so it may (or may not) make a +difference in your users' experience. + ## Usage ### Import the package @@ -20,6 +56,32 @@ dependencies: ``` ### Use the plugin -You should be able to use `package:image_picker` as normal. + +You should be able to use `package:image_picker` _almost_ as normal. + +Once the user has picked a file, the returned `PickedFile` instance will contain a +`network`-accessible URL (pointing to a location within the browser). + +The instace will also let you retrieve the bytes of the selected file across all platforms. + +If you want to use the path directly, your code would need look like this: + +```dart +... +if (kIsWeb) { + Image.network(pickedFile.path); +} else { + Image.file(File(pickedFile.path)); +} +... +``` + +Or, using bytes: + +```dart +... +Image.memory(await pickedFile.readAsBytes()) +... +``` [1]: ../image_picker/image_picker