Files
Client/lib/state/app/modules/timetable/bloc/timetable_bloc.dart
T
2026-05-05 13:49:45 +02:00

189 lines
6.1 KiB
Dart

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<TimetableEvent, TimetableState, TimetableRepository> {
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<String, dynamic> json) => TimetableState.fromJson(json);
@override
Map<String, dynamic>? toStorage(TimetableState state) => state.toJson();
@override
Future<void> 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<void> addCustomEvent(CustomTimetableEvent event) async {
await repo.data.addCustomEvent(event);
await _refreshCustomEvents();
}
Future<void> updateCustomEvent(String id, CustomTimetableEvent event) async {
await repo.data.updateCustomEvent(id, event);
await _refreshCustomEvents();
}
Future<void> removeCustomEvent(String id) async {
await repo.data.removeCustomEvent(id);
await _refreshCustomEvents();
}
Future<void> _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<void> _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<void> _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<void> _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<String, GetTimetableResponse>.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);
}
}