added timetable widget for android devices
This commit is contained in:
parent
769fbc1b6a
commit
b0bbad7f97
@ -57,6 +57,9 @@ android {
|
|||||||
signingConfig signingConfigs.debug
|
signingConfig signingConfigs.debug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
|
@ -1,45 +1,69 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Required to query activities that can process text, see:
|
||||||
|
https://developer.android.com/training/package-visibility?hl=en and
|
||||||
|
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||||
|
|
||||||
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin.
|
||||||
|
-->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.PROCESS_TEXT" />
|
||||||
|
|
||||||
|
<data android:mimeType="text/plain" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="Marianum Fulda"
|
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="Marianum Fulda">
|
||||||
|
<receiver
|
||||||
|
android:name=".TimetableWidget"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/timetable_widget_info" />
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
|
||||||
android:hardwareAccelerated="true"
|
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
|
||||||
|
<!--
|
||||||
|
Specifies an Android theme to apply to this Activity as soon as
|
||||||
the Android process has started. This theme is visible to the user
|
the Android process has started. This theme is visible to the user
|
||||||
while the Flutter UI initializes. After that, this theme continues
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
to determine the Window background behind the Flutter UI. -->
|
to determine the Window background behind the Flutter UI.
|
||||||
|
-->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
android:resource="@style/NormalTheme"
|
android:resource="@style/NormalTheme" />
|
||||||
/>
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<!-- Don't delete the meta-data below.
|
<!--
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
Don't delete the meta-data below.
|
||||||
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java
|
||||||
|
-->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
</application>
|
</application>
|
||||||
<!-- Required to query activities that can process text, see:
|
|
||||||
https://developer.android.com/training/package-visibility?hl=en and
|
|
||||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
|
||||||
|
|
||||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
|
||||||
<queries>
|
|
||||||
<intent>
|
|
||||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
|
||||||
<data android:mimeType="text/plain"/>
|
|
||||||
</intent>
|
|
||||||
</queries>
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
|
||||||
</manifest>
|
</manifest>
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 98 KiB |
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
Background for widgets to make the rounded corners based on the
|
||||||
|
appWidgetRadius attribute value
|
||||||
|
-->
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
|
||||||
|
<corners android:radius="?attr/appWidgetRadius" />
|
||||||
|
<solid android:color="?android:attr/colorBackground" />
|
||||||
|
</shape>
|
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
Background for views inside widgets to make the rounded corners based on the
|
||||||
|
appWidgetInnerRadius attribute value
|
||||||
|
-->
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
|
||||||
|
<corners android:radius="?attr/appWidgetInnerRadius" />
|
||||||
|
<solid android:color="?android:attr/colorAccent" />
|
||||||
|
</shape>
|
26
android/app/src/main/res/layout/timetable_widget.xml
Normal file
26
android/app/src/main/res/layout/timetable_widget.xml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/background"
|
||||||
|
style="@style/Widget.Android.AppWidget.Container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:padding="0dp"
|
||||||
|
android:theme="@style/Theme.Android.AppWidgetContainer">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/widget_image"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_alignParentLeft="true"
|
||||||
|
android:layout_marginLeft="0dp"
|
||||||
|
android:layout_marginTop="0dp"
|
||||||
|
android:layout_marginBottom="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:adjustViewBounds="false"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:src="@drawable/timetable_widget_default"
|
||||||
|
android:visibility="visible"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
</RelativeLayout>
|
10
android/app/src/main/res/values-night-v31/themes.xml
Normal file
10
android/app/src/main/res/values-night-v31/themes.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!--
|
||||||
|
Having themes.xml for night-v31 because of the priority order of the resource qualifiers.
|
||||||
|
-->
|
||||||
|
<style name="Theme.Android.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault.DayNight">
|
||||||
|
<item name="appWidgetRadius">@android:dimen/system_app_widget_background_radius</item>
|
||||||
|
<item name="appWidgetInnerRadius">@android:dimen/system_app_widget_inner_radius</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
14
android/app/src/main/res/values-v21/styles.xml
Normal file
14
android/app/src/main/res/values-v21/styles.xml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<resources>
|
||||||
|
|
||||||
|
<style name="Widget.Android.AppWidget.Container" parent="android:Widget">
|
||||||
|
<item name="android:id">@android:id/background</item>
|
||||||
|
<item name="android:padding">?attr/appWidgetPadding</item>
|
||||||
|
<item name="android:background">@drawable/app_widget_background</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Widget.Android.AppWidget.InnerView" parent="android:Widget">
|
||||||
|
<item name="android:padding">?attr/appWidgetPadding</item>
|
||||||
|
<item name="android:background">@drawable/app_widget_inner_view_background</item>
|
||||||
|
<item name="android:textColor">?android:attr/textColorPrimary</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
@ -18,4 +18,18 @@
|
|||||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
<item name="android:windowBackground">?android:colorBackground</item>
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="Widget.Android.AppWidget.Container" parent="android:Widget">
|
||||||
|
<item name="android:id">@android:id/background</item>
|
||||||
|
<item name="android:padding">?attr/appWidgetPadding</item>
|
||||||
|
<item name="android:background">@drawable/app_widget_background</item>
|
||||||
|
<item name="android:clipToOutline">true</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Widget.Android.AppWidget.InnerView" parent="android:Widget">
|
||||||
|
<item name="android:padding">?attr/appWidgetPadding</item>
|
||||||
|
<item name="android:background">@drawable/app_widget_inner_view_background</item>
|
||||||
|
<item name="android:textColor">?android:attr/textColorPrimary</item>
|
||||||
|
<item name="android:clipToOutline">true</item>
|
||||||
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
11
android/app/src/main/res/values-v31/themes.xml
Normal file
11
android/app/src/main/res/values-v31/themes.xml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!--
|
||||||
|
Having themes.xml for v31 variant because @android:dimen/system_app_widget_background_radius
|
||||||
|
and @android:dimen/system_app_widget_internal_padding requires API level 31
|
||||||
|
-->
|
||||||
|
<style name="Theme.Android.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault.DayNight">
|
||||||
|
<item name="appWidgetRadius">@android:dimen/system_app_widget_background_radius</item>
|
||||||
|
<item name="appWidgetInnerRadius">@android:dimen/system_app_widget_inner_radius</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
7
android/app/src/main/res/values/attrs.xml
Normal file
7
android/app/src/main/res/values/attrs.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<resources>
|
||||||
|
<declare-styleable name="AppWidgetAttrs">
|
||||||
|
<attr name="appWidgetPadding" format="dimension" />
|
||||||
|
<attr name="appWidgetInnerRadius" format="dimension" />
|
||||||
|
<attr name="appWidgetRadius" format="dimension" />
|
||||||
|
</declare-styleable>
|
||||||
|
</resources>
|
6
android/app/src/main/res/values/colors.xml
Normal file
6
android/app/src/main/res/values/colors.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<resources>
|
||||||
|
<color name="light_blue_50">#FFE1F5FE</color>
|
||||||
|
<color name="light_blue_200">#FF81D4FA</color>
|
||||||
|
<color name="light_blue_600">#FF039BE5</color>
|
||||||
|
<color name="light_blue_900">#FF01579B</color>
|
||||||
|
</resources>
|
10
android/app/src/main/res/values/dimens.xml
Normal file
10
android/app/src/main/res/values/dimens.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Refer to App Widget Documentation for margin information
|
||||||
|
http://developer.android.com/guide/topics/appwidgets/index.html#CreatingLayout
|
||||||
|
-->
|
||||||
|
<dimen name="widget_margin">0dp</dimen>
|
||||||
|
|
||||||
|
</resources>
|
6
android/app/src/main/res/values/strings.xml
Normal file
6
android/app/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="appwidget_text">Marianum Vertretungsplan</string>
|
||||||
|
<string name="add_widget">Hinzufügen</string>
|
||||||
|
<string name="app_widget_description">Übersicht zum Vertretungsplan</string>
|
||||||
|
</resources>
|
@ -19,4 +19,14 @@
|
|||||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
<item name="android:windowBackground">?android:colorBackground</item>
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="Widget.Android.AppWidget.Container" parent="android:Widget">
|
||||||
|
<item name="android:id">@android:id/background</item>
|
||||||
|
<item name="android:background">?android:attr/colorBackground</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Widget.Android.AppWidget.InnerView" parent="android:Widget">
|
||||||
|
<item name="android:background">?android:attr/colorBackground</item>
|
||||||
|
<item name="android:textColor">?android:attr/textColorPrimary</item>
|
||||||
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
17
android/app/src/main/res/values/themes.xml
Normal file
17
android/app/src/main/res/values/themes.xml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<resources>
|
||||||
|
|
||||||
|
<style name="Theme.Android.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault">
|
||||||
|
<!-- Radius of the outer bound of widgets to make the rounded corners -->
|
||||||
|
<item name="appWidgetRadius">16dp</item>
|
||||||
|
<!--
|
||||||
|
Radius of the inner view's bound of widgets to make the rounded corners.
|
||||||
|
It needs to be 8dp or less than the value of appWidgetRadius
|
||||||
|
-->
|
||||||
|
<item name="appWidgetInnerRadius">8dp</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Theme.Android.AppWidgetContainer" parent="Theme.Android.AppWidgetContainerParent">
|
||||||
|
<!-- Apply padding to avoid the content of the widget colliding with the rounded corners -->
|
||||||
|
<item name="appWidgetPadding">16dp</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
16
android/app/src/main/res/xml/timetable_widget_info.xml
Normal file
16
android/app/src/main/res/xml/timetable_widget_info.xml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:description="@string/app_widget_description"
|
||||||
|
android:initialKeyguardLayout="@layout/timetable_widget"
|
||||||
|
android:initialLayout="@layout/timetable_widget"
|
||||||
|
android:minWidth="220dp"
|
||||||
|
android:minHeight="294dp"
|
||||||
|
android:minResizeWidth="110dp"
|
||||||
|
android:minResizeHeight="147dp"
|
||||||
|
android:previewImage="@drawable/timetable_widget_preview"
|
||||||
|
android:previewLayout="@layout/timetable_widget"
|
||||||
|
android:resizeMode="horizontal|vertical"
|
||||||
|
android:targetCellWidth="3"
|
||||||
|
android:targetCellHeight="4"
|
||||||
|
android:updatePeriodMillis="86400000"
|
||||||
|
android:widgetCategory="home_screen" />
|
@ -5,6 +5,16 @@ allprojects {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rootProject.buildDir = '../build'
|
rootProject.buildDir = '../build'
|
||||||
subprojects {
|
subprojects {
|
||||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||||
|
76
lib/homescreen_widgets/timetable/timetableHomeWidget.dart
Normal file
76
lib/homescreen_widgets/timetable/timetableHomeWidget.dart
Normal file
@ -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<TimetableProps>(context, listen: false);
|
||||||
|
var settings = Provider.of<SettingsProvider>(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<String>('screen', base64.encode(value));
|
||||||
|
HomeWidget.updateWidget(name: 'TimetableWidget');
|
||||||
|
log('Widget screen successfully updated! (${value.length})');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -125,7 +125,6 @@ class _MainState extends State<Main> {
|
|||||||
checkerboardOffscreenLayers: devToolsSettings.checkerboardOffscreenLayers,
|
checkerboardOffscreenLayers: devToolsSettings.checkerboardOffscreenLayers,
|
||||||
checkerboardRasterCacheImages: devToolsSettings.checkerboardRasterCacheImages,
|
checkerboardRasterCacheImages: devToolsSettings.checkerboardRasterCacheImages,
|
||||||
|
|
||||||
debugShowCheckedModeBanner: false,
|
|
||||||
localizationsDelegates: const [
|
localizationsDelegates: const [
|
||||||
...GlobalMaterialLocalizations.delegates,
|
...GlobalMaterialLocalizations.delegates,
|
||||||
GlobalWidgetsLocalizations.delegate,
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
287
lib/view/pages/timetable/calendar.dart
Normal file
287
lib/view/pages/timetable/calendar.dart
Normal file
@ -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<Calendar> createState() => _CalendarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CalendarState extends State<Calendar> {
|
||||||
|
@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<TimetableProps>(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<TimeRegion> _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<DateTime>.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<GetTimetableResponseObject> _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;
|
||||||
|
}
|
||||||
|
}
|
@ -2,24 +2,16 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../../../extensions/dateTime.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:syncfusion_flutter_calendar/calendar.dart';
|
import 'package:syncfusion_flutter_calendar/calendar.dart';
|
||||||
|
|
||||||
import '../../../api/webuntis/queries/getHolidays/getHolidaysResponse.dart';
|
import '../../../homescreen_widgets/timetable/timetableHomeWidget.dart';
|
||||||
import '../../../api/webuntis/queries/getTimetable/getTimetableResponse.dart';
|
|
||||||
import '../../../model/timetable/timetableProps.dart';
|
import '../../../model/timetable/timetableProps.dart';
|
||||||
import '../../../storage/base/settingsProvider.dart';
|
import '../../../storage/base/settingsProvider.dart';
|
||||||
import '../../../widget/loadingSpinner.dart';
|
import '../../../widget/loadingSpinner.dart';
|
||||||
import '../../../widget/placeholderView.dart';
|
import '../../../widget/placeholderView.dart';
|
||||||
import 'appointmenetComponent.dart';
|
import 'calendar.dart';
|
||||||
import 'appointmentDetails.dart';
|
|
||||||
import 'arbitraryAppointment.dart';
|
|
||||||
import 'customTimetableColors.dart';
|
|
||||||
import 'customTimetableEventEditDialog.dart';
|
import 'customTimetableEventEditDialog.dart';
|
||||||
import 'timeRegionComponent.dart';
|
|
||||||
import 'timetableEvents.dart';
|
|
||||||
import 'timetableNameMode.dart';
|
|
||||||
import 'viewCustomTimetableEvents.dart';
|
import 'viewCustomTimetableEvents.dart';
|
||||||
|
|
||||||
class Timetable extends StatefulWidget {
|
class Timetable extends StatefulWidget {
|
||||||
@ -34,12 +26,11 @@ enum CalendarActions { addEvent, viewEvents }
|
|||||||
class _TimetableState extends State<Timetable> {
|
class _TimetableState extends State<Timetable> {
|
||||||
CalendarController controller = CalendarController();
|
CalendarController controller = CalendarController();
|
||||||
late Timer updateTimings;
|
late Timer updateTimings;
|
||||||
late final SettingsProvider settings;
|
late SettingsProvider settings;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
settings = Provider.of<SettingsProvider>(context, listen: false);
|
settings = Provider.of<SettingsProvider>(context, listen: false);
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
Provider.of<TimetableProps>(context, listen: false).run();
|
Provider.of<TimetableProps>(context, listen: false).run();
|
||||||
});
|
});
|
||||||
@ -56,6 +47,7 @@ class _TimetableState extends State<Timetable> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Stunden & Vertretungsplan'),
|
title: const Text('Stunden & Vertretungsplan'),
|
||||||
actions: [
|
actions: [
|
||||||
|
IconButton(onPressed: () => TimetableHomeWidget.update(context), icon: Icon(Icons.screen_share_outlined)),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.home_outlined),
|
icon: const Icon(Icons.home_outlined),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@ -119,62 +111,19 @@ class _TimetableState extends State<Timetable> {
|
|||||||
|
|
||||||
if(value.primaryLoading()) return const LoadingSpinner();
|
if(value.primaryLoading()) return const LoadingSpinner();
|
||||||
|
|
||||||
var holidays = value.getHolidaysResponse;
|
|
||||||
|
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
child: SfCalendar(
|
child: Calendar(
|
||||||
timeZone: 'W. Europe Standard Time',
|
controller: controller,
|
||||||
view: CalendarView.workWeek,
|
timetableProps: value,
|
||||||
dataSource: _buildTableEvents(value),
|
settings: settings,
|
||||||
|
),
|
||||||
maxDate: DateTime.now().add(const Duration(days: 7)).nextWeekday(DateTime.saturday),
|
onRefresh: () async {
|
||||||
minDate: DateTime.now().subtract(const Duration (days: 14)).nextWeekday(DateTime.sunday),
|
Provider.of<TimetableProps>(context, listen: false).run(renew: true);
|
||||||
|
return Future.delayed(const Duration(seconds: 3));
|
||||||
controller: controller,
|
}
|
||||||
|
);
|
||||||
onViewChanged: (ViewChangedDetails details) {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
|
||||||
Provider.of<TimetableProps>(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<TimetableProps>(context, listen: false).run(renew: true);
|
|
||||||
return Future.delayed(const Duration(seconds: 3));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -182,210 +131,4 @@ class _TimetableState extends State<Timetable> {
|
|||||||
updateTimings.cancel();
|
updateTimings.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<TimeRegion> _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<DateTime>.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<GetTimetableResponseObject> _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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
# 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
|
# 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.
|
# 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:
|
environment:
|
||||||
sdk: '>3.0.0'
|
sdk: '>3.0.0'
|
||||||
@ -101,6 +101,8 @@ dependencies:
|
|||||||
time_range_picker: ^2.3.0
|
time_range_picker: ^2.3.0
|
||||||
url_launcher: ^6.3.1
|
url_launcher: ^6.3.1
|
||||||
uuid: ^4.5.1
|
uuid: ^4.5.1
|
||||||
|
home_widget: ^0.7.0+1
|
||||||
|
screenshot: ^3.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user