Skip to content
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