import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/utils/client_download_content_extension.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 Duration animationDuration; final Curve animationCurve; final ThumbnailMethod thumbnailMethod; final Widget Function(BuildContext context)? placeholder; final String? cacheKey; final Client? client; const MxcImage({ this.uri, this.event, this.width, this.height, this.fit, this.placeholder, this.isThumbnail = true, this.animated = false, this.animationDuration = FluffyThemes.animationDuration, this.retryDuration = const Duration(seconds: 2), this.animationCurve = FluffyThemes.animationCurve, this.thumbnailMethod = ThumbnailMethod.scale, this.cacheKey, this.client, super.key, }); @override State createState() => _MxcImageState(); } class _MxcImageState extends State { static final Map _imageDataCache = {}; Uint8List? _imageDataNoCache; Uint8List? get _imageData => widget.cacheKey == null ? _imageDataNoCache : _imageDataCache[widget.cacheKey]; set _imageData(Uint8List? data) { if (data == null) return; final cacheKey = widget.cacheKey; cacheKey == null ? _imageDataNoCache = data : _imageDataCache[cacheKey] = data; } Future _load() async { final client = widget.client ?? Matrix.of(context).client; final uri = widget.uri; final event = widget.event; if (uri != null) { 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 remoteData = await client.downloadMxcCached( uri, width: realWidth, height: realHeight, thumbnailMethod: widget.thumbnailMethod, isThumbnail: widget.isThumbnail, animated: widget.animated, ); if (!mounted) return; setState(() { _imageData = remoteData; }); } 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) { 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 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, ), ); } }