From 9dff520585091b528462a9bfd62c24bb1c49e1c6 Mon Sep 17 00:00:00 2001 From: mertalev <101130780+mertalev@users.noreply.github.com> Date: Tue, 8 Jul 2025 16:17:03 +0300 Subject: [PATCH] thumbhash improvements --- .../widgets/images/thumb_hash_provider.dart | 39 --------------- .../widgets/images/thumbnail.widget.dart | 9 +--- .../widgets/memory/memory_card.widget.dart | 49 +++++++++--------- mobile/lib/utils/hooks/blurhash_hook.dart | 26 ---------- .../lib/widgets/common/immich_thumbnail.dart | 11 ++-- mobile/lib/widgets/common/thumbhash.dart | 23 +++++++++ .../widgets/common/thumbhash_placeholder.dart | 14 +++--- mobile/lib/widgets/memories/memory_card.dart | 50 ++++++++++--------- 8 files changed, 88 insertions(+), 133 deletions(-) delete mode 100644 mobile/lib/presentation/widgets/images/thumb_hash_provider.dart delete mode 100644 mobile/lib/utils/hooks/blurhash_hook.dart create mode 100644 mobile/lib/widgets/common/thumbhash.dart diff --git a/mobile/lib/presentation/widgets/images/thumb_hash_provider.dart b/mobile/lib/presentation/widgets/images/thumb_hash_provider.dart deleted file mode 100644 index 8d292523d7..0000000000 --- a/mobile/lib/presentation/widgets/images/thumb_hash_provider.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'dart:convert' hide Codec; -import 'dart:ui'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/rendering.dart'; -import 'package:thumbhash/thumbhash.dart'; - -class ThumbHashProvider extends ImageProvider { - final String thumbHash; - - const ThumbHashProvider({required this.thumbHash}); - - @override - Future obtainKey(ImageConfiguration configuration) { - return SynchronousFuture(this); - } - - @override - ImageStreamCompleter loadImage(ThumbHashProvider key, ImageDecoderCallback decode) { - return MultiFrameImageStreamCompleter(codec: _loadCodec(key, decode), scale: 1.0); - } - - Future _loadCodec(ThumbHashProvider key, ImageDecoderCallback decode) async { - final image = thumbHashToRGBA(base64Decode(key.thumbHash)); - return decode(await ImmutableBuffer.fromUint8List(rgbaToBmp(image))); - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is ThumbHashProvider) { - return thumbHash == other.thumbHash; - } - return false; - } - - @override - int get hashCode => thumbHash.hashCode; -} diff --git a/mobile/lib/presentation/widgets/images/thumbnail.widget.dart b/mobile/lib/presentation/widgets/images/thumbnail.widget.dart index 8335bd406b..b44b92b223 100644 --- a/mobile/lib/presentation/widgets/images/thumbnail.widget.dart +++ b/mobile/lib/presentation/widgets/images/thumbnail.widget.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; -import 'package:immich_mobile/presentation/widgets/images/thumb_hash_provider.dart'; import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; -import 'package:immich_mobile/widgets/common/fade_in_placeholder_image.dart'; +import 'package:immich_mobile/widgets/common/thumbhash.dart'; import 'package:logging/logging.dart'; import 'package:octo_image/octo_image.dart'; @@ -40,11 +39,7 @@ class Thumbnail extends StatelessWidget { OctoPlaceholderBuilder _blurHashPlaceholderBuilder(String? thumbHash, {BoxFit? fit}) { return (context) => thumbHash == null ? const ThumbnailPlaceholder() - : FadeInPlaceholderImage( - placeholder: const ThumbnailPlaceholder(), - image: ThumbHashProvider(thumbHash: thumbHash), - fit: fit ?? BoxFit.cover, - ); + : Thumbhash(blurhash: thumbHash, fit: fit ?? BoxFit.cover); } OctoErrorBuilder _blurHashErrorBuilder(String? blurhash, {BaseAsset? asset, ImageProvider? provider, BoxFit? fit}) => diff --git a/mobile/lib/presentation/widgets/memory/memory_card.widget.dart b/mobile/lib/presentation/widgets/memory/memory_card.widget.dart index eaed60b204..0073c42dab 100644 --- a/mobile/lib/presentation/widgets/memory/memory_card.widget.dart +++ b/mobile/lib/presentation/widgets/memory/memory_card.widget.dart @@ -7,7 +7,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.widget.dart'; import 'package:immich_mobile/presentation/widgets/images/full_image.widget.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; -import 'package:immich_mobile/utils/hooks/blurhash_hook.dart'; +import 'package:immich_mobile/widgets/common/thumbhash.dart'; class DriftMemoryCard extends StatelessWidget { final RemoteAsset asset; @@ -88,31 +88,34 @@ class _BlurredBackdrop extends HookWidget { @override Widget build(BuildContext context) { - final blurhash = useDriftBlurHashRef(asset).value; + final blurhash = asset.thumbHash; if (blurhash != null) { // Use a nice cheap blur hash image decoration - return Container( - decoration: BoxDecoration( - image: DecorationImage(image: MemoryImage(blurhash), fit: BoxFit.cover), - ), - child: Container(color: Colors.black.withValues(alpha: 0.2)), - ); - } else { - // Fall back to using a more expensive image filtered - // Since the ImmichImage is already precached, we can - // safely use that as the image provider - return ImageFiltered( - imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), - child: Container( - decoration: BoxDecoration( - image: DecorationImage( - image: getFullImageProvider(asset, size: Size(context.width, context.height)), - fit: BoxFit.cover, - ), - ), - child: Container(color: Colors.black.withValues(alpha: 0.2)), - ), + return Stack( + children: [ + const ColoredBox(color: Color.fromRGBO(0, 0, 0, 0.2)), + Thumbhash(blurhash: blurhash, fit: BoxFit.cover), + ], ); } + + // Fall back to using a more expensive image filtered + // Since the ImmichImage is already precached, we can + // safely use that as the image provider + return ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), + child: DecoratedBox( + decoration: BoxDecoration( + image: DecorationImage( + image: getFullImageProvider( + asset, + size: Size(context.width, context.height), + ), + fit: BoxFit.cover, + ), + ), + child: const ColoredBox(color: Color.fromRGBO(0, 0, 0, 0.2)), + ), + ); } } diff --git a/mobile/lib/utils/hooks/blurhash_hook.dart b/mobile/lib/utils/hooks/blurhash_hook.dart deleted file mode 100644 index ac5fd31724..0000000000 --- a/mobile/lib/utils/hooks/blurhash_hook.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:thumbhash/thumbhash.dart' as thumbhash; - -ObjectRef useBlurHashRef(Asset? asset) { - if (asset?.thumbhash == null) { - return useRef(null); - } - - final rbga = thumbhash.thumbHashToRGBA(base64Decode(asset!.thumbhash!)); - - return useRef(thumbhash.rgbaToBmp(rbga)); -} - -ObjectRef useDriftBlurHashRef(RemoteAsset? asset) { - if (asset?.thumbHash == null) { - return useRef(null); - } - - final rbga = thumbhash.thumbHashToRGBA(base64Decode(asset!.thumbHash!)); - - return useRef(thumbhash.rgbaToBmp(rbga)); -} diff --git a/mobile/lib/widgets/common/immich_thumbnail.dart b/mobile/lib/widgets/common/immich_thumbnail.dart index 612a6a4bd0..101ac107e0 100644 --- a/mobile/lib/widgets/common/immich_thumbnail.dart +++ b/mobile/lib/widgets/common/immich_thumbnail.dart @@ -1,11 +1,8 @@ -import 'dart:typed_data'; - import 'package:flutter/material.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'; -import 'package:immich_mobile/utils/hooks/blurhash_hook.dart'; import 'package:immich_mobile/utils/thumbnail_utils.dart'; import 'package:immich_mobile/widgets/common/immich_image.dart'; import 'package:immich_mobile/widgets/common/thumbhash_placeholder.dart'; @@ -42,7 +39,6 @@ class ImmichThumbnail extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - Uint8List? blurhash = useBlurHashRef(asset).value; final userId = ref.watch(currentUserProvider)?.id; if (asset == null) { @@ -54,14 +50,14 @@ class ImmichThumbnail extends HookConsumerWidget { ); } - final assetAltText = getAltText(asset!.exifInfo, asset!.fileCreatedAt, asset!.type, []); + final assetAltText = getAltText(asset!.exifInfo, asset!.fileCreatedAt, asset!.type, const []); final thumbnailProviderInstance = ImmichThumbnail.imageProvider(asset: asset, userId: userId); customErrorBuilder(BuildContext ctx, Object error, StackTrace? stackTrace) { thumbnailProviderInstance.evict(); - final originalErrorWidgetBuilder = blurHashErrorBuilder(blurhash, fit: fit); + final originalErrorWidgetBuilder = blurHashErrorBuilder(asset?.thumbhash, fit: fit); return originalErrorWidgetBuilder(ctx, error, stackTrace); } @@ -72,7 +68,8 @@ class ImmichThumbnail extends HookConsumerWidget { fadeInDuration: Duration.zero, fadeOutDuration: const Duration(milliseconds: 100), octoSet: OctoSet( - placeholderBuilder: blurHashPlaceholderBuilder(blurhash, fit: fit), + placeholderBuilder: + blurHashPlaceholderBuilder(asset?.thumbhash, fit: fit), errorBuilder: customErrorBuilder, ), image: thumbnailProviderInstance, diff --git a/mobile/lib/widgets/common/thumbhash.dart b/mobile/lib/widgets/common/thumbhash.dart new file mode 100644 index 0000000000..81ba62db07 --- /dev/null +++ b/mobile/lib/widgets/common/thumbhash.dart @@ -0,0 +1,23 @@ +import 'dart:convert'; + +import 'package:flutter/widgets.dart'; +import 'package:thumbhash/thumbhash.dart' as thumbhash; + +class Thumbhash extends StatelessWidget { + final String blurhash; + final BoxFit fit; + + const Thumbhash({ + required this.blurhash, + this.fit = BoxFit.cover, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Image.memory( + thumbhash.rgbaToBmp(thumbhash.thumbHashToRGBA(base64.decode(blurhash))), + fit: fit, + ); + } +} diff --git a/mobile/lib/widgets/common/thumbhash_placeholder.dart b/mobile/lib/widgets/common/thumbhash_placeholder.dart index 0cb1222989..f3879fda9e 100644 --- a/mobile/lib/widgets/common/thumbhash_placeholder.dart +++ b/mobile/lib/widgets/common/thumbhash_placeholder.dart @@ -1,30 +1,28 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; -import 'package:immich_mobile/widgets/common/fade_in_placeholder_image.dart'; +import 'package:immich_mobile/widgets/common/thumbhash.dart'; import 'package:octo_image/octo_image.dart'; /// Simple set to show [OctoPlaceholder.circularProgressIndicator] as /// placeholder and [OctoError.icon] as error. -OctoSet blurHashOrPlaceholder(Uint8List? blurhash, {BoxFit? fit, Text? errorMessage}) { +OctoSet blurHashOrPlaceholder(String? blurhash, {BoxFit? fit, Text? errorMessage}) { return OctoSet( placeholderBuilder: blurHashPlaceholderBuilder(blurhash, fit: fit), errorBuilder: blurHashErrorBuilder(blurhash, fit: fit, message: errorMessage), ); } -OctoPlaceholderBuilder blurHashPlaceholderBuilder(Uint8List? blurhash, {BoxFit? fit}) { +OctoPlaceholderBuilder blurHashPlaceholderBuilder(String? blurhash, {BoxFit? fit}) { return (context) => blurhash == null ? const ThumbnailPlaceholder() - : FadeInPlaceholderImage( - placeholder: const ThumbnailPlaceholder(), - image: MemoryImage(blurhash), + : Thumbhash( + blurhash: blurhash, fit: fit ?? BoxFit.cover, ); } OctoErrorBuilder blurHashErrorBuilder( - Uint8List? blurhash, { + String? blurhash, { BoxFit? fit, Text? message, IconData? icon, diff --git a/mobile/lib/widgets/memories/memory_card.dart b/mobile/lib/widgets/memories/memory_card.dart index 189cc67428..6c45bb7529 100644 --- a/mobile/lib/widgets/memories/memory_card.dart +++ b/mobile/lib/widgets/memories/memory_card.dart @@ -5,8 +5,8 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/pages/common/native_video_viewer.page.dart'; -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.dart'; class MemoryCard extends StatelessWidget { final Asset asset; @@ -87,31 +87,35 @@ class _BlurredBackdrop extends HookWidget { @override Widget build(BuildContext context) { - final blurhash = useBlurHashRef(asset).value; + final blurhash = asset.thumbhash; if (blurhash != null) { // Use a nice cheap blur hash image decoration - return Container( - decoration: BoxDecoration( - image: DecorationImage(image: MemoryImage(blurhash), fit: BoxFit.cover), - ), - child: Container(color: Colors.black.withValues(alpha: 0.2)), - ); - } else { - // Fall back to using a more expensive image filtered - // Since the ImmichImage is already precached, we can - // safely use that as the image provider - return ImageFiltered( - imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), - child: Container( - decoration: BoxDecoration( - image: DecorationImage( - image: ImmichImage.imageProvider(asset: asset, height: context.height, width: context.width), - fit: BoxFit.cover, - ), - ), - child: Container(color: Colors.black.withValues(alpha: 0.2)), - ), + return Stack( + children: [ + const ColoredBox(color: Color.fromRGBO(0, 0, 0, 0.2)), + Thumbhash(blurhash: blurhash, fit: BoxFit.cover), + ], ); } + + // Fall back to using a more expensive image filtered + // Since the ImmichImage is already precached, we can + // safely use that as the image provider + return ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), + child: DecoratedBox( + decoration: BoxDecoration( + image: DecorationImage( + image: ImmichImage.imageProvider( + asset: asset, + height: context.height, + width: context.width, + ), + fit: BoxFit.cover, + ), + ), + child: const ColoredBox(color: Color.fromRGBO(0, 0, 0, 0.2)), + ), + ); } }