import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:syncfusion_flutter_calendar/calendar.dart'; import '../../../api/webuntis/queries/getHolidays/getHolidaysResponse.dart'; import '../../../api/webuntis/queries/getTimetable/getTimetableResponse.dart'; import '../../../extensions/dateTime.dart'; import '../../../model/timetable/timetableProps.dart'; import '../../../storage/base/settingsProvider.dart'; import 'appointmenetComponent.dart'; import 'appointmentDetails.dart'; import 'arbitraryAppointment.dart'; import 'customTimetableColors.dart'; import 'timeRegionComponent.dart'; import 'timetableEvents.dart'; import 'timetableNameMode.dart'; class Calendar extends StatefulWidget { final CalendarController controller; final TimetableProps timetableProps; final SettingsProvider settings; final bool isHomeWidget; const Calendar({super.key, required this.controller, required this.timetableProps, required this.settings, this.isHomeWidget = false}); @override State createState() => _CalendarState(); } class _CalendarState extends State { @override Widget build(BuildContext context) { var holidays = widget.timetableProps.getHolidaysResponse; return SfCalendar( timeZone: 'W. Europe Standard Time', view: widget.isHomeWidget ? CalendarView.day : CalendarView.workWeek, dataSource: _buildTableEvents(widget.timetableProps), maxDate: DateTime.now().add(const Duration(days: 7)).nextWeekday(DateTime.saturday), minDate: DateTime.now().subtract(const Duration (days: 14)).nextWeekday(DateTime.sunday), controller: widget.controller, onViewChanged: (ViewChangedDetails details) { if(widget.isHomeWidget) return; WidgetsBinding.instance.addPostFrameCallback((timeStamp) { Provider.of(context, listen: false).updateWeek(details.visibleDates.first, details.visibleDates.last); }); }, onTap: (calendarTapDetails) { if(calendarTapDetails.appointments == null) return; Appointment tapped = calendarTapDetails.appointments!.first; AppointmentDetails.show(context, widget.timetableProps, tapped); }, firstDayOfWeek: DateTime.monday, specialRegions: _buildSpecialTimeRegions(holidays), timeSlotViewSettings: TimeSlotViewSettings( startHour: widget.isHomeWidget ? 08 : 07.5, endHour: widget.isHomeWidget ? 16 : 16.5, timeInterval: Duration(minutes: 30), timeFormat: 'HH:mm', dayFormat: 'EE', timeIntervalHeight: 40, ), timeRegionBuilder: (BuildContext context, TimeRegionDetails timeRegionDetails) => TimeRegionComponent(details: timeRegionDetails), appointmentBuilder: (BuildContext context, CalendarAppointmentDetails details) => AppointmentComponent( details: details, crossedOut: _isCrossedOut(details) ), headerHeight: 0, selectionDecoration: const BoxDecoration(), allowAppointmentResize: false, allowDragAndDrop: false, allowViewNavigation: false, ); } List _buildSpecialTimeRegions(GetHolidaysResponse holidays) { var lastMonday = DateTime.now().subtract(const Duration(days: 14)).nextWeekday(DateTime.monday); var firstBreak = lastMonday.copyWith(hour: 10, minute: 15); var secondBreak = lastMonday.copyWith(hour: 13, minute: 50); var holidayList = holidays.result.map((holiday) { var startDay = _parseWebuntisTimestamp(holiday.startDate, 0); var dayCount = _parseWebuntisTimestamp(holiday.endDate, 0) .difference(startDay) .inDays; var days = List.generate(dayCount, (index) => startDay.add(Duration(days: index))); return days.map((holidayDay) => TimeRegion( startTime: holidayDay.copyWith(hour: 07, minute: 55), endTime: holidayDay.copyWith(hour: 16, minute: 30), text: 'holiday:${holiday.name}', color: Theme .of(context) .disabledColor .withAlpha(50), iconData: Icons.holiday_village_outlined )); }).expand((e) => e); bool isInHoliday(DateTime time) => holidayList.any((element) => element.startTime.isSameDay(time)); return [ ...holidayList, if(!isInHoliday(firstBreak)) TimeRegion( startTime: firstBreak, endTime: firstBreak.add(const Duration(minutes: 20)), recurrenceRule: 'FREQ=DAILY;INTERVAL=1', text: 'centerIcon', color: Theme.of(context).primaryColor.withAlpha(50), iconData: Icons.restaurant ), if(!isInHoliday(secondBreak)) TimeRegion( startTime: secondBreak, endTime: secondBreak.add(const Duration(minutes: 15)), recurrenceRule: 'FREQ=DAILY;INTERVAL=1', text: 'centerIcon', color: Theme.of(context).primaryColor.withAlpha(50), iconData: Icons.restaurant ), ]; } List _removeDuplicates(TimetableProps data, Duration maxTimeBetweenDouble) { var timetableList = data.getTimetableResponse.result.toList(); if(timetableList.isEmpty) return timetableList; timetableList.sort((a, b) => _parseWebuntisTimestamp(a.date, a.startTime).compareTo(_parseWebuntisTimestamp(b.date, b.startTime))); var previousElement = timetableList.first; for(var i = 1; i < timetableList.length; i++) { var currentElement = timetableList.elementAt(i); bool isSameLesson() { var currentSubjectId = currentElement.su.firstOrNull?.id; var previousSubjectId = previousElement.su.firstOrNull?.id; if(currentSubjectId == null || previousSubjectId == null || currentSubjectId != previousSubjectId) return false; var currentRoomId = currentElement.ro.firstOrNull?.id; var previousRoomId = previousElement.ro.firstOrNull?.id; if(currentRoomId != previousRoomId) return false; var currentTeacherId = currentElement.te.firstOrNull?.id; var previousTeacherId = previousElement.te.firstOrNull?.id; if(currentTeacherId != previousTeacherId) return false; var currentStatusCode = currentElement.code; var previousStatusCode = previousElement.code; if(currentStatusCode != previousStatusCode) return false; return true; } bool isNotSeparated() => _parseWebuntisTimestamp(previousElement.date, previousElement.endTime).add(maxTimeBetweenDouble) .isSameOrAfter(_parseWebuntisTimestamp(currentElement.date, currentElement.startTime)); if(isSameLesson() && isNotSeparated()) { previousElement.endTime = currentElement.endTime; timetableList.remove(currentElement); i--; } else { previousElement = currentElement; } } return timetableList; } TimetableEvents _buildTableEvents(TimetableProps data) { var timetableList = data.getTimetableResponse.result.toList(); if(widget.settings.val().timetableSettings.connectDoubleLessons) { timetableList = _removeDuplicates(data, const Duration(minutes: 5)); } var appointments = timetableList.map((element) { var rooms = data.getRoomsResponse; var subjects = data.getSubjectsResponse; try { var startTime = _parseWebuntisTimestamp(element.date, element.startTime); var endTime = _parseWebuntisTimestamp(element.date, element.endTime); var subject = subjects.result.firstWhere((subject) => subject.id == element.su[0].id); var subjectName = { TimetableNameMode.name: subject.name, TimetableNameMode.longName: subject.longName, TimetableNameMode.alternateName: subject.alternateName, }[widget.settings.val().timetableSettings.timetableNameMode]; return Appointment( id: ArbitraryAppointment(webuntis: element), startTime: startTime, endTime: endTime, subject: subjectName!, location: '' '${rooms.result.firstWhere((room) => room.id == element.ro[0].id).name}' '\n' '${element.te.first.longname}', notes: element.activityType, color: _getEventColor(element, startTime, endTime), ); } catch(e) { var endTime = _parseWebuntisTimestamp(element.date, element.endTime); return Appointment( id: ArbitraryAppointment(webuntis: element), startTime: _parseWebuntisTimestamp(element.date, element.startTime), endTime: endTime, subject: 'Änderung', notes: element.info, location: 'Unbekannt', color: endTime.isBefore(DateTime.now()) ? Theme.of(context).primaryColor.withAlpha(100) : Theme.of(context).primaryColor, startTimeZone: '', endTimeZone: '', ); } }).toList(); appointments.addAll(data.getCustomTimetableEventResponse.events.map((customEvent) => Appointment( id: ArbitraryAppointment(custom: customEvent), startTime: customEvent.startDate, endTime: customEvent.endDate, location: customEvent.description, subject: customEvent.title, recurrenceRule: customEvent.rrule, color: TimetableColors.getColorFromString(customEvent.color ?? TimetableColors.defaultColor.name), startTimeZone: '', endTimeZone: '', ))); return TimetableEvents(appointments); } DateTime _parseWebuntisTimestamp(int date, int time) { var timeString = time.toString().padLeft(4, '0'); return DateTime.parse('$date ${timeString.substring(0, 2)}:${timeString.substring(2, 4)}'); } Color _getEventColor(GetTimetableResponseObject webuntisElement, DateTime startTime, DateTime endTime) { // Make element darker, when it already took place var alpha = endTime.isBefore(DateTime.now()) ? 100 : 255; // Cancelled if(webuntisElement.code == 'cancelled') return const Color(0xff000000).withAlpha(alpha); // Any changes or no teacher at this element if(webuntisElement.code == 'irregular' || webuntisElement.te.first.id == 0) return const Color(0xff8F19B3).withAlpha(alpha); // Teacher has changed if(webuntisElement.te.any((element) => element.orgname != null)) return const Color(0xFF29639B).withAlpha(alpha); // Event was in the past if(endTime.isBefore(DateTime.now())) return Theme.of(context).primaryColor.withAlpha(alpha); // Event takes currently place if(endTime.isAfter(DateTime.now()) && startTime.isBefore(DateTime.now())) return Theme.of(context).primaryColor.withRed(200); // Fallback return Theme.of(context).primaryColor.withAlpha(alpha); } bool _isCrossedOut(CalendarAppointmentDetails calendarEntry) { var appointment = calendarEntry.appointments.first.id as ArbitraryAppointment; if(appointment.hasWebuntis()) { return appointment.webuntis!.code == 'cancelled'; } return false; } }