refactored internal documentation and simplified comments across chat BLoCs, file viewer, and navigation components
This commit is contained in:
+14
-45
@@ -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;
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
Reference in New Issue
Block a user