merged develop
This commit is contained in:
@@ -48,7 +48,7 @@ class TimetableBloc
|
||||
TimetableState fromNothing() {
|
||||
final reference = DateTime.now().addDays(2);
|
||||
return TimetableState(
|
||||
startDate: _startOfWeek(reference),
|
||||
startDate: reference.mondayOfWeek,
|
||||
endDate: _endOfWeek(reference),
|
||||
);
|
||||
}
|
||||
@@ -61,7 +61,7 @@ class TimetableBloc
|
||||
final stored = TimetableState.fromJson(json);
|
||||
final reference = DateTime.now().addDays(2);
|
||||
return stored.copyWith(
|
||||
startDate: _startOfWeek(reference),
|
||||
startDate: reference.mondayOfWeek,
|
||||
endDate: _endOfWeek(reference),
|
||||
accessibleStartDate: null,
|
||||
accessibleEndDate: null,
|
||||
@@ -120,13 +120,15 @@ class TimetableBloc
|
||||
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);
|
||||
runInSession(() {
|
||||
_loadCurrentWeek(startDate, endDate);
|
||||
_prefetchAdjacentWeeks(startDate, endDate);
|
||||
});
|
||||
}
|
||||
|
||||
void resetWeek() {
|
||||
final reference = DateTime.now().addDays(2);
|
||||
changeWeek(_startOfWeek(reference), _endOfWeek(reference));
|
||||
changeWeek(reference.mondayOfWeek, _endOfWeek(reference));
|
||||
}
|
||||
|
||||
void refresh() => fetch();
|
||||
@@ -171,11 +173,7 @@ class TimetableBloc
|
||||
|
||||
Future<void> _refreshSubjects() async {
|
||||
final subjects = await repo.data.getSubjects(renew: true);
|
||||
add(
|
||||
DataGathered(
|
||||
(s) => s.copyWith(subjects: subjects, dataVersion: s.dataVersion + 1),
|
||||
),
|
||||
);
|
||||
add(DataGathered((s) => s.withReferenceData(subjects: subjects)));
|
||||
}
|
||||
|
||||
Future<void> _loadCurrentWeek(
|
||||
@@ -214,12 +212,11 @@ class TimetableBloc
|
||||
|
||||
add(
|
||||
Emit(
|
||||
(s) => s.copyWith(
|
||||
(s) => s.withReferenceData(
|
||||
rooms: rooms,
|
||||
subjects: subjects,
|
||||
schoolHolidays: schoolHolidays,
|
||||
schoolyear: schoolyear,
|
||||
dataVersion: s.dataVersion + 1,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -229,11 +226,7 @@ class TimetableBloc
|
||||
|
||||
try {
|
||||
final timegrid = await repo.data.getTimegrid(renew: renew);
|
||||
add(
|
||||
Emit(
|
||||
(s) => s.copyWith(timegrid: timegrid, dataVersion: s.dataVersion + 1),
|
||||
),
|
||||
);
|
||||
add(Emit((s) => s.withReferenceData(timegrid: timegrid)));
|
||||
} catch (_) {
|
||||
// Timegrid load failure falls back to a hardcoded schedule in the UI layer.
|
||||
}
|
||||
@@ -249,12 +242,7 @@ class TimetableBloc
|
||||
renew: renew,
|
||||
onError: onError,
|
||||
);
|
||||
add(
|
||||
Emit(
|
||||
(s) =>
|
||||
s.copyWith(customEvents: events, dataVersion: s.dataVersion + 1),
|
||||
),
|
||||
);
|
||||
add(Emit((s) => s.withReferenceData(customEvents: events)));
|
||||
} catch (e) {
|
||||
onError?.call(e);
|
||||
}
|
||||
@@ -262,11 +250,7 @@ class TimetableBloc
|
||||
|
||||
Future<void> _refreshCustomEvents() async {
|
||||
final events = await repo.data.getCustomEvents(renew: true);
|
||||
add(
|
||||
DataGathered(
|
||||
(s) => s.copyWith(customEvents: events, dataVersion: s.dataVersion + 1),
|
||||
),
|
||||
);
|
||||
add(DataGathered((s) => s.withReferenceData(customEvents: events)));
|
||||
}
|
||||
|
||||
void _prefetchAdjacentWeeks(DateTime start, DateTime end) {
|
||||
@@ -282,19 +266,7 @@ class TimetableBloc
|
||||
}
|
||||
|
||||
void _writeWeekToCache(DateTime weekStart, TimetableGetWeekResponse week) {
|
||||
final key = weekStart.weekKey();
|
||||
add(
|
||||
Emit((s) {
|
||||
final updated = Map<String, TimetableGetWeekResponse>.of(s.weekCache);
|
||||
updated[key] = week;
|
||||
return s.copyWith(weekCache: updated, dataVersion: s.dataVersion + 1);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
static DateTime _startOfWeek(DateTime reference) {
|
||||
final monday = reference.subtractDays(reference.weekday - 1);
|
||||
return DateTime(monday.year, monday.month, monday.day);
|
||||
add(Emit((s) => s.withFetchedWeek(weekStart, week)));
|
||||
}
|
||||
|
||||
static DateTime _endOfWeek(DateTime reference) {
|
||||
|
||||
@@ -7,6 +7,8 @@ import '../../../../../api/marianumconnect/queries/timetable_get_subjects/timeta
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_timegrid/timetable_get_timegrid_response.dart';
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
|
||||
import '../../../../../api/mhsl/custom_timetable_event/get/get_custom_timetable_event_response.dart';
|
||||
import '../../../../../utils/json_equality.dart';
|
||||
import 'week_cache.dart';
|
||||
|
||||
part 'timetable_state.freezed.dart';
|
||||
part 'timetable_state.g.dart';
|
||||
@@ -40,6 +42,62 @@ abstract class TimetableState with _$TimetableState {
|
||||
Iterable<McTimetableEntry> getAllKnownLessons() =>
|
||||
weekCache.values.expand((response) => response.entries);
|
||||
|
||||
/// This state with [week] cached, or `this` when its content is unchanged
|
||||
/// (so the emit is dropped as equal).
|
||||
TimetableState withFetchedWeek(
|
||||
DateTime weekStart,
|
||||
TimetableGetWeekResponse week,
|
||||
) {
|
||||
final updated = mergeWeekIntoCache(
|
||||
weekCache,
|
||||
weekStart,
|
||||
week,
|
||||
viewedWeekStart: startDate,
|
||||
now: DateTime.now(),
|
||||
);
|
||||
if (updated == null) return this;
|
||||
return copyWith(weekCache: updated, dataVersion: dataVersion + 1);
|
||||
}
|
||||
|
||||
/// Applies freshly loaded reference data. Values whose content equals the
|
||||
/// current one keep the old instance, and when nothing changed at all this
|
||||
/// returns `this` — every `dataVersion` bump and new identity makes the
|
||||
/// calendar rebuild all appointments and break regions.
|
||||
TimetableState withReferenceData({
|
||||
TimetableGetRoomsResponse? rooms,
|
||||
TimetableGetSubjectsResponse? subjects,
|
||||
TimetableGetHolidaysResponse? schoolHolidays,
|
||||
TimetableGetSchoolyearResponse? schoolyear,
|
||||
TimetableGetTimegridResponse? timegrid,
|
||||
GetCustomTimetableEventResponse? customEvents,
|
||||
}) {
|
||||
T? changed<T>(T? next, T? current) =>
|
||||
next == null || sameJson(next, current) ? null : next;
|
||||
final newRooms = changed(rooms, this.rooms);
|
||||
final newSubjects = changed(subjects, this.subjects);
|
||||
final newHolidays = changed(schoolHolidays, this.schoolHolidays);
|
||||
final newSchoolyear = changed(schoolyear, this.schoolyear);
|
||||
final newTimegrid = changed(timegrid, this.timegrid);
|
||||
final newCustomEvents = changed(customEvents, this.customEvents);
|
||||
if (newRooms == null &&
|
||||
newSubjects == null &&
|
||||
newHolidays == null &&
|
||||
newSchoolyear == null &&
|
||||
newTimegrid == null &&
|
||||
newCustomEvents == null) {
|
||||
return this;
|
||||
}
|
||||
return copyWith(
|
||||
rooms: newRooms ?? this.rooms,
|
||||
subjects: newSubjects ?? this.subjects,
|
||||
schoolHolidays: newHolidays ?? this.schoolHolidays,
|
||||
schoolyear: newSchoolyear ?? this.schoolyear,
|
||||
timegrid: newTimegrid ?? this.timegrid,
|
||||
customEvents: newCustomEvents ?? this.customEvents,
|
||||
dataVersion: dataVersion + 1,
|
||||
);
|
||||
}
|
||||
|
||||
/// Whether the calendar has everything it needs to render. Custom events
|
||||
/// only exist for subjects that support them; requiring them elsewhere would
|
||||
/// keep foreign plans loading forever.
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
|
||||
import '../../../../../extensions/date_time.dart';
|
||||
import '../../../../../utils/json_equality.dart';
|
||||
|
||||
/// Weeks kept around the viewed week and around today's week. Everything
|
||||
/// further away is dropped: every state emit re-serializes the whole cache for
|
||||
/// HydratedBloc and the calendar rebuilds its appointments from all of it, so
|
||||
/// an unbounded cache makes week swipes slower the longer the app is used.
|
||||
const int kWeekCacheRadius = 8;
|
||||
|
||||
/// Returns the cache with [week] stored under [weekStart], pruned to the
|
||||
/// weeks near [viewedWeekStart] or [now]. Returns null when the stored week
|
||||
/// already has identical content, so callers can skip the emit entirely.
|
||||
Map<String, TimetableGetWeekResponse>? mergeWeekIntoCache(
|
||||
Map<String, TimetableGetWeekResponse> cache,
|
||||
DateTime weekStart,
|
||||
TimetableGetWeekResponse week, {
|
||||
required DateTime viewedWeekStart,
|
||||
required DateTime now,
|
||||
}) {
|
||||
final key = weekStart.weekKey();
|
||||
final existing = cache[key];
|
||||
if (sameJson(existing, week)) return null;
|
||||
|
||||
final viewedMonday = viewedWeekStart.mondayOfWeek;
|
||||
final todayMonday = now.mondayOfWeek;
|
||||
bool isNear(DateTime monday, DateTime anchor) =>
|
||||
monday.difference(anchor).inDays.abs() <= kWeekCacheRadius * 7 + 1;
|
||||
|
||||
final updated = <String, TimetableGetWeekResponse>{};
|
||||
for (final entry in cache.entries) {
|
||||
final monday = DateTime.tryParse(entry.key);
|
||||
if (monday == null ||
|
||||
isNear(monday, viewedMonday) ||
|
||||
isNear(monday, todayMonday)) {
|
||||
updated[entry.key] = entry.value;
|
||||
}
|
||||
}
|
||||
updated[key] = week;
|
||||
return updated;
|
||||
}
|
||||
Reference in New Issue
Block a user