You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			179 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			Dart
		
	
			
		
		
	
	
			179 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			Dart
		
	
import 'dart:io';
 | 
						|
import 'dart:math';
 | 
						|
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<MxcImage> createState() => _MxcImageState();
 | 
						|
}
 | 
						|
 | 
						|
class _MxcImageState extends State<MxcImage> {
 | 
						|
  static final Map<String, Uint8List> _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<void> _load() async {
 | 
						|
    if (!mounted) return;
 | 
						|
    final client =
 | 
						|
        widget.client ?? widget.event?.room.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 || widget.isThumbnail) {
 | 
						|
        if (!mounted) return;
 | 
						|
        setState(() {
 | 
						|
          _imageData = data.bytes;
 | 
						|
        });
 | 
						|
        return;
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  void _tryLoad() async {
 | 
						|
    if (_imageData != null) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    try {
 | 
						|
      await _load();
 | 
						|
    } on IOException 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: const Duration(milliseconds: 128),
 | 
						|
      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, e, s) {
 | 
						|
                Logs().d('Unable to render mxc image', e, s);
 | 
						|
                return SizedBox(
 | 
						|
                  width: widget.width,
 | 
						|
                  height: widget.height,
 | 
						|
                  child: Material(
 | 
						|
                    color: Theme.of(context).colorScheme.surfaceContainer,
 | 
						|
                    child: Icon(
 | 
						|
                      Icons.broken_image_outlined,
 | 
						|
                      size: min(widget.height ?? 64, 64),
 | 
						|
                      color: Theme.of(context).colorScheme.onSurface,
 | 
						|
                    ),
 | 
						|
                  ),
 | 
						|
                );
 | 
						|
              },
 | 
						|
            )
 | 
						|
          : SizedBox(
 | 
						|
              width: widget.width,
 | 
						|
              height: widget.height,
 | 
						|
            ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 |