Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/image_picker/image_picker_for_web/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 3.0.0

* **BREAKING CHANGE:** Removes all code and tests mentioning `PickedFile`.
* Listens to `cancel` event on file selection. When the selection is canceled:
* `Future<XFile?>` methods return `null`
* `Future<List<XFile>>` methods return an empty list.

## 2.2.0

* Adds `getMedia` method.
Expand Down
45 changes: 24 additions & 21 deletions packages/image_picker/image_picker_for_web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,12 @@ A web implementation of [`image_picker`][1].

## Limitations on the web platform

Since Web Browsers don't offer direct access to their users' file system,
this plugin provides a `PickedFile` abstraction to make access uniform
across platforms.
### `XFile`

The web version of the plugin puts network-accessible URIs as the `path`
in the returned `PickedFile`.
This plugin uses `XFile` objects to abstract files picked/created by the user.

### 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.
Read more about `XFile` on the web in
[`package:cross_file`'s README](https://pub.dev/packages/cross_file).

### input file "accept"

Expand All @@ -42,11 +31,25 @@ In order to "take a photo", some mobile browsers offer a [`capture` attribute](h
Each browser may implement `capture` any way they please, so it may (or may not) make a
difference in your users' experience.

### pickImage()
The arguments `maxWidth`, `maxHeight` and `imageQuality` are not supported for gif images.
The argument `imageQuality` only works for jpeg and webp images.
### input file "cancel"

The [`cancel` event](https://caniuse.com/mdn-api_htmlinputelement_cancel_event)
used by the plugin to detect when users close the file selector without picking
a file is relatively new, and will only work in recent browsers.

### `getImage()` parameters and `MediaOptions`

The `getImage`, `getMultiImage` and `getMedia` methods receive either a
`MediaOptions` object with `maxWidth`, `maxHeight` and `imageQuality` options,
or separately as individual parameters.

On the web:

* `maxWidth`, `maxHeight` and `imageQuality` are not supported for `gif` images.
* `imageQuality` only affects `jpg` and `webp` images.

### `getVideo()`

### pickVideo()
The argument `maxDuration` is not supported on the web.

## Usage
Expand All @@ -65,8 +68,8 @@ should add it to your `pubspec.yaml` as usual.

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).
Once the user has picked a file, the returned `XFile` instance will contain a
`network`-accessible Blob URL (pointing to a location within the browser).

The instance will also let you retrieve the bytes of the selected file across all platforms.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,28 +33,6 @@ void main() {
plugin = ImagePickerPlugin();
});

testWidgets('Can select a file (Deprecated)', (WidgetTester tester) async {
final html.FileUploadInputElement mockInput = html.FileUploadInputElement();

final ImagePickerPluginTestOverrides overrides =
ImagePickerPluginTestOverrides()
..createInputElement = ((_, __) => mockInput)
..getMultipleFilesFromInput = ((_) => <html.File>[textFile]);

final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides);

// Init the pick file dialog...
final Future<PickedFile> file = plugin.pickFile();

// Mock the browser behavior of selecting a file...
mockInput.dispatchEvent(html.Event('change'));

// Now the file should be available
expect(file, completes);
// And readable
expect((await file).readAsBytes(), completion(isNotEmpty));
});

testWidgets('Can select a file', (WidgetTester tester) async {
final html.FileUploadInputElement mockInput = html.FileUploadInputElement();

Expand All @@ -66,7 +44,7 @@ void main() {
final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides);

// Init the pick file dialog...
final Future<XFile> image = plugin.getImage(source: ImageSource.camera);
final Future<XFile?> image = plugin.getImage(source: ImageSource.camera);

// Mock the browser behavior of selecting a file...
mockInput.dispatchEvent(html.Event('change'));
Expand All @@ -75,8 +53,9 @@ void main() {
expect(image, completes);

// And readable
final XFile file = await image;
expect(file.readAsBytes(), completion(isNotEmpty));
final XFile? file = await image;
expect(file, isNotNull);
expect(file!.readAsBytes(), completion(isNotEmpty));
expect(file.name, textFile.name);
expect(file.length(), completion(textFile.size));
expect(file.mimeType, textFile.type);
Expand Down Expand Up @@ -150,7 +129,70 @@ void main() {
expect(secondFile.length(), completion(secondTextFile.size));
});

// There's no good way of detecting when the user has "aborted" the selection.
group('cancel event', () {
late html.FileUploadInputElement mockInput;
late ImagePickerPluginTestOverrides overrides;
late ImagePickerPlugin plugin;

setUp(() {
mockInput = html.FileUploadInputElement();
overrides = ImagePickerPluginTestOverrides()
..createInputElement = ((_, __) => mockInput)
..getMultipleFilesFromInput = ((_) => <html.File>[textFile]);
plugin = ImagePickerPlugin(overrides: overrides);
});

void mockCancel() {
mockInput.dispatchEvent(html.Event('cancel'));
}

testWidgets('getFiles - returns empty list', (WidgetTester _) async {
final Future<List<XFile>> files = plugin.getFiles();
mockCancel();

expect(files, completes);
expect(await files, isEmpty);
});

testWidgets('getMedia - returns empty list', (WidgetTester _) async {
final Future<List<XFile>?> files = plugin.getMedia(
options: const MediaOptions(
allowMultiple: true,
));
mockCancel();

expect(files, completes);
expect(await files, isEmpty);
});

testWidgets('getMultiImage - returns empty list', (WidgetTester _) async {
final Future<List<XFile>?> files = plugin.getMultiImage();
mockCancel();

expect(files, completes);
expect(await files, isEmpty);
});

testWidgets('getImage - returns null', (WidgetTester _) async {
final Future<XFile?> file = plugin.getImage(
source: ImageSource.gallery,
);
mockCancel();

expect(file, completes);
expect(await file, isNull);
});

testWidgets('getVideo - returns null', (WidgetTester _) async {
final Future<XFile?> file = plugin.getVideo(
source: ImageSource.gallery,
);
mockCancel();

expect(file, completes);
expect(await file, isNull);
});
});

testWidgets('computeCaptureAttribute', (WidgetTester tester) async {
expect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,70 +42,6 @@ class ImagePickerPlugin extends ImagePickerPlatform {
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,
}) {
final 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,
}) {
final 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,
}) {
final 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
Expand All @@ -119,7 +55,7 @@ class ImagePickerPlugin extends ImagePickerPlatform {
///
/// If no images were picked, the return value is null.
@override
Future<XFile> getImage({
Future<XFile?> getImage({
required ImageSource source,
double? maxWidth,
double? maxHeight,
Expand All @@ -132,12 +68,14 @@ class ImagePickerPlugin extends ImagePickerPlatform {
accept: _kAcceptImageMimeType,
capture: capture,
);
return _imageResizer.resizeImageIfNeeded(
files.first,
maxWidth,
maxHeight,
imageQuality,
);
return files.isEmpty
? null
: _imageResizer.resizeImageIfNeeded(
files.first,
maxWidth,
maxHeight,
imageQuality,
);
}

/// Returns an [XFile] containing the video that was picked.
Expand All @@ -153,7 +91,7 @@ class ImagePickerPlugin extends ImagePickerPlatform {
///
/// If no images were picked, the return value is null.
@override
Future<XFile> getVideo({
Future<XFile?> getVideo({
required ImageSource source,
CameraDevice preferredCameraDevice = CameraDevice.rear,
Duration? maxDuration,
Expand All @@ -164,7 +102,7 @@ class ImagePickerPlugin extends ImagePickerPlatform {
accept: _kAcceptVideoMimeType,
capture: capture,
);
return files.first;
return files.isEmpty ? null : files.first;
}

/// Injects a file input, and returns a list of XFile images that the user selected locally.
Expand Down Expand Up @@ -267,29 +205,6 @@ class ImagePickerPlugin extends ImagePickerPlatform {
return input == null ? null : _getFilesFromInput(input);
}

/// 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((html.Event event) {
final List<html.File>? files = _handleOnChangeEvent(event);
if (!completer.isCompleted && files != null) {
completer.complete(PickedFile(
html.Url.createObjectUrl(files.first),
));
}
});
input.onError.first.then((html.Event 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;
}

/// Monitors an <input type="file"> and returns the selected file(s).
Future<List<XFile>> _getSelectedXFiles(html.FileUploadInputElement input) {
final Completer<List<XFile>> completer = Completer<List<XFile>>();
Expand All @@ -310,6 +225,11 @@ class ImagePickerPlugin extends ImagePickerPlatform {
}).toList());
}
});

input.addEventListener('cancel', (html.Event _) {
completer.complete(<XFile>[]);
});

input.onError.first.then((html.Event event) {
if (!completer.isCompleted) {
completer.completeError(event);
Expand Down Expand Up @@ -361,6 +281,7 @@ class ImagePickerPlugin extends ImagePickerPlatform {
void _injectAndActivate(html.Element element) {
_target.children.clear();
_target.children.add(element);
// TODO(dit): Reimplement this with the showPicker() API, https://github.com/flutter/flutter/issues/130365
element.click();
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/image_picker/image_picker_for_web/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: image_picker_for_web
description: Web platform implementation of image_picker
repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_for_web
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
version: 2.2.0
version: 3.0.0

environment:
sdk: ">=2.18.0 <4.0.0"
Expand Down