blob: 08ce801cafbed9653584422685d98f31a408e66b [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
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/3gpp,video/x-m4v,video/mp4,video/*';
/// The web implementation of [ImagePickerPlatform].
///
/// This class implements the `package:image_picker` functionality for the web.
class ImagePickerPlugin extends ImagePickerPlatform {
final ImagePickerPluginTestOverrides? _overrides;
bool get _hasOverrides => _overrides != null;
late html.Element _target;
/// A constructor that allows tests to override the function that creates file inputs.
ImagePickerPlugin({
@visibleForTesting ImagePickerPluginTestOverrides? overrides,
}) : _overrides = overrides {
_target = _ensureInitialized(_kImagePickerInputsDomId);
}
/// Registers this class as the default instance of [ImagePickerPlatform].
static void registerWith(Registrar registrar) {
ImagePickerPlatform.instance = ImagePickerPlugin();
}
/// 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].
///
/// Note that the `maxWidth`, `maxHeight` and `imageQuality` arguments are not supported on the web. If any of these arguments is supplied, it'll be silently ignored by the web version of the plugin.
///
/// 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].
///
/// If no images were picked, the return value is null.
@override
Future<PickedFile> 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);
}
/// 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].
///
/// Note that the `maxDuration` argument is not supported on the web. If the argument is supplied, it'll be silently ignored by the web version of the plugin.
///
/// 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].
///
/// If no images were picked, the return value is null.
@override
Future<PickedFile> 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<PickedFile> pickFile({
String? accept,
String? capture,
}) {
html.FileUploadInputElement input =
createInputElement(accept, capture) as html.FileUploadInputElement;
_injectAndActivate(input);
return _getSelectedFile(input);
}
/// Returns an [XFile] with the image that was picked.
///
/// The `source` argument controls where the image comes from. This can
/// be either [ImageSource.camera] or [ImageSource.gallery].
///
/// Note that the `maxWidth`, `maxHeight` and `imageQuality` arguments are not supported on the web. If any of these arguments is supplied, it'll be silently ignored by the web version of the plugin.
///
/// 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].
///
/// If no images were picked, the return value is null.
@override
Future<XFile> getImage({
required ImageSource source,
double? maxWidth,
double? maxHeight,
int? imageQuality,
CameraDevice preferredCameraDevice = CameraDevice.rear,
}) {
String? capture = computeCaptureAttribute(source, preferredCameraDevice);
return getFile(accept: _kAcceptImageMimeType, capture: capture);
}
/// Returns an [XFile] containing the video that was picked.
///
/// The [source] argument controls where the video comes from. This can
/// be either [ImageSource.camera] or [ImageSource.gallery].
///
/// Note that the `maxDuration` argument is not supported on the web. If the argument is supplied, it'll be silently ignored by the web version of the plugin.
///
/// 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].
///
/// If no images were picked, the return value is null.
@override
Future<XFile> getVideo({
required ImageSource source,
CameraDevice preferredCameraDevice = CameraDevice.rear,
Duration? maxDuration,
}) {
String? capture = computeCaptureAttribute(source, preferredCameraDevice);
return getFile(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<XFile> getFile({
String? accept,
String? capture,
}) {
html.FileUploadInputElement input =
createInputElement(accept, capture) as html.FileUploadInputElement;
_injectAndActivate(input);
return _getSelectedXFile(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) {
if (source == ImageSource.camera) {
return (device == CameraDevice.front) ? 'user' : 'environment';
}
return null;
}
html.File? _getFileFromInput(html.FileUploadInputElement input) {
if (_hasOverrides) {
return _overrides!.getFileFromInput(input);
}
return input.files?.first;
}
/// Handles the OnChange event from a FileUploadInputElement object
/// Returns the objectURL of the selected file.
String? _handleOnChangeEvent(html.Event event) {
final html.FileUploadInputElement input =
event.target as html.FileUploadInputElement;
final html.File? file = _getFileFromInput(input);
if (file != null) {
return html.Url.createObjectUrl(file);
}
return null;
}
/// Monitors an <input type="file"> and returns the selected file.
Future<PickedFile> _getSelectedFile(html.FileUploadInputElement input) {
final Completer<PickedFile> _completer = Completer<PickedFile>();
// Observe the input until we can return something
input.onChange.first.then((event) {
final objectUrl = _handleOnChangeEvent(event);
if (!_completer.isCompleted && objectUrl != null) {
_completer.complete(PickedFile(objectUrl));
}
});
input.onError.first.then((event) {
if (!_completer.isCompleted) {
_completer.completeError(event);
}
});
// Note that we don't bother detaching from these streams, since the
// "input" gets re-created in the DOM every time the user needs to
// pick a file.
return _completer.future;
}
Future<XFile> _getSelectedXFile(html.FileUploadInputElement input) {
final Completer<XFile> _completer = Completer<XFile>();
// Observe the input until we can return something
input.onChange.first.then((event) {
final objectUrl = _handleOnChangeEvent(event);
if (!_completer.isCompleted && objectUrl != null) {
_completer.complete(XFile(objectUrl));
}
});
input.onError.first.then((event) {
if (!_completer.isCompleted) {
_completer.completeError(event);
}
});
// Note that we don't bother detaching from these streams, since the
// "input" gets re-created in the DOM every time the user needs to
// pick a file.
return _completer.future;
}
/// Initializes a DOM container where we can host input elements.
html.Element _ensureInitialized(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) {
if (_hasOverrides) {
return _overrides!.createInputElement(accept, capture);
}
html.Element element = html.FileUploadInputElement()..accept = accept;
if (capture != null) {
element.setAttribute('capture', capture);
}
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();
}
}
// Some tools to override behavior for unit-testing
/// A function that creates a file input with the passed in `accept` and `capture` attributes.
@visibleForTesting
typedef OverrideCreateInputFunction = html.Element Function(
String? accept,
String? capture,
);
/// A function that extracts a [html.File] from the file `input` passed in.
@visibleForTesting
typedef OverrideExtractFilesFromInputFunction = html.File Function(
html.Element? input,
);
/// Overrides for some of the functionality above.
@visibleForTesting
class ImagePickerPluginTestOverrides {
/// Override the creation of the input element.
late OverrideCreateInputFunction createInputElement;
/// Override the extraction of the selected file from an input element.
late OverrideExtractFilesFromInputFunction getFileFromInput;
}