import 'dart:typed_data'; import 'package:flutter_qjs/flutter_qjs.dart'; import 'package:venera/foundation/cache_manager.dart'; import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/consts.dart'; import 'package:venera/utils/image.dart'; import 'app_dio.dart'; class ImageDownloader { static Stream loadThumbnail( String url, String? sourceKey) async* { final cacheKey = "$url@$sourceKey"; final cache = await CacheManager().findCache(cacheKey); if (cache != null) { var data = await cache.readAsBytes(); yield ImageDownloadProgress( currentBytes: data.length, totalBytes: data.length, imageBytes: data, ); } var configs = {}; if (sourceKey != null) { var comicSource = ComicSource.find(sourceKey); configs = comicSource?.getThumbnailLoadingConfig?.call(url) ?? {}; } configs['headers'] ??= {}; if (configs['headers']['user-agent'] == null && configs['headers']['User-Agent'] == null) { configs['headers']['user-agent'] = webUA; } var dio = AppDio(BaseOptions( headers: Map.from(configs['headers']), method: configs['method'] ?? 'GET', responseType: ResponseType.stream, )); var req = await dio.request(configs['url'] ?? url, data: configs['data']); var stream = req.data?.stream ?? (throw "Error: Empty response body."); int? expectedBytes = req.data!.contentLength; if (expectedBytes == -1) { expectedBytes = null; } var buffer = []; await for (var data in stream) { buffer.addAll(data); if (expectedBytes != null) { yield ImageDownloadProgress( currentBytes: buffer.length, totalBytes: expectedBytes, ); } } if (configs['onResponse'] != null) { buffer = configs['onResponse'](buffer); } await CacheManager().writeCache(cacheKey, buffer); yield ImageDownloadProgress( currentBytes: buffer.length, totalBytes: buffer.length, imageBytes: Uint8List.fromList(buffer), ); } static Stream loadComicImage( String imageKey, String? sourceKey, String cid, String eid) async* { final cacheKey = "$imageKey@$sourceKey@$cid@$eid"; final cache = await CacheManager().findCache(cacheKey); if (cache != null) { var data = await cache.readAsBytes(); yield ImageDownloadProgress( currentBytes: data.length, totalBytes: data.length, imageBytes: data, ); } Future?> Function()? onLoadFailed; var configs = {}; if (sourceKey != null) { var comicSource = ComicSource.find(sourceKey); configs = (await comicSource!.getImageLoadingConfig ?.call(imageKey, cid, eid)) ?? {}; } var retryLimit = 5; while (true) { try { configs['headers'] ??= { 'user-agent': webUA, }; if (configs['onLoadFailed'] is JSInvokable) { onLoadFailed = () async { dynamic result = configs['onLoadFailed'](); if (result is Future) { result = await result; } if (result is! Map) return null; return result; }; } var dio = AppDio(BaseOptions( headers: configs['headers'], method: configs['method'] ?? 'GET', responseType: ResponseType.stream, )); var req = await dio.request(configs['url'] ?? imageKey, data: configs['data']); var stream = req.data?.stream ?? (throw "Error: Empty response body."); int? expectedBytes = req.data!.contentLength; if (expectedBytes == -1) { expectedBytes = null; } var buffer = []; await for (var data in stream) { buffer.addAll(data); if (expectedBytes != null) { yield ImageDownloadProgress( currentBytes: buffer.length, totalBytes: expectedBytes, ); } } if (configs['onResponse'] != null) { buffer = configs['onResponse'](buffer); } var data = Uint8List.fromList(buffer); buffer.clear(); if (configs['modifyImage'] != null) { var newData = await modifyImageWithScript( data, configs['modifyImage'], ); data = newData; } await CacheManager().writeCache(cacheKey, data); yield ImageDownloadProgress( currentBytes: data.length, totalBytes: data.length, imageBytes: data, ); return; } catch (e) { if(retryLimit < 0 || onLoadFailed == null) { rethrow; } var newConfig = await onLoadFailed(); onLoadFailed = null; if(newConfig == null) { rethrow; } configs = newConfig; retryLimit--; } } } } class ImageDownloadProgress { final int currentBytes; final int totalBytes; final Uint8List? imageBytes; const ImageDownloadProgress({ required this.currentBytes, required this.totalBytes, this.imageBytes, }); }