wip: Sicherungs-Commit aller Änderungen seit April + Arbeitsstruktur (CLAUDE.md, ROADMAP.md, PROGRESS.md, Autopilot)
6 Wochen uncommittete Arbeit (Voice-Channels, LiveKit-Manager, Settings-Modal u.v.m.) als ein WIP-Commit gesichert, damit nichts verloren geht und der Pi den aktuellen Stand klonen kann. Thematische Aufarbeitung: siehe ROADMAP M0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CPrAGBxBT6GfPXzeWQ4AXb
This commit is contained in:
+184
-32
@@ -1,53 +1,205 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:pyramid/core/matrix_client.dart';
|
||||
import 'package:pyramid/core/media_cache.dart';
|
||||
|
||||
class MxcImage extends ConsumerWidget {
|
||||
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<MxcAvatar> createState() => _MxcAvatarState();
|
||||
}
|
||||
|
||||
class _MxcAvatarState extends State<MxcAvatar> {
|
||||
late Future<Uint8List?> _future;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_future = _load();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(MxcAvatar old) {
|
||||
super.didUpdateWidget(old);
|
||||
if (old.mxcUri != widget.mxcUri) _future = _load();
|
||||
}
|
||||
|
||||
Future<Uint8List?> _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<Uint8List?>(
|
||||
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 Function(BuildContext)? placeholder;
|
||||
final Widget? placeholder;
|
||||
|
||||
const MxcImage({
|
||||
super.key,
|
||||
required this.mxcUri,
|
||||
required this.client,
|
||||
this.fit = BoxFit.cover,
|
||||
this.width,
|
||||
this.height,
|
||||
this.placeholder,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final clientAsync = ref.watch(matrixClientProvider);
|
||||
final uri = mxcUri;
|
||||
State<MxcImage> createState() => _MxcImageState();
|
||||
}
|
||||
|
||||
if (uri == null) return _fallback(context);
|
||||
class _MxcImageState extends State<MxcImage> {
|
||||
late Future<Uint8List?> _future;
|
||||
|
||||
return clientAsync.when(
|
||||
loading: () => _fallback(context),
|
||||
error: (e, st) => _fallback(context),
|
||||
data: (client) => FutureBuilder<Uri>(
|
||||
future: uri.getThumbnailUri(
|
||||
client,
|
||||
width: width != null ? (width! * 2).toInt() : 64,
|
||||
height: height != null ? (height! * 2).toInt() : 64,
|
||||
method: ThumbnailMethod.crop,
|
||||
),
|
||||
builder: (context, snap) {
|
||||
if (!snap.hasData) return _fallback(context);
|
||||
return Image.network(
|
||||
snap.data!.toString(),
|
||||
width: width,
|
||||
height: height,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (ctx, err, st) => _fallback(context),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_future = _load();
|
||||
}
|
||||
|
||||
Widget _fallback(BuildContext context) =>
|
||||
placeholder?.call(context) ?? SizedBox(width: width, height: height);
|
||||
@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<Uint8List?> _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<Uint8List?>(
|
||||
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();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user