implemented recurrence exception (EXDATE) support for custom events, refactored timetable break and holiday generation logic, and refined RRule editor UI/theming and tile layouts

This commit is contained in:
2026-05-14 12:58:29 +02:00
parent 194d8d1857
commit 2cb8321d07
8 changed files with 221 additions and 48 deletions
@@ -22,36 +22,56 @@ class SpecialRegionsBuilder {
});
List<TimeRegion> build() {
final lastMonday = DateTime.now()
final rangeStart = DateTime.now()
.subtract(const Duration(days: 14))
.nextWeekday(DateTime.monday);
// Far enough out to cover comfortable scrolling without rebuilding;
// Syncfusion only paints regions that intersect the visible window so
// the extra entries don't hurt rendering cost.
final rangeEnd = DateTime.now().add(const Duration(days: 180));
final holidayRegions = _buildHolidayRegions().toList();
bool isInHoliday(DateTime time) =>
holidayRegions.any((region) => region.startTime.isSameDay(time));
final holidayDays = holidayRegions
.map((r) => _dayKey(r.startTime))
.toSet();
final breakRegions = schedule.periods
.where((p) => p.isBreak)
.map((p) {
final start = lastMonday.copyWith(
hour: p.start.hour,
minute: p.start.minute,
);
return _breakRegion(start, p.duration);
})
.where((region) => !isInHoliday(region.startTime));
// Materialise one TimeRegion per break per non-holiday day. Tried
// `recurrenceRule: FREQ=DAILY` with `recurrenceExceptionDates` first,
// but Syncfusion's recurrence exception matching is unreliable in
// practice — the break overlay kept showing on holiday days. Generating
// explicit per-day regions and just skipping holidays is robust.
final breakPeriods = schedule.periods.where((p) => p.isBreak).toList();
final breakRegions = <TimeRegion>[];
for (
var day = rangeStart;
!day.isAfter(rangeEnd);
day = day.add(const Duration(days: 1))
) {
if (holidayDays.contains(_dayKey(day))) continue;
for (final p in breakPeriods) {
final start = day.copyWith(
hour: p.start.hour,
minute: p.start.minute,
);
breakRegions.add(_breakRegion(start, p.duration));
}
}
return [...holidayRegions, ...breakRegions];
}
static String _dayKey(DateTime d) => '${d.year}-${d.month}-${d.day}';
Iterable<TimeRegion> _buildHolidayRegions() => holidays.result.expand((
holiday,
) {
final startDay = WebuntisTime.parse(holiday.startDate, 0);
final dayCount = WebuntisTime.parse(
holiday.endDate,
0,
).difference(startDay).inDays;
final endDay = WebuntisTime.parse(holiday.endDate, 0);
// Webuntis treats endDate inclusively (last day of the break) — the
// `+ 1` covers single-day public holidays (where startDate == endDate)
// and the final day of a multi-day vacation, both of which would
// otherwise be skipped.
final dayCount = endDay.difference(startDay).inDays + 1;
final days = List<DateTime>.generate(
dayCount,
(i) => startDay.add(Duration(days: i)),
@@ -66,7 +86,6 @@ class SpecialRegionsBuilder {
endTime: day.copyWith(hour: gridEndHour, minute: gridEndMinute),
text: '$kTimeRegionHolidayPrefix${holiday.name}',
color: disabledColor.withAlpha(50),
iconData: Icons.holiday_village_outlined,
),
);
});
@@ -74,7 +93,6 @@ class SpecialRegionsBuilder {
TimeRegion _breakRegion(DateTime start, Duration duration) => TimeRegion(
startTime: start,
endTime: start.add(duration),
recurrenceRule: 'FREQ=DAILY;INTERVAL=1',
text: kTimeRegionCenterIcon,
color: colorScheme.primary.withAlpha(50),
iconData: Icons.restaurant,