Files
venera/lib/foundation/image_provider/base_image_provider.dart
2024-12-11 13:41:34 +08:00

157 lines
4.2 KiB
Dart

import 'dart:async' show Future, StreamController, scheduleMicrotask;
import 'dart:convert';
import 'dart:math';
import 'dart:ui' as ui show Codec;
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:venera/foundation/cache_manager.dart';
abstract class BaseImageProvider<T extends BaseImageProvider<T>>
extends ImageProvider<T> {
const BaseImageProvider();
static double? _effectiveScreenWidth;
static const double _normalComicImageRatio = 0.72;
static const double _minComicImageWidth = 1920 * _normalComicImageRatio;
static TargetImageSize _getTargetSize(width, height) {
if (_effectiveScreenWidth == null) {
final screens = PlatformDispatcher.instance.displays;
for (var screen in screens) {
if (screen.size.width > screen.size.height) {
_effectiveScreenWidth = max(
_effectiveScreenWidth ?? 0,
screen.size.height * _normalComicImageRatio,
);
} else {
_effectiveScreenWidth = max(
_effectiveScreenWidth ?? 0,
screen.size.width
);
}
}
if (_effectiveScreenWidth! < _minComicImageWidth) {
_effectiveScreenWidth = _minComicImageWidth;
}
}
if (width > _effectiveScreenWidth!) {
height = (height * _effectiveScreenWidth! / width).round();
width = _effectiveScreenWidth!.round();
}
return TargetImageSize(width: width, height: height);
}
@override
ImageStreamCompleter loadImage(T key, ImageDecoderCallback decode) {
final chunkEvents = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
codec: _loadBufferAsync(key, chunkEvents, decode),
chunkEvents: chunkEvents.stream,
scale: 1.0,
informationCollector: () sync* {
yield DiagnosticsProperty<ImageProvider>(
'Image provider: $this \n Image key: $key',
this,
style: DiagnosticsTreeStyle.errorProperty,
);
},
);
}
Future<ui.Codec> _loadBufferAsync(
T key,
StreamController<ImageChunkEvent> chunkEvents,
ImageDecoderCallback decode,
) async {
try {
int retryTime = 1;
bool stop = false;
chunkEvents.onCancel = () {
stop = true;
};
Uint8List? data;
while (data == null && !stop) {
try {
data = await load(chunkEvents);
} catch (e) {
if (e.toString().contains("Invalid Status Code: 404")) {
rethrow;
}
if (e.toString().contains("Invalid Status Code: 403")) {
rethrow;
}
if (e.toString().contains("handshake")) {
if (retryTime < 5) {
retryTime = 5;
}
}
retryTime <<= 1;
if (retryTime > (1 << 3) || stop) {
rethrow;
}
await Future.delayed(Duration(seconds: retryTime));
}
}
if (stop) {
throw Exception("Image loading is stopped");
}
if (data!.isEmpty) {
throw Exception("Empty image data");
}
try {
final buffer = await ImmutableBuffer.fromUint8List(data);
return await decode(buffer, getTargetSize: _getTargetSize);
} catch (e) {
await CacheManager().delete(this.key);
if (data.length < 2 * 1024) {
// data is too short, it's likely that the data is text, not image
try {
var text =
const Utf8Codec(allowMalformed: false).decoder.convert(data);
throw Exception("Expected image data, but got text: $text");
} catch (e) {
// ignore
}
}
rethrow;
}
} catch (e) {
scheduleMicrotask(() {
PaintingBinding.instance.imageCache.evict(key);
});
rethrow;
} finally {
chunkEvents.close();
}
}
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents);
String get key;
@override
bool operator ==(Object other) {
return other is BaseImageProvider<T> && key == other.key;
}
@override
int get hashCode => key.hashCode;
@override
String toString() {
return "$runtimeType($key)";
}
}
typedef FileDecoderCallback = Future<ui.Codec> Function(Uint8List);