thumbhash render box

This commit is contained in:
mertalev 2025-07-10 16:41:53 +03:00
parent 9dff520585
commit 2a1e914245
No known key found for this signature in database
GPG Key ID: DF6ABC77AAD98C95
6 changed files with 183 additions and 56 deletions

View File

@ -26,7 +26,7 @@ const String kDownloadGroupLivePhoto = 'group_livephoto';
// Timeline constants // Timeline constants
const int kTimelineNoneSegmentSize = 120; const int kTimelineNoneSegmentSize = 120;
const int kTimelineAssetLoadBatchSize = 256; const int kTimelineAssetLoadBatchSize = 1024;
const int kTimelineAssetLoadOppositeSize = 64; const int kTimelineAssetLoadOppositeSize = 64;
// Widget keys // Widget keys

View File

@ -1,7 +1,6 @@
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/widgets/asset_grid/thumbnail_placeholder.dart';
import 'package:immich_mobile/widgets/common/thumbhash.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';
@ -37,9 +36,7 @@ class Thumbnail extends StatelessWidget {
} }
OctoPlaceholderBuilder _blurHashPlaceholderBuilder(String? thumbHash, {BoxFit? fit}) { OctoPlaceholderBuilder _blurHashPlaceholderBuilder(String? thumbHash, {BoxFit? fit}) {
return (context) => thumbHash == null return (context) => Thumbhash(blurhash: thumbHash, fit: fit ?? BoxFit.cover);
? const ThumbnailPlaceholder()
: Thumbhash(blurhash: 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

@ -91,12 +91,7 @@ class _BlurredBackdrop extends HookWidget {
final blurhash = asset.thumbHash; 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 Stack( return Thumbhash(blurhash: blurhash, fit: BoxFit.cover);
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 // Fall back to using a more expensive image filtered

View File

@ -1,30 +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)),
],
),
);
}
}

View File

@ -1,23 +1,194 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:ui' as ui;
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:thumbhash/thumbhash.dart' as thumbhash; import 'package:thumbhash/thumbhash.dart' as thumbhash;
class Thumbhash extends StatelessWidget { class ThumbhashImage extends RenderBox {
final String blurhash; 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 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({ const Thumbhash({
required this.blurhash, required this.blurhash,
this.fit = BoxFit.cover, this.fit = BoxFit.cover,
this.placeholderColor = const Color.fromRGBO(0, 0, 0, 0.2),
super.key, super.key,
}); });
@override
State<Thumbhash> createState() => _ThumbhashState();
}
class _ThumbhashState extends State<Thumbhash> {
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<void> _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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Image.memory( return ThumbhashLeaf(
thumbhash.rgbaToBmp(thumbhash.thumbHashToRGBA(base64.decode(blurhash))), image: _image,
fit: fit, fit: fit!,
placeholderColor: placeholderColor!,
); );
} }
@override
void dispose() {
_image?.dispose();
super.dispose();
}
} }

View File

@ -1,29 +1,23 @@
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/common/thumbhash.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(String? blurhash, {BoxFit? fit, Text? errorMessage}) { OctoSet blurHashOrPlaceholder(String? blurhash, {BoxFit fit = BoxFit.cover, 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(String? blurhash, {BoxFit? fit}) { OctoPlaceholderBuilder blurHashPlaceholderBuilder(String? blurhash, {required BoxFit fit}) {
return (context) => blurhash == null return (context) => Thumbhash(blurhash: blurhash, fit: fit);
? const ThumbnailPlaceholder()
: Thumbhash(
blurhash: blurhash,
fit: fit ?? BoxFit.cover,
);
} }
OctoErrorBuilder blurHashErrorBuilder( OctoErrorBuilder blurHashErrorBuilder(
String? blurhash, { String? blurhash, {
BoxFit? fit, BoxFit fit = BoxFit.cover,
Text? message, Text? message,
IconData? icon, IconData? icon,
Color? iconColor, Color? iconColor,