simplified code across the app and consolidated duplicated helpers
This commit is contained in:
@@ -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)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user