mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
initial commit
This commit is contained in:
148
lib/foundation/image_provider/base_image_provider.dart
Normal file
148
lib/foundation/image_provider/base_image_provider.dart
Normal file
@@ -0,0 +1,148 @@
|
||||
import 'dart:async' show Future, StreamController, scheduleMicrotask;
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
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();
|
||||
|
||||
@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 {
|
||||
if(_cache.containsKey(key.key)){
|
||||
data = _cache[key.key];
|
||||
} else {
|
||||
data = await load(chunkEvents);
|
||||
_checkCacheSize();
|
||||
_cache[key.key] = data;
|
||||
_cacheSize += data.length;
|
||||
}
|
||||
} catch (e) {
|
||||
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);
|
||||
} catch (e) {
|
||||
await CacheManager().delete(this.key);
|
||||
Object error = e;
|
||||
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);
|
||||
error = Exception("Expected image data, but got text: $text");
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} catch (e) {
|
||||
scheduleMicrotask(() {
|
||||
PaintingBinding.instance.imageCache.evict(key);
|
||||
});
|
||||
rethrow;
|
||||
} finally {
|
||||
chunkEvents.close();
|
||||
}
|
||||
}
|
||||
|
||||
static final _cache = LinkedHashMap<String, Uint8List>();
|
||||
|
||||
static var _cacheSize = 0;
|
||||
|
||||
static var _cacheSizeLimit = 50 * 1024 * 1024;
|
||||
|
||||
static void _checkCacheSize(){
|
||||
while (_cacheSize > _cacheSizeLimit){
|
||||
var firstKey = _cache.keys.first;
|
||||
_cacheSize -= _cache[firstKey]!.length;
|
||||
_cache.remove(firstKey);
|
||||
}
|
||||
}
|
||||
|
||||
static void clearCache(){
|
||||
_cache.clear();
|
||||
_cacheSize = 0;
|
||||
}
|
||||
|
||||
static void setCacheSizeLimit(int size){
|
||||
_cacheSizeLimit = size;
|
||||
_checkCacheSize();
|
||||
}
|
||||
|
||||
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);
|
80
lib/foundation/image_provider/cached_image.dart
Normal file
80
lib/foundation/image_provider/cached_image.dart
Normal file
@@ -0,0 +1,80 @@
|
||||
import 'dart:async' show Future, StreamController;
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.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/network/app_dio.dart';
|
||||
import 'base_image_provider.dart';
|
||||
import 'cached_image.dart' as image_provider;
|
||||
|
||||
class CachedImageProvider
|
||||
extends BaseImageProvider<image_provider.CachedImageProvider> {
|
||||
/// Image provider for normal image.
|
||||
const CachedImageProvider(this.url, {this.headers, this.sourceKey});
|
||||
|
||||
final String url;
|
||||
|
||||
final Map<String, String>? headers;
|
||||
|
||||
final String? sourceKey;
|
||||
|
||||
@override
|
||||
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {
|
||||
final cacheKey = "$url@$sourceKey";
|
||||
final cache = await CacheManager().findCache(cacheKey);
|
||||
|
||||
if (cache != null) {
|
||||
return await cache.readAsBytes();
|
||||
}
|
||||
|
||||
var configs = <String, dynamic>{};
|
||||
if (sourceKey != null) {
|
||||
var comicSource = ComicSource.find(sourceKey!);
|
||||
configs = comicSource!.getThumbnailLoadingConfig?.call(url) ?? {};
|
||||
}
|
||||
configs['headers'] ??= {
|
||||
'user-agent': webUA,
|
||||
};
|
||||
|
||||
var dio = AppDio(BaseOptions(
|
||||
headers: configs['headers'],
|
||||
method: configs['method'] ?? 'GET',
|
||||
responseType: ResponseType.stream,
|
||||
));
|
||||
|
||||
var req = await dio.request<ResponseBody>(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 = <int>[];
|
||||
await for (var data in stream) {
|
||||
buffer.addAll(data);
|
||||
if (expectedBytes != null) {
|
||||
chunkEvents.add(ImageChunkEvent(
|
||||
cumulativeBytesLoaded: buffer.length,
|
||||
expectedTotalBytes: expectedBytes,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if(configs['onResponse'] != null) {
|
||||
buffer = configs['onResponse'](buffer);
|
||||
}
|
||||
|
||||
await CacheManager().writeCache(cacheKey, buffer);
|
||||
return Uint8List.fromList(buffer);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CachedImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||
return SynchronousFuture(this);
|
||||
}
|
||||
|
||||
@override
|
||||
String get key => url;
|
||||
}
|
Reference in New Issue
Block a user