diff --git a/lib/foundation/image_provider/base_image_provider.dart b/lib/foundation/image_provider/base_image_provider.dart index f7c981b..15f41ab 100644 --- a/lib/foundation/image_provider/base_image_provider.dart +++ b/lib/foundation/image_provider/base_image_provider.dart @@ -1,5 +1,6 @@ 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'; @@ -10,6 +11,39 @@ abstract class BaseImageProvider> extends ImageProvider { 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(); @@ -45,19 +79,12 @@ abstract class BaseImageProvider> 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; - } + data = await load(chunkEvents); } catch (e) { - if(e.toString().contains("Invalid Status Code: 404")) { + if (e.toString().contains("Invalid Status Code: 404")) { rethrow; } - if(e.toString().contains("Invalid Status Code: 403")) { + if (e.toString().contains("Invalid Status Code: 403")) { rethrow; } if (e.toString().contains("handshake")) { @@ -73,23 +100,24 @@ abstract class BaseImageProvider> } } - if(stop) { + if (stop) { throw Exception("Image loading is stopped"); } - if(data!.isEmpty) { + if (data!.isEmpty) { throw Exception("Empty image data"); } try { final buffer = await ImmutableBuffer.fromUint8List(data); - return await decode(buffer); + 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); + var text = + const Utf8Codec(allowMalformed: false).decoder.convert(data); throw Exception("Expected image data, but got text: $text"); } catch (e) { // ignore @@ -107,30 +135,6 @@ abstract class BaseImageProvider> } } - static final _cache = {}; - - 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; diff --git a/lib/foundation/image_provider/reader_image.dart b/lib/foundation/image_provider/reader_image.dart index 6734acf..1686040 100644 --- a/lib/foundation/image_provider/reader_image.dart +++ b/lib/foundation/image_provider/reader_image.dart @@ -2,6 +2,7 @@ import 'dart:async' show Future, StreamController; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:venera/network/images.dart'; +import 'package:venera/utils/io.dart'; import 'base_image_provider.dart'; import 'reader_image.dart' as image_provider; @@ -20,6 +21,14 @@ class ReaderImageProvider @override Future load(StreamController chunkEvents) async { + if (imageKey.startsWith('file://')) { + var file = File(imageKey); + if (await file.exists()) { + return file.readAsBytes(); + } + throw "Error: File not found."; + } + await for (var event in ImageDownloader.loadComicImage(imageKey, sourceKey, cid, eid)) { chunkEvents.add(ImageChunkEvent( diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index f3eb839..9998775 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -162,19 +162,22 @@ class _GalleryModeState extends State<_GalleryMode> } else { int pageIndex = index - 1; int startIndex = pageIndex * reader.imagesPerPage; - int endIndex = math.min(startIndex + reader.imagesPerPage, reader.images!.length); - List pageImages = reader.images!.sublist(startIndex, endIndex); + int endIndex = math.min( + startIndex + reader.imagesPerPage, reader.images!.length); + List pageImages = + reader.images!.sublist(startIndex, endIndex); cached[index] = true; cache(index); photoViewControllers[index] = PhotoViewController(); - if(reader.imagesPerPage == 1) { + if (reader.imagesPerPage == 1) { return PhotoViewGalleryPageOptions( filterQuality: FilterQuality.medium, controller: photoViewControllers[index], - imageProvider: _createImageProviderFromKey(pageImages[0], context), + imageProvider: + _createImageProviderFromKey(pageImages[0], context), fit: BoxFit.contain, errorBuilder: (_, error, s, retry) { return NetworkError(message: error.toString(), retry: retry); @@ -645,32 +648,19 @@ class _ContinuousModeState extends State<_ContinuousMode> ImageProvider _createImageProviderFromKey( String imageKey, BuildContext context) { - if (imageKey.startsWith('file://')) { - return FileImage(File(imageKey.replaceFirst("file://", ''))); - } else { - var reader = context.reader; - return ReaderImageProvider( - imageKey, - reader.type.comicSource!.key, - reader.cid, - reader.eid, - ); - } + var reader = context.reader; + return ReaderImageProvider( + imageKey, + reader.type.comicSource!.key, + reader.cid, + reader.eid, + ); } ImageProvider _createImageProvider(int page, BuildContext context) { var reader = context.reader; var imageKey = reader.images![page - 1]; - if (imageKey.startsWith('file://')) { - return FileImage(File(imageKey.replaceFirst("file://", ''))); - } else { - return ReaderImageProvider( - imageKey, - reader.type.comicSource!.key, - reader.cid, - reader.eid, - ); - } + return _createImageProviderFromKey(imageKey, context); } void _precacheImage(int page, BuildContext context) { diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index 8f91306..636cd8b 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -9,6 +9,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_memory_info/flutter_memory_info.dart'; import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view_gallery.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; @@ -21,6 +22,7 @@ import 'package:venera/foundation/comic_type.dart'; import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/image_provider/reader_image.dart'; import 'package:venera/foundation/local.dart'; +import 'package:venera/foundation/log.dart'; import 'package:venera/pages/settings/settings_page.dart'; import 'package:venera/utils/data_sync.dart'; import 'package:venera/utils/file_type.dart'; @@ -142,9 +144,27 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { if(appdata.settings['enableTurnPageByVolumeKey']) { handleVolumeEvent(); } + setImageCacheSize(); super.initState(); } + void setImageCacheSize() async { + var availableRAM = await MemoryInfo.getFreePhysicalMemorySize(); + if (availableRAM == null) return; + int maxImageCacheSize; + if (availableRAM < 1 << 30) { + maxImageCacheSize = 100 << 20; + } else if (availableRAM < 2 << 30) { + maxImageCacheSize = 200 << 20; + } else if (availableRAM < 4 << 30) { + maxImageCacheSize = 300 << 20; + } else { + maxImageCacheSize = 500 << 20; + } + Log.info("Reader", "Detect available RAM: $availableRAM, set image cache size to $maxImageCacheSize"); + PaintingBinding.instance.imageCache.maximumSizeBytes = maxImageCacheSize; + } + @override void dispose() { autoPageTurningTimer?.cancel(); @@ -154,6 +174,7 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { Future.microtask(() { DataSync().onDataChanged(); }); + PaintingBinding.instance.imageCache.maximumSizeBytes = 100 << 20; super.dispose(); } diff --git a/pubspec.lock b/pubspec.lock index 4d57838..12b1e64 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -388,6 +388,15 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_memory_info: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: a401e3f96dca6ab0ab07b6b2ec0b649833f04c14 + url: "https://github.com/wgh136/flutter_memory_info" + source: git + version: "0.0.1" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -1070,10 +1079,10 @@ packages: dependency: transitive description: name: win32 - sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" + sha256: "8b338d4486ab3fbc0ba0db9f9b4f5239b6697fcee427939a40e720cbb9ee0a69" url: "https://pub.dev" source: hosted - version: "5.5.4" + version: "5.9.0" window_manager: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index e4da6a9..2027b20 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -72,6 +72,9 @@ dependencies: pdf: ^3.11.1 dynamic_color: ^1.7.0 shimmer: ^3.0.0 + flutter_memory_info: + git: + url: https://github.com/wgh136/flutter_memory_info dev_dependencies: flutter_test: