23 Commits

Author SHA1 Message Date
6bbc75fa94 implemented scheduled updates for widgets 2025-03-11 15:50:02 +01:00
b0bbad7f97 added timetable widget for android devices 2025-02-16 18:08:04 +01:00
769fbc1b6a added timetable color substitute teachers 2025-02-14 19:31:46 +01:00
8daf57bcee #75 pinned timetable to german timezone 2025-02-13 22:28:45 +01:00
33d488946a updated android configuration files 2025-02-09 20:42:47 +01:00
41a5e021c5 Merge pull request 'made app modules movable in their order' (#84) from develop-reorderableAppModules into develop
Reviewed-on: #84
Reviewed-by: Pupsi <larslukasneuhaus@gmx.de>
2025-02-09 17:36:20 +00:00
8f58893553 Merge branch 'develop' into develop-reorderableAppModules 2025-02-09 17:36:12 +00:00
626d3d5564 added minimum message drag distance for chat reply 2025-02-09 15:16:02 +01:00
d833cdb733 made app modules movable in their order 2025-02-09 15:06:14 +01:00
8868914a76 restructured settings and devtools 2025-02-08 23:21:20 +01:00
70e6f82b10 updated chat images loading animation 2025-02-08 22:53:14 +01:00
6651613331 fixed files cache not working correctly 2025-02-08 22:35:20 +01:00
9ad0f624de Merge remote-tracking branch 'origin/develop' into develop 2025-02-08 21:40:49 +01:00
82c143f847 bumped version 2025-02-08 21:40:39 +01:00
1fdf731b81 Merge pull request 'added option for timetable naming modes' (#83) from feature-timetableNamingSetting into develop
Reviewed-on: #83
Reviewed-by: Pupsi <larslukasneuhaus@gmx.de>
2025-01-24 22:27:06 +00:00
2d3ccd25b4 Merge branch 'develop' into feature-timetableNamingSetting 2025-01-24 22:26:52 +00:00
385ee806d6 updated feedback info text 2025-01-24 13:53:12 +01:00
92aef41031 updated room plan image 2025-01-24 11:54:35 +01:00
65b29ec4b8 added option for timetable naming modes 2025-01-24 11:50:14 +01:00
9f51d68531 updated build runner tasks 2025-01-24 11:02:03 +01:00
5bc4ba6332 replaced version wildcard with version range 2025-01-23 23:39:01 +01:00
e9739ac2d5 updated app badger 2025-01-23 22:41:22 +01:00
4d3a33dd9b updated project 2025-01-23 11:20:08 +01:00
94 changed files with 1723 additions and 964 deletions

View File

@ -25,9 +25,10 @@ if (flutterVersionName == null) {
android {
namespace "eu.mhsl.marianum.mobile.client"
compileSdk flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
ndkVersion "27.0.12077973"
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
@ -41,11 +42,8 @@ android {
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "eu.mhsl.marianum.mobile.client"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion 21
minSdkVersion 26
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
@ -59,6 +57,9 @@ android {
signingConfig signingConfigs.debug
}
}
buildFeatures {
viewBinding true
}
}
flutter {
@ -66,5 +67,6 @@ flutter {
}
dependencies {
implementation 'com.android.support:multidex:1.0.3'
implementation 'com.android.support:multidex:2.0.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
}

0
android/app/proguard-rules.pro vendored Normal file
View File

View File

@ -1,45 +1,73 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
>
<!--
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"
tools:replace="android:label"
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>

View File

@ -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

View File

@ -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>

View File

@ -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>

View 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>

View 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>

View 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>

View File

@ -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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@ -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>

View 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>

View 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" />

View File

@ -2,6 +2,21 @@ allprojects {
repositories {
google()
mavenCentral()
// [required] background_fetch
maven {
url "${project(':background_fetch').projectDir}/libs"
}
}
}
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10'
}
}

View File

@ -1,3 +1,6 @@
org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true
android.enableJetifier=true
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false

View File

@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -19,7 +19,7 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
id "com.android.application" version '8.7.3' apply false
id "org.jetbrains.kotlin.android" version "1.8.10" apply false
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 101 KiB

View File

@ -5,5 +5,5 @@ abstract class ApiResponse {
late http.Response rawResponse;
@JsonKey(includeIfNull: false)
late Map<String, String>? headers;
Map<String, String>? headers;
}

View File

@ -16,26 +16,19 @@ GetHolidaysResponse _$GetHolidaysResponseFromJson(Map<String, dynamic> json) =>
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$GetHolidaysResponseToJson(GetHolidaysResponse instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('headers', instance.headers);
val['data'] = instance.data.map((e) => e.toJson()).toList();
return val;
}
Map<String, dynamic> _$GetHolidaysResponseToJson(
GetHolidaysResponse instance) =>
<String, dynamic>{
if (instance.headers case final value?) 'headers': value,
'data': instance.data.map((e) => e.toJson()).toList(),
};
GetHolidaysResponseObject _$GetHolidaysResponseObjectFromJson(
Map<String, dynamic> json) =>
GetHolidaysResponseObject(
start: json['start'] as String,
end: json['end'] as String,
year: json['year'] as int,
year: (json['year'] as num).toInt(),
stateCode: json['stateCode'] as String,
name: json['name'] as String,
slug: json['slug'] as String,

View File

@ -9,7 +9,7 @@ part of 'fileSharingApiParams.dart';
FileSharingApiParams _$FileSharingApiParamsFromJson(
Map<String, dynamic> json) =>
FileSharingApiParams(
shareType: json['shareType'] as int,
shareType: (json['shareType'] as num).toInt(),
shareWith: json['shareWith'] as String,
path: json['path'] as String,
referenceId: json['referenceId'] as String?,

View File

@ -10,37 +10,32 @@ GetChatParams _$GetChatParamsFromJson(Map<String, dynamic> json) =>
GetChatParams(
lookIntoFuture:
$enumDecode(_$GetChatParamsSwitchEnumMap, json['lookIntoFuture']),
limit: json['limit'] as int?,
lastKnownMessageId: json['lastKnownMessageId'] as int?,
lastCommonReadId: json['lastCommonReadId'] as int?,
timeout: json['timeout'] as int?,
limit: (json['limit'] as num?)?.toInt(),
lastKnownMessageId: (json['lastKnownMessageId'] as num?)?.toInt(),
lastCommonReadId: (json['lastCommonReadId'] as num?)?.toInt(),
timeout: (json['timeout'] as num?)?.toInt(),
setReadMarker: $enumDecodeNullable(
_$GetChatParamsSwitchEnumMap, json['setReadMarker']),
includeLastKnown: $enumDecodeNullable(
_$GetChatParamsSwitchEnumMap, json['includeLastKnown']),
);
Map<String, dynamic> _$GetChatParamsToJson(GetChatParams instance) {
final val = <String, dynamic>{
'lookIntoFuture': _$GetChatParamsSwitchEnumMap[instance.lookIntoFuture]!,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('limit', instance.limit);
writeNotNull('lastKnownMessageId', instance.lastKnownMessageId);
writeNotNull('lastCommonReadId', instance.lastCommonReadId);
writeNotNull('timeout', instance.timeout);
writeNotNull(
'setReadMarker', _$GetChatParamsSwitchEnumMap[instance.setReadMarker]);
writeNotNull('includeLastKnown',
_$GetChatParamsSwitchEnumMap[instance.includeLastKnown]);
return val;
}
Map<String, dynamic> _$GetChatParamsToJson(GetChatParams instance) =>
<String, dynamic>{
'lookIntoFuture': _$GetChatParamsSwitchEnumMap[instance.lookIntoFuture]!,
if (instance.limit case final value?) 'limit': value,
if (instance.lastKnownMessageId case final value?)
'lastKnownMessageId': value,
if (instance.lastCommonReadId case final value?)
'lastCommonReadId': value,
if (instance.timeout case final value?) 'timeout': value,
if (_$GetChatParamsSwitchEnumMap[instance.setReadMarker]
case final value?)
'setReadMarker': value,
if (_$GetChatParamsSwitchEnumMap[instance.includeLastKnown]
case final value?)
'includeLastKnown': value,
};
const _$GetChatParamsSwitchEnumMap = {
GetChatParamsSwitch.on: 1,

View File

@ -15,30 +15,22 @@ GetChatResponse _$GetChatResponseFromJson(Map<String, dynamic> json) =>
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$GetChatResponseToJson(GetChatResponse instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('headers', instance.headers);
val['data'] = instance.data.map((e) => e.toJson()).toList();
return val;
}
Map<String, dynamic> _$GetChatResponseToJson(GetChatResponse instance) =>
<String, dynamic>{
if (instance.headers case final value?) 'headers': value,
'data': instance.data.map((e) => e.toJson()).toList(),
};
GetChatResponseObject _$GetChatResponseObjectFromJson(
Map<String, dynamic> json) =>
GetChatResponseObject(
json['id'] as int,
(json['id'] as num).toInt(),
json['token'] as String,
$enumDecode(
_$GetRoomResponseObjectMessageActorTypeEnumMap, json['actorType']),
json['actorId'] as String,
json['actorDisplayName'] as String,
json['timestamp'] as int,
(json['timestamp'] as num).toInt(),
json['systemMessage'] as String,
$enumDecode(
_$GetRoomResponseObjectMessageTypeEnumMap, json['messageType']),
@ -47,7 +39,7 @@ GetChatResponseObject _$GetChatResponseObjectFromJson(
json['message'] as String,
_fromJson(json['messageParameters']),
(json['reactions'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as int),
(k, e) => MapEntry(k, (e as num).toInt()),
),
(json['reactionsSelf'] as List<dynamic>?)
?.map((e) => e as String)

View File

@ -8,7 +8,7 @@ part of 'createRoomParams.dart';
CreateRoomParams _$CreateRoomParamsFromJson(Map<String, dynamic> json) =>
CreateRoomParams(
roomType: json['roomType'] as int,
roomType: (json['roomType'] as num).toInt(),
invite: json['invite'] as String,
source: json['source'] as String?,
roomName: json['roomName'] as String?,

View File

@ -18,34 +18,26 @@ GetParticipantsResponse _$GetParticipantsResponseFromJson(
);
Map<String, dynamic> _$GetParticipantsResponseToJson(
GetParticipantsResponse instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('headers', instance.headers);
val['data'] = instance.data.map((e) => e.toJson()).toList();
return val;
}
GetParticipantsResponse instance) =>
<String, dynamic>{
if (instance.headers case final value?) 'headers': value,
'data': instance.data.map((e) => e.toJson()).toList(),
};
GetParticipantsResponseObject _$GetParticipantsResponseObjectFromJson(
Map<String, dynamic> json) =>
GetParticipantsResponseObject(
json['attendeeId'] as int,
(json['attendeeId'] as num).toInt(),
json['actorType'] as String,
json['actorId'] as String,
json['displayName'] as String,
$enumDecode(_$GetParticipantsResponseObjectParticipantTypeEnumMap,
json['participantType']),
json['lastPing'] as int,
(json['lastPing'] as num).toInt(),
$enumDecode(_$GetParticipantsResponseObjectParticipantsInCallFlagsEnumMap,
json['inCall']),
json['permissions'] as int,
json['attendeePermissions'] as int,
(json['permissions'] as num).toInt(),
(json['attendeePermissions'] as num).toInt(),
json['sessionId'] as String?,
(json['sessionIds'] as List<dynamic>).map((e) => e as String).toList(),
json['status'] as String?,

View File

@ -22,20 +22,12 @@ GetReactionsResponse _$GetReactionsResponseFromJson(
);
Map<String, dynamic> _$GetReactionsResponseToJson(
GetReactionsResponse instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('headers', instance.headers);
val['data'] = instance.data
.map((k, e) => MapEntry(k, e.map((e) => e.toJson()).toList()));
return val;
}
GetReactionsResponse instance) =>
<String, dynamic>{
if (instance.headers case final value?) 'headers': value,
'data': instance.data
.map((k, e) => MapEntry(k, e.map((e) => e.toJson()).toList())),
};
GetReactionsResponseObject _$GetReactionsResponseObjectFromJson(
Map<String, dynamic> json) =>
@ -44,7 +36,7 @@ GetReactionsResponseObject _$GetReactionsResponseObjectFromJson(
_$GetReactionsResponseObjectActorTypeEnumMap, json['actorType']),
json['actorId'] as String,
json['actorDisplayName'] as String,
json['timestamp'] as int,
(json['timestamp'] as num).toInt(),
);
Map<String, dynamic> _$GetReactionsResponseObjectToJson(

View File

@ -11,7 +11,7 @@ GetRoomParams _$GetRoomParamsFromJson(Map<String, dynamic> json) =>
noStatusUpdate: $enumDecodeNullable(
_$GetRoomParamsStatusUpdateEnumMap, json['noStatusUpdate']),
includeStatus: json['includeStatus'] as bool?,
modifiedSince: json['modifiedSince'] as int?,
modifiedSince: (json['modifiedSince'] as num?)?.toInt(),
);
Map<String, dynamic> _$GetRoomParamsToJson(GetRoomParams instance) =>

View File

@ -15,50 +15,42 @@ GetRoomResponse _$GetRoomResponseFromJson(Map<String, dynamic> json) =>
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$GetRoomResponseToJson(GetRoomResponse instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('headers', instance.headers);
val['data'] = instance.data.map((e) => e.toJson()).toList();
return val;
}
Map<String, dynamic> _$GetRoomResponseToJson(GetRoomResponse instance) =>
<String, dynamic>{
if (instance.headers case final value?) 'headers': value,
'data': instance.data.map((e) => e.toJson()).toList(),
};
GetRoomResponseObject _$GetRoomResponseObjectFromJson(
Map<String, dynamic> json) =>
GetRoomResponseObject(
json['id'] as int,
(json['id'] as num).toInt(),
json['token'] as String,
$enumDecode(_$GetRoomResponseObjectConversationTypeEnumMap, json['type']),
json['name'] as String,
json['displayName'] as String,
json['description'] as String,
json['participantType'] as int,
json['participantFlags'] as int,
json['readOnly'] as int,
json['listable'] as int,
json['lastPing'] as int,
(json['participantType'] as num).toInt(),
(json['participantFlags'] as num).toInt(),
(json['readOnly'] as num).toInt(),
(json['listable'] as num).toInt(),
(json['lastPing'] as num).toInt(),
json['sessionId'] as String,
json['hasPassword'] as bool,
json['hasCall'] as bool,
json['callFlag'] as int,
(json['callFlag'] as num).toInt(),
json['canStartCall'] as bool,
json['canDeleteConversation'] as bool,
json['canLeaveConversation'] as bool,
json['lastActivity'] as int,
(json['lastActivity'] as num).toInt(),
json['isFavorite'] as bool,
$enumDecode(_$GetRoomResponseObjectParticipantNotificationLevelEnumMap,
json['notificationLevel']),
json['unreadMessages'] as int,
(json['unreadMessages'] as num).toInt(),
json['unreadMention'] as bool,
json['unreadMentionDirect'] as bool,
json['lastReadMessage'] as int,
json['lastCommonReadMessage'] as int,
(json['lastReadMessage'] as num).toInt(),
(json['lastCommonReadMessage'] as num).toInt(),
GetChatResponseObject.fromJson(
json['lastMessage'] as Map<String, dynamic>),
json['status'] as String?,

View File

@ -12,17 +12,8 @@ SendMessageParams _$SendMessageParamsFromJson(Map<String, dynamic> json) =>
replyTo: json['replyTo'] as String?,
);
Map<String, dynamic> _$SendMessageParamsToJson(SendMessageParams instance) {
final val = <String, dynamic>{
'message': instance.message,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('replyTo', instance.replyTo);
return val;
}
Map<String, dynamic> _$SendMessageParamsToJson(SendMessageParams instance) =>
<String, dynamic>{
'message': instance.message,
if (instance.replyTo case final value?) 'replyTo': value,
};

View File

@ -8,7 +8,7 @@ part of 'setReadMarkerParams.dart';
SetReadMarkerParams _$SetReadMarkerParamsFromJson(Map<String, dynamic> json) =>
SetReadMarkerParams(
lastReadMessage: json['lastReadMessage'] as int?,
lastReadMessage: (json['lastReadMessage'] as num?)?.toInt(),
);
Map<String, dynamic> _$SetReadMarkerParamsToJson(

View File

@ -12,7 +12,7 @@ CacheableFile _$CacheableFileFromJson(Map<String, dynamic> json) =>
isDirectory: json['isDirectory'] as bool,
name: json['name'] as String,
mimeType: json['mimeType'] as String?,
size: json['size'] as int?,
size: (json['size'] as num?)?.toInt(),
eTag: json['eTag'] as String?,
createdAt: json['createdAt'] == null
? null

View File

@ -15,16 +15,8 @@ ListFilesResponse _$ListFilesResponseFromJson(Map<String, dynamic> json) =>
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$ListFilesResponseToJson(ListFilesResponse instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('headers', instance.headers);
val['files'] = instance.files.map((e) => e.toJson()).toList();
return val;
}
Map<String, dynamic> _$ListFilesResponseToJson(ListFilesResponse instance) =>
<String, dynamic>{
if (instance.headers case final value?) 'headers': value,
'files': instance.files.map((e) => e.toJson()).toList(),
};

View File

@ -17,20 +17,13 @@ GetBreakersResponse _$GetBreakersResponseFromJson(Map<String, dynamic> json) =>
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$GetBreakersResponseToJson(GetBreakersResponse instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('headers', instance.headers);
val['global'] = instance.global.toJson();
val['regional'] = instance.regional.map((k, e) => MapEntry(k, e.toJson()));
return val;
}
Map<String, dynamic> _$GetBreakersResponseToJson(
GetBreakersResponse instance) =>
<String, dynamic>{
if (instance.headers case final value?) 'headers': value,
'global': instance.global.toJson(),
'regional': instance.regional.map((k, e) => MapEntry(k, e.toJson())),
};
GetBreakersReponseObject _$GetBreakersReponseObjectFromJson(
Map<String, dynamic> json) =>

View File

@ -17,16 +17,8 @@ GetCustomTimetableEventResponse _$GetCustomTimetableEventResponseFromJson(
);
Map<String, dynamic> _$GetCustomTimetableEventResponseToJson(
GetCustomTimetableEventResponse instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('headers', instance.headers);
val['events'] = instance.events;
return val;
}
GetCustomTimetableEventResponse instance) =>
<String, dynamic>{
if (instance.headers case final value?) 'headers': value,
'events': instance.events,
};

View File

@ -11,7 +11,7 @@ AddFeedbackParams _$AddFeedbackParamsFromJson(Map<String, dynamic> json) =>
user: json['user'] as String,
feedback: json['feedback'] as String,
screenshot: json['screenshot'] as String?,
appVersion: json['appVersion'] as int,
appVersion: (json['appVersion'] as num).toInt(),
);
Map<String, dynamic> _$AddFeedbackParamsToJson(AddFeedbackParams instance) =>

View File

@ -12,7 +12,7 @@ UpdateUserIndexParams _$UpdateUserIndexParamsFromJson(
user: json['user'] as String,
username: json['username'] as String,
device: json['device'] as String,
appVersion: json['appVersion'] as int,
appVersion: (json['appVersion'] as num).toInt(),
deviceInfo: json['deviceInfo'] as String,
);

View File

@ -4,7 +4,7 @@ import 'dart:developer';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:http/http.dart' as http;
import 'package:package_info/package_info.dart';
import 'package:package_info_plus/package_info_plus.dart';
import '../../../../../model/accountData.dart';
import '../../../mhslApi.dart';

View File

@ -3,7 +3,6 @@ import 'dart:convert';
import 'package:localstore/localstore.dart';
import 'apiResponse.dart';
import 'webuntis/webuntisError.dart';
abstract class RequestCache<T extends ApiResponse?> {
static const int cacheNothing = 0;
@ -40,7 +39,7 @@ abstract class RequestCache<T extends ApiResponse?> {
'json': jsonEncode(newValue),
'lastupdate': DateTime.now().millisecondsSinceEpoch
});
} on WebuntisError catch(e) {
} on Exception catch(e) {
onError(e);
}
}

View File

@ -10,27 +10,19 @@ AuthenticateResponse _$AuthenticateResponseFromJson(
Map<String, dynamic> json) =>
AuthenticateResponse(
json['sessionId'] as String,
json['personType'] as int,
json['personId'] as int,
json['klasseId'] as int,
(json['personType'] as num).toInt(),
(json['personId'] as num).toInt(),
(json['klasseId'] as num).toInt(),
)..headers = (json['headers'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$AuthenticateResponseToJson(
AuthenticateResponse instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('headers', instance.headers);
val['sessionId'] = instance.sessionId;
val['personType'] = instance.personType;
val['personId'] = instance.personId;
val['klasseId'] = instance.klasseId;
return val;
}
AuthenticateResponse instance) =>
<String, dynamic>{
if (instance.headers case final value?) 'headers': value,
'sessionId': instance.sessionId,
'personType': instance.personType,
'personId': instance.personId,
'klasseId': instance.klasseId,
};

View File

@ -16,28 +16,21 @@ GetHolidaysResponse _$GetHolidaysResponseFromJson(Map<String, dynamic> json) =>
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$GetHolidaysResponseToJson(GetHolidaysResponse instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('headers', instance.headers);
val['result'] = instance.result.map((e) => e.toJson()).toList();
return val;
}
Map<String, dynamic> _$GetHolidaysResponseToJson(
GetHolidaysResponse instance) =>
<String, dynamic>{
if (instance.headers case final value?) 'headers': value,
'result': instance.result.map((e) => e.toJson()).toList(),
};
GetHolidaysResponseObject _$GetHolidaysResponseObjectFromJson(
Map<String, dynamic> json) =>
GetHolidaysResponseObject(
json['id'] as int,
(json['id'] as num).toInt(),
json['name'] as String,
json['longName'] as String,
json['startDate'] as int,
json['endDate'] as int,
(json['startDate'] as num).toInt(),
(json['endDate'] as num).toInt(),
);
Map<String, dynamic> _$GetHolidaysResponseObjectToJson(

View File

@ -16,24 +16,16 @@ GetRoomsResponse _$GetRoomsResponseFromJson(Map<String, dynamic> json) =>
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$GetRoomsResponseToJson(GetRoomsResponse instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('headers', instance.headers);
val['result'] = instance.result.map((e) => e.toJson()).toList();
return val;
}
Map<String, dynamic> _$GetRoomsResponseToJson(GetRoomsResponse instance) =>
<String, dynamic>{
if (instance.headers case final value?) 'headers': value,
'result': instance.result.map((e) => e.toJson()).toList(),
};
GetRoomsResponseObject _$GetRoomsResponseObjectFromJson(
Map<String, dynamic> json) =>
GetRoomsResponseObject(
json['id'] as int,
(json['id'] as num).toInt(),
json['name'] as String,
json['longName'] as String,
json['active'] as bool,

View File

@ -16,24 +16,17 @@ GetSubjectsResponse _$GetSubjectsResponseFromJson(Map<String, dynamic> json) =>
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$GetSubjectsResponseToJson(GetSubjectsResponse instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('headers', instance.headers);
val['result'] = instance.result.map((e) => e.toJson()).toList();
return val;
}
Map<String, dynamic> _$GetSubjectsResponseToJson(
GetSubjectsResponse instance) =>
<String, dynamic>{
if (instance.headers case final value?) 'headers': value,
'result': instance.result.map((e) => e.toJson()).toList(),
};
GetSubjectsResponseObject _$GetSubjectsResponseObjectFromJson(
Map<String, dynamic> json) =>
GetSubjectsResponseObject(
json['id'] as int,
(json['id'] as num).toInt(),
json['name'] as String,
json['longName'] as String,
json['alternateName'] as String,

View File

@ -22,8 +22,8 @@ GetTimetableParamsOptions _$GetTimetableParamsOptionsFromJson(
GetTimetableParamsOptions(
element: GetTimetableParamsOptionsElement.fromJson(
json['element'] as Map<String, dynamic>),
startDate: json['startDate'] as int?,
endDate: json['endDate'] as int?,
startDate: (json['startDate'] as num?)?.toInt(),
endDate: (json['endDate'] as num?)?.toInt(),
onlyBaseTimetable: json['onlyBaseTimetable'] as bool?,
showBooking: json['showBooking'] as bool?,
showInfo: json['showInfo'] as bool?,
@ -46,48 +46,41 @@ GetTimetableParamsOptions _$GetTimetableParamsOptionsFromJson(
);
Map<String, dynamic> _$GetTimetableParamsOptionsToJson(
GetTimetableParamsOptions instance) {
final val = <String, dynamic>{
'element': instance.element.toJson(),
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('startDate', instance.startDate);
writeNotNull('endDate', instance.endDate);
writeNotNull('onlyBaseTimetable', instance.onlyBaseTimetable);
writeNotNull('showBooking', instance.showBooking);
writeNotNull('showInfo', instance.showInfo);
writeNotNull('showSubstText', instance.showSubstText);
writeNotNull('showLsText', instance.showLsText);
writeNotNull('showLsNumber', instance.showLsNumber);
writeNotNull('showStudentgroup', instance.showStudentgroup);
writeNotNull(
'klasseFields',
instance.klasseFields
?.map((e) => _$GetTimetableParamsOptionsFieldsEnumMap[e]!)
.toList());
writeNotNull(
'roomFields',
instance.roomFields
?.map((e) => _$GetTimetableParamsOptionsFieldsEnumMap[e]!)
.toList());
writeNotNull(
'subjectFields',
instance.subjectFields
?.map((e) => _$GetTimetableParamsOptionsFieldsEnumMap[e]!)
.toList());
writeNotNull(
'teacherFields',
instance.teacherFields
?.map((e) => _$GetTimetableParamsOptionsFieldsEnumMap[e]!)
.toList());
return val;
}
GetTimetableParamsOptions instance) =>
<String, dynamic>{
'element': instance.element.toJson(),
if (instance.startDate case final value?) 'startDate': value,
if (instance.endDate case final value?) 'endDate': value,
if (instance.onlyBaseTimetable case final value?)
'onlyBaseTimetable': value,
if (instance.showBooking case final value?) 'showBooking': value,
if (instance.showInfo case final value?) 'showInfo': value,
if (instance.showSubstText case final value?) 'showSubstText': value,
if (instance.showLsText case final value?) 'showLsText': value,
if (instance.showLsNumber case final value?) 'showLsNumber': value,
if (instance.showStudentgroup case final value?)
'showStudentgroup': value,
if (instance.klasseFields
?.map((e) => _$GetTimetableParamsOptionsFieldsEnumMap[e]!)
.toList()
case final value?)
'klasseFields': value,
if (instance.roomFields
?.map((e) => _$GetTimetableParamsOptionsFieldsEnumMap[e]!)
.toList()
case final value?)
'roomFields': value,
if (instance.subjectFields
?.map((e) => _$GetTimetableParamsOptionsFieldsEnumMap[e]!)
.toList()
case final value?)
'subjectFields': value,
if (instance.teacherFields
?.map((e) => _$GetTimetableParamsOptionsFieldsEnumMap[e]!)
.toList()
case final value?)
'teacherFields': value,
};
const _$GetTimetableParamsOptionsFieldsEnumMap = {
GetTimetableParamsOptionsFields.id: 'id',
@ -99,29 +92,21 @@ const _$GetTimetableParamsOptionsFieldsEnumMap = {
GetTimetableParamsOptionsElement _$GetTimetableParamsOptionsElementFromJson(
Map<String, dynamic> json) =>
GetTimetableParamsOptionsElement(
id: json['id'] as int,
type: json['type'] as int,
id: (json['id'] as num).toInt(),
type: (json['type'] as num).toInt(),
keyType: $enumDecodeNullable(
_$GetTimetableParamsOptionsElementKeyTypeEnumMap, json['keyType']),
);
Map<String, dynamic> _$GetTimetableParamsOptionsElementToJson(
GetTimetableParamsOptionsElement instance) {
final val = <String, dynamic>{
'id': instance.id,
'type': instance.type,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('keyType',
_$GetTimetableParamsOptionsElementKeyTypeEnumMap[instance.keyType]);
return val;
}
GetTimetableParamsOptionsElement instance) =>
<String, dynamic>{
'id': instance.id,
'type': instance.type,
if (_$GetTimetableParamsOptionsElementKeyTypeEnumMap[instance.keyType]
case final value?)
'keyType': value,
};
const _$GetTimetableParamsOptionsElementKeyTypeEnumMap = {
GetTimetableParamsOptionsElementKeyType.id: 'id',

View File

@ -18,33 +18,25 @@ GetTimetableResponse _$GetTimetableResponseFromJson(
);
Map<String, dynamic> _$GetTimetableResponseToJson(
GetTimetableResponse instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('headers', instance.headers);
val['result'] = instance.result.map((e) => e.toJson()).toList();
return val;
}
GetTimetableResponse instance) =>
<String, dynamic>{
if (instance.headers case final value?) 'headers': value,
'result': instance.result.map((e) => e.toJson()).toList(),
};
GetTimetableResponseObject _$GetTimetableResponseObjectFromJson(
Map<String, dynamic> json) =>
GetTimetableResponseObject(
id: json['id'] as int,
date: json['date'] as int,
startTime: json['startTime'] as int,
endTime: json['endTime'] as int,
id: (json['id'] as num).toInt(),
date: (json['date'] as num).toInt(),
startTime: (json['startTime'] as num).toInt(),
endTime: (json['endTime'] as num).toInt(),
lstype: json['lstype'] as String?,
code: json['code'] as String?,
info: json['info'] as String?,
substText: json['substText'] as String?,
lstext: json['lstext'] as String?,
lsnumber: json['lsnumber'] as int?,
lsnumber: (json['lsnumber'] as num?)?.toInt(),
statflags: json['statflags'] as String?,
activityType: json['activityType'] as String?,
sg: json['sg'] as String?,
@ -110,7 +102,7 @@ GetTimetableResponseObjectFieldsObject
_$GetTimetableResponseObjectFieldsObjectFromJson(
Map<String, dynamic> json) =>
GetTimetableResponseObjectFieldsObject(
id: json['id'] as int?,
id: (json['id'] as num?)?.toInt(),
name: json['name'] as String?,
longname: json['longname'] as String?,
externalkey: json['externalkey'] as String?,
@ -128,7 +120,7 @@ Map<String, dynamic> _$GetTimetableResponseObjectFieldsObjectToJson(
GetTimetableResponseObjectClass _$GetTimetableResponseObjectClassFromJson(
Map<String, dynamic> json) =>
GetTimetableResponseObjectClass(
json['id'] as int,
(json['id'] as num).toInt(),
json['name'] as String,
json['longname'] as String,
json['externalkey'] as String?,
@ -146,10 +138,10 @@ Map<String, dynamic> _$GetTimetableResponseObjectClassToJson(
GetTimetableResponseObjectTeacher _$GetTimetableResponseObjectTeacherFromJson(
Map<String, dynamic> json) =>
GetTimetableResponseObjectTeacher(
json['id'] as int,
(json['id'] as num).toInt(),
json['name'] as String,
json['longname'] as String,
json['orgid'] as int?,
(json['orgid'] as num?)?.toInt(),
json['orgname'] as String?,
json['externalkey'] as String?,
);
@ -168,7 +160,7 @@ Map<String, dynamic> _$GetTimetableResponseObjectTeacherToJson(
GetTimetableResponseObjectSubject _$GetTimetableResponseObjectSubjectFromJson(
Map<String, dynamic> json) =>
GetTimetableResponseObjectSubject(
json['id'] as int,
(json['id'] as num).toInt(),
json['name'] as String,
json['longname'] as String,
);
@ -184,7 +176,7 @@ Map<String, dynamic> _$GetTimetableResponseObjectSubjectToJson(
GetTimetableResponseObjectRoom _$GetTimetableResponseObjectRoomFromJson(
Map<String, dynamic> json) =>
GetTimetableResponseObjectRoom(
json['id'] as int,
(json['id'] as num).toInt(),
json['name'] as String,
json['longname'] as String,
);

View File

@ -8,7 +8,6 @@ import 'package:flutter/material.dart';
import 'state/app/modules/app_modules.dart';
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
import 'package:provider/provider.dart';
import 'package:badges/badges.dart' as badges;
import 'api/mhsl/breaker/getBreakers/getBreakersResponse.dart';
import 'api/mhsl/server/userIndex/update/updateUserindex.dart';
@ -25,6 +24,7 @@ import 'storage/base/settingsProvider.dart';
import 'view/pages/overhang.dart';
class App extends StatefulWidget {
static GlobalKey appContext = GlobalKey();
const App({super.key});
@override
@ -32,7 +32,6 @@ class App extends StatefulWidget {
}
class _AppState extends State<App> with WidgetsBindingObserver {
late Timer refetchChats;
late Timer updateTimings;
@ -93,7 +92,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
}
@override
Widget build(BuildContext context) => PersistentTabView(
Widget build(BuildContext context) => Consumer<SettingsProvider>(builder: (context, settings, child) => PersistentTabView(
controller: Main.bottomNavigator,
navBarOverlap: const NavBarOverlap.none(),
backgroundColor: Theme.of(context).colorScheme.primary,
@ -101,29 +100,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
screenTransitionAnimation: const ScreenTransitionAnimation(curve: Curves.easeOutQuad, duration: Duration(milliseconds: 200)),
tabs: [
AppModule.getModule(Modules.timetable).toBottomTab(context),
AppModule.getModule(Modules.talk).toBottomTab(
context,
itemBuilder: (icon) => Consumer<ChatListProps>(
builder: (context, value, child) {
if(value.primaryLoading()) return Icon(icon);
var messages = value.getRoomsResponse.data.map((e) => e.unreadMessages).reduce((a, b) => a+b);
return badges.Badge(
showBadge: messages > 0,
position: badges.BadgePosition.topEnd(top: -3, end: -3),
stackFit: StackFit.loose,
badgeStyle: badges.BadgeStyle(
padding: const EdgeInsets.all(3),
badgeColor: Theme.of(context).primaryColor,
elevation: 1,
),
badgeContent: Text('$messages', style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold)),
child: Icon(icon),
);
},
),
),
AppModule.getModule(Modules.files).toBottomTab(context),
...AppModule.getBottomBarModules(context).map((e) => e.toBottomTab(context)),
PersistentTabConfig(
screen: const Breaker(breaker: BreakerArea.more, child: Overhang()),
@ -142,7 +119,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
color: Theme.of(context).colorScheme.surface,
),
),
);
));
@override
void dispose() {

View File

@ -0,0 +1,64 @@
import 'dart:developer';
import 'package:background_fetch/background_fetch.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../app.dart';
import '../homescreen_widgets/timetable/timetableHomeWidget.dart';
class ScheduledTask {
static final String fetchApiLastRunTimestampKey = 'fetchApiLastRunTimestamp';
static Future<void> configure() async {
var status = await BackgroundFetch.configure(BackgroundFetchConfig(
minimumFetchInterval: 15,
stopOnTerminate: false,
enableHeadless: true,
requiresBatteryNotLow: false,
requiresCharging: false,
requiresStorageNotLow: false,
requiresDeviceIdle: false,
requiredNetworkType: NetworkType.ANY,
startOnBoot: true,
), (String taskId) async {
log('Background fetch started with id $taskId');
await ScheduledTask.backgroundFetch();
BackgroundFetch.finish(taskId);
}, (String taskId) async {
log('Background fetch stopped because of an timeout with id $taskId');
BackgroundFetch.finish(taskId);
});
log('Background Fetch-API status: $status');
}
// called periodically, iOS and Android
static Future<void> backgroundFetch() async {
var sp = await SharedPreferences.getInstance();
var history = sp.getStringList(fetchApiLastRunTimestampKey) ?? List.empty(growable: true);
history.add(DateTime.now().toIso8601String());
try {
TimetableHomeWidget.update(App.appContext.currentContext!);
} on Exception catch(e) {
history.add('Got Error:');
history.add(e.toString());
history.add('--- EXCEPTION END ---');
}
sp.setStringList(fetchApiLastRunTimestampKey, history.take(100).toList());
}
// only Android, starts when app is terminated
@pragma('vm:entry-point')
static Future<void> headless(HeadlessTask task) async {
var taskId = task.taskId;
var isTimeout = task.timeout;
if (isTimeout) {
log('Background fetch headless task timed-out: $taskId');
BackgroundFetch.finish(taskId);
return;
}
log('Background fetch headless event received.');
await backgroundFetch();
BackgroundFetch.finish(taskId);
}
}

View File

@ -0,0 +1,85 @@
import 'dart:async';
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:screenshot/screenshot.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart';
import '../../model/accountData.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 Future<void> update(BuildContext context) async {
await AccountData().waitForPopulation();
var data = TimetableProps();
var settings = SettingsProvider();
settings.waitForPopulation();
var completer = Completer();
data.addListener(() async {
if(completer.isCompleted) return;
if(data.primaryLoading()) return;
await _generate(data, settings);
completer.complete();
});
data.run();
await completer.future;
}
static Future<void> _generate(TimetableProps data, SettingsProvider settings) async {
log('Generating widget screen...');
var screenshotController = ScreenshotController();
var calendarController = CalendarController();
calendarController.displayDate = DateTime.now().copyWith(hour: 07, minute: 00);
var imageData = await screenshotController.captureFromWidget(
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,
),
),
),
),
),
),
),
);
HomeWidget.saveWidgetData<String>('screen', base64.encode(imageData));
HomeWidget.updateWidget(name: 'TimetableWidget');
log('Widget screen successfully updated! (${imageData.length})');
}
}

View File

@ -2,21 +2,23 @@ import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'package:background_fetch/background_fetch.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:jiffy/jiffy.dart';
import 'package:loader_overlay/loader_overlay.dart';
import 'package:path_provider/path_provider.dart';
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
import 'package:provider/provider.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'api/mhsl/breaker/getBreakers/getBreakersResponse.dart';
import 'app.dart';
import 'background_tasks/scheduledTask.dart';
import 'firebase_options.dart';
import 'model/accountData.dart';
import 'model/accountModel.dart';
@ -48,7 +50,9 @@ Future<void> main() async {
PlatformAssetBundle().load('assets/ca/lets-encrypt-r10.pem').then(addCertificateAsTrusted),
Future(() async {
await HydratedStorage.build(storageDirectory: await getTemporaryDirectory()).then((storage) => HydratedBloc.storage = storage);
await HydratedStorage.build(
storageDirectory: HydratedStorageDirectory((await getTemporaryDirectory()).path)
).then((storage) => HydratedBloc.storage = storage);
})
];
@ -82,6 +86,8 @@ Future<void> main() async {
child: const Main(),
)
);
BackgroundFetch.registerHeadlessTask(ScheduledTask.headless);
}
class Main extends StatefulWidget {
@ -109,6 +115,7 @@ class _MainState extends State<Main> {
Provider.of<BreakerProps>(context, listen: false).run();
});
ScheduledTask.configure();
super.initState();
}
@ -123,7 +130,6 @@ class _MainState extends State<Main> {
checkerboardOffscreenLayers: devToolsSettings.checkerboardOffscreenLayers,
checkerboardRasterCacheImages: devToolsSettings.checkerboardRasterCacheImages,
debugShowCheckedModeBanner: false,
localizationsDelegates: const [
...GlobalMaterialLocalizations.delegates,
GlobalWidgetsLocalizations.delegate,
@ -145,7 +151,7 @@ class _MainState extends State<Main> {
child: Consumer<AccountModel>(
builder: (context, accountModel, child) {
switch(accountModel.state) {
case AccountModelState.loggedIn: return const App();
case AccountModelState.loggedIn: return App(key: App.appContext);
case AccountModelState.loggedOut: return const Login();
case AccountModelState.undefined: return const PlaceholderView(icon: Icons.timer, text: 'Daten werden geladen');
}

View File

@ -23,7 +23,11 @@ class _BreakerState extends State<Breaker> {
builder: (context, value, child) {
var blocked = value.isBlocked(widget.breaker);
if(blocked != null) {
return PlaceholderView(icon: Icons.security_outlined, text: "Die App/ Dieser Bereich wurde als Schutzmaßnahme deaktiviert!\n\n${blocked.isEmpty ? "Es wurde vom Server kein Grund übermittelt." : blocked}");
return PlaceholderView(
icon: Icons.app_blocking_outlined,
text: 'Die App / Dieser Bereich ist zurzeit nicht verfügbar!\n\n'
"${blocked.isEmpty ? "Es wurde vom Server kein Grund übermittelt.\nAktualisiere die App und versuche es später erneut" : blocked}"
);
}
return widget.child;

View File

@ -1,4 +1,4 @@
import 'package:package_info/package_info.dart';
import 'package:package_info_plus/package_info_plus.dart';
import '../../api/apiResponse.dart';
import '../../api/mhsl/breaker/getBreakers/getBreakersCache.dart';
@ -10,7 +10,7 @@ class BreakerProps extends DataHolder {
GetBreakersResponse get getBreakersResponse => _getBreakersResponse!;
PackageInfo? packageInfo;
String? isBlocked(BreakerArea? type) {
if(packageInfo == null) {
PackageInfo.fromPlatform().then((value) => packageInfo = value);

View File

@ -1,5 +1,5 @@
import 'package:flutter_app_badger/flutter_app_badger.dart';
import 'package:flutter_app_badge/flutter_app_badge.dart';
import '../../api/apiResponse.dart';
import '../../api/marianumcloud/talk/room/getRoomCache.dart';
@ -20,7 +20,7 @@ class ChatListProps extends DataHolder {
onUpdate: (GetRoomResponse data) => {
_getRoomResponse = data,
notifyListeners(),
FlutterAppBadger.updateBadgeCount(data.data.map((e) => e.unreadMessages).reduce((a, b) => a+b))
FlutterAppBadge.count(data.data.map((e) => e.unreadMessages).reduce((a, b) => a+b))
}
);
}

View File

@ -44,7 +44,7 @@ class NotificationController {
}
static Future<void> onAppOpenedByNotification(RemoteMessage message, BuildContext context) async {
NotificationTasks.navigateToTalk();
NotificationTasks.navigateToTalk(context);
NotificationTasks.updateProviders(context);
DebugTile(context).run(() {

View File

@ -15,9 +15,6 @@ class NotificationService {
);
final iosSettings = DarwinInitializationSettings(
onDidReceiveLocalNotification: (id, title, body, payload) {
// TODO Navigate to Talk section (This runs when an Notification is tapped)
},
);

View File

@ -1,15 +1,16 @@
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_app_badger/flutter_app_badger.dart';
import 'package:flutter_app_badge/flutter_app_badge.dart';
import 'package:provider/provider.dart';
import '../main.dart';
import '../model/chatList/chatListProps.dart';
import '../model/chatList/chatProps.dart';
import '../state/app/modules/app_modules.dart';
class NotificationTasks {
static void updateBadgeCount(RemoteMessage notification) {
FlutterAppBadger.updateBadgeCount(int.parse(notification.data['unreadCount'] ?? 0));
FlutterAppBadge.count(int.parse(notification.data['unreadCount'] ?? 0));
}
static void updateProviders(BuildContext context) {
@ -17,7 +18,9 @@ class NotificationTasks {
Provider.of<ChatProps>(context, listen: false).run();
}
static void navigateToTalk() {
Main.bottomNavigator.jumpToTab(1);
static void navigateToTalk(BuildContext context) {
var talkTab = AppModule.getBottomBarModules(context).map((e) => e.module).toList().indexOf(Modules.talk);
if(talkTab == -1) return;
Main.bottomNavigator.jumpToTab(talkTab);
}
}

View File

@ -19,7 +19,9 @@ mixin _$LoadableStateState {
List<ConnectivityResult>? get connections =>
throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of LoadableStateState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$LoadableStateStateCopyWith<LoadableStateState> get copyWith =>
throw _privateConstructorUsedError;
}
@ -43,6 +45,8 @@ class _$LoadableStateStateCopyWithImpl<$Res, $Val extends LoadableStateState>
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of LoadableStateState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -76,6 +80,8 @@ class __$$LoadableStateStateImplCopyWithImpl<$Res>
$Res Function(_$LoadableStateStateImpl) _then)
: super(_value, _then);
/// Create a copy of LoadableStateState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -125,7 +131,9 @@ class _$LoadableStateStateImpl implements _LoadableStateState {
int get hashCode => Object.hash(
runtimeType, const DeepCollectionEquality().hash(_connections));
@JsonKey(ignore: true)
/// Create a copy of LoadableStateState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$LoadableStateStateImplCopyWith<_$LoadableStateStateImpl> get copyWith =>
@ -140,8 +148,11 @@ abstract class _LoadableStateState implements LoadableStateState {
@override
List<ConnectivityResult>? get connections;
/// Create a copy of LoadableStateState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$LoadableStateStateImplCopyWith<_$LoadableStateStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -22,7 +22,9 @@ mixin _$LoadableState<TState> {
void Function()? get reFetch => throw _privateConstructorUsedError;
LoadingError? get error => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of LoadableState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$LoadableStateCopyWith<TState, LoadableState<TState>> get copyWith =>
throw _privateConstructorUsedError;
}
@ -54,6 +56,8 @@ class _$LoadableStateCopyWithImpl<TState, $Res,
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of LoadableState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -87,6 +91,8 @@ class _$LoadableStateCopyWithImpl<TState, $Res,
) as $Val);
}
/// Create a copy of LoadableState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$LoadingErrorCopyWith<$Res>? get error {
@ -128,6 +134,8 @@ class __$$LoadableStateImplCopyWithImpl<TState, $Res>
$Res Function(_$LoadableStateImpl<TState>) _then)
: super(_value, _then);
/// Create a copy of LoadableState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -166,27 +174,22 @@ class __$$LoadableStateImplCopyWithImpl<TState, $Res>
class _$LoadableStateImpl<TState> extends _LoadableState<TState> {
const _$LoadableStateImpl(
{this.isLoading = true,
this.data = null,
this.lastFetch = null,
this.reFetch = null,
this.error = null})
{required this.isLoading,
required this.data,
required this.lastFetch,
required this.reFetch,
required this.error})
: super._();
@override
@JsonKey()
final bool isLoading;
@override
@JsonKey()
final TState? data;
@override
@JsonKey()
final int? lastFetch;
@override
@JsonKey()
final void Function()? reFetch;
@override
@JsonKey()
final LoadingError? error;
@override
@ -212,7 +215,9 @@ class _$LoadableStateImpl<TState> extends _LoadableState<TState> {
int get hashCode => Object.hash(runtimeType, isLoading,
const DeepCollectionEquality().hash(data), lastFetch, reFetch, error);
@JsonKey(ignore: true)
/// Create a copy of LoadableState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$LoadableStateImplCopyWith<TState, _$LoadableStateImpl<TState>>
@ -222,11 +227,11 @@ class _$LoadableStateImpl<TState> extends _LoadableState<TState> {
abstract class _LoadableState<TState> extends LoadableState<TState> {
const factory _LoadableState(
{final bool isLoading,
final TState? data,
final int? lastFetch,
final void Function()? reFetch,
final LoadingError? error}) = _$LoadableStateImpl<TState>;
{required final bool isLoading,
required final TState? data,
required final int? lastFetch,
required final void Function()? reFetch,
required final LoadingError? error}) = _$LoadableStateImpl<TState>;
const _LoadableState._() : super._();
@override
@ -239,8 +244,11 @@ abstract class _LoadableState<TState> extends LoadableState<TState> {
void Function()? get reFetch;
@override
LoadingError? get error;
/// Create a copy of LoadableState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$LoadableStateImplCopyWith<TState, _$LoadableStateImpl<TState>>
get copyWith => throw _privateConstructorUsedError;
}

View File

@ -19,7 +19,9 @@ mixin _$LoadingError {
String get message => throw _privateConstructorUsedError;
bool get allowRetry => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of LoadingError
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$LoadingErrorCopyWith<LoadingError> get copyWith =>
throw _privateConstructorUsedError;
}
@ -43,6 +45,8 @@ class _$LoadingErrorCopyWithImpl<$Res, $Val extends LoadingError>
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of LoadingError
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -81,6 +85,8 @@ class __$$LoadingErrorImplCopyWithImpl<$Res>
_$LoadingErrorImpl _value, $Res Function(_$LoadingErrorImpl) _then)
: super(_value, _then);
/// Create a copy of LoadingError
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -129,7 +135,9 @@ class _$LoadingErrorImpl implements _LoadingError {
@override
int get hashCode => Object.hash(runtimeType, message, allowRetry);
@JsonKey(ignore: true)
/// Create a copy of LoadingError
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$LoadingErrorImplCopyWith<_$LoadingErrorImpl> get copyWith =>
@ -145,8 +153,11 @@ abstract class _LoadingError implements LoadingError {
String get message;
@override
bool get allowRetry;
/// Create a copy of LoadingError
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$LoadingErrorImplCopyWith<_$LoadingErrorImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -22,8 +22,12 @@ LoadableSaveContext _$LoadableSaveContextFromJson(Map<String, dynamic> json) {
mixin _$LoadableSaveContext {
int get timestamp => throw _privateConstructorUsedError;
/// Serializes this LoadableSaveContext to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of LoadableSaveContext
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$LoadableSaveContextCopyWith<LoadableSaveContext> get copyWith =>
throw _privateConstructorUsedError;
}
@ -47,6 +51,8 @@ class _$LoadableSaveContextCopyWithImpl<$Res, $Val extends LoadableSaveContext>
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of LoadableSaveContext
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -80,6 +86,8 @@ class __$$LoadableSaveContextImplCopyWithImpl<$Res>
$Res Function(_$LoadableSaveContextImpl) _then)
: super(_value, _then);
/// Create a copy of LoadableSaveContext
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -119,11 +127,13 @@ class _$LoadableSaveContextImpl extends _LoadableSaveContext {
other.timestamp == timestamp));
}
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, timestamp);
@JsonKey(ignore: true)
/// Create a copy of LoadableSaveContext
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$LoadableSaveContextImplCopyWith<_$LoadableSaveContextImpl> get copyWith =>
@ -148,8 +158,11 @@ abstract class _LoadableSaveContext extends LoadableSaveContext {
@override
int get timestamp;
/// Create a copy of LoadableSaveContext
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$LoadableSaveContextImplCopyWith<_$LoadableSaveContextImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -9,7 +9,7 @@ part of 'loadable_save_context.dart';
_$LoadableSaveContextImpl _$$LoadableSaveContextImplFromJson(
Map<String, dynamic> json) =>
_$LoadableSaveContextImpl(
timestamp: json['timestamp'] as int,
timestamp: (json['timestamp'] as num).toInt(),
);
Map<String, dynamic> _$$LoadableSaveContextImplToJson(

View File

@ -1,8 +1,11 @@
import 'package:flutter/material.dart';
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
import 'package:provider/provider.dart';
import '../../../api/mhsl/breaker/getBreakers/getBreakersResponse.dart';
import '../../../model/breakers/Breaker.dart';
import '../../../model/chatList/chatListProps.dart';
import '../../../storage/base/settingsProvider.dart';
import '../../../view/pages/files/files.dart';
import '../../../view/pages/more/roomplan/roomplan.dart';
import '../../../view/pages/talk/chatList.dart';
@ -12,38 +15,115 @@ import 'gradeAverages/view/grade_averages_view.dart';
import 'holidays/view/holidays_view.dart';
import 'marianumMessage/view/marianum_message_list_view.dart';
import 'package:badges/badges.dart' as badges;
class AppModule {
Modules module;
String name;
IconData icon;
Widget Function() icon;
BreakerArea breakerArea;
Widget Function() create;
AppModule(this.name, this.icon, this.create);
AppModule(this.module, {required this.name, required this.icon, this.breakerArea = BreakerArea.global, required this.create});
static Map<Modules, AppModule> modules() => {
Modules.timetable: AppModule('Vertretung', Icons.calendar_month, Timetable.new),
Modules.talk: AppModule('Talk', Icons.chat, ChatList.new),
Modules.files: AppModule('Files', Icons.folder, Files.new),
Modules.marianumMessage: AppModule('Marianum Message', Icons.newspaper, MarianumMessageListView.new),
Modules.roomPlan: AppModule('Raumplan', Icons.location_pin, Roomplan.new),
Modules.gradeAveragesCalculator: AppModule('Notendurschnittsrechner', Icons.calculate, GradeAveragesView.new),
Modules.holidays: AppModule('Schulferien', Icons.flight, HolidaysView.new),
};
static Map<Modules, AppModule> modules(BuildContext context, { showFiltered = false }) {
var settings = Provider.of<SettingsProvider>(context, listen: false);
var available = {
Modules.timetable: AppModule(
Modules.timetable,
name: 'Vertretung',
icon: () => Icon(Icons.calendar_month),
breakerArea: BreakerArea.timetable,
create: Timetable.new,
),
Modules.talk: AppModule(
Modules.talk,
name: 'Talk',
icon: () => Consumer<ChatListProps>(
builder: (context, value, child) {
if(value.primaryLoading()) return Icon(Icons.chat);
var messages = value.getRoomsResponse.data.map((e) => e.unreadMessages).reduce((a, b) => a+b);
return badges.Badge(
showBadge: messages > 0,
position: badges.BadgePosition.topEnd(top: -3, end: -3),
stackFit: StackFit.loose,
badgeStyle: badges.BadgeStyle(
padding: const EdgeInsets.all(3),
badgeColor: Theme.of(context).primaryColor,
elevation: 1,
),
badgeContent: Text('$messages', style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold)),
child: Icon(Icons.chat),
);
},
),
breakerArea: BreakerArea.talk,
create: ChatList.new,
),
Modules.files: AppModule(
Modules.files,
name: 'Files',
icon: () => Icon(Icons.folder),
breakerArea: BreakerArea.files,
create: Files.new,
),
Modules.marianumMessage: AppModule(
Modules.marianumMessage,
name: 'Marianum Message',
icon: () => Icon(Icons.newspaper),
breakerArea: BreakerArea.more,
create: MarianumMessageListView.new,
),
Modules.roomPlan: AppModule(
Modules.roomPlan,
name: 'Raumplan',
icon: () => Icon(Icons.location_pin),
breakerArea: BreakerArea.more,
create: Roomplan.new,
),
Modules.gradeAveragesCalculator: AppModule(
Modules.gradeAveragesCalculator,
name: 'Notendurschnittsrechner',
icon: () => Icon(Icons.calculate),
breakerArea: BreakerArea.more,
create: GradeAveragesView.new,
),
Modules.holidays: AppModule(
Modules.holidays,
name: 'Schulferien',
icon: () => Icon(Icons.flight),
breakerArea: BreakerArea.more,
create: HolidaysView.new,
),
};
static AppModule getModule(Modules module) => modules()[module]!;
if(!showFiltered) available.removeWhere((key, value) => settings.val().modulesSettings.hiddenModules.contains(key));
Widget toListTile(BuildContext context) => ListTile(
leading: CenteredLeading(Icon(icon)),
return { for (var element in settings.val().modulesSettings.moduleOrder.where((element) => available.containsKey(element))) element : available[element]! };
}
static List<AppModule> getBottomBarModules(BuildContext context) => modules(context).values.toList().getRange(0, 3).toList();
static List<AppModule> getOverhangModules(BuildContext context) => modules(context).values.skip(3).toList();
Widget toListTile(BuildContext context, {Key? key, bool isReorder = false, Function()? onVisibleChange, bool isVisible = true}) => ListTile(
key: key,
leading: CenteredLeading(icon()),
title: Text(name),
onTap: () => pushScreen(context, withNavBar: false, screen: create()),
trailing: const Icon(Icons.arrow_right),
onTap: isReorder ? null : () => pushScreen(context, withNavBar: false, screen: create()),
trailing: isReorder
? Row(mainAxisSize: MainAxisSize.min, children: [
IconButton(onPressed: onVisibleChange, icon: Icon(isVisible ? Icons.visibility_outlined : Icons.visibility_off_outlined)),
Icon(Icons.drag_handle_outlined)
])
: const Icon(Icons.arrow_right),
);
PersistentTabConfig toBottomTab(BuildContext context, {Widget Function(IconData icon)? itemBuilder}) => PersistentTabConfig(
screen: Breaker(breaker: BreakerArea.global, child: create()),
PersistentTabConfig toBottomTab(BuildContext context, {Widget Function(IconData icon)? iconBuilder}) => PersistentTabConfig(
screen: Breaker(breaker: breakerArea, child: create()),
item: ItemConfig(
activeForegroundColor: Theme.of(context).primaryColor,
inactiveForegroundColor: Theme.of(context).colorScheme.secondary,
icon: itemBuilder == null ? Icon(icon) : itemBuilder(icon),
icon: icon(),
title: name
),
);

View File

@ -24,8 +24,12 @@ mixin _$GradeAveragesState {
throw _privateConstructorUsedError;
List<int> get grades => throw _privateConstructorUsedError;
/// Serializes this GradeAveragesState to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of GradeAveragesState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$GradeAveragesStateCopyWith<GradeAveragesState> get copyWith =>
throw _privateConstructorUsedError;
}
@ -49,6 +53,8 @@ class _$GradeAveragesStateCopyWithImpl<$Res, $Val extends GradeAveragesState>
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of GradeAveragesState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -87,6 +93,8 @@ class __$$GradeAveragesStateImplCopyWithImpl<$Res>
$Res Function(_$GradeAveragesStateImpl) _then)
: super(_value, _then);
/// Create a copy of GradeAveragesState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -141,12 +149,14 @@ class _$GradeAveragesStateImpl implements _GradeAveragesState {
const DeepCollectionEquality().equals(other._grades, _grades));
}
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType, gradingSystem, const DeepCollectionEquality().hash(_grades));
@JsonKey(ignore: true)
/// Create a copy of GradeAveragesState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$GradeAveragesStateImplCopyWith<_$GradeAveragesStateImpl> get copyWith =>
@ -173,8 +183,11 @@ abstract class _GradeAveragesState implements GradeAveragesState {
GradeAveragesGradingSystem get gradingSystem;
@override
List<int> get grades;
/// Create a copy of GradeAveragesState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$GradeAveragesStateImplCopyWith<_$GradeAveragesStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -11,7 +11,9 @@ _$GradeAveragesStateImpl _$$GradeAveragesStateImplFromJson(
_$GradeAveragesStateImpl(
gradingSystem: $enumDecode(
_$GradeAveragesGradingSystemEnumMap, json['gradingSystem']),
grades: (json['grades'] as List<dynamic>).map((e) => e as int).toList(),
grades: (json['grades'] as List<dynamic>)
.map((e) => (e as num).toInt())
.toList(),
);
Map<String, dynamic> _$$GradeAveragesStateImplToJson(

View File

@ -24,8 +24,12 @@ mixin _$HolidaysState {
bool get showDisclaimer => throw _privateConstructorUsedError;
List<Holiday> get holidays => throw _privateConstructorUsedError;
/// Serializes this HolidaysState to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of HolidaysState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$HolidaysStateCopyWith<HolidaysState> get copyWith =>
throw _privateConstructorUsedError;
}
@ -50,6 +54,8 @@ class _$HolidaysStateCopyWithImpl<$Res, $Val extends HolidaysState>
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of HolidaysState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -94,6 +100,8 @@ class __$$HolidaysStateImplCopyWithImpl<$Res>
_$HolidaysStateImpl _value, $Res Function(_$HolidaysStateImpl) _then)
: super(_value, _then);
/// Create a copy of HolidaysState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -171,12 +179,14 @@ class _$HolidaysStateImpl
const DeepCollectionEquality().equals(other._holidays, _holidays));
}
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, showPastHolidays, showDisclaimer,
const DeepCollectionEquality().hash(_holidays));
@JsonKey(ignore: true)
/// Create a copy of HolidaysState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$HolidaysStateImplCopyWith<_$HolidaysStateImpl> get copyWith =>
@ -205,8 +215,11 @@ abstract class _HolidaysState implements HolidaysState {
bool get showDisclaimer;
@override
List<Holiday> get holidays;
/// Create a copy of HolidaysState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$HolidaysStateImplCopyWith<_$HolidaysStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}
@ -224,8 +237,12 @@ mixin _$Holiday {
String get name => throw _privateConstructorUsedError;
String get slug => throw _privateConstructorUsedError;
/// Serializes this Holiday to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of Holiday
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$HolidayCopyWith<Holiday> get copyWith => throw _privateConstructorUsedError;
}
@ -253,6 +270,8 @@ class _$HolidayCopyWithImpl<$Res, $Val extends Holiday>
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of Holiday
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -316,6 +335,8 @@ class __$$HolidayImplCopyWithImpl<$Res>
_$HolidayImpl _value, $Res Function(_$HolidayImpl) _then)
: super(_value, _then);
/// Create a copy of Holiday
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -414,12 +435,14 @@ class _$HolidayImpl with DiagnosticableTreeMixin implements _Holiday {
(identical(other.slug, slug) || other.slug == slug));
}
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode =>
Object.hash(runtimeType, start, end, year, stateCode, name, slug);
@JsonKey(ignore: true)
/// Create a copy of Holiday
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$HolidayImplCopyWith<_$HolidayImpl> get copyWith =>
@ -456,8 +479,11 @@ abstract class _Holiday implements Holiday {
String get name;
@override
String get slug;
/// Create a copy of Holiday
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$HolidayImplCopyWith<_$HolidayImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -26,7 +26,7 @@ _$HolidayImpl _$$HolidayImplFromJson(Map<String, dynamic> json) =>
_$HolidayImpl(
start: json['start'] as String,
end: json['end'] as String,
year: json['year'] as int,
year: (json['year'] as num).toInt(),
stateCode: json['stateCode'] as String,
name: json['name'] as String,
slug: json['slug'] as String,

View File

@ -22,8 +22,12 @@ MarianumMessageState _$MarianumMessageStateFromJson(Map<String, dynamic> json) {
mixin _$MarianumMessageState {
MarianumMessageList get messageList => throw _privateConstructorUsedError;
/// Serializes this MarianumMessageState to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of MarianumMessageState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$MarianumMessageStateCopyWith<MarianumMessageState> get copyWith =>
throw _privateConstructorUsedError;
}
@ -50,6 +54,8 @@ class _$MarianumMessageStateCopyWithImpl<$Res,
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of MarianumMessageState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -63,6 +69,8 @@ class _$MarianumMessageStateCopyWithImpl<$Res,
) as $Val);
}
/// Create a copy of MarianumMessageState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$MarianumMessageListCopyWith<$Res> get messageList {
@ -94,6 +102,8 @@ class __$$MarianumMessageStateImplCopyWithImpl<$Res>
$Res Function(_$MarianumMessageStateImpl) _then)
: super(_value, _then);
/// Create a copy of MarianumMessageState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -133,11 +143,13 @@ class _$MarianumMessageStateImpl implements _MarianumMessageState {
other.messageList == messageList));
}
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, messageList);
@JsonKey(ignore: true)
/// Create a copy of MarianumMessageState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$MarianumMessageStateImplCopyWith<_$MarianumMessageStateImpl>
@ -163,8 +175,11 @@ abstract class _MarianumMessageState implements MarianumMessageState {
@override
MarianumMessageList get messageList;
/// Create a copy of MarianumMessageState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$MarianumMessageStateImplCopyWith<_$MarianumMessageStateImpl>
get copyWith => throw _privateConstructorUsedError;
}
@ -178,8 +193,12 @@ mixin _$MarianumMessageList {
String get base => throw _privateConstructorUsedError;
List<MarianumMessage> get messages => throw _privateConstructorUsedError;
/// Serializes this MarianumMessageList to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of MarianumMessageList
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$MarianumMessageListCopyWith<MarianumMessageList> get copyWith =>
throw _privateConstructorUsedError;
}
@ -203,6 +222,8 @@ class _$MarianumMessageListCopyWithImpl<$Res, $Val extends MarianumMessageList>
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of MarianumMessageList
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -241,6 +262,8 @@ class __$$MarianumMessageListImplCopyWithImpl<$Res>
$Res Function(_$MarianumMessageListImpl) _then)
: super(_value, _then);
/// Create a copy of MarianumMessageList
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -294,12 +317,14 @@ class _$MarianumMessageListImpl implements _MarianumMessageList {
const DeepCollectionEquality().equals(other._messages, _messages));
}
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType, base, const DeepCollectionEquality().hash(_messages));
@JsonKey(ignore: true)
/// Create a copy of MarianumMessageList
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$MarianumMessageListImplCopyWith<_$MarianumMessageListImpl> get copyWith =>
@ -327,8 +352,11 @@ abstract class _MarianumMessageList implements MarianumMessageList {
String get base;
@override
List<MarianumMessage> get messages;
/// Create a copy of MarianumMessageList
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$MarianumMessageListImplCopyWith<_$MarianumMessageListImpl> get copyWith =>
throw _privateConstructorUsedError;
}
@ -343,8 +371,12 @@ mixin _$MarianumMessage {
String get date => throw _privateConstructorUsedError;
String get url => throw _privateConstructorUsedError;
/// Serializes this MarianumMessage to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of MarianumMessage
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$MarianumMessageCopyWith<MarianumMessage> get copyWith =>
throw _privateConstructorUsedError;
}
@ -368,6 +400,8 @@ class _$MarianumMessageCopyWithImpl<$Res, $Val extends MarianumMessage>
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of MarianumMessage
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -411,6 +445,8 @@ class __$$MarianumMessageImplCopyWithImpl<$Res>
_$MarianumMessageImpl _value, $Res Function(_$MarianumMessageImpl) _then)
: super(_value, _then);
/// Create a copy of MarianumMessage
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -466,11 +502,13 @@ class _$MarianumMessageImpl implements _MarianumMessage {
(identical(other.url, url) || other.url == url));
}
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, name, date, url);
@JsonKey(ignore: true)
/// Create a copy of MarianumMessage
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$MarianumMessageImplCopyWith<_$MarianumMessageImpl> get copyWith =>
@ -500,8 +538,11 @@ abstract class _MarianumMessage implements MarianumMessage {
String get date;
@override
String get url;
/// Create a copy of MarianumMessage
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$MarianumMessageImplCopyWith<_$MarianumMessageImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -4,6 +4,7 @@ import 'package:json_annotation/json_annotation.dart';
import '../devTools/devToolsSettings.dart';
import '../file/fileSettings.dart';
import '../fileView/fileViewSettings.dart';
import '../general/modulesSettings.dart';
import '../holidays/holidaysSettings.dart';
import '../notification/notificationSettings.dart';
import '../talk/talkSettings.dart';
@ -20,6 +21,7 @@ class Settings {
ThemeMode appTheme;
bool devToolsEnabled;
ModulesSettings modulesSettings;
TimetableSettings timetableSettings;
TalkSettings talkSettings;
FileSettings fileSettings;
@ -31,6 +33,7 @@ class Settings {
Settings({
required this.appTheme,
required this.devToolsEnabled,
required this.modulesSettings,
required this.timetableSettings,
required this.talkSettings,
required this.fileSettings,

View File

@ -9,6 +9,8 @@ part of 'settings.dart';
Settings _$SettingsFromJson(Map<String, dynamic> json) => Settings(
appTheme: Settings._themeFromJson(json['appTheme'] as String),
devToolsEnabled: json['devToolsEnabled'] as bool,
modulesSettings: ModulesSettings.fromJson(
json['modulesSettings'] as Map<String, dynamic>),
timetableSettings: TimetableSettings.fromJson(
json['timetableSettings'] as Map<String, dynamic>),
talkSettings:
@ -28,6 +30,7 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) => Settings(
Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
'appTheme': Settings._themeToJson(instance.appTheme),
'devToolsEnabled': instance.devToolsEnabled,
'modulesSettings': instance.modulesSettings.toJson(),
'timetableSettings': instance.timetableSettings.toJson(),
'talkSettings': instance.talkSettings.toJson(),
'fileSettings': instance.fileSettings.toJson(),

View File

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:easy_debounce/easy_debounce.dart';
@ -13,6 +14,8 @@ class SettingsProvider extends ChangeNotifier {
late SharedPreferences _storage;
late Settings _settings = DefaultSettings.get();
final Completer<void> _populated = Completer();
Settings val({bool write = false}) {
if(write) {
notifyListeners();
@ -56,6 +59,7 @@ class SettingsProvider extends ChangeNotifier {
}
notifyListeners();
_populated.complete();
}
Future<void> update() async {
@ -77,4 +81,8 @@ class SettingsProvider extends ChangeNotifier {
return mergedMap;
}
Future<void> waitForPopulation() async {
await _populated.future;
}
}

View File

@ -0,0 +1,19 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../state/app/modules/app_modules.dart';
part 'modulesSettings.g.dart';
@JsonSerializable()
class ModulesSettings {
List<Modules> moduleOrder;
List<Modules> hiddenModules;
ModulesSettings({
required this.moduleOrder,
required this.hiddenModules
});
factory ModulesSettings.fromJson(Map<String, dynamic> json) => _$ModulesSettingsFromJson(json);
Map<String, dynamic> toJson() => _$ModulesSettingsToJson(this);
}

View File

@ -0,0 +1,35 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'modulesSettings.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ModulesSettings _$ModulesSettingsFromJson(Map<String, dynamic> json) =>
ModulesSettings(
moduleOrder: (json['moduleOrder'] as List<dynamic>)
.map((e) => $enumDecode(_$ModulesEnumMap, e))
.toList(),
hiddenModules: (json['hiddenModules'] as List<dynamic>)
.map((e) => $enumDecode(_$ModulesEnumMap, e))
.toList(),
);
Map<String, dynamic> _$ModulesSettingsToJson(ModulesSettings instance) =>
<String, dynamic>{
'moduleOrder':
instance.moduleOrder.map((e) => _$ModulesEnumMap[e]!).toList(),
'hiddenModules':
instance.hiddenModules.map((e) => _$ModulesEnumMap[e]!).toList(),
};
const _$ModulesEnumMap = {
Modules.timetable: 'timetable',
Modules.talk: 'talk',
Modules.files: 'files',
Modules.marianumMessage: 'marianumMessage',
Modules.roomPlan: 'roomPlan',
Modules.gradeAveragesCalculator: 'gradeAveragesCalculator',
Modules.holidays: 'holidays',
};

View File

@ -1,12 +1,18 @@
import 'package:json_annotation/json_annotation.dart';
import '../../view/pages/timetable/timetableNameMode.dart';
part 'timetableSettings.g.dart';
@JsonSerializable()
class TimetableSettings {
bool connectDoubleLessons;
TimetableNameMode timetableNameMode;
TimetableSettings({required this.connectDoubleLessons});
TimetableSettings({
required this.connectDoubleLessons,
required this.timetableNameMode
});
factory TimetableSettings.fromJson(Map<String, dynamic> json) => _$TimetableSettingsFromJson(json);
Map<String, dynamic> toJson() => _$TimetableSettingsToJson(this);

View File

@ -9,9 +9,19 @@ part of 'timetableSettings.dart';
TimetableSettings _$TimetableSettingsFromJson(Map<String, dynamic> json) =>
TimetableSettings(
connectDoubleLessons: json['connectDoubleLessons'] as bool,
timetableNameMode:
$enumDecode(_$TimetableNameModeEnumMap, json['timetableNameMode']),
);
Map<String, dynamic> _$TimetableSettingsToJson(TimetableSettings instance) =>
<String, dynamic>{
'connectDoubleLessons': instance.connectDoubleLessons,
'timetableNameMode':
_$TimetableNameModeEnumMap[instance.timetableNameMode]!,
};
const _$TimetableNameModeEnumMap = {
TimetableNameMode.name: 'name',
TimetableNameMode.longName: 'longName',
TimetableNameMode.alternateName: 'alternateName',
};

View File

@ -1,26 +1,21 @@
import 'package:flutter/material.dart';
import '../widget/dropdownDisplay.dart';
class AppTheme {
static ThemeModeDisplay getDisplayOptions(ThemeMode theme) {
static DropdownDisplay getDisplayOptions(ThemeMode theme) {
switch(theme) {
case ThemeMode.system:
return ThemeModeDisplay(icon: Icons.auto_fix_high_outlined, displayName: 'Systemvorgabe');
return DropdownDisplay(icon: Icons.auto_fix_high_outlined, displayName: 'Systemvorgabe');
case ThemeMode.light:
return ThemeModeDisplay(icon: Icons.wb_sunny_outlined, displayName: 'Hell');
return DropdownDisplay(icon: Icons.wb_sunny_outlined, displayName: 'Hell');
case ThemeMode.dark:
return ThemeModeDisplay(icon: Icons.dark_mode_outlined, displayName: 'Dunkel');
return DropdownDisplay(icon: Icons.dark_mode_outlined, displayName: 'Dunkel');
}
}
static bool isDarkMode(BuildContext context) => Theme.of(context).brightness == Brightness.dark;
}
class ThemeModeDisplay {
final IconData icon;
final String displayName;
ThemeModeDisplay({required this.icon, required this.displayName});
}

View File

@ -5,7 +5,7 @@ import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:loader_overlay/loader_overlay.dart';
import 'package:package_info/package_info.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart';
import 'package:badges/badges.dart' as badges;
@ -70,7 +70,7 @@ class _FeedbackDialogState extends State<FeedbackDialog> {
decoration: InputDecoration(
border: const OutlineInputBorder(),
label: const Text('Feedback und Verbesserungen'),
errorText: _textFieldEmpty ? 'Bitte gib eine Beschreibung an???' : null,
errorText: _textFieldEmpty ? 'Bitte gib eine Beschreibung an!' : null,
),
minLines: 4,
maxLines: 7,

View File

@ -3,76 +3,124 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:in_app_review/in_app_review.dart';
import 'package:provider/provider.dart';
import '../../extensions/renderNotNull.dart';
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
import '../../state/app/modules/app_modules.dart';
import '../../storage/base/settingsProvider.dart';
import '../../widget/centeredLeading.dart';
import '../../widget/infoDialog.dart';
import '../settings/defaultSettings.dart';
import '../settings/settings.dart';
import 'more/feedback/feedbackDialog.dart';
import 'more/share/selectShareTypeDialog.dart';
class Overhang extends StatelessWidget {
class Overhang extends StatefulWidget {
const Overhang({super.key});
@override
Widget build(BuildContext context) => Scaffold(
State<Overhang> createState() => _OverhangState();
}
class _OverhangState extends State<Overhang> {
bool editMode = false;
@override
Widget build(BuildContext context) => Consumer<SettingsProvider>(builder: (context, settings, child) => Scaffold(
appBar: AppBar(
title: const Text('Mehr'),
actions: [
IconButton(onPressed: () => pushScreen(context, screen: const Settings(), withNavBar: false), icon: const Icon(Icons.settings))
if(editMode) IconButton(
onPressed: settings.val().modulesSettings.toJson().toString() != DefaultSettings.get().modulesSettings.toJson().toString()
? () => settings.val(write: true).modulesSettings = DefaultSettings.get().modulesSettings
: null,
icon: Icon(Icons.undo_outlined)
),
IconButton(onPressed: () => setState(() => editMode = !editMode), icon: Icon(Icons.edit_note_outlined), color: editMode ? Theme.of(context).primaryColor : null),
IconButton(onPressed: editMode ? null : () => pushScreen(context, screen: const Settings(), withNavBar: false), icon: const Icon(Icons.settings)),
],
),
body: ListView(
children: [
AppModule.getModule(Modules.marianumMessage).toListTile(context),
AppModule.getModule(Modules.roomPlan).toListTile(context),
AppModule.getModule(Modules.gradeAveragesCalculator).toListTile(context),
AppModule.getModule(Modules.holidays).toListTile(context),
body: editMode ? _sorting() : _overhang(),
));
const Divider(),
Widget _sorting() => Consumer<SettingsProvider>(builder: (context, settings, child) {
void changeVisibility(Modules module) {
var hidden = settings.val(write: true).modulesSettings.hiddenModules;
hidden.contains(module) ? hidden.remove(module) : (hidden.length < 3 ? hidden.add(module) : null);
}
ListTile(
return ReorderableListView(
header: const Center(
heightFactor: 2,
child: Text('Halte und ziehe einen Eintrag, um ihn zu verschieben.\nEs können 3 Bereiche ausgeblendet werden.', textAlign: TextAlign.center)
),
children: AppModule.modules(context, showFiltered: true)
.map((key, value) => MapEntry(key, value.toListTile(
context,
key: Key(key.name),
isReorder: true,
onVisibleChange: () => changeVisibility(key),
isVisible: !settings.val().modulesSettings.hiddenModules.contains(key)
)))
.values
.toList(),
onReorder: (oldIndex, newIndex) {
if (newIndex > oldIndex) newIndex -= 1;
var order = settings.val().modulesSettings.moduleOrder.toList();
final movedModule = order.removeAt(oldIndex);
order.insert(newIndex, movedModule);
settings.val(write: true).modulesSettings.moduleOrder = order;
}
);
});
Widget _overhang() => ListView(
children: [
...AppModule.getOverhangModules(context).map((e) => e.toListTile(context)),
const Divider(),
ListTile(
leading: const Icon(Icons.share_outlined),
title: const Text('Teile die App'),
subtitle: const Text('Mit Freunden und deiner Klasse teilen'),
trailing: const Icon(Icons.arrow_right),
onTap: () => showDialog(context: context, builder: (context) => const SelectShareTypeDialog())
),
FutureBuilder(
future: InAppReview.instance.isAvailable(),
builder: (context, snapshot) {
if(!snapshot.hasData) return const SizedBox.shrink();
),
FutureBuilder(
future: InAppReview.instance.isAvailable(),
builder: (context, snapshot) {
if(!snapshot.hasData) return const SizedBox.shrink();
String? getPlatformStoreName() {
if(Platform.isAndroid) return 'Play store';
if(Platform.isIOS) return 'App store';
return null;
}
String? getPlatformStoreName() {
if(Platform.isAndroid) return 'Play store';
if(Platform.isIOS) return 'App store';
return null;
}
return ListTile(
leading: const CenteredLeading(Icon(Icons.star_rate_outlined)),
title: const Text('App bewerten'),
subtitle: getPlatformStoreName().wrapNullable((data) => Text('Im $data')),
trailing: const Icon(Icons.arrow_right),
onTap: () {
InAppReview.instance.openStoreListing(appStoreId: '6458789560').then(
(value) => InfoDialog.show(context, 'Vielen Dank!'),
onError: (error) => InfoDialog.show(context, error.toString())
);
},
);
},
),
ListTile(
leading: const CenteredLeading(Icon(Icons.feedback_outlined)),
title: const Text('Du hast eine Idee?'),
subtitle: const Text('Fehler und Verbessungsvorschläge'),
trailing: const Icon(Icons.arrow_right),
onTap: () => pushScreen(context, withNavBar: false, screen: const FeedbackDialog()),
),
],
),
return ListTile(
leading: const CenteredLeading(Icon(Icons.star_rate_outlined)),
title: const Text('App bewerten'),
subtitle: getPlatformStoreName().wrapNullable((data) => Text('Im $data')),
trailing: const Icon(Icons.arrow_right),
onTap: () {
InAppReview.instance.openStoreListing(appStoreId: '6458789560').then(
(value) => InfoDialog.show(context, 'Vielen Dank!'),
onError: (error) => InfoDialog.show(context, error.toString())
);
},
);
},
),
ListTile(
leading: const CenteredLeading(Icon(Icons.feedback_outlined)),
title: const Text('Du hast eine Idee?'),
subtitle: const Text('Fehler und Verbessungsvorschläge'),
trailing: const Icon(Icons.arrow_right),
onTap: () => pushScreen(context, withNavBar: false, screen: const FeedbackDialog()),
),
],
);
}

View File

@ -130,7 +130,7 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
emojis.EmojiPicker(
config: emojis.Config(
height: 256,
swapCategoryAndBottomBar: true,
// swapCategoryAndBottomBar: true, // TODO this property is no longer supported, need to find an replacement
emojiViewConfig: emojis.EmojiViewConfig(
backgroundColor: Theme.of(context).canvasColor,
recentsLimit: 67,
@ -148,7 +148,7 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
),
searchViewConfig: emojis.SearchViewConfig(
backgroundColor: Theme.of(context).dividerColor,
buttonColor: Theme.of(context).dividerColor,
// buttonColor: Theme.of(context).dividerColor, // TODO property no longer supported
hintText: 'Suchen',
buttonIconColor: Colors.white,
),
@ -282,14 +282,15 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
if(!widget.bubbleData.isReplyable) return;
var dx = details.delta.dx - _dragStartPosition.dx;
setState(() {
_position = (_position.dx + dx).abs() > 30 ? Offset(_position.dx, 0) : Offset(_position.dx + dx, 0);
_position = (_position.dx + dx).abs() > 60 ? Offset(_position.dx, 0) : Offset(_position.dx + dx, 0);
});
},
onHorizontalDragEnd: (DragEndDetails details) {
var isAction = _position.dx.abs() > 50;
setState(() {
_position = const Offset(0, 0);
});
if(widget.bubbleData.isReplyable) {
if(widget.bubbleData.isReplyable && isAction) {
Provider.of<ChatProps>(context, listen: false).setReferenceMessageId(widget.bubbleData.id, context, widget.chatData.token);
}
},

View File

@ -36,8 +36,8 @@ class ChatMessage {
);
}
return CachedNetworkImage(
errorWidget: (context, url, error) => Padding(padding: const EdgeInsets.only(top: 10), child: Row(
return Padding(padding: const EdgeInsets.only(top: 5), child: CachedNetworkImage(
errorWidget: (context, url, error) => Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
@ -46,14 +46,14 @@ class ChatMessage {
Flexible(child: Text(file!.name, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontWeight: FontWeight.bold))),
const SizedBox(width: 10),
],
)),
),
alignment: Alignment.center,
placeholder: (context, url) => const Padding(padding: EdgeInsets.all(10), child: CircularProgressIndicator()),
placeholder: (context, url) => const Padding(padding: EdgeInsets.all(15), child: SizedBox(width: 50, child: LinearProgressIndicator())),
fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
errorListener: (value) {},
imageUrl: 'https://${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().full()}/index.php/core/preview?fileId=${file!.id}&x=100&y=-1&a=1',
);
));
}
Future<void> onOpen(LinkableElement link) async {

View 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;
}
}

View File

@ -2,23 +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 'viewCustomTimetableEvents.dart';
class Timetable extends StatefulWidget {
@ -33,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();
});
@ -55,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: () {
@ -72,7 +65,6 @@ class _TimetableState extends State<Timetable> {
title = 'Kalendereintrag hinzufügen';
icon = const Icon(Icons.add);
case CalendarActions.viewEvents:
default:
title = 'Kalendereinträge anzeigen';
icon = const Icon(Icons.perm_contact_calendar_outlined);
}
@ -119,61 +111,19 @@ class _TimetableState extends State<Timetable> {
if(value.primaryLoading()) return const LoadingSpinner();
var holidays = value.getHolidaysResponse;
return RefreshIndicator(
child: SfCalendar(
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
@ -181,198 +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);
return Appointment(
id: ArbitraryAppointment(webuntis: element),
startTime: startTime,
endTime: endTime,
subject: subjects.result.firstWhere((subject) => subject.id == element.su[0].id).name,
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);
// 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;
}
}

View File

@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
import '../../../widget/dropdownDisplay.dart';
enum TimetableNameMode {
name,
longName,
alternateName
}
class TimetableNameModes {
static DropdownDisplay getDisplayOptions(TimetableNameMode theme) {
switch(theme) {
case TimetableNameMode.name:
return DropdownDisplay(icon: Icons.device_unknown_outlined, displayName: 'Name');
case TimetableNameMode.longName:
return DropdownDisplay(icon: Icons.perm_device_info_outlined, displayName: 'Langname');
case TimetableNameMode.alternateName:
return DropdownDisplay(icon: Icons.on_device_training_outlined, displayName: 'Kurzform');
}
}
}

View File

@ -2,22 +2,38 @@ import 'dart:io';
import 'package:flutter/material.dart';
import '../../state/app/modules/app_modules.dart';
import '../../storage/base/settings.dart';
import '../../storage/devTools/devToolsSettings.dart';
import '../../storage/file/fileSettings.dart';
import '../../storage/fileView/fileViewSettings.dart';
import '../../storage/general/modulesSettings.dart';
import '../../storage/holidays/holidaysSettings.dart';
import '../../storage/notification/notificationSettings.dart';
import '../../storage/talk/talkSettings.dart';
import '../../storage/timetable/timetableSettings.dart';
import '../pages/files/files.dart';
import '../pages/timetable/timetableNameMode.dart';
class DefaultSettings {
static Settings get() => Settings(
appTheme: ThemeMode.system,
devToolsEnabled: false,
modulesSettings: ModulesSettings(
moduleOrder: [
Modules.timetable,
Modules.talk,
Modules.files,
Modules.marianumMessage,
Modules.roomPlan,
Modules.gradeAveragesCalculator,
Modules.holidays
],
hiddenModules: [],
),
timetableSettings: TimetableSettings(
connectDoubleLessons: false,
timetableNameMode: TimetableNameMode.name
),
talkSettings: TalkSettings(
sortFavoritesToTop: true,

View File

@ -1,27 +1,109 @@
import 'package:background_fetch/background_fetch.dart';
import 'package:filesize/filesize.dart';
import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../background_tasks/scheduledTask.dart';
import '../../storage/base/settingsProvider.dart';
import '../../widget/centeredLeading.dart';
import '../../widget/confirmDialog.dart';
import '../../widget/debug/cacheView.dart';
import '../../widget/debug/jsonViewer.dart';
import '../../widget/infoDialog.dart';
class DevToolsSettingsDialog extends StatefulWidget {
class DevToolsSettings extends StatefulWidget {
final SettingsProvider settings;
const DevToolsSettingsDialog({required this.settings, super.key});
const DevToolsSettings({required this.settings, super.key});
@override
State<DevToolsSettingsDialog> createState() => _DevToolsSettingsDialogState();
State<DevToolsSettings> createState() => _DevToolsSettingsState();
}
class _DevToolsSettingsDialogState extends State<DevToolsSettingsDialog> {
class _DevToolsSettingsState extends State<DevToolsSettings> {
@override
Widget build(BuildContext context) => Column(
children: [
ListTile(
leading: const CenteredLeading(Icon(Icons.task_outlined)),
title: const Text('Background app fetch task'),
trailing: const Icon(Icons.arrow_right),
onTap: () {
showDialog(context: context, builder: (context) => AlertDialog(
title: Text('Background fetch task'),
content: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
FutureBuilder(future: BackgroundFetch.status, builder: (context, snapshot) {
if(snapshot.hasData) {
var fetchStatus = switch(snapshot.data) {
BackgroundFetch.STATUS_AVAILABLE => 'STATUS_AVAILABLE, Background updates are available for the app.',
BackgroundFetch.STATUS_DENIED => 'STATUS_DENIED, The user explicitly disabled background behavior for this app or for the whole system.',
BackgroundFetch.STATUS_RESTRICTED => 'STATUS_RESTRICTED, Background updates are unavailable and the user cannot enable them again. For example, this status can occur when parental controls are in effect for the current user.',
_ => 'UNKNOWN',
};
return Text('(${snapshot.data}): $fetchStatus');
}
return LinearProgressIndicator();
}),
const Divider(),
const Text('There is no indicator if the Fetch-API is currently running or not!'),
const Divider(),
FutureBuilder(
future: SharedPreferences.getInstance(),
builder: (context, snapshot) {
if(!snapshot.hasData) return LinearProgressIndicator();
return Text('Last fetch timestamp: ${snapshot.data?.getStringList(ScheduledTask.fetchApiLastRunTimestampKey)?.last ?? 'No entry'}');
},
)
],
),
actions: [
FutureBuilder(future: SharedPreferences.getInstance(), builder: (context, snapshot) {
if(!snapshot.hasData) return LinearProgressIndicator();
return TextButton(
onPressed: () {
InfoDialog.show(
context,
(snapshot.data!.getStringList(ScheduledTask.fetchApiLastRunTimestampKey) ?? []).reversed.join('\n')
);
},
child: Text('Fetch history')
);
}),
TextButton(
onPressed: () => ConfirmDialog(
title: 'Warning',
content: 'Background Fetch worker will be started! This basically happens on every app startup.',
onConfirm: BackgroundFetch.start
).asDialog(context),
child: Text('Fetch-API Start')
),
TextButton(
onPressed: () => ConfirmDialog(
title: 'Warning',
content: 'Background Fetch worker will be terminated. This will result in outdated Information when App is not in foreground!',
onConfirm: BackgroundFetch.stop
).asDialog(context),
child: Text('Fetch-API Stop')
),
TextButton(
onPressed: () => ConfirmDialog(
title: 'Warning',
content: 'Background fetch will run now! This happens in the application layer and does not interact with the Fetch-API!',
confirmButton: 'Run',
onConfirm: ScheduledTask.backgroundFetch
).asDialog(context),
child: Text('Run task manually')
),
TextButton(onPressed: () => Navigator.of(context).pop(), child: Text('Zurück'))
],
));
},
),
ListTile(
leading: const CenteredLeading(Icon(Icons.speed_outlined)),
title: const Text('Performance overlays'),
@ -59,9 +141,9 @@ class _DevToolsSettingsDialogState extends State<DevToolsSettingsDialog> {
),
ListTile(
leading: const CenteredLeading(Icon(Icons.image_outlined)),
title: const Text('Cached Thumbnails löschen'),
subtitle: Text('etwa ${filesize(PaintingBinding.instance.imageCache.currentSizeBytes)}'),
onTap: () {
title: const Text('Thumb-storage'),
subtitle: Text('etwa ${filesize(PaintingBinding.instance.imageCache.currentSizeBytes)}\nLange tippen um zu löschen'),
onLongPress: () {
ConfirmDialog(
title: 'Thumbs cache löschen',
content: 'Alle zwischengespeicherten Bilder werden gelöscht.',
@ -69,7 +151,6 @@ class _DevToolsSettingsDialogState extends State<DevToolsSettingsDialog> {
onConfirm: () => PaintingBinding.instance.imageCache.clear(),
).asDialog(context);
},
trailing: const Icon(Icons.arrow_right),
),
ListTile(
leading: const CenteredLeading(Icon(Icons.settings_applications_outlined)),
@ -80,7 +161,7 @@ class _DevToolsSettingsDialogState extends State<DevToolsSettingsDialog> {
},
onLongPress: () {
ConfirmDialog(
title: 'App-Speicher löschen',
title: 'Einstellungen löschen',
content: 'Alle Einstellungen gehen verloren! Accountdaten sowie App-Daten sind nicht betroffen.',
confirmButton: 'Unwiederruflich Löschen',
onConfirm: () {
@ -112,7 +193,7 @@ class _DevToolsSettingsDialogState extends State<DevToolsSettingsDialog> {
),
ListTile(
leading: const CenteredLeading(Icon(Icons.data_object)),
title: const Text('BLOC State cache'),
title: const Text('BLOC-storage state cache'),
subtitle: const Text('Lange tippen um zu löschen'),
onTap: () {
// Navigator.push(context, MaterialPageRoute(builder: (context) => const CacheView()));
@ -125,7 +206,6 @@ class _DevToolsSettingsDialogState extends State<DevToolsSettingsDialog> {
onConfirm: () => HydratedBloc.storage.clear(),
).asDialog(context);
},
trailing: const Icon(Icons.arrow_right),
),
],
);

View File

@ -2,7 +2,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:package_info/package_info.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -14,8 +14,9 @@ import '../../theming/appTheme.dart';
import '../../widget/centeredLeading.dart';
import '../../widget/confirmDialog.dart';
import '../../widget/debug/cacheView.dart';
import '../pages/timetable/timetableNameMode.dart';
import 'defaultSettings.dart';
import 'devToolsSettingsDialog.dart';
import 'devToolsSettings.dart';
import 'privacyInfo.dart';
class Settings extends StatefulWidget {
@ -95,6 +96,29 @@ class _SettingsState extends State<Settings> {
const Divider(),
ListTile(
leading: const Icon(Icons.abc_outlined),
title: const Text('Fachbezeichnung'),
trailing: DropdownButton<TimetableNameMode>(
value: settings.val().timetableSettings.timetableNameMode,
icon: Icon(Icons.arrow_drop_down),
items: TimetableNameMode.values.map((e) => DropdownMenuItem(
value: e,
enabled: e != settings.val().timetableSettings.timetableNameMode,
child: Row(
children: [
Icon(TimetableNameModes.getDisplayOptions(e).icon),
const SizedBox(width: 10),
Text(TimetableNameModes.getDisplayOptions(e).displayName),
],
),
)).toList(),
onChanged: (value) {
settings.val(write: true).timetableSettings.timetableNameMode = value!;
Provider.of<TimetableProps>(context, listen: false).run(renew: false);
},
)
),
ListTile(
leading: const Icon(Icons.calendar_view_day_outlined),
title: const Text('Doppelstunden zusammenhängend anzeigen'),
@ -131,34 +155,6 @@ class _SettingsState extends State<Settings> {
),
),
const Divider(),
ListTile(
leading: const Icon(Icons.drive_folder_upload_outlined),
title: const Text('Ordner in Dateien nach oben sortieren'),
trailing: Checkbox(
value: settings.val().fileSettings.sortFoldersToTop,
onChanged: (e) {
settings.val(write: true).fileSettings.sortFoldersToTop = e!;
},
),
),
const Divider(),
ListTile(
leading: const Icon(Icons.open_in_new_outlined),
title: const Text('Dateien immer mit Systemdialog öffnen'),
trailing: Checkbox(
value: settings.val().fileViewSettings.alwaysOpenExternally,
onChanged: (e) {
settings.val(write: true).fileViewSettings.alwaysOpenExternally = e!;
},
),
),
const Divider(),
ListTile(
leading: const CenteredLeading(Icon(Icons.notifications_active_outlined)),
title: const Text('Push-Benachrichtigungen aktivieren'),
@ -190,6 +186,30 @@ class _SettingsState extends State<Settings> {
const Divider(),
ListTile(
leading: const Icon(Icons.drive_folder_upload_outlined),
title: const Text('Ordner in Dateien nach oben sortieren'),
trailing: Checkbox(
value: settings.val().fileSettings.sortFoldersToTop,
onChanged: (e) {
settings.val(write: true).fileSettings.sortFoldersToTop = e!;
},
),
),
ListTile(
leading: const Icon(Icons.open_in_new_outlined),
title: const Text('Dateien immer mit Systemdialog öffnen'),
trailing: Checkbox(
value: settings.val().fileViewSettings.alwaysOpenExternally,
onChanged: (e) {
settings.val(write: true).fileViewSettings.alwaysOpenExternally = e!;
},
),
),
const Divider(),
ListTile(
leading: const Icon(Icons.live_help_outlined),
title: const Text('Informationen und Lizenzen'),
@ -245,14 +265,11 @@ class _SettingsState extends State<Settings> {
const Divider(),
Visibility(
visible: !kReleaseMode,
child: ListTile(
leading: const CenteredLeading(Icon(Icons.code)),
title: const Text('Quellcode MarianumMobile/Client'),
subtitle: const Text('GNU GPL v3'),
onTap: () => ConfirmDialog.openBrowser(context, 'https://mhsl.eu/gitea/MarianumMobile/Client'),
),
ListTile(
leading: const CenteredLeading(Icon(Icons.code)),
title: const Text('Quellcode MarianumMobile/Client'),
subtitle: const Text('GNU GPL v3'),
onTap: () => ConfirmDialog.openBrowser(context, 'https://mhsl.eu/gitea/MarianumMobile/Client'),
),
ListTile(
@ -287,7 +304,7 @@ class _SettingsState extends State<Settings> {
Visibility(
visible: settings.val().devToolsEnabled,
child: DevToolsSettingsDialog(settings: settings),
child: DevToolsSettings(settings: settings),
),
],
),

View File

@ -3,7 +3,6 @@ import 'dart:async';
import 'dart:convert';
import 'package:filesize/filesize.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:jiffy/jiffy.dart';
import 'package:localstore/localstore.dart';
@ -41,7 +40,7 @@ class _CacheViewState extends State<CacheView> {
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('Lokaler cache'),
title: const Text('Cache storage'),
),
body: FutureBuilder(
future: files,
@ -58,27 +57,7 @@ class _CacheViewState extends State<CacheView> {
title: Text(filename),
subtitle: Text("${filesize(jsonEncode(element).length * 8)}, ${Jiffy.parseFromMillisecondsSinceEpoch(element['lastupdate']).fromNow()}"),
trailing: const Icon(Icons.arrow_right),
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => JsonViewer(title: filename, data: jsonDecode(element['json'])),));
},
onLongPress: () {
showDialog(context: context, builder: (context) => SimpleDialog(
children: [
const ListTile(
leading: Icon(Icons.delete_forever),
title: Text('Diese Datei löschen'),
),
ListTile(
leading: const Icon(Icons.copy),
title: const Text('Dateitext kopieren'),
onTap: () {
Clipboard.setData(ClipboardData(text: jsonEncode(element)));
Navigator.of(context).pop();
},
)
],
));
},
onTap: () => JsonViewer.asDialog(context, jsonDecode(element['json'])),
);
},
);

View File

@ -0,0 +1,8 @@
import 'package:flutter/material.dart';
class DropdownDisplay {
final IconData icon;
final String displayName;
DropdownDisplay({required this.icon, required this.displayName});
}

View File

@ -7,29 +7,26 @@ class PlaceholderView extends StatelessWidget {
const PlaceholderView({super.key, required this.icon, required this.text, this.button});
@override
Widget build(BuildContext context) => DefaultTextStyle(
style: const TextStyle(),
child: Center(
child: Container(
margin: const EdgeInsets.only(top: 100, left: 20, right: 20),
child: Column(
children: [
Container(
margin: const EdgeInsets.all(30),
child: Icon(icon, color: Colors.grey, size: 60),
),
Text(text,
style: const TextStyle(
fontSize: 20,
color: Colors.grey,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
if(button != null) button!,
],
),
Widget build(BuildContext context) => Scaffold(
body: Center(
child: Container(
margin: const EdgeInsets.only(top: 100, left: 20, right: 20),
child: Column(
children: [
Container(
margin: const EdgeInsets.all(30),
child: Icon(icon, size: 60),
),
Text(
text,
style: const TextStyle(fontSize: 20,),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
if(button != null) button!,
],
),
),
);
),
);
}

View File

@ -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.0.9+35
version: 0.1.2+40
environment:
sdk: '>3.0.0'
@ -30,7 +30,7 @@ environment:
# versions available, run `flutter pub outdated`.
dependency_overrides:
#xml: ^6.2.2
intl: any
dependencies:
flutter:
@ -39,85 +39,85 @@ dependencies:
flutter_localizations:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
flutter_native_splash: ^2.2.14
flutter_login: ^5.0.0
animated_digit: ^3.2.3
async: ^2.11.0
badges: ^3.1.2
better_open_file: ^3.6.5
bloc: ^9.0.0
bottom_sheet: ^4.0.4
bubble: ^1.2.1
http: ^1.1.0
shared_preferences: ^2.0.15
provider: ^6.0.4
jiffy: ^6.1.0
json_annotation: ^4.8.1
localstore: ^1.2.3
intl: ^0.18.0
nextcloud:
git:
url: https://github.com/provokateurin/nextcloud-neon
path: packages/nextcloud
ref: 3683491a94670393e46cbc83ad85b994f7df7481
flutter_launcher_icons: ^0.13.1
pretty_json: ^2.0.0
cached_network_image: ^3.2.3
url_launcher: ^6.1.10
flutter_linkify: ^6.0.0
cached_network_image: ^3.4.1
connectivity_plus: ^6.1.2
crypto: ^3.0.6
cupertino_icons: ^1.0.8
device_info_plus: ^11.2.1
dio: ^4.0.6
easy_debounce: ^2.0.3
emoji_picker_flutter: ^4.3.0
fast_rsa: ^3.7.1
file_picker: ^8.1.7
filesize: ^2.0.1
path_provider: ^2.0.13
better_open_file: ^3.6.4
firebase_core: ^3.10.1
firebase_in_app_messaging: ^0.8.1+1
firebase_messaging: ^15.2.1
flowder:
git:
url: https://github.com/Harsh223/flowder.git
persistent_bottom_nav_bar_v2: ^5.0.0
badges: ^3.0.2
image_picker: ^1.0.0
file_picker: ^8.0.0+1
loader_overlay: ^4.0.0
crypto: ^3.0.3
package_info: ^2.0.2
syncfusion_flutter_calendar: ^24.1.44
async: ^2.11.0
animated_digit: ^3.2.1
syncfusion_flutter_pdfviewer: ^24.1.44
photo_view: ^0.14.0
uuid: ^4.2.2
firebase_messaging: ^14.6.5
firebase_core: ^2.15.0
firebase_in_app_messaging: ^0.7.3+4
flutter_local_notifications: ^17.0.0
fast_rsa: ^3.6.1
share_plus: ^8.0.2
flutter_app_badge: ^2.0.2
flutter_bloc: ^9.0.0
flutter_launcher_icons: ^0.14.3
flutter_linkify: ^6.0.0
flutter_local_notifications: ^18.0.1
flutter_login: ^5.0.0
flutter_native_splash: ^2.4.4
flutter_split_view: ^0.1.2
bottom_sheet: ^4.0.0
device_info_plus: ^9.0.3
flutter_app_badger: ^1.5.0
freezed_annotation: ^2.4.4
http: ^1.3.0
hydrated_bloc: ^10.0.0
image_picker: ^1.1.2
in_app_review: ^2.0.10
jiffy: ^6.2.1
json_annotation: ^4.9.0
loader_overlay: ^5.0.0
localstore: ^1.4.0
nextcloud:
git:
path: packages/nextcloud
ref: 3683491a94670393e46cbc83ad85b994f7df7481
url: https://github.com/provokateurin/nextcloud-neon
package_info_plus: ^8.1.3
path_provider: ^2.1.5
persistent_bottom_nav_bar_v2: ^5.3.1
photo_view: ^0.15.0
pretty_json: ^2.0.0
provider: ^6.1.2
qr_flutter: ^4.1.0
easy_debounce: ^2.0.3
rrule_generator: ^0.7.0+1
rrule: ^0.2.16
time_range_picker: ^2.2.0
in_app_review: ^2.0.8
emoji_picker_flutter: ^2.1.1
bloc: ^8.1.4
flutter_bloc: ^8.1.5
freezed_annotation: ^2.4.1
connectivity_plus: ^6.0.3
hydrated_bloc: ^9.1.5
dio: ^4.0.6
rrule: ^0.2.17
rrule_generator: ^0.9.0
share_plus: ^10.1.4
shared_preferences: ^2.3.5
syncfusion_flutter_calendar: ^28.1.41
syncfusion_flutter_pdfviewer: ^28.1.41
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
background_fetch: ^1.3.7
dev_dependencies:
flutter_test:
sdk: flutter
json_serializable: ^6.7.1
build_runner: ^2.4.9
json_serializable: any
build_runner: any
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^3.0.1
freezed: ^2.5.2
flutter_lints: any
freezed: any
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec