189 lines
6.1 KiB
Dart
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);
|
|
}
|
|
}
|