added timetable widget for android devices
This commit is contained in:
		@@ -57,6 +57,9 @@ android {
 | 
			
		||||
            signingConfig signingConfigs.debug
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    buildFeatures {
 | 
			
		||||
        viewBinding true
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
flutter {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,45 +1,69 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<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
 | 
			
		||||
        android:label="Marianum Fulda"
 | 
			
		||||
        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
 | 
			
		||||
            android:name=".MainActivity"
 | 
			
		||||
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
 | 
			
		||||
            android:exported="true"
 | 
			
		||||
            android:hardwareAccelerated="true"
 | 
			
		||||
            android:launchMode="singleTop"
 | 
			
		||||
            android:theme="@style/LaunchTheme"
 | 
			
		||||
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
 | 
			
		||||
            android:hardwareAccelerated="true"
 | 
			
		||||
            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
 | 
			
		||||
                 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
 | 
			
		||||
              android:name="io.flutter.embedding.android.NormalTheme"
 | 
			
		||||
              android:resource="@style/NormalTheme"
 | 
			
		||||
              />
 | 
			
		||||
                android:name="io.flutter.embedding.android.NormalTheme"
 | 
			
		||||
                android:resource="@style/NormalTheme" />
 | 
			
		||||
 | 
			
		||||
            <intent-filter>
 | 
			
		||||
                <action android:name="android.intent.action.MAIN"/>
 | 
			
		||||
                <category android:name="android.intent.category.LAUNCHER"/>
 | 
			
		||||
                <action android:name="android.intent.action.MAIN" />
 | 
			
		||||
                <category android:name="android.intent.category.LAUNCHER" />
 | 
			
		||||
            </intent-filter>
 | 
			
		||||
        </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
 | 
			
		||||
            android:name="flutterEmbedding"
 | 
			
		||||
            android:value="2" />
 | 
			
		||||
    </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">
 | 
			
		||||
        <item name="android:windowBackground">?android:colorBackground</item>
 | 
			
		||||
    </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>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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">
 | 
			
		||||
        <item name="android:windowBackground">?android:colorBackground</item>
 | 
			
		||||
    </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>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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'
 | 
			
		||||
subprojects {
 | 
			
		||||
    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,
 | 
			
		||||
            checkerboardRasterCacheImages: devToolsSettings.checkerboardRasterCacheImages,
 | 
			
		||||
 | 
			
		||||
            debugShowCheckedModeBanner: false,
 | 
			
		||||
            localizationsDelegates: const [
 | 
			
		||||
              ...GlobalMaterialLocalizations.delegates,
 | 
			
		||||
              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 '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<Timetable> {
 | 
			
		||||
  CalendarController controller = CalendarController();
 | 
			
		||||
  late Timer updateTimings;
 | 
			
		||||
  late final SettingsProvider settings;
 | 
			
		||||
  late SettingsProvider settings;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    settings = Provider.of<SettingsProvider>(context, listen: false);
 | 
			
		||||
 | 
			
		||||
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
 | 
			
		||||
      Provider.of<TimetableProps>(context, listen: false).run();
 | 
			
		||||
    });
 | 
			
		||||
@@ -56,6 +47,7 @@ class _TimetableState extends State<Timetable> {
 | 
			
		||||
      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<Timetable> {
 | 
			
		||||
 | 
			
		||||
          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<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));
 | 
			
		||||
              }
 | 
			
		||||
            );
 | 
			
		||||
            child: Calendar(
 | 
			
		||||
              controller: controller,
 | 
			
		||||
              timetableProps: value,
 | 
			
		||||
              settings: settings,
 | 
			
		||||
            ),
 | 
			
		||||
            onRefresh: () async {
 | 
			
		||||
              Provider.of<TimetableProps>(context, listen: false).run(renew: true);
 | 
			
		||||
              return Future.delayed(const Duration(seconds: 3));
 | 
			
		||||
            }
 | 
			
		||||
          );
 | 
			
		||||
        },
 | 
			
		||||
      ),
 | 
			
		||||
      )
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -182,210 +131,4 @@ class _TimetableState extends State<Timetable> {
 | 
			
		||||
    updateTimings.cancel();
 | 
			
		||||
    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
 | 
			
		||||
# 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:
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user