// ignore_for_file: non_nullable_equals_parameter, depend_on_referenced_packages, implementation_imports import 'dart:async'; import 'dart:io'; import 'dart:ui' as ui show Codec; import 'package:extended_image_library/src/extended_image_provider.dart'; import 'package:extended_image_library/src/platform.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:http_client_helper/http_client_helper.dart'; import 'package:mangayomi/services/http/m_client.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:extended_image_library/src/network/extended_network_image_provider.dart' as image_provider; class CustomExtendedNetworkImageProvider extends ImageProvider with ExtendedImageProvider implements image_provider.ExtendedNetworkImageProvider { /// Creates an object that fetches the image at the given URL. /// /// The arguments must not be null. CustomExtendedNetworkImageProvider( this.url, { this.scale = 1.0, this.headers, this.cache = true, this.retries = 3, this.timeLimit, this.timeRetry = const Duration(milliseconds: 100), this.cacheKey, this.printError = true, this.cacheRawData = false, this.cancelToken, this.imageCacheName, this.cacheMaxAge = const Duration(days: 30), this.showCloudFlareError = false, }); /// The name of [ImageCache], you can define custom [ImageCache] to store this provider. @override final String? imageCacheName; /// Whether cache raw data if you need to get raw data directly. /// For example, we need raw image data to edit, /// but [ui.Image.toByteData()] is very slow. So we cache the image /// data here. @override final bool cacheRawData; /// The time limit to request image @override final Duration? timeLimit; /// The time to retry to request @override final int retries; /// The time duration to retry to request @override final Duration timeRetry; /// Whether cache image to local @override final bool cache; /// The URL from which the image will be fetched. @override final String url; /// The scale to place in the [ImageInfo] object of the image. @override final double scale; /// The HTTP headers that will be used with [HttpClient.get] to fetch image from network. @override final Map? headers; /// The token to cancel network request @override final CancellationToken? cancelToken; /// Custom cache key @override final String? cacheKey; /// print error @override final bool printError; /// The max duration to cahce image. /// After this time the cache is expired and the image is reloaded. @override final Duration? cacheMaxAge; final bool showCloudFlareError; @override ImageStreamCompleter loadImage( image_provider.ExtendedNetworkImageProvider key, ImageDecoderCallback 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 chunkEvents = StreamController(); return MultiFrameImageStreamCompleter( codec: _loadAsync( key as CustomExtendedNetworkImageProvider, chunkEvents, decode, ), scale: key.scale, chunkEvents: chunkEvents.stream, debugLabel: key.url, informationCollector: () { return [ DiagnosticsProperty('Image provider', this), DiagnosticsProperty( 'Image key', key), ]; }, ); } @override Future obtainKey( ImageConfiguration configuration) { return SynchronousFuture(this); } Future _loadAsync( CustomExtendedNetworkImageProvider key, StreamController chunkEvents, ImageDecoderCallback decode, ) async { assert(key == this); final String md5Key = cacheKey ?? keyToMd5(key.url); ui.Codec? result; if (cache) { try { final Uint8List? data = await _loadCache( key, chunkEvents, md5Key, ); if (data != null) { result = await instantiateImageCodec(data, decode); } } catch (e) { if (kDebugMode) { print(e); } } } if (result == null) { try { final Uint8List? data = await _loadNetwork( key, chunkEvents, ); if (data != null) { result = await instantiateImageCodec(data, decode); } } catch (e) { if (kDebugMode) { print(e); } } } //Failed to load if (result == null) { //result = await ui.instantiateImageCodec(kTransparentImage); return Future.error(StateError('Failed to load $url.')); } return result; } /// Get the image from cache folder. Future _loadCache( CustomExtendedNetworkImageProvider key, StreamController? chunkEvents, String md5Key, ) async { final Directory cacheImagesDirectory = Directory( join((await getTemporaryDirectory()).path, cacheImageFolderName)); Uint8List? data; // exist, try to find cache image file if (cacheImagesDirectory.existsSync()) { final File cacheFlie = File(join(cacheImagesDirectory.path, md5Key)); if (cacheFlie.existsSync()) { if (key.cacheMaxAge != null) { final DateTime now = DateTime.now(); final FileStat fs = cacheFlie.statSync(); if (now.subtract(key.cacheMaxAge!).isAfter(fs.changed)) { cacheFlie.deleteSync(recursive: true); } else { data = await cacheFlie.readAsBytes(); } } else { data = await cacheFlie.readAsBytes(); } } } // create folder else { await cacheImagesDirectory.create(); } // load from network if (data == null) { data = await _loadNetwork( key, chunkEvents, ); if (data != null) { // cache image file await File(join(cacheImagesDirectory.path, md5Key)).writeAsBytes(data); } } return data; } /// Get the image from network. Future _loadNetwork( CustomExtendedNetworkImageProvider key, StreamController? chunkEvents, ) async { try { final Uri resolved = Uri.base.resolve(key.url); final StreamedResponse? response = await _tryGetResponse(resolved); List bytes = []; final int total = response!.contentLength ?? 0; if (response.statusCode == HttpStatus.ok) { int received = 0; response.stream.asBroadcastStream(); await for (var chunk in response.stream) { bytes.addAll(chunk); try { received += chunk.length; if (chunkEvents != null) {} chunkEvents!.add(ImageChunkEvent( cumulativeBytesLoaded: received, expectedTotalBytes: total)); } catch (e) { if (kDebugMode) { print(e); } } } } else { return null; } if (bytes.isEmpty) { return Future.error( StateError('NetworkImage is an empty file: $resolved')); } return Uint8List.fromList(bytes); } on OperationCanceledError catch (_) { if (kDebugMode) { print('User cancel request $url.'); } return Future.error(StateError('User cancel request $url.')); } catch (e) { if (kDebugMode) { print(e); } } finally { await chunkEvents?.close(); } return null; } Future _getResponse(Uri resolved) async { var request = Request('GET', resolved); request.headers.addAll(headers ?? {}); StreamedResponse response = await MClient.init(showCloudFlareError: showCloudFlareError) .send(request); if (response.request != null) { final res = await MClient.init( reqcopyWith: {'useDartHttpClient': true}, showCloudFlareError: showCloudFlareError) .send(response.request!); if (![403, 503].contains(res.statusCode) && ["cloudflare-nginx", "cloudflare"].contains(res.headers["server"])) { return res; } } return response; } // Http get with cancel, delay try again Future _tryGetResponse( Uri resolved, ) async { cancelToken?.throwIfCancellationRequested(); return await RetryHelper.tryRun( () { return CancellationTokenSource.register( cancelToken, _getResponse(resolved), ); }, cancelToken: cancelToken, timeRetry: timeRetry, retries: retries, ); } @override bool operator ==(dynamic other) { if (other.runtimeType != runtimeType) { return false; } return other is CustomExtendedNetworkImageProvider && url == other.url && scale == other.scale && cacheRawData == other.cacheRawData && timeLimit == other.timeLimit && cancelToken == other.cancelToken && timeRetry == other.timeRetry && cache == other.cache && cacheKey == other.cacheKey && //headers == other.headers && retries == other.retries && imageCacheName == other.imageCacheName && cacheMaxAge == other.cacheMaxAge; } @override int get hashCode => Object.hash( url, scale, cacheRawData, timeLimit, cancelToken, timeRetry, cache, cacheKey, //headers, retries, imageCacheName, cacheMaxAge, ); @override String toString() => '$runtimeType("$url", scale: $scale)'; @override /// Get network image data from cached Future getNetworkImageData({ StreamController? chunkEvents, }) async { final String uId = cacheKey ?? keyToMd5(url); if (cache) { return await _loadCache( this, chunkEvents, uId, ); } return await _loadNetwork( this, chunkEvents, ); } }