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

Commit 4ebe203

Browse files
committed
Add file_selector_web
1 parent 05879a3 commit 4ebe203

File tree

14 files changed

+415
-9
lines changed

14 files changed

+415
-9
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# 0.7.0
2+
3+
- Initial open-source release.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
Copyright 2020 The Flutter Authors. All rights reserved.
2+
3+
Redistribution and use in source and binary forms, with or without modification,
4+
are permitted provided that the following conditions are met:
5+
6+
* Redistributions of source code must retain the above copyright
7+
notice, this list of conditions and the following disclaimer.
8+
* Redistributions in binary form must reproduce the above
9+
copyright notice, this list of conditions and the following
10+
disclaimer in the documentation and/or other materials provided
11+
with the distribution.
12+
* Neither the name of Google Inc. nor the names of its
13+
contributors may be used to endorse or promote products derived
14+
from this software without specific prior written permission.
15+
16+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
20+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
23+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# file_picker_web
2+
3+
The web implementation of [`file_picker`][1].
4+
5+
**Please set your constraint to `file_picker_web: '>=0.1.y+x <2.0.0'`**
6+
7+
## Backward compatible 1.0.0 version is coming
8+
The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.1.y+z`.
9+
Please use `file_picker_web: '>=0.1.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration.
10+
For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0
11+
12+
## Usage
13+
14+
### Import the package
15+
To use this plugin in your Flutter Web app, simply add it as a dependency in
16+
your pubspec alongside the base `file_picker` plugin.
17+
18+
_(This is only temporary: in the future we hope to make this package an
19+
"endorsed" implementation of `file_picker`, so that it is automatically
20+
included in your Flutter Web app when you depend on `package:file_picker`.)_
21+
22+
This is what the above means to your `pubspec.yaml`:
23+
24+
```yaml
25+
...
26+
dependencies:
27+
...
28+
file_picker: ^0.1.0
29+
file_picker_web: ^0.1.0
30+
...
31+
```
32+
33+
### Use the plugin
34+
Once you have the `file_picker_web` dependency in your pubspec, you should
35+
be able to use `package:file_picker` as normal.
36+
37+
[1]: ../file_picker/file_picker
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import 'dart:async';
2+
import 'package:flutter/services.dart';
3+
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
4+
import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
5+
import 'dart:html';
6+
7+
/// The web implementation of [FileSelectorPlatform].
8+
///
9+
/// This class implements the `package:file_selector` functionality for the web.
10+
class FileSelectorWeb extends FileSelectorPlatform {
11+
final _input = FileUploadInputElement();
12+
13+
/// Registers this class as the default instance of [FileSelectorPlatform].
14+
static void registerWith(Registrar registrar) {
15+
FileSelectorPlatform.instance = FileSelectorWeb();
16+
}
17+
18+
/// Default constructor, initializes _container to a DOM element that we can use
19+
/// to host HTML elements.
20+
/// overrides parameter allows for testing to override functions
21+
FileSelectorWeb() {
22+
final body = querySelector('body');
23+
final container =
24+
querySelector('file-selector') ?? Element.tag('file-selector');
25+
body.children.add(container);
26+
container.children.add(_input);
27+
}
28+
29+
/// Open file dialog for loading files and return a XFile
30+
@override
31+
Future<XFile> openFile({
32+
List<XTypeGroup> acceptedTypeGroups,
33+
String initialDirectory,
34+
String confirmButtonText,
35+
}) async {
36+
final files = await _openFiles(acceptedTypeGroups: acceptedTypeGroups);
37+
return files.first;
38+
}
39+
40+
/// Open file dialog for loading files and return a XFile
41+
@override
42+
Future<List<XFile>> openFiles({
43+
List<XTypeGroup> acceptedTypeGroups,
44+
String initialDirectory,
45+
String confirmButtonText,
46+
}) async {
47+
return _openFiles(acceptedTypeGroups: acceptedTypeGroups, multiple: true);
48+
}
49+
50+
@override
51+
Future<String> getSavePath({
52+
List<XTypeGroup> acceptedTypeGroups,
53+
String initialDirectory,
54+
String suggestedName,
55+
String confirmButtonText,
56+
}) async =>
57+
null;
58+
59+
@override
60+
Future<String> getDirectoryPath({
61+
String initialDirectory,
62+
String confirmButtonText,
63+
}) async =>
64+
null;
65+
66+
Future<List<XFile>> _openFiles({
67+
List<XTypeGroup> acceptedTypeGroups,
68+
bool multiple = false,
69+
}) async {
70+
final accept = _acceptedTypesToString(acceptedTypeGroups);
71+
_input.accept = accept;
72+
_input.multiple = multiple;
73+
return _getFiles(_input);
74+
}
75+
76+
/// For a given input, returns the files selected by an user.
77+
static Future<List<XFile>> _getFiles(FileUploadInputElement input) {
78+
final Completer<List<XFile>> _completer = Completer();
79+
80+
input.onChange.first.then((_) {
81+
final List<XFile> files = input.files.map(_convertFileToXFile).toList();
82+
_completer.complete(files);
83+
});
84+
85+
input.onError.first.then((event) {
86+
final ErrorEvent error = event;
87+
final platformException = PlatformException(
88+
code: error.type,
89+
message: error.message,
90+
);
91+
_completer.completeError(platformException);
92+
});
93+
94+
input.click();
95+
96+
return _completer.future;
97+
}
98+
99+
/// Helper to convert an html.File to an XFile
100+
static XFile _convertFileToXFile(File file) => XFile(
101+
Url.createObjectUrl(file),
102+
name: file.name,
103+
length: file.size,
104+
lastModified: DateTime.fromMillisecondsSinceEpoch(file.lastModified),
105+
);
106+
107+
/// Convert list of XTypeGroups to a comma-separated string
108+
static String _acceptedTypesToString(List<XTypeGroup> acceptedTypes) {
109+
if (acceptedTypes == null) return '';
110+
final List<String> allTypes = [];
111+
for (final group in acceptedTypes) {
112+
_assertTypeGroupIsValid(group);
113+
if (group.extensions != null) {
114+
allTypes.addAll(group.extensions.map(_normalizeExtension));
115+
}
116+
if (group.mimeTypes != null) {
117+
allTypes.addAll(group.mimeTypes);
118+
}
119+
if (group.webWildCards != null) {
120+
allTypes.addAll(group.webWildCards);
121+
}
122+
}
123+
return allTypes.join(',');
124+
}
125+
126+
/// Make sure that at least one of its fields is populated.
127+
static void _assertTypeGroupIsValid(XTypeGroup group) {
128+
assert(
129+
!((group.extensions == null || group.extensions.isEmpty) &&
130+
(group.mimeTypes == null || group.mimeTypes.isEmpty) &&
131+
(group.webWildCards == null || group.webWildCards.isEmpty)),
132+
'At least one of extensions / mimeTypes / webWildCards is required for web.');
133+
}
134+
135+
/// Append a dot at the beggining if it is not there png -> .png
136+
static String _normalizeExtension(String ext) {
137+
return ext.isNotEmpty && ext[0] != '.' ? '.' + ext : ext;
138+
}
139+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: file_selector_web
2+
description: Web platform implementation of file_selector
3+
homepage: https://github.com/flutter/plugins/tree/master/packages/file_selector/file_selector_web
4+
version: 0.7.0
5+
6+
flutter:
7+
plugin:
8+
platforms:
9+
web:
10+
pluginClass: FileSelectorWeb
11+
fileName: file_selector_web.dart
12+
13+
dependencies:
14+
file_selector_platform_interface: ^1.0.0
15+
platform_detect: ^1.4.0
16+
flutter:
17+
sdk: flutter
18+
flutter_web_plugins:
19+
sdk: flutter
20+
meta: ^1.1.7
21+
22+
dev_dependencies:
23+
flutter_test:
24+
sdk: flutter
25+
pedantic: ^1.8.0
26+
integration_test:
27+
path: ../../integration_test
28+
29+
environment:
30+
sdk: ">=2.2.0 <3.0.0"
31+
flutter: ">=1.10.0 <2.0.0"
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
7+
void main() {
8+
runApp(MyApp());
9+
}
10+
11+
/// App for testing
12+
class MyApp extends StatefulWidget {
13+
@override
14+
_MyAppState createState() => _MyAppState();
15+
}
16+
17+
class _MyAppState extends State<MyApp> {
18+
@override
19+
Widget build(BuildContext context) {
20+
return Text('Testing... Look at the console output for results!');
21+
}
22+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: file_selector_web_tests
2+
publish_to: none
3+
4+
environment:
5+
sdk: ">=2.2.2 <3.0.0"
6+
7+
dependencies:
8+
flutter:
9+
sdk: flutter
10+
flutter_driver:
11+
sdk: flutter
12+
flutter_test:
13+
sdk: flutter
14+
mockito: ^4.1.1
15+
integration_test:
16+
path: ../../../integration_test
17+
file_selector_web:
18+
path: ../
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/usr/bin/bash
2+
if pgrep -lf chromedriver > /dev/null; then
3+
echo "chromedriver is running."
4+
5+
if [ $# -eq 0 ]; then
6+
echo "No target specified, running all tests..."
7+
find test_driver/ -iname *_integration.dart | xargs -n1 -i -t flutter drive -d web-server --web-port=7357 --browser-name=chrome --target='{}'
8+
else
9+
echo "Running test target: $1..."
10+
set -x
11+
flutter drive -d web-server --web-port=7357 --browser-name=chrome --target=$1
12+
fi
13+
14+
else
15+
echo "chromedriver is not running."
16+
fi
17+
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright 2020 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// @dart = 2.9
6+
7+
import 'dart:html';
8+
import 'package:flutter_test/flutter_test.dart';
9+
import 'package:integration_test/integration_test.dart';
10+
import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
11+
import 'package:file_selector_web/file_selector_web.dart';
12+
13+
FileList FileListItems(List<File> files) {
14+
final dt = DataTransfer();
15+
files.forEach((file) => dt.items.add(file));
16+
return dt.files;
17+
}
18+
19+
void main() {
20+
group('FileSelectorWeb', () {
21+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
22+
FileSelectorWeb plugin;
23+
FileUploadInputElement input;
24+
25+
setUp(() {
26+
plugin = FileSelectorWeb();
27+
input = querySelector('file-selector') as FileUploadInputElement;
28+
assert(input != null);
29+
});
30+
31+
group('openFile', () {
32+
final mockFile = File(['random content'], 'image.png');
33+
34+
setUp(() {
35+
input.files = FileListItems([mockFile]);
36+
});
37+
38+
testWidgets('works', (WidgetTester _) async {
39+
final typeGroup = XTypeGroup(
40+
label: 'images',
41+
extensions: ['jpg', 'jpeg'],
42+
mimeTypes: ['image/png'],
43+
webWildCards: ['image/*'],
44+
);
45+
46+
final futureFile = plugin.openFile(acceptedTypeGroups: [typeGroup]);
47+
48+
//simulate a user has chosen a file
49+
input.dispatchEvent(Event('change'));
50+
51+
final file = await futureFile;
52+
53+
expect(input.accept, '.jpg,.jpeg,image/png,image/*');
54+
expect(input.multiple, false);
55+
expect(file.name, mockFile.name);
56+
expect(await file.length(), 14);
57+
expect(await file.readAsString(), 'random content');
58+
expect(await file.lastModified(), isNotNull);
59+
});
60+
});
61+
62+
group('openFiles', () {
63+
final mockFile = File(['123456'], 'log.txt');
64+
65+
setUp(() {
66+
input.files = FileListItems([mockFile]);
67+
});
68+
69+
testWidgets('works', (WidgetTester _) async {
70+
final txts = XTypeGroup(
71+
label: 'txt',
72+
mimeTypes: ['file/txt'],
73+
);
74+
75+
final jsons = XTypeGroup(
76+
label: 'JSON',
77+
extensions: ['json'],
78+
);
79+
80+
final futureFiles = plugin.openFiles(acceptedTypeGroups: [txts, jsons]);
81+
82+
//simulate a user has chosen a file
83+
input.dispatchEvent(Event('change'));
84+
85+
final files = await futureFiles;
86+
87+
expect(input.accept, 'file/txt,.json');
88+
expect(input.multiple, true);
89+
expect(files.length, 1);
90+
expect(files[0].name, 'log.txt');
91+
expect(await files[0].length(), 6);
92+
expect(await files[0].readAsString(), '123456');
93+
expect(await files[0].lastModified(), isNotNull);
94+
});
95+
});
96+
});
97+
}

0 commit comments

Comments
 (0)