diff --git a/dio/lib/src/options.dart b/dio/lib/src/options.dart index 1e3842fc0..4d938e4c6 100644 --- a/dio/lib/src/options.dart +++ b/dio/lib/src/options.dart @@ -41,6 +41,13 @@ enum ResponseType { /// Get the original bytes, the [Response.data] will be [List]. bytes, + + /// Get blob url. + /// This value is needed when you need content in the form of a blob url. + /// Only work in web. + /// + /// the [Response.data] will be [String]. + blobUrl, } /// {@template dio.options.ListFormat} diff --git a/example_flutter_app/lib/download_web/download_blob.dart b/example_flutter_app/lib/download_web/download_blob.dart new file mode 100644 index 000000000..53e70d0be --- /dev/null +++ b/example_flutter_app/lib/download_web/download_blob.dart @@ -0,0 +1,14 @@ +import 'dart:js_interop'; +import 'package:web/web.dart'; + +void downloadBlob(String blobUrl, String name) { + final Document htmlDocument = document; + final HTMLAnchorElement anchor = + htmlDocument.createElement('a') as HTMLAnchorElement; + anchor.href = blobUrl; + anchor.style.display = name; + anchor.download = name; + document.body!.add(anchor); + anchor.click(); + anchor.remove(); +} diff --git a/example_flutter_app/lib/download_web/main_web_download.dart b/example_flutter_app/lib/download_web/main_web_download.dart new file mode 100644 index 000000000..823ff1934 --- /dev/null +++ b/example_flutter_app/lib/download_web/main_web_download.dart @@ -0,0 +1,103 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; + +import 'download_blob.dart'; + +late Dio dio; + +void main() { + dio = Dio( + BaseOptions( + connectTimeout: const Duration(hours: 3), + ), + ); + + dio.interceptors.add(LogInterceptor()); + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter web download blob Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: MyHomePage(title: 'Flutter web download blob Demo'), + ); + } +} + +class MyHomePage extends StatefulWidget { + MyHomePage({ + super.key, + this.title = '', + }); + + final String title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + var url = + 'https://jsoncompare.org/LearningContainer/SampleFiles/Video/MP4/sample-mp4-file.mp4'; + + CancelToken cancelToken = CancelToken(); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Center( + child: Column( + spacing: 20, + children: [ + ElevatedButton( + child: const Text('Request'), + onPressed: () async { + if (cancelToken.isCancelled) { + cancelToken = CancelToken(); + } + + try { + // Fetch blob + final Response res = await dio.download( + url, + '', + onReceiveProgress: (count, total) { + print((count / total) * 100); + }, + cancelToken: cancelToken, + ); + + // Download blob + downloadBlob(res.data, url.split('/').last); + print('fin'); + } catch (e) { + print('error'); + } + }, + ), + ElevatedButton( + child: const Text('Cancel'), + onPressed: () async { + cancelToken.cancel(); + }, + ), + ], + ), + ), + ); + } +} diff --git a/plugins/web_adapter/lib/src/adapter.dart b/plugins/web_adapter/lib/src/adapter.dart index df43ed629..a7a9c4b7f 100644 --- a/plugins/web_adapter/lib/src/adapter.dart +++ b/plugins/web_adapter/lib/src/adapter.dart @@ -37,7 +37,8 @@ class BrowserHttpClientAdapter implements HttpClientAdapter { xhrs.add(xhr); xhr ..open(options.method, '${options.uri}') - ..responseType = 'arraybuffer'; + ..responseType = + options.responseType == ResponseType.blobUrl ? 'blob' : 'arraybuffer'; final withCredentialsOption = options.extra['withCredentials']; if (withCredentialsOption != null) { @@ -66,18 +67,32 @@ class BrowserHttpClientAdapter implements HttpClientAdapter { final completer = Completer(); xhr.onLoad.first.then((_) { - final ByteBuffer body = (xhr.response as JSArrayBuffer).toDart; - completer.complete( - ResponseBody.fromBytes( - body.asUint8List(), - xhr.status, - headers: xhr.getResponseHeaders(), - statusMessage: xhr.statusText, - isRedirect: xhr.status == 302 || - xhr.status == 301 || - options.uri.toString() != xhr.responseURL, - ), - ); + if (options.responseType == ResponseType.blobUrl) { + completer.complete( + ResponseBody.fromString( + web.URL.createObjectURL(xhr.response as web.Blob), + xhr.status, + headers: xhr.getResponseHeaders(), + statusMessage: xhr.statusText, + isRedirect: xhr.status == 302 || + xhr.status == 301 || + options.uri.toString() != xhr.responseURL, + ), + ); + } else { + final ByteBuffer body = (xhr.response as JSArrayBuffer).toDart; + completer.complete( + ResponseBody.fromBytes( + body.asUint8List(), + xhr.status, + headers: xhr.getResponseHeaders(), + statusMessage: xhr.statusText, + isRedirect: xhr.status == 302 || + xhr.status == 301 || + options.uri.toString() != xhr.responseURL, + ), + ); + } }); Timer? connectTimeoutTimer; diff --git a/plugins/web_adapter/lib/src/dio_impl.dart b/plugins/web_adapter/lib/src/dio_impl.dart index ac1dc309c..4f784b599 100644 --- a/plugins/web_adapter/lib/src/dio_impl.dart +++ b/plugins/web_adapter/lib/src/dio_impl.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:dio/dio.dart'; import 'adapter.dart'; @@ -26,9 +28,17 @@ class DioForBrowser with DioMixin implements Dio { String lengthHeader = Headers.contentLengthHeader, Object? data, Options? options, - }) { - throw UnsupportedError( - 'The download method is not available in the Web environment.', + }) async { + return fetch( + RequestOptions( + baseUrl: urlPath, + data: data, + method: options?.method ?? 'GET', + responseType: ResponseType.blobUrl, + queryParameters: queryParameters, + cancelToken: cancelToken, + onReceiveProgress: onReceiveProgress, + ), ); } } diff --git a/plugins/web_adapter/test/browser_test.dart b/plugins/web_adapter/test/browser_test.dart index d752fe85e..acdf0207b 100644 --- a/plugins/web_adapter/test/browser_test.dart +++ b/plugins/web_adapter/test/browser_test.dart @@ -18,4 +18,18 @@ void main() { browserAdapter.fetch(opts, testStream, cancelFuture); expect(browserAdapter.xhrs.every((e) => e.withCredentials == true), isTrue); }); + + test('ResponseType in blobUrl', () async { + final browserAdapter = BrowserHttpClientAdapter(withCredentials: true); + final opts = RequestOptions(responseType: ResponseType.blobUrl); + final testStream = Stream.periodic( + const Duration(seconds: 1), + (x) => Uint8List(x), + ); + final cancelFuture = opts.cancelToken?.whenCancel; + + browserAdapter.fetch(opts, testStream, cancelFuture); + expect(browserAdapter.xhrs.every((e) => e.withCredentials == true), isTrue); + expect(opts.responseType, ResponseType.blobUrl); + }); }