Add thumb queue

This commit is contained in:
Neeraj Gupta
2025-03-24 16:14:43 +05:30
parent da8edfd34e
commit 676bbb4d88
4 changed files with 58 additions and 23 deletions

View File

@@ -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<String>(
maxConcurrentTasks: 15,
taskTimeout: const Duration(minutes: 1),
maxQueueSize: 1000, // Limit the queue to 50 pending tasks
);
final mediumThumbnailQueue = TaskQueue<String>(
maxConcurrentTasks: 5,
taskTimeout: const Duration(minutes: 1),
maxQueueSize: 1000, // Limit the queue to 50 pending tasks
);
class LocalThumbnailProvider extends ImageProvider<LocalThumbnailProviderKey> {
final LocalThumbnailProviderKey key;
@@ -36,6 +49,11 @@ class LocalThumbnailProvider extends ImageProvider<LocalThumbnailProviderKey> {
);
}
static Future<void> cancelRequest(LocalThumbnailProviderKey key) async {
thumbnailQueue.removeTask('${key.asset.id}-small');
mediumThumbnailQueue.removeTask('${key.asset.id}-medium');
}
Stream<ui.Codec> _codec(
LocalThumbnailProviderKey key,
ImageDecoderCallback decode,
@@ -56,10 +74,16 @@ class LocalThumbnailProvider extends ImageProvider<LocalThumbnailProviderKey> {
Uint8List? thumbBytes =
enteImageCache.getThumbByID(asset.id, key.smallThumbWidth);
if (thumbBytes == null) {
thumbBytes = await asset.thumbnailDataWithSize(
ThumbnailSize(key.smallThumbWidth, key.smallThumbHeight),
quality: 75,
);
final Completer<Uint8List?> 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<LocalThumbnailProviderKey> {
}
if (normalThumbBytes == null) {
normalThumbBytes = await asset.thumbnailDataWithSize(
ThumbnailSize(key.width, key.height),
quality: 50,
);
final Completer<Uint8List?> 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<LocalThumbnailProviderKey> {
"$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();

View File

@@ -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<Uint8List?> _cached(LocalThumbnailProviderKey key) async {
return null;
}
}

View File

@@ -71,6 +71,7 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
ImageProvider? _imageProvider;
int? optimizedImageHeight;
int? optimizedImageWidth;
LocalThumbnailProviderKey? localImageProviderKey;
@override
void initState() {
@@ -81,7 +82,13 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
@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<ThumbnailWidget> {
).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;

View File

@@ -8,13 +8,16 @@ class _QueueItem<T> {
final Future<void> Function() task;
final Completer<void> completer;
DateTime lastUpdated;
int counter;
_QueueItem(this.id, this.task)
: lastUpdated = DateTime.now(),
counter = 1,
completer = Completer<void>();
void updateTimestamp() {
lastUpdated = DateTime.now();
counter++;
}
bool isTimedOut(Duration timeout) {
@@ -148,8 +151,11 @@ class TaskQueue<T> {
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'));