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/example/lib/main.dart b/packages/image_picker/image_picker/example/lib/main.dart index 29781f7df449..f76be2986db3 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,25 @@ 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) { + _controller = VideoPlayerController.network(file.path); + } else { + _controller = VideoPlayerController.file(File(file.path)); + } await _controller.setVolume(1.0); await _controller.initialize(); await _controller.setLooping(true); @@ -64,21 +70,26 @@ 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( - 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; + }); } }); } @@ -132,7 +143,13 @@ class _MyHomePageState extends State { return retrieveError; } if (_imageFile != null) { - return Image.file(_imageFile); + if (kIsWeb) { + return Image.network(_imageFile.path); + // Or from memory... + } 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 +164,7 @@ class _MyHomePageState extends State { } Future retrieveLostData() async { - final LostDataResponse response = await ImagePicker.retrieveLostData(); + final LostData response = await _picker.getLostData(); if (response.isEmpty) { return; } @@ -173,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 000000000000..8aaa46ac1ae2 Binary files /dev/null and b/packages/image_picker/image_picker/example/web/favicon.png differ 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 000000000000..b749bfef0747 Binary files /dev/null and b/packages/image_picker/image_picker/example/web/icons/Icon-192.png differ 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 000000000000..88cfd48dff11 Binary files /dev/null and b/packages/image_picker/image_picker/example/web/icons/Icon-512.png differ 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/lib/image_picker.dart b/packages/image_picker/image_picker/lib/image_picker.dart index 0dd9cac8d346..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'; @@ -15,7 +17,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 +51,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 +69,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 +123,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 +137,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 +181,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..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: @@ -12,12 +12,17 @@ flutter: pluginClass: ImagePickerPlugin ios: pluginClass: FLTImagePickerPlugin + web: + default_package: image_picker_for_web 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 + image_picker_for_web: + path: ../image_picker_for_web dev_dependencies: video_player: ^0.10.3 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'; 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..e94ef805cace --- /dev/null +++ b/packages/image_picker/image_picker_for_web/README.md @@ -0,0 +1,87 @@ +# image_picker_for_web + +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 + +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` _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 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..940866fcbc71 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart @@ -0,0 +1,157 @@ +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/*'; +// This may not be enough for Safari. +final String _kAcceptVideoMimeType = 'video/*'; + +/// The web implementation of [ImagePickerPlatform]. +/// +/// 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 function that creates file inputs. + ImagePickerPlugin({@visibleForTesting Function overrideCreateInput}) + : _overrideCreateInput = overrideCreateInput { + _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 + @visibleForTesting + 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. + /// + /// 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'; + } + 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) + @visibleForTesting + 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( + '', + 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) { + if (!_shouldOverrideInput) { + _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/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..eaff00a7d40e --- /dev/null +++ b/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart @@ -0,0 +1,115 @@ +// 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 dart:html + +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'; + +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() { + // Mock the "pick file" browser behavior. + MockFileInput mockInput; + MockElementStream mockStream; + MockElementStream mockErrorStream; + MockOnChangeEvent mockEvent; + + // Under test... + 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); + + plugin = ImagePickerPlugin(overrideCreateInput: (_, __) => mockInput); + }); + + test('Can select a file', () async { + // Init the pick file dialog... + 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 available + expect(file, completes); + // And readable + 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', + ); + }); + + 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')); + }); + }); +} 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..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 @@ -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, @@ -71,10 +103,48 @@ 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 + // 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')); @@ -97,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/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/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..08afe6f1e41e --- /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. + 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. + Future readAsBytes() { + throw UnimplementedError('readAsBytes() 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..0faf531f3f75 --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/html.dart @@ -0,0 +1,45 @@ +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. +/// +/// It wraps the bytes of a selected file. +class PickedFile extends PickedFileBase { + final String path; + final Uint8List _initBytes; + + /// 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}) + : _initBytes = bytes, + super(path); + + Future get _bytes async { + if (_initBytes != null) { + return Future.value(UnmodifiableUint8ListView(_initBytes)); + } + return http.readBytes(path); + } + + @override + Future readAsString({Encoding encoding = utf8}) async { + return encoding.decode(await _bytes); + } + + @override + Future readAsBytes() async { + return Future.value(await _bytes); + } + + @override + 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 new file mode 100644 index 000000000000..dd64558bf044 --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart @@ -0,0 +1,37 @@ +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 + Future readAsString({Encoding encoding = utf8}) { + return _file.readAsString(encoding: encoding); + } + + @override + Future readAsBytes() { + return _file.readAsBytes(); + } + + @override + Stream openRead([int start, int end]) { + return _file + .openRead(start ?? 0, end) + .map((chunk) => Uint8List.fromList(chunk)); + } +} 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.'); + } +} 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..946cf80c5187 100644 --- a/packages/image_picker/image_picker_platform_interface/pubspec.yaml +++ b/packages/image_picker/image_picker_platform_interface/pubspec.yaml @@ -3,12 +3,13 @@ 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: sdk: flutter meta: ^1.1.8 + http: ^0.12.1 plugin_platform_interface: ^1.0.2 dev_dependencies: @@ -18,5 +19,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" 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 701379b84aae..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 @@ -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); @@ -338,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..e7abe37e4838 --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart @@ -0,0 +1,353 @@ +// 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..49d84ff88f88 --- /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..94ff759a2fb2 --- /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))); + }); + }); +}