mangayomi/lib/modules/widgets/custom_extended_image_provider.dart

364 lines
9.9 KiB
Dart

// 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/interceptor.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<image_provider.ExtendedNetworkImageProvider>
with ExtendedImageProvider<image_provider.ExtendedNetworkImageProvider>
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),
});
/// 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<String, String>? 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;
@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<ImageChunkEvent> chunkEvents =
StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
codec: _loadAsync(
key as CustomExtendedNetworkImageProvider,
chunkEvents,
decode,
),
scale: key.scale,
chunkEvents: chunkEvents.stream,
debugLabel: key.url,
informationCollector: () {
return <DiagnosticsNode>[
DiagnosticsProperty<ImageProvider>('Image provider', this),
DiagnosticsProperty<image_provider.ExtendedNetworkImageProvider>(
'Image key', key),
];
},
);
}
@override
Future<CustomExtendedNetworkImageProvider> obtainKey(
ImageConfiguration configuration) {
return SynchronousFuture<CustomExtendedNetworkImageProvider>(this);
}
Future<ui.Codec> _loadAsync(
CustomExtendedNetworkImageProvider key,
StreamController<ImageChunkEvent> 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<ui.Codec>.error(StateError('Failed to load $url.'));
}
return result;
}
/// Get the image from cache folder.
Future<Uint8List?> _loadCache(
CustomExtendedNetworkImageProvider key,
StreamController<ImageChunkEvent>? 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<Uint8List?> _loadNetwork(
CustomExtendedNetworkImageProvider key,
StreamController<ImageChunkEvent>? chunkEvents,
) async {
try {
final Uri resolved = Uri.base.resolve(key.url);
final StreamedResponse? response = await _tryGetResponse(resolved);
List<int> 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<Uint8List>.error(
StateError('NetworkImage is an empty file: $resolved'));
}
return Uint8List.fromList(bytes);
} on OperationCanceledError catch (_) {
if (kDebugMode) {
print('User cancel request $url.');
}
return Future<Uint8List>.error(StateError('User cancel request $url.'));
} catch (e) {
if (kDebugMode) {
print(e);
}
} finally {
await chunkEvents?.close();
}
return null;
}
Future<StreamedResponse> _getResponse(Uri resolved) async {
var request = Request('GET', resolved);
request.headers.addAll(headers ?? {});
StreamedResponse response = await MInterceptor.init().send(request);
return response;
}
// Http get with cancel, delay try again
Future<StreamedResponse?> _tryGetResponse(
Uri resolved,
) async {
cancelToken?.throwIfCancellationRequested();
return await RetryHelper.tryRun<StreamedResponse>(
() {
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<Uint8List?> getNetworkImageData({
StreamController<ImageChunkEvent>? chunkEvents,
}) async {
final String uId = cacheKey ?? keyToMd5(url);
if (cache) {
return await _loadCache(
this,
chunkEvents,
uId,
);
}
return await _loadNetwork(
this,
chunkEvents,
);
}
}