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: