simplified code across the app and consolidated duplicated helpers

This commit is contained in:
2026-09-27 17:29:54 +02:00
parent 3e856d0b1c
commit ee1240f71a
155 changed files with 1772 additions and 2546 deletions
@@ -1,14 +1,13 @@
import 'dart:developer';
import '../../../../../api/marianumconnect/queries/timetable_custom_events/custom_events_migration.dart';
import '../../../../../api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
import '../../../../../api/mhsl/custom_timetable_event/custom_timetable_event.dart';
import '../../../../../extensions/date_time.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 '../data_provider/timetable_data_provider.dart';
import '../repository/timetable_repository.dart';
import 'timetable_event.dart';
import 'timetable_state.dart';
import 'timetable_week_loader.dart';
class TimetableBloc
extends
@@ -16,11 +15,10 @@ class TimetableBloc
TimetableEvent,
TimetableState,
TimetableRepository
> {
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].
>
with TimetableWeekLoader<TimetableRepository> {
/// Set by [retry] to force the next [gatherData] to bypass the custom-event
/// cache and actually hit the network. Cleared at the top of [gatherData].
bool _forceRenew = false;
@override
@@ -33,24 +31,21 @@ class TimetableBloc
TimetableRepository repository() => TimetableRepository();
@override
TimetableState fromNothing() {
final reference = DateTime.now().addDays(2);
return TimetableState(
startDate: reference.mondayOfWeek,
endDate: _endOfWeek(reference),
);
}
TimetableDataProvider get referenceData => repo.data;
@override
Future<TimetableGetWeekResponse> fetchWeek(DateTime start, DateTime end) =>
repo.data.getWeek(start, end);
/// Persisted state may carry a stale `startDate`/`endDate` from the user's
/// last view. Reset on every cold start so the calendar always mounts on
/// the current week, not on whatever week the user closed the app on.
@override
TimetableState fromStorage(Map<String, dynamic> json) {
final stored = TimetableState.fromJson(json);
final reference = DateTime.now().addDays(2);
return stored.copyWith(
startDate: reference.mondayOfWeek,
endDate: _endOfWeek(reference),
final (start, end) = currentTimetableWeek();
return TimetableState.fromJson(json).copyWith(
startDate: start,
endDate: end,
accessibleStartDate: null,
accessibleEndDate: null,
);
@@ -60,50 +55,14 @@ class TimetableBloc
Map<String, dynamic>? toStorage(TimetableState state) => state.toJson();
@override
Future<void> gatherData() async {
final initial = innerState ?? fromNothing();
Future<void> gatherData() {
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);
return gatherWeek(
also: (onError) => _loadCustomEvents(onError: onError, renew: renew),
);
}
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)));
runInSession(() {
_loadCurrentWeek(startDate, endDate);
_prefetchAdjacentWeeks(startDate, endDate);
});
}
void resetWeek() {
final reference = DateTime.now().addDays(2);
changeWeek(reference.mondayOfWeek, _endOfWeek(reference));
}
void refresh() => fetch();
Future<void> addCustomEvent(CustomTimetableEvent event) async {
await repo.data.addCustomEvent(event);
await _refreshCustomEvents();
@@ -132,67 +91,10 @@ class TimetableBloc
}
Future<void> _refreshSubjects() async {
final subjects = await repo.data.getSubjects(renew: true);
final subjects = await repo.data.getSubjects();
add(DataGathered((s) => s.withReferenceData(subjects: subjects)));
}
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) {
log('getWeek error for $startDate–$endDate: $e');
onError?.call(e);
}
}
Future<void> _loadStaticReferenceData({
void Function(Object)? onError,
bool renew = false,
}) async {
try {
final (rooms, subjects, schoolHolidays, schoolyear) = await (
repo.data.getRooms(onError: onError, renew: renew),
repo.data.getSubjects(onError: onError, renew: renew),
repo.data.getSchoolHolidays(onError: onError, renew: renew),
repo.data.getCurrentSchoolyear(onError: onError, renew: renew),
).wait;
add(
Emit(
(s) => s.withReferenceData(
rooms: rooms,
subjects: subjects,
schoolHolidays: schoolHolidays,
schoolyear: schoolyear,
),
),
);
} catch (e) {
onError?.call(e);
}
try {
final timegrid = await repo.data.getTimegrid(renew: renew);
add(Emit((s) => s.withReferenceData(timegrid: timegrid)));
} catch (_) {
// Timegrid load failure falls back to a hardcoded schedule in the UI layer.
}
}
Future<void> _loadCustomEvents({
void Function(Object)? onError,
bool renew = false,
@@ -213,27 +115,4 @@ class TimetableBloc
final events = await repo.data.getCustomEvents(renew: true);
add(DataGathered((s) => s.withReferenceData(customEvents: events)));
}
void _prefetchAdjacentWeeks(DateTime start, DateTime end) {
_prefetchWeek(start.subtractDays(7), end.subtractDays(7));
_prefetchWeek(start.addDays(7), end.addDays(7));
}
void _prefetchWeek(DateTime start, DateTime end) {
repo.data
.getWeek(start, end)
.then((week) => _writeWeekToCache(start, week))
.catchError((_) {});
}
void _writeWeekToCache(DateTime weekStart, TimetableGetWeekResponse week) {
add(Emit((s) => s.withFetchedWeek(weekStart, week)));
}
static DateTime _endOfWeek(DateTime reference) {
final friday = reference.addDays(
DateTime.daysPerWeek - reference.weekday - 2,
);
return DateTime(friday.year, friday.month, friday.day);
}
}
@@ -98,9 +98,9 @@ abstract class TimetableState with _$TimetableState {
);
}
bool get hasReferenceData =>
rooms != null &&
subjects != null &&
schoolHolidays != null &&
customEvents != null;
/// Everything a foreign plan needs to render; it never has custom events.
bool get hasSchoolReferenceData =>
rooms != null && subjects != null && schoolHolidays != null;
bool get hasReferenceData => hasSchoolReferenceData && customEvents != null;
}
@@ -0,0 +1,138 @@
import 'dart:developer';
import '../../../../../api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
import '../../../../../extensions/date_time.dart';
import '../../../infrastructure/repository/repository.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 '../data_provider/timetable_data_provider.dart';
import 'timetable_event.dart';
import 'timetable_state.dart';
/// The date whose week the timetable opens on — two days ahead, so Saturday
/// and Sunday already show the next week.
DateTime timetableAnchorDate() => DateTime.now().addDays(2);
/// Monday 00:00 to Friday 00:00 of the week the timetable opens on.
(DateTime, DateTime) currentTimetableWeek() {
final reference = timetableAnchorDate();
final friday = reference.addDays(
DateTime.daysPerWeek - reference.weekday - 2,
);
return (reference.mondayOfWeek, friday.dateOnly);
}
/// Week navigation, week loading/prefetching and the school-wide reference
/// data, shared by the own and the foreign timetable.
mixin TimetableWeekLoader<R extends Repository<TimetableState>>
on LoadableHydratedBloc<TimetableEvent, TimetableState, R> {
DateTime _lastWeekRequestStart = DateTime.fromMillisecondsSinceEpoch(0);
Future<TimetableGetWeekResponse> fetchWeek(DateTime start, DateTime end);
TimetableDataProvider get referenceData;
@override
TimetableState fromNothing() {
final (start, end) = currentTimetableWeek();
return TimetableState(startDate: start, endDate: end);
}
/// Loads the viewed week and the reference data (plus [also]), fails with
/// the first error of any of them and prefetches the neighbouring weeks.
Future<void> gatherWeek({
Future<void> Function(void Function(Object) onError)? also,
}) async {
final initial = innerState ?? fromNothing();
Object? firstError;
void recordError(Object e) => firstError ??= e;
await Future.wait([
_loadWeek(initial.startDate, initial.endDate, onError: recordError),
_loadReferenceData(onError: recordError),
if (also != null) also(recordError),
]);
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)));
runInSession(() {
_loadWeek(startDate, endDate);
_prefetchAdjacentWeeks(startDate, endDate);
});
}
void refresh() => fetch();
Future<void> _loadWeek(
DateTime startDate,
DateTime endDate, {
void Function(Object)? onError,
}) async {
final requestStart = DateTime.now();
_lastWeekRequestStart = requestStart;
try {
final week = await fetchWeek(startDate, endDate);
if (_lastWeekRequestStart.isAfter(requestStart)) return;
_writeWeekToCache(startDate, week);
} catch (e) {
log('Week load error for $startDate–$endDate: $e');
onError?.call(e);
}
}
Future<void> _loadReferenceData({void Function(Object)? onError}) async {
try {
final (rooms, subjects, schoolHolidays, schoolyear) = await (
referenceData.getRooms(onError: onError),
referenceData.getSubjects(onError: onError),
referenceData.getSchoolHolidays(onError: onError),
referenceData.getCurrentSchoolyear(onError: onError),
).wait;
add(
Emit(
(s) => s.withReferenceData(
rooms: rooms,
subjects: subjects,
schoolHolidays: schoolHolidays,
schoolyear: schoolyear,
),
),
);
} catch (e) {
onError?.call(e);
}
try {
final timegrid = await referenceData.getTimegrid();
add(Emit((s) => s.withReferenceData(timegrid: timegrid)));
} catch (_) {
// Timegrid load failure falls back to a hardcoded schedule in the UI.
}
}
void _prefetchAdjacentWeeks(DateTime start, DateTime end) {
_prefetchWeek(start.subtractDays(7), end.subtractDays(7));
_prefetchWeek(start.addDays(7), end.addDays(7));
}
void _prefetchWeek(DateTime start, DateTime end) {
fetchWeek(
start,
end,
).then((week) => _writeWeekToCache(start, week)).catchError((_) {});
}
void _writeWeekToCache(DateTime weekStart, TimetableGetWeekResponse week) {
add(Emit((s) => s.withFetchedWeek(weekStart, week)));
}
}