import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; import 'package:pyramid/core/media_cache.dart'; class MxcAvatar extends StatefulWidget { final Uri? mxcUri; final Client client; final double size; final BorderRadius? borderRadius; final Widget Function(BuildContext) placeholder; const MxcAvatar({ super.key, required this.mxcUri, required this.client, required this.size, required this.placeholder, this.borderRadius, }); @override State createState() => _MxcAvatarState(); } class _MxcAvatarState extends State { late Future _future; @override void initState() { super.initState(); _future = _load(); } @override void didUpdateWidget(MxcAvatar old) { super.didUpdateWidget(old); if (old.mxcUri != widget.mxcUri) _future = _load(); } Future _load() async { final mxc = widget.mxcUri; if (mxc == null) return null; final key = 'avatar:${mxc}_${widget.size.round()}'; // Memory → Disk → Netzwerk. Disk-Treffer machen Avatare nach einem // App-Neustart sofort sichtbar statt nachzuladen. final hit = await MediaCache.instance.getPersistent(key); if (hit != null) return hit; final headers = {'authorization': 'Bearer ${widget.client.accessToken}'}; try { // Try thumbnail first; fall back to full download if 404 (server // doesn't support on-the-fly thumbnailing, e.g. Continuwuity). final thumbUri = await mxc.getThumbnailUri( widget.client, width: widget.size.round(), height: widget.size.round(), method: ThumbnailMethod.crop, ); final thumbResp = await widget.client.httpClient.get(thumbUri, headers: headers); if (thumbResp.statusCode == 200) { await MediaCache.instance.putPersistent(key, thumbResp.bodyBytes); return thumbResp.bodyBytes; } // Thumbnail not available — download full image. final dlUri = await mxc.getDownloadUri(widget.client); final dlResp = await widget.client.httpClient.get(dlUri, headers: headers); if (dlResp.statusCode != 200) return null; await MediaCache.instance.putPersistent(key, dlResp.bodyBytes); return dlResp.bodyBytes; } catch (_) { return null; } } @override Widget build(BuildContext context) { return FutureBuilder( future: _future, builder: (context, snap) { if (snap.hasData && snap.data != null) { return ClipRRect( borderRadius: widget.borderRadius ?? BorderRadius.circular(widget.size / 2), child: Image.memory( snap.data!, width: widget.size, height: widget.size, fit: BoxFit.cover, errorBuilder: (_, _, _) => widget.placeholder(context), ), ); } return widget.placeholder(context); }, ); } } // Rectangular MXC image (no fixed size, fills parent) class MxcImage extends StatefulWidget { final String mxcUri; final Client client; final BoxFit fit; final double? width; final double? height; final Widget? placeholder; const MxcImage({ super.key, required this.mxcUri, required this.client, this.fit = BoxFit.cover, this.width, this.height, this.placeholder, }); @override State createState() => _MxcImageState(); } class _MxcImageState extends State { late Future _future; @override void initState() { super.initState(); _future = _load(); } @override void didUpdateWidget(MxcImage old) { super.didUpdateWidget(old); if (old.mxcUri != widget.mxcUri) { // Block-Body: die Closure darf KEIN Future zurückgeben, sonst wirft // Flutter "setState() callback returned a Future". setState(() { _future = _load(); }); } } Future _load() async { final key = 'img:${widget.mxcUri}'; final hit = await MediaCache.instance.getPersistent(key); if (hit != null) return hit; try { final uri = Uri.parse(widget.mxcUri); final headers = { 'authorization': 'Bearer ${widget.client.accessToken}', }; // Erst Thumbnail versuchen (spart Bandbreite, v.a. mobil) — Fallback // auf Volldownload für Server ohne Thumbnail-Support (Continuwuity). try { final thumbUri = await uri.getThumbnailUri( widget.client, width: 800, height: 600, method: ThumbnailMethod.scale, ); final res = await widget.client.httpClient.get(thumbUri, headers: headers); if (res.statusCode == 200) { await MediaCache.instance.putPersistent(key, res.bodyBytes); return res.bodyBytes; } } catch (_) {} final httpUri = await uri.getDownloadUri(widget.client); final res = await widget.client.httpClient.get(httpUri, headers: headers); if (res.statusCode != 200) return null; final bytes = res.bodyBytes; await MediaCache.instance.putPersistent(key, bytes); return bytes; } catch (_) { return null; } } @override Widget build(BuildContext context) { return FutureBuilder( future: _future, builder: (_, snap) { if (snap.connectionState == ConnectionState.waiting) { return widget.placeholder ?? const SizedBox.shrink(); } if (snap.hasData && snap.data != null) { return Image.memory( snap.data!, fit: widget.fit, width: widget.width, height: widget.height, gaplessPlayback: true, errorBuilder: (_, _, _) => widget.placeholder ?? const SizedBox.shrink(), ); } return widget.placeholder ?? const SizedBox.shrink(); }, ); } }