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