import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; import 'package:fluffychat/widgets/matrix.dart'; class MxcImage extends StatefulWidget { final Uri? uri; final Event? event; final double? width; final double? height; final BoxFit? fit; final bool isThumbnail; final bool animated; final Duration retryDuration; final ThumbnailMethod thumbnailMethod; final Widget Function(BuildContext context)? placeholder; const MxcImage({ this.uri, this.event, this.width, this.height, this.fit, this.placeholder, this.isThumbnail = true, this.animated = false, this.retryDuration = const Duration(seconds: 2), this.thumbnailMethod = ThumbnailMethod.scale, super.key, }); @override State createState() => _MxcImageState(); } class _MxcImageState extends State { Uint8List? _imageData; Future _load() async { final event = widget.event; if (event != null) { final data = await event.downloadAndDecryptAttachment( getThumbnail: widget.isThumbnail, ); if (data.detectFileType is MatrixImageFile) { if (!mounted) return; setState(() { _imageData = data.bytes; }); return; } } } void _tryLoad(_) async { if (_imageData != null || widget.event == null) { return; } try { await _load(); } catch (_) { if (!mounted) return; await Future.delayed(widget.retryDuration); _tryLoad(_); } } @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback(_tryLoad); } Widget placeholder(BuildContext context) => widget.placeholder?.call(context) ?? Container( width: widget.width, height: widget.height, alignment: Alignment.center, child: const CircularProgressIndicator.adaptive(strokeWidth: 2), ); @override Widget build(BuildContext context) { final uri = widget.uri; if (uri != null) { final client = Matrix.of(context).client; final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; final width = widget.width; final realWidth = width == null ? null : width * devicePixelRatio; final height = widget.height; final realHeight = height == null ? null : height * devicePixelRatio; final httpUri = widget.isThumbnail ? uri.getThumbnail( client, width: realWidth, height: realHeight, animated: widget.animated, method: widget.thumbnailMethod, ) : uri.getDownloadLink(client); return CachedNetworkImage( imageUrl: httpUri.toString(), width: width, height: height, fit: widget.fit, placeholder: (context, _) => placeholder(context), errorWidget: (context, _, __) => placeholder(context), ); } final data = _imageData; final hasData = data != null && data.isNotEmpty; return AnimatedCrossFade( crossFadeState: hasData ? CrossFadeState.showSecond : CrossFadeState.showFirst, duration: FluffyThemes.animationDuration, firstChild: placeholder(context), secondChild: hasData ? Image.memory( data, width: widget.width, height: widget.height, fit: widget.fit, filterQuality: widget.isThumbnail ? FilterQuality.low : FilterQuality.medium, errorBuilder: (context, __, ___) { _imageData = null; WidgetsBinding.instance.addPostFrameCallback(_tryLoad); return placeholder(context); }, ) : SizedBox( width: widget.width, height: widget.height, ), ); } }