121 lines
4.3 KiB
Dart
121 lines
4.3 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:syncfusion_flutter_calendar/calendar.dart';
|
|
|
|
import '../../../../api/webuntis/queries/get_holidays/get_holidays_response.dart';
|
|
import '../../../../extensions/date_time.dart';
|
|
import '../data/calendar_layout.dart';
|
|
import '../data/lesson_period_schedule.dart';
|
|
import '../data/webuntis_time.dart';
|
|
import 'time_region_tile.dart';
|
|
|
|
class SpecialRegionsBuilder {
|
|
final GetHolidaysResponse holidays;
|
|
final LessonPeriodSchedule schedule;
|
|
final ColorScheme colorScheme;
|
|
final Color disabledColor;
|
|
|
|
SpecialRegionsBuilder({
|
|
required this.holidays,
|
|
required this.schedule,
|
|
required this.colorScheme,
|
|
required this.disabledColor,
|
|
});
|
|
|
|
List<TimeRegion> build() {
|
|
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();
|
|
final holidayDays = holidayRegions
|
|
.map((r) => _dayKey(r.startTime))
|
|
.toSet();
|
|
|
|
// 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() {
|
|
// Multiple Webuntis holiday entries can cover the same day (e.g. a
|
|
// public holiday falling inside a vacation period). Collapse them
|
|
// per-day so we emit exactly one TimeRegion per day and the
|
|
// overlapping labels don't render on top of each other.
|
|
final byDay = <String, _HolidayDay>{};
|
|
for (final holiday in holidays.result) {
|
|
final startDay = WebuntisTime.parse(holiday.startDate, 0);
|
|
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;
|
|
for (var i = 0; i < dayCount; i++) {
|
|
final day = startDay.add(Duration(days: i));
|
|
final key = _dayKey(day);
|
|
byDay.putIfAbsent(key, () => _HolidayDay(day, [])).names.add(
|
|
holiday.name,
|
|
);
|
|
}
|
|
}
|
|
final gridStartHour = kCalendarStartHour.floor();
|
|
final gridStartMinute = ((kCalendarStartHour - gridStartHour) * 60).round();
|
|
final gridEndHour = kCalendarEndHour.floor();
|
|
final gridEndMinute = ((kCalendarEndHour - gridEndHour) * 60).round();
|
|
return byDay.values.map(
|
|
(entry) => TimeRegion(
|
|
startTime: entry.day.copyWith(
|
|
hour: gridStartHour,
|
|
minute: gridStartMinute,
|
|
),
|
|
endTime: entry.day.copyWith(
|
|
hour: gridEndHour,
|
|
minute: gridEndMinute,
|
|
),
|
|
text: '$kTimeRegionHolidayPrefix${entry.names.join(" / ")}',
|
|
color: disabledColor.withAlpha(50),
|
|
),
|
|
);
|
|
}
|
|
|
|
TimeRegion _breakRegion(DateTime start, Duration duration) => TimeRegion(
|
|
startTime: start,
|
|
endTime: start.add(duration),
|
|
text: kTimeRegionCenterIcon,
|
|
color: colorScheme.primary.withAlpha(50),
|
|
iconData: Icons.restaurant,
|
|
);
|
|
}
|
|
|
|
class _HolidayDay {
|
|
final DateTime day;
|
|
final List<String> names;
|
|
_HolidayDay(this.day, this.names);
|
|
}
|