| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:typed_data'; |
| import 'dart:ui' as ui; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:js/js.dart'; |
| import 'package:js/js_util.dart' as js_util; |
| |
| import 'image_provider.dart' as image_provider; |
| import 'image_stream.dart'; |
| |
| /// [DomXMLHttpRequest] interop class. |
| @JS() |
| @staticInterop |
| class DomXMLHttpRequest {} |
| |
| /// [DomXMLHttpRequest] extension. |
| extension DomXMLHttpRequestExtension on DomXMLHttpRequest { |
| /// Gets the response. |
| external dynamic get response; |
| |
| /// Gets the response text. |
| external String? get responseText; |
| |
| /// Gets the response type. |
| external String get responseType; |
| |
| /// Gets the status. |
| external int? get status; |
| |
| /// Set the response type. |
| external set responseType(String value); |
| |
| /// Set the request header. |
| external void setRequestHeader(String header, String value); |
| |
| /// Open the request. |
| void open(String method, String url, bool isAsync) => js_util.callMethod( |
| this, 'open', <Object>[method, url, isAsync]); |
| |
| /// Send the request. |
| void send() => js_util.callMethod(this, 'send', <Object>[]); |
| |
| /// Add event listener. |
| void addEventListener(String type, DomEventListener? listener, |
| [bool? useCapture]) { |
| if (listener != null) { |
| js_util.callMethod(this, 'addEventListener', |
| <Object>[type, listener, if (useCapture != null) useCapture]); |
| } |
| } |
| } |
| |
| /// Factory function for creating [DomXMLHttpRequest]. |
| DomXMLHttpRequest createDomXMLHttpRequest() => |
| domCallConstructorString('XMLHttpRequest', <Object?>[])! |
| as DomXMLHttpRequest; |
| |
| /// Type for event listener. |
| typedef DomEventListener = void Function(DomEvent event); |
| |
| /// [DomEvent] interop object. |
| @JS() |
| @staticInterop |
| class DomEvent {} |
| |
| /// [DomEvent] reqiured extension. |
| extension DomEventExtension on DomEvent { |
| /// Get the event type. |
| external String get type; |
| |
| /// Initialize an event. |
| void initEvent(String type, [bool? bubbles, bool? cancelable]) => |
| js_util.callMethod(this, 'initEvent', <Object>[ |
| type, |
| if (bubbles != null) bubbles, |
| if (cancelable != null) cancelable |
| ]); |
| } |
| |
| /// [DomProgressEvent] interop object. |
| @JS() |
| @staticInterop |
| class DomProgressEvent extends DomEvent {} |
| |
| /// [DomProgressEvent] reqiured extension. |
| extension DomProgressEventExtension on DomProgressEvent { |
| /// Amount of work done. |
| external int? get loaded; |
| |
| /// Total amount of work. |
| external int? get total; |
| } |
| |
| /// Gets a constructor from a [String]. |
| Object? domGetConstructor(String constructorName) => |
| js_util.getProperty(domWindow, constructorName); |
| |
| /// Calls a constructor as a [String]. |
| Object? domCallConstructorString(String constructorName, List<Object?> args) { |
| final Object? constructor = domGetConstructor(constructorName); |
| if (constructor == null) { |
| return null; |
| } |
| return js_util.callConstructor(constructor, args); |
| } |
| |
| /// The underyling window object. |
| @JS('window') |
| external Object get domWindow; |
| |
| /// Creates a type for an overridable factory function for testing purposes. |
| typedef HttpRequestFactory = DomXMLHttpRequest Function(); |
| |
| /// Default HTTP client. |
| DomXMLHttpRequest _httpClient() { |
| return DomXMLHttpRequest(); |
| } |
| |
| /// Creates an overridable factory function. |
| HttpRequestFactory httpRequestFactory = _httpClient; |
| |
| /// Restores to the default HTTP request factory. |
| void debugRestoreHttpRequestFactory() { |
| httpRequestFactory = _httpClient; |
| } |
| |
| /// The web implementation of [image_provider.NetworkImage]. |
| /// |
| /// NetworkImage on the web does not support decoding to a specified size. |
| @immutable |
| class NetworkImage |
| extends image_provider.ImageProvider<image_provider.NetworkImage> |
| implements image_provider.NetworkImage { |
| /// Creates an object that fetches the image at the given URL. |
| /// |
| /// The arguments [url] and [scale] must not be null. |
| const NetworkImage(this.url, {this.scale = 1.0, this.headers}) |
| : assert(url != null), |
| assert(scale != null); |
| |
| @override |
| final String url; |
| |
| @override |
| final double scale; |
| |
| @override |
| final Map<String, String>? headers; |
| |
| @override |
| Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) { |
| return SynchronousFuture<NetworkImage>(this); |
| } |
| |
| @override |
| ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) { |
| // Ownership of this controller is handed off to [_loadAsync]; it is that |
| // method's responsibility to close the controller's stream when the image |
| // has been loaded or an error is thrown. |
| final StreamController<ImageChunkEvent> chunkEvents = |
| StreamController<ImageChunkEvent>(); |
| |
| return MultiFrameImageStreamCompleter( |
| chunkEvents: chunkEvents.stream, |
| codec: _loadAsync(key as NetworkImage, null, decode, chunkEvents), |
| scale: key.scale, |
| debugLabel: key.url, |
| informationCollector: _imageStreamInformationCollector(key), |
| ); |
| } |
| |
| @override |
| ImageStreamCompleter loadBuffer(image_provider.NetworkImage key, image_provider.DecoderBufferCallback decode) { |
| // Ownership of this controller is handed off to [_loadAsync]; it is that |
| // method's responsibility to close the controller's stream when the image |
| // has been loaded or an error is thrown. |
| final StreamController<ImageChunkEvent> chunkEvents = |
| StreamController<ImageChunkEvent>(); |
| |
| return MultiFrameImageStreamCompleter( |
| chunkEvents: chunkEvents.stream, |
| codec: _loadAsync(key as NetworkImage, decode, null, chunkEvents), |
| scale: key.scale, |
| debugLabel: key.url, |
| informationCollector: _imageStreamInformationCollector(key), |
| ); |
| } |
| |
| InformationCollector? _imageStreamInformationCollector(image_provider.NetworkImage key) { |
| InformationCollector? collector; |
| assert(() { |
| collector = () => <DiagnosticsNode>[ |
| DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this), |
| DiagnosticsProperty<NetworkImage>('Image key', key as NetworkImage), |
| ]; |
| return true; |
| }()); |
| return collector; |
| } |
| |
| // TODO(garyq): We should eventually support custom decoding of network images on Web as |
| // well, see https://github.com/flutter/flutter/issues/42789. |
| // |
| // Web does not support decoding network images to a specified size. The decode parameter |
| // here is ignored and the web-only `ui.webOnlyInstantiateImageCodecFromUrl` will be used |
| // directly in place of the typical `instantiateImageCodec` method. |
| Future<ui.Codec> _loadAsync( |
| NetworkImage key, |
| image_provider.DecoderBufferCallback? decode, |
| image_provider.DecoderCallback? decodeDepreacted, |
| StreamController<ImageChunkEvent> chunkEvents, |
| ) async { |
| assert(key == this); |
| |
| final Uri resolved = Uri.base.resolve(key.url); |
| |
| // We use a different method when headers are set because the |
| // `ui.webOnlyInstantiateImageCodecFromUrl` method is not capable of handling headers. |
| if (key.headers?.isNotEmpty ?? false) { |
| final Completer<DomXMLHttpRequest> completer = |
| Completer<DomXMLHttpRequest>(); |
| final DomXMLHttpRequest request = httpRequestFactory(); |
| |
| request.open('GET', key.url, true); |
| request.responseType = 'arraybuffer'; |
| key.headers!.forEach((String header, String value) { |
| request.setRequestHeader(header, value); |
| }); |
| |
| request.addEventListener('load', allowInterop((DomEvent e) { |
| final int? status = request.status; |
| final bool accepted = status! >= 200 && status < 300; |
| final bool fileUri = status == 0; // file:// URIs have status of 0. |
| final bool notModified = status == 304; |
| final bool unknownRedirect = status > 307 && status < 400; |
| final bool success = |
| accepted || fileUri || notModified || unknownRedirect; |
| |
| if (success) { |
| completer.complete(request); |
| } else { |
| completer.completeError(e); |
| throw image_provider.NetworkImageLoadException( |
| statusCode: request.status ?? 400, uri: resolved); |
| } |
| })); |
| |
| request.addEventListener('error', allowInterop(completer.completeError)); |
| |
| request.send(); |
| |
| await completer.future; |
| |
| final Uint8List bytes = (request.response as ByteBuffer).asUint8List(); |
| |
| if (bytes.lengthInBytes == 0) { |
| throw image_provider.NetworkImageLoadException( |
| statusCode: request.status!, uri: resolved); |
| } |
| |
| if (decode != null) { |
| final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes); |
| return decode(buffer); |
| } else { |
| assert(decodeDepreacted != null); |
| return decodeDepreacted!(bytes); |
| } |
| } else { |
| // This API only exists in the web engine implementation and is not |
| // contained in the analyzer summary for Flutter. |
| // ignore: undefined_function, avoid_dynamic_calls |
| return ui.webOnlyInstantiateImageCodecFromUrl( |
| resolved, |
| chunkCallback: (int bytes, int total) { |
| chunkEvents.add(ImageChunkEvent( |
| cumulativeBytesLoaded: bytes, expectedTotalBytes: total)); |
| }, |
| ) as Future<ui.Codec>; |
| } |
| } |
| |
| @override |
| bool operator ==(Object other) { |
| if (other.runtimeType != runtimeType) { |
| return false; |
| } |
| return other is NetworkImage && other.url == url && other.scale == scale; |
| } |
| |
| @override |
| int get hashCode => Object.hash(url, scale); |
| |
| @override |
| String toString() => |
| '${objectRuntimeType(this, 'NetworkImage')}("$url", scale: $scale)'; |
| } |