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:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user