widget refresh enhancements
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:workmanager/workmanager.dart';
|
||||
@@ -32,12 +33,20 @@ class WidgetBackgroundTask {
|
||||
|
||||
static const Duration periodicFrequency = Duration(minutes: 30);
|
||||
|
||||
/// A snapshot younger than this is considered fresh enough — a second
|
||||
/// trigger within the window (push + periodic slot coinciding) is skipped.
|
||||
static const Duration refreshDebounce = Duration(minutes: 10);
|
||||
|
||||
static Future<void> initialize() async {
|
||||
await Workmanager().initialize(_callbackDispatcher);
|
||||
await Workmanager().registerPeriodicTask(
|
||||
periodicTaskName,
|
||||
periodicTaskName,
|
||||
frequency: periodicFrequency,
|
||||
// iOS ignores `frequency:` and instead uses initialDelay as the
|
||||
// BGAppRefresh earliestBeginDate on every (auto-)resubmission —
|
||||
// without it each completed run is immediately eligible again.
|
||||
initialDelay: Platform.isIOS ? periodicFrequency : Duration.zero,
|
||||
constraints: Constraints(networkType: NetworkType.connected),
|
||||
existingWorkPolicy: ExistingPeriodicWorkPolicy.keep,
|
||||
backoffPolicy: BackoffPolicy.linear,
|
||||
@@ -45,7 +54,21 @@ class WidgetBackgroundTask {
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> requestImmediateRefresh() async {
|
||||
/// Single owner of the platform strategy for "refresh soon": Android
|
||||
/// enqueues a WorkManager one-off (retry + network constraint included),
|
||||
/// iOS runs inline — one-off Workmanager tasks there only execute
|
||||
/// in-process anyway, so the direct call is equivalent and skips the extra
|
||||
/// background engine. [inlineTimeout] bounds the inline path for callers
|
||||
/// with a hard budget (FCM handler).
|
||||
static Future<void> requestImmediateRefresh({
|
||||
bool force = true,
|
||||
Duration? inlineTimeout,
|
||||
}) async {
|
||||
if (Platform.isIOS) {
|
||||
final refresh = runRefreshNow(force: force);
|
||||
await (inlineTimeout == null ? refresh : refresh.timeout(inlineTimeout));
|
||||
return;
|
||||
}
|
||||
await Workmanager().registerOneOffTask(
|
||||
'$oneOffTaskName-${DateTime.now().millisecondsSinceEpoch}',
|
||||
oneOffTaskName,
|
||||
@@ -54,24 +77,61 @@ class WidgetBackgroundTask {
|
||||
);
|
||||
}
|
||||
|
||||
/// Shared refresh entry for the periodic worker, push triggers, and login.
|
||||
/// Throws on fetch failure so the worker path can signal a retry.
|
||||
static Future<void> runRefreshNow({bool force = false}) async {
|
||||
await WidgetSync.ensureInitialized();
|
||||
bool populated;
|
||||
try {
|
||||
// Bounded: a hanging keystore read must not stall the caller's budget
|
||||
// (FCM handler ~25s on iOS) forever.
|
||||
populated = await AccountData().waitForPopulation().timeout(
|
||||
const Duration(seconds: 10),
|
||||
);
|
||||
} on TimeoutException {
|
||||
populated = false;
|
||||
}
|
||||
if (!populated) {
|
||||
// Deliberately does NOT flip the widget to logged-out: a failed or slow
|
||||
// keychain read (locked iOS device during the 06:00 silent push) is
|
||||
// indistinguishable from "never logged in" here, and blanking the
|
||||
// widget on a transient failure is worse than keeping the snapshot.
|
||||
// Logout/login manage the flag explicitly (WidgetSync.clear / login).
|
||||
log('[widget-refresh] credentials unavailable, skipping refresh');
|
||||
return;
|
||||
}
|
||||
final fetchedAt = await WidgetSync.getFetchedAt();
|
||||
if (shouldSkipRefresh(fetchedAt: fetchedAt, now: DateTime.now(), force: force)) {
|
||||
log('[widget-refresh] snapshot is fresh, skipping refresh');
|
||||
return;
|
||||
}
|
||||
await _refresh();
|
||||
}
|
||||
|
||||
static Future<void> cancelAll() async {
|
||||
await Workmanager().cancelAll();
|
||||
}
|
||||
}
|
||||
|
||||
/// Pure debounce decision so it stays unit-testable. A `fetchedAt` in the
|
||||
/// future (clock change, debug time shift) never skips — refreshing is the
|
||||
/// safe direction.
|
||||
bool shouldSkipRefresh({
|
||||
required DateTime? fetchedAt,
|
||||
required DateTime now,
|
||||
required bool force,
|
||||
}) {
|
||||
if (force || fetchedAt == null) return false;
|
||||
final age = now.difference(fetchedAt);
|
||||
return !age.isNegative && age < WidgetBackgroundTask.refreshDebounce;
|
||||
}
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
void _callbackDispatcher() {
|
||||
Workmanager().executeTask((task, inputData) async {
|
||||
try {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await AccountData().waitForPopulation();
|
||||
if (!AccountData().isPopulated()) {
|
||||
log('[widget-bg] not logged in, skipping refresh');
|
||||
await WidgetSync.setLoggedIn(false);
|
||||
await WidgetSync.triggerUpdate();
|
||||
return true;
|
||||
}
|
||||
await _refresh();
|
||||
await WidgetBackgroundTask.runRefreshNow();
|
||||
return true;
|
||||
} on Exception catch (e, s) {
|
||||
log('[widget-bg] refresh failed: $e', stackTrace: s);
|
||||
@@ -94,35 +154,44 @@ Future<void> _refresh() async {
|
||||
}
|
||||
|
||||
final now = WidgetPublisher.widgetNow();
|
||||
// 14-day window so the week-widget rolls forward into next Monday's
|
||||
// lessons on Friday evening.
|
||||
final weekStart = _startOfWeek(now);
|
||||
final weekEndExclusive = weekStart.add(const Duration(days: 14));
|
||||
// Fetch window matches the week payload's window so the widget can roll
|
||||
// forward into next week's lessons without fresh data.
|
||||
final weekStart = WidgetDataMapper.startOfCalendarWeek(now);
|
||||
final weekEndExclusive = weekStart.add(
|
||||
const Duration(days: WidgetDataMapper.weekWindowDays),
|
||||
);
|
||||
|
||||
final timetable = await TimetableGetWeek().run(
|
||||
// All six requests are independent — run them concurrently so the total
|
||||
// latency is the slowest request, not the sum (matters for the push path's
|
||||
// hard time budget). Reference-data failures fall through to null in the
|
||||
// mapper rather than aborting the whole refresh.
|
||||
final timetableFuture = TimetableGetWeek().run(
|
||||
from: weekStart,
|
||||
until: weekEndExclusive.subtract(const Duration(days: 1)),
|
||||
);
|
||||
|
||||
// Reference data — failures fall through to null in the mapper rather
|
||||
// than aborting the whole refresh.
|
||||
final subjects = await _runOrNull<TimetableGetSubjectsResponse>(
|
||||
final subjectsFuture = _runOrNull<TimetableGetSubjectsResponse>(
|
||||
() => TimetableGetSubjects().run(),
|
||||
);
|
||||
final rooms = await _runOrNull<TimetableGetRoomsResponse>(
|
||||
final roomsFuture = _runOrNull<TimetableGetRoomsResponse>(
|
||||
() => TimetableGetRooms().run(),
|
||||
);
|
||||
final holidays = await _runOrNull<TimetableGetHolidaysResponse>(
|
||||
final holidaysFuture = _runOrNull<TimetableGetHolidaysResponse>(
|
||||
() => TimetableGetHolidays().run(),
|
||||
);
|
||||
final timegrid = await _runOrNull<TimetableGetTimegridResponse>(
|
||||
final timegridFuture = _runOrNull<TimetableGetTimegridResponse>(
|
||||
() => TimetableGetTimegrid().run(),
|
||||
);
|
||||
final customEvents = await _runOrNull<GetCustomTimetableEventResponse>(
|
||||
final customEventsFuture = _runOrNull<GetCustomTimetableEventResponse>(
|
||||
() => GetCustomTimetableEvent(
|
||||
GetCustomTimetableEventParams(AccountData().getUserSecret()),
|
||||
).run(),
|
||||
);
|
||||
final timetable = await timetableFuture;
|
||||
final subjects = await subjectsFuture;
|
||||
final rooms = await roomsFuture;
|
||||
final holidays = await holidaysFuture;
|
||||
final timegrid = await timegridFuture;
|
||||
final customEvents = await customEventsFuture;
|
||||
|
||||
final lessons = timetable.entries;
|
||||
|
||||
@@ -158,11 +227,6 @@ Future<void> _refresh() async {
|
||||
);
|
||||
}
|
||||
|
||||
DateTime _startOfWeek(DateTime reference) {
|
||||
final monday = reference.subtract(Duration(days: reference.weekday - 1));
|
||||
return DateTime(monday.year, monday.month, monday.day);
|
||||
}
|
||||
|
||||
Future<T?> _runOrNull<T>(Future<T> Function() task) async {
|
||||
try {
|
||||
return await task();
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:developer';
|
||||
import 'package:crypton/crypton.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
|
||||
import '../background/widget_background_task.dart';
|
||||
import '../notification/notification_service.dart';
|
||||
import 'chat_thread_store.dart';
|
||||
import 'nid_store.dart';
|
||||
@@ -12,6 +13,10 @@ import 'push_registration_store.dart';
|
||||
import 'push_renderer.dart';
|
||||
import 'push_subject.dart';
|
||||
|
||||
/// Wire value of the FCM `type` field for silent widget-refresh pushes.
|
||||
/// Mirrors PUSH_TYPE_WIDGET_REFRESH in MarianumConnect's MarMobileApiService.
|
||||
const String widgetRefreshPushType = 'widget-refresh';
|
||||
|
||||
/// How an incoming FCM payload should be interpreted.
|
||||
enum PushKind {
|
||||
/// Encrypted Nextcloud push-v2 notification (`subject` + `signature`).
|
||||
@@ -20,6 +25,11 @@ enum PushKind {
|
||||
/// Plaintext MarianumConnect direct push (`source == "connect"`).
|
||||
connect,
|
||||
|
||||
/// Silent MarianumConnect push requesting a home-widget data refresh
|
||||
/// (`source == "connect"` + `type == "widget-refresh"`). Never rendered,
|
||||
/// processed even with notifications off.
|
||||
widgetRefresh,
|
||||
|
||||
/// Neither — ignored.
|
||||
unknown,
|
||||
}
|
||||
@@ -31,7 +41,10 @@ PushKind classifyPush(Map<String, dynamic> data) {
|
||||
final hasSubject = (data['subject'] as String?)?.isNotEmpty ?? false;
|
||||
final hasSignature = (data['signature'] as String?)?.isNotEmpty ?? false;
|
||||
if (hasSubject && hasSignature) return PushKind.nextcloud;
|
||||
if (data['source'] == 'connect') return PushKind.connect;
|
||||
if (data['source'] == 'connect') {
|
||||
if (data['type'] == widgetRefreshPushType) return PushKind.widgetRefresh;
|
||||
return PushKind.connect;
|
||||
}
|
||||
return PushKind.unknown;
|
||||
}
|
||||
|
||||
@@ -97,11 +110,30 @@ class PushMessageHandler {
|
||||
notificationsEnabled: notificationsEnabled,
|
||||
);
|
||||
break;
|
||||
case PushKind.widgetRefresh:
|
||||
// Deliberately before any notificationsEnabled gate: silent sync
|
||||
// pushes must work with notifications off.
|
||||
await _handleWidgetRefresh();
|
||||
break;
|
||||
case PushKind.unknown:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleWidgetRefresh() async {
|
||||
try {
|
||||
// The iOS FCM handler runs in the main isolate with a ~25s APNs
|
||||
// budget — bound the inline refresh below that so the completion
|
||||
// handler always fires in time.
|
||||
await WidgetBackgroundTask.requestImmediateRefresh(
|
||||
force: false,
|
||||
inlineTimeout: const Duration(seconds: 20),
|
||||
);
|
||||
} on Exception catch (e) {
|
||||
log('[push] widget refresh failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleConnect(
|
||||
RemoteMessage message, {
|
||||
required bool foreground,
|
||||
|
||||
@@ -61,6 +61,20 @@ abstract class WidgetPeriod with _$WidgetPeriod {
|
||||
_$WidgetPeriodFromJson(json);
|
||||
}
|
||||
|
||||
/// Per-day metadata for the week payload, so native renderers can derive a
|
||||
/// single day's view (including its holiday state) without a day payload.
|
||||
@freezed
|
||||
abstract class WidgetDayInfo with _$WidgetDayInfo {
|
||||
const factory WidgetDayInfo({
|
||||
required DateTime date,
|
||||
@Default(false) bool isHoliday,
|
||||
String? holidayName,
|
||||
}) = _WidgetDayInfo;
|
||||
|
||||
factory WidgetDayInfo.fromJson(Map<String, Object?> json) =>
|
||||
_$WidgetDayInfoFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class WidgetTimetableData with _$WidgetTimetableData {
|
||||
const factory WidgetTimetableData({
|
||||
@@ -73,6 +87,8 @@ abstract class WidgetTimetableData with _$WidgetTimetableData {
|
||||
@Default(<WidgetPeriod>[]) List<WidgetPeriod> periods,
|
||||
@Default(false) bool isHoliday,
|
||||
String? holidayName,
|
||||
/// Week payload only: one entry per day of the covered window.
|
||||
@Default(<WidgetDayInfo>[]) List<WidgetDayInfo> days,
|
||||
}) = _WidgetTimetableData;
|
||||
|
||||
factory WidgetTimetableData.fromJson(Map<String, Object?> json) =>
|
||||
|
||||
@@ -593,13 +593,283 @@ as int,
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$WidgetDayInfo {
|
||||
|
||||
DateTime get date; bool get isHoliday; String? get holidayName;
|
||||
/// Create a copy of WidgetDayInfo
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$WidgetDayInfoCopyWith<WidgetDayInfo> get copyWith => _$WidgetDayInfoCopyWithImpl<WidgetDayInfo>(this as WidgetDayInfo, _$identity);
|
||||
|
||||
/// Serializes this WidgetDayInfo to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is WidgetDayInfo&&(identical(other.date, date) || other.date == date)&&(identical(other.isHoliday, isHoliday) || other.isHoliday == isHoliday)&&(identical(other.holidayName, holidayName) || other.holidayName == holidayName));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,date,isHoliday,holidayName);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'WidgetDayInfo(date: $date, isHoliday: $isHoliday, holidayName: $holidayName)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $WidgetDayInfoCopyWith<$Res> {
|
||||
factory $WidgetDayInfoCopyWith(WidgetDayInfo value, $Res Function(WidgetDayInfo) _then) = _$WidgetDayInfoCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
DateTime date, bool isHoliday, String? holidayName
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$WidgetDayInfoCopyWithImpl<$Res>
|
||||
implements $WidgetDayInfoCopyWith<$Res> {
|
||||
_$WidgetDayInfoCopyWithImpl(this._self, this._then);
|
||||
|
||||
final WidgetDayInfo _self;
|
||||
final $Res Function(WidgetDayInfo) _then;
|
||||
|
||||
/// Create a copy of WidgetDayInfo
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? date = null,Object? isHoliday = null,Object? holidayName = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,isHoliday: null == isHoliday ? _self.isHoliday : isHoliday // ignore: cast_nullable_to_non_nullable
|
||||
as bool,holidayName: freezed == holidayName ? _self.holidayName : holidayName // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [WidgetDayInfo].
|
||||
extension WidgetDayInfoPatterns on WidgetDayInfo {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _WidgetDayInfo value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _WidgetDayInfo() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _WidgetDayInfo value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _WidgetDayInfo():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _WidgetDayInfo value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _WidgetDayInfo() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( DateTime date, bool isHoliday, String? holidayName)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _WidgetDayInfo() when $default != null:
|
||||
return $default(_that.date,_that.isHoliday,_that.holidayName);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( DateTime date, bool isHoliday, String? holidayName) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _WidgetDayInfo():
|
||||
return $default(_that.date,_that.isHoliday,_that.holidayName);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( DateTime date, bool isHoliday, String? holidayName)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _WidgetDayInfo() when $default != null:
|
||||
return $default(_that.date,_that.isHoliday,_that.holidayName);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _WidgetDayInfo implements WidgetDayInfo {
|
||||
const _WidgetDayInfo({required this.date, this.isHoliday = false, this.holidayName});
|
||||
factory _WidgetDayInfo.fromJson(Map<String, dynamic> json) => _$WidgetDayInfoFromJson(json);
|
||||
|
||||
@override final DateTime date;
|
||||
@override@JsonKey() final bool isHoliday;
|
||||
@override final String? holidayName;
|
||||
|
||||
/// Create a copy of WidgetDayInfo
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$WidgetDayInfoCopyWith<_WidgetDayInfo> get copyWith => __$WidgetDayInfoCopyWithImpl<_WidgetDayInfo>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$WidgetDayInfoToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _WidgetDayInfo&&(identical(other.date, date) || other.date == date)&&(identical(other.isHoliday, isHoliday) || other.isHoliday == isHoliday)&&(identical(other.holidayName, holidayName) || other.holidayName == holidayName));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,date,isHoliday,holidayName);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'WidgetDayInfo(date: $date, isHoliday: $isHoliday, holidayName: $holidayName)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$WidgetDayInfoCopyWith<$Res> implements $WidgetDayInfoCopyWith<$Res> {
|
||||
factory _$WidgetDayInfoCopyWith(_WidgetDayInfo value, $Res Function(_WidgetDayInfo) _then) = __$WidgetDayInfoCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
DateTime date, bool isHoliday, String? holidayName
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$WidgetDayInfoCopyWithImpl<$Res>
|
||||
implements _$WidgetDayInfoCopyWith<$Res> {
|
||||
__$WidgetDayInfoCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _WidgetDayInfo _self;
|
||||
final $Res Function(_WidgetDayInfo) _then;
|
||||
|
||||
/// Create a copy of WidgetDayInfo
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? date = null,Object? isHoliday = null,Object? holidayName = freezed,}) {
|
||||
return _then(_WidgetDayInfo(
|
||||
date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,isHoliday: null == isHoliday ? _self.isHoliday : isHoliday // ignore: cast_nullable_to_non_nullable
|
||||
as bool,holidayName: freezed == holidayName ? _self.holidayName : holidayName // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$WidgetTimetableData {
|
||||
|
||||
DateTime get fetchedAt;/// The day this widget snapshot is "about" — display anchor.
|
||||
/// For the day variant: the rendered school day.
|
||||
/// For the week variant: the Monday of the rendered school week.
|
||||
DateTime get anchorDate; List<WidgetLesson> get lessons; List<WidgetPeriod> get periods; bool get isHoliday; String? get holidayName;
|
||||
DateTime get anchorDate; List<WidgetLesson> get lessons; List<WidgetPeriod> get periods; bool get isHoliday; String? get holidayName;/// Week payload only: one entry per day of the covered window.
|
||||
List<WidgetDayInfo> get days;
|
||||
/// Create a copy of WidgetTimetableData
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -612,16 +882,16 @@ $WidgetTimetableDataCopyWith<WidgetTimetableData> get copyWith => _$WidgetTimeta
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is WidgetTimetableData&&(identical(other.fetchedAt, fetchedAt) || other.fetchedAt == fetchedAt)&&(identical(other.anchorDate, anchorDate) || other.anchorDate == anchorDate)&&const DeepCollectionEquality().equals(other.lessons, lessons)&&const DeepCollectionEquality().equals(other.periods, periods)&&(identical(other.isHoliday, isHoliday) || other.isHoliday == isHoliday)&&(identical(other.holidayName, holidayName) || other.holidayName == holidayName));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is WidgetTimetableData&&(identical(other.fetchedAt, fetchedAt) || other.fetchedAt == fetchedAt)&&(identical(other.anchorDate, anchorDate) || other.anchorDate == anchorDate)&&const DeepCollectionEquality().equals(other.lessons, lessons)&&const DeepCollectionEquality().equals(other.periods, periods)&&(identical(other.isHoliday, isHoliday) || other.isHoliday == isHoliday)&&(identical(other.holidayName, holidayName) || other.holidayName == holidayName)&&const DeepCollectionEquality().equals(other.days, days));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,fetchedAt,anchorDate,const DeepCollectionEquality().hash(lessons),const DeepCollectionEquality().hash(periods),isHoliday,holidayName);
|
||||
int get hashCode => Object.hash(runtimeType,fetchedAt,anchorDate,const DeepCollectionEquality().hash(lessons),const DeepCollectionEquality().hash(periods),isHoliday,holidayName,const DeepCollectionEquality().hash(days));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'WidgetTimetableData(fetchedAt: $fetchedAt, anchorDate: $anchorDate, lessons: $lessons, periods: $periods, isHoliday: $isHoliday, holidayName: $holidayName)';
|
||||
return 'WidgetTimetableData(fetchedAt: $fetchedAt, anchorDate: $anchorDate, lessons: $lessons, periods: $periods, isHoliday: $isHoliday, holidayName: $holidayName, days: $days)';
|
||||
}
|
||||
|
||||
|
||||
@@ -632,7 +902,7 @@ abstract mixin class $WidgetTimetableDataCopyWith<$Res> {
|
||||
factory $WidgetTimetableDataCopyWith(WidgetTimetableData value, $Res Function(WidgetTimetableData) _then) = _$WidgetTimetableDataCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
DateTime fetchedAt, DateTime anchorDate, List<WidgetLesson> lessons, List<WidgetPeriod> periods, bool isHoliday, String? holidayName
|
||||
DateTime fetchedAt, DateTime anchorDate, List<WidgetLesson> lessons, List<WidgetPeriod> periods, bool isHoliday, String? holidayName, List<WidgetDayInfo> days
|
||||
});
|
||||
|
||||
|
||||
@@ -649,7 +919,7 @@ class _$WidgetTimetableDataCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of WidgetTimetableData
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? fetchedAt = null,Object? anchorDate = null,Object? lessons = null,Object? periods = null,Object? isHoliday = null,Object? holidayName = freezed,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? fetchedAt = null,Object? anchorDate = null,Object? lessons = null,Object? periods = null,Object? isHoliday = null,Object? holidayName = freezed,Object? days = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
fetchedAt: null == fetchedAt ? _self.fetchedAt : fetchedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,anchorDate: null == anchorDate ? _self.anchorDate : anchorDate // ignore: cast_nullable_to_non_nullable
|
||||
@@ -657,7 +927,8 @@ as DateTime,lessons: null == lessons ? _self.lessons : lessons // ignore: cast_n
|
||||
as List<WidgetLesson>,periods: null == periods ? _self.periods : periods // ignore: cast_nullable_to_non_nullable
|
||||
as List<WidgetPeriod>,isHoliday: null == isHoliday ? _self.isHoliday : isHoliday // ignore: cast_nullable_to_non_nullable
|
||||
as bool,holidayName: freezed == holidayName ? _self.holidayName : holidayName // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
as String?,days: null == days ? _self.days : days // ignore: cast_nullable_to_non_nullable
|
||||
as List<WidgetDayInfo>,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -742,10 +1013,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( DateTime fetchedAt, DateTime anchorDate, List<WidgetLesson> lessons, List<WidgetPeriod> periods, bool isHoliday, String? holidayName)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( DateTime fetchedAt, DateTime anchorDate, List<WidgetLesson> lessons, List<WidgetPeriod> periods, bool isHoliday, String? holidayName, List<WidgetDayInfo> days)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _WidgetTimetableData() when $default != null:
|
||||
return $default(_that.fetchedAt,_that.anchorDate,_that.lessons,_that.periods,_that.isHoliday,_that.holidayName);case _:
|
||||
return $default(_that.fetchedAt,_that.anchorDate,_that.lessons,_that.periods,_that.isHoliday,_that.holidayName,_that.days);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -763,10 +1034,10 @@ return $default(_that.fetchedAt,_that.anchorDate,_that.lessons,_that.periods,_th
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( DateTime fetchedAt, DateTime anchorDate, List<WidgetLesson> lessons, List<WidgetPeriod> periods, bool isHoliday, String? holidayName) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( DateTime fetchedAt, DateTime anchorDate, List<WidgetLesson> lessons, List<WidgetPeriod> periods, bool isHoliday, String? holidayName, List<WidgetDayInfo> days) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _WidgetTimetableData():
|
||||
return $default(_that.fetchedAt,_that.anchorDate,_that.lessons,_that.periods,_that.isHoliday,_that.holidayName);case _:
|
||||
return $default(_that.fetchedAt,_that.anchorDate,_that.lessons,_that.periods,_that.isHoliday,_that.holidayName,_that.days);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
@@ -783,10 +1054,10 @@ return $default(_that.fetchedAt,_that.anchorDate,_that.lessons,_that.periods,_th
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( DateTime fetchedAt, DateTime anchorDate, List<WidgetLesson> lessons, List<WidgetPeriod> periods, bool isHoliday, String? holidayName)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( DateTime fetchedAt, DateTime anchorDate, List<WidgetLesson> lessons, List<WidgetPeriod> periods, bool isHoliday, String? holidayName, List<WidgetDayInfo> days)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _WidgetTimetableData() when $default != null:
|
||||
return $default(_that.fetchedAt,_that.anchorDate,_that.lessons,_that.periods,_that.isHoliday,_that.holidayName);case _:
|
||||
return $default(_that.fetchedAt,_that.anchorDate,_that.lessons,_that.periods,_that.isHoliday,_that.holidayName,_that.days);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -798,7 +1069,7 @@ return $default(_that.fetchedAt,_that.anchorDate,_that.lessons,_that.periods,_th
|
||||
@JsonSerializable()
|
||||
|
||||
class _WidgetTimetableData implements WidgetTimetableData {
|
||||
const _WidgetTimetableData({required this.fetchedAt, required this.anchorDate, required final List<WidgetLesson> lessons, final List<WidgetPeriod> periods = const <WidgetPeriod>[], this.isHoliday = false, this.holidayName}): _lessons = lessons,_periods = periods;
|
||||
const _WidgetTimetableData({required this.fetchedAt, required this.anchorDate, required final List<WidgetLesson> lessons, final List<WidgetPeriod> periods = const <WidgetPeriod>[], this.isHoliday = false, this.holidayName, final List<WidgetDayInfo> days = const <WidgetDayInfo>[]}): _lessons = lessons,_periods = periods,_days = days;
|
||||
factory _WidgetTimetableData.fromJson(Map<String, dynamic> json) => _$WidgetTimetableDataFromJson(json);
|
||||
|
||||
@override final DateTime fetchedAt;
|
||||
@@ -822,6 +1093,15 @@ class _WidgetTimetableData implements WidgetTimetableData {
|
||||
|
||||
@override@JsonKey() final bool isHoliday;
|
||||
@override final String? holidayName;
|
||||
/// Week payload only: one entry per day of the covered window.
|
||||
final List<WidgetDayInfo> _days;
|
||||
/// Week payload only: one entry per day of the covered window.
|
||||
@override@JsonKey() List<WidgetDayInfo> get days {
|
||||
if (_days is EqualUnmodifiableListView) return _days;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_days);
|
||||
}
|
||||
|
||||
|
||||
/// Create a copy of WidgetTimetableData
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -836,16 +1116,16 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _WidgetTimetableData&&(identical(other.fetchedAt, fetchedAt) || other.fetchedAt == fetchedAt)&&(identical(other.anchorDate, anchorDate) || other.anchorDate == anchorDate)&&const DeepCollectionEquality().equals(other._lessons, _lessons)&&const DeepCollectionEquality().equals(other._periods, _periods)&&(identical(other.isHoliday, isHoliday) || other.isHoliday == isHoliday)&&(identical(other.holidayName, holidayName) || other.holidayName == holidayName));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _WidgetTimetableData&&(identical(other.fetchedAt, fetchedAt) || other.fetchedAt == fetchedAt)&&(identical(other.anchorDate, anchorDate) || other.anchorDate == anchorDate)&&const DeepCollectionEquality().equals(other._lessons, _lessons)&&const DeepCollectionEquality().equals(other._periods, _periods)&&(identical(other.isHoliday, isHoliday) || other.isHoliday == isHoliday)&&(identical(other.holidayName, holidayName) || other.holidayName == holidayName)&&const DeepCollectionEquality().equals(other._days, _days));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,fetchedAt,anchorDate,const DeepCollectionEquality().hash(_lessons),const DeepCollectionEquality().hash(_periods),isHoliday,holidayName);
|
||||
int get hashCode => Object.hash(runtimeType,fetchedAt,anchorDate,const DeepCollectionEquality().hash(_lessons),const DeepCollectionEquality().hash(_periods),isHoliday,holidayName,const DeepCollectionEquality().hash(_days));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'WidgetTimetableData(fetchedAt: $fetchedAt, anchorDate: $anchorDate, lessons: $lessons, periods: $periods, isHoliday: $isHoliday, holidayName: $holidayName)';
|
||||
return 'WidgetTimetableData(fetchedAt: $fetchedAt, anchorDate: $anchorDate, lessons: $lessons, periods: $periods, isHoliday: $isHoliday, holidayName: $holidayName, days: $days)';
|
||||
}
|
||||
|
||||
|
||||
@@ -856,7 +1136,7 @@ abstract mixin class _$WidgetTimetableDataCopyWith<$Res> implements $WidgetTimet
|
||||
factory _$WidgetTimetableDataCopyWith(_WidgetTimetableData value, $Res Function(_WidgetTimetableData) _then) = __$WidgetTimetableDataCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
DateTime fetchedAt, DateTime anchorDate, List<WidgetLesson> lessons, List<WidgetPeriod> periods, bool isHoliday, String? holidayName
|
||||
DateTime fetchedAt, DateTime anchorDate, List<WidgetLesson> lessons, List<WidgetPeriod> periods, bool isHoliday, String? holidayName, List<WidgetDayInfo> days
|
||||
});
|
||||
|
||||
|
||||
@@ -873,7 +1153,7 @@ class __$WidgetTimetableDataCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of WidgetTimetableData
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? fetchedAt = null,Object? anchorDate = null,Object? lessons = null,Object? periods = null,Object? isHoliday = null,Object? holidayName = freezed,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? fetchedAt = null,Object? anchorDate = null,Object? lessons = null,Object? periods = null,Object? isHoliday = null,Object? holidayName = freezed,Object? days = null,}) {
|
||||
return _then(_WidgetTimetableData(
|
||||
fetchedAt: null == fetchedAt ? _self.fetchedAt : fetchedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,anchorDate: null == anchorDate ? _self.anchorDate : anchorDate // ignore: cast_nullable_to_non_nullable
|
||||
@@ -881,7 +1161,8 @@ as DateTime,lessons: null == lessons ? _self._lessons : lessons // ignore: cast_
|
||||
as List<WidgetLesson>,periods: null == periods ? _self._periods : periods // ignore: cast_nullable_to_non_nullable
|
||||
as List<WidgetPeriod>,isHoliday: null == isHoliday ? _self.isHoliday : isHoliday // ignore: cast_nullable_to_non_nullable
|
||||
as bool,holidayName: freezed == holidayName ? _self.holidayName : holidayName // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
as String?,days: null == days ? _self._days : days // ignore: cast_nullable_to_non_nullable
|
||||
as List<WidgetDayInfo>,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,20 @@ Map<String, dynamic> _$WidgetPeriodToJson(_WidgetPeriod instance) =>
|
||||
'virtualEndMinutes': instance.virtualEndMinutes,
|
||||
};
|
||||
|
||||
_WidgetDayInfo _$WidgetDayInfoFromJson(Map<String, dynamic> json) =>
|
||||
_WidgetDayInfo(
|
||||
date: DateTime.parse(json['date'] as String),
|
||||
isHoliday: json['isHoliday'] as bool? ?? false,
|
||||
holidayName: json['holidayName'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$WidgetDayInfoToJson(_WidgetDayInfo instance) =>
|
||||
<String, dynamic>{
|
||||
'date': instance.date.toIso8601String(),
|
||||
'isHoliday': instance.isHoliday,
|
||||
'holidayName': instance.holidayName,
|
||||
};
|
||||
|
||||
_WidgetTimetableData _$WidgetTimetableDataFromJson(Map<String, dynamic> json) =>
|
||||
_WidgetTimetableData(
|
||||
fetchedAt: DateTime.parse(json['fetchedAt'] as String),
|
||||
@@ -77,6 +91,11 @@ _WidgetTimetableData _$WidgetTimetableDataFromJson(Map<String, dynamic> json) =>
|
||||
const <WidgetPeriod>[],
|
||||
isHoliday: json['isHoliday'] as bool? ?? false,
|
||||
holidayName: json['holidayName'] as String?,
|
||||
days:
|
||||
(json['days'] as List<dynamic>?)
|
||||
?.map((e) => WidgetDayInfo.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const <WidgetDayInfo>[],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$WidgetTimetableDataToJson(
|
||||
@@ -88,4 +107,5 @@ Map<String, dynamic> _$WidgetTimetableDataToJson(
|
||||
'periods': instance.periods,
|
||||
'isHoliday': instance.isHoliday,
|
||||
'holidayName': instance.holidayName,
|
||||
'days': instance.days,
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import '../api/marianumconnect/queries/timetable_get_timegrid/timetable_get_time
|
||||
import '../api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
|
||||
import '../api/mhsl/custom_timetable_event/custom_timetable_event.dart';
|
||||
import '../api/mhsl/custom_timetable_event/get/get_custom_timetable_event_response.dart';
|
||||
import '../extensions/date_time.dart';
|
||||
import '../view/pages/timetable/data/lesson_merger.dart';
|
||||
import '../view/pages/timetable/data/lesson_period_schedule.dart';
|
||||
import '../view/pages/timetable/data/lesson_status.dart';
|
||||
@@ -34,12 +35,20 @@ class WidgetDataMapper {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
static DateTime resolveWeekAnchor(DateTime now) {
|
||||
final anchor = resolveDayAnchor(now);
|
||||
final monday = anchor.subtract(Duration(days: anchor.weekday - 1));
|
||||
static DateTime resolveWeekAnchor(DateTime now) =>
|
||||
startOfCalendarWeek(resolveDayAnchor(now));
|
||||
|
||||
/// Monday of the calendar week containing [reference] — no roll-forward,
|
||||
/// unlike [resolveWeekAnchor]. Start of the week payload's 14-day window.
|
||||
static DateTime startOfCalendarWeek(DateTime reference) {
|
||||
final monday = reference.subtract(Duration(days: reference.weekday - 1));
|
||||
return DateTime(monday.year, monday.month, monday.day);
|
||||
}
|
||||
|
||||
/// Days covered by the week payload: current calendar week + the next, so
|
||||
/// native renderers can roll the view forward without fresh data.
|
||||
static const int weekWindowDays = 14;
|
||||
|
||||
static WidgetTimetableData buildDayData({
|
||||
required DateTime now,
|
||||
required Iterable<McTimetableEntry> lessons,
|
||||
@@ -83,10 +92,14 @@ class WidgetDataMapper {
|
||||
bool connectDoubleLessons = true,
|
||||
}) {
|
||||
final anchor = resolveWeekAnchor(now);
|
||||
final endExclusive = anchor.add(const Duration(days: 5));
|
||||
// The window is anchored at the *current* calendar week, not the
|
||||
// (possibly rolled-forward) week anchor: on Friday evening the payload
|
||||
// must still contain today for renderers that derive day slices.
|
||||
final windowStart = startOfCalendarWeek(now);
|
||||
final endExclusive = windowStart.add(const Duration(days: weekWindowDays));
|
||||
final weekLessons = lessons.where((l) {
|
||||
final dt = l.startDateTime;
|
||||
return !dt.isBefore(anchor) && dt.isBefore(endExclusive);
|
||||
return !dt.isBefore(windowStart) && dt.isBefore(endExclusive);
|
||||
}).toList();
|
||||
// Per-day merge: otherwise a 4th-period lesson on Mon would collapse with
|
||||
// a 1st-period lesson on Tue if subject/teacher match.
|
||||
@@ -95,13 +108,38 @@ class WidgetDataMapper {
|
||||
: weekLessons;
|
||||
final mapped = <WidgetLesson>[
|
||||
...source.map((l) => _mapLesson(l, now, subjects, rooms)),
|
||||
..._expandCustomEvents(customEvents, anchor, endExclusive),
|
||||
..._expandCustomEvents(customEvents, windowStart, endExclusive),
|
||||
]..sort((a, b) => a.start.compareTo(b.start));
|
||||
final days = [
|
||||
for (var i = 0; i < weekWindowDays; i++)
|
||||
_dayInfo(windowStart.addDays(i), holidays),
|
||||
];
|
||||
// The anchor always lies inside the window; the orElse only guards the
|
||||
// impossible.
|
||||
final anchorInfo = days.firstWhere(
|
||||
(d) => d.date == anchor,
|
||||
orElse: () => _dayInfo(anchor, holidays),
|
||||
);
|
||||
return WidgetTimetableData(
|
||||
fetchedAt: now,
|
||||
anchorDate: anchor,
|
||||
lessons: _resolveCollisions(mapped),
|
||||
periods: _resolvePeriods(timegrid),
|
||||
isHoliday: anchorInfo.isHoliday,
|
||||
holidayName: anchorInfo.holidayName,
|
||||
days: days,
|
||||
);
|
||||
}
|
||||
|
||||
static WidgetDayInfo _dayInfo(
|
||||
DateTime day,
|
||||
TimetableGetHolidaysResponse? holidays,
|
||||
) {
|
||||
final holiday = _findHoliday(day, holidays);
|
||||
return WidgetDayInfo(
|
||||
date: day,
|
||||
isHoliday: holiday != null,
|
||||
holidayName: holiday?.longName,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,14 +12,18 @@ class WidgetSync {
|
||||
static const String iosAppGroupId =
|
||||
'group.eu.mhsl.marianum.mobile.client.widget';
|
||||
|
||||
static const String iosWidgetKind = 'TimetableWidget';
|
||||
// Must match the WidgetKit `kind` strings declared in
|
||||
// TimetableWidgetExtension.swift — a mismatch makes reloadTimelines a no-op.
|
||||
static const String iosDayWidgetKind = 'TimetableDayWidget';
|
||||
static const String iosWeekWidgetKind = 'TimetableWeekWidget';
|
||||
static const String androidDayProvider = 'TimetableDayWidget';
|
||||
static const String androidWeekProvider = 'TimetableWeekWidget';
|
||||
|
||||
// `_v1` suffix lets a future schema change invalidate stale snapshots
|
||||
// by bumping the key instead of risking a parse crash.
|
||||
// Version suffix lets a schema change invalidate stale snapshots by
|
||||
// bumping the key instead of risking a parse crash.
|
||||
static const String dayDataKey = 'widget_data_day_v1';
|
||||
static const String weekDataKey = 'widget_data_week_v1';
|
||||
// v2: 14-day window + per-day `days` holiday info.
|
||||
static const String weekDataKey = 'widget_data_week_v2';
|
||||
static const String fetchedAtKey = 'widget_data_fetched_at_v1';
|
||||
static const String loggedInKey = 'widget_data_logged_in_v1';
|
||||
// Mirrored into widget storage so the background isolate can read it
|
||||
@@ -90,6 +94,12 @@ class WidgetSync {
|
||||
return HomeWidget.getWidgetData<String>(marianumConnectBaseUrlKey);
|
||||
}
|
||||
|
||||
static Future<DateTime?> getFetchedAt() async {
|
||||
await ensureInitialized();
|
||||
final raw = await HomeWidget.getWidgetData<String>(fetchedAtKey);
|
||||
return raw == null ? null : DateTime.tryParse(raw);
|
||||
}
|
||||
|
||||
static Future<void> clear() async {
|
||||
await ensureInitialized();
|
||||
await HomeWidget.saveWidgetData<String>(dayDataKey, null);
|
||||
@@ -103,11 +113,11 @@ class WidgetSync {
|
||||
try {
|
||||
await HomeWidget.updateWidget(
|
||||
androidName: androidDayProvider,
|
||||
iOSName: iosWidgetKind,
|
||||
iOSName: iosDayWidgetKind,
|
||||
);
|
||||
await HomeWidget.updateWidget(
|
||||
androidName: androidWeekProvider,
|
||||
iOSName: iosWidgetKind,
|
||||
iOSName: iosWeekWidgetKind,
|
||||
);
|
||||
} on Exception catch (e) {
|
||||
log('WidgetSync.triggerUpdate failed: $e');
|
||||
|
||||
Reference in New Issue
Block a user