From b19265ef324ac0a4376143aff8e85d9cf8429e7b Mon Sep 17 00:00:00 2001 From: lifegpc Date: Mon, 27 May 2024 09:12:52 +0800 Subject: [PATCH] Add support to cache images to disk --- lib/provider/dio_image_provider.dart | 165 +++++++++++++++++++++++++++ lib/viewer/single.dart | 2 +- pubspec.lock | 8 -- pubspec.yaml | 1 - 4 files changed, 166 insertions(+), 10 deletions(-) create mode 100644 lib/provider/dio_image_provider.dart diff --git a/lib/provider/dio_image_provider.dart b/lib/provider/dio_image_provider.dart new file mode 100644 index 0000000..3c20b38 --- /dev/null +++ b/lib/provider/dio_image_provider.dart @@ -0,0 +1,165 @@ +// Original code from https://github.com/ueman/image_provider/blob/ed6ff43e1ba69a8ab5a3701d70974cae9cd68a34/dio_image_provider/lib/dio_image_provider.dart +// Modified by lifegpc +import 'dart:async'; +import 'dart:ui' as ui; + +import 'package:flutter/foundation.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter/widgets.dart'; +import 'package:logging/logging.dart'; + +import '../globals.dart'; + +final _log = Logger("DioImageProvider"); + +/// Fetches the given URL from the network, associating it with the given scale. +/// +/// The image will be cached regardless of cache headers from the server. +/// +/// See also: +/// +/// * [Image.network]. +/// * https://pub.dev/packages/http_image_provider +@immutable +class DioImage extends ImageProvider { + static Dio defaultDio = Dio(); + + /// Creates an object that fetches the image at the given URL. + /// + /// The arguments [url] and [scale] must not be null. + /// [dio] will be the default [Dio] if not set. + DioImage.string(String url, {this.scale = 1.0, this.headers, Dio? dio}) + : dio = dio ?? defaultDio, + url = Uri.parse(url); + + /// Creates an object that fetches the image at the given URL. + /// + /// The arguments [url] and [scale] must not be null. + /// [dio] will be the default [Dio] if not set. + DioImage(this.url, {this.scale = 1.0, this.headers, Dio? dio}) + : dio = dio ?? defaultDio; + + /// The URL from which the image will be fetched. + final Uri url; + + /// The scale to place in the [ImageInfo] object of the image. + final double scale; + + /// The HTTP headers that will be used with [HttpClient.get] to fetch image from network. + /// + /// When running flutter on the web, headers are not used. + final Map? headers; + + /// [dio] will be the default [Dio] if not set. + final Dio dio; + + @override + Future obtainKey(ImageConfiguration configuration) { + return SynchronousFuture(this); + } + + @override + ImageStreamCompleter loadImage(DioImage 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 chunkEvents = StreamController(); + + return MultiFrameImageStreamCompleter( + codec: _loadAsync(key, chunkEvents, decode), + chunkEvents: chunkEvents.stream, + scale: key.scale, + debugLabel: key.url.toString(), + informationCollector: () => [ + DiagnosticsProperty('Image provider', this), + DiagnosticsProperty('Image key', key), + ], + ); + } + + Future _loadAsync( + DioImage key, + StreamController chunkEvents, + ImageDecoderCallback decode, + ) async { + try { + assert(key == this); + + if (isImageCacheEnabled) { + try { + final cache = await imageCaches.getCache(url.toString()); + if (cache != null) { + final buffer = await ui.ImmutableBuffer.fromUint8List(cache!.$1); + return decode(buffer); + } + } catch (e) { + _log.warning("Failed to get cache for ${url.toString()}: $e"); + } + } + + final response = await dio.getUri( + url, + options: Options(headers: headers, responseType: ResponseType.bytes), + onReceiveProgress: (count, total) { + chunkEvents.add(ImageChunkEvent( + cumulativeBytesLoaded: count, + expectedTotalBytes: total >= 0 ? total : null, + )); + }, + ); + + if (response.statusCode != 200) { + throw NetworkImageLoadException( + uri: url, + statusCode: response.statusCode!, + ); + } + + final bytes = Uint8List.fromList(response.data as List); + + if (bytes.lengthInBytes == 0) { + throw NetworkImageLoadException( + uri: url, + statusCode: response.statusCode!, + ); + } + + if (isImageCacheEnabled) { + try { + await imageCaches.putCache(url.toString(), bytes, + response.headers.map, response.realUri.toString()); + } catch (e) { + _log.warning("Failed to put cache for ${url.toString()}: $e"); + } + } + + final buffer = await ui.ImmutableBuffer.fromUint8List(bytes); + return decode(buffer); + } catch (e) { + // Depending on where the exception was thrown, the image cache may not + // have had a chance to track the key in the cache at all. + // Schedule a microtask to give the cache a chance to add the key. + scheduleMicrotask(() { + PaintingBinding.instance.imageCache.evict(key); + }); + rethrow; + } finally { + unawaited(chunkEvents.close()); + } + } + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is DioImage && other.url == url && other.scale == scale; + } + + @override + int get hashCode => Object.hash(url, scale); + + @override + String toString() => + '${objectRuntimeType(this, 'DioImage')}("$url", scale: $scale)'; +} diff --git a/lib/viewer/single.dart b/lib/viewer/single.dart index ef054e2..336438c 100644 --- a/lib/viewer/single.dart +++ b/lib/viewer/single.dart @@ -1,5 +1,4 @@ import 'package:dio/dio.dart'; -import 'package:dio_image_provider/dio_image_provider.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -16,6 +15,7 @@ import '../api/gallery.dart'; import '../components/fit_text.dart'; import '../globals.dart'; import '../platform/media_query.dart'; +import '../provider/dio_image_provider.dart'; final _log = Logger("SinglePageViewer"); diff --git a/pubspec.lock b/pubspec.lock index a0b589c..5c26585 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -242,14 +242,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" - dio_image_provider: - dependency: "direct main" - description: - name: dio_image_provider - sha256: d7937f2b33c52db70cbf6209cf6ef6a929e006afcadaebfb6b0dc2da80dce52b - url: "https://pub.dev" - source: hosted - version: "0.0.2" enum_flag: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 84e2449..99eb82b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,7 +12,6 @@ dependencies: cryptography_flutter: ^2.3.0 dio: ^5.3.2 dio_cookie_manager: ^3.1.0+1 - dio_image_provider: any enum_flag: ^1.0.2 event_listener: ^0.2.0 file: ^7.0.0