refactored lesson details, centralized logout logic, and added resume re-fetch
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 101 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
@@ -17,6 +17,7 @@ import 'state/app/modules/app_modules.dart';
|
||||
import 'state/app/modules/breaker/bloc/breaker_bloc.dart';
|
||||
import 'state/app/modules/chat_list/bloc/chat_list_bloc.dart';
|
||||
import 'state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import 'state/app/modules/timetable/bloc/timetable_bloc.dart';
|
||||
import 'utils/debouncer.dart';
|
||||
import 'view/pages/overhang.dart';
|
||||
import 'widget/breaker/breaker.dart';
|
||||
@@ -54,6 +55,10 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
if (!mounted) return;
|
||||
context.read<BreakerBloc>().refresh();
|
||||
context.read<ChatListBloc>().refresh();
|
||||
// App is freshly mounted on every login (BlocConsumer in main.dart
|
||||
// swaps it in for Login), so this also covers the post-logout case
|
||||
// where the bloc was reset to an empty state and needs a fresh fetch.
|
||||
context.read<TimetableBloc>().refresh();
|
||||
});
|
||||
|
||||
_updateTimings = Timer.periodic(const Duration(seconds: 30), (_) {
|
||||
|
||||
+61
-1
@@ -15,6 +15,7 @@ import 'package:jiffy/jiffy.dart';
|
||||
import 'package:loader_overlay/loader_overlay.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'api/mhsl/breaker/get_breakers/get_breakers_response.dart';
|
||||
import 'app.dart';
|
||||
@@ -33,6 +34,7 @@ import 'theming/light_app_theme.dart';
|
||||
import 'view/login/login.dart';
|
||||
import 'widget/app_progress_indicator.dart';
|
||||
import 'widget/breaker/breaker.dart';
|
||||
import 'widget/debug/cache_view.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
log('MarianumMobile started');
|
||||
@@ -160,7 +162,40 @@ class _MainState extends State<Main> {
|
||||
home: LoaderOverlay(
|
||||
child: Breaker(
|
||||
breaker: BreakerArea.global,
|
||||
child: BlocBuilder<AccountBloc, AccountState>(
|
||||
child: BlocConsumer<AccountBloc, AccountState>(
|
||||
listenWhen: (previous, current) => previous.status != current.status,
|
||||
listener: (context, accountState) {
|
||||
if (accountState.status != AccountStatus.loggedOut) return;
|
||||
// Routes pushed via AppRoutes (e.g. Settings) live on the
|
||||
// root navigator and survive the home swap below, so they
|
||||
// would still cover the Login screen after logout. Pop
|
||||
// them here so the user immediately sees Login.
|
||||
final navigator = Navigator.of(context);
|
||||
if (navigator.canPop()) {
|
||||
navigator.popUntil((route) => route.isFirst);
|
||||
}
|
||||
// Capture bloc references before the post-frame callback
|
||||
// — by the time it runs the dialog/Settings context is
|
||||
// gone but this listener context is still valid.
|
||||
final settingsCubit = context.read<SettingsCubit>();
|
||||
final timetableBloc = context.read<TimetableBloc>();
|
||||
final chatListBloc = context.read<ChatListBloc>();
|
||||
final chatBloc = context.read<ChatBloc>();
|
||||
final breakerBloc = context.read<BreakerBloc>();
|
||||
// Defer the actual wipe until after this frame so the
|
||||
// App tree (TimetableBloc/ChatListBloc watchers etc.)
|
||||
// is already torn down. Resetting blocs while App is
|
||||
// still in front caused a black-frame race.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
unawaited(_wipeUserState(
|
||||
settingsCubit: settingsCubit,
|
||||
timetableBloc: timetableBloc,
|
||||
chatListBloc: chatListBloc,
|
||||
chatBloc: chatBloc,
|
||||
breakerBloc: breakerBloc,
|
||||
));
|
||||
});
|
||||
},
|
||||
builder: (context, accountState) {
|
||||
switch (accountState.status) {
|
||||
case AccountStatus.loggedIn:
|
||||
@@ -190,3 +225,28 @@ class _MainState extends State<Main> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _wipeUserState({
|
||||
required SettingsCubit settingsCubit,
|
||||
required TimetableBloc timetableBloc,
|
||||
required ChatListBloc chatListBloc,
|
||||
required ChatBloc chatBloc,
|
||||
required BreakerBloc breakerBloc,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.clear();
|
||||
PaintingBinding.instance.imageCache.clear();
|
||||
await settingsCubit.reset();
|
||||
await Future.wait([
|
||||
timetableBloc.reset(),
|
||||
chatListBloc.reset(),
|
||||
chatBloc.reset(),
|
||||
breakerBloc.reset(),
|
||||
]);
|
||||
await HydratedBloc.storage.clear();
|
||||
await const CacheView().clear();
|
||||
} catch (e, s) {
|
||||
log('User state wipe failed: $e', stackTrace: s);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,16 @@ import 'package:jiffy/jiffy.dart';
|
||||
import 'loadable_state_event.dart';
|
||||
import 'loadable_state_state.dart';
|
||||
|
||||
class LoadableStateBloc extends Bloc<LoadableStateEvent, LoadableStateState> {
|
||||
class LoadableStateBloc extends Bloc<LoadableStateEvent, LoadableStateState>
|
||||
with WidgetsBindingObserver {
|
||||
late StreamSubscription<List<ConnectivityResult>> _updateStream;
|
||||
void Function()? reFetch;
|
||||
|
||||
/// Last time [reFetch] was triggered by an [AppLifecycleState.resumed]
|
||||
/// event. Used to coalesce rapid foreground/background flips so we don't
|
||||
/// spam the network when the user briefly checks notifications.
|
||||
DateTime _lastResumeRefetch = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
|
||||
LoadableStateBloc() : super(const LoadableStateState(connections: null)) {
|
||||
on<ConnectivityChanged>((event, emit) {
|
||||
emit(event.state);
|
||||
@@ -25,6 +31,23 @@ class LoadableStateBloc extends Bloc<LoadableStateEvent, LoadableStateState> {
|
||||
|
||||
Connectivity().checkConnectivity().then(emitConnectivity);
|
||||
_updateStream = Connectivity().onConnectivityChanged.listen(emitConnectivity);
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state != AppLifecycleState.resumed) return;
|
||||
final now = DateTime.now();
|
||||
if (now.difference(_lastResumeRefetch) < const Duration(seconds: 10)) 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.
|
||||
unawaited(Connectivity().checkConnectivity().then(
|
||||
(result) => add(ConnectivityChanged(LoadableStateState(connections: result))),
|
||||
));
|
||||
}
|
||||
|
||||
bool connectivityStatusKnown() => state.connections != null;
|
||||
@@ -55,6 +78,7 @@ class LoadableStateBloc extends Bloc<LoadableStateEvent, LoadableStateState> {
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_updateStream.cancel();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
+17
@@ -60,10 +60,27 @@ abstract class LoadableHydratedBloc<
|
||||
error: event.error
|
||||
)));
|
||||
|
||||
on<Reset<TState>>((event, emit) => emit(const LoadableState(
|
||||
isLoading: false,
|
||||
data: null,
|
||||
lastFetch: null,
|
||||
reFetch: null,
|
||||
error: null,
|
||||
)));
|
||||
|
||||
_repository = repository();
|
||||
fetch();
|
||||
}
|
||||
|
||||
/// Wipes this bloc's persisted state and resets the in-memory state to an
|
||||
/// empty, non-loading shell. Intended for logout: callers must trigger a
|
||||
/// fresh [fetch] (e.g. via [retry] or page-specific refresh) once the user
|
||||
/// is authenticated again, otherwise the UI would stay blank.
|
||||
Future<void> reset() async {
|
||||
await clear();
|
||||
add(Reset<TState>());
|
||||
}
|
||||
|
||||
TState? get innerState => state.data;
|
||||
TRepository get repo => _repository;
|
||||
|
||||
|
||||
+1
@@ -14,3 +14,4 @@ class Error<TState> extends LoadableHydratedBlocEvent<TState> {
|
||||
Error(this.error);
|
||||
}
|
||||
class RefetchStarted<TState> extends LoadableHydratedBlocEvent<TState> {}
|
||||
class Reset<TState> extends LoadableHydratedBlocEvent<TState> {}
|
||||
|
||||
@@ -10,7 +10,7 @@ class Roomplan extends StatelessWidget {
|
||||
title: const Text('Raumplan'),
|
||||
),
|
||||
body: PhotoView(
|
||||
imageProvider: Image.asset('assets/img/raumplan.jpg').image,
|
||||
imageProvider: Image.asset('assets/img/raumplan.png').image,
|
||||
minScale: 0.5,
|
||||
maxScale: 2.0,
|
||||
backgroundDecoration: BoxDecoration(color: Theme.of(context).colorScheme.surface),
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../../../model/account_data.dart';
|
||||
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../../../widget/centered_leading.dart';
|
||||
import '../../../../widget/confirm_dialog.dart';
|
||||
import '../../../../widget/debug/cache_view.dart';
|
||||
|
||||
class AccountSection extends StatelessWidget {
|
||||
const AccountSection({super.key});
|
||||
@@ -26,16 +22,13 @@ class AccountSection extends StatelessWidget {
|
||||
title: 'Abmelden?',
|
||||
content: 'Möchtest du dich wirklich abmelden?',
|
||||
confirmButton: 'Abmelden',
|
||||
onConfirmAsync: () async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.clear();
|
||||
PaintingBinding.instance.imageCache.clear();
|
||||
if (!context.mounted) return;
|
||||
await context.read<SettingsCubit>().reset();
|
||||
await const CacheView().clear();
|
||||
if (!context.mounted) return;
|
||||
await AccountData().removeData(context: context);
|
||||
},
|
||||
// Cleanup of caches, hydrated bloc storage and bloc in-memory state is
|
||||
// handled by the AccountBloc listener in main.dart on the loggedOut
|
||||
// transition. Doing the cleanup *before* setting loggedOut caused
|
||||
// rebuilds in the still-mounted App tree (TimetableBloc/ChatListBloc
|
||||
// emitting empty states) which raced with the home-route swap and
|
||||
// produced a black screen.
|
||||
onConfirmAsync: () => AccountData().removeData(context: context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ class ChatBubbleStyles {
|
||||
|
||||
BubbleStyle getSystemStyle() => BubbleStyle(
|
||||
color: AppTheme.isDarkMode(context) ? const Color(0xff182229) : Colors.white,
|
||||
borderWidth: 1,
|
||||
elevation: 2,
|
||||
margin: const BubbleEdges.only(bottom: 20, top: 10),
|
||||
alignment: Alignment.center,
|
||||
@@ -35,7 +34,6 @@ class ChatBubbleStyles {
|
||||
return BubbleStyle(
|
||||
nip: BubbleNip.leftTop,
|
||||
color: seamless ? Colors.transparent : color,
|
||||
borderWidth: seamless ? 0 : 1,
|
||||
elevation: seamless ? 0 : 1,
|
||||
margin: const BubbleEdges.only(bottom: 10, left: 10, right: 50),
|
||||
alignment: Alignment.topLeft,
|
||||
@@ -47,7 +45,6 @@ class ChatBubbleStyles {
|
||||
return BubbleStyle(
|
||||
nip: BubbleNip.rightBottom,
|
||||
color: seamless ? Colors.transparent : color,
|
||||
borderWidth: seamless ? 0 : 1,
|
||||
elevation: seamless ? 0 : 1,
|
||||
margin: const BubbleEdges.only(bottom: 10, right: 10, left: 50),
|
||||
alignment: Alignment.topRight,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
@@ -18,8 +19,9 @@ class WebuntisLessonSheet {
|
||||
final state = bloc.state.data;
|
||||
if (state == null) return;
|
||||
|
||||
final subject = _resolveSubject(state, lesson);
|
||||
final room = _resolveRoom(state, lesson);
|
||||
final headerSubject = _resolveSubject(state, lesson.su.firstOrNull?.id);
|
||||
final headerTitle = _firstNonEmpty([headerSubject.alternateName, headerSubject.name, headerSubject.longName, '?']);
|
||||
final headerLongName = headerSubject.longName.isNotEmpty && headerSubject.longName != headerTitle ? headerSubject.longName : '';
|
||||
|
||||
showAppointmentBottomSheet(
|
||||
context,
|
||||
@@ -28,12 +30,12 @@ class WebuntisLessonSheet {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'${_codePrefix(lesson.code)}${subject.alternateName}',
|
||||
'${_codePrefix(lesson.code)}$headerTitle',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 25),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(subject.longName),
|
||||
if (headerLongName.isNotEmpty) Text(headerLongName),
|
||||
Text(
|
||||
'${Jiffy.parseFromDateTime(appointment.startTime).format(pattern: 'HH:mm')} - '
|
||||
'${Jiffy.parseFromDateTime(appointment.endTime).format(pattern: 'HH:mm')}',
|
||||
@@ -42,68 +44,210 @@ class WebuntisLessonSheet {
|
||||
],
|
||||
),
|
||||
),
|
||||
body: (_) => SliverChildListDelegate([
|
||||
body: (_) => SliverChildListDelegate(<Widget>[
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.notifications_active),
|
||||
title: Text('Status: ${lesson.code != null ? "Geändert" : "Regulär"}'),
|
||||
title: Text('Status: ${_statusLabel(lesson.code)}'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.room),
|
||||
title: Text('Raum: ${room.name} (${room.longName})'),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.house_outlined),
|
||||
onPressed: () => AppRoutes.openRoomplan(context),
|
||||
if (lesson.su.length > 1)
|
||||
_listTile(
|
||||
icon: Icons.book_outlined,
|
||||
label: 'Fächer',
|
||||
entries: lesson.su.map((s) {
|
||||
final resolved = _resolveSubject(state, s.id);
|
||||
return _formatLine(
|
||||
_firstNonEmpty([resolved.name, s.name, '?']),
|
||||
longname: _firstNonEmpty([resolved.longName, s.longname, '']),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person),
|
||||
title: lesson.te.isNotEmpty
|
||||
? Text(
|
||||
'Lehrkraft: ${lesson.te[0].name}'
|
||||
'${lesson.te[0].longname.isNotEmpty ? " (${lesson.te[0].longname})" : ""}',
|
||||
)
|
||||
: const Text('?'),
|
||||
trailing: Visibility(
|
||||
visible: !kReleaseMode,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.textsms_outlined),
|
||||
onPressed: () => UnimplementedDialog.show(context),
|
||||
),
|
||||
_roomTile(context, state, lesson),
|
||||
_teacherTile(context, lesson),
|
||||
if ((lesson.activityType ?? '').trim().isNotEmpty)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.abc),
|
||||
title: Text('Typ: ${lesson.activityType}'),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.abc),
|
||||
title: Text('Typ: ${lesson.activityType}'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.people),
|
||||
title: Text('Klasse(n): ${lesson.kl.map((e) => e.name).join(", ")}'),
|
||||
),
|
||||
if (lesson.kl.isNotEmpty)
|
||||
_listTile(
|
||||
icon: Icons.people,
|
||||
label: lesson.kl.length == 1 ? 'Klasse' : 'Klassen',
|
||||
entries: lesson.kl
|
||||
.map((k) => _formatLine(
|
||||
k.name.isNotEmpty ? k.name : '?',
|
||||
longname: k.longname,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
..._optionalTextTiles(lesson),
|
||||
DebugTile(context).jsonData(lesson.toJson()),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _roomTile(BuildContext context, TimetableState state, GetTimetableResponseObject lesson) {
|
||||
final trailing = IconButton(
|
||||
icon: const Icon(Icons.house_outlined),
|
||||
onPressed: () => AppRoutes.openRoomplan(context),
|
||||
);
|
||||
|
||||
if (lesson.ro.isEmpty) {
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.room),
|
||||
title: const Text('Raum: ?'),
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
|
||||
final entries = lesson.ro.map((r) {
|
||||
final resolved = _resolveRoom(state, r.id);
|
||||
final name = _firstNonEmpty([resolved.name, r.name, '?']);
|
||||
final longname = _firstNonEmpty([resolved.longName, r.longname, '']);
|
||||
final building = resolved.building.trim();
|
||||
return _formatLine(
|
||||
name,
|
||||
longname: longname,
|
||||
extra: (building.isNotEmpty && building != '?') ? building : null,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return _listTile(
|
||||
icon: Icons.room,
|
||||
label: lesson.ro.length == 1 ? 'Raum' : 'Räume',
|
||||
entries: entries,
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _teacherTile(BuildContext context, GetTimetableResponseObject lesson) {
|
||||
final trailing = Visibility(
|
||||
visible: !kReleaseMode,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.textsms_outlined),
|
||||
onPressed: () => UnimplementedDialog.show(context),
|
||||
),
|
||||
);
|
||||
|
||||
if (lesson.te.isEmpty) {
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.person),
|
||||
title: const Text('Lehrkraft: ?'),
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
|
||||
final entries = lesson.te.map((t) {
|
||||
final base = _formatLine(
|
||||
t.name.isNotEmpty ? t.name : '?',
|
||||
longname: t.longname,
|
||||
);
|
||||
final orgname = (t.orgname ?? '').trim();
|
||||
return orgname.isEmpty ? base : '$base · ehemals $orgname';
|
||||
}).toList();
|
||||
|
||||
return _listTile(
|
||||
icon: Icons.person,
|
||||
label: lesson.te.length == 1 ? 'Lehrkraft' : 'Lehrkräfte',
|
||||
entries: entries,
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _listTile({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required List<String> entries,
|
||||
Widget? trailing,
|
||||
}) {
|
||||
if (entries.length == 1) {
|
||||
return ListTile(
|
||||
leading: Icon(icon),
|
||||
title: Text('$label: ${entries.first}'),
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
return ListTile(
|
||||
leading: Icon(icon),
|
||||
title: Text(label),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: entries.map<Widget>(Text.new).toList(),
|
||||
),
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
|
||||
static List<Widget> _optionalTextTiles(GetTimetableResponseObject lesson) {
|
||||
return <Widget?>[
|
||||
_textTile(Icons.info_outline, 'Info', lesson.info),
|
||||
_textTile(Icons.swap_horiz, 'Vertretungstext', lesson.substText),
|
||||
_textTile(Icons.subject, 'Stundentext', lesson.lstext),
|
||||
_textTile(Icons.category_outlined, 'Stundentyp', lesson.lstype),
|
||||
_textTile(Icons.flag_outlined, 'Statusmerkmale', lesson.statflags),
|
||||
_textTile(Icons.school_outlined, 'Lerngruppe', lesson.sg),
|
||||
_textTile(Icons.bookmark_outline, 'Buchungshinweis', lesson.bkRemark),
|
||||
_textTile(Icons.notes, 'Buchungstext', lesson.bkText),
|
||||
].whereType<Widget>().toList();
|
||||
}
|
||||
|
||||
static Widget? _textTile(IconData icon, String label, String? value) {
|
||||
final text = (value ?? '').trim();
|
||||
if (text.isEmpty) return null;
|
||||
return ListTile(
|
||||
leading: Icon(icon),
|
||||
title: Text(label),
|
||||
subtitle: Text(text),
|
||||
);
|
||||
}
|
||||
|
||||
static String _formatLine(String name, {String? longname, String? extra}) {
|
||||
final parts = <String>[
|
||||
if (name.isNotEmpty) name else '?',
|
||||
];
|
||||
final ln = (longname ?? '').trim();
|
||||
if (ln.isNotEmpty && ln != name) parts.add('($ln)');
|
||||
final ex = (extra ?? '').trim();
|
||||
if (ex.isNotEmpty) parts.add('· $ex');
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
static String _firstNonEmpty(List<String> values) {
|
||||
for (final v in values) {
|
||||
if (v.trim().isNotEmpty) return v;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
static String _statusLabel(String? code) {
|
||||
switch (code) {
|
||||
case null:
|
||||
case '':
|
||||
return 'Regulär';
|
||||
case 'cancelled':
|
||||
return 'Entfällt';
|
||||
case 'irregular':
|
||||
return 'Geändert';
|
||||
default:
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
static String _codePrefix(String? code) {
|
||||
if (code == 'cancelled') return 'Entfällt: ';
|
||||
if (code == 'irregular') return 'Änderung: ';
|
||||
return code ?? '';
|
||||
}
|
||||
|
||||
static GetSubjectsResponseObject _resolveSubject(TimetableState state, GetTimetableResponseObject lesson) {
|
||||
try {
|
||||
return state.subjects!.result.firstWhere((s) => s.id == lesson.su[0].id);
|
||||
} catch (_) {
|
||||
return GetSubjectsResponseObject(0, '?', 'Unbekannt', '?', true);
|
||||
}
|
||||
static GetSubjectsResponseObject _resolveSubject(TimetableState state, int? id) {
|
||||
final fallback = GetSubjectsResponseObject(0, '?', 'Unbekannt', '?', true);
|
||||
if (id == null) return fallback;
|
||||
return state.subjects?.result.firstWhereOrNull((s) => s.id == id) ?? fallback;
|
||||
}
|
||||
|
||||
static GetRoomsResponseObject _resolveRoom(TimetableState state, GetTimetableResponseObject lesson) {
|
||||
try {
|
||||
return state.rooms!.result.firstWhere((r) => r.id == lesson.ro[0].id);
|
||||
} catch (_) {
|
||||
return GetRoomsResponseObject(0, '?', 'Unbekannt', true, '?');
|
||||
}
|
||||
static GetRoomsResponseObject _resolveRoom(TimetableState state, int? id) {
|
||||
final fallback = GetRoomsResponseObject(0, '?', 'Unbekannt', true, '');
|
||||
if (id == null) return fallback;
|
||||
return state.rooms?.result.firstWhereOrNull((r) => r.id == id) ?? fallback;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user