thumbhash improvements

This commit is contained in:
mertalev 2025-07-08 16:17:03 +03:00
parent 6b50d958f4
commit 9dff520585
No known key found for this signature in database
GPG Key ID: DF6ABC77AAD98C95
8 changed files with 88 additions and 133 deletions

View File

@ -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<ThumbHashProvider> {
final String thumbHash;
const ThumbHashProvider({required this.thumbHash});
@override
Future<ThumbHashProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture(this);
}
@override
ImageStreamCompleter loadImage(ThumbHashProvider key, ImageDecoderCallback decode) {
return MultiFrameImageStreamCompleter(codec: _loadCodec(key, decode), scale: 1.0);
}
Future<Codec> _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;
}

View File

@ -1,9 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.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/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/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:logging/logging.dart';
import 'package:octo_image/octo_image.dart'; import 'package:octo_image/octo_image.dart';
@ -40,11 +39,7 @@ class Thumbnail extends StatelessWidget {
OctoPlaceholderBuilder _blurHashPlaceholderBuilder(String? thumbHash, {BoxFit? fit}) { OctoPlaceholderBuilder _blurHashPlaceholderBuilder(String? thumbHash, {BoxFit? fit}) {
return (context) => thumbHash == null return (context) => thumbHash == null
? const ThumbnailPlaceholder() ? const ThumbnailPlaceholder()
: FadeInPlaceholderImage( : Thumbhash(blurhash: thumbHash, fit: fit ?? BoxFit.cover);
placeholder: const ThumbnailPlaceholder(),
image: ThumbHashProvider(thumbHash: thumbHash),
fit: fit ?? BoxFit.cover,
);
} }
OctoErrorBuilder _blurHashErrorBuilder(String? blurhash, {BaseAsset? asset, ImageProvider? provider, BoxFit? fit}) => OctoErrorBuilder _blurHashErrorBuilder(String? blurhash, {BaseAsset? asset, ImageProvider? provider, BoxFit? fit}) =>

View File

@ -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/asset_viewer/video_viewer.widget.dart';
import 'package:immich_mobile/presentation/widgets/images/full_image.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/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 { class DriftMemoryCard extends StatelessWidget {
final RemoteAsset asset; final RemoteAsset asset;
@ -88,31 +88,34 @@ class _BlurredBackdrop extends HookWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final blurhash = useDriftBlurHashRef(asset).value; final blurhash = asset.thumbHash;
if (blurhash != null) { if (blurhash != null) {
// Use a nice cheap blur hash image decoration // Use a nice cheap blur hash image decoration
return Container( return Stack(
decoration: BoxDecoration( children: [
image: DecorationImage(image: MemoryImage(blurhash), fit: BoxFit.cover), const ColoredBox(color: Color.fromRGBO(0, 0, 0, 0.2)),
), Thumbhash(blurhash: 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)),
),
); );
} }
// 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)),
),
);
} }
} }

View File

@ -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<Uint8List?> useBlurHashRef(Asset? asset) {
if (asset?.thumbhash == null) {
return useRef(null);
}
final rbga = thumbhash.thumbHashToRGBA(base64Decode(asset!.thumbhash!));
return useRef(thumbhash.rgbaToBmp(rbga));
}
ObjectRef<Uint8List?> useDriftBlurHashRef(RemoteAsset? asset) {
if (asset?.thumbHash == null) {
return useRef(null);
}
final rbga = thumbhash.thumbHashToRGBA(base64Decode(asset!.thumbHash!));
return useRef(thumbhash.rgbaToBmp(rbga));
}

View File

@ -1,11 +1,8 @@
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.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_local_thumbnail_provider.dart';
import 'package:immich_mobile/providers/image/immich_remote_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/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/utils/thumbnail_utils.dart';
import 'package:immich_mobile/widgets/common/immich_image.dart'; import 'package:immich_mobile/widgets/common/immich_image.dart';
import 'package:immich_mobile/widgets/common/thumbhash_placeholder.dart'; import 'package:immich_mobile/widgets/common/thumbhash_placeholder.dart';
@ -42,7 +39,6 @@ class ImmichThumbnail extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
Uint8List? blurhash = useBlurHashRef(asset).value;
final userId = ref.watch(currentUserProvider)?.id; final userId = ref.watch(currentUserProvider)?.id;
if (asset == null) { 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); final thumbnailProviderInstance = ImmichThumbnail.imageProvider(asset: asset, userId: userId);
customErrorBuilder(BuildContext ctx, Object error, StackTrace? stackTrace) { customErrorBuilder(BuildContext ctx, Object error, StackTrace? stackTrace) {
thumbnailProviderInstance.evict(); thumbnailProviderInstance.evict();
final originalErrorWidgetBuilder = blurHashErrorBuilder(blurhash, fit: fit); final originalErrorWidgetBuilder = blurHashErrorBuilder(asset?.thumbhash, fit: fit);
return originalErrorWidgetBuilder(ctx, error, stackTrace); return originalErrorWidgetBuilder(ctx, error, stackTrace);
} }
@ -72,7 +68,8 @@ class ImmichThumbnail extends HookConsumerWidget {
fadeInDuration: Duration.zero, fadeInDuration: Duration.zero,
fadeOutDuration: const Duration(milliseconds: 100), fadeOutDuration: const Duration(milliseconds: 100),
octoSet: OctoSet( octoSet: OctoSet(
placeholderBuilder: blurHashPlaceholderBuilder(blurhash, fit: fit), placeholderBuilder:
blurHashPlaceholderBuilder(asset?.thumbhash, fit: fit),
errorBuilder: customErrorBuilder, errorBuilder: customErrorBuilder,
), ),
image: thumbnailProviderInstance, image: thumbnailProviderInstance,

View File

@ -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,
);
}
}

View File

@ -1,30 +1,28 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.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'; import 'package:octo_image/octo_image.dart';
/// Simple set to show [OctoPlaceholder.circularProgressIndicator] as /// Simple set to show [OctoPlaceholder.circularProgressIndicator] as
/// placeholder and [OctoError.icon] as error. /// placeholder and [OctoError.icon] as error.
OctoSet blurHashOrPlaceholder(Uint8List? blurhash, {BoxFit? fit, Text? errorMessage}) { OctoSet blurHashOrPlaceholder(String? blurhash, {BoxFit? fit, Text? errorMessage}) {
return OctoSet( return OctoSet(
placeholderBuilder: blurHashPlaceholderBuilder(blurhash, fit: fit), placeholderBuilder: blurHashPlaceholderBuilder(blurhash, fit: fit),
errorBuilder: blurHashErrorBuilder(blurhash, fit: fit, message: errorMessage), errorBuilder: blurHashErrorBuilder(blurhash, fit: fit, message: errorMessage),
); );
} }
OctoPlaceholderBuilder blurHashPlaceholderBuilder(Uint8List? blurhash, {BoxFit? fit}) { OctoPlaceholderBuilder blurHashPlaceholderBuilder(String? blurhash, {BoxFit? fit}) {
return (context) => blurhash == null return (context) => blurhash == null
? const ThumbnailPlaceholder() ? const ThumbnailPlaceholder()
: FadeInPlaceholderImage( : Thumbhash(
placeholder: const ThumbnailPlaceholder(), blurhash: blurhash,
image: MemoryImage(blurhash),
fit: fit ?? BoxFit.cover, fit: fit ?? BoxFit.cover,
); );
} }
OctoErrorBuilder blurHashErrorBuilder( OctoErrorBuilder blurHashErrorBuilder(
Uint8List? blurhash, { String? blurhash, {
BoxFit? fit, BoxFit? fit,
Text? message, Text? message,
IconData? icon, IconData? icon,

View File

@ -5,8 +5,8 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.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/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/immich_image.dart';
import 'package:immich_mobile/widgets/common/thumbhash.dart';
class MemoryCard extends StatelessWidget { class MemoryCard extends StatelessWidget {
final Asset asset; final Asset asset;
@ -87,31 +87,35 @@ class _BlurredBackdrop extends HookWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final blurhash = useBlurHashRef(asset).value; final blurhash = asset.thumbhash;
if (blurhash != null) { if (blurhash != null) {
// Use a nice cheap blur hash image decoration // Use a nice cheap blur hash image decoration
return Container( return Stack(
decoration: BoxDecoration( children: [
image: DecorationImage(image: MemoryImage(blurhash), fit: BoxFit.cover), const ColoredBox(color: Color.fromRGBO(0, 0, 0, 0.2)),
), Thumbhash(blurhash: 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)),
),
); );
} }
// 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)),
),
);
} }
} }