perf(mobile): remove small thumbnail and cache generated thumbnails (#17792)

* Remove small thumbnail and cache generated thumbnails

* Creating the small thumbnails takes quite some time, which should not be underestimated.
* The time needed to generate the small or big thumbnail is not too different from each other. Therefore there is no real benefit of the small thumbnail and it only adds frustration to the end user experience. That is because the image appeared to have loaded (the visual move from blur to something better) but it's still so bad that it is basically a blur. The better solution is therefore to stay at the blur until the actual thumbnail has loaded.
* Additionaly to the faster generation of the thumbnail, it now also gets cached similarly to the remote thumbnail which already gets cached. This further speeds up the all over usage of the app and prevents a repeatet thumbnail generation when opening the app.
* Decreased the quality from the default 95 to 80 to provide similar quality with much reduces thumbnail size.
* Use try catch around the read of the cache file.
* Use the key provided in the loadImage method instead of the asset of the constructor.

* Use userId instead of ownerId

* Remove import

* Add checksum to thumbnail cache key
This commit is contained in:
Toni 2025-04-23 17:31:35 +02:00 committed by GitHub
parent 19746a8685
commit 59fa8fbd0e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 45 additions and 19 deletions

View File

@ -2,11 +2,14 @@ import 'dart:async';
import 'dart:ui' as ui;
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:immich_mobile/providers/image/cache/thumbnail_image_cache_manager.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:photo_manager/photo_manager.dart' show ThumbnailSize;
import 'package:logging/logging.dart';
/// The local image provider for an asset
/// Only viable
@ -15,11 +18,16 @@ class ImmichLocalThumbnailProvider
final Asset asset;
final int height;
final int width;
final CacheManager? cacheManager;
final Logger log = Logger("ImmichLocalThumbnailProvider");
final String? userId;
ImmichLocalThumbnailProvider({
required this.asset,
this.height = 256,
this.width = 256,
this.cacheManager,
this.userId,
}) : assert(asset.local != null, 'Only usable when asset.local is set');
/// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key
@ -36,11 +44,10 @@ class ImmichLocalThumbnailProvider
ImmichLocalThumbnailProvider key,
ImageDecoderCallback decode,
) {
final chunkEvents = StreamController<ImageChunkEvent>();
final cache = cacheManager ?? ThumbnailImageCacheManager();
return MultiImageStreamCompleter(
codec: _codec(key.asset, decode, chunkEvents),
codec: _codec(key.asset, cache, decode),
scale: 1.0,
chunkEvents: chunkEvents.stream,
informationCollector: () sync* {
yield ErrorDescription(key.asset.fileName);
},
@ -50,25 +57,38 @@ class ImmichLocalThumbnailProvider
// Streams in each stage of the image as we ask for it
Stream<ui.Codec> _codec(
Asset assetData,
CacheManager cache,
ImageDecoderCallback decode,
StreamController<ImageChunkEvent> chunkEvents,
) async* {
final thumbBytes = await assetData.local
?.thumbnailDataWithSize(ThumbnailSize(width, height));
if (thumbBytes == null) {
chunkEvents.close();
final cacheKey =
'$userId${assetData.localId}${assetData.checksum}$width$height';
final fileFromCache = await cache.getFileFromCache(cacheKey);
if (fileFromCache != null) {
try {
final buffer =
await ui.ImmutableBuffer.fromFilePath(fileFromCache.file.path);
final codec = await decode(buffer);
yield codec;
return;
} catch (error) {
log.severe('Found thumbnail in cache, but loading it failed', error);
}
}
final thumbnailBytes = await assetData.local?.thumbnailDataWithSize(
ThumbnailSize(width, height),
quality: 80,
);
if (thumbnailBytes == null) {
throw StateError(
"Loading thumb for local photo ${asset.fileName} failed",
"Loading thumb for local photo ${assetData.fileName} failed",
);
}
try {
final buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes);
final codec = await decode(buffer);
yield codec;
} finally {
chunkEvents.close();
}
final buffer = await ui.ImmutableBuffer.fromUint8List(thumbnailBytes);
final codec = await decode(buffer);
yield codec;
await cache.putFile(cacheKey, thumbnailBytes);
}
@override

View File

@ -1,7 +1,7 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/image/immich_local_thumbnail_provider.dart';
import 'package:immich_mobile/providers/image/immich_remote_thumbnail_provider.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
@ -9,8 +9,9 @@ import 'package:immich_mobile/utils/hooks/blurhash_hook.dart';
import 'package:immich_mobile/widgets/common/immich_image.dart';
import 'package:immich_mobile/widgets/common/thumbhash_placeholder.dart';
import 'package:octo_image/octo_image.dart';
import 'package:immich_mobile/providers/user.provider.dart';
class ImmichThumbnail extends HookWidget {
class ImmichThumbnail extends HookConsumerWidget {
const ImmichThumbnail({
this.asset,
this.width = 250,
@ -31,6 +32,7 @@ class ImmichThumbnail extends HookWidget {
static ImageProvider imageProvider({
Asset? asset,
String? assetId,
String? userId,
int thumbnailSize = 256,
}) {
if (asset == null && assetId == null) {
@ -48,6 +50,7 @@ class ImmichThumbnail extends HookWidget {
asset: asset,
height: thumbnailSize,
width: thumbnailSize,
userId: userId,
);
} else {
return ImmichRemoteThumbnailProvider(
@ -59,8 +62,10 @@ class ImmichThumbnail extends HookWidget {
}
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
Uint8List? blurhash = useBlurHashRef(asset).value;
final userId = ref.watch(currentUserProvider)?.id;
if (asset == null) {
return Container(
color: Colors.grey,
@ -79,6 +84,7 @@ class ImmichThumbnail extends HookWidget {
octoSet: blurHashOrPlaceholder(blurhash),
image: ImmichThumbnail.imageProvider(
asset: asset,
userId: userId,
),
width: width,
height: height,