Skip to content

[cross_file] Migrate to pkg:web, bump min SDK to Dart 3.2 #5520

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Nov 30, 2023
5 changes: 5 additions & 0 deletions packages/cross_file/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.3.3+8

* Now supports `dart2wasm` compilation.
* Updates minimum supported SDK version to Flutter 3.16/Dart 3.2.

## 0.3.3+7

* Updates README to improve example of instantiating an XFile.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'package:cross_file/cross_file.dart';
import 'package:cross_file_example/readme_excerpts.dart';
import 'package:test/test.dart';

const bool kIsWeb = bool.fromEnvironment('dart.library.js_util');
const bool kIsWeb = bool.fromEnvironment('dart.library.js_interop');

void main() {
test('instantiateXFile loads asset file', () async {
Expand Down
27 changes: 18 additions & 9 deletions packages/cross_file/lib/src/types/html.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@

import 'dart:async';
import 'dart:convert';
import 'dart:html';
import 'dart:js_interop';
import 'dart:typed_data';

import 'package:meta/meta.dart';
import 'package:web/helpers.dart';

import '../web_helpers/web_helpers.dart';
import 'base.dart';
Expand Down Expand Up @@ -65,7 +66,9 @@ class XFile extends XFileBase {
super(path) {
if (path == null) {
_browserBlob = _createBlobFromBytes(bytes, mimeType);
_path = Url.createObjectUrl(_browserBlob);
// TODO(kevmoo): drop ignore when pkg:web constraint excludes v0.3
// ignore: unnecessary_cast
_path = URL.createObjectURL(_browserBlob! as JSObject);
} else {
_path = path;
}
Expand All @@ -74,8 +77,9 @@ class XFile extends XFileBase {
// Initializes a Blob from a bunch of `bytes` and an optional `mimeType`.
Blob _createBlobFromBytes(Uint8List bytes, String? mimeType) {
return (mimeType == null)
? Blob(<dynamic>[bytes])
: Blob(<dynamic>[bytes], mimeType);
? Blob(<JSUint8Array>[bytes.toJS].toJS)
: Blob(
<JSUint8Array>[bytes.toJS].toJS, BlobPropertyBag(type: mimeType));
}

// Overridable (meta) data that can be specified by the constructors.
Expand Down Expand Up @@ -127,11 +131,13 @@ class XFile extends XFileBase {

// Attempt to re-hydrate the blob from the `path` via a (local) HttpRequest.
// Note that safari hangs if the Blob is >=4GB, so bail out in that case.
// TODO(kevmoo): Remove ignore and fix when the MIN Dart SDK is 3.3
// ignore: unnecessary_non_null_assertion
if (isSafari() && _length != null && _length! >= _fourGigabytes) {
throw Exception('Safari cannot handle XFiles larger than 4GB.');
}

late HttpRequest request;
late XMLHttpRequest request;
try {
request = await HttpRequest.request(path, responseType: 'blob');
} on ProgressEvent catch (e) {
Expand Down Expand Up @@ -181,7 +187,8 @@ class XFile extends XFileBase {

await reader.onLoadEnd.first;

final Uint8List? result = reader.result as Uint8List?;
final Uint8List? result =
(reader.result as JSArrayBuffer?)?.toDart.asUint8List();

if (result == null) {
throw Exception('Cannot read bytes from Blob. Is it still available?');
Expand All @@ -201,12 +208,14 @@ class XFile extends XFileBase {

// Create an <a> tag with the appropriate download attributes and click it
// May be overridden with CrossFileTestOverrides
final AnchorElement element = _hasTestOverrides
? _overrides!.createAnchorElement(this.path, name) as AnchorElement
final HTMLAnchorElement element = _hasTestOverrides
? _overrides!.createAnchorElement(this.path, name) as HTMLAnchorElement
: createAnchorElement(this.path, name);

// Clear the children in _target and add an element to click
_target.children.clear();
while (_target.children.length > 0) {
_target.removeChild(_target.children.item(0)!);
}
addElementToContainerAndClick(_target, element);
}
}
Expand Down
4 changes: 4 additions & 0 deletions packages/cross_file/lib/src/types/io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ class XFile extends XFileBase {
await _file.copy(path);
} else {
final File fileToSave = File(path);
// TODO(kevmoo): Remove ignore and fix when the MIN Dart SDK is 3.3
// ignore: unnecessary_non_null_assertion
await fileToSave.writeAsBytes(_bytes!);
}
}
Expand All @@ -106,6 +108,8 @@ class XFile extends XFileBase {
@override
Future<String> readAsString({Encoding encoding = utf8}) {
if (_bytes != null) {
// TODO(kevmoo): Remove ignore and fix when the MIN Dart SDK is 3.3
// ignore: unnecessary_non_null_assertion
return Future<String>.value(String.fromCharCodes(_bytes!));
}
return _file.readAsString(encoding: encoding);
Expand Down
25 changes: 9 additions & 16 deletions packages/cross_file/lib/src/web_helpers/web_helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,29 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:html';
import 'package:web/helpers.dart';

/// Create anchor element with download attribute
AnchorElement createAnchorElement(String href, String? suggestedName) {
final AnchorElement element = AnchorElement(href: href);

if (suggestedName == null) {
element.download = 'download';
} else {
element.download = suggestedName;
}

return element;
}
HTMLAnchorElement createAnchorElement(String href, String? suggestedName) =>
(document.createElement('a') as HTMLAnchorElement)
..href = href
..download = suggestedName ?? 'download';

/// Add an element to a container and click it
void addElementToContainerAndClick(Element container, Element element) {
void addElementToContainerAndClick(Element container, HTMLElement element) {
// Add the element and click it
// All previous elements will be removed before adding the new one
container.children.add(element);
container.appendChild(element);
element.click();
}

/// Initializes a DOM container where elements can be injected.
Element ensureInitialized(String id) {
Element? target = querySelector('#$id');
if (target == null) {
final Element targetElement = Element.tag('flt-x-file')..id = id;
final Element targetElement = document.createElement('flt-x-file')..id = id;

querySelector('body')!.children.add(targetElement);
querySelector('body')!.appendChild(targetElement);
target = targetElement;
}
return target;
Expand Down
2 changes: 1 addition & 1 deletion packages/cross_file/lib/src/x_file.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
// found in the LICENSE file.

export 'types/interface.dart'
if (dart.library.html) 'types/html.dart'
if (dart.library.js_interop) 'types/html.dart'
if (dart.library.io) 'types/io.dart';
6 changes: 3 additions & 3 deletions packages/cross_file/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ name: cross_file
description: An abstraction to allow working with files across multiple platforms.
repository: https://github.com/flutter/packages/tree/main/packages/cross_file
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+cross_file%22
version: 0.3.3+7
version: 0.3.3+8

environment:
sdk: ">=3.0.0 <4.0.0"
sdk: ^3.2.0

dependencies:
js: ^0.6.3
meta: ^1.3.0
web: '>=0.3.0 <0.5.0'

dev_dependencies:
path: ^1.8.1
Expand Down
39 changes: 25 additions & 14 deletions packages/cross_file/test/x_file_html_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,21 @@
@TestOn('chrome') // Uses web-only Flutter SDK

import 'dart:convert';
import 'dart:html' as html;
import 'dart:js_interop';
import 'dart:typed_data';

import 'package:cross_file/cross_file.dart';
import 'package:js/js_util.dart' as js_util;
import 'package:test/test.dart';
import 'package:web/helpers.dart' as html;

const String expectedStringContents = 'Hello, world! I ❤ ñ! 空手';
final Uint8List bytes = Uint8List.fromList(utf8.encode(expectedStringContents));
final html.File textFile = html.File(<Object>[bytes], 'hello.txt');
final String textFileUrl = html.Url.createObjectUrl(textFile);
final html.File textFile =
html.File(<JSUint8Array>[bytes.toJS].toJS, 'hello.txt');
final String textFileUrl =
// TODO(kevmoo): drop ignore when pkg:web constraint excludes v0.3
// ignore: unnecessary_cast
html.URL.createObjectURL(textFile as JSObject);

void main() {
group('Create with an objectUrl', () {
Expand Down Expand Up @@ -63,16 +67,16 @@ void main() {

test('Stores data as a Blob', () async {
// Read the blob from its path 'natively'
final Object response = await html.window.fetch(file.path) as Object;
// Call '.arrayBuffer()' on the fetch response object to look at its bytes.
final ByteBuffer data = await js_util.promiseToFuture(
js_util.callMethod(response, 'arrayBuffer', <Object?>[]),
);
final html.Response response =
(await html.window.fetch(file.path.toJS).toDart)! as html.Response;

final JSAny? arrayBuffer = await response.arrayBuffer().toDart;
final ByteBuffer data = (arrayBuffer! as JSArrayBuffer).toDart;
expect(data.asUint8List(), equals(bytes));
});

test('Data may be purged from the blob!', () async {
html.Url.revokeObjectUrl(file.path);
html.URL.revokeObjectURL(file.path);

expect(() async {
await file.readAsBytes();
Expand Down Expand Up @@ -102,17 +106,24 @@ void main() {

final html.Element container =
html.querySelector('#$crossFileDomElementId')!;
final html.AnchorElement element = container.children
.firstWhere((html.Element element) => element.tagName == 'A')
as html.AnchorElement;

late html.HTMLAnchorElement element;
for (int i = 0; i < container.childNodes.length; i++) {
final html.Element test = container.children.item(i)!;
if (test.tagName == 'A') {
element = test as html.HTMLAnchorElement;
break;
}
}

// if element is not found, the `firstWhere` call will throw StateError.
expect(element.href, file.path);
expect(element.download, file.name);
});

test('anchor element is clicked', () async {
final html.AnchorElement mockAnchor = html.AnchorElement();
final html.HTMLAnchorElement mockAnchor =
html.document.createElement('a') as html.HTMLAnchorElement;

final CrossFileTestOverrides overrides = CrossFileTestOverrides(
createAnchorElement: (_, __) => mockAnchor,
Expand Down