From 85549cbcd02fcd12ca51af867d9fbe7c122b9420 Mon Sep 17 00:00:00 2001 From: Raju M <24muliyashiya@gmail.com> Date: Tue, 11 Nov 2025 16:56:01 +0530 Subject: [PATCH 1/5] [cross_file] [web] Separate "Save As" implementation details from XFile web class - fixes #91400 --- packages/cross_file/CHANGELOG.md | 4 ++ packages/cross_file/lib/src/types/html.dart | 49 ++----------------- .../lib/src/web_helpers/web_helpers.dart | 28 +++++++++++ packages/cross_file/pubspec.yaml | 2 +- .../cross_file/test/x_file_html_test.dart | 11 ++--- 5 files changed, 41 insertions(+), 53 deletions(-) diff --git a/packages/cross_file/CHANGELOG.md b/packages/cross_file/CHANGELOG.md index 7f37f7bd53df..c54c7beaaa66 100644 --- a/packages/cross_file/CHANGELOG.md +++ b/packages/cross_file/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.6 + +* Separate "Save As" implementation details from XFile web class. + ## 0.3.5+1 * Replaces README mentions of `dart:html` with `package:web`. diff --git a/packages/cross_file/lib/src/types/html.dart b/packages/cross_file/lib/src/types/html.dart index 9c85e1511b85..86ab6be52c6b 100644 --- a/packages/cross_file/lib/src/types/html.dart +++ b/packages/cross_file/lib/src/types/html.dart @@ -7,7 +7,6 @@ import 'dart:convert'; import 'dart:js_interop'; import 'dart:typed_data'; -import 'package:meta/meta.dart'; import 'package:web/web.dart'; import '../web_helpers/web_helpers.dart'; @@ -35,11 +34,9 @@ class XFile extends XFileBase { int? length, Uint8List? bytes, DateTime? lastModified, - @visibleForTesting CrossFileTestOverrides? overrides, }) : _mimeType = mimeType, _path = path, _length = length, - _overrides = overrides, _lastModified = lastModified ?? DateTime.fromMillisecondsSinceEpoch(0), _name = name ?? '', super(path) { @@ -57,10 +54,8 @@ class XFile extends XFileBase { int? length, DateTime? lastModified, String? path, - @visibleForTesting CrossFileTestOverrides? overrides, }) : _mimeType = mimeType, _length = length, - _overrides = overrides, _lastModified = lastModified ?? DateTime.fromMillisecondsSinceEpoch(0), _name = name ?? '', super(path) { @@ -82,12 +77,16 @@ class XFile extends XFileBase { // MimeType of the file (eg: "image/gif"). final String? _mimeType; + // Name (with extension) of the file (eg: "anim.gif") final String _name; + // Path of the file (must be a valid Blob URL, when set manually!) late String _path; + // The size of the file (in bytes). final int? _length; + // The time the file was last modified. final DateTime _lastModified; @@ -97,17 +96,6 @@ class XFile extends XFileBase { // (Similar to a (read-only) dart:io File.) Blob? _browserBlob; - // An html Element that will be used to trigger a "save as" dialog later. - // TODO(dit): https://github.com/flutter/flutter/issues/91400 Remove this _target. - late Element _target; - - // Overrides for testing - // TODO(dit): https://github.com/flutter/flutter/issues/91400 Remove these _overrides, - // they're only used to Save As... - final CrossFileTestOverrides? _overrides; - - bool get _hasTestOverrides => _overrides != null; - @override String? get mimeType => _mimeType; @@ -200,35 +188,8 @@ class XFile extends XFileBase { /// Saves the data of this CrossFile at the location indicated by path. /// For the web implementation, the path variable is ignored. - // TODO(dit): https://github.com/flutter/flutter/issues/91400 - // Move implementation to web_helpers.dart @override Future saveTo(String path) async { - // Create a DOM container where the anchor can be injected. - _target = ensureInitialized('__x_file_dom_element'); - - // Create an tag with the appropriate download attributes and click it - // May be overridden with CrossFileTestOverrides - 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 - while (_target.children.length > 0) { - _target.removeChild(_target.children.item(0)!); - } - addElementToContainerAndClick(_target, element); + await saveFileAs(this, path); } } - -/// Overrides some functions to allow testing -// TODO(dit): https://github.com/flutter/flutter/issues/91400 -// Move this to web_helpers_test.dart -@visibleForTesting -class CrossFileTestOverrides { - /// Default constructor for overrides - CrossFileTestOverrides({required this.createAnchorElement}); - - /// For overriding the creation of the file input element. - Element Function(String href, String suggestedName) createAnchorElement; -} diff --git a/packages/cross_file/lib/src/web_helpers/web_helpers.dart b/packages/cross_file/lib/src/web_helpers/web_helpers.dart index daac03159509..1ee3594869dc 100644 --- a/packages/cross_file/lib/src/web_helpers/web_helpers.dart +++ b/packages/cross_file/lib/src/web_helpers/web_helpers.dart @@ -3,6 +3,14 @@ // found in the LICENSE file. import 'package:web/web.dart'; +import '../types/html.dart'; + +/// Type definition for function that creates anchor elements +typedef CreateAnchorElement = + HTMLAnchorElement Function(String href, String? suggestedName); + +/// Override for creating anchor elements for testing purposes +CreateAnchorElement? anchorElementOverride; /// Create anchor element with download attribute HTMLAnchorElement createAnchorElement(String href, String? suggestedName) => @@ -35,3 +43,23 @@ Element ensureInitialized(String id) { bool isSafari() { return window.navigator.vendor == 'Apple Computer, Inc.'; } + +/// Saves the given [XFile] to user's device ("Save As" dialog). +Future saveFileAs(XFile file, String path) async { + // Create container element. + final Element target = ensureInitialized('__x_file_dom_element'); + + // Create element. + final HTMLAnchorElement element = + anchorElementOverride != null + ? anchorElementOverride!(file.path, file.name) + : createAnchorElement(file.path, file.name); + + // Clear existing children before appending new one. + while (target.children.length > 0) { + target.removeChild(target.children.item(0)!); + } + + // Add and click. + addElementToContainerAndClick(target, element); +} diff --git a/packages/cross_file/pubspec.yaml b/packages/cross_file/pubspec.yaml index 532f392faef8..af3993fb7518 100644 --- a/packages/cross_file/pubspec.yaml +++ b/packages/cross_file/pubspec.yaml @@ -2,7 +2,7 @@ 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.5+1 +version: 0.3.6 environment: sdk: ^3.8.0 diff --git a/packages/cross_file/test/x_file_html_test.dart b/packages/cross_file/test/x_file_html_test.dart index 75bbcebc6aa4..72f0fbf1d787 100644 --- a/packages/cross_file/test/x_file_html_test.dart +++ b/packages/cross_file/test/x_file_html_test.dart @@ -10,6 +10,7 @@ import 'dart:js_interop'; import 'dart:typed_data'; import 'package:cross_file/cross_file.dart'; +import 'package:cross_file/src/web_helpers/web_helpers.dart' as helpers; import 'package:test/test.dart'; import 'package:web/web.dart' as html; @@ -149,15 +150,9 @@ void main() { final mockAnchor = html.document.createElement('a') as html.HTMLAnchorElement; - final overrides = CrossFileTestOverrides( - createAnchorElement: (_, __) => mockAnchor, - ); + helpers.anchorElementOverride = (_, __) => mockAnchor; - final file = XFile.fromData( - bytes, - name: textFile.name, - overrides: overrides, - ); + final file = XFile.fromData(bytes, name: textFile.name); var clicked = false; mockAnchor.onClick.listen((html.MouseEvent event) => clicked = true); From 664990fa9dbe985d3ee953262c479d3891f678d5 Mon Sep 17 00:00:00 2001 From: Raju M <24muliyashiya@gmail.com> Date: Thu, 4 Dec 2025 15:58:11 +0530 Subject: [PATCH 2/5] Code format and address gemini comment --- packages/cross_file/CHANGELOG.md | 2 +- packages/cross_file/lib/src/types/html.dart | 49 ++++++++++--------- .../cross_file/test/x_file_html_test.dart | 17 +++---- 3 files changed, 35 insertions(+), 33 deletions(-) diff --git a/packages/cross_file/CHANGELOG.md b/packages/cross_file/CHANGELOG.md index c54c7beaaa66..ee893bb023d4 100644 --- a/packages/cross_file/CHANGELOG.md +++ b/packages/cross_file/CHANGELOG.md @@ -1,6 +1,6 @@ ## 0.3.6 -* Separate "Save As" implementation details from XFile web class. +* Separates "Save As" implementation details from XFile web class. ## 0.3.5+1 diff --git a/packages/cross_file/lib/src/types/html.dart b/packages/cross_file/lib/src/types/html.dart index 86ab6be52c6b..ec2df43c7b01 100644 --- a/packages/cross_file/lib/src/types/html.dart +++ b/packages/cross_file/lib/src/types/html.dart @@ -68,9 +68,9 @@ class XFile extends XFileBase { return (mimeType == null) ? Blob([bytes.toJS].toJS) : Blob( - [bytes.toJS].toJS, - BlobPropertyBag(type: mimeType), - ); + [bytes.toJS].toJS, + BlobPropertyBag(type: mimeType), + ); } // Overridable (meta) data that can be specified by the constructors. @@ -122,24 +122,27 @@ class XFile extends XFileBase { final blobCompleter = Completer(); late XMLHttpRequest request; - request = XMLHttpRequest() - ..open('get', path, true) - ..responseType = 'blob' - ..onLoad.listen((ProgressEvent e) { - assert( - request.response != null, - 'The Blob backing this XFile cannot be null!', - ); - blobCompleter.complete(request.response! as Blob); - }) - ..onError.listen((ProgressEvent e) { - if (e.type == 'error') { - blobCompleter.completeError( - Exception('Could not load Blob from its URL. Has it been revoked?'), - ); - } - }) - ..send(); + request = + XMLHttpRequest() + ..open('get', path, true) + ..responseType = 'blob' + ..onLoad.listen((ProgressEvent e) { + assert( + request.response != null, + 'The Blob backing this XFile cannot be null!', + ); + blobCompleter.complete(request.response! as Blob); + }) + ..onError.listen((ProgressEvent e) { + if (e.type == 'error') { + blobCompleter.completeError( + Exception( + 'Could not load Blob from its URL. Has it been revoked?', + ), + ); + } + }) + ..send(); return blobCompleter.future; } @@ -176,8 +179,8 @@ class XFile extends XFileBase { await reader.onLoadEnd.first; - final Uint8List? result = (reader.result as JSArrayBuffer?)?.toDart - .asUint8List(); + final Uint8List? result = + (reader.result as JSArrayBuffer?)?.toDart.asUint8List(); if (result == null) { throw Exception('Cannot read bytes from Blob. Is it still available?'); diff --git a/packages/cross_file/test/x_file_html_test.dart b/packages/cross_file/test/x_file_html_test.dart index 72f0fbf1d787..27648f0caf22 100644 --- a/packages/cross_file/test/x_file_html_test.dart +++ b/packages/cross_file/test/x_file_html_test.dart @@ -21,9 +21,9 @@ final html.File textFile = html.File( '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); +// 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', () { @@ -89,9 +89,8 @@ void main() { test('Stores data as a Blob', () async { // Read the blob from its path 'natively' - final html.Response response = await html.window - .fetch(file.path.toJS) - .toDart; + final html.Response response = + await html.window.fetch(file.path.toJS).toDart; final JSAny arrayBuffer = await response.arrayBuffer().toDart; final ByteBuffer data = (arrayBuffer as JSArrayBuffer).toDart; @@ -128,9 +127,8 @@ void main() { await file.saveTo('path'); - final html.Element container = html.document.querySelector( - '#$crossFileDomElementId', - )!; + final html.Element container = + html.document.querySelector('#$crossFileDomElementId')!; late html.HTMLAnchorElement element; for (var i = 0; i < container.childNodes.length; i++) { @@ -150,6 +148,7 @@ void main() { final mockAnchor = html.document.createElement('a') as html.HTMLAnchorElement; + addTearDown(() => helpers.anchorElementOverride = null); helpers.anchorElementOverride = (_, __) => mockAnchor; final file = XFile.fromData(bytes, name: textFile.name); From aa5ba67bdf348234098b8539186cb0ed4763fbd5 Mon Sep 17 00:00:00 2001 From: Raju M <24muliyashiya@gmail.com> Date: Thu, 4 Dec 2025 16:24:41 +0530 Subject: [PATCH 3/5] Code format --- packages/cross_file/lib/src/types/html.dart | 49 +++++++++---------- .../lib/src/web_helpers/web_helpers.dart | 7 ++- .../cross_file/test/x_file_html_test.dart | 16 +++--- 3 files changed, 35 insertions(+), 37 deletions(-) diff --git a/packages/cross_file/lib/src/types/html.dart b/packages/cross_file/lib/src/types/html.dart index ec2df43c7b01..86ab6be52c6b 100644 --- a/packages/cross_file/lib/src/types/html.dart +++ b/packages/cross_file/lib/src/types/html.dart @@ -68,9 +68,9 @@ class XFile extends XFileBase { return (mimeType == null) ? Blob([bytes.toJS].toJS) : Blob( - [bytes.toJS].toJS, - BlobPropertyBag(type: mimeType), - ); + [bytes.toJS].toJS, + BlobPropertyBag(type: mimeType), + ); } // Overridable (meta) data that can be specified by the constructors. @@ -122,27 +122,24 @@ class XFile extends XFileBase { final blobCompleter = Completer(); late XMLHttpRequest request; - request = - XMLHttpRequest() - ..open('get', path, true) - ..responseType = 'blob' - ..onLoad.listen((ProgressEvent e) { - assert( - request.response != null, - 'The Blob backing this XFile cannot be null!', - ); - blobCompleter.complete(request.response! as Blob); - }) - ..onError.listen((ProgressEvent e) { - if (e.type == 'error') { - blobCompleter.completeError( - Exception( - 'Could not load Blob from its URL. Has it been revoked?', - ), - ); - } - }) - ..send(); + request = XMLHttpRequest() + ..open('get', path, true) + ..responseType = 'blob' + ..onLoad.listen((ProgressEvent e) { + assert( + request.response != null, + 'The Blob backing this XFile cannot be null!', + ); + blobCompleter.complete(request.response! as Blob); + }) + ..onError.listen((ProgressEvent e) { + if (e.type == 'error') { + blobCompleter.completeError( + Exception('Could not load Blob from its URL. Has it been revoked?'), + ); + } + }) + ..send(); return blobCompleter.future; } @@ -179,8 +176,8 @@ class XFile extends XFileBase { await reader.onLoadEnd.first; - final Uint8List? result = - (reader.result as JSArrayBuffer?)?.toDart.asUint8List(); + final Uint8List? result = (reader.result as JSArrayBuffer?)?.toDart + .asUint8List(); if (result == null) { throw Exception('Cannot read bytes from Blob. Is it still available?'); diff --git a/packages/cross_file/lib/src/web_helpers/web_helpers.dart b/packages/cross_file/lib/src/web_helpers/web_helpers.dart index 1ee3594869dc..f532df3e5d32 100644 --- a/packages/cross_file/lib/src/web_helpers/web_helpers.dart +++ b/packages/cross_file/lib/src/web_helpers/web_helpers.dart @@ -50,10 +50,9 @@ Future saveFileAs(XFile file, String path) async { final Element target = ensureInitialized('__x_file_dom_element'); // Create element. - final HTMLAnchorElement element = - anchorElementOverride != null - ? anchorElementOverride!(file.path, file.name) - : createAnchorElement(file.path, file.name); + final HTMLAnchorElement element = anchorElementOverride != null + ? anchorElementOverride!(file.path, file.name) + : createAnchorElement(file.path, file.name); // Clear existing children before appending new one. while (target.children.length > 0) { diff --git a/packages/cross_file/test/x_file_html_test.dart b/packages/cross_file/test/x_file_html_test.dart index 27648f0caf22..c57ac4db1504 100644 --- a/packages/cross_file/test/x_file_html_test.dart +++ b/packages/cross_file/test/x_file_html_test.dart @@ -21,9 +21,9 @@ final html.File textFile = html.File( '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); + // 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', () { @@ -89,8 +89,9 @@ void main() { test('Stores data as a Blob', () async { // Read the blob from its path 'natively' - final html.Response response = - await html.window.fetch(file.path.toJS).toDart; + final html.Response response = await html.window + .fetch(file.path.toJS) + .toDart; final JSAny arrayBuffer = await response.arrayBuffer().toDart; final ByteBuffer data = (arrayBuffer as JSArrayBuffer).toDart; @@ -127,8 +128,9 @@ void main() { await file.saveTo('path'); - final html.Element container = - html.document.querySelector('#$crossFileDomElementId')!; + final html.Element container = html.document.querySelector( + '#$crossFileDomElementId', + )!; late html.HTMLAnchorElement element; for (var i = 0; i < container.childNodes.length; i++) { From 04dcee5ada42b19a505726251473ab90744d982c Mon Sep 17 00:00:00 2001 From: rajumuliyashiya <24muliyashiya@gmail.com> Date: Sat, 10 Jan 2026 20:34:37 +0530 Subject: [PATCH 4/5] update changelog & version as per request --- packages/cross_file/CHANGELOG.md | 2 +- packages/cross_file/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cross_file/CHANGELOG.md b/packages/cross_file/CHANGELOG.md index ee893bb023d4..73e9db3048af 100644 --- a/packages/cross_file/CHANGELOG.md +++ b/packages/cross_file/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.3.6 +## 0.3.5+2 * Separates "Save As" implementation details from XFile web class. diff --git a/packages/cross_file/pubspec.yaml b/packages/cross_file/pubspec.yaml index af3993fb7518..dedcc19f8ae5 100644 --- a/packages/cross_file/pubspec.yaml +++ b/packages/cross_file/pubspec.yaml @@ -2,7 +2,7 @@ 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.6 +version: 0.3.5+2 environment: sdk: ^3.8.0 From 067a729e5b0d93cddd96bda3f800d2dbac3180e9 Mon Sep 17 00:00:00 2001 From: rajumuliyashiya <24muliyashiya@gmail.com> Date: Fri, 30 Jan 2026 20:24:31 +0530 Subject: [PATCH 5/5] address requested changes --- packages/cross_file/lib/src/types/html.dart | 2 +- .../lib/src/web_helpers/web_helpers.dart | 28 +++++++++++-------- .../cross_file/test/x_file_html_test.dart | 10 +++++-- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/packages/cross_file/lib/src/types/html.dart b/packages/cross_file/lib/src/types/html.dart index 86ab6be52c6b..40703aa754fc 100644 --- a/packages/cross_file/lib/src/types/html.dart +++ b/packages/cross_file/lib/src/types/html.dart @@ -190,6 +190,6 @@ class XFile extends XFileBase { /// For the web implementation, the path variable is ignored. @override Future saveTo(String path) async { - await saveFileAs(this, path); + await saveFileAs(this); } } diff --git a/packages/cross_file/lib/src/web_helpers/web_helpers.dart b/packages/cross_file/lib/src/web_helpers/web_helpers.dart index f532df3e5d32..58231dd8c30e 100644 --- a/packages/cross_file/lib/src/web_helpers/web_helpers.dart +++ b/packages/cross_file/lib/src/web_helpers/web_helpers.dart @@ -2,21 +2,26 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:meta/meta.dart'; import 'package:web/web.dart'; + import '../types/html.dart'; /// Type definition for function that creates anchor elements typedef CreateAnchorElement = HTMLAnchorElement Function(String href, String? suggestedName); -/// Override for creating anchor elements for testing purposes -CreateAnchorElement? anchorElementOverride; - /// Create anchor element with download attribute -HTMLAnchorElement createAnchorElement(String href, String? suggestedName) => - (document.createElement('a') as HTMLAnchorElement) - ..href = href - ..download = suggestedName ?? 'download'; +HTMLAnchorElement _createAnchorElementImpl( + String href, + String? suggestedName, +) => (document.createElement('a') as HTMLAnchorElement) + ..href = href + ..download = suggestedName ?? 'download'; + +/// Function for creating anchor elements. Can be overridden for testing. +@visibleForTesting +CreateAnchorElement createAnchorElementFunction = _createAnchorElementImpl; /// Add an element to a container and click it void addElementToContainerAndClick(Element container, HTMLElement element) { @@ -45,14 +50,15 @@ bool isSafari() { } /// Saves the given [XFile] to user's device ("Save As" dialog). -Future saveFileAs(XFile file, String path) async { +Future saveFileAs(XFile file) async { // Create container element. final Element target = ensureInitialized('__x_file_dom_element'); // Create element. - final HTMLAnchorElement element = anchorElementOverride != null - ? anchorElementOverride!(file.path, file.name) - : createAnchorElement(file.path, file.name); + final HTMLAnchorElement element = createAnchorElementFunction( + file.path, + file.name, + ); // Clear existing children before appending new one. while (target.children.length > 0) { diff --git a/packages/cross_file/test/x_file_html_test.dart b/packages/cross_file/test/x_file_html_test.dart index c57ac4db1504..e57e36c285c7 100644 --- a/packages/cross_file/test/x_file_html_test.dart +++ b/packages/cross_file/test/x_file_html_test.dart @@ -150,8 +150,14 @@ void main() { final mockAnchor = html.document.createElement('a') as html.HTMLAnchorElement; - addTearDown(() => helpers.anchorElementOverride = null); - helpers.anchorElementOverride = (_, __) => mockAnchor; + // Save original function so we can restore it + final helpers.CreateAnchorElement original = + helpers.createAnchorElementFunction; + + addTearDown(() { + helpers.createAnchorElementFunction = original; + }); + helpers.createAnchorElementFunction = (_, __) => mockAnchor; final file = XFile.fromData(bytes, name: textFile.name);