refactored internal documentation and simplified comments across chat BLoCs, file viewer, and navigation components

This commit is contained in:
2026-05-10 17:01:50 +02:00
parent a0bc46f522
commit 1a11b9ac60
8 changed files with 68 additions and 185 deletions
+14 -45
View File
@@ -26,9 +26,8 @@ class FileViewer extends StatefulWidget {
final String path;
final bool openExternal;
/// When set, enables the in-app actions "An Chat senden" and "In Dateien
/// speichern" — these need a server-side reference, not the local cache
/// path. Aufrufer reichen die Referenz durch (siehe AppRoutes.openFileViewer).
/// Enables in-app "An Chat senden" / "In Dateien speichern" — these
/// need a server-side reference instead of the local cache path.
final RemoteFileRef? remoteFile;
const FileViewer({
@@ -56,8 +55,6 @@ const Set<String> _imageExtensions = {
'wbmp',
};
/// Video container formats whose playback the platform decoders (ExoPlayer
/// on Android, AVPlayer on iOS) handle out of the box.
const Set<String> _videoExtensions = {
'mp4',
'm4v',
@@ -67,9 +64,8 @@ const Set<String> _videoExtensions = {
'3gp',
};
/// Audio formats playable through the same `video_player` pipeline. Some
/// (ogg/opus/flac) work on Android only — iOS will surface an init error
/// which we catch and surface as a friendly fallback.
/// ogg/opus/flac are Android-only; iOS init errors fall through to the
/// "format not supported" message.
const Set<String> _audioExtensions = {
'mp3',
'm4a',
@@ -81,9 +77,7 @@ const Set<String> _audioExtensions = {
'opus',
};
/// Extensions whose contents we render directly as plain text. Anything
/// outside this list still gets a content-based fallback check (see
/// [_looksLikeText]) so generic "what is this file" cases work too.
/// Unknown extensions still get a content sniff via [_looksLikeText].
const Set<String> _textExtensions = {
'txt', 'md', 'markdown', 'rst', 'log',
'json', 'json5', 'xml', 'yaml', 'yml', 'toml',
@@ -104,10 +98,7 @@ const Set<String> _textExtensions = {
'srt', 'vtt',
};
/// Reads up to 8 KB and decides whether the bytes look like UTF-8 text.
/// NUL bytes and non-decodable sequences disqualify the file. Used as a
/// fallback for unknown extensions so plain text files without a familiar
/// suffix still open in the in-app viewer.
/// 8 KB sniff: NUL bytes or non-UTF-8 sequences disqualify.
Future<bool> _looksLikeText(String path) async {
final file = File(path);
RandomAccessFile? raf;
@@ -126,10 +117,8 @@ Future<bool> _looksLikeText(String path) async {
}
}
/// Workaround for a Syncfusion PDF viewer race: SfPdfViewer's internal
/// LayoutBuilder calls `localToGlobal` during build, which asserts when an
/// ancestor RenderTransform (from the page-push animation) is still mid-layout.
/// We wait for the route's enter animation to complete before mounting it.
/// SfPdfViewer asserts on `localToGlobal` if mounted during the page-push
/// animation. Defer until the route enter animation completes.
class _DeferredPdfViewer extends StatefulWidget {
const _DeferredPdfViewer({required this.path});
final String path;
@@ -189,8 +178,6 @@ class _FileViewerState extends State<FileViewer> {
settings.val().fileViewSettings.alwaysOpenExternally ||
widget.openExternal;
if (openExternal) {
// Settings or popup explicitly chose "open externally" — fire and
// forget, then pop back. Same one-shot behaviour as the old viewer.
WidgetsBinding.instance.addPostFrameCallback(
(_) => _openExternallyAndPop(),
);
@@ -256,13 +243,9 @@ class _FileViewerState extends State<FileViewer> {
try {
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
// file_picker has no path/stream save API, so the whole file
// gets loaded into RAM. Cap big media; user falls back to share.
const maxBytes = 200 * 1024 * 1024;
if (size > maxBytes) {
if (!mounted) return;
InfoDialog.show(
@@ -298,8 +281,6 @@ class _FileViewerState extends State<FileViewer> {
List<_ActionDescriptor> _availableActions() => [
_ActionDescriptor(
action: FileViewingActions.openExternal,
// iOS opens the system share sheet (square-with-arrow icon), Android
// the standard app picker; mirror that visually and verbally.
icon: Platform.isIOS ? Icons.ios_share : Icons.open_in_new,
label: Platform.isIOS ? 'Extern öffnen' : 'Öffnen mit',
),
@@ -459,8 +440,7 @@ class _FileViewerState extends State<FileViewer> {
}
final payload = snapshot.data!;
final lines = const LineSplitter().convert(payload.content);
// Reserve gutter width by the digit count of the highest line number,
// so the gutter stays stable as the user scrolls down.
// Stable gutter width — sized by the highest line number's digit count.
final gutterWidth = (lines.length.toString().length * 9.0) + 16;
return SelectionArea(
child: Scrollbar(
@@ -564,8 +544,7 @@ class _FileViewerState extends State<FileViewer> {
final raf = await file.open();
try {
final bytes = await raf.read(_textViewMaxBytes);
// Truncated payloads cannot be reliably re-formatted (parser will
// choke on the dangling tail), so they stay raw.
// Truncated payloads stay raw — a parser would choke on the dangling tail.
return _TextPayload(
content: utf8.decode(bytes, allowMalformed: true),
truncated: true,
@@ -575,9 +554,7 @@ class _FileViewerState extends State<FileViewer> {
}
}
/// Re-indents JSON so dumped/minified payloads from the server are easier
/// to read. Falls through to the original text on parse errors so we
/// never destroy the user's content.
/// Falls through to the original text on parse errors.
String _maybePrettify(String content, String ext) {
if (ext != 'json') return content;
try {
@@ -606,10 +583,6 @@ class _TextPayload {
const _TextPayload({required this.content, required this.truncated});
}
/// Plays back a local file via `video_player`. Renders the standard Chewie
/// controls for video files; audio files get a centered icon plus a custom
/// transport row (slider, time, play/pause), since Chewie's chrome is
/// designed around a video frame.
class _MediaPlayer extends StatefulWidget {
final String path;
final bool isAudio;
@@ -799,10 +772,6 @@ class _AudioControls extends StatelessWidget {
}
}
/// One row in the text viewer: line number on the left (not selectable so
/// it never ends up in copied selections), monospace content on the right.
/// Odd-numbered lines get a slightly tinted background so long files are
/// easier to scan.
class _CodeLine extends StatelessWidget {
final int number;
final String text;
+3 -6
View File
@@ -36,15 +36,12 @@ class _AvatarCacheEntry {
_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.
// LRU via LinkedHashMap insertion order + remove-on-hit. TTL so
// server-side avatar updates become visible within a session.
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.
// Pending map dedups concurrent mounts onto a single HTTP call.
final LinkedHashMap<String, _AvatarCacheEntry> _resolvedAvatars =
LinkedHashMap<String, _AvatarCacheEntry>();
final Map<String, Future<_AvatarPayload?>> _pendingAvatars = {};