import 'package:intl/intl.dart'; import '../../../../../api/mhsl/customTimetableEvent/customTimetableEvent.dart'; import '../../../../../api/webuntis/queries/getTimetable/getTimetableResponse.dart'; import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart'; import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart'; import '../repository/timetable_repository.dart'; import 'timetable_event.dart'; import 'timetable_state.dart'; class TimetableBloc extends LoadableHydratedBloc { static const Duration _weekSpan = Duration(days: 7); static final DateFormat _weekKeyFormat = DateFormat('yyyyMMdd'); DateTime _lastWeekRequestStart = DateTime.fromMillisecondsSinceEpoch(0); @override TimetableRepository repository() => TimetableRepository(); @override TimetableState fromNothing() { final reference = DateTime.now().add(const Duration(days: 2)); return TimetableState( startDate: _startOfWeek(reference), endDate: _endOfWeek(reference), ); } @override TimetableState fromStorage(Map json) => TimetableState.fromJson(json); @override Map? toStorage(TimetableState state) => state.toJson(); @override Future gatherData() async { final initial = innerState ?? fromNothing(); await Future.wait([ _loadCurrentWeek(initial.startDate, initial.endDate), _loadStaticReferenceData(), _loadCustomEvents(), ]); _prefetchAdjacentWeeks(initial.startDate, initial.endDate); } void changeWeek(DateTime startDate, DateTime endDate) { final current = innerState ?? fromNothing(); if (current.startDate == startDate && current.endDate == endDate) return; add(Emit((s) => s.copyWith(startDate: startDate, endDate: endDate))); _loadCurrentWeek(startDate, endDate); _prefetchAdjacentWeeks(startDate, endDate); } void resetWeek() { final reference = DateTime.now().add(const Duration(days: 2)); changeWeek(_startOfWeek(reference), _endOfWeek(reference)); } void refresh() => fetch(); Future addCustomEvent(CustomTimetableEvent event) async { await repo.data.addCustomEvent(event); await _refreshCustomEvents(); } Future updateCustomEvent(String id, CustomTimetableEvent event) async { await repo.data.updateCustomEvent(id, event); await _refreshCustomEvents(); } Future removeCustomEvent(String id) async { await repo.data.removeCustomEvent(id); await _refreshCustomEvents(); } Future _loadCurrentWeek(DateTime startDate, DateTime endDate) async { final requestStart = DateTime.now(); _lastWeekRequestStart = requestStart; try { final week = await repo.data.getWeek(startDate, endDate); if (_lastWeekRequestStart.isAfter(requestStart)) return; _writeWeekToCache(startDate, week); } catch (_) { // Errors are surfaced via LoadableHydratedBloc.fetch's catchError. rethrow; } } Future _loadStaticReferenceData() async { final (rooms, subjects, schoolHolidays) = await ( repo.data.getRooms(), repo.data.getSubjects(), repo.data.getSchoolHolidays(), ).wait; add(DataGathered((s) => s.copyWith( rooms: rooms, subjects: subjects, schoolHolidays: schoolHolidays, dataVersion: s.dataVersion + 1, ))); } Future _loadCustomEvents({bool renew = false}) async { final events = await repo.data.getCustomEvents(renew: renew); add(DataGathered((s) => s.copyWith(customEvents: events, dataVersion: s.dataVersion + 1))); } Future _refreshCustomEvents() => _loadCustomEvents(renew: true); void _prefetchAdjacentWeeks(DateTime start, DateTime end) { _prefetchWeek(start.subtract(_weekSpan), end.subtract(_weekSpan)); _prefetchWeek(start.add(_weekSpan), end.add(_weekSpan)); } void _prefetchWeek(DateTime start, DateTime end) { repo.data.getWeek(start, end).then((week) => _writeWeekToCache(start, week)).catchError((_) {}); } void _writeWeekToCache(DateTime weekStart, GetTimetableResponse week) { final key = _weekKeyFormat.format(weekStart); add(DataGathered((s) { final updated = Map.of(s.weekCache); updated[key] = week; return s.copyWith(weekCache: updated, dataVersion: s.dataVersion + 1); })); } static DateTime _startOfWeek(DateTime reference) { final monday = reference.subtract(Duration(days: reference.weekday - 1)); return DateTime(monday.year, monday.month, monday.day); } static DateTime _endOfWeek(DateTime reference) { final friday = reference.add(Duration(days: DateTime.daysPerWeek - reference.weekday - 2)); return DateTime(friday.year, friday.month, friday.day); } }