added base homescreen-widget setup, working on Android, iOS in progress
This commit is contained in:
+44
-1
@@ -13,15 +13,19 @@ import 'model/data_cleaner.dart';
|
||||
import 'notification/notification_controller.dart';
|
||||
import 'notification/notification_tasks.dart';
|
||||
import 'notification/notify_updater.dart';
|
||||
import 'routing/app_routes.dart';
|
||||
import 'state/app/modules/app_modules.dart';
|
||||
import 'state/app/modules/breaker/bloc/breaker_bloc.dart';
|
||||
import 'state/app/modules/chat_list/bloc/chat_list_bloc.dart';
|
||||
import 'state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import 'state/app/modules/timetable/bloc/timetable_bloc.dart';
|
||||
import 'state/app/modules/timetable/bloc/timetable_state.dart';
|
||||
import 'storage/settings.dart' as model;
|
||||
import 'utils/debouncer.dart';
|
||||
import 'view/pages/overhang.dart';
|
||||
import 'widget/breaker/breaker.dart';
|
||||
import 'widget_data/widget_navigation.dart';
|
||||
import 'widget_data/widget_publisher.dart';
|
||||
|
||||
class App extends StatefulWidget {
|
||||
const App({super.key});
|
||||
@@ -33,6 +37,7 @@ class App extends StatefulWidget {
|
||||
class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
late Timer _refetchChats;
|
||||
late Timer _updateTimings;
|
||||
StreamSubscription<dynamic>? _timetableWidgetSync;
|
||||
// Tracked via the bottom-nav controller's listener so it always reflects the
|
||||
// user's actual position, even between rapid setting emits where the
|
||||
// controller hasn't caught up to a scheduled jump yet.
|
||||
@@ -52,9 +57,16 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
log('Refreshing due to LifecycleChange');
|
||||
NotificationTasks.updateProviders(context);
|
||||
});
|
||||
_handlePendingWidgetNavigation();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handlePendingWidgetNavigation() async {
|
||||
final pending = await WidgetNavigation.consumePendingTimetableTap();
|
||||
if (!pending || !mounted) return;
|
||||
AppRoutes.goToTab(context, Modules.timetable);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -69,7 +81,37 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
// App is freshly mounted on every login (BlocConsumer in main.dart
|
||||
// swaps it in for Login), so this also covers the post-logout case
|
||||
// where the bloc was reset to an empty state and needs a fresh fetch.
|
||||
context.read<TimetableBloc>().refresh();
|
||||
final timetable = context.read<TimetableBloc>();
|
||||
timetable.refresh();
|
||||
// Push the freshest timetable state into the home-screen widget any
|
||||
// time the BLoC reports new data — without waiting for the periodic
|
||||
// background refresh. This is the "user just opened the app" path:
|
||||
// the widget gets the same data the user is looking at on screen.
|
||||
final settingsCubit = context.read<SettingsCubit>();
|
||||
_timetableWidgetSync?.cancel();
|
||||
_timetableWidgetSync = timetable.stream.listen((state) {
|
||||
final data = state.data;
|
||||
if (data is TimetableState && !state.isLoading) {
|
||||
unawaited(
|
||||
WidgetPublisher.publishFromBlocState(
|
||||
data,
|
||||
settings: settingsCubit.val(),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
// Also publish the current state once, in case data is already loaded
|
||||
// from hydrated storage before the listener attaches.
|
||||
final initialData = timetable.state.data;
|
||||
if (initialData is TimetableState) {
|
||||
unawaited(
|
||||
WidgetPublisher.publishFromBlocState(
|
||||
initialData,
|
||||
settings: settingsCubit.val(),
|
||||
),
|
||||
);
|
||||
}
|
||||
unawaited(_handlePendingWidgetNavigation());
|
||||
});
|
||||
|
||||
_updateTimings = Timer.periodic(const Duration(seconds: 30), (_) {
|
||||
@@ -115,6 +157,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
void dispose() {
|
||||
_refetchChats.cancel();
|
||||
_updateTimings.cancel();
|
||||
_timetableWidgetSync?.cancel();
|
||||
Main.bottomNavigator.removeListener(_onTabControllerChanged);
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:workmanager/workmanager.dart';
|
||||
|
||||
import '../api/mhsl/custom_timetable_event/get/get_custom_timetable_event.dart';
|
||||
import '../api/mhsl/custom_timetable_event/get/get_custom_timetable_event_params.dart';
|
||||
import '../api/mhsl/custom_timetable_event/get/get_custom_timetable_event_response.dart';
|
||||
import '../api/webuntis/queries/authenticate/authenticate.dart';
|
||||
import '../api/webuntis/queries/get_holidays/get_holidays.dart';
|
||||
import '../api/webuntis/queries/get_holidays/get_holidays_response.dart';
|
||||
import '../api/webuntis/queries/get_rooms/get_rooms.dart';
|
||||
import '../api/webuntis/queries/get_rooms/get_rooms_response.dart';
|
||||
import '../api/webuntis/queries/get_subjects/get_subjects.dart';
|
||||
import '../api/webuntis/queries/get_subjects/get_subjects_response.dart';
|
||||
import '../api/webuntis/queries/get_timegrid_units/get_timegrid_units.dart';
|
||||
import '../api/webuntis/queries/get_timegrid_units/get_timegrid_units_response.dart';
|
||||
import '../api/webuntis/queries/get_timetable/get_timetable.dart';
|
||||
import '../api/webuntis/queries/get_timetable/get_timetable_params.dart';
|
||||
import '../model/account_data.dart';
|
||||
import '../widget_data/widget_data_mapper.dart';
|
||||
import '../widget_data/widget_publisher.dart';
|
||||
import '../widget_data/widget_sync.dart';
|
||||
|
||||
/// Periodic widget refresh in a background Dart isolate. Native HTTP would
|
||||
/// mean reimplementing WebUntis JSON-RPC (auth, session-timeout retry -8520,
|
||||
/// payload quirks) twice — Dart isolate keeps that logic in one place.
|
||||
class WidgetBackgroundTask {
|
||||
static const String periodicTaskName = 'eu.mhsl.marianum.widget.refresh';
|
||||
static const String oneOffTaskName = 'eu.mhsl.marianum.widget.refresh.once';
|
||||
|
||||
static const Duration periodicFrequency = Duration(minutes: 30);
|
||||
|
||||
static Future<void> initialize() async {
|
||||
await Workmanager().initialize(_callbackDispatcher);
|
||||
await Workmanager().registerPeriodicTask(
|
||||
periodicTaskName,
|
||||
periodicTaskName,
|
||||
frequency: periodicFrequency,
|
||||
constraints: Constraints(networkType: NetworkType.connected),
|
||||
existingWorkPolicy: ExistingPeriodicWorkPolicy.keep,
|
||||
backoffPolicy: BackoffPolicy.linear,
|
||||
backoffPolicyDelay: const Duration(minutes: 5),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> requestImmediateRefresh() async {
|
||||
await Workmanager().registerOneOffTask(
|
||||
'$oneOffTaskName-${DateTime.now().millisecondsSinceEpoch}',
|
||||
oneOffTaskName,
|
||||
constraints: Constraints(networkType: NetworkType.connected),
|
||||
existingWorkPolicy: ExistingWorkPolicy.append,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> cancelAll() async {
|
||||
await Workmanager().cancelAll();
|
||||
}
|
||||
}
|
||||
|
||||
@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();
|
||||
return true;
|
||||
} on Exception catch (e, s) {
|
||||
log('[widget-bg] refresh failed: $e', stackTrace: s);
|
||||
// false → Workmanager retries with backoff. Native side keeps the
|
||||
// last good snapshot so the user still sees something.
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
await WidgetSync.ensureInitialized();
|
||||
await Authenticate.createSession();
|
||||
|
||||
final now = WidgetPublisher.widgetNow();
|
||||
final dateFormat = DateFormat('yyyyMMdd');
|
||||
// 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));
|
||||
final session = await Authenticate.getSession();
|
||||
|
||||
final timetable = await GetTimetable(
|
||||
GetTimetableParams(
|
||||
options: GetTimetableParamsOptions(
|
||||
element: GetTimetableParamsOptionsElement(
|
||||
id: session.personId,
|
||||
type: session.personType,
|
||||
keyType: GetTimetableParamsOptionsElementKeyType.id,
|
||||
),
|
||||
startDate: int.parse(dateFormat.format(weekStart)),
|
||||
endDate: int.parse(
|
||||
dateFormat.format(weekEndExclusive.subtract(const Duration(days: 1))),
|
||||
),
|
||||
teacherFields: GetTimetableParamsOptionsFields.all,
|
||||
subjectFields: GetTimetableParamsOptionsFields.all,
|
||||
roomFields: GetTimetableParamsOptionsFields.all,
|
||||
klasseFields: GetTimetableParamsOptionsFields.all,
|
||||
),
|
||||
),
|
||||
).run();
|
||||
|
||||
// Reference data — failures fall through to null in the mapper rather
|
||||
// than aborting the whole refresh.
|
||||
final subjects = await _runOrNull<GetSubjectsResponse>(() => GetSubjects().run());
|
||||
final rooms = await _runOrNull<GetRoomsResponse>(() => GetRooms().run());
|
||||
final holidays = await _runOrNull<GetHolidaysResponse>(() => GetHolidays().run());
|
||||
final timegrid = await _runOrNull<GetTimegridUnitsResponse>(
|
||||
() => GetTimegridUnits().run(),
|
||||
);
|
||||
final customEvents = await _runOrNull<GetCustomTimetableEventResponse>(
|
||||
() => GetCustomTimetableEvent(
|
||||
GetCustomTimetableEventParams(AccountData().getUserSecret()),
|
||||
).run(),
|
||||
);
|
||||
|
||||
final lessons = timetable.result;
|
||||
|
||||
final connectDouble = await WidgetSync.getConnectDoubleLessons();
|
||||
final dayData = WidgetDataMapper.buildDayData(
|
||||
now: now,
|
||||
lessons: lessons,
|
||||
subjects: subjects,
|
||||
rooms: rooms,
|
||||
holidays: holidays,
|
||||
timegrid: timegrid,
|
||||
customEvents: customEvents,
|
||||
connectDoubleLessons: connectDouble,
|
||||
);
|
||||
final weekData = WidgetDataMapper.buildWeekData(
|
||||
now: now,
|
||||
lessons: lessons,
|
||||
subjects: subjects,
|
||||
rooms: rooms,
|
||||
holidays: holidays,
|
||||
timegrid: timegrid,
|
||||
customEvents: customEvents,
|
||||
connectDoubleLessons: connectDouble,
|
||||
);
|
||||
|
||||
await WidgetSync.writeDayData(dayData);
|
||||
await WidgetSync.writeWeekData(weekData);
|
||||
await WidgetSync.setLoggedIn(true);
|
||||
await WidgetSync.triggerUpdate();
|
||||
log(
|
||||
'[widget-bg] refreshed: day=${dayData.lessons.length} '
|
||||
'week=${weekData.lessons.length}',
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
} on Exception catch (e) {
|
||||
log('[widget-bg] reference fetch failed: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'api/mhsl/breaker/get_breakers/get_breakers_response.dart';
|
||||
import 'app.dart';
|
||||
import 'background/widget_background_task.dart';
|
||||
import 'firebase_options.dart';
|
||||
import 'model/account_data.dart';
|
||||
import 'state/app/modules/account/bloc/account_bloc.dart';
|
||||
@@ -35,6 +36,7 @@ import 'view/login/login.dart';
|
||||
import 'widget/app_progress_indicator.dart';
|
||||
import 'widget/breaker/breaker.dart';
|
||||
import 'widget/debug/cache_view.dart';
|
||||
import 'widget_data/widget_sync.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
log('MarianumMobile started');
|
||||
@@ -72,6 +74,15 @@ Future<void> main() async {
|
||||
await Future.wait(initialisationTasks);
|
||||
log('app initialisation done!');
|
||||
|
||||
// Wire up the home-screen widget bridge before runApp so any widget render
|
||||
// triggered during startup hits initialised native storage.
|
||||
await WidgetSync.ensureInitialized();
|
||||
unawaited(
|
||||
WidgetBackgroundTask.initialize().onError(
|
||||
(e, _) => log('Workmanager init failed: $e'),
|
||||
),
|
||||
);
|
||||
|
||||
unawaited(
|
||||
FirebaseMessaging.instance.getToken().then(
|
||||
(token) => log('Firebase token: ${token ?? "Error: no Firebase token!"}'),
|
||||
@@ -287,6 +298,12 @@ Future<void> _wipeUserState({
|
||||
await prefs.clear();
|
||||
await HydratedBloc.storage.clear();
|
||||
await const CacheView().clear();
|
||||
// Stop the periodic widget refresh job so the background isolate doesn't
|
||||
// wake up every 30 minutes only to write `loggedIn=false`. Re-registers
|
||||
// on the next successful login.
|
||||
await WidgetBackgroundTask.cancelAll();
|
||||
await WidgetSync.clear();
|
||||
await WidgetSync.triggerUpdate();
|
||||
} catch (e, s) {
|
||||
log('User state wipe failed: $e', stackTrace: s);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../background/widget_background_task.dart';
|
||||
import '../../state/app/modules/account/bloc/account_bloc.dart';
|
||||
import '../../state/app/modules/account/bloc/account_state.dart';
|
||||
import '../../theming/light_app_theme.dart';
|
||||
@@ -34,6 +37,11 @@ class _LoginState extends State<Login> {
|
||||
|
||||
void _onLoginSuccess() {
|
||||
context.read<AccountBloc>().setStatus(AccountStatus.loggedIn);
|
||||
// Re-register the periodic refresh (cancelAll runs on logout) and kick
|
||||
// off an immediate one-off so the widget populates within seconds
|
||||
// instead of waiting up to 30 minutes for the next periodic slot.
|
||||
unawaited(WidgetBackgroundTask.initialize());
|
||||
unawaited(WidgetBackgroundTask.requestImmediateRefresh());
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -7,6 +7,7 @@ import '../../api/errors/error_mapper.dart';
|
||||
import '../../api/marianumcloud/talk/room/get_room.dart';
|
||||
import '../../api/marianumcloud/talk/room/get_room_params.dart';
|
||||
import '../../model/account_data.dart';
|
||||
import '../../widget_data/widget_sync.dart';
|
||||
|
||||
/// Owns the login flow's transient state (loading, last error) so it can be
|
||||
/// driven from a thin Stateful view and unit-tested without a widget tree.
|
||||
@@ -31,6 +32,11 @@ class LoginController extends ChangeNotifier {
|
||||
final user = username.trim().toLowerCase();
|
||||
try {
|
||||
await AccountData().removeData();
|
||||
// Drop any cached widget snapshot from a previous account before the
|
||||
// new credentials populate it — otherwise a re-login with a different
|
||||
// user briefly shows the previous owner's timetable on the home screen.
|
||||
await WidgetSync.clear();
|
||||
await WidgetSync.triggerUpdate();
|
||||
await AccountData().setData(user, password);
|
||||
await GetRoom(GetRoomParams(includeStatus: false)).run();
|
||||
_loading = false;
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'widget_data.freezed.dart';
|
||||
part 'widget_data.g.dart';
|
||||
|
||||
/// Status mirror of [LessonStatus] in
|
||||
/// `lib/view/pages/timetable/data/lesson_status.dart`. Native widget code
|
||||
/// switches on the string form, so the JSON name MUST stay stable.
|
||||
enum WidgetLessonStatus {
|
||||
regular,
|
||||
ongoing,
|
||||
past,
|
||||
cancelled,
|
||||
irregular,
|
||||
teacherChanged,
|
||||
event,
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class WidgetLesson with _$WidgetLesson {
|
||||
const factory WidgetLesson({
|
||||
required DateTime start,
|
||||
required DateTime end,
|
||||
required String subjectShort,
|
||||
String? subjectLong,
|
||||
String? room,
|
||||
String? teacher,
|
||||
String? originalTeacher,
|
||||
required WidgetLessonStatus status,
|
||||
String? customColor,
|
||||
@Default(0) int siblingCount,
|
||||
}) = _WidgetLesson;
|
||||
|
||||
factory WidgetLesson.fromJson(Map<String, Object?> json) =>
|
||||
_$WidgetLessonFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class WidgetPeriod with _$WidgetPeriod {
|
||||
const factory WidgetPeriod({
|
||||
/// Webuntis period name — typically the lesson number as string ("1",
|
||||
/// "2", "3", …). Native renderers append a trailing "." for display.
|
||||
required String name,
|
||||
/// Minutes since midnight, e.g. 480 for 08:00. Cheap to read in
|
||||
/// Kotlin/Swift without re-parsing time strings.
|
||||
required int startMinutes,
|
||||
required int endMinutes,
|
||||
/// Position on the **virtual** time axis used by the widget. Small
|
||||
/// between-lesson gaps are squeezed out so periods stack flush; only
|
||||
/// big breaks (> 5 min) remain as visible gaps. Computed by the
|
||||
/// mapper so native renderers don't have to redo the maths.
|
||||
required int virtualStartMinutes,
|
||||
required int virtualEndMinutes,
|
||||
}) = _WidgetPeriod;
|
||||
|
||||
factory WidgetPeriod.fromJson(Map<String, Object?> json) =>
|
||||
_$WidgetPeriodFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class WidgetTimetableData with _$WidgetTimetableData {
|
||||
const factory WidgetTimetableData({
|
||||
required DateTime 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.
|
||||
required DateTime anchorDate,
|
||||
required List<WidgetLesson> lessons,
|
||||
@Default(<WidgetPeriod>[]) List<WidgetPeriod> periods,
|
||||
@Default(false) bool isHoliday,
|
||||
String? holidayName,
|
||||
}) = _WidgetTimetableData;
|
||||
|
||||
factory WidgetTimetableData.fromJson(Map<String, Object?> json) =>
|
||||
_$WidgetTimetableDataFromJson(json);
|
||||
}
|
||||
@@ -0,0 +1,891 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'widget_data.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$WidgetLesson {
|
||||
|
||||
DateTime get start; DateTime get end; String get subjectShort; String? get subjectLong; String? get room; String? get teacher; String? get originalTeacher; WidgetLessonStatus get status; String? get customColor; int get siblingCount;
|
||||
/// Create a copy of WidgetLesson
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$WidgetLessonCopyWith<WidgetLesson> get copyWith => _$WidgetLessonCopyWithImpl<WidgetLesson>(this as WidgetLesson, _$identity);
|
||||
|
||||
/// Serializes this WidgetLesson to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is WidgetLesson&&(identical(other.start, start) || other.start == start)&&(identical(other.end, end) || other.end == end)&&(identical(other.subjectShort, subjectShort) || other.subjectShort == subjectShort)&&(identical(other.subjectLong, subjectLong) || other.subjectLong == subjectLong)&&(identical(other.room, room) || other.room == room)&&(identical(other.teacher, teacher) || other.teacher == teacher)&&(identical(other.originalTeacher, originalTeacher) || other.originalTeacher == originalTeacher)&&(identical(other.status, status) || other.status == status)&&(identical(other.customColor, customColor) || other.customColor == customColor)&&(identical(other.siblingCount, siblingCount) || other.siblingCount == siblingCount));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,start,end,subjectShort,subjectLong,room,teacher,originalTeacher,status,customColor,siblingCount);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'WidgetLesson(start: $start, end: $end, subjectShort: $subjectShort, subjectLong: $subjectLong, room: $room, teacher: $teacher, originalTeacher: $originalTeacher, status: $status, customColor: $customColor, siblingCount: $siblingCount)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $WidgetLessonCopyWith<$Res> {
|
||||
factory $WidgetLessonCopyWith(WidgetLesson value, $Res Function(WidgetLesson) _then) = _$WidgetLessonCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
DateTime start, DateTime end, String subjectShort, String? subjectLong, String? room, String? teacher, String? originalTeacher, WidgetLessonStatus status, String? customColor, int siblingCount
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$WidgetLessonCopyWithImpl<$Res>
|
||||
implements $WidgetLessonCopyWith<$Res> {
|
||||
_$WidgetLessonCopyWithImpl(this._self, this._then);
|
||||
|
||||
final WidgetLesson _self;
|
||||
final $Res Function(WidgetLesson) _then;
|
||||
|
||||
/// Create a copy of WidgetLesson
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? start = null,Object? end = null,Object? subjectShort = null,Object? subjectLong = freezed,Object? room = freezed,Object? teacher = freezed,Object? originalTeacher = freezed,Object? status = null,Object? customColor = freezed,Object? siblingCount = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
start: null == start ? _self.start : start // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,end: null == end ? _self.end : end // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,subjectShort: null == subjectShort ? _self.subjectShort : subjectShort // ignore: cast_nullable_to_non_nullable
|
||||
as String,subjectLong: freezed == subjectLong ? _self.subjectLong : subjectLong // ignore: cast_nullable_to_non_nullable
|
||||
as String?,room: freezed == room ? _self.room : room // ignore: cast_nullable_to_non_nullable
|
||||
as String?,teacher: freezed == teacher ? _self.teacher : teacher // ignore: cast_nullable_to_non_nullable
|
||||
as String?,originalTeacher: freezed == originalTeacher ? _self.originalTeacher : originalTeacher // ignore: cast_nullable_to_non_nullable
|
||||
as String?,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
||||
as WidgetLessonStatus,customColor: freezed == customColor ? _self.customColor : customColor // ignore: cast_nullable_to_non_nullable
|
||||
as String?,siblingCount: null == siblingCount ? _self.siblingCount : siblingCount // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [WidgetLesson].
|
||||
extension WidgetLessonPatterns on WidgetLesson {
|
||||
/// 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( _WidgetLesson value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _WidgetLesson() 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( _WidgetLesson value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _WidgetLesson():
|
||||
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( _WidgetLesson value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _WidgetLesson() 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 start, DateTime end, String subjectShort, String? subjectLong, String? room, String? teacher, String? originalTeacher, WidgetLessonStatus status, String? customColor, int siblingCount)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _WidgetLesson() when $default != null:
|
||||
return $default(_that.start,_that.end,_that.subjectShort,_that.subjectLong,_that.room,_that.teacher,_that.originalTeacher,_that.status,_that.customColor,_that.siblingCount);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 start, DateTime end, String subjectShort, String? subjectLong, String? room, String? teacher, String? originalTeacher, WidgetLessonStatus status, String? customColor, int siblingCount) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _WidgetLesson():
|
||||
return $default(_that.start,_that.end,_that.subjectShort,_that.subjectLong,_that.room,_that.teacher,_that.originalTeacher,_that.status,_that.customColor,_that.siblingCount);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 start, DateTime end, String subjectShort, String? subjectLong, String? room, String? teacher, String? originalTeacher, WidgetLessonStatus status, String? customColor, int siblingCount)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _WidgetLesson() when $default != null:
|
||||
return $default(_that.start,_that.end,_that.subjectShort,_that.subjectLong,_that.room,_that.teacher,_that.originalTeacher,_that.status,_that.customColor,_that.siblingCount);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _WidgetLesson implements WidgetLesson {
|
||||
const _WidgetLesson({required this.start, required this.end, required this.subjectShort, this.subjectLong, this.room, this.teacher, this.originalTeacher, required this.status, this.customColor, this.siblingCount = 0});
|
||||
factory _WidgetLesson.fromJson(Map<String, dynamic> json) => _$WidgetLessonFromJson(json);
|
||||
|
||||
@override final DateTime start;
|
||||
@override final DateTime end;
|
||||
@override final String subjectShort;
|
||||
@override final String? subjectLong;
|
||||
@override final String? room;
|
||||
@override final String? teacher;
|
||||
@override final String? originalTeacher;
|
||||
@override final WidgetLessonStatus status;
|
||||
@override final String? customColor;
|
||||
@override@JsonKey() final int siblingCount;
|
||||
|
||||
/// Create a copy of WidgetLesson
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$WidgetLessonCopyWith<_WidgetLesson> get copyWith => __$WidgetLessonCopyWithImpl<_WidgetLesson>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$WidgetLessonToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _WidgetLesson&&(identical(other.start, start) || other.start == start)&&(identical(other.end, end) || other.end == end)&&(identical(other.subjectShort, subjectShort) || other.subjectShort == subjectShort)&&(identical(other.subjectLong, subjectLong) || other.subjectLong == subjectLong)&&(identical(other.room, room) || other.room == room)&&(identical(other.teacher, teacher) || other.teacher == teacher)&&(identical(other.originalTeacher, originalTeacher) || other.originalTeacher == originalTeacher)&&(identical(other.status, status) || other.status == status)&&(identical(other.customColor, customColor) || other.customColor == customColor)&&(identical(other.siblingCount, siblingCount) || other.siblingCount == siblingCount));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,start,end,subjectShort,subjectLong,room,teacher,originalTeacher,status,customColor,siblingCount);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'WidgetLesson(start: $start, end: $end, subjectShort: $subjectShort, subjectLong: $subjectLong, room: $room, teacher: $teacher, originalTeacher: $originalTeacher, status: $status, customColor: $customColor, siblingCount: $siblingCount)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$WidgetLessonCopyWith<$Res> implements $WidgetLessonCopyWith<$Res> {
|
||||
factory _$WidgetLessonCopyWith(_WidgetLesson value, $Res Function(_WidgetLesson) _then) = __$WidgetLessonCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
DateTime start, DateTime end, String subjectShort, String? subjectLong, String? room, String? teacher, String? originalTeacher, WidgetLessonStatus status, String? customColor, int siblingCount
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$WidgetLessonCopyWithImpl<$Res>
|
||||
implements _$WidgetLessonCopyWith<$Res> {
|
||||
__$WidgetLessonCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _WidgetLesson _self;
|
||||
final $Res Function(_WidgetLesson) _then;
|
||||
|
||||
/// Create a copy of WidgetLesson
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? start = null,Object? end = null,Object? subjectShort = null,Object? subjectLong = freezed,Object? room = freezed,Object? teacher = freezed,Object? originalTeacher = freezed,Object? status = null,Object? customColor = freezed,Object? siblingCount = null,}) {
|
||||
return _then(_WidgetLesson(
|
||||
start: null == start ? _self.start : start // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,end: null == end ? _self.end : end // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,subjectShort: null == subjectShort ? _self.subjectShort : subjectShort // ignore: cast_nullable_to_non_nullable
|
||||
as String,subjectLong: freezed == subjectLong ? _self.subjectLong : subjectLong // ignore: cast_nullable_to_non_nullable
|
||||
as String?,room: freezed == room ? _self.room : room // ignore: cast_nullable_to_non_nullable
|
||||
as String?,teacher: freezed == teacher ? _self.teacher : teacher // ignore: cast_nullable_to_non_nullable
|
||||
as String?,originalTeacher: freezed == originalTeacher ? _self.originalTeacher : originalTeacher // ignore: cast_nullable_to_non_nullable
|
||||
as String?,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
||||
as WidgetLessonStatus,customColor: freezed == customColor ? _self.customColor : customColor // ignore: cast_nullable_to_non_nullable
|
||||
as String?,siblingCount: null == siblingCount ? _self.siblingCount : siblingCount // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$WidgetPeriod {
|
||||
|
||||
/// Webuntis period name — typically the lesson number as string ("1",
|
||||
/// "2", "3", …). Native renderers append a trailing "." for display.
|
||||
String get name;/// Minutes since midnight, e.g. 480 for 08:00. Cheap to read in
|
||||
/// Kotlin/Swift without re-parsing time strings.
|
||||
int get startMinutes; int get endMinutes;/// Position on the **virtual** time axis used by the widget. Small
|
||||
/// between-lesson gaps are squeezed out so periods stack flush; only
|
||||
/// big breaks (> 5 min) remain as visible gaps. Computed by the
|
||||
/// mapper so native renderers don't have to redo the maths.
|
||||
int get virtualStartMinutes; int get virtualEndMinutes;
|
||||
/// Create a copy of WidgetPeriod
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$WidgetPeriodCopyWith<WidgetPeriod> get copyWith => _$WidgetPeriodCopyWithImpl<WidgetPeriod>(this as WidgetPeriod, _$identity);
|
||||
|
||||
/// Serializes this WidgetPeriod to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is WidgetPeriod&&(identical(other.name, name) || other.name == name)&&(identical(other.startMinutes, startMinutes) || other.startMinutes == startMinutes)&&(identical(other.endMinutes, endMinutes) || other.endMinutes == endMinutes)&&(identical(other.virtualStartMinutes, virtualStartMinutes) || other.virtualStartMinutes == virtualStartMinutes)&&(identical(other.virtualEndMinutes, virtualEndMinutes) || other.virtualEndMinutes == virtualEndMinutes));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,name,startMinutes,endMinutes,virtualStartMinutes,virtualEndMinutes);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'WidgetPeriod(name: $name, startMinutes: $startMinutes, endMinutes: $endMinutes, virtualStartMinutes: $virtualStartMinutes, virtualEndMinutes: $virtualEndMinutes)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $WidgetPeriodCopyWith<$Res> {
|
||||
factory $WidgetPeriodCopyWith(WidgetPeriod value, $Res Function(WidgetPeriod) _then) = _$WidgetPeriodCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String name, int startMinutes, int endMinutes, int virtualStartMinutes, int virtualEndMinutes
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$WidgetPeriodCopyWithImpl<$Res>
|
||||
implements $WidgetPeriodCopyWith<$Res> {
|
||||
_$WidgetPeriodCopyWithImpl(this._self, this._then);
|
||||
|
||||
final WidgetPeriod _self;
|
||||
final $Res Function(WidgetPeriod) _then;
|
||||
|
||||
/// Create a copy of WidgetPeriod
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? startMinutes = null,Object? endMinutes = null,Object? virtualStartMinutes = null,Object? virtualEndMinutes = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,startMinutes: null == startMinutes ? _self.startMinutes : startMinutes // ignore: cast_nullable_to_non_nullable
|
||||
as int,endMinutes: null == endMinutes ? _self.endMinutes : endMinutes // ignore: cast_nullable_to_non_nullable
|
||||
as int,virtualStartMinutes: null == virtualStartMinutes ? _self.virtualStartMinutes : virtualStartMinutes // ignore: cast_nullable_to_non_nullable
|
||||
as int,virtualEndMinutes: null == virtualEndMinutes ? _self.virtualEndMinutes : virtualEndMinutes // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [WidgetPeriod].
|
||||
extension WidgetPeriodPatterns on WidgetPeriod {
|
||||
/// 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( _WidgetPeriod value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _WidgetPeriod() 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( _WidgetPeriod value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _WidgetPeriod():
|
||||
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( _WidgetPeriod value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _WidgetPeriod() 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( String name, int startMinutes, int endMinutes, int virtualStartMinutes, int virtualEndMinutes)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _WidgetPeriod() when $default != null:
|
||||
return $default(_that.name,_that.startMinutes,_that.endMinutes,_that.virtualStartMinutes,_that.virtualEndMinutes);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( String name, int startMinutes, int endMinutes, int virtualStartMinutes, int virtualEndMinutes) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _WidgetPeriod():
|
||||
return $default(_that.name,_that.startMinutes,_that.endMinutes,_that.virtualStartMinutes,_that.virtualEndMinutes);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( String name, int startMinutes, int endMinutes, int virtualStartMinutes, int virtualEndMinutes)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _WidgetPeriod() when $default != null:
|
||||
return $default(_that.name,_that.startMinutes,_that.endMinutes,_that.virtualStartMinutes,_that.virtualEndMinutes);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _WidgetPeriod implements WidgetPeriod {
|
||||
const _WidgetPeriod({required this.name, required this.startMinutes, required this.endMinutes, required this.virtualStartMinutes, required this.virtualEndMinutes});
|
||||
factory _WidgetPeriod.fromJson(Map<String, dynamic> json) => _$WidgetPeriodFromJson(json);
|
||||
|
||||
/// Webuntis period name — typically the lesson number as string ("1",
|
||||
/// "2", "3", …). Native renderers append a trailing "." for display.
|
||||
@override final String name;
|
||||
/// Minutes since midnight, e.g. 480 for 08:00. Cheap to read in
|
||||
/// Kotlin/Swift without re-parsing time strings.
|
||||
@override final int startMinutes;
|
||||
@override final int endMinutes;
|
||||
/// Position on the **virtual** time axis used by the widget. Small
|
||||
/// between-lesson gaps are squeezed out so periods stack flush; only
|
||||
/// big breaks (> 5 min) remain as visible gaps. Computed by the
|
||||
/// mapper so native renderers don't have to redo the maths.
|
||||
@override final int virtualStartMinutes;
|
||||
@override final int virtualEndMinutes;
|
||||
|
||||
/// Create a copy of WidgetPeriod
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$WidgetPeriodCopyWith<_WidgetPeriod> get copyWith => __$WidgetPeriodCopyWithImpl<_WidgetPeriod>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$WidgetPeriodToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _WidgetPeriod&&(identical(other.name, name) || other.name == name)&&(identical(other.startMinutes, startMinutes) || other.startMinutes == startMinutes)&&(identical(other.endMinutes, endMinutes) || other.endMinutes == endMinutes)&&(identical(other.virtualStartMinutes, virtualStartMinutes) || other.virtualStartMinutes == virtualStartMinutes)&&(identical(other.virtualEndMinutes, virtualEndMinutes) || other.virtualEndMinutes == virtualEndMinutes));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,name,startMinutes,endMinutes,virtualStartMinutes,virtualEndMinutes);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'WidgetPeriod(name: $name, startMinutes: $startMinutes, endMinutes: $endMinutes, virtualStartMinutes: $virtualStartMinutes, virtualEndMinutes: $virtualEndMinutes)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$WidgetPeriodCopyWith<$Res> implements $WidgetPeriodCopyWith<$Res> {
|
||||
factory _$WidgetPeriodCopyWith(_WidgetPeriod value, $Res Function(_WidgetPeriod) _then) = __$WidgetPeriodCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String name, int startMinutes, int endMinutes, int virtualStartMinutes, int virtualEndMinutes
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$WidgetPeriodCopyWithImpl<$Res>
|
||||
implements _$WidgetPeriodCopyWith<$Res> {
|
||||
__$WidgetPeriodCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _WidgetPeriod _self;
|
||||
final $Res Function(_WidgetPeriod) _then;
|
||||
|
||||
/// Create a copy of WidgetPeriod
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? startMinutes = null,Object? endMinutes = null,Object? virtualStartMinutes = null,Object? virtualEndMinutes = null,}) {
|
||||
return _then(_WidgetPeriod(
|
||||
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,startMinutes: null == startMinutes ? _self.startMinutes : startMinutes // ignore: cast_nullable_to_non_nullable
|
||||
as int,endMinutes: null == endMinutes ? _self.endMinutes : endMinutes // ignore: cast_nullable_to_non_nullable
|
||||
as int,virtualStartMinutes: null == virtualStartMinutes ? _self.virtualStartMinutes : virtualStartMinutes // ignore: cast_nullable_to_non_nullable
|
||||
as int,virtualEndMinutes: null == virtualEndMinutes ? _self.virtualEndMinutes : virtualEndMinutes // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @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;
|
||||
/// Create a copy of WidgetTimetableData
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$WidgetTimetableDataCopyWith<WidgetTimetableData> get copyWith => _$WidgetTimetableDataCopyWithImpl<WidgetTimetableData>(this as WidgetTimetableData, _$identity);
|
||||
|
||||
/// Serializes this WidgetTimetableData to a JSON map.
|
||||
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));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,fetchedAt,anchorDate,const DeepCollectionEquality().hash(lessons),const DeepCollectionEquality().hash(periods),isHoliday,holidayName);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'WidgetTimetableData(fetchedAt: $fetchedAt, anchorDate: $anchorDate, lessons: $lessons, periods: $periods, isHoliday: $isHoliday, holidayName: $holidayName)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
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
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$WidgetTimetableDataCopyWithImpl<$Res>
|
||||
implements $WidgetTimetableDataCopyWith<$Res> {
|
||||
_$WidgetTimetableDataCopyWithImpl(this._self, this._then);
|
||||
|
||||
final WidgetTimetableData _self;
|
||||
final $Res Function(WidgetTimetableData) _then;
|
||||
|
||||
/// 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,}) {
|
||||
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
|
||||
as DateTime,lessons: null == lessons ? _self.lessons : lessons // ignore: cast_nullable_to_non_nullable
|
||||
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?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [WidgetTimetableData].
|
||||
extension WidgetTimetableDataPatterns on WidgetTimetableData {
|
||||
/// 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( _WidgetTimetableData value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _WidgetTimetableData() 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( _WidgetTimetableData value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _WidgetTimetableData():
|
||||
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( _WidgetTimetableData value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _WidgetTimetableData() 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 fetchedAt, DateTime anchorDate, List<WidgetLesson> lessons, List<WidgetPeriod> periods, bool isHoliday, String? holidayName)? $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 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 fetchedAt, DateTime anchorDate, List<WidgetLesson> lessons, List<WidgetPeriod> periods, bool isHoliday, String? holidayName) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _WidgetTimetableData():
|
||||
return $default(_that.fetchedAt,_that.anchorDate,_that.lessons,_that.periods,_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 fetchedAt, DateTime anchorDate, List<WidgetLesson> lessons, List<WidgetPeriod> periods, bool isHoliday, String? holidayName)? $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 null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@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;
|
||||
factory _WidgetTimetableData.fromJson(Map<String, dynamic> json) => _$WidgetTimetableDataFromJson(json);
|
||||
|
||||
@override final DateTime 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.
|
||||
@override final DateTime anchorDate;
|
||||
final List<WidgetLesson> _lessons;
|
||||
@override List<WidgetLesson> get lessons {
|
||||
if (_lessons is EqualUnmodifiableListView) return _lessons;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_lessons);
|
||||
}
|
||||
|
||||
final List<WidgetPeriod> _periods;
|
||||
@override@JsonKey() List<WidgetPeriod> get periods {
|
||||
if (_periods is EqualUnmodifiableListView) return _periods;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_periods);
|
||||
}
|
||||
|
||||
@override@JsonKey() final bool isHoliday;
|
||||
@override final String? holidayName;
|
||||
|
||||
/// Create a copy of WidgetTimetableData
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$WidgetTimetableDataCopyWith<_WidgetTimetableData> get copyWith => __$WidgetTimetableDataCopyWithImpl<_WidgetTimetableData>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$WidgetTimetableDataToJson(this, );
|
||||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,fetchedAt,anchorDate,const DeepCollectionEquality().hash(_lessons),const DeepCollectionEquality().hash(_periods),isHoliday,holidayName);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'WidgetTimetableData(fetchedAt: $fetchedAt, anchorDate: $anchorDate, lessons: $lessons, periods: $periods, isHoliday: $isHoliday, holidayName: $holidayName)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$WidgetTimetableDataCopyWith<$Res> implements $WidgetTimetableDataCopyWith<$Res> {
|
||||
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
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$WidgetTimetableDataCopyWithImpl<$Res>
|
||||
implements _$WidgetTimetableDataCopyWith<$Res> {
|
||||
__$WidgetTimetableDataCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _WidgetTimetableData _self;
|
||||
final $Res Function(_WidgetTimetableData) _then;
|
||||
|
||||
/// 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,}) {
|
||||
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
|
||||
as DateTime,lessons: null == lessons ? _self._lessons : lessons // ignore: cast_nullable_to_non_nullable
|
||||
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?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@@ -0,0 +1,90 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'widget_data.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_WidgetLesson _$WidgetLessonFromJson(Map<String, dynamic> json) =>
|
||||
_WidgetLesson(
|
||||
start: DateTime.parse(json['start'] as String),
|
||||
end: DateTime.parse(json['end'] as String),
|
||||
subjectShort: json['subjectShort'] as String,
|
||||
subjectLong: json['subjectLong'] as String?,
|
||||
room: json['room'] as String?,
|
||||
teacher: json['teacher'] as String?,
|
||||
originalTeacher: json['originalTeacher'] as String?,
|
||||
status: $enumDecode(_$WidgetLessonStatusEnumMap, json['status']),
|
||||
customColor: json['customColor'] as String?,
|
||||
siblingCount: (json['siblingCount'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$WidgetLessonToJson(_WidgetLesson instance) =>
|
||||
<String, dynamic>{
|
||||
'start': instance.start.toIso8601String(),
|
||||
'end': instance.end.toIso8601String(),
|
||||
'subjectShort': instance.subjectShort,
|
||||
'subjectLong': instance.subjectLong,
|
||||
'room': instance.room,
|
||||
'teacher': instance.teacher,
|
||||
'originalTeacher': instance.originalTeacher,
|
||||
'status': _$WidgetLessonStatusEnumMap[instance.status]!,
|
||||
'customColor': instance.customColor,
|
||||
'siblingCount': instance.siblingCount,
|
||||
};
|
||||
|
||||
const _$WidgetLessonStatusEnumMap = {
|
||||
WidgetLessonStatus.regular: 'regular',
|
||||
WidgetLessonStatus.ongoing: 'ongoing',
|
||||
WidgetLessonStatus.past: 'past',
|
||||
WidgetLessonStatus.cancelled: 'cancelled',
|
||||
WidgetLessonStatus.irregular: 'irregular',
|
||||
WidgetLessonStatus.teacherChanged: 'teacherChanged',
|
||||
WidgetLessonStatus.event: 'event',
|
||||
};
|
||||
|
||||
_WidgetPeriod _$WidgetPeriodFromJson(Map<String, dynamic> json) =>
|
||||
_WidgetPeriod(
|
||||
name: json['name'] as String,
|
||||
startMinutes: (json['startMinutes'] as num).toInt(),
|
||||
endMinutes: (json['endMinutes'] as num).toInt(),
|
||||
virtualStartMinutes: (json['virtualStartMinutes'] as num).toInt(),
|
||||
virtualEndMinutes: (json['virtualEndMinutes'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$WidgetPeriodToJson(_WidgetPeriod instance) =>
|
||||
<String, dynamic>{
|
||||
'name': instance.name,
|
||||
'startMinutes': instance.startMinutes,
|
||||
'endMinutes': instance.endMinutes,
|
||||
'virtualStartMinutes': instance.virtualStartMinutes,
|
||||
'virtualEndMinutes': instance.virtualEndMinutes,
|
||||
};
|
||||
|
||||
_WidgetTimetableData _$WidgetTimetableDataFromJson(Map<String, dynamic> json) =>
|
||||
_WidgetTimetableData(
|
||||
fetchedAt: DateTime.parse(json['fetchedAt'] as String),
|
||||
anchorDate: DateTime.parse(json['anchorDate'] as String),
|
||||
lessons: (json['lessons'] as List<dynamic>)
|
||||
.map((e) => WidgetLesson.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
periods:
|
||||
(json['periods'] as List<dynamic>?)
|
||||
?.map((e) => WidgetPeriod.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const <WidgetPeriod>[],
|
||||
isHoliday: json['isHoliday'] as bool? ?? false,
|
||||
holidayName: json['holidayName'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$WidgetTimetableDataToJson(
|
||||
_WidgetTimetableData instance,
|
||||
) => <String, dynamic>{
|
||||
'fetchedAt': instance.fetchedAt.toIso8601String(),
|
||||
'anchorDate': instance.anchorDate.toIso8601String(),
|
||||
'lessons': instance.lessons,
|
||||
'periods': instance.periods,
|
||||
'isHoliday': instance.isHoliday,
|
||||
'holidayName': instance.holidayName,
|
||||
};
|
||||
@@ -0,0 +1,472 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:rrule/rrule.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 '../api/webuntis/queries/get_holidays/get_holidays_response.dart';
|
||||
import '../api/webuntis/queries/get_rooms/get_rooms_response.dart';
|
||||
import '../api/webuntis/queries/get_subjects/get_subjects_response.dart';
|
||||
import '../api/webuntis/queries/get_timegrid_units/get_timegrid_units_response.dart';
|
||||
import '../api/webuntis/queries/get_timetable/get_timetable_response.dart';
|
||||
import '../view/pages/timetable/data/lesson_period_schedule.dart';
|
||||
import '../view/pages/timetable/data/lesson_status.dart';
|
||||
import '../view/pages/timetable/data/webuntis_time.dart';
|
||||
import 'widget_data.dart';
|
||||
|
||||
class WidgetDataMapper {
|
||||
/// After 17:00 the user's question shifts from "what's left today" to
|
||||
/// "what's tomorrow", so the day-widget rolls forward.
|
||||
static const int _dayWidgetCutoffHour = 17;
|
||||
|
||||
static const _weekend = {DateTime.saturday, DateTime.sunday};
|
||||
|
||||
static DateTime resolveDayAnchor(DateTime now) {
|
||||
var candidate = DateTime(now.year, now.month, now.day);
|
||||
final shiftToTomorrow =
|
||||
now.hour >= _dayWidgetCutoffHour || _weekend.contains(now.weekday);
|
||||
if (shiftToTomorrow) {
|
||||
candidate = candidate.add(const Duration(days: 1));
|
||||
}
|
||||
while (_weekend.contains(candidate.weekday)) {
|
||||
candidate = candidate.add(const Duration(days: 1));
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
static DateTime resolveWeekAnchor(DateTime now) {
|
||||
final anchor = resolveDayAnchor(now);
|
||||
final monday = anchor.subtract(Duration(days: anchor.weekday - 1));
|
||||
return DateTime(monday.year, monday.month, monday.day);
|
||||
}
|
||||
|
||||
static WidgetTimetableData buildDayData({
|
||||
required DateTime now,
|
||||
required Iterable<GetTimetableResponseObject> lessons,
|
||||
required GetSubjectsResponse? subjects,
|
||||
required GetRoomsResponse? rooms,
|
||||
required GetHolidaysResponse? holidays,
|
||||
GetTimegridUnitsResponse? timegrid,
|
||||
GetCustomTimetableEventResponse? customEvents,
|
||||
bool connectDoubleLessons = true,
|
||||
}) {
|
||||
final anchor = resolveDayAnchor(now);
|
||||
final holiday = _findHoliday(anchor, holidays);
|
||||
final dayStart = anchor;
|
||||
final dayEnd = anchor.add(const Duration(days: 1));
|
||||
final dayLessons = lessons.where((l) => _onSameDay(l, anchor)).toList();
|
||||
final source = connectDoubleLessons
|
||||
? _mergeAdjacentLessons(dayLessons)
|
||||
: dayLessons;
|
||||
final mapped = <WidgetLesson>[
|
||||
...source.map((l) => _mapLesson(l, now, subjects, rooms)),
|
||||
..._expandCustomEvents(customEvents, dayStart, dayEnd),
|
||||
]..sort((a, b) => a.start.compareTo(b.start));
|
||||
return WidgetTimetableData(
|
||||
fetchedAt: now,
|
||||
anchorDate: anchor,
|
||||
lessons: _resolveCollisions(mapped),
|
||||
periods: _resolvePeriods(timegrid),
|
||||
isHoliday: holiday != null,
|
||||
holidayName: holiday?.longName,
|
||||
);
|
||||
}
|
||||
|
||||
static WidgetTimetableData buildWeekData({
|
||||
required DateTime now,
|
||||
required Iterable<GetTimetableResponseObject> lessons,
|
||||
required GetSubjectsResponse? subjects,
|
||||
required GetRoomsResponse? rooms,
|
||||
required GetHolidaysResponse? holidays,
|
||||
GetTimegridUnitsResponse? timegrid,
|
||||
GetCustomTimetableEventResponse? customEvents,
|
||||
bool connectDoubleLessons = true,
|
||||
}) {
|
||||
final anchor = resolveWeekAnchor(now);
|
||||
final endExclusive = anchor.add(const Duration(days: 5));
|
||||
final weekLessons = lessons.where((l) {
|
||||
final dt = WebuntisTime.parse(l.date, l.startTime);
|
||||
return !dt.isBefore(anchor) && 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.
|
||||
final source = connectDoubleLessons
|
||||
? _mergePerDay(weekLessons)
|
||||
: weekLessons;
|
||||
final mapped = <WidgetLesson>[
|
||||
...source.map((l) => _mapLesson(l, now, subjects, rooms)),
|
||||
..._expandCustomEvents(customEvents, anchor, endExclusive),
|
||||
]..sort((a, b) => a.start.compareTo(b.start));
|
||||
return WidgetTimetableData(
|
||||
fetchedAt: now,
|
||||
anchorDate: anchor,
|
||||
lessons: _resolveCollisions(mapped),
|
||||
periods: _resolvePeriods(timegrid),
|
||||
);
|
||||
}
|
||||
|
||||
/// cancelled (0) < event (1) < regular (2) — events replace cancelled
|
||||
/// lessons but lose to real ones, leaving a `+1` hint on the survivor.
|
||||
static int _priority(WidgetLessonStatus status) => switch (status) {
|
||||
WidgetLessonStatus.cancelled => 0,
|
||||
WidgetLessonStatus.event => 1,
|
||||
_ => 2,
|
||||
};
|
||||
|
||||
static List<WidgetLesson> _resolveCollisions(List<WidgetLesson> lessons) {
|
||||
if (lessons.length <= 1) return lessons;
|
||||
|
||||
bool overlaps(WidgetLesson l, WidgetLesson other) =>
|
||||
l != other && l.start.isBefore(other.end) && l.end.isAfter(other.start);
|
||||
|
||||
// Index-based: a long event covering several regulars must bump *every*
|
||||
// covered lesson, not just the first overlap.
|
||||
final dropped = List<bool>.filled(lessons.length, false);
|
||||
final bumps = List<int>.filled(lessons.length, 0);
|
||||
for (var i = 0; i < lessons.length; i++) {
|
||||
final l = lessons[i];
|
||||
final myPrio = _priority(l.status);
|
||||
final overrideIdxs = <int>[];
|
||||
for (var j = 0; j < lessons.length; j++) {
|
||||
if (i == j) continue;
|
||||
if (_priority(lessons[j].status) <= myPrio) continue;
|
||||
if (!overlaps(l, lessons[j])) continue;
|
||||
overrideIdxs.add(j);
|
||||
}
|
||||
if (overrideIdxs.isNotEmpty) {
|
||||
dropped[i] = true;
|
||||
if (l.status == WidgetLessonStatus.event) {
|
||||
for (final idx in overrideIdxs) {
|
||||
bumps[idx] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
final filtered = <WidgetLesson>[];
|
||||
for (var i = 0; i < lessons.length; i++) {
|
||||
if (dropped[i]) continue;
|
||||
final l = lessons[i];
|
||||
filtered.add(
|
||||
bumps[i] > 0
|
||||
? l.copyWith(siblingCount: l.siblingCount + bumps[i])
|
||||
: l,
|
||||
);
|
||||
}
|
||||
if (filtered.length <= 1) return filtered;
|
||||
|
||||
final groups = <String, List<WidgetLesson>>{};
|
||||
for (final l in filtered) {
|
||||
final key =
|
||||
'${l.start.year}-${l.start.month}-${l.start.day}-${l.start.hour}-${l.start.minute}';
|
||||
groups.putIfAbsent(key, () => []).add(l);
|
||||
}
|
||||
final result = <WidgetLesson>[];
|
||||
for (final group in groups.values) {
|
||||
if (group.length == 1) {
|
||||
result.add(group.first);
|
||||
continue;
|
||||
}
|
||||
final active = group
|
||||
.where((l) => l.status != WidgetLessonStatus.cancelled)
|
||||
.toList();
|
||||
if (active.isEmpty) {
|
||||
result.addAll(group);
|
||||
continue;
|
||||
}
|
||||
active.sort((a, b) => a.subjectShort.compareTo(b.subjectShort));
|
||||
// Additive — preserves the event-bump from the priority pass, otherwise
|
||||
// a slot with another regular lesson AND a hidden event would show +1
|
||||
// instead of +2.
|
||||
final keeper = active.first;
|
||||
result.add(
|
||||
keeper.copyWith(
|
||||
siblingCount: keeper.siblingCount + active.length - 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
return result..sort((a, b) => a.start.compareTo(b.start));
|
||||
}
|
||||
|
||||
/// Gaps below this collapse to zero on the virtual axis so 45-min slots
|
||||
/// stack flush; bigger gaps survive as visible Pause-blocks.
|
||||
static const int _smallBreakThresholdMinutes = 5;
|
||||
|
||||
static List<WidgetPeriod> _resolvePeriods(
|
||||
GetTimegridUnitsResponse? timegrid,
|
||||
) {
|
||||
final schedule =
|
||||
(timegrid != null ? LessonPeriodSchedule.fromApi(timegrid) : null) ??
|
||||
LessonPeriodSchedule.fallback();
|
||||
final raw = schedule.periods
|
||||
.map(
|
||||
(p) => (
|
||||
name: p.name,
|
||||
start: p.start.hour * 60 + p.start.minute,
|
||||
end: p.end.hour * 60 + p.end.minute,
|
||||
),
|
||||
)
|
||||
.toList()
|
||||
..sort((a, b) => a.start.compareTo(b.start));
|
||||
|
||||
final result = <WidgetPeriod>[];
|
||||
var virtualOffset = 0;
|
||||
int? prevEnd;
|
||||
for (final p in raw) {
|
||||
if (prevEnd != null) {
|
||||
final gap = p.start - prevEnd;
|
||||
if (gap > _smallBreakThresholdMinutes) virtualOffset += gap;
|
||||
}
|
||||
final duration = p.end - p.start;
|
||||
result.add(
|
||||
WidgetPeriod(
|
||||
name: p.name,
|
||||
startMinutes: p.start,
|
||||
endMinutes: p.end,
|
||||
virtualStartMinutes: virtualOffset,
|
||||
virtualEndMinutes: virtualOffset + duration,
|
||||
),
|
||||
);
|
||||
virtualOffset += duration;
|
||||
prevEnd = p.end;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static List<GetTimetableResponseObject> _mergePerDay(
|
||||
List<GetTimetableResponseObject> lessons,
|
||||
) {
|
||||
final byDay = <int, List<GetTimetableResponseObject>>{};
|
||||
for (final l in lessons) {
|
||||
byDay.putIfAbsent(l.date, () => []).add(l);
|
||||
}
|
||||
return [for (final group in byDay.values) ..._mergeAdjacentLessons(group)];
|
||||
}
|
||||
|
||||
/// Mirrors `TimetableAppointmentFactory._mergeAdjacentLessons` so the
|
||||
/// widget shows the same merged blocks the in-app calendar does.
|
||||
static List<GetTimetableResponseObject> _mergeAdjacentLessons(
|
||||
List<GetTimetableResponseObject> input, {
|
||||
Duration maxGap = const Duration(minutes: 5),
|
||||
}) {
|
||||
if (input.isEmpty) return const [];
|
||||
final sorted = [...input]..sort(
|
||||
(a, b) => WebuntisTime.parse(
|
||||
a.date,
|
||||
a.startTime,
|
||||
).compareTo(WebuntisTime.parse(b.date, b.startTime)),
|
||||
);
|
||||
final merged = <GetTimetableResponseObject>[];
|
||||
for (final current in sorted) {
|
||||
if (merged.isNotEmpty && _canMerge(merged.last, current, maxGap)) {
|
||||
merged.last.endTime = current.endTime;
|
||||
} else {
|
||||
merged.add(GetTimetableResponseObject.fromJson(current.toJson()));
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
static bool _canMerge(
|
||||
GetTimetableResponseObject a,
|
||||
GetTimetableResponseObject b,
|
||||
Duration maxGap,
|
||||
) {
|
||||
final aSubject = a.su.firstOrNull?.id;
|
||||
final bSubject = b.su.firstOrNull?.id;
|
||||
if (aSubject == null || bSubject == null || aSubject != bSubject) {
|
||||
return false;
|
||||
}
|
||||
if (a.ro.firstOrNull?.id != b.ro.firstOrNull?.id) return false;
|
||||
if (a.te.firstOrNull?.id != b.te.firstOrNull?.id) return false;
|
||||
if (a.code != b.code) return false;
|
||||
final gap = WebuntisTime.parse(
|
||||
b.date,
|
||||
b.startTime,
|
||||
).difference(WebuntisTime.parse(a.date, a.endTime));
|
||||
return !gap.isNegative && gap <= maxGap;
|
||||
}
|
||||
|
||||
static WidgetLesson _mapLesson(
|
||||
GetTimetableResponseObject lesson,
|
||||
DateTime now,
|
||||
GetSubjectsResponse? subjects,
|
||||
GetRoomsResponse? rooms,
|
||||
) {
|
||||
final start = WebuntisTime.parse(lesson.date, lesson.startTime);
|
||||
final end = WebuntisTime.parse(lesson.date, lesson.endTime);
|
||||
final status = _mapStatus(
|
||||
LessonStatusClassifier.classify(lesson, start, end, now),
|
||||
);
|
||||
final subject = lesson.su.firstOrNull;
|
||||
// Webuntis sometimes ships subject-less entries (Wandertag etc.). Fall
|
||||
// back to "Event" so the tile isn't just a dash.
|
||||
final rawSubjectName = subject?.name.trim() ?? '';
|
||||
final subjectShort = rawSubjectName.isEmpty ? 'Event' : rawSubjectName;
|
||||
String? subjectLong;
|
||||
if (subjects != null && subject != null) {
|
||||
final found = subjects.result.where((s) => s.id == subject.id).firstOrNull;
|
||||
subjectLong = found?.longName;
|
||||
}
|
||||
subjectLong ??= subject?.longname;
|
||||
final room = lesson.ro.firstOrNull;
|
||||
var roomName = room?.name;
|
||||
if (rooms != null && room != null) {
|
||||
final resolved =
|
||||
rooms.result.where((r) => r.id == room.id).firstOrNull?.name;
|
||||
roomName = resolved ?? roomName;
|
||||
}
|
||||
final teacher = lesson.te.firstOrNull;
|
||||
final teacherName = teacher?.id == 0 ? null : teacher?.name;
|
||||
final originalTeacher = teacher?.orgname;
|
||||
return WidgetLesson(
|
||||
start: start,
|
||||
end: end,
|
||||
subjectShort: subjectShort,
|
||||
subjectLong: subjectLong,
|
||||
room: roomName,
|
||||
teacher: teacherName,
|
||||
originalTeacher: originalTeacher,
|
||||
status: status,
|
||||
);
|
||||
}
|
||||
|
||||
static WidgetLessonStatus _mapStatus(LessonStatus status) {
|
||||
switch (status) {
|
||||
case LessonStatus.cancelled:
|
||||
return WidgetLessonStatus.cancelled;
|
||||
case LessonStatus.event:
|
||||
return WidgetLessonStatus.event;
|
||||
case LessonStatus.irregular:
|
||||
return WidgetLessonStatus.irregular;
|
||||
case LessonStatus.teacherChanged:
|
||||
return WidgetLessonStatus.teacherChanged;
|
||||
case LessonStatus.past:
|
||||
return WidgetLessonStatus.past;
|
||||
case LessonStatus.ongoing:
|
||||
return WidgetLessonStatus.ongoing;
|
||||
case LessonStatus.regular:
|
||||
return WidgetLessonStatus.regular;
|
||||
}
|
||||
}
|
||||
|
||||
static bool _onSameDay(GetTimetableResponseObject lesson, DateTime day) {
|
||||
final dt = WebuntisTime.parse(lesson.date, lesson.startTime);
|
||||
return dt.year == day.year && dt.month == day.month && dt.day == day.day;
|
||||
}
|
||||
|
||||
static GetHolidaysResponseObject? _findHoliday(
|
||||
DateTime day,
|
||||
GetHolidaysResponse? holidays,
|
||||
) {
|
||||
if (holidays == null) return null;
|
||||
final asInt = WebuntisTime.formatDate(day);
|
||||
for (final h in holidays.result) {
|
||||
if (asInt >= h.startDate && asInt <= h.endDate) return h;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Iterable<WidgetLesson> _expandCustomEvents(
|
||||
GetCustomTimetableEventResponse? customEvents,
|
||||
DateTime rangeStart,
|
||||
DateTime rangeEndExclusive,
|
||||
) sync* {
|
||||
if (customEvents == null) return;
|
||||
final rangeStartUtc = rangeStart.toUtc();
|
||||
final rangeEndUtc = rangeEndExclusive.toUtc();
|
||||
for (final event in customEvents.events) {
|
||||
yield* _expandSingleEvent(event, rangeStartUtc, rangeEndUtc);
|
||||
}
|
||||
}
|
||||
|
||||
static Iterable<WidgetLesson> _expandSingleEvent(
|
||||
CustomTimetableEvent event,
|
||||
DateTime rangeStartUtc,
|
||||
DateTime rangeEndUtc,
|
||||
) sync* {
|
||||
final rule = event.rrule;
|
||||
final duration = event.endDate.difference(event.startDate);
|
||||
|
||||
if (rule.isEmpty) {
|
||||
final startUtc = event.startDate.toUtc();
|
||||
if (startUtc.isBefore(rangeStartUtc) ||
|
||||
!startUtc.isBefore(rangeEndUtc)) {
|
||||
return;
|
||||
}
|
||||
yield* _customEventToWidgetLessons(event, event.startDate, duration);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final parsed = RecurrenceRule.fromString(rule);
|
||||
final anchorUtc = event.startDate.toUtc();
|
||||
for (final occUtc in parsed.getInstances(start: anchorUtc)) {
|
||||
if (!occUtc.isBefore(rangeEndUtc)) break;
|
||||
if (occUtc.isBefore(rangeStartUtc)) continue;
|
||||
final occLocal = occUtc.toLocal();
|
||||
final occStart = DateTime(
|
||||
occLocal.year,
|
||||
occLocal.month,
|
||||
occLocal.day,
|
||||
event.startDate.hour,
|
||||
event.startDate.minute,
|
||||
);
|
||||
yield* _customEventToWidgetLessons(event, occStart, duration);
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
log('Widget mapper: invalid rrule "$rule" on event ${event.id}: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Splits multi-day events into one block per local calendar day, so each
|
||||
/// affected day on the week-widget shows the event. All-day events
|
||||
/// (start = end = midnight) collapse to a single 00:00–23:59 block.
|
||||
static Iterable<WidgetLesson> _customEventToWidgetLessons(
|
||||
CustomTimetableEvent event,
|
||||
DateTime occurrenceStart,
|
||||
Duration duration,
|
||||
) sync* {
|
||||
final title = event.title.trim();
|
||||
WidgetLesson buildBlock(DateTime start, DateTime end) => WidgetLesson(
|
||||
start: start,
|
||||
end: end,
|
||||
subjectShort: title.isEmpty ? 'Termin' : title,
|
||||
subjectLong: title.isEmpty ? null : title,
|
||||
status: WidgetLessonStatus.event,
|
||||
customColor: event.color,
|
||||
);
|
||||
|
||||
final isAllDay = duration == Duration.zero && _isMidnight(event.startDate);
|
||||
if (isAllDay) {
|
||||
yield buildBlock(
|
||||
occurrenceStart,
|
||||
DateTime(
|
||||
occurrenceStart.year,
|
||||
occurrenceStart.month,
|
||||
occurrenceStart.day,
|
||||
23,
|
||||
59,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final actualEnd = occurrenceStart.add(duration);
|
||||
var segmentStart = occurrenceStart;
|
||||
while (segmentStart.isBefore(actualEnd)) {
|
||||
final nextMidnight = DateTime(
|
||||
segmentStart.year,
|
||||
segmentStart.month,
|
||||
segmentStart.day,
|
||||
).add(const Duration(days: 1));
|
||||
final segmentEnd = actualEnd.isBefore(nextMidnight)
|
||||
? actualEnd
|
||||
: nextMidnight.subtract(const Duration(minutes: 1));
|
||||
yield buildBlock(segmentStart, segmentEnd);
|
||||
segmentStart = nextMidnight;
|
||||
}
|
||||
}
|
||||
|
||||
static bool _isMidnight(DateTime d) =>
|
||||
d.hour == 0 && d.minute == 0 && d.second == 0;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// Android-only bridge: MainActivity stashes `widget_open_timetable=true`
|
||||
/// from the launch Intent extra when a widget is tapped, Dart polls once
|
||||
/// per app-resume to consume and route. iOS widgets simply launch the app
|
||||
/// without a navigation hint (no widgetURL set) so this returns `false`
|
||||
/// there via MissingPluginException.
|
||||
class WidgetNavigation {
|
||||
static const MethodChannel _channel = MethodChannel('eu.mhsl.marianum.widget');
|
||||
|
||||
static Future<bool> consumePendingTimetableTap() async {
|
||||
try {
|
||||
final raw = await _channel.invokeMethod<bool>('consumePendingNavigation');
|
||||
return raw ?? false;
|
||||
} on MissingPluginException {
|
||||
return false;
|
||||
} on PlatformException catch (e) {
|
||||
log('WidgetNavigation channel error: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../state/app/modules/timetable/bloc/timetable_state.dart';
|
||||
import '../storage/settings.dart';
|
||||
import 'widget_data_mapper.dart';
|
||||
import 'widget_sync.dart';
|
||||
|
||||
/// Pushes timetable state to the native widget whenever the foreground bloc
|
||||
/// has fresh data, so the widget doesn't have to wait for the next periodic
|
||||
/// background fetch.
|
||||
class WidgetPublisher {
|
||||
/// Debug-only "now" offset. Gated by [kDebugMode] so a stray non-zero
|
||||
/// value cannot ship in release.
|
||||
static const Duration debugTimeShift = Duration.zero;
|
||||
|
||||
static DateTime widgetNow() =>
|
||||
kDebugMode ? DateTime.now().add(debugTimeShift) : DateTime.now();
|
||||
|
||||
static Future<void> publishFromBlocState(
|
||||
TimetableState state, {
|
||||
Settings? settings,
|
||||
}) async {
|
||||
try {
|
||||
final connectDouble =
|
||||
settings?.timetableSettings.connectDoubleLessons ?? true;
|
||||
// Mirror into widget storage so the background isolate sees the same
|
||||
// value the user just toggled.
|
||||
await WidgetSync.setConnectDoubleLessons(connectDouble);
|
||||
await WidgetSync.setThemeMode(_themeName(settings?.appTheme));
|
||||
final lessons = state.getAllKnownLessons();
|
||||
final now = widgetNow();
|
||||
final dayData = WidgetDataMapper.buildDayData(
|
||||
now: now,
|
||||
lessons: lessons,
|
||||
subjects: state.subjects,
|
||||
rooms: state.rooms,
|
||||
holidays: state.schoolHolidays,
|
||||
timegrid: state.timegrid,
|
||||
customEvents: state.customEvents,
|
||||
connectDoubleLessons: connectDouble,
|
||||
);
|
||||
final weekData = WidgetDataMapper.buildWeekData(
|
||||
now: now,
|
||||
lessons: lessons,
|
||||
subjects: state.subjects,
|
||||
rooms: state.rooms,
|
||||
holidays: state.schoolHolidays,
|
||||
timegrid: state.timegrid,
|
||||
customEvents: state.customEvents,
|
||||
connectDoubleLessons: connectDouble,
|
||||
);
|
||||
await WidgetSync.writeDayData(dayData);
|
||||
await WidgetSync.writeWeekData(weekData);
|
||||
await WidgetSync.setLoggedIn(true);
|
||||
await WidgetSync.triggerUpdate();
|
||||
} on Object catch (e, s) {
|
||||
// Catch Object: non-Exception Errors (RangeError, StateError) from the
|
||||
// bloc layer must not escape into the stream listener.
|
||||
log('WidgetPublisher.publishFromBlocState failed: $e', stackTrace: s);
|
||||
}
|
||||
}
|
||||
|
||||
static String _themeName(ThemeMode? mode) {
|
||||
switch (mode) {
|
||||
case ThemeMode.light:
|
||||
return 'light';
|
||||
case ThemeMode.dark:
|
||||
return 'dark';
|
||||
case ThemeMode.system:
|
||||
case null:
|
||||
return 'system';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:home_widget/home_widget.dart';
|
||||
|
||||
import 'widget_data.dart';
|
||||
|
||||
/// Bridge to the native widget host. All keys/names live here so the Kotlin
|
||||
/// and Swift sides stay in sync.
|
||||
class WidgetSync {
|
||||
static const String iosAppGroupId =
|
||||
'group.eu.mhsl.marianum.mobile.client.widget';
|
||||
|
||||
static const String iosWidgetKind = 'TimetableWidget';
|
||||
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.
|
||||
static const String dayDataKey = 'widget_data_day_v1';
|
||||
static const String weekDataKey = 'widget_data_week_v1';
|
||||
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
|
||||
// without reopening HydratedBloc storage.
|
||||
static const String connectDoubleLessonsKey =
|
||||
'widget_setting_connect_double_lessons_v1';
|
||||
static const String themeModeKey = 'widget_setting_theme_mode_v1';
|
||||
|
||||
static bool _initialised = false;
|
||||
|
||||
static Future<void> ensureInitialized() async {
|
||||
if (_initialised) return;
|
||||
await HomeWidget.setAppGroupId(iosAppGroupId);
|
||||
_initialised = true;
|
||||
}
|
||||
|
||||
static Future<void> writeDayData(WidgetTimetableData data) async {
|
||||
await ensureInitialized();
|
||||
await HomeWidget.saveWidgetData<String>(dayDataKey, jsonEncode(data.toJson()));
|
||||
await HomeWidget.saveWidgetData<String>(
|
||||
fetchedAtKey,
|
||||
data.fetchedAt.toIso8601String(),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> writeWeekData(WidgetTimetableData data) async {
|
||||
await ensureInitialized();
|
||||
await HomeWidget.saveWidgetData<String>(
|
||||
weekDataKey,
|
||||
jsonEncode(data.toJson()),
|
||||
);
|
||||
await HomeWidget.saveWidgetData<String>(
|
||||
fetchedAtKey,
|
||||
data.fetchedAt.toIso8601String(),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> setLoggedIn(bool loggedIn) async {
|
||||
await ensureInitialized();
|
||||
await HomeWidget.saveWidgetData<bool>(loggedInKey, loggedIn);
|
||||
}
|
||||
|
||||
static Future<void> setConnectDoubleLessons(bool value) async {
|
||||
await ensureInitialized();
|
||||
await HomeWidget.saveWidgetData<bool>(connectDoubleLessonsKey, value);
|
||||
}
|
||||
|
||||
/// Default `true` matches `default_settings.dart` — fresh install behaves
|
||||
/// like the in-app calendar.
|
||||
static Future<bool> getConnectDoubleLessons() async {
|
||||
await ensureInitialized();
|
||||
final value = await HomeWidget.getWidgetData<bool>(
|
||||
connectDoubleLessonsKey,
|
||||
defaultValue: true,
|
||||
);
|
||||
return value ?? true;
|
||||
}
|
||||
|
||||
static Future<void> setThemeMode(String mode) async {
|
||||
await ensureInitialized();
|
||||
await HomeWidget.saveWidgetData<String>(themeModeKey, mode);
|
||||
}
|
||||
|
||||
static Future<void> clear() async {
|
||||
await ensureInitialized();
|
||||
await HomeWidget.saveWidgetData<String>(dayDataKey, null);
|
||||
await HomeWidget.saveWidgetData<String>(weekDataKey, null);
|
||||
await HomeWidget.saveWidgetData<String>(fetchedAtKey, null);
|
||||
await HomeWidget.saveWidgetData<bool>(loggedInKey, false);
|
||||
}
|
||||
|
||||
static Future<void> triggerUpdate() async {
|
||||
await ensureInitialized();
|
||||
try {
|
||||
await HomeWidget.updateWidget(
|
||||
androidName: androidDayProvider,
|
||||
iOSName: iosWidgetKind,
|
||||
);
|
||||
await HomeWidget.updateWidget(
|
||||
androidName: androidWeekProvider,
|
||||
iOSName: iosWidgetKind,
|
||||
);
|
||||
} on Exception catch (e) {
|
||||
log('WidgetSync.triggerUpdate failed: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user