diff --git a/lib/api/marianumcloud/talk/talk_api.dart b/lib/api/marianumcloud/talk/talk_api.dart index 59e6d3d..44bc16f 100644 --- a/lib/api/marianumcloud/talk/talk_api.dart +++ b/lib/api/marianumcloud/talk/talk_api.dart @@ -60,10 +60,9 @@ abstract class TalkApi extends ApiRequest { final status = data.statusCode; if (status < 200 || status >= 300) { - // Talk's OCS errors put the real reason in the response body (e.g. - // expired session, removed participant, malformed reply target). - // Include a trimmed preview so the in-app error dialog and logs - // surface the cause instead of just the bare status code. + // Talk's OCS errors carry the real reason in the body (expired session, + // removed participant, ...); include a trimmed preview so the dialog and + // logs surface the cause instead of just the bare status code. final body = data.body.replaceAll(RegExp(r'\s+'), ' ').trim(); final preview = body.length > 500 ? '${body.substring(0, 500)}…' : body; final detail = body.isEmpty diff --git a/lib/api/marianumcloud/webdav/queries/list_files/list_files.dart b/lib/api/marianumcloud/webdav/queries/list_files/list_files.dart index 3376023..fc241f1 100644 --- a/lib/api/marianumcloud/webdav/queries/list_files/list_files.dart +++ b/lib/api/marianumcloud/webdav/queries/list_files/list_files.dart @@ -17,9 +17,8 @@ class ListFiles extends WebdavApi { ListFiles(this.params, {this.onRetry}) : super(params); - // The Nextcloud root listing is significantly slower than subdirectories on - // our instance, so it gets a much longer ceiling. Subfolders fall back to a - // tighter timeout to keep the UI responsive. + // The root listing is much slower than subdirectories on our instance, so it + // gets a longer timeout ceiling; subfolders stay tighter to keep the UI snappy. static const Duration _rootTimeout = Duration(minutes: 3); static const Duration _subfolderTimeout = Duration(seconds: 30); diff --git a/lib/api/marianumcloud/webdav/queries/list_files/list_files_cache.dart b/lib/api/marianumcloud/webdav/queries/list_files/list_files_cache.dart index 06bbf7c..a6e8178 100644 --- a/lib/api/marianumcloud/webdav/queries/list_files/list_files_cache.dart +++ b/lib/api/marianumcloud/webdav/queries/list_files/list_files_cache.dart @@ -27,18 +27,12 @@ class ListFilesCache extends SimpleCache { start(_documentId(path)); } - /// The Nextcloud root listing is significantly slower than subfolders on - /// our instance and frequently returns HTTP 500. Since its content rarely - /// changes, the root payload is cached for a full day so app-resume and - /// connectivity-change auto-refetch triggers do not re-hit the slow root - /// endpoint within the same day. To avoid a long wait on the very first - /// open of the Files page, `prefetchRootListing` (called from `main`) - /// kicks off an async warm-up fetch in the background while the user is - /// still on the launch screen / other modules. Subfolders keep the - /// previous "always refetch on visit" TTL because their content changes - /// more often. Explicit user refreshes (rename, delete, copy/move, - /// upload) bypass the TTL via the inherited [renew] flag or via - /// [invalidate]. + /// The Nextcloud root listing is much slower than subfolders on our instance + /// and frequently returns HTTP 500, but its content rarely changes — so it is + /// cached for a full day to keep app-resume/connectivity auto-refetches off + /// the slow root endpoint. [prefetchRootListing] warms it up in the background + /// after login. Subfolders keep the "always refetch on visit" TTL; explicit + /// user refreshes bypass the TTL via the inherited [renew] flag or [invalidate]. static int _cacheTimeFor(String path) { final stripped = path.replaceAll('/', '').trim(); return stripped.isEmpty ? RequestCache.cacheDay : RequestCache.cacheNothing; diff --git a/lib/extensions/date_time.dart b/lib/extensions/date_time.dart index 763c087..b21f7e8 100644 --- a/lib/extensions/date_time.dart +++ b/lib/extensions/date_time.dart @@ -23,11 +23,9 @@ extension IsSameDay on DateTime { bool isSameOrAfter(DateTime other) => isSameDateTime(other) || isAfter(other); } -/// Calendar-aware day arithmetic. `DateTime.add(Duration(days: n))` adds -/// `n * 24h` of real-world time, which on local DateTimes silently drifts by -/// ±1h across DST transitions — so 7 days from "Monday 00:00 CEST" before a -/// DST fall-back lands at "Sunday 23:00 CET", shifting the entire next week -/// onto the wrong calendar day. [addDays]/[subtractDays] normalize through +/// Calendar-aware day arithmetic. `DateTime.add(Duration(days: n))` adds real +/// time that drifts ±1h across DST, shifting a whole week onto the wrong +/// calendar day. [addDays]/[subtractDays] normalize through /// `DateTime(year, month, day + n)` so the wall-clock fields stay fixed. extension CalendarDayArithmetic on DateTime { DateTime addDays(int days) => DateTime( diff --git a/lib/main.dart b/lib/main.dart index 7066918..96e0b1f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -243,11 +243,8 @@ class _MainState extends State
{ }); } - /// Fires a background credential check against Marianum Connect — runs in - /// the background so it never blocks the cold-start path. A 401 means the - /// password has been rotated server-side; the validator wipes the local - /// session and we flip the account bloc back to `loggedOut`, which sends - /// the user to the login screen. + /// Warms the core caches (timetable, chat list, files root) in the + /// background so the first screen render hits populated data. void _prefetchBaseData(BuildContext context) { context.read().refresh(); unawaited(context.read().refresh(silent: true)); @@ -265,6 +262,9 @@ class _MainState extends State
{ ); } + /// Background credential check: a 401 means the password was rotated + /// server-side, so the validator wipes the local session and flips the + /// account bloc to `loggedOut` (sending the user to the login screen). void _scheduleSessionValidation(AccountBloc accountBloc) { unawaited( SessionValidator.probeStored( diff --git a/lib/push/chat_thread_store.dart b/lib/push/chat_thread_store.dart index b0fbafc..b61f5a7 100644 --- a/lib/push/chat_thread_store.dart +++ b/lib/push/chat_thread_store.dart @@ -77,15 +77,11 @@ List removeThreadNid(List messages, int nid) => /// History for the next notification after [message] arrives, given whether /// the chat's previous notification is still on screen ([isActive]): +/// `true` → stacks onto existing history; `false` → notification gone +/// (dismissed/read), thread restarts with only [message]; `null` → probe +/// failed/unsupported, keep stacking (degraded stacking never loses a message). /// -/// - `true` → the user hasn't dismissed/read it, so [message] STACKS onto the -/// existing history. -/// - `false` → the notification is gone (swiped away or the chat was read -/// without our cleanup running), so the thread RESTARTS with only [message]. -/// - `null` → the active-notification probe failed or isn't supported; keep -/// stacking defensively — degraded stacking never loses a message. -/// -/// Android provides no reliable "notification dismissed" callback, so the +/// Android has no reliable "notification dismissed" callback, so the /// visible-state probe at append time is the substitute. List threadAfterIncoming( List existing, @@ -98,11 +94,9 @@ List threadAfterIncoming( } /// MessagingStyle header per Android convention: a 1:1 chat gets NO -/// `conversationTitle` — the system then shows the person once as the header -/// and plain texts per line; setting a title would repeat the name on every -/// row. Groups get the ROOM NAME as title (parsed from the subject) plus -/// per-line sender names; when no room name is known, >1 distinct sender -/// still marks the thread as group with the last sender as title fallback. +/// `conversationTitle` (setting one would repeat the person's name on every +/// row); groups use the room name as title, falling back to the last sender +/// when there is >1 distinct sender but no room name is known. ({String? conversationTitle, bool groupConversation}) conversationHeader( List messages, ) { diff --git a/lib/push/push_actions.dart b/lib/push/push_actions.dart index b08ceaf..3548143 100644 --- a/lib/push/push_actions.dart +++ b/lib/push/push_actions.dart @@ -146,16 +146,13 @@ class PushActions { } } - /// Ends the reply interaction. The card itself is already gone — the - /// action's native `cancelNotification` removed it the moment the reply - /// fired (a Dart-side cancel instead does NOT work everywhere: MIUI/HyperOS - /// ignores app cancels while an inline reply is pending). What remains: - /// success clears the stacked history so the next push starts fresh (plus a - /// redundant defensive cancel); failure re-renders silently, which the - /// renderer's active-probe turns into a no-op when the card is really gone - /// — the failure surface is the error card posted by the caller. - /// Injectable seams so tests can observe the flow without platform - /// channels. + /// Ends the reply interaction. The card is already gone — the action's + /// native `cancelNotification` removed it when the reply fired (a Dart-side + /// cancel is unreliable: MIUI/HyperOS ignores app cancels while an inline + /// reply is pending). Success clears the stacked history (plus a defensive + /// cancel); failure re-renders silently, a no-op when the card is really + /// gone — the real failure surface is the error card posted by the caller. + /// Injectable seams let tests observe the flow without platform channels. static Future finishReply({ required String chatToken, required bool sent, diff --git a/lib/push/push_renderer.dart b/lib/push/push_renderer.dart index 0f8d307..bcdbc2d 100644 --- a/lib/push/push_renderer.dart +++ b/lib/push/push_renderer.dart @@ -147,13 +147,11 @@ class PushRenderer { bool alert = true, }) async { if (messages.isEmpty) return; - // A silent render only ever UPDATES an existing card (delete-push shrunk - // the thread, late avatar arrived, failed reply needs its spinner - // stopped). If the card is verifiably gone meanwhile (reply/mark-read - // cleanup cancelled it, or the user swiped it away), re-posting would - // resurrect it — and Android would re-attach a pending inline reply on - // top. Probe failure (null) still renders: stopping a possible reply - // spinner outweighs a rare resurrection. + // A silent render only UPDATES an existing card. If the card is verifiably + // gone (cleanup cancelled it or the user swiped it away), re-posting would + // resurrect it — with Android re-attaching a pending inline reply on top. + // Probe failure (null) still renders: stopping a possible reply spinner + // outweighs a rare resurrection. if (!alert && await _isChatNotificationActive(chatToken) == false) { debugPrint('PushRenderer: skip silent re-render, card gone ($chatToken)'); return; diff --git a/lib/state/app/infrastructure/loadable_state/bloc/loadable_state_bloc.dart b/lib/state/app/infrastructure/loadable_state/bloc/loadable_state_bloc.dart index a17c85f..edcdeda 100644 --- a/lib/state/app/infrastructure/loadable_state/bloc/loadable_state_bloc.dart +++ b/lib/state/app/infrastructure/loadable_state/bloc/loadable_state_bloc.dart @@ -45,11 +45,8 @@ class LoadableStateBloc extends Bloc return; } _lastResumeRefetch = now; - // Re-check connectivity. The resulting [ConnectivityChanged] event takes - // it from there: its handler updates the offline/online indicator and - // triggers [reFetch] when the device is connected, so a stale - // "Verbindung fehlgeschlagen" bar from a suspend-time fetch clears as - // soon as the network is reachable again. + // Re-check connectivity so the resulting [ConnectivityChanged] handler + // clears a stale error bar and triggers [reFetch] once reachable again. unawaited( Connectivity().checkConnectivity().then( (result) => diff --git a/lib/state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart b/lib/state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart index ddeaff0..7ddfa98 100644 --- a/lib/state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart +++ b/lib/state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart @@ -65,11 +65,9 @@ class LoadableStateConsumer< final showError = hasError && !hasContent; final showErrorBar = hasError && hasContent; - // Keep the wrapper hierarchy stable across refresh cycles. The bloc clears - // reFetch to null while a refetch is in flight and restores it on - // completion; flipping the RefreshIndicator in and out on that signal - // would change the widget tree under the ListView and reset its scroll - // position every refresh. + // Keep the wrapper hierarchy stable across refresh cycles: reFetch flips to + // null mid-refetch, and toggling the RefreshIndicator on that signal would + // rebuild the tree under the ListView and reset its scroll position. final content = SizedBox( height: MediaQuery.of(context).size.height, child: hasContent diff --git a/lib/state/app/infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart b/lib/state/app/infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart index 8d5509c..9601c00 100644 --- a/lib/state/app/infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart +++ b/lib/state/app/infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart @@ -128,10 +128,8 @@ abstract class LoadableHydratedBloc< gatherData() .catchError((e) { log('Error while fetching ${TState.toString()}: ${e.toString()}'); - // The bloc may have been closed before this async error landed (e.g. - // when its scoping widget tree was disposed mid-fetch). Adding to a - // closed bloc throws "Cannot add new events after calling close", - // so swallow that case quietly. + // The bloc may have been closed before this async error landed; + // adding to a closed bloc throws, so swallow that case. if (isClosed) return; add( Error( diff --git a/lib/state/app/modules/chat/bloc/chat_bloc.dart b/lib/state/app/modules/chat/bloc/chat_bloc.dart index 7785ec4..cacd874 100644 --- a/lib/state/app/modules/chat/bloc/chat_bloc.dart +++ b/lib/state/app/modules/chat/bloc/chat_bloc.dart @@ -196,12 +196,9 @@ class ChatBloc void _startLongPoll(String token) { if (!_appResumed) return; - // A load chain may finish AFTER the user already switched chats — e.g. a - // notification tap resumes the app (lifecycle refresh starts loading the - // still-open chat A) and then navigates to chat B. Without this guard the - // stale chain's completion would hijack the long-poll back to A and its - // responses would merge A's messages into B's state (chat B showing chat - // A's content) while B never receives live updates. + // A load chain may finish after the user switched chats (A→B); without this + // guard the stale chain hijacks the long-poll back to A and merges A's + // messages into B's state while B never receives live updates. if ((innerState?.currentToken ?? '') != token) return; if (_pollingToken == token) return; _stopLongPoll(); diff --git a/lib/state/app/modules/files/bloc/files_bloc.dart b/lib/state/app/modules/files/bloc/files_bloc.dart index 5b318e8..d4b7bad 100644 --- a/lib/state/app/modules/files/bloc/files_bloc.dart +++ b/lib/state/app/modules/files/bloc/files_bloc.dart @@ -44,11 +44,9 @@ class FilesBloc await _query(path, renew: true); } - /// LoadableState.reFetch (used by the pull-to-refresh indicator and the - /// error-screen retry button) routes through here. The inherited retry() - /// goes via gatherData() which respects the cache TTL — for an explicit - /// user-initiated reload we must bypass it, otherwise the root listing - /// silently returns its day-old cached payload without hitting the server. + /// Pull-to-refresh and error-screen retry route through here. Unlike the + /// inherited retry() (via gatherData(), which respects the cache TTL), this + /// bypasses the TTL — otherwise the root listing returns its day-old cache. @override void retry() { unawaited(refresh()); diff --git a/lib/state/app/modules/files/data_provider/files_data_provider.dart b/lib/state/app/modules/files/data_provider/files_data_provider.dart index 079e2b9..e4a1d5f 100644 --- a/lib/state/app/modules/files/data_provider/files_data_provider.dart +++ b/lib/state/app/modules/files/data_provider/files_data_provider.dart @@ -6,16 +6,13 @@ import '../../../../../api/marianumcloud/webdav/webdav_api.dart'; import '../../../../../api/request_cache.dart'; class FilesDataProvider { - /// Lists files at [path]. Cached payload is delivered via [onCacheData] as - /// soon as it is read from disk, so callers can render stale data while the - /// network call is still pending. The Future itself resolves once both the - /// cache lookup and the network attempt have settled, throwing if no payload - /// could be obtained at all. + /// Lists files at [path]. Cached payload is delivered via [onCacheData] the + /// moment it is read from disk so callers can render stale data while the + /// network call is pending; the Future resolves once both have settled, + /// throwing if no payload could be obtained at all. /// - /// Pass [renew] for explicit user-triggered reloads (pull-to-refresh, after - /// a rename / delete / move / upload). It bypasses the per-path TTL in - /// [ListFilesCache] so the root listing — which is otherwise cached for a - /// full day — still refetches when the user actively asks for it. + /// Pass [renew] for explicit user-triggered reloads to bypass the per-path + /// TTL in [ListFilesCache] (the root listing is otherwise cached for a day). Future listFiles( String path, { void Function(ListFilesResponse)? onCacheData, diff --git a/lib/state/app/modules/foreign_timetable/bloc/foreign_timetable_bloc.dart b/lib/state/app/modules/foreign_timetable/bloc/foreign_timetable_bloc.dart index 0892344..e4ce3aa 100644 --- a/lib/state/app/modules/foreign_timetable/bloc/foreign_timetable_bloc.dart +++ b/lib/state/app/modules/foreign_timetable/bloc/foreign_timetable_bloc.dart @@ -12,14 +12,11 @@ import '../../timetable/bloc/timetable_event.dart'; import '../../timetable/bloc/timetable_state.dart'; import '../repository/foreign_timetable_repository.dart'; -/// Drives a foreign element's timetable. Mirrors the week-loading and -/// week-navigation logic of `TimetableBloc` but (a) loads weeks from the -/// element endpoint, (b) carries no custom events, and (c) does not persist — -/// it is created per opened page and recreated for every selected element. -/// -/// It reuses [TimetableState] verbatim so the existing render pipeline works -/// unchanged; `customEvents` simply stays null (the foreign view uses an -/// `isReady` predicate that ignores it). +/// Drives a foreign element's timetable. Mirrors `TimetableBloc`'s week-loading +/// and navigation but loads weeks from the element endpoint, carries no custom +/// events, and does not persist (page-scoped, recreated per element). Reuses +/// [TimetableState] verbatim so the render pipeline is unchanged; `customEvents` +/// stays null (the foreign view's `isReady` predicate ignores it). class ForeignTimetableBloc extends LoadableHydratedBloc< @@ -56,10 +53,8 @@ class ForeignTimetableBloc ); } - // Persistence is disabled: this bloc is page-scoped and element-specific, so - // there is nothing worth restoring across launches. Returning null from - // toJson means HydratedBloc never writes anything; fromJson ignores any - // legacy payload and starts fresh. + // Persistence disabled: page-scoped and element-specific, nothing worth + // restoring. toJson returns null so nothing is written; fromJson starts fresh. @override Map? toJson(LoadableState state) => null; diff --git a/lib/state/app/modules/foreign_timetable/data_provider/foreign_timetable_data_provider.dart b/lib/state/app/modules/foreign_timetable/data_provider/foreign_timetable_data_provider.dart index 83eb638..304065f 100644 --- a/lib/state/app/modules/foreign_timetable/data_provider/foreign_timetable_data_provider.dart +++ b/lib/state/app/modules/foreign_timetable/data_provider/foreign_timetable_data_provider.dart @@ -10,9 +10,9 @@ import '../../timetable/data_provider/timetable_data_provider.dart'; /// Data access for a foreign element's timetable. The week comes from the /// element-specific endpoint; all reference data (rooms/subjects/holidays/ -/// school year/timegrid) is school-wide and identical to the user's own plan, -/// so it is delegated to the existing [TimetableDataProvider] (which already -/// caches it). Custom events are intentionally absent — they are user-private. +/// school year/timegrid) is school-wide, so it delegates to the existing +/// [TimetableDataProvider] (which caches it). Custom events are intentionally +/// absent — they are user-private. class ForeignTimetableDataProvider { final TimetableDataProvider _base; diff --git a/lib/state/app/modules/settings/bloc/settings_cubit.dart b/lib/state/app/modules/settings/bloc/settings_cubit.dart index a785e22..35c10dc 100644 --- a/lib/state/app/modules/settings/bloc/settings_cubit.dart +++ b/lib/state/app/modules/settings/bloc/settings_cubit.dart @@ -17,9 +17,8 @@ class SettingsCubit extends HydratedCubit { Settings val({bool write = false}) { if (write) { // Defer the emit until the synchronous mutation on the returned object - // has finished. Without this scheduleMicrotask the cubit emits a copy - // captured *before* the assignment runs, so listeners (and HydratedBloc - // persistence) see the old value on the first emit. + // has finished — without this microtask the cubit emits a copy captured + // *before* the assignment, so listeners see the old value. if (!_emitScheduled) { _emitScheduled = true; scheduleMicrotask(() { diff --git a/lib/state/app/modules/timetable/bloc/timetable_state.dart b/lib/state/app/modules/timetable/bloc/timetable_state.dart index a816318..df00dd7 100644 --- a/lib/state/app/modules/timetable/bloc/timetable_state.dart +++ b/lib/state/app/modules/timetable/bloc/timetable_state.dart @@ -27,10 +27,9 @@ abstract class TimetableState with _$TimetableState { required DateTime startDate, required DateTime endDate, @Default(0) int dataVersion, - // Boundaries learned from past server denials of inaccessible weeks. - // Inclusive: weeks whose start is on/before `accessibleEndDate` and - // whose end is on/after `accessibleStartDate` are within the user's - // permitted range. Null = no upper / lower bound discovered yet. + // Boundaries learned from past server denials. A week is permitted when its + // start is on/before `accessibleEndDate` and its end on/after + // `accessibleStartDate`. Null = that bound not discovered yet. DateTime? accessibleStartDate, DateTime? accessibleEndDate, }) = _TimetableState; diff --git a/lib/state/app/modules/timetable/data_provider/timetable_data_provider.dart b/lib/state/app/modules/timetable/data_provider/timetable_data_provider.dart index 425ee84..c7aea5c 100644 --- a/lib/state/app/modules/timetable/data_provider/timetable_data_provider.dart +++ b/lib/state/app/modules/timetable/data_provider/timetable_data_provider.dart @@ -23,10 +23,9 @@ import '../../../../../api/mhsl/custom_timetable_event/update/update_custom_time import '../../../../../api/request_cache.dart'; import '../../../../../model/account_data.dart'; -/// Pulls the timetable from the Marianum-Connect mobile API. Each MC endpoint -/// is its own HTTP call; this provider just exposes the lazy futures so the -/// bloc can chain them without seeing the dio layer. Custom events still come -/// from the MHSL backend and are unchanged. +/// Pulls the timetable from the Marianum-Connect mobile API. Each endpoint is +/// its own HTTP call; this provider exposes the lazy futures so the bloc can +/// chain them without seeing the dio layer. Custom events still come from MHSL. class TimetableDataProvider { Future getWeek( DateTime startDate, diff --git a/lib/storage/dev_tools_settings.dart b/lib/storage/dev_tools_settings.dart index 02f59da..d9b86ce 100644 --- a/lib/storage/dev_tools_settings.dart +++ b/lib/storage/dev_tools_settings.dart @@ -25,12 +25,9 @@ class DevToolsSettings { this.marianumConnectCustomUrl = '', }); - // Resolves the effective base URL for the Marianum-Connect mobile API. - // Falls back to live when the custom URL is empty or malformed. HTTP is - // accepted alongside HTTPS only in debug/profile builds (developers can - // point at `http://10.0.2.2:8080` without configuring TLS locally); release - // builds restrict the custom endpoint to HTTPS so a leaked debug URL never - // ships an unencrypted bearer token over the wire. + // Resolves the effective base URL, falling back to live when the custom URL + // is empty or malformed. HTTP custom endpoints are allowed only outside + // release builds so a leaked debug URL never ships a plaintext bearer token. String resolveMarianumConnectBaseUrl() { switch (marianumConnectEndpoint) { case MarianumConnectEndpoint.live: diff --git a/lib/view/login/login.dart b/lib/view/login/login.dart index ba0ab93..d34a9f4 100644 --- a/lib/view/login/login.dart +++ b/lib/view/login/login.dart @@ -79,13 +79,9 @@ class _LoginState extends State with SingleTickerProviderStateMixin { minHeight: constraints.maxHeight, maxWidth: 420, ), - // spaceBetween statt Spacer-in-IntrinsicHeight: bei jeder - // Inhaltsänderung im unteren Block (z.B. EndpointLink mit - // dynamischem Label) würde IntrinsicHeight sonst die Column - // an die intrinsic-Höhe pinnen und ein paar Pixel Overflow - // produzieren. spaceBetween fügt nur den verbleibenden Gap - // ein und schrumpft sauber auf 0, wenn der Inhalt zu hoch - // wird — dann übernimmt der äußere ScrollView. + // spaceBetween statt Spacer-in-IntrinsicHeight: Letzteres würde + // die Column bei Inhaltsänderungen im unteren Block auf die + // intrinsic-Höhe pinnen und ein paar Pixel Overflow erzeugen. child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/lib/view/pages/files/data/file_type_icon.dart b/lib/view/pages/files/data/file_type_icon.dart index 792fe0c..259d34e 100644 --- a/lib/view/pages/files/data/file_type_icon.dart +++ b/lib/view/pages/files/data/file_type_icon.dart @@ -54,14 +54,12 @@ IconData? _iconForMime(String mime) { const map = { 'application/pdf': Icons.picture_as_pdf_outlined, - // Word processing 'application/msword': Icons.description_outlined, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': Icons.description_outlined, 'application/vnd.oasis.opendocument.text': Icons.description_outlined, 'application/rtf': Icons.description_outlined, - // Spreadsheets 'application/vnd.ms-excel': Icons.table_chart_outlined, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': Icons.table_chart_outlined, @@ -70,13 +68,11 @@ IconData? _iconForMime(String mime) { 'application/vnd.ms-excel.sheet.macroenabled.12': Icons.table_chart_outlined, - // Presentations 'application/vnd.ms-powerpoint': Icons.slideshow_outlined, 'application/vnd.openxmlformats-officedocument.presentationml.presentation': Icons.slideshow_outlined, 'application/vnd.oasis.opendocument.presentation': Icons.slideshow_outlined, - // Archives 'application/zip': Icons.folder_zip_outlined, 'application/x-zip-compressed': Icons.folder_zip_outlined, 'application/x-rar-compressed': Icons.folder_zip_outlined, @@ -88,7 +84,6 @@ IconData? _iconForMime(String mime) { 'application/x-xz': Icons.folder_zip_outlined, 'application/zstd': Icons.folder_zip_outlined, - // Code / structured data 'application/json': Icons.code, 'application/ld+json': Icons.code, 'application/xml': Icons.code, @@ -96,26 +91,21 @@ IconData? _iconForMime(String mime) { 'application/javascript': Icons.code, 'application/x-sh': Icons.terminal, - // Calendar / contacts 'text/calendar': Icons.calendar_month_outlined, 'text/vcard': Icons.contact_page_outlined, - // E-books 'application/epub+zip': Icons.menu_book_outlined, 'application/x-mobipocket-ebook': Icons.menu_book_outlined, - // Executables / installers 'application/x-msdownload': Icons.terminal, 'application/x-msi': Icons.terminal, 'application/x-apple-diskimage': Icons.album_outlined, 'application/vnd.android.package-archive': Icons.android_outlined, 'application/octet-stream': Icons.insert_drive_file_outlined, - // Databases 'application/x-sqlite3': Icons.storage_outlined, 'application/vnd.sqlite3': Icons.storage_outlined, - // 3D 'model/gltf-binary': Icons.view_in_ar_outlined, 'model/gltf+json': Icons.view_in_ar_outlined, 'model/stl': Icons.view_in_ar_outlined, @@ -125,7 +115,6 @@ IconData? _iconForMime(String mime) { } const _extensionIcons = { - // Images 'jpg': Icons.image_outlined, 'jpeg': Icons.image_outlined, 'png': Icons.image_outlined, 'gif': Icons.image_outlined, 'webp': Icons.image_outlined, 'bmp': Icons.image_outlined, @@ -137,7 +126,6 @@ const _extensionIcons = { 'svg': Icons.gesture_outlined, 'eps': Icons.gesture_outlined, 'ai': Icons.gesture_outlined, - // Video 'mp4': Icons.movie_outlined, 'm4v': Icons.movie_outlined, 'mov': Icons.movie_outlined, 'mkv': Icons.movie_outlined, 'avi': Icons.movie_outlined, 'webm': Icons.movie_outlined, @@ -145,7 +133,6 @@ const _extensionIcons = { '3gp': Icons.movie_outlined, 'mpg': Icons.movie_outlined, 'mpeg': Icons.movie_outlined, 'ogv': Icons.movie_outlined, - // Audio 'mp3': Icons.audiotrack, 'm4a': Icons.audiotrack, 'aac': Icons.audiotrack, 'wav': Icons.audiotrack, 'flac': Icons.audiotrack, 'ogg': Icons.audiotrack, 'oga': Icons.audiotrack, 'opus': Icons.audiotrack, 'wma': Icons.audiotrack, @@ -156,31 +143,25 @@ const _extensionIcons = { 'musicxml': Icons.music_note, 'mxl': Icons.music_note, 'midi': Icons.music_note, 'mid': Icons.music_note, - // PDF 'pdf': Icons.picture_as_pdf_outlined, - // Word 'doc': Icons.description_outlined, 'docx': Icons.description_outlined, 'odt': Icons.description_outlined, 'rtf': Icons.description_outlined, 'pages': Icons.description_outlined, - // Spreadsheets 'xls': Icons.table_chart_outlined, 'xlsx': Icons.table_chart_outlined, 'xlsm': Icons.table_chart_outlined, 'ods': Icons.table_chart_outlined, 'csv': Icons.table_chart_outlined, 'tsv': Icons.table_chart_outlined, 'numbers': Icons.table_chart_outlined, - // Presentations 'ppt': Icons.slideshow_outlined, 'pptx': Icons.slideshow_outlined, 'pps': Icons.slideshow_outlined, 'odp': Icons.slideshow_outlined, 'key': Icons.slideshow_outlined, - // Plain text / notes 'txt': Icons.article_outlined, 'md': Icons.article_outlined, 'markdown': Icons.article_outlined, 'log': Icons.article_outlined, 'rst': Icons.article_outlined, - // Code 'html': Icons.code, 'htm': Icons.code, 'xhtml': Icons.code, 'css': Icons.code, 'scss': Icons.code, 'sass': Icons.code, 'less': Icons.code, 'js': Icons.code, 'mjs': Icons.code, 'cjs': Icons.code, @@ -195,12 +176,10 @@ const _extensionIcons = { 'json': Icons.code, 'json5': Icons.code, 'xml': Icons.code, 'yaml': Icons.code, 'yml': Icons.code, - // Shell 'sh': Icons.terminal, 'bash': Icons.terminal, 'zsh': Icons.terminal, 'fish': Icons.terminal, 'ps1': Icons.terminal, 'bat': Icons.terminal, 'cmd': Icons.terminal, - // Archives 'zip': Icons.folder_zip_outlined, 'rar': Icons.folder_zip_outlined, '7z': Icons.folder_zip_outlined, 'tar': Icons.folder_zip_outlined, 'gz': Icons.folder_zip_outlined, 'bz2': Icons.folder_zip_outlined, @@ -218,26 +197,21 @@ const _extensionIcons = { 'dockerignore': Icons.settings_outlined, 'dockerfile': Icons.settings_outlined, - // Fonts 'ttf': Icons.font_download_outlined, 'otf': Icons.font_download_outlined, 'woff': Icons.font_download_outlined, 'woff2': Icons.font_download_outlined, 'eot': Icons.font_download_outlined, - // Calendar / contacts 'ics': Icons.calendar_month_outlined, 'ical': Icons.calendar_month_outlined, 'vcf': Icons.contact_page_outlined, 'vcard': Icons.contact_page_outlined, - // E-books 'epub': Icons.menu_book_outlined, 'mobi': Icons.menu_book_outlined, 'azw': Icons.menu_book_outlined, 'azw3': Icons.menu_book_outlined, - // 3D 'stl': Icons.view_in_ar_outlined, 'obj': Icons.view_in_ar_outlined, 'fbx': Icons.view_in_ar_outlined, 'blend': Icons.view_in_ar_outlined, 'glb': Icons.view_in_ar_outlined, 'gltf': Icons.view_in_ar_outlined, '3ds': Icons.view_in_ar_outlined, - // Executables / packages 'exe': Icons.terminal, 'msi': Icons.terminal, 'app': Icons.terminal, 'deb': Icons.terminal, 'rpm': Icons.terminal, 'apk': Icons.android_outlined, 'ipa': Icons.terminal, @@ -247,7 +221,6 @@ const _extensionIcons = { 'iso': Icons.album_outlined, 'img': Icons.album_outlined, 'dmg': Icons.album_outlined, - // Databases 'db': Icons.storage_outlined, 'sqlite': Icons.storage_outlined, 'sqlite3': Icons.storage_outlined, }; diff --git a/lib/view/pages/more/share/qr_share_view.dart b/lib/view/pages/more/share/qr_share_view.dart index 05d9ff6..cde2bb8 100644 --- a/lib/view/pages/more/share/qr_share_view.dart +++ b/lib/view/pages/more/share/qr_share_view.dart @@ -19,12 +19,10 @@ class QrShareView extends StatefulWidget { class _QrShareViewState extends State with SingleTickerProviderStateMixin { - // Owning the TabController explicitly (instead of DefaultTabController) is - // a workaround for a Flutter framework bug where TabBarView's - // didChangeDependencies issues a jumpToPage mid-build, whose scroll - // notification then calls setState on _TabStyle while the frame is still - // being built — only reproduces reliably with a bottomNavigationBar in the - // Scaffold underneath. + // Own the TabController explicitly (not DefaultTabController) to dodge a + // Flutter bug: TabBarView's didChangeDependencies fires jumpToPage mid-build, + // whose scroll notification then calls setState on _TabStyle during the frame + // — only reproduces with a bottomNavigationBar in the Scaffold underneath. late final TabController _tabController = TabController( length: 2, vsync: this, diff --git a/lib/view/pages/settings/sections/account_section.dart b/lib/view/pages/settings/sections/account_section.dart index 511426d..14beb54 100644 --- a/lib/view/pages/settings/sections/account_section.dart +++ b/lib/view/pages/settings/sections/account_section.dart @@ -174,12 +174,10 @@ class _AccountSectionState extends State { } Future _showLogoutDialog(BuildContext context) async { - // Sequential logout flow: dialog wipes secure storage, dialog closes - // (single Navigator.pop), then we flip the AccountBloc state. The bloc - // listener in main.dart pops the Settings route and runs the in-memory - // wipe. Triggering setStatus from inside removeData (the previous - // approach) raced AsyncDialogAction's pop(true) against popUntil(isFirst) - // and could leave the navigator in an inconsistent state. + // Flip AccountBloc state only after the dialog fully closes: doing it from + // inside removeData (the previous approach) raced AsyncDialogAction's + // pop(true) against the listener's popUntil(isFirst) and could leave the + // navigator in an inconsistent state. final confirmed = await showDialog( context: context, builder: (dialogContext) => ConfirmDialog( diff --git a/lib/view/pages/talk/widgets/chat_textfield.dart b/lib/view/pages/talk/widgets/chat_textfield.dart index 1024159..153d6c5 100644 --- a/lib/view/pages/talk/widgets/chat_textfield.dart +++ b/lib/view/pages/talk/widgets/chat_textfield.dart @@ -114,12 +114,8 @@ class _ChatTextfieldState extends State { @override void dispose() { - // Clear the published bar height only after this unmount completes. Setting - // the shared notifier synchronously here fires its listener (the download - // tray's AnimatedBuilder → setState) while the element tree is locked during - // teardown, throwing "setState() called when widget tree was locked". - // Running it at the end of the frame lets the reset land once the tree is - // writable again; a newly opened chat re-publishes its own height afterwards. + // Defer to end-of-frame: resetting the shared notifier synchronously during + // teardown fires setState while the tree is locked ("widget tree was locked"). WidgetsBinding.instance.addPostFrameCallback( (_) => downloadChipBottomObstruction.value = 0, ); diff --git a/lib/view/pages/timetable/custom_events/custom_event_edit_dialog.dart b/lib/view/pages/timetable/custom_events/custom_event_edit_dialog.dart index 8503e60..1cc736b 100644 --- a/lib/view/pages/timetable/custom_events/custom_event_edit_dialog.dart +++ b/lib/view/pages/timetable/custom_events/custom_event_edit_dialog.dart @@ -281,11 +281,8 @@ class _CustomEventEditDialogState extends State { ), ), const Divider(), - // The RRuleGenerator widget has zero outer padding while every - // surrounding ListTile uses the default 16px horizontal indent. - // Wrap it to match — keeps the rule editor visually aligned with - // the date/time/color rows above instead of hugging the dialog - // border. + // RRuleGenerator has zero outer padding; wrap it in the ListTile + // default 16px indent so it aligns with the rows above. Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: RRuleGenerator( @@ -309,12 +306,9 @@ class _CustomEventEditDialogState extends State { ], ); - /// Maps the rrule_generator widget onto Marianum's Material theme so its - /// pickers blend in with the rest of the form (the package's defaults - /// have an out-of-place red switch outline and bold ALL-CAPS-feeling - /// headers). Layout itself stays as the package provides — fully custom - /// styling beyond what `RRuleGeneratorConfig` exposes isn't possible - /// without forking it. + /// Maps the rrule_generator widget onto the app's Material theme so its + /// pickers blend in. Styling is limited to what `RRuleGeneratorConfig` + /// exposes — fully custom styling isn't possible without forking it. RRuleGeneratorConfig _rruleConfig(BuildContext context) { final theme = Theme.of(context); final cs = theme.colorScheme; diff --git a/lib/widget/emoji_text.dart b/lib/widget/emoji_text.dart index 47f2584..6ada371 100644 --- a/lib/widget/emoji_text.dart +++ b/lib/widget/emoji_text.dart @@ -2,11 +2,9 @@ import 'package:flutter/material.dart'; /// Central place for rendering emoji glyphs (reactions, pickers, …). /// -/// Emojis used to be drawn as bare `Text(emoji)` widgets all over the Talk UI, -/// which meant they inherited the small default body text size and looked -/// inconsistent. [EmojiText] gives every emoji a uniform, comfortably large -/// size and forces the platform's color-emoji font so the rendering is the same -/// everywhere. +/// Gives every emoji a uniform, comfortably large size and forces the +/// platform's color-emoji font, so rendering stays consistent everywhere — +/// bare `Text(emoji)` inherited the small body size and looked inconsistent. class EmojiText extends StatelessWidget { /// Size for emojis shown inline next to other text, e.g. reaction chips. static const double sizeInline = 15;