diff --git a/android/app/build.gradle b/android/app/build.gradle index 53332cf..77903e3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -57,6 +57,9 @@ android { signingConfig signingConfigs.debug } } + buildFeatures { + viewBinding true + } } flutter { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 3c24865..530ed37 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,45 +1,69 @@ + + + + + + + + + + + + + + android:icon="@mipmap/ic_launcher" + android:label="Marianum Fulda"> + + + + + + + + - + to determine the Window background behind the Flutter UI. + --> + android:name="io.flutter.embedding.android.NormalTheme" + android:resource="@style/NormalTheme" /> + - - + + - + - - - - - - - - - + \ No newline at end of file diff --git a/android/app/src/main/java/eu/mhsl/marianum/mobile/client/TimetableWidget.kt b/android/app/src/main/java/eu/mhsl/marianum/mobile/client/TimetableWidget.kt new file mode 100644 index 0000000..1c9e3ac --- /dev/null +++ b/android/app/src/main/java/eu/mhsl/marianum/mobile/client/TimetableWidget.kt @@ -0,0 +1,39 @@ +package eu.mhsl.marianum.mobile.client + +import android.app.PendingIntent +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.widget.RemoteViews + +import es.antonborri.home_widget.HomeWidgetPlugin +import android.util.Base64 + +/** + * Implementation of App Widget functionality. + */ +class TimetableWidget : AppWidgetProvider() { + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray, + ) { + for (appWidgetId in appWidgetIds) { + val widgetData = HomeWidgetPlugin.getData(context) + val views = RemoteViews(context.packageName, R.layout.timetable_widget).apply { + val imageBase64 = widgetData.getString("screen", null) ?: return@apply + val imageBytes = Base64.decode(imageBase64, Base64.DEFAULT); + val imageBitmap: Bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) + setImageViewBitmap(R.id.widget_image, imageBitmap) + } + + val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) + val pendingIntent = PendingIntent.getActivity(context, 0, launchIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + views.setOnClickPendingIntent(R.id.background, pendingIntent) + + appWidgetManager.updateAppWidget(appWidgetId, views) + } + } +} \ No newline at end of file diff --git a/android/app/src/main/res/drawable-nodpi/timetable_widget_default.png b/android/app/src/main/res/drawable-nodpi/timetable_widget_default.png new file mode 100644 index 0000000..4079ef9 Binary files /dev/null and b/android/app/src/main/res/drawable-nodpi/timetable_widget_default.png differ diff --git a/android/app/src/main/res/drawable-nodpi/timetable_widget_preview.png b/android/app/src/main/res/drawable-nodpi/timetable_widget_preview.png new file mode 100644 index 0000000..3f3ca0d Binary files /dev/null and b/android/app/src/main/res/drawable-nodpi/timetable_widget_preview.png differ diff --git a/android/app/src/main/res/drawable-v21/app_widget_background.xml b/android/app/src/main/res/drawable-v21/app_widget_background.xml new file mode 100644 index 0000000..785445c --- /dev/null +++ b/android/app/src/main/res/drawable-v21/app_widget_background.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable-v21/app_widget_inner_view_background.xml b/android/app/src/main/res/drawable-v21/app_widget_inner_view_background.xml new file mode 100644 index 0000000..007e287 --- /dev/null +++ b/android/app/src/main/res/drawable-v21/app_widget_inner_view_background.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/timetable_widget.xml b/android/app/src/main/res/layout/timetable_widget.xml new file mode 100644 index 0000000..56cb7fd --- /dev/null +++ b/android/app/src/main/res/layout/timetable_widget.xml @@ -0,0 +1,26 @@ + + + + \ No newline at end of file diff --git a/android/app/src/main/res/values-night-v31/themes.xml b/android/app/src/main/res/values-night-v31/themes.xml new file mode 100644 index 0000000..f253c9d --- /dev/null +++ b/android/app/src/main/res/values-night-v31/themes.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/values-v21/styles.xml b/android/app/src/main/res/values-v21/styles.xml new file mode 100644 index 0000000..0b35f7d --- /dev/null +++ b/android/app/src/main/res/values-v21/styles.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/values-v31/styles.xml b/android/app/src/main/res/values-v31/styles.xml index ae64507..c5f4f25 100644 --- a/android/app/src/main/res/values-v31/styles.xml +++ b/android/app/src/main/res/values-v31/styles.xml @@ -18,4 +18,18 @@ + + + + diff --git a/android/app/src/main/res/values-v31/themes.xml b/android/app/src/main/res/values-v31/themes.xml new file mode 100644 index 0000000..badd306 --- /dev/null +++ b/android/app/src/main/res/values-v31/themes.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/values/attrs.xml b/android/app/src/main/res/values/attrs.xml new file mode 100644 index 0000000..7781ac8 --- /dev/null +++ b/android/app/src/main/res/values/attrs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..b2bffa8 --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + #FFE1F5FE + #FF81D4FA + #FF039BE5 + #FF01579B + \ No newline at end of file diff --git a/android/app/src/main/res/values/dimens.xml b/android/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..4db8c59 --- /dev/null +++ b/android/app/src/main/res/values/dimens.xml @@ -0,0 +1,10 @@ + + + + + 0dp + + \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..3bf528f --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,6 @@ + + + Marianum Vertretungsplan + Hinzufügen + Übersicht zum Vertretungsplan + \ No newline at end of file diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index 0d1fa8f..4a56836 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -19,4 +19,14 @@ + + + + diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..dcd8899 --- /dev/null +++ b/android/app/src/main/res/values/themes.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/xml/timetable_widget_info.xml b/android/app/src/main/res/xml/timetable_widget_info.xml new file mode 100644 index 0000000..a05e013 --- /dev/null +++ b/android/app/src/main/res/xml/timetable_widget_info.xml @@ -0,0 +1,16 @@ + + \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index bc157bd..d236fb3 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -5,6 +5,16 @@ allprojects { } } +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10' + } +} + rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" diff --git a/lib/homescreen_widgets/timetable/timetableHomeWidget.dart b/lib/homescreen_widgets/timetable/timetableHomeWidget.dart new file mode 100644 index 0000000..c8c803f --- /dev/null +++ b/lib/homescreen_widgets/timetable/timetableHomeWidget.dart @@ -0,0 +1,76 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:home_widget/home_widget.dart'; +import 'package:provider/provider.dart'; +import 'package:screenshot/screenshot.dart'; +import 'package:syncfusion_flutter_calendar/calendar.dart'; + +import '../../model/timetable/timetableProps.dart'; +import '../../storage/base/settingsProvider.dart'; +import '../../theming/darkAppTheme.dart'; +import '../../theming/lightAppTheme.dart'; +import '../../view/pages/timetable/calendar.dart'; + +class TimetableHomeWidget { + + static void update(BuildContext context) { + var data = Provider.of(context, listen: false); + var settings = Provider.of(context, listen: false); + + if(data.primaryLoading()) { + log('Could not generate widget screen because no data was found!'); + return; + } + + log('Generating widget screen...'); + var screenshotController = ScreenshotController(); + var calendarController = CalendarController(); + calendarController.displayDate = DateTime.now().copyWith(hour: 07, minute: 00); + + screenshotController.captureFromWidget( + delay: Duration(milliseconds: 100), + SizedBox( + height: 700, + width: 300, + child: Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: MediaQueryData(), + child: MaterialApp( + localizationsDelegates: const [ + ...GlobalMaterialLocalizations.delegates, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: const [ + Locale('de'), + Locale('en'), + ], + locale: const Locale('de'), + darkTheme: DarkAppTheme.theme, + theme: LightAppTheme.theme, + themeMode: settings.val().appTheme, + home: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: Scaffold( + body: Calendar( + controller: calendarController, + timetableProps: data, + settings: settings, + isHomeWidget: true, + ), + ), + ), + ), + ), + ), + ), + ).then((value) { + HomeWidget.saveWidgetData('screen', base64.encode(value)); + HomeWidget.updateWidget(name: 'TimetableWidget'); + log('Widget screen successfully updated! (${value.length})'); + }); + } +} diff --git a/lib/main.dart b/lib/main.dart index 6f20ebb..d9d6ec1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -125,7 +125,6 @@ class _MainState extends State
{ checkerboardOffscreenLayers: devToolsSettings.checkerboardOffscreenLayers, checkerboardRasterCacheImages: devToolsSettings.checkerboardRasterCacheImages, - debugShowCheckedModeBanner: false, localizationsDelegates: const [ ...GlobalMaterialLocalizations.delegates, GlobalWidgetsLocalizations.delegate, diff --git a/lib/view/pages/timetable/calendar.dart b/lib/view/pages/timetable/calendar.dart new file mode 100644 index 0000000..0f99fdd --- /dev/null +++ b/lib/view/pages/timetable/calendar.dart @@ -0,0 +1,287 @@ +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; + } +} diff --git a/lib/view/pages/timetable/timetable.dart b/lib/view/pages/timetable/timetable.dart index 9c939be..8109c51 100644 --- a/lib/view/pages/timetable/timetable.dart +++ b/lib/view/pages/timetable/timetable.dart @@ -2,24 +2,16 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import '../../../extensions/dateTime.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 '../../../homescreen_widgets/timetable/timetableHomeWidget.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 'arbitraryAppointment.dart'; -import 'customTimetableColors.dart'; +import 'calendar.dart'; import 'customTimetableEventEditDialog.dart'; -import 'timeRegionComponent.dart'; -import 'timetableEvents.dart'; -import 'timetableNameMode.dart'; import 'viewCustomTimetableEvents.dart'; class Timetable extends StatefulWidget { @@ -34,12 +26,11 @@ enum CalendarActions { addEvent, viewEvents } class _TimetableState extends State { CalendarController controller = CalendarController(); late Timer updateTimings; - late final SettingsProvider settings; + late SettingsProvider settings; @override void initState() { settings = Provider.of(context, listen: false); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { Provider.of(context, listen: false).run(); }); @@ -56,6 +47,7 @@ class _TimetableState extends State { appBar: AppBar( title: const Text('Stunden & Vertretungsplan'), actions: [ + IconButton(onPressed: () => TimetableHomeWidget.update(context), icon: Icon(Icons.screen_share_outlined)), IconButton( icon: const Icon(Icons.home_outlined), onPressed: () { @@ -119,62 +111,19 @@ class _TimetableState extends State { if(value.primaryLoading()) return const LoadingSpinner(); - var holidays = value.getHolidaysResponse; - return RefreshIndicator( - child: SfCalendar( - timeZone: 'W. Europe Standard Time', - view: CalendarView.workWeek, - dataSource: _buildTableEvents(value), - - maxDate: DateTime.now().add(const Duration(days: 7)).nextWeekday(DateTime.saturday), - minDate: DateTime.now().subtract(const Duration (days: 14)).nextWeekday(DateTime.sunday), - - 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: const TimeSlotViewSettings( - startHour: 07.5, - endHour: 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, - ), - onRefresh: () async { - Provider.of(context, listen: false).run(renew: true); - return Future.delayed(const Duration(seconds: 3)); - } - ); + child: Calendar( + controller: controller, + timetableProps: value, + settings: settings, + ), + onRefresh: () async { + Provider.of(context, listen: false).run(renew: true); + return Future.delayed(const Duration(seconds: 3)); + } + ); }, - ), + ) ); @override @@ -182,210 +131,4 @@ class _TimetableState extends State { updateTimings.cancel(); super.dispose(); } - - 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(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, - }[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; - } } diff --git a/pubspec.yaml b/pubspec.yaml index 7ad1cea..a526b75 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.1.1+39 +version: 0.1.2+40 environment: sdk: '>3.0.0' @@ -101,6 +101,8 @@ dependencies: time_range_picker: ^2.3.0 url_launcher: ^6.3.1 uuid: ^4.5.1 + home_widget: ^0.7.0+1 + screenshot: ^3.0.0 dev_dependencies: flutter_test: