diff --git a/mobile/lib/image/provider/local_thumbnail_img.dart b/mobile/lib/image/provider/local_thumbnail_img.dart index 327a2fbab5..4d644e4ede 100644 --- a/mobile/lib/image/provider/local_thumbnail_img.dart +++ b/mobile/lib/image/provider/local_thumbnail_img.dart @@ -7,6 +7,19 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; import 'package:photo_manager/photo_manager.dart'; import "package:photos/image/in_memory_image_cache.dart"; +import "package:photos/utils/standalone/task_queue.dart"; + +final thumbnailQueue = TaskQueue( + maxConcurrentTasks: 15, + taskTimeout: const Duration(minutes: 1), + maxQueueSize: 1000, // Limit the queue to 50 pending tasks +); + +final mediumThumbnailQueue = TaskQueue( + maxConcurrentTasks: 5, + taskTimeout: const Duration(minutes: 1), + maxQueueSize: 1000, // Limit the queue to 50 pending tasks +); class LocalThumbnailProvider extends ImageProvider { final LocalThumbnailProviderKey key; @@ -36,6 +49,11 @@ class LocalThumbnailProvider extends ImageProvider { ); } + static Future cancelRequest(LocalThumbnailProviderKey key) async { + thumbnailQueue.removeTask('${key.asset.id}-small'); + mediumThumbnailQueue.removeTask('${key.asset.id}-medium'); + } + Stream _codec( LocalThumbnailProviderKey key, ImageDecoderCallback decode, @@ -56,10 +74,16 @@ class LocalThumbnailProvider extends ImageProvider { Uint8List? thumbBytes = enteImageCache.getThumbByID(asset.id, key.smallThumbWidth); if (thumbBytes == null) { - thumbBytes = await asset.thumbnailDataWithSize( - ThumbnailSize(key.smallThumbWidth, key.smallThumbHeight), - quality: 75, - ); + final Completer future = Completer(); + await thumbnailQueue.addTask('${asset.id}-small', () async { + final thumbBytes = await asset.thumbnailDataWithSize( + ThumbnailSize(key.smallThumbWidth, key.smallThumbHeight), + quality: 75, + ); + enteImageCache.putThumbByID(asset.id, thumbBytes, key.smallThumbWidth); + future.complete(thumbBytes); + }); + thumbBytes = await future.future; enteImageCache.putThumbByID(asset.id, thumbBytes, key.smallThumbWidth); } if (thumbBytes != null) { @@ -71,10 +95,16 @@ class LocalThumbnailProvider extends ImageProvider { } if (normalThumbBytes == null) { - normalThumbBytes = await asset.thumbnailDataWithSize( - ThumbnailSize(key.width, key.height), - quality: 50, - ); + final Completer future = Completer(); + await mediumThumbnailQueue.addTask('${asset.id}-medium', () async { + normalThumbBytes = await asset.thumbnailDataWithSize( + ThumbnailSize(key.width, key.height), + quality: 50, + ); + enteImageCache.putThumbByID(asset.id, normalThumbBytes, key.height); + future.complete(normalThumbBytes); + }); + normalThumbBytes = await future.future; enteImageCache.putThumbByID(asset.id, normalThumbBytes, key.height); } if (normalThumbBytes == null) { @@ -82,7 +112,7 @@ class LocalThumbnailProvider extends ImageProvider { "$runtimeType biThumb ${asset.title} failed", ); } - final buffer = await ui.ImmutableBuffer.fromUint8List(normalThumbBytes); + final buffer = await ui.ImmutableBuffer.fromUint8List(normalThumbBytes!); final codec = await decode(buffer); yield codec; chunkEvents.close().ignore(); diff --git a/mobile/lib/image/thumnail/local_thumb_service.dart b/mobile/lib/image/thumnail/local_thumb_service.dart index c8e150aeab..acd362b401 100644 --- a/mobile/lib/image/thumnail/local_thumb_service.dart +++ b/mobile/lib/image/thumnail/local_thumb_service.dart @@ -1,6 +1,3 @@ -import "dart:typed_data"; - -import "package:photos/image/provider/local_thumbnail_img.dart"; import "package:photos/utils/standalone/task_queue.dart"; class LocalThumbnailService { @@ -9,8 +6,4 @@ class LocalThumbnailService { taskTimeout: const Duration(minutes: 1), maxQueueSize: 100, // Limit the queue to 50 pending tasks ); - - Future _cached(LocalThumbnailProviderKey key) async { - return null; - } } diff --git a/mobile/lib/ui/viewer/file/thumbnail_widget.dart b/mobile/lib/ui/viewer/file/thumbnail_widget.dart index 037ea27558..d548b4f250 100644 --- a/mobile/lib/ui/viewer/file/thumbnail_widget.dart +++ b/mobile/lib/ui/viewer/file/thumbnail_widget.dart @@ -71,6 +71,7 @@ class _ThumbnailWidgetState extends State { ImageProvider? _imageProvider; int? optimizedImageHeight; int? optimizedImageWidth; + LocalThumbnailProviderKey? localImageProviderKey; @override void initState() { @@ -81,7 +82,13 @@ class _ThumbnailWidgetState extends State { @override void dispose() { super.dispose(); + Future.delayed(const Duration(milliseconds: 10), () { + if (!mounted) { + if (localImageProviderKey != null) { + LocalThumbnailProvider.cancelRequest(localImageProviderKey!); + } + } // Cancel request only if the widget has been unmounted if (!mounted && widget.file.isRemoteFile && !_hasLoadedThumbnail) { removePendingGetThumbnailRequestIfAny(widget.file); @@ -125,13 +132,12 @@ class _ThumbnailWidgetState extends State { ).image; _hasLoadedThumbnail = true; } else { - _imageProvider = LocalThumbnailProvider( - LocalThumbnailProviderKey( - asset: widget.file.asset!, - height: widget.thumbnailSize, - width: widget.thumbnailSize, - ), + localImageProviderKey = LocalThumbnailProviderKey( + asset: widget.file.asset!, + height: widget.thumbnailSize, + width: widget.thumbnailSize, ); + _imageProvider = LocalThumbnailProvider(localImageProviderKey!); } } Widget? image; diff --git a/mobile/lib/utils/standalone/task_queue.dart b/mobile/lib/utils/standalone/task_queue.dart index 5ce897b4b0..222c70f544 100644 --- a/mobile/lib/utils/standalone/task_queue.dart +++ b/mobile/lib/utils/standalone/task_queue.dart @@ -8,13 +8,16 @@ class _QueueItem { final Future Function() task; final Completer completer; DateTime lastUpdated; + int counter; _QueueItem(this.id, this.task) : lastUpdated = DateTime.now(), + counter = 1, completer = Completer(); void updateTimestamp() { lastUpdated = DateTime.now(); + counter++; } bool isTimedOut(Duration timeout) { @@ -148,8 +151,11 @@ class TaskQueue { if (_taskMap.containsKey(id)) { final item = _taskMap[id]!; + item.counter--; + if (item.counter > 0) { + return false; + } _priorityQueue.remove(item); - // Complete the future with a cancellation error if (!item.completer.isCompleted) { item.completer.completeError(Exception('Task $id was cancelled'));