diff --git a/mobile/lib/constants/constants.dart b/mobile/lib/constants/constants.dart index 6d98152efc..9258ae47db 100644 --- a/mobile/lib/constants/constants.dart +++ b/mobile/lib/constants/constants.dart @@ -18,7 +18,7 @@ const String kSecuredPinCode = "secured_pin_code"; // Timeline constants const int kTimelineNoneSegmentSize = 120; -const int kTimelineAssetLoadBatchSize = 256; +const int kTimelineAssetLoadBatchSize = 1024; const int kTimelineAssetLoadOppositeSize = 64; // Widget keys diff --git a/mobile/lib/presentation/widgets/images/thumbnail.widget.dart b/mobile/lib/presentation/widgets/images/thumbnail.widget.dart index b6d98185a3..a6dfdb657d 100644 --- a/mobile/lib/presentation/widgets/images/thumbnail.widget.dart +++ b/mobile/lib/presentation/widgets/images/thumbnail.widget.dart @@ -1,7 +1,6 @@ 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/widgets/asset_grid/thumbnail_placeholder.dart'; import 'package:immich_mobile/widgets/common/thumbhash.dart'; import 'package:logging/logging.dart'; import 'package:octo_image/octo_image.dart'; @@ -55,9 +54,7 @@ OctoPlaceholderBuilder _blurHashPlaceholderBuilder( String? thumbHash, { BoxFit? fit, }) { - return (context) => thumbHash == null - ? const ThumbnailPlaceholder() - : Thumbhash(blurhash: thumbHash, fit: fit ?? BoxFit.cover); + return (context) => Thumbhash(blurhash: thumbHash, fit: fit ?? BoxFit.cover); } OctoErrorBuilder _blurHashErrorBuilder( diff --git a/mobile/lib/presentation/widgets/memory/memory_card.widget.dart b/mobile/lib/presentation/widgets/memory/memory_card.widget.dart index cb59edf165..efb7817aca 100644 --- a/mobile/lib/presentation/widgets/memory/memory_card.widget.dart +++ b/mobile/lib/presentation/widgets/memory/memory_card.widget.dart @@ -120,12 +120,7 @@ class _BlurredBackdrop extends HookWidget { final blurhash = asset.thumbHash; if (blurhash != null) { // Use a nice cheap blur hash image decoration - return Stack( - children: [ - const ColoredBox(color: Color.fromRGBO(0, 0, 0, 0.2)), - Thumbhash(blurhash: blurhash, fit: BoxFit.cover), - ], - ); + return Thumbhash(blurhash: blurhash, fit: BoxFit.cover); } // Fall back to using a more expensive image filtered diff --git a/mobile/lib/widgets/common/fade_in_placeholder_image.dart b/mobile/lib/widgets/common/fade_in_placeholder_image.dart deleted file mode 100644 index 2be32fa8ba..0000000000 --- a/mobile/lib/widgets/common/fade_in_placeholder_image.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/widgets/common/transparent_image.dart'; - -class FadeInPlaceholderImage extends StatelessWidget { - final Widget placeholder; - final ImageProvider image; - final Duration duration; - final BoxFit fit; - - const FadeInPlaceholderImage({ - super.key, - required this.placeholder, - required this.image, - this.duration = const Duration(milliseconds: 100), - this.fit = BoxFit.cover, - }); - - @override - Widget build(BuildContext context) { - return SizedBox.expand( - child: Stack( - fit: StackFit.expand, - children: [ - placeholder, - FadeInImage( - fadeInDuration: duration, - image: image, - fit: fit, - placeholder: MemoryImage(kTransparentImage), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/common/thumbhash.dart b/mobile/lib/widgets/common/thumbhash.dart index 81ba62db07..3b6d970742 100644 --- a/mobile/lib/widgets/common/thumbhash.dart +++ b/mobile/lib/widgets/common/thumbhash.dart @@ -1,23 +1,194 @@ +import 'dart:async'; import 'dart:convert'; +import 'dart:ui' as ui; +import 'dart:ui'; +import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:thumbhash/thumbhash.dart' as thumbhash; -class Thumbhash extends StatelessWidget { - final String blurhash; +class ThumbhashImage extends RenderBox { + Color _placeholderColor; + ui.Image? _image; + BoxFit _fit; + + ThumbhashImage({ + required ui.Image? image, + required BoxFit fit, + required Color placeholderColor, + }) : _image = image, + _fit = fit, + _placeholderColor = placeholderColor; + + @override + void paint(PaintingContext context, Offset offset) { + final image = _image; + final rect = offset & size; + if (image == null) { + final paint = Paint(); + paint.color = _placeholderColor; + context.canvas.drawRect(rect, paint); + return; + } + + paintImage( + canvas: context.canvas, + rect: rect, + image: image, + fit: _fit, + filterQuality: FilterQuality.low, + ); + } + + @override + void performLayout() { + size = constraints.biggest; + } + + set image(ui.Image? value) { + if (_image != value) { + _image = value; + markNeedsPaint(); + } + } + + set fit(BoxFit value) { + if (_fit != value) { + _fit = value; + markNeedsPaint(); + } + } + + set placeholderColor(Color value) { + if (_placeholderColor != value) { + _placeholderColor = value; + markNeedsPaint(); + } + } +} + +class ThumbhashLeaf extends LeafRenderObjectWidget { + final ui.Image? image; final BoxFit fit; + final Color placeholderColor; + + const ThumbhashLeaf({ + super.key, + required this.image, + required this.fit, + required this.placeholderColor, + }); + + @override + RenderObject createRenderObject(BuildContext context) { + return ThumbhashImage( + image: image, + fit: fit, + placeholderColor: placeholderColor, + ); + } + + @override + void updateRenderObject(BuildContext context, ThumbhashImage renderObject) { + renderObject.fit = fit; + renderObject.image = image; + renderObject.placeholderColor = placeholderColor; + } +} + +class Thumbhash extends StatefulWidget { + final String? blurhash; + final BoxFit fit; + final Color placeholderColor; const Thumbhash({ required this.blurhash, this.fit = BoxFit.cover, + this.placeholderColor = const Color.fromRGBO(0, 0, 0, 0.2), super.key, }); + @override + State createState() => _ThumbhashState(); +} + +class _ThumbhashState extends State { + String? blurhash; + BoxFit? fit; + ui.Image? _image; + Color? placeholderColor; + + @override + void initState() { + super.initState(); + final blurhash_ = blurhash = widget.blurhash; + fit = widget.fit; + placeholderColor = widget.placeholderColor; + if (blurhash_ == null) { + return; + } + final image = thumbhash.thumbHashToRGBA(base64.decode(blurhash_)); + _decode(image); + } + + Future _decode(thumbhash.Image image) async { + if (!mounted) { + return; + } + final buffer = await ImmutableBuffer.fromUint8List(image.rgba); + if (!mounted) { + buffer.dispose(); + return; + } + + final descriptor = ImageDescriptor.raw( + buffer, + width: image.width, + height: image.height, + pixelFormat: PixelFormat.rgba8888, + ); + if (!mounted) { + buffer.dispose(); + descriptor.dispose(); + return; + } + + final codec = await descriptor.instantiateCodec( + targetWidth: image.width, + targetHeight: image.height, + ); + if (!mounted) { + buffer.dispose(); + descriptor.dispose(); + codec.dispose(); + return; + } + + final frame = (await codec.getNextFrame()).image; + buffer.dispose(); + descriptor.dispose(); + codec.dispose(); + if (!mounted) { + frame.dispose(); + return; + } + setState(() { + _image = frame; + }); + } + @override Widget build(BuildContext context) { - return Image.memory( - thumbhash.rgbaToBmp(thumbhash.thumbHashToRGBA(base64.decode(blurhash))), - fit: fit, + return ThumbhashLeaf( + image: _image, + fit: fit!, + placeholderColor: placeholderColor!, ); } + + @override + void dispose() { + _image?.dispose(); + super.dispose(); + } } diff --git a/mobile/lib/widgets/common/thumbhash_placeholder.dart b/mobile/lib/widgets/common/thumbhash_placeholder.dart index 384f68e252..776857eee4 100644 --- a/mobile/lib/widgets/common/thumbhash_placeholder.dart +++ b/mobile/lib/widgets/common/thumbhash_placeholder.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; import 'package:immich_mobile/widgets/common/thumbhash.dart'; import 'package:octo_image/octo_image.dart'; @@ -7,7 +6,7 @@ import 'package:octo_image/octo_image.dart'; /// placeholder and [OctoError.icon] as error. OctoSet blurHashOrPlaceholder( String? blurhash, { - BoxFit? fit, + BoxFit fit = BoxFit.cover, Text? errorMessage, }) { return OctoSet( @@ -19,19 +18,14 @@ OctoSet blurHashOrPlaceholder( OctoPlaceholderBuilder blurHashPlaceholderBuilder( String? blurhash, { - BoxFit? fit, + required BoxFit fit, }) { - return (context) => blurhash == null - ? const ThumbnailPlaceholder() - : Thumbhash( - blurhash: blurhash, - fit: fit ?? BoxFit.cover, - ); + return (context) => Thumbhash(blurhash: blurhash, fit: fit); } OctoErrorBuilder blurHashErrorBuilder( String? blurhash, { - BoxFit? fit, + BoxFit fit = BoxFit.cover, Text? message, IconData? icon, Color? iconColor,