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/getRooms/getRoomsResponse.dart'; import '../../../api/webuntis/queries/getSubjects/getSubjectsResponse.dart'; import '../../../api/webuntis/queries/getTimetable/getTimetableResponse.dart'; import '../../../model/timetable/timetableProps.dart'; import '../../../storage/base/settingsProvider.dart'; import '../../../widget/loadingSpinner.dart'; import '../../../widget/placeholderView.dart'; import 'appointmenetComponent.dart'; import 'appointmentDetails.dart'; import 'timeRegionComponent.dart'; import 'timetableEvents.dart'; class Timetable extends StatefulWidget { const Timetable({Key? key}) : super(key: key); @override State createState() => _TimetableState(); } class _TimetableState extends State { CalendarController controller = CalendarController(); double elementScale = 40; double baseElementScale = 40; late final SettingsProvider settings; @override void initState() { settings = Provider.of(context, listen: false); elementScale = baseElementScale = settings.val().timetableSettings.zoom; WidgetsBinding.instance.addPostFrameCallback((timeStamp) { Provider.of(context, listen: false).run(); }); controller.displayDate = DateTime.now().add(const Duration(days: 2)); super.initState(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("Stunden & Vertretungsplan"), actions: [ IconButton( icon: const Icon(Icons.home_outlined), onPressed: () { controller.displayDate = DateTime.now().add(const Duration(days: 2)); } ), ], ), body: Consumer( builder: (context, value, child) { if(value.hasError) { return PlaceholderView( icon: Icons.calendar_month, text: "Webuntis error: ${value.error.toString()}", button: TextButton( child: const Text("Neu laden"), onPressed: () { Provider.of(context, listen: false).resetWeek(); }, ), ); } if(value.primaryLoading()) return const LoadingSpinner(); GetHolidaysResponse holidays = value.getHolidaysResponse; return GestureDetector( onScaleStart: (details) => baseElementScale = elementScale, onScaleUpdate: (details) { setState(() { elementScale = (baseElementScale * details.scale).clamp(40, 80); }); }, onScaleEnd: (details) { settings.val(write: true).timetableSettings.zoom = elementScale; }, child: SfCalendar( view: CalendarView.workWeek, dataSource: _buildTableEvents(value), maxDate: DateTime.now().add(const Duration(days: 7)), minDate: DateTime.now().subtract(const Duration (days: 14)), controller: controller, onViewChanged: (ViewChangedDetails details) { 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, value, tapped); }, firstDayOfWeek: DateTime.monday, specialRegions: _buildSpecialTimeRegions(holidays), timeSlotViewSettings: TimeSlotViewSettings( startHour: 07.5, endHour: 16.5, timeInterval: const Duration(minutes: 30), timeFormat: "HH:mm", dayFormat: "EE", timeIntervalHeight: elementScale, ), 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, ) { DateTime lastMonday = DateTime.now().subtract(Duration(days: DateTime.now().weekday - 1)); DateTime firstBreak = lastMonday.copyWith(hour: 10, minute: 15); DateTime secondBreak = lastMonday.copyWith(hour: 13, minute: 50); DateTime beforeSchool = lastMonday.copyWith(hour: 7, minute: 30); return [ ...holidays.result.map((e) { return TimeRegion( startTime: _parseWebuntisTimestamp(e.startDate, 755), endTime: _parseWebuntisTimestamp(e.startDate, 1630), text: 'holiday:${e.name}', color: Theme.of(context).disabledColor.withAlpha(50), iconData: Icons.holiday_village_outlined ); }), TimeRegion( startTime: firstBreak, endTime: firstBreak.add(const Duration(minutes: 20)), recurrenceRule: 'FREQ=DAILY;INTERVAL=1;COUNT=5', text: 'centerIcon', color: Theme.of(context).primaryColor.withAlpha(50), iconData: Icons.restaurant ), TimeRegion( startTime: secondBreak, endTime: secondBreak.add(const Duration(minutes: 15)), recurrenceRule: 'FREQ=DAILY;INTERVAL=1;COUNT=5', text: 'centerIcon', color: Theme.of(context).primaryColor.withAlpha(50), iconData: Icons.restaurant ), TimeRegion( startTime: beforeSchool, endTime: beforeSchool.add(const Duration(minutes: 25)), recurrenceRule: 'FREQ=DAILY;INTERVAL=1;COUNT=5', color: Theme.of(context).disabledColor.withAlpha(50), text: "centerIcon", ), ]; } TimetableEvents _buildTableEvents(TimetableProps data) { List appointments = data.getTimetableResponse.result.map((element) { GetRoomsResponse rooms = data.getRoomsResponse; GetSubjectsResponse subjects = data.getSubjectsResponse; try { DateTime startTime = _parseWebuntisTimestamp(element.date, element.startTime); DateTime endTime = _parseWebuntisTimestamp(element.date, element.endTime); return Appointment( id: element, startTime: startTime, endTime: endTime, subject: subjects.result.firstWhere((subject) => subject.id == element.su[0].id).name, 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) { DateTime endTime = _parseWebuntisTimestamp(element.date, element.endTime); return Appointment( id: 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(); return TimetableEvents(appointments); } DateTime _parseWebuntisTimestamp(int date, int time) { String 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 int 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); // 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(100); // Fallback return Theme.of(context).primaryColor; } bool _isCrossedOut(CalendarAppointmentDetails calendarEntry) { GetTimetableResponseObject webuntisElement = (calendarEntry.appointments.first.id as GetTimetableResponseObject); return webuntisElement.code == "cancelled"; } }