import 'package:intl/intl.dart'; import '../../../../../api/mhsl/custom_timetable_event/custom_timetable_event.dart'; import '../../../../../api/webuntis/queries/get_timetable/get_timetable_response.dart'; import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart'; import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/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); /// Set by [retry] to force the next [gatherData] to bypass cache freshness /// checks and actually hit the network. Cleared at the top of [gatherData]. bool _forceRenew = false; @override void retry() { _forceRenew = true; super.retry(); } @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(); final renew = _forceRenew; _forceRenew = false; Object? firstError; void recordError(Object e) { firstError ??= e; } await Future.wait([ _loadCurrentWeek(initial.startDate, initial.endDate, onError: recordError, renew: renew), _loadStaticReferenceData(onError: recordError, renew: renew), _loadCustomEvents(onError: recordError, renew: renew), ]); if (firstError != null) throw firstError!; add(DataGathered((s) => s)); _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, { void Function(Object)? onError, bool renew = false, }) async { final requestStart = DateTime.now(); _lastWeekRequestStart = requestStart; try { final week = await repo.data.getWeek(startDate, endDate, onError: onError, renew: renew); if (_lastWeekRequestStart.isAfter(requestStart)) return; _writeWeekToCache(startDate, week); } catch (e) { onError?.call(e); } } Future _loadStaticReferenceData({ void Function(Object)? onError, bool renew = false, }) async { try { final (rooms, subjects, schoolHolidays) = await ( repo.data.getRooms(onError: onError, renew: renew), repo.data.getSubjects(onError: onError, renew: renew), repo.data.getSchoolHolidays(onError: onError, renew: renew), ).wait; add(Emit((s) => s.copyWith( rooms: rooms, subjects: subjects, schoolHolidays: schoolHolidays, dataVersion: s.dataVersion + 1, ))); } catch (e) { onError?.call(e); } try { final timegrid = await repo.data.getTimegrid(renew: renew); add(Emit((s) => s.copyWith(timegrid: timegrid, dataVersion: s.dataVersion + 1))); } catch (_) { // Timegrid load failure falls back to a hardcoded schedule in the UI layer. } } Future _loadCustomEvents({ void Function(Object)? onError, bool renew = false, }) async { try { final events = await repo.data.getCustomEvents(renew: renew, onError: onError); add(Emit((s) => s.copyWith(customEvents: events, dataVersion: s.dataVersion + 1))); } catch (e) { onError?.call(e); } } Future _refreshCustomEvents() async { final events = await repo.data.getCustomEvents(renew: true); add(DataGathered((s) => s.copyWith(customEvents: events, dataVersion: s.dataVersion + 1))); } 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(Emit((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); } }