mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
thumbhash improvements
This commit is contained in:
parent
df4a27e8a7
commit
0dadfc52dd
@ -1,50 +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;
|
|
||||||
}
|
|
@ -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';
|
||||||
|
|
||||||
@ -58,11 +57,7 @@ OctoPlaceholderBuilder _blurHashPlaceholderBuilder(
|
|||||||
}) {
|
}) {
|
||||||
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(
|
OctoErrorBuilder _blurHashErrorBuilder(
|
||||||
|
@ -6,7 +6,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.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;
|
||||||
@ -117,43 +117,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(
|
||||||
|
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(
|
decoration: BoxDecoration(
|
||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
image: MemoryImage(
|
image: getFullImageProvider(
|
||||||
blurhash,
|
asset,
|
||||||
|
size: Size(context.width, context.height),
|
||||||
),
|
),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Container(
|
child: const ColoredBox(color: Color.fromRGBO(0, 0, 0, 0.2)),
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,30 +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));
|
|
||||||
}
|
|
@ -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';
|
||||||
@ -64,7 +61,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) {
|
||||||
@ -82,7 +78,7 @@ class ImmichThumbnail extends HookConsumerWidget {
|
|||||||
asset!.exifInfo,
|
asset!.exifInfo,
|
||||||
asset!.fileCreatedAt,
|
asset!.fileCreatedAt,
|
||||||
asset!.type,
|
asset!.type,
|
||||||
[],
|
const [],
|
||||||
);
|
);
|
||||||
|
|
||||||
final thumbnailProviderInstance = ImmichThumbnail.imageProvider(
|
final thumbnailProviderInstance = ImmichThumbnail.imageProvider(
|
||||||
@ -94,7 +90,7 @@ class ImmichThumbnail extends HookConsumerWidget {
|
|||||||
thumbnailProviderInstance.evict();
|
thumbnailProviderInstance.evict();
|
||||||
|
|
||||||
final originalErrorWidgetBuilder =
|
final originalErrorWidgetBuilder =
|
||||||
blurHashErrorBuilder(blurhash, fit: fit);
|
blurHashErrorBuilder(asset?.thumbhash, fit: fit);
|
||||||
return originalErrorWidgetBuilder(ctx, error, stackTrace);
|
return originalErrorWidgetBuilder(ctx, error, stackTrace);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,7 +101,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,
|
||||||
|
23
mobile/lib/widgets/common/thumbhash.dart
Normal file
23
mobile/lib/widgets/common/thumbhash.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,12 @@
|
|||||||
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(
|
OctoSet blurHashOrPlaceholder(
|
||||||
Uint8List? blurhash, {
|
String? blurhash, {
|
||||||
BoxFit? fit,
|
BoxFit? fit,
|
||||||
Text? errorMessage,
|
Text? errorMessage,
|
||||||
}) {
|
}) {
|
||||||
@ -19,20 +18,19 @@ OctoSet blurHashOrPlaceholder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
OctoPlaceholderBuilder blurHashPlaceholderBuilder(
|
OctoPlaceholderBuilder blurHashPlaceholderBuilder(
|
||||||
Uint8List? blurhash, {
|
String? blurhash, {
|
||||||
BoxFit? fit,
|
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,
|
||||||
|
@ -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;
|
||||||
@ -113,44 +113,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(
|
||||||
|
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(
|
decoration: BoxDecoration(
|
||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
image: MemoryImage(
|
image: ImmichImage.imageProvider(
|
||||||
blurhash,
|
asset: asset,
|
||||||
|
height: context.height,
|
||||||
|
width: context.width,
|
||||||
),
|
),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Container(
|
child: const ColoredBox(color: Color.fromRGBO(0, 0, 0, 0.2)),
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user