From 9a1247de5f228f7546402f60f559cfac5caa154a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 20 May 2023 16:30:06 +0200 Subject: [PATCH] Restored full timetable implementation using SFCalendar --- .idea/libraries/Flutter_Plugins.xml | 35 +++ lib/data/timetable/timetableProps.dart | 25 +- lib/main.dart | 15 +- .../timetable/appointmenetComponent.dart | 112 ++++++++ .../pages/timetable/appointmentDetails.dart | 90 ++++++ .../pages/timetable/timeRegionComponent.dart | 59 ++++ lib/screen/pages/timetable/timetable.dart | 211 ++++++++++++-- .../pages/timetable/timetableEvents.dart | 8 + lib/screen/pages/timetable/weekView.dart | 272 ------------------ 9 files changed, 515 insertions(+), 312 deletions(-) create mode 100644 .idea/libraries/Flutter_Plugins.xml create mode 100644 lib/screen/pages/timetable/appointmenetComponent.dart create mode 100644 lib/screen/pages/timetable/appointmentDetails.dart create mode 100644 lib/screen/pages/timetable/timeRegionComponent.dart create mode 100644 lib/screen/pages/timetable/timetableEvents.dart delete mode 100644 lib/screen/pages/timetable/weekView.dart diff --git a/.idea/libraries/Flutter_Plugins.xml b/.idea/libraries/Flutter_Plugins.xml new file mode 100644 index 0000000..5480177 --- /dev/null +++ b/.idea/libraries/Flutter_Plugins.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/data/timetable/timetableProps.dart b/lib/data/timetable/timetableProps.dart index 73ad618..3704c98 100644 --- a/lib/data/timetable/timetableProps.dart +++ b/lib/data/timetable/timetableProps.dart @@ -14,7 +14,7 @@ import '../../api/webuntis/queries/getTimetable/getTimetableResponse.dart'; import '../../api/webuntis/webuntisError.dart'; extension DateTimeExtension on DateTime { - DateTime next(int day) { + DateTime jumpToNextWeekDay(int day) { return add( Duration( days: (day - weekday) % DateTime.daysPerWeek, @@ -24,7 +24,7 @@ extension DateTimeExtension on DateTime { } class TimetableProps extends DataHolder { - var _queryWeek = DateTime.now(); + final _queryWeek = DateTime.now().add(const Duration(days: 2)); late DateTime startDate = getDate(_queryWeek.subtract(Duration(days: _queryWeek.weekday - 1))); late DateTime endDate = getDate(_queryWeek.add(Duration(days: DateTime.daysPerWeek - _queryWeek.weekday))); @@ -87,33 +87,18 @@ class TimetableProps extends DataHolder { ); } - void nearest() { - _queryWeek = _queryWeek = DateTime.now(); - if(_queryWeek.weekday == DateTime.saturday || _queryWeek.weekday == DateTime.sunday) _queryWeek = _queryWeek.add(const Duration(days: 2)); - updateWeek(); - } - - void switchWeek({previous = false}) { - if(previous) { - _queryWeek = _queryWeek.subtract(const Duration(days: 7)); - } else { - _queryWeek = _queryWeek.add(const Duration(days: 7)); - } - updateWeek(); - } - DateTime getDate(DateTime d) => DateTime(d.year, d.month, d.day); bool isWeekend(DateTime queryDate) { return queryDate.weekday == DateTime.saturday || queryDate.weekday == DateTime.sunday; } - void updateWeek() { + void updateWeek(DateTime start, DateTime end) { properties().forEach((element) => element = null); error = null; notifyListeners(); - startDate = getDate(_queryWeek.subtract(Duration(days: _queryWeek.weekday - 1))); - endDate = getDate(_queryWeek.add(Duration(days: DateTime.daysPerWeek - _queryWeek.weekday))); + startDate = start.subtract(const Duration(days: 7)); + endDate = end.add(const Duration(days: 7)); try { run(); } on WebuntisError catch(e) { diff --git a/lib/main.dart b/lib/main.dart index d5f9382..82df1b4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,6 +8,7 @@ import 'package:marianum_mobile/screen/login/login.dart'; import 'package:marianum_mobile/widget/errorView.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'app.dart'; import 'data/chatList/chatListProps.dart'; @@ -71,6 +72,16 @@ class _MainState extends State
{ return MaterialApp( debugShowCheckedModeBanner: false, + localizationsDelegates: const [ + ...GlobalMaterialLocalizations.delegates, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: const [ + Locale('de'), + Locale('en'), + ], + locale: const Locale('de'), + title: 'Marianum Fulda', theme: ThemeData( @@ -79,7 +90,7 @@ class _MainState extends State
{ colorScheme: const ColorScheme( brightness: Brightness.light, surface: Colors.white, - onSurface: Colors.white, + onSurface: Colors.black, onSecondary: Colors.white, onPrimary: Colors.white, onError: marianumRed, @@ -91,7 +102,7 @@ class _MainState extends State
{ ), hintColor: marianumRed, inputDecorationTheme: const InputDecorationTheme( - border: UnderlineInputBorder(borderSide: BorderSide(color: marianumRed)), + border: UnderlineInputBorder(borderSide: BorderSide(color: marianumRed)), ), appBarTheme: const AppBarTheme( backgroundColor: marianumRed, diff --git a/lib/screen/pages/timetable/appointmenetComponent.dart b/lib/screen/pages/timetable/appointmenetComponent.dart new file mode 100644 index 0000000..d553934 --- /dev/null +++ b/lib/screen/pages/timetable/appointmenetComponent.dart @@ -0,0 +1,112 @@ + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:syncfusion_flutter_calendar/calendar.dart'; + +class AppointmentComponent extends StatefulWidget { + final CalendarAppointmentDetails details; + const AppointmentComponent({Key? key, required this.details}) : super(key: key); + + @override + State createState() => _AppointmentComponentState(); +} + +class _AppointmentComponentState extends State { + + @override + Widget build(BuildContext context) { + final Appointment meeting = widget.details.appointments.first; + final appointmentHeight = widget.details.bounds.height; + double headerHeight = 50; + const double footerHeight = 5; + final double infoHeight = appointmentHeight - (headerHeight + footerHeight); + if(infoHeight < 0) headerHeight += infoHeight; + + return Column( + children: [ + Container( + padding: const EdgeInsets.all(3), + height: headerHeight, + alignment: Alignment.topLeft, + decoration: BoxDecoration( + shape: BoxShape.rectangle, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(5), + topRight: Radius.circular(5)), + color: meeting.color, + ), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FittedBox( + fit: BoxFit.fitWidth, + child: Text( + meeting.subject, + style: const TextStyle( + color: Colors.white, + fontSize: 15, + fontWeight: FontWeight.w500, + ), + maxLines: 1, + softWrap: false, + ), + ), + FittedBox( + fit: BoxFit.fitWidth, + child: Text( + meeting.location ?? "?", + maxLines: 3, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: const TextStyle( + color: Colors.white, + fontSize: 10, + ), + ), + ) + ], + )), + ), + Visibility( + visible: meeting.notes != null && infoHeight > 10, + replacement: Container( + color: meeting.color, + height: infoHeight, + ), + child: Container( + height: infoHeight, + padding: const EdgeInsets.fromLTRB(3, 5, 3, 2), + color: meeting.color.withOpacity(0.8), + alignment: Alignment.topLeft, + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + meeting.notes ?? "", + style: const TextStyle( + color: Colors.white, + fontSize: 10, + ), + ) + ], + )), + ), + ), + Container( + height: footerHeight, + decoration: BoxDecoration( + shape: BoxShape.rectangle, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(5), + bottomRight: Radius.circular(5)), + color: meeting.color, + ), + ), + ], + ); + } +} diff --git a/lib/screen/pages/timetable/appointmentDetails.dart b/lib/screen/pages/timetable/appointmentDetails.dart new file mode 100644 index 0000000..06261e6 --- /dev/null +++ b/lib/screen/pages/timetable/appointmentDetails.dart @@ -0,0 +1,90 @@ + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:jiffy/jiffy.dart'; +import 'package:marianum_mobile/api/webuntis/queries/getTimetable/getTimetableResponse.dart'; +import 'package:marianum_mobile/data/timetable/timetableProps.dart'; +import 'package:persistent_bottom_nav_bar/persistent_tab_view.dart'; +import 'package:syncfusion_flutter_calendar/calendar.dart'; + +import '../../../api/webuntis/queries/getRooms/getRoomsResponse.dart'; +import '../../../api/webuntis/queries/getSubjects/getSubjectsResponse.dart'; +import '../../settings/debug/jsonViewer.dart'; +import '../more/roomplan/roomplan.dart'; + +class AppointmentDetails { + static String _getEventPrefix(String? code) { + if(code == "cancelled") return "Entfällt: "; + if(code == "irregular") return "Änderung: "; + return code ?? ""; + } + static void show(BuildContext context, TimetableProps webuntisData, Appointment appointment) { + GetTimetableResponseObject timetableData = appointment.id as GetTimetableResponseObject; + + //GetTimetableResponseObject timetableData = webuntisData.getTimetableResponse.result.firstWhere((element) => element.id == timetableObject.id); + GetSubjectsResponseObject subject = webuntisData.getSubjectsResponse.result.firstWhere((subject) => subject.id == timetableData.su[0]['id']); + GetRoomsResponseObject room = webuntisData.getRoomsResponse.result.firstWhere((room) => room.id == timetableData.ro[0]['id']); + + showModalBottomSheet(context: context, builder: (context) => Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 30), + child: Center( + child: Column( + children: [ + Icon(Icons.info, color: appointment.color), + const SizedBox(height: 10), + Text("${_getEventPrefix(timetableData.code)}${subject.alternateName} - (${subject.longName})", style: const TextStyle(fontSize: 30)), + Text("${Jiffy(appointment.startTime).format("HH:mm")} - ${Jiffy(appointment.endTime).format("HH:mm")}", style: const TextStyle(fontSize: 15)), + ], + ), + ), + ), + + Expanded( + child: ListView( + children: [ + ListTile( + leading: const Icon(Icons.notifications_active), + title: Text("Status: ${timetableData.code != null ? "Geändert" : "Regulär"}"), + ), + ListTile( + leading: const Icon(Icons.room), + title: Text("Raum: ${room.name}"), + trailing: IconButton( + icon: const Icon(Icons.house_outlined), + onPressed: () { + PersistentNavBarNavigator.pushNewScreen(context, withNavBar: false, screen: const Roomplan()); + }, + ), + ), + ListTile( + leading: const Icon(Icons.person), + title: Text("Lehrkraft: (${timetableData.te[0]['name']}) ${timetableData.te[0]['longname']}"), + trailing: IconButton( + icon: const Icon(Icons.textsms_outlined), + onPressed: () { + showDialog(context: context, builder: (context) => const AlertDialog(content: Text("Not implemented yet"))); + }, + ), + ), + ListTile( + leading: const Icon(Icons.abc), + title: Text("Typ: ${timetableData.activityType}"), + ), + ListTile( + leading: const Icon(Icons.people), + title: Text("Klasse(n): ${timetableData.kl.map((e) => e['name']).join(", ")}"), + ), + ListTile( + leading: const Icon(Icons.bug_report_outlined), + title: const Text("Webuntis Rohdaten zeigen"), + onTap: () => JsonViewer.asDialog(context, timetableData.toJson()), + ) + ], + ), + ) + ], + )); + } +} \ No newline at end of file diff --git a/lib/screen/pages/timetable/timeRegionComponent.dart b/lib/screen/pages/timetable/timeRegionComponent.dart new file mode 100644 index 0000000..cbbd007 --- /dev/null +++ b/lib/screen/pages/timetable/timeRegionComponent.dart @@ -0,0 +1,59 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:syncfusion_flutter_calendar/calendar.dart'; + +class TimeRegionComponent extends StatefulWidget { + final TimeRegionDetails details; + const TimeRegionComponent({Key? key, required this.details}) : super(key: key); + + @override + State createState() => _TimeRegionComponentState(); +} + +class _TimeRegionComponentState extends State { + @override + Widget build(BuildContext context) { + String text = widget.details.region.text!; + Color? color = widget.details.region.color; + + if (text == 'centerIcon') { + return Container( + color: color, + alignment: Alignment.center, + child: Icon( + widget.details.region.iconData, + size: 17, + color: Theme.of(context).primaryColor, + ), + ); + } else if(text.startsWith('holiday')) { + return Container( + color: color, + alignment: Alignment.center, + child: Column( + children: [ + const SizedBox(height: 5), + const Icon(Icons.cake), + const Text("FREI"), + const SizedBox(height: 5), + RotatedBox( + quarterTurns: 1, + child: Text( + text.split(":").last, + maxLines: 1, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + decorationStyle: TextDecorationStyle.dashed, + letterSpacing: 2, + ), + ), + ), + ], + ), + ); + } + + return const Placeholder(); + } +} diff --git a/lib/screen/pages/timetable/timetable.dart b/lib/screen/pages/timetable/timetable.dart index d8b5526..ba7faf9 100644 --- a/lib/screen/pages/timetable/timetable.dart +++ b/lib/screen/pages/timetable/timetable.dart @@ -1,10 +1,20 @@ +import 'dart:developer'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:marianum_mobile/data/timetable/timetableProps.dart'; -import 'package:marianum_mobile/screen/pages/timetable/weekView.dart'; -import 'package:marianum_mobile/widget/errorView.dart'; +import 'package:marianum_mobile/screen/pages/timetable/appointmenetComponent.dart'; +import 'package:marianum_mobile/screen/pages/timetable/timeRegionComponent.dart'; +import 'package:marianum_mobile/screen/pages/timetable/timetableEvents.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 '../../../data/timetable/timetableProps.dart'; +import 'appointmentDetails.dart'; class Timetable extends StatefulWidget { const Timetable({Key? key}) : super(key: key); @@ -14,44 +24,209 @@ class Timetable extends StatefulWidget { } class _TimetableState extends State { - bool draggable = true; + CalendarController controller = CalendarController(); + + double elementScale = 40; + double baseElementScale = 40; @override void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - Provider.of(context, listen: false).nearest(); + Provider.of(context, listen: false).run(); }); + + super.initState(); } - + @override Widget build(BuildContext context) { - TimetableProps timetable = Provider.of(context, listen: false); return Scaffold( appBar: AppBar( - title: const Text("Vertretungsplan"), + title: const Text("Stunden & Vertretungsplan"), actions: [ - IconButton(onPressed: () => timetable.switchWeek(previous: true), icon: const Icon(Icons.chevron_left)), - IconButton(onPressed: () => timetable.nearest(), icon: const Icon(Icons.home)), - IconButton(onPressed: () => timetable.switchWeek(), icon: const Icon(Icons.chevron_right)) + IconButton( + icon: const Icon(Icons.today), + onPressed: () { + controller.displayDate = DateTime.now().jumpToNextWeekDay(DateTime.monday); + //controller.displayDate = DateTime.now().add(Duration(days: 2)); + //controller.selectedDate = DateTime.now(); + } + ), ], ), body: Consumer( builder: (context, value, child) { + if(value.primaryLoading()) return const Placeholder(); + + GetHolidaysResponse holidays = value.getHolidaysResponse; if(value.hasError) { - return ErrorView(icon: Icons.error, text: value.error?.message ?? "Unbekannter Fehler"); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + showDialog(context: context, builder: (context) { + return AlertDialog( + title: Text("Webuntis error"), + content: Text(value.error.toString()), + ); + }); + }); } - if(value.primaryLoading()) { - return const Center(child: CircularProgressIndicator()); - } + return GestureDetector( + onScaleStart: (details) => baseElementScale = elementScale, + onScaleUpdate: (details) { + setState(() { + elementScale = (baseElementScale * details.scale).clamp(40, 80); + }); + }, + onScaleEnd: (details) { + // TODO save scale for later + }, - return WeekView(value); + child: SfCalendar( + view: CalendarView.workWeek, + dataSource: _buildTableEvents(value), + + controller: controller, + + onViewChanged: (ViewChangedDetails details) { + log(details.visibleDates.toString()); + 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); + log(tapped.id.toString()); + }, + + 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), + + 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']).alternateName, + location: "" + "${rooms.result.firstWhere((room) => room.id == element.ro[0]['id']).name}" + "\n" + "${element.te.first['longname']}", + notes: element.activityType, + color: _getEventColor(element.code, startTime, endTime), + ); + } on Error catch(e) { + log(e.toString()); + return Appointment( + startTime: _parseWebuntisTimestamp(element.date, element.startTime), + endTime: _parseWebuntisTimestamp(element.date, element.endTime), + subject: "ERROR", + notes: element.info, + location: 'LOCATION', + color: 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(String? code, DateTime startTime, DateTime endTime) { + int opacity = endTime.isBefore(DateTime.now()) ? 100 : 255; + + if(code == "cancelled") return const Color(0xff000000).withAlpha(opacity); + if(code == "irregular") return const Color(0xff8F19B3).withAlpha(opacity); + + if(endTime.isBefore(DateTime.now())) return Theme.of(context).primaryColor.withAlpha(opacity); + if(endTime.isAfter(DateTime.now()) && startTime.isBefore(DateTime.now())) return Theme.of(context).primaryColor.withRed(100); + + return Theme.of(context).primaryColor; + } } diff --git a/lib/screen/pages/timetable/timetableEvents.dart b/lib/screen/pages/timetable/timetableEvents.dart new file mode 100644 index 0000000..03510fa --- /dev/null +++ b/lib/screen/pages/timetable/timetableEvents.dart @@ -0,0 +1,8 @@ + +import 'package:syncfusion_flutter_calendar/calendar.dart'; + +class TimetableEvents extends CalendarDataSource { + TimetableEvents(List source) { + appointments = source; + } +} \ No newline at end of file diff --git a/lib/screen/pages/timetable/weekView.dart b/lib/screen/pages/timetable/weekView.dart deleted file mode 100644 index 3816121..0000000 --- a/lib/screen/pages/timetable/weekView.dart +++ /dev/null @@ -1,272 +0,0 @@ - -import 'package:flutter/cupertino.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:jiffy/jiffy.dart'; -import 'package:marianum_mobile/api/webuntis/queries/getHolidays/getHolidaysResponse.dart'; -import 'package:marianum_mobile/screen/pages/more/roomplan/roomplan.dart'; -import 'package:marianum_mobile/screen/settings/debug/jsonViewer.dart'; -import 'package:persistent_bottom_nav_bar/persistent_tab_view.dart'; -import 'package:timetable_view/timetable_view.dart'; - -import '../../../api/webuntis/queries/getHolidays/getHolidays.dart'; -import '../../../api/webuntis/queries/getRooms/getRoomsResponse.dart'; -import '../../../api/webuntis/queries/getSubjects/getSubjectsResponse.dart'; -import '../../../api/webuntis/queries/getTimetable/getTimetableResponse.dart'; -import '../../../data/timetable/timetableProps.dart'; - -extension DateHelpers on DateTime { - bool isToday() { - final now = DateTime.now(); - return now.day == day && - now.month == month && - now.year == year; - } -} - -class WeekView extends StatefulWidget { - final TimetableProps value; - const WeekView(this.value, {Key? key}) : super(key: key); - - @override - State createState() => _WeekViewState(); -} - -class _WeekViewState extends State { - @override - Widget build(BuildContext context) { - return TimetableView( - laneEventsList: _buildLaneEvents(widget.value), - onEventTap: (TableEvent event) { - - try { - GetTimetableResponseObject timetableData = widget.value.getTimetableResponse.result.firstWhere((element) => element.id == event.eventId); - GetSubjectsResponseObject subject = widget.value.getSubjectsResponse.result.firstWhere((subject) => subject.id == timetableData.su[0]['id']); - GetRoomsResponseObject room = widget.value.getRoomsResponse.result.firstWhere((room) => room.id == timetableData.ro[0]['id']); - - showModalBottomSheet(context: context, builder: (context) => Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 30), - child: Center( - child: Column( - children: [ - Icon(Icons.info, color: event.backgroundColor), - const SizedBox(height: 10), - Text("${getEventPrefix(timetableData.code)}${subject.alternateName} - (${subject.longName})", style: const TextStyle(fontSize: 30)), - Text("${Jiffy(event.startTime).format("HH:mm")} - ${Jiffy(event.endTime).format("HH:mm")}", style: const TextStyle(fontSize: 15)), - ], - ), - ), - ), - - Expanded( - child: ListView( - children: [ - ListTile( - leading: const Icon(Icons.notifications_active), - title: Text("Status: ${timetableData.code != null ? "Geändert" : "Regulär"}"), - ), - ListTile( - leading: const Icon(Icons.room), - title: Text("Raum: ${room.name}"), - trailing: IconButton( - icon: const Icon(Icons.house_outlined), - onPressed: () { - PersistentNavBarNavigator.pushNewScreen(context, withNavBar: false, screen: const Roomplan()); - }, - ), - ), - ListTile( - leading: const Icon(Icons.person), - title: Text("Lehrkraft: (${timetableData.te[0]['name']}) ${timetableData.te[0]['longname']}"), - trailing: IconButton( - icon: const Icon(Icons.textsms_outlined), - onPressed: () { - showDialog(context: context, builder: (context) => const AlertDialog(content: Text("Not implemented yet"))); - }, - ), - ), - ListTile( - leading: const Icon(Icons.abc), - title: Text("Typ: ${timetableData.activityType}"), - ), - ListTile( - leading: const Icon(Icons.people), - title: Text("Klasse(n): ${timetableData.kl.map((e) => e['name']).join(", ")}"), - ), - ListTile( - leading: const Icon(Icons.bug_report_outlined), - title: const Text("Webuntis Rohdaten zeigen"), - onTap: () => JsonViewer.asDialog(context, timetableData.toJson()), - ) - ], - ), - ) - ], - )); - - - } on StateError { - return; - } - }, - timetableStyle: CustomTableStyle(context), - onEmptySlotTap: (int laneIndex, TableEventTime start, TableEventTime end) => { - - }, - ); - } - - List _buildLaneEvents(TimetableProps data) { - List laneEvents = List.empty(growable: true); - - if(data.primaryLoading()) throw UnimplementedError(); - - GetTimetableResponse timetable = data.getTimetableResponse; - GetRoomsResponse rooms = data.getRoomsResponse; - GetSubjectsResponse subjects = data.getSubjectsResponse; - GetHolidaysResponse holidays = data.getHolidaysResponse; - - List dayList = timetable.result.map((e) => e.date).toSet().toList(); - dayList.sort((a, b) => a-b); - - for(int i = 0; i <= data.endDate.difference(data.startDate).inDays; i++) { - DateTime currentDay = data.startDate.copyWith().add(Duration(days: i)); - - // Check Holiday Information - GetHolidaysResponseObject? holidayInfo = GetHolidays.find(holidays, time: currentDay); - if(holidayInfo != null) { - laneEvents.add( - LaneEvents( - lane: getLane(currentDay), - events: List.of([ - TableEvent( - title: holidayInfo.name, - eventId: holidayInfo.id, - laneIndex: data.startDate.millisecondsSinceEpoch, - startTime: parseTime(0800), - endTime: parseTime(1500), - padding: const EdgeInsets.all(5), - backgroundColor: const Color(0xff3D62B3), - location: "\n${holidayInfo.longName}", - ) - ]), - ) - ); - } - } - - for (var day in dayList) { - DateTime currentDay = DateTime.parse("$day"); - Lane currentLane = getLane(currentDay); - //Every Day - - List events = List.generate( - timetable.result.where((element) => element.date == day).length, (index) { - GetTimetableResponseObject tableEvent = timetable.result.where((element) => element.date == day).elementAt(index); - try { - GetSubjectsResponseObject subject = subjects.result.firstWhere((subject) => subject.id == tableEvent.su[0]['id']); - - return TableEvent( - title: "${getEventPrefix(tableEvent.code)}${subject.alternateName} (${subject.longName})", - eventId: tableEvent.id, - laneIndex: day, - startTime: parseTime(tableEvent.startTime), - endTime: parseTime(tableEvent.endTime), - padding: const EdgeInsets.all(5), - backgroundColor: getEventColor( - tableEvent.code ?? "", - currentDay.add(Duration(hours: parseTime(tableEvent.startTime).hour, minutes: parseTime(tableEvent.startTime).minute)), - currentDay.add(Duration(hours: parseTime(tableEvent.endTime).hour, minutes: parseTime(tableEvent.endTime).minute)), - ), - location: "\n${rooms.result.firstWhereOrNull((room) => room.id == tableEvent.ro[0]['id'])?.name ?? "?"} - ${tableEvent.te[0]['longname']} (${tableEvent.te[0]['name']})", - ); - } on Error { - return TableEvent(title: "Unbekannt", eventId: index, laneIndex: day, startTime: parseTime(tableEvent.startTime), endTime: parseTime(tableEvent.endTime)); - } - } - ); - - //Timepointer - if(currentDay.isToday()) { - events.add(TableEvent( - title: "", - eventId: 0, - laneIndex: day, - startTime: formatTime(DateTime.now()), - endTime: formatTime(DateTime.now().add(const Duration(minutes: 3))), - backgroundColor: Theme.of(context).disabledColor, - )); - } - - laneEvents.add( - LaneEvents( - lane: currentLane, - events: events - ) - ); - } - - return laneEvents; - } - - Lane getLane(DateTime currentDay) { - return Lane( - backgroundColor: currentDay.isToday() ? Theme.of(context).dividerColor : Colors.white, - laneIndex: currentDay.millisecondsSinceEpoch, - name: "${Jiffy(currentDay.toString()).format("dd MMM")}\n${Jiffy(currentDay.toString()).format("EE")}", - textStyle: TextStyle( - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.bold, - fontSize: 14 - ) - ); - } - - TableEventTime parseTime(int input) { - String time = input.toString().length < 4 ? "0$input" : input.toString(); - return TableEventTime(hour: int.parse(time.substring(0, 2)), minute: int.parse(time.substring(2, 4))); - } - - TableEventTime formatTime(DateTime input) { - return TableEventTime(hour: input.hour, minute: input.minute); - } - - String getEventPrefix(String? code) { - if(code == "cancelled") return "Entfällt: "; - if(code == "irregular") return "Änderung: "; - return code ?? ""; - } - - Color getEventColor(String? code, DateTime startTime, DateTime endTime) { - if(code == "cancelled") return const Color(0xff8F19B3); - if(code == "irregular") return const Color(0xff992B99); - if(endTime.isBefore(DateTime.now())) return Colors.grey; - if(startTime.isAfter(DateTime.now())) return Theme.of(context).primaryColor; - return const Color(0xff99563A); - } -} - -class CustomTableStyle extends TimetableStyle { - dynamic context; - CustomTableStyle(this.context); - - @override - int get startHour => 07; - @override - int get endHour => 17; - @override - Color get cornerColor => Theme.of(context).primaryColor; - @override - Color get timeItemTextColor => Theme.of(context).primaryColor; - @override - double get timeItemHeight => MediaQuery.of(context).size.width > 1000 ? 60 : 90; - @override - double get timeItemWidth => 40; - @override - double get laneHeight => 40; - @override - double get laneWidth => (MediaQuery.of(context).size.width - timeItemWidth) / 5; - -} \ No newline at end of file