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> extends ImageProvider { const BaseImageProvider(); @override ImageStreamCompleter loadImage(T key, ImageDecoderCallback decode) { final chunkEvents = StreamController(); return MultiFrameImageStreamCompleter( codec: _loadBufferAsync(key, chunkEvents, decode), chunkEvents: chunkEvents.stream, scale: 1.0, informationCollector: () sync* { yield DiagnosticsProperty( 'Image provider: $this \n Image key: $key', this, style: DiagnosticsTreeStyle.errorProperty, ); }, ); } Future _loadBufferAsync( T key, StreamController 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("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); } 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(); } } static final _cache = LinkedHashMap(); 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 load(StreamController chunkEvents); String get key; @override bool operator ==(Object other) { return other is BaseImageProvider && key == other.key; } @override int get hashCode => key.hashCode; @override String toString() { return "$runtimeType($key)"; } } typedef FileDecoderCallback = Future Function(Uint8List);