timetable performance optimization
This commit is contained in:
@@ -213,10 +213,12 @@ class TimetableAppointmentFactory {
|
||||
return LessonColor.forStatus(status);
|
||||
}
|
||||
|
||||
McSubject? _findSubject(String? subjectShort) {
|
||||
if (subjectShort == null) return null;
|
||||
return subjects.where((s) => s.shortName == subjectShort).firstOrNull;
|
||||
}
|
||||
late final Map<String, McSubject> _subjectsByShort = {
|
||||
for (final s in subjects.reversed) s.shortName: s,
|
||||
};
|
||||
|
||||
McSubject? _findSubject(String? subjectShort) =>
|
||||
subjectShort == null ? null : _subjectsByShort[subjectShort];
|
||||
|
||||
String _subjectName(String? subjectShort, McTimetableEntry lesson) {
|
||||
if (subjectShort != null) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncfusion_flutter_calendar/calendar.dart';
|
||||
|
||||
@@ -11,16 +12,19 @@ class AppointmentTile extends StatelessWidget {
|
||||
final Appointment appointment;
|
||||
final bool crossedOut;
|
||||
|
||||
/// Ticking clock; only the tile background listens, so a lesson fades to
|
||||
/// the "past" look without rebuilding the text layout.
|
||||
final ValueListenable<DateTime> now;
|
||||
|
||||
const AppointmentTile({
|
||||
super.key,
|
||||
required this.appointment,
|
||||
required this.now,
|
||||
this.crossedOut = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isPast = appointment.endTime.isBefore(DateTime.now());
|
||||
final color = appointment.color.withAlpha(isPast ? 160 : 255);
|
||||
final isCustom = appointment.id is CustomAppointment;
|
||||
final description = appointment.location ?? '';
|
||||
|
||||
@@ -29,13 +33,19 @@ class AppointmentTile extends StatelessWidget {
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
alignment: Alignment.topLeft,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.rectangle,
|
||||
borderRadius: _radius,
|
||||
color: color,
|
||||
child: ValueListenableBuilder<DateTime>(
|
||||
valueListenable: now,
|
||||
builder: (context, now, child) => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
alignment: Alignment.topLeft,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.rectangle,
|
||||
borderRadius: _radius,
|
||||
color: appointment.color.withAlpha(
|
||||
appointment.endTime.isBefore(now) ? 160 : 255,
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
child: _TileContent(
|
||||
title: appointment.subject,
|
||||
@@ -100,8 +110,10 @@ class _TileContent extends StatelessWidget {
|
||||
// entirely; the coloured rectangle is enough.
|
||||
if (available < titleLineHeight) return const SizedBox.shrink();
|
||||
|
||||
final remaining =
|
||||
(available - titleLineHeight).clamp(0.0, double.infinity);
|
||||
final remaining = (available - titleLineHeight).clamp(
|
||||
0.0,
|
||||
double.infinity,
|
||||
);
|
||||
final bodyLineCapacity = (remaining / bodyLineHeight).floor();
|
||||
|
||||
if (isCustom) {
|
||||
@@ -187,7 +199,9 @@ class _AdaptiveTitle extends StatelessWidget {
|
||||
maxLines: 1,
|
||||
textScaler: textScaler,
|
||||
)..layout();
|
||||
if (probe.width > constraints.maxWidth) {
|
||||
final overflows = probe.width > constraints.maxWidth;
|
||||
probe.dispose();
|
||||
if (overflows) {
|
||||
return Text(
|
||||
text,
|
||||
style: baseStyle.copyWith(fontSize: minFontSize),
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
part of '../custom_workweek_calendar.dart';
|
||||
|
||||
class _OutsideHoursStrip extends StatelessWidget {
|
||||
final DateTime weekStart;
|
||||
final List<Appointment> appointments;
|
||||
final List<List<Appointment>> outside;
|
||||
final double rulerWidth;
|
||||
final void Function(Appointment) onAppointmentTap;
|
||||
final bool Function(Appointment) isCrossedOut;
|
||||
|
||||
const _OutsideHoursStrip({
|
||||
super.key,
|
||||
required this.weekStart,
|
||||
required this.appointments,
|
||||
required this.outside,
|
||||
required this.rulerWidth,
|
||||
required this.onAppointmentTap,
|
||||
required this.isCrossedOut,
|
||||
@@ -18,10 +16,6 @@ class _OutsideHoursStrip extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final outside = partitionAppointmentsForWeek(
|
||||
appointments,
|
||||
weekStart,
|
||||
).outside;
|
||||
if (outside.every((day) => day.isEmpty)) return const SizedBox.shrink();
|
||||
|
||||
final theme = Theme.of(context);
|
||||
|
||||
@@ -1,10 +1,31 @@
|
||||
part of '../custom_workweek_calendar.dart';
|
||||
|
||||
/// Per-week derived data, computed once per appointment/region set instead of
|
||||
/// on every rebuild of a week page.
|
||||
class _WeekData {
|
||||
final List<List<Appointment>> inside;
|
||||
final List<List<Appointment>> outside;
|
||||
final List<List<BoundRegion>> regions;
|
||||
|
||||
_WeekData._(this.inside, this.outside, this.regions);
|
||||
|
||||
factory _WeekData.compute(
|
||||
DateTime weekStart,
|
||||
List<Appointment> appointments,
|
||||
List<TimeRegion> timeRegions,
|
||||
) {
|
||||
final partitioned = partitionAppointmentsForWeek(appointments, weekStart);
|
||||
return _WeekData._(partitioned.inside, partitioned.outside, [
|
||||
for (var d = 0; d < 5; d++)
|
||||
expandRegionsForDay(timeRegions, weekStart.addDays(d)),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
class _WeekGrid extends StatelessWidget {
|
||||
final DateTime weekStart;
|
||||
final LessonPeriodSchedule schedule;
|
||||
final List<Appointment> appointments;
|
||||
final List<TimeRegion> timeRegions;
|
||||
final _WeekData data;
|
||||
final void Function(Appointment) onAppointmentTap;
|
||||
final bool Function(Appointment) isCrossedOut;
|
||||
final void Function(DateTime start, DateTime end)? onCreateEvent;
|
||||
@@ -16,8 +37,7 @@ class _WeekGrid extends StatelessWidget {
|
||||
const _WeekGrid({
|
||||
required this.weekStart,
|
||||
required this.schedule,
|
||||
required this.appointments,
|
||||
required this.timeRegions,
|
||||
required this.data,
|
||||
required this.onAppointmentTap,
|
||||
required this.isCrossedOut,
|
||||
required this.onCreateEvent,
|
||||
@@ -29,8 +49,6 @@ class _WeekGrid extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final partitioned = partitionAppointmentsForWeek(appointments, weekStart);
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
@@ -48,8 +66,8 @@ class _WeekGrid extends StatelessWidget {
|
||||
child: _DayColumn(
|
||||
date: weekStart.addDays(d),
|
||||
schedule: schedule,
|
||||
appointments: partitioned.inside[d],
|
||||
timeRegions: timeRegions,
|
||||
appointments: data.inside[d],
|
||||
regions: data.regions[d],
|
||||
layout: layout,
|
||||
today: today,
|
||||
nowNotifier: nowNotifier,
|
||||
@@ -195,7 +213,7 @@ class _DayColumn extends StatelessWidget {
|
||||
final DateTime date;
|
||||
final LessonPeriodSchedule schedule;
|
||||
final List<Appointment> appointments;
|
||||
final List<TimeRegion> timeRegions;
|
||||
final List<BoundRegion> regions;
|
||||
final PeriodLayout layout;
|
||||
final DateTime today;
|
||||
final ValueListenable<DateTime> nowNotifier;
|
||||
@@ -207,7 +225,7 @@ class _DayColumn extends StatelessWidget {
|
||||
required this.date,
|
||||
required this.schedule,
|
||||
required this.appointments,
|
||||
required this.timeRegions,
|
||||
required this.regions,
|
||||
required this.layout,
|
||||
required this.today,
|
||||
required this.nowNotifier,
|
||||
@@ -280,12 +298,10 @@ class _DayColumn extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final dayAppointments = appointments;
|
||||
final dayRegions = expandRegionsForDay(timeRegions, date);
|
||||
final isToday = date.isSameDay(today);
|
||||
|
||||
final isTablet = MediaQuery.of(context).size.shortestSide >= 600;
|
||||
final laidOut = assignLanes(dayAppointments, maxLanes: isTablet ? 3 : 2);
|
||||
final isTablet = MediaQuery.sizeOf(context).shortestSide >= 600;
|
||||
final laidOut = assignLanes(appointments, maxLanes: isTablet ? 3 : 2);
|
||||
|
||||
final dayName = DateFormat(
|
||||
'EEEE',
|
||||
@@ -304,8 +320,7 @@ class _DayColumn extends StatelessWidget {
|
||||
sortKey: OrdinalSortKey(date.weekday.toDouble()),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onLongPressStart: (details) =>
|
||||
_handleLongPress(details, dayAppointments),
|
||||
onLongPressStart: (details) => _handleLongPress(details, appointments),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: isToday ? theme.colorScheme.primary.withAlpha(14) : null,
|
||||
@@ -342,7 +357,7 @@ class _DayColumn extends StatelessWidget {
|
||||
color: theme.dividerColor.withAlpha(60),
|
||||
),
|
||||
),
|
||||
for (final region in dayRegions)
|
||||
for (final region in regions)
|
||||
Positioned(
|
||||
top: layout.yOfDateTime(region.start),
|
||||
height:
|
||||
@@ -379,6 +394,7 @@ class _DayColumn extends StatelessWidget {
|
||||
onTap: () => onAppointmentTap(appointment),
|
||||
child: AppointmentTile(
|
||||
appointment: appointment,
|
||||
now: nowNotifier,
|
||||
crossedOut: isCrossedOut(appointment),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -61,29 +61,41 @@ class CustomWorkWeekCalendarState extends State<CustomWorkWeekCalendar> {
|
||||
static const double _rulerWidth = 36;
|
||||
|
||||
late PageController _pageController;
|
||||
late int _currentWeekIndex;
|
||||
// Drives header + all-day strip. Kept out of setState so a page change
|
||||
// mid-swipe doesn't rebuild the PageView and every tile in it.
|
||||
late final ValueNotifier<int> _visibleWeekIndex;
|
||||
late int _reportedWeekIndex;
|
||||
late DateTime _firstMonday;
|
||||
late int _totalWeeks;
|
||||
late Timer _ticker;
|
||||
late ValueNotifier<DateTime> _nowNotifier;
|
||||
DateTime _today = _dateOnly(DateTime.now());
|
||||
DateTime _today = DateTime.now().dateOnly;
|
||||
|
||||
final Map<int, _WeekData> _weekData = {};
|
||||
// Returning the identical widget instance lets Flutter skip rebuilding a
|
||||
// week page when the host rebuilds for unrelated reasons (e.g. the bloc
|
||||
// recording the new week after a swipe).
|
||||
final Map<int, _WeekGrid> _gridCache = {};
|
||||
Object? _cacheKey;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_firstMonday = _mondayOf(widget.minDate);
|
||||
final lastMonday = _mondayOf(widget.maxDate);
|
||||
_firstMonday = widget.minDate.mondayOfWeek;
|
||||
final lastMonday = widget.maxDate.mondayOfWeek;
|
||||
_totalWeeks = lastMonday.difference(_firstMonday).inDays ~/ 7 + 1;
|
||||
_currentWeekIndex =
|
||||
_mondayOf(widget.initialDate).difference(_firstMonday).inDays ~/ 7;
|
||||
_pageController = PageController(initialPage: _currentWeekIndex);
|
||||
final initialIndex =
|
||||
widget.initialDate.mondayOfWeek.difference(_firstMonday).inDays ~/ 7;
|
||||
_visibleWeekIndex = ValueNotifier<int>(initialIndex);
|
||||
_reportedWeekIndex = initialIndex;
|
||||
_pageController = PageController(initialPage: initialIndex);
|
||||
_nowNotifier = ValueNotifier<DateTime>(DateTime.now());
|
||||
|
||||
_ticker = Timer.periodic(const Duration(seconds: 30), (_) {
|
||||
if (!mounted) return;
|
||||
final now = DateTime.now();
|
||||
_nowNotifier.value = now;
|
||||
final newToday = _dateOnly(now);
|
||||
final newToday = now.dateOnly;
|
||||
if (newToday != _today) setState(() => _today = newToday);
|
||||
});
|
||||
}
|
||||
@@ -99,11 +111,11 @@ class CustomWorkWeekCalendarState extends State<CustomWorkWeekCalendar> {
|
||||
// the initial mount with the conservative fallback). Recompute the
|
||||
// range and snap the controller to the page that still represents the
|
||||
// currently visible week so the user doesn't get yanked around.
|
||||
final newFirstMonday = _mondayOf(widget.minDate);
|
||||
final newLastMonday = _mondayOf(widget.maxDate);
|
||||
final newFirstMonday = widget.minDate.mondayOfWeek;
|
||||
final newLastMonday = widget.maxDate.mondayOfWeek;
|
||||
final newTotalWeeks =
|
||||
newLastMonday.difference(newFirstMonday).inDays ~/ 7 + 1;
|
||||
final visibleWeekStart = _firstMonday.addDays(_currentWeekIndex * 7);
|
||||
final visibleWeekStart = _firstMonday.addDays(_visibleWeekIndex.value * 7);
|
||||
final newIndex = visibleWeekStart.difference(newFirstMonday).inDays ~/ 7;
|
||||
final clampedIndex = newIndex.clamp(0, newTotalWeeks - 1);
|
||||
final oldController = _pageController;
|
||||
@@ -112,8 +124,9 @@ class CustomWorkWeekCalendarState extends State<CustomWorkWeekCalendar> {
|
||||
setState(() {
|
||||
_firstMonday = newFirstMonday;
|
||||
_totalWeeks = newTotalWeeks;
|
||||
_currentWeekIndex = clampedIndex;
|
||||
});
|
||||
_visibleWeekIndex.value = clampedIndex;
|
||||
_reportedWeekIndex = clampedIndex;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -121,13 +134,12 @@ class CustomWorkWeekCalendarState extends State<CustomWorkWeekCalendar> {
|
||||
_pageController.dispose();
|
||||
_ticker.cancel();
|
||||
_nowNotifier.dispose();
|
||||
_visibleWeekIndex.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
static DateTime _dateOnly(DateTime d) => DateTime(d.year, d.month, d.day);
|
||||
|
||||
void jumpToDate(DateTime date) {
|
||||
final target = _mondayOf(date).difference(_firstMonday).inDays ~/ 7;
|
||||
final target = date.mondayOfWeek.difference(_firstMonday).inDays ~/ 7;
|
||||
if (target < 0 || target >= _totalWeeks) return;
|
||||
_pageController.animateToPage(
|
||||
target,
|
||||
@@ -136,16 +148,79 @@ class CustomWorkWeekCalendarState extends State<CustomWorkWeekCalendar> {
|
||||
);
|
||||
}
|
||||
|
||||
static DateTime _mondayOf(DateTime d) {
|
||||
final monday = d.subtractDays(d.weekday - 1);
|
||||
return DateTime(monday.year, monday.month, monday.day);
|
||||
/// Drops both per-week caches when any input they were built from changed.
|
||||
void _syncCaches() {
|
||||
final key = (
|
||||
widget.appointments,
|
||||
widget.timeRegions,
|
||||
widget.schedule,
|
||||
widget.onCreateEvent == null,
|
||||
_today,
|
||||
_firstMonday,
|
||||
);
|
||||
if (key == _cacheKey) return;
|
||||
_cacheKey = key;
|
||||
_weekData.clear();
|
||||
_gridCache.clear();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final visibleWeekStart = _firstMonday.addDays(_currentWeekIndex * 7);
|
||||
_WeekData _weekDataFor(int weekIndex) {
|
||||
return _weekData.putIfAbsent(
|
||||
weekIndex,
|
||||
() => _WeekData.compute(
|
||||
_firstMonday.addDays(weekIndex * 7),
|
||||
widget.appointments,
|
||||
widget.timeRegions,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Tells the host about the new week only once the swipe has settled, so
|
||||
/// the resulting bloc emit + rebuild never lands inside the page animation.
|
||||
bool _onPageScroll(ScrollNotification notification) {
|
||||
if (notification is! ScrollEndNotification || notification.depth != 0) {
|
||||
return false;
|
||||
}
|
||||
final index = _visibleWeekIndex.value;
|
||||
if (index == _reportedWeekIndex) return false;
|
||||
_reportedWeekIndex = index;
|
||||
final weekStart = _firstMonday.addDays(index * 7);
|
||||
widget.onWeekChanged(weekStart, weekStart.addDays(4));
|
||||
return false;
|
||||
}
|
||||
|
||||
void _onAppointmentTap(Appointment appointment) =>
|
||||
widget.onAppointmentTap(appointment);
|
||||
|
||||
bool _isCrossedOut(Appointment appointment) =>
|
||||
widget.isCrossedOut(appointment);
|
||||
|
||||
void _onCreateEvent(DateTime start, DateTime end) =>
|
||||
widget.onCreateEvent?.call(start, end);
|
||||
|
||||
_WeekGrid _weekGrid(int weekIndex, PeriodLayout layout) {
|
||||
final cached = _gridCache[weekIndex];
|
||||
if (cached != null && cached.layout.lessonHeight == layout.lessonHeight) {
|
||||
return cached;
|
||||
}
|
||||
// Only the pages next to the visible one are ever alive.
|
||||
_gridCache.removeWhere((i, _) => (i - weekIndex).abs() > 2);
|
||||
return _gridCache[weekIndex] = _WeekGrid(
|
||||
weekStart: _firstMonday.addDays(weekIndex * 7),
|
||||
schedule: widget.schedule,
|
||||
data: _weekDataFor(weekIndex),
|
||||
onAppointmentTap: _onAppointmentTap,
|
||||
isCrossedOut: _isCrossedOut,
|
||||
onCreateEvent: widget.onCreateEvent == null ? null : _onCreateEvent,
|
||||
today: _today,
|
||||
nowNotifier: _nowNotifier,
|
||||
rulerWidth: _rulerWidth,
|
||||
layout: layout,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWeekHeader(int weekIndex) {
|
||||
final visibleWeekStart = _firstMonday.addDays(weekIndex * 7);
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
@@ -190,15 +265,29 @@ class CustomWorkWeekCalendarState extends State<CustomWorkWeekCalendar> {
|
||||
FadeTransition(opacity: animation, child: child),
|
||||
child: _OutsideHoursStrip(
|
||||
key: ValueKey(visibleWeekStart),
|
||||
weekStart: visibleWeekStart,
|
||||
appointments: widget.appointments,
|
||||
outside: _weekDataFor(weekIndex).outside,
|
||||
rulerWidth: _rulerWidth,
|
||||
onAppointmentTap: widget.onAppointmentTap,
|
||||
isCrossedOut: widget.isCrossedOut,
|
||||
onAppointmentTap: _onAppointmentTap,
|
||||
isCrossedOut: _isCrossedOut,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
_syncCaches();
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
ValueListenableBuilder<int>(
|
||||
valueListenable: _visibleWeekIndex,
|
||||
builder: (_, weekIndex, _) => _buildWeekHeader(weekIndex),
|
||||
),
|
||||
Container(height: 0.5, color: theme.dividerColor.withAlpha(110)),
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
@@ -225,30 +314,15 @@ class CustomWorkWeekCalendarState extends State<CustomWorkWeekCalendar> {
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: SizedBox(
|
||||
height: gridHeight,
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
itemCount: _totalWeeks,
|
||||
onPageChanged: (index) {
|
||||
setState(() => _currentWeekIndex = index);
|
||||
final weekStart = _firstMonday.addDays(index * 7);
|
||||
widget.onWeekChanged(weekStart, weekStart.addDays(4));
|
||||
},
|
||||
itemBuilder: (_, weekIndex) {
|
||||
final weekStart = _firstMonday.addDays(weekIndex * 7);
|
||||
return _WeekGrid(
|
||||
weekStart: weekStart,
|
||||
schedule: widget.schedule,
|
||||
appointments: widget.appointments,
|
||||
timeRegions: widget.timeRegions,
|
||||
onAppointmentTap: widget.onAppointmentTap,
|
||||
isCrossedOut: widget.isCrossedOut,
|
||||
onCreateEvent: widget.onCreateEvent,
|
||||
today: _today,
|
||||
nowNotifier: _nowNotifier,
|
||||
rulerWidth: _rulerWidth,
|
||||
layout: layout,
|
||||
);
|
||||
},
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: _onPageScroll,
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
itemCount: _totalWeeks,
|
||||
onPageChanged: (index) => _visibleWeekIndex.value = index,
|
||||
itemBuilder: (_, weekIndex) =>
|
||||
_weekGrid(weekIndex, layout),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -55,6 +55,13 @@ class TimetableCalendarViewState extends State<TimetableCalendarView> {
|
||||
// the same identity checks the cache always used.
|
||||
(int, TimetableSettings, List<CustomTimetableEvent>, bool)? _cacheKey;
|
||||
|
||||
// Stable identities let the calendar reuse its per-week pages across
|
||||
// rebuilds; rebuilding these every frame would invalidate that cache.
|
||||
LessonPeriodSchedule? _schedule;
|
||||
Object? _scheduleKey;
|
||||
List<TimeRegion>? _regions;
|
||||
Object? _regionsKey;
|
||||
|
||||
DateTime _initialDisplayDate() => DateTime.now().addDays(2);
|
||||
|
||||
/// Snaps the calendar back to the current week. Exposed so host pages can
|
||||
@@ -90,7 +97,37 @@ class TimetableCalendarViewState extends State<TimetableCalendarView> {
|
||||
).build();
|
||||
}
|
||||
|
||||
bool _isCrossedOut(Appointment appointment) {
|
||||
LessonPeriodSchedule _scheduleFor(TimetableState state) {
|
||||
if (_schedule != null && identical(_scheduleKey, state.timegrid)) {
|
||||
return _schedule!;
|
||||
}
|
||||
_scheduleKey = state.timegrid;
|
||||
return _schedule = LessonPeriodSchedule.fromState(state);
|
||||
}
|
||||
|
||||
List<TimeRegion> _regionsFor(
|
||||
TimetableState state,
|
||||
LessonPeriodSchedule schedule,
|
||||
) {
|
||||
final theme = Theme.of(context);
|
||||
final key = (
|
||||
state.schoolHolidays,
|
||||
schedule,
|
||||
theme.colorScheme,
|
||||
theme.disabledColor,
|
||||
DateTime.now().dateOnly,
|
||||
);
|
||||
if (_regions != null && key == _regionsKey) return _regions!;
|
||||
_regionsKey = key;
|
||||
return _regions = SpecialRegionsBuilder(
|
||||
holidays: state.schoolHolidays!,
|
||||
schedule: schedule,
|
||||
colorScheme: theme.colorScheme,
|
||||
disabledColor: theme.disabledColor,
|
||||
).build();
|
||||
}
|
||||
|
||||
static bool _isCrossedOut(Appointment appointment) {
|
||||
final id = appointment.id;
|
||||
if (id is LessonAppointment) return id.entry.status == 'CANCELLED';
|
||||
return false;
|
||||
@@ -104,14 +141,9 @@ class TimetableCalendarViewState extends State<TimetableCalendarView> {
|
||||
// transient null can never crash the build.
|
||||
if (state.schoolHolidays == null) return const SizedBox.shrink();
|
||||
|
||||
final schedule = LessonPeriodSchedule.fromState(state);
|
||||
final schedule = _scheduleFor(state);
|
||||
final appointments = _appointments(state);
|
||||
final regions = SpecialRegionsBuilder(
|
||||
holidays: state.schoolHolidays!,
|
||||
schedule: schedule,
|
||||
colorScheme: Theme.of(context).colorScheme,
|
||||
disabledColor: Theme.of(context).disabledColor,
|
||||
).build();
|
||||
final regions = _regionsFor(state, schedule);
|
||||
|
||||
final capabilities = context.watch<CapabilitiesCubit>();
|
||||
final (minDate, maxDate) = _scrollBounds(
|
||||
@@ -171,8 +203,8 @@ class TimetableCalendarViewState extends State<TimetableCalendarView> {
|
||||
? state.accessibleEndDate!
|
||||
: baseMax)
|
||||
: baseMax;
|
||||
final today = _startOfDay(DateTime.now());
|
||||
final todayMonday = _mondayOf(today);
|
||||
final today = DateTime.now().dateOnly;
|
||||
final todayMonday = today.mondayOfWeek;
|
||||
final currentWeekEnd = todayMonday.addDays(DateTime.daysPerWeek - 1);
|
||||
final capMin = pastDays == null
|
||||
? null
|
||||
@@ -202,11 +234,6 @@ class TimetableCalendarViewState extends State<TimetableCalendarView> {
|
||||
return (mondayMin, finalMax);
|
||||
}
|
||||
|
||||
static DateTime _mondayOf(DateTime d) =>
|
||||
_startOfDay(d.subtractDays(d.weekday - 1));
|
||||
|
||||
static DateTime _startOfDay(DateTime d) => DateTime(d.year, d.month, d.day);
|
||||
|
||||
static DateTime _earlier(DateTime a, DateTime b) => a.isBefore(b) ? a : b;
|
||||
|
||||
static DateTime _later(DateTime a, DateTime b) => a.isAfter(b) ? a : b;
|
||||
|
||||
Reference in New Issue
Block a user