Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 036fb33

Browse files
authored
[image_picker_for_web] Support multiple pick. Store name and other local file properties in XFile. (#4166)
1 parent 45a1ba6 commit 036fb33

File tree

5 files changed

+159
-44
lines changed

5 files changed

+159
-44
lines changed

packages/image_picker/image_picker_for_web/AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,4 @@ Aleksandr Yurkovskiy <[email protected]>
6464
Anton Borries <[email protected]>
6565
6666
Rahul Raj <[email protected]>
67+
Balvinder Singh Gambhir <[email protected]>

packages/image_picker/image_picker_for_web/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# 2.1.1
2+
3+
* Implemented `getMultiImage`.
4+
* Initialized the following `XFile` attributes for picked files:
5+
* `name`, `length`, `mimeType` and `lastModified`.
6+
17
# 2.1.0
28

39
* Implemented `getImage`, `getVideo` and `getFile` methods that return `XFile` instances.

packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,16 @@ import 'package:image_picker_for_web/image_picker_for_web.dart';
1111
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
1212
import 'package:integration_test/integration_test.dart';
1313

14-
final String expectedStringContents = "Hello, world!";
14+
final String expectedStringContents = 'Hello, world!';
15+
final String otherStringContents = 'Hello again, world!';
1516
final Uint8List bytes = utf8.encode(expectedStringContents) as Uint8List;
16-
final html.File textFile = html.File([bytes], "hello.txt");
17+
final Uint8List otherBytes = utf8.encode(otherStringContents) as Uint8List;
18+
final Map<String, dynamic> options = {
19+
'type': 'text/plain',
20+
'lastModified': DateTime.utc(2017, 12, 13).millisecondsSinceEpoch,
21+
};
22+
final html.File textFile = html.File([bytes], 'hello.txt', options);
23+
final html.File secondTextFile = html.File([otherBytes], 'secondFile.txt');
1724

1825
void main() {
1926
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -30,7 +37,7 @@ void main() {
3037

3138
final overrides = ImagePickerPluginTestOverrides()
3239
..createInputElement = ((_, __) => mockInput)
33-
..getFileFromInput = ((_) => textFile);
40+
..getMultipleFilesFromInput = ((_) => [textFile]);
3441

3542
final plugin = ImagePickerPlugin(overrides: overrides);
3643

@@ -51,20 +58,58 @@ void main() {
5158

5259
final overrides = ImagePickerPluginTestOverrides()
5360
..createInputElement = ((_, __) => mockInput)
54-
..getFileFromInput = ((_) => textFile);
61+
..getMultipleFilesFromInput = ((_) => [textFile]);
5562

5663
final plugin = ImagePickerPlugin(overrides: overrides);
5764

5865
// Init the pick file dialog...
59-
final file = plugin.getFile();
66+
final image = plugin.getImage(source: ImageSource.camera);
6067

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

6471
// Now the file should be available
65-
expect(file, completes);
72+
expect(image, completes);
73+
6674
// And readable
67-
expect((await file).readAsBytes(), completion(isNotEmpty));
75+
final XFile file = await image;
76+
expect(file.readAsBytes(), completion(isNotEmpty));
77+
expect(file.name, textFile.name);
78+
expect(file.length(), completion(textFile.size));
79+
expect(file.mimeType, textFile.type);
80+
expect(
81+
file.lastModified(),
82+
completion(
83+
DateTime.fromMillisecondsSinceEpoch(textFile.lastModified!),
84+
));
85+
});
86+
87+
testWidgets('Can select multiple files', (WidgetTester tester) async {
88+
final mockInput = html.FileUploadInputElement();
89+
90+
final overrides = ImagePickerPluginTestOverrides()
91+
..createInputElement = ((_, __) => mockInput)
92+
..getMultipleFilesFromInput = ((_) => [textFile, secondTextFile]);
93+
94+
final plugin = ImagePickerPlugin(overrides: overrides);
95+
96+
// Init the pick file dialog...
97+
final files = plugin.getMultiImage();
98+
99+
// Mock the browser behavior of selecting a file...
100+
mockInput.dispatchEvent(html.Event('change'));
101+
102+
// Now the file should be available
103+
expect(files, completes);
104+
105+
// And readable
106+
expect((await files).first.readAsBytes(), completion(isNotEmpty));
107+
108+
// Peek into the second file...
109+
final XFile secondFile = (await files).elementAt(1);
110+
expect(secondFile.readAsBytes(), completion(isNotEmpty));
111+
expect(secondFile.name, secondTextFile.name);
112+
expect(secondFile.length(), completion(secondTextFile.size));
68113
});
69114

70115
// There's no good way of detecting when the user has "aborted" the selection.
@@ -94,13 +139,35 @@ void main() {
94139

95140
expect(input.attributes, containsPair('accept', 'any'));
96141
expect(input.attributes, isNot(contains('capture')));
142+
expect(input.attributes, isNot(contains('multiple')));
97143
});
98144

99145
testWidgets('accept: any, capture: something', (WidgetTester tester) async {
100146
html.Element input = plugin.createInputElement('any', 'something');
101147

102148
expect(input.attributes, containsPair('accept', 'any'));
103149
expect(input.attributes, containsPair('capture', 'something'));
150+
expect(input.attributes, isNot(contains('multiple')));
151+
});
152+
153+
testWidgets('accept: any, capture: null, multi: true',
154+
(WidgetTester tester) async {
155+
html.Element input =
156+
plugin.createInputElement('any', null, multiple: true);
157+
158+
expect(input.attributes, containsPair('accept', 'any'));
159+
expect(input.attributes, isNot(contains('capture')));
160+
expect(input.attributes, contains('multiple'));
161+
});
162+
163+
testWidgets('accept: any, capture: something, multi: true',
164+
(WidgetTester tester) async {
165+
html.Element input =
166+
plugin.createInputElement('any', 'something', multiple: true);
167+
168+
expect(input.attributes, containsPair('accept', 'any'));
169+
expect(input.attributes, containsPair('capture', 'something'));
170+
expect(input.attributes, contains('multiple'));
104171
});
105172
});
106173
}

packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart

Lines changed: 77 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ final String _kAcceptVideoMimeType = 'video/3gpp,video/x-m4v,video/mp4,video/*';
1818
/// This class implements the `package:image_picker` functionality for the web.
1919
class ImagePickerPlugin extends ImagePickerPlatform {
2020
final ImagePickerPluginTestOverrides? _overrides;
21+
2122
bool get _hasOverrides => _overrides != null;
2223

2324
late html.Element _target;
@@ -115,9 +116,13 @@ class ImagePickerPlugin extends ImagePickerPlatform {
115116
double? maxHeight,
116117
int? imageQuality,
117118
CameraDevice preferredCameraDevice = CameraDevice.rear,
118-
}) {
119+
}) async {
119120
String? capture = computeCaptureAttribute(source, preferredCameraDevice);
120-
return getFile(accept: _kAcceptImageMimeType, capture: capture);
121+
List<XFile> files = await getFiles(
122+
accept: _kAcceptImageMimeType,
123+
capture: capture,
124+
);
125+
return files.first;
121126
}
122127

123128
/// Returns an [XFile] containing the video that was picked.
@@ -137,25 +142,48 @@ class ImagePickerPlugin extends ImagePickerPlatform {
137142
required ImageSource source,
138143
CameraDevice preferredCameraDevice = CameraDevice.rear,
139144
Duration? maxDuration,
140-
}) {
145+
}) async {
141146
String? capture = computeCaptureAttribute(source, preferredCameraDevice);
142-
return getFile(accept: _kAcceptVideoMimeType, capture: capture);
147+
List<XFile> files = await getFiles(
148+
accept: _kAcceptVideoMimeType,
149+
capture: capture,
150+
);
151+
return files.first;
152+
}
153+
154+
/// Injects a file input, and returns a list of XFile that the user selected locally.
155+
@override
156+
Future<List<XFile>> getMultiImage({
157+
double? maxWidth,
158+
double? maxHeight,
159+
int? imageQuality,
160+
}) {
161+
return getFiles(accept: _kAcceptImageMimeType, multiple: true);
143162
}
144163

145164
/// Injects a file input with the specified accept+capture attributes, and
146-
/// returns the PickedFile that the user selected locally.
165+
/// returns a list of XFile that the user selected locally.
147166
///
148167
/// `capture` is only supported in mobile browsers.
168+
///
169+
/// `multiple` can be passed to allow for multiple selection of files. Defaults
170+
/// to false.
171+
///
149172
/// See https://caniuse.com/#feat=html-media-capture
150173
@visibleForTesting
151-
Future<XFile> getFile({
174+
Future<List<XFile>> getFiles({
152175
String? accept,
153176
String? capture,
177+
bool multiple = false,
154178
}) {
155-
html.FileUploadInputElement input =
156-
createInputElement(accept, capture) as html.FileUploadInputElement;
179+
html.FileUploadInputElement input = createInputElement(
180+
accept,
181+
capture,
182+
multiple: multiple,
183+
) as html.FileUploadInputElement;
157184
_injectAndActivate(input);
158-
return _getSelectedXFile(input);
185+
186+
return _getSelectedXFiles(input);
159187
}
160188

161189
// DOM methods
@@ -171,34 +199,31 @@ class ImagePickerPlugin extends ImagePickerPlatform {
171199
return null;
172200
}
173201

174-
html.File? _getFileFromInput(html.FileUploadInputElement input) {
202+
List<html.File>? _getFilesFromInput(html.FileUploadInputElement input) {
175203
if (_hasOverrides) {
176-
return _overrides!.getFileFromInput(input);
204+
return _overrides!.getMultipleFilesFromInput(input);
177205
}
178-
return input.files?.first;
206+
return input.files;
179207
}
180208

181209
/// Handles the OnChange event from a FileUploadInputElement object
182-
/// Returns the objectURL of the selected file.
183-
String? _handleOnChangeEvent(html.Event event) {
210+
/// Returns a list of selected files.
211+
List<html.File>? _handleOnChangeEvent(html.Event event) {
184212
final html.FileUploadInputElement input =
185213
event.target as html.FileUploadInputElement;
186-
final html.File? file = _getFileFromInput(input);
187-
188-
if (file != null) {
189-
return html.Url.createObjectUrl(file);
190-
}
191-
return null;
214+
return _getFilesFromInput(input);
192215
}
193216

194217
/// Monitors an <input type="file"> and returns the selected file.
195218
Future<PickedFile> _getSelectedFile(html.FileUploadInputElement input) {
196219
final Completer<PickedFile> _completer = Completer<PickedFile>();
197220
// Observe the input until we can return something
198221
input.onChange.first.then((event) {
199-
final objectUrl = _handleOnChangeEvent(event);
200-
if (!_completer.isCompleted && objectUrl != null) {
201-
_completer.complete(PickedFile(objectUrl));
222+
final files = _handleOnChangeEvent(event);
223+
if (!_completer.isCompleted && files != null) {
224+
_completer.complete(PickedFile(
225+
html.Url.createObjectUrl(files.first),
226+
));
202227
}
203228
});
204229
input.onError.first.then((event) {
@@ -212,13 +237,24 @@ class ImagePickerPlugin extends ImagePickerPlatform {
212237
return _completer.future;
213238
}
214239

215-
Future<XFile> _getSelectedXFile(html.FileUploadInputElement input) {
216-
final Completer<XFile> _completer = Completer<XFile>();
240+
/// Monitors an <input type="file"> and returns the selected file(s).
241+
Future<List<XFile>> _getSelectedXFiles(html.FileUploadInputElement input) {
242+
final Completer<List<XFile>> _completer = Completer<List<XFile>>();
217243
// Observe the input until we can return something
218244
input.onChange.first.then((event) {
219-
final objectUrl = _handleOnChangeEvent(event);
220-
if (!_completer.isCompleted && objectUrl != null) {
221-
_completer.complete(XFile(objectUrl));
245+
final files = _handleOnChangeEvent(event);
246+
if (!_completer.isCompleted && files != null) {
247+
_completer.complete(files
248+
.map((file) => XFile(
249+
html.Url.createObjectUrl(file),
250+
name: file.name,
251+
length: file.size,
252+
lastModified: DateTime.fromMillisecondsSinceEpoch(
253+
file.lastModified ?? DateTime.now().millisecondsSinceEpoch,
254+
),
255+
mimeType: file.type,
256+
))
257+
.toList());
222258
}
223259
});
224260
input.onError.first.then((event) {
@@ -248,12 +284,18 @@ class ImagePickerPlugin extends ImagePickerPlatform {
248284
/// Creates an input element that accepts certain file types, and
249285
/// allows to `capture` from the device's cameras (where supported)
250286
@visibleForTesting
251-
html.Element createInputElement(String? accept, String? capture) {
287+
html.Element createInputElement(
288+
String? accept,
289+
String? capture, {
290+
bool multiple = false,
291+
}) {
252292
if (_hasOverrides) {
253293
return _overrides!.createInputElement(accept, capture);
254294
}
255295

256-
html.Element element = html.FileUploadInputElement()..accept = accept;
296+
html.Element element = html.FileUploadInputElement()
297+
..accept = accept
298+
..multiple = multiple;
257299

258300
if (capture != null) {
259301
element.setAttribute('capture', capture);
@@ -278,18 +320,17 @@ typedef OverrideCreateInputFunction = html.Element Function(
278320
String? capture,
279321
);
280322

281-
/// A function that extracts a [html.File] from the file `input` passed in.
323+
/// A function that extracts list of files from the file `input` passed in.
282324
@visibleForTesting
283-
typedef OverrideExtractFilesFromInputFunction = html.File Function(
284-
html.Element? input,
285-
);
325+
typedef OverrideExtractMultipleFilesFromInputFunction = List<html.File>
326+
Function(html.Element? input);
286327

287328
/// Overrides for some of the functionality above.
288329
@visibleForTesting
289330
class ImagePickerPluginTestOverrides {
290331
/// Override the creation of the input element.
291332
late OverrideCreateInputFunction createInputElement;
292333

293-
/// Override the extraction of the selected file from an input element.
294-
late OverrideExtractFilesFromInputFunction getFileFromInput;
334+
/// Override the extraction of the selected files from an input element.
335+
late OverrideExtractMultipleFilesFromInputFunction getMultipleFilesFromInput;
295336
}

packages/image_picker/image_picker_for_web/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: image_picker_for_web
22
description: Web platform implementation of image_picker
33
repository: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker_for_web
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
5-
version: 2.1.0
5+
version: 2.1.1
66

77
environment:
88
sdk: ">=2.12.0 <3.0.0"

0 commit comments

Comments
 (0)