optimized avatar and linkify performance, refined navigation to preserve popups, implemented read marker caching, and added file size limits for saving, minor timetable details changes

This commit is contained in:
2026-05-10 16:40:39 +02:00
parent 1458d8ce49
commit a0bc46f522
12 changed files with 234 additions and 64 deletions
+20 -1
View File
@@ -254,7 +254,26 @@ class _FileViewerState extends State<FileViewer> {
break;
case FileViewingActions.save:
try {
final bytes = await File(widget.path).readAsBytes();
final source = File(widget.path);
final size = await source.length();
// Hard-cap to avoid loading the entire file into memory just to
// hand it back to the platform's saveFile dialog. The package
// currently has no streaming/path-based save path, so for big
// media the user has to fall back to "Teilen" → save-to-files.
// 200 MB peak is comfortable on modern mid-range devices and big
// enough for typical school videos.
const maxBytes = 200 * 1024 * 1024; // 200 MB
if (size > maxBytes) {
if (!mounted) return;
InfoDialog.show(
context,
'Diese Datei ist zu groß (${(size / (1024 * 1024)).toStringAsFixed(0)} MB), '
'um direkt gespeichert zu werden. Nutze stattdessen die Teilen-Funktion.',
title: 'Speichern nicht möglich',
);
return;
}
final bytes = await source.readAsBytes();
final saved = await FilePicker.saveFile(
fileName: widget.path.split('/').last,
bytes: bytes,
+39 -4
View File
@@ -1,3 +1,4 @@
import 'dart:collection';
import 'dart:convert';
import 'dart:typed_data';
@@ -29,11 +30,44 @@ class _AvatarPayload {
_AvatarPayload(this.bytes, this.isSvg);
}
class _AvatarCacheEntry {
final _AvatarPayload? payload;
final DateTime fetchedAt;
_AvatarCacheEntry(this.payload, this.fetchedAt);
}
// Cap keeps the heap bounded for power-users in Talk; TTL ensures
// server-side avatar updates become visible within a session without
// requiring an app restart. LinkedHashMap insertion-order plus a remove
// on hit gives us LRU eviction.
const int _kAvatarCacheMax = 256;
const Duration _kAvatarCacheTtl = Duration(minutes: 30);
// Resolved payloads are cached so re-mounts render synchronously; in-flight
// requests are deduped so concurrent mounts share one HTTP call.
final Map<String, _AvatarPayload?> _resolvedAvatars = {};
final LinkedHashMap<String, _AvatarCacheEntry> _resolvedAvatars =
LinkedHashMap<String, _AvatarCacheEntry>();
final Map<String, Future<_AvatarPayload?>> _pendingAvatars = {};
_AvatarCacheEntry? _readAvatarCache(String url) {
final entry = _resolvedAvatars.remove(url);
if (entry == null) return null;
if (DateTime.now().difference(entry.fetchedAt) > _kAvatarCacheTtl) {
return null;
}
// Re-insert at the tail so it counts as most-recently-used.
_resolvedAvatars[url] = entry;
return entry;
}
void _writeAvatarCache(String url, _AvatarPayload? payload) {
_resolvedAvatars.remove(url);
_resolvedAvatars[url] = _AvatarCacheEntry(payload, DateTime.now());
while (_resolvedAvatars.length > _kAvatarCacheMax) {
_resolvedAvatars.remove(_resolvedAvatars.keys.first);
}
}
class _UserAvatarState extends State<UserAvatar> {
_AvatarPayload? _payload;
@@ -63,14 +97,15 @@ class _UserAvatarState extends State<UserAvatar> {
void _attach() {
final url = _url();
if (_resolvedAvatars.containsKey(url)) {
_payload = _resolvedAvatars[url];
final cached = _readAvatarCache(url);
if (cached != null) {
_payload = cached.payload;
return;
}
_payload = null;
final pending = _pendingAvatars.putIfAbsent(url, () => _fetch(url));
pending.then((p) {
_resolvedAvatars[url] = p;
_writeAvatarCache(url, p);
_pendingAvatars.remove(url);
if (!mounted || _url() != url) return;
setState(() => _payload = p);