diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..32b90da --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + // Verwendet IntelliSense zum Ermitteln möglicher Attribute. + // Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen. + // Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "app", + "request": "launch", + "type": "dart" + }, + { + "name": "app (profile mode)", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + } + ] +} \ No newline at end of file diff --git a/android.zip b/android.zip new file mode 100644 index 0000000..dc96427 Binary files /dev/null and b/android.zip differ diff --git a/android/.idea/gradle.xml b/android/.idea/gradle.xml index 80f1a96..682acbf 100644 --- a/android/.idea/gradle.xml +++ b/android/.idea/gradle.xml @@ -4,15 +4,21 @@ \ No newline at end of file diff --git a/android/.idea/libraries/Flutter_Plugins.xml b/android/.idea/libraries/Flutter_Plugins.xml deleted file mode 100644 index b0f6971..0000000 --- a/android/.idea/libraries/Flutter_Plugins.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/android/.idea/libraries/Gradle__androidx_activity_activity_1_0_0_aar.xml b/android/.idea/libraries/Gradle__androidx_activity_activity_1_0_0_aar.xml index 06d1f86..de662e1 100644 --- a/android/.idea/libraries/Gradle__androidx_activity_activity_1_0_0_aar.xml +++ b/android/.idea/libraries/Gradle__androidx_activity_activity_1_0_0_aar.xml @@ -1,9 +1,10 @@ - - + + + diff --git a/android/.idea/libraries/Gradle__androidx_annotation_annotation_1_1_0.xml b/android/.idea/libraries/Gradle__androidx_annotation_annotation_1_1_0.xml deleted file mode 100644 index b2158ac..0000000 --- a/android/.idea/libraries/Gradle__androidx_annotation_annotation_1_1_0.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/.idea/libraries/Gradle__androidx_arch_core_core_runtime_2_0_0_aar.xml b/android/.idea/libraries/Gradle__androidx_arch_core_core_runtime_2_0_0_aar.xml index 2809833..1d30de5 100644 --- a/android/.idea/libraries/Gradle__androidx_arch_core_core_runtime_2_0_0_aar.xml +++ b/android/.idea/libraries/Gradle__androidx_arch_core_core_runtime_2_0_0_aar.xml @@ -1,9 +1,10 @@ - - + + + diff --git a/android/.idea/libraries/Gradle__androidx_core_core_1_1_0_aar.xml b/android/.idea/libraries/Gradle__androidx_core_core_1_1_0_aar.xml deleted file mode 100644 index b5d056e..0000000 --- a/android/.idea/libraries/Gradle__androidx_core_core_1_1_0_aar.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/.idea/libraries/Gradle__androidx_customview_customview_1_0_0_aar.xml b/android/.idea/libraries/Gradle__androidx_customview_customview_1_0_0_aar.xml index cbcb46a..ae103ed 100644 --- a/android/.idea/libraries/Gradle__androidx_customview_customview_1_0_0_aar.xml +++ b/android/.idea/libraries/Gradle__androidx_customview_customview_1_0_0_aar.xml @@ -1,9 +1,10 @@ - - + + + diff --git a/android/.idea/libraries/Gradle__androidx_fragment_fragment_1_1_0_aar.xml b/android/.idea/libraries/Gradle__androidx_fragment_fragment_1_1_0_aar.xml index c1e4382..e2d579b 100644 --- a/android/.idea/libraries/Gradle__androidx_fragment_fragment_1_1_0_aar.xml +++ b/android/.idea/libraries/Gradle__androidx_fragment_fragment_1_1_0_aar.xml @@ -1,12 +1,13 @@ - + - - + + + diff --git a/android/.idea/libraries/Gradle__androidx_lifecycle_lifecycle_livedata_2_0_0_aar.xml b/android/.idea/libraries/Gradle__androidx_lifecycle_lifecycle_livedata_2_0_0_aar.xml index 15e56dd..1b458b8 100644 --- a/android/.idea/libraries/Gradle__androidx_lifecycle_lifecycle_livedata_2_0_0_aar.xml +++ b/android/.idea/libraries/Gradle__androidx_lifecycle_lifecycle_livedata_2_0_0_aar.xml @@ -1,9 +1,10 @@ - - + + + diff --git a/android/.idea/libraries/Gradle__androidx_lifecycle_lifecycle_livedata_core_2_0_0_aar.xml b/android/.idea/libraries/Gradle__androidx_lifecycle_lifecycle_livedata_core_2_0_0_aar.xml index 73bb1d5..2116e61 100644 --- a/android/.idea/libraries/Gradle__androidx_lifecycle_lifecycle_livedata_core_2_0_0_aar.xml +++ b/android/.idea/libraries/Gradle__androidx_lifecycle_lifecycle_livedata_core_2_0_0_aar.xml @@ -1,9 +1,10 @@ - - + + + diff --git a/android/.idea/libraries/Gradle__androidx_lifecycle_lifecycle_runtime_2_2_0_aar.xml b/android/.idea/libraries/Gradle__androidx_lifecycle_lifecycle_runtime_2_2_0_aar.xml index a05f202..d835567 100644 --- a/android/.idea/libraries/Gradle__androidx_lifecycle_lifecycle_runtime_2_2_0_aar.xml +++ b/android/.idea/libraries/Gradle__androidx_lifecycle_lifecycle_runtime_2_2_0_aar.xml @@ -1,9 +1,10 @@ - - + + + diff --git a/android/.idea/libraries/Gradle__androidx_lifecycle_lifecycle_viewmodel_2_1_0_aar.xml b/android/.idea/libraries/Gradle__androidx_lifecycle_lifecycle_viewmodel_2_1_0_aar.xml index bcfcd11..ae54ce2 100644 --- a/android/.idea/libraries/Gradle__androidx_lifecycle_lifecycle_viewmodel_2_1_0_aar.xml +++ b/android/.idea/libraries/Gradle__androidx_lifecycle_lifecycle_viewmodel_2_1_0_aar.xml @@ -1,9 +1,10 @@ - - + + + diff --git a/android/.idea/libraries/Gradle__androidx_loader_loader_1_0_0_aar.xml b/android/.idea/libraries/Gradle__androidx_loader_loader_1_0_0_aar.xml index 2f72bdd..2cea050 100644 --- a/android/.idea/libraries/Gradle__androidx_loader_loader_1_0_0_aar.xml +++ b/android/.idea/libraries/Gradle__androidx_loader_loader_1_0_0_aar.xml @@ -1,9 +1,10 @@ - - + + + diff --git a/android/.idea/libraries/Gradle__androidx_savedstate_savedstate_1_0_0_aar.xml b/android/.idea/libraries/Gradle__androidx_savedstate_savedstate_1_0_0_aar.xml index 811dc16..fcdc9b4 100644 --- a/android/.idea/libraries/Gradle__androidx_savedstate_savedstate_1_0_0_aar.xml +++ b/android/.idea/libraries/Gradle__androidx_savedstate_savedstate_1_0_0_aar.xml @@ -1,9 +1,10 @@ - - + + + diff --git a/android/.idea/libraries/Gradle__androidx_versionedparcelable_versionedparcelable_1_1_0_aar.xml b/android/.idea/libraries/Gradle__androidx_versionedparcelable_versionedparcelable_1_1_0_aar.xml deleted file mode 100644 index 944df01..0000000 --- a/android/.idea/libraries/Gradle__androidx_versionedparcelable_versionedparcelable_1_1_0_aar.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/.idea/libraries/Gradle__androidx_viewpager_viewpager_1_0_0_aar.xml b/android/.idea/libraries/Gradle__androidx_viewpager_viewpager_1_0_0_aar.xml index 4f21492..2a6fc1e 100644 --- a/android/.idea/libraries/Gradle__androidx_viewpager_viewpager_1_0_0_aar.xml +++ b/android/.idea/libraries/Gradle__androidx_viewpager_viewpager_1_0_0_aar.xml @@ -1,9 +1,10 @@ - - + + + diff --git a/android/.idea/libraries/Gradle__io_flutter_arm64_v8a_debug_1_0_0_241c87ad800beeab545ab867354d4683d5bfb6ce.xml b/android/.idea/libraries/Gradle__io_flutter_arm64_v8a_debug_1_0_0_241c87ad800beeab545ab867354d4683d5bfb6ce.xml deleted file mode 100644 index 5eb1eaf..0000000 --- a/android/.idea/libraries/Gradle__io_flutter_arm64_v8a_debug_1_0_0_241c87ad800beeab545ab867354d4683d5bfb6ce.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/android/.idea/libraries/Gradle__io_flutter_armeabi_v7a_debug_1_0_0_241c87ad800beeab545ab867354d4683d5bfb6ce.xml b/android/.idea/libraries/Gradle__io_flutter_armeabi_v7a_debug_1_0_0_241c87ad800beeab545ab867354d4683d5bfb6ce.xml deleted file mode 100644 index 466a0f8..0000000 --- a/android/.idea/libraries/Gradle__io_flutter_armeabi_v7a_debug_1_0_0_241c87ad800beeab545ab867354d4683d5bfb6ce.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/android/.idea/libraries/Gradle__io_flutter_flutter_embedding_debug_1_0_0_241c87ad800beeab545ab867354d4683d5bfb6ce.xml b/android/.idea/libraries/Gradle__io_flutter_flutter_embedding_debug_1_0_0_241c87ad800beeab545ab867354d4683d5bfb6ce.xml deleted file mode 100644 index 05d5808..0000000 --- a/android/.idea/libraries/Gradle__io_flutter_flutter_embedding_debug_1_0_0_241c87ad800beeab545ab867354d4683d5bfb6ce.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/android/.idea/libraries/Gradle__io_flutter_x86_64_debug_1_0_0_241c87ad800beeab545ab867354d4683d5bfb6ce.xml b/android/.idea/libraries/Gradle__io_flutter_x86_64_debug_1_0_0_241c87ad800beeab545ab867354d4683d5bfb6ce.xml deleted file mode 100644 index be9188f..0000000 --- a/android/.idea/libraries/Gradle__io_flutter_x86_64_debug_1_0_0_241c87ad800beeab545ab867354d4683d5bfb6ce.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/android/.idea/libraries/Gradle__io_flutter_x86_debug_1_0_0_241c87ad800beeab545ab867354d4683d5bfb6ce.xml b/android/.idea/libraries/Gradle__io_flutter_x86_debug_1_0_0_241c87ad800beeab545ab867354d4683d5bfb6ce.xml deleted file mode 100644 index 4d125f3..0000000 --- a/android/.idea/libraries/Gradle__io_flutter_x86_debug_1_0_0_241c87ad800beeab545ab867354d4683d5bfb6ce.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/android/.idea/misc.xml b/android/.idea/misc.xml index 5500d01..8401cbc 100644 --- a/android/.idea/misc.xml +++ b/android/.idea/misc.xml @@ -3,7 +3,7 @@ - + diff --git a/android/.idea/modules.xml b/android/.idea/modules.xml index 5333642..4cfe785 100644 --- a/android/.idea/modules.xml +++ b/android/.idea/modules.xml @@ -4,8 +4,13 @@ - - + + + + + + + \ No newline at end of file diff --git a/android/.idea/modules/-442492029/android.path_provider.iml b/android/.idea/modules/-442492029/android.path_provider.iml deleted file mode 100644 index 1b6d5cf..0000000 --- a/android/.idea/modules/-442492029/android.path_provider.iml +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/.idea/modules/1049519328/android.shared_preferences.iml b/android/.idea/modules/1049519328/android.shared_preferences.iml deleted file mode 100644 index 5a1a7ce..0000000 --- a/android/.idea/modules/1049519328/android.shared_preferences.iml +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/.idea/modules/android.iml b/android/.idea/modules/android.iml index 616d2d8..4e86d15 100644 --- a/android/.idea/modules/android.iml +++ b/android/.idea/modules/android.iml @@ -3,12 +3,11 @@ - - + diff --git a/android/.idea/modules/app/android.app.iml b/android/.idea/modules/app/android.app.iml index 7ddc274..bf05337 100644 --- a/android/.idea/modules/app/android.app.iml +++ b/android/.idea/modules/app/android.app.iml @@ -4,8 +4,8 @@ @@ -31,7 +31,7 @@ + + + + + + + + - - - - - + + + + + - + - + - - + + - - - + + + - - - - - - + + + + + + + + + + + + \ No newline at end of file diff --git a/android/.idea/runConfigurations.xml b/android/.idea/runConfigurations.xml deleted file mode 100644 index 797acea..0000000 --- a/android/.idea/runConfigurations.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index cfa383a..3c0bd5e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 30 + compileSdkVersion rootProject.ext.compileSdkVersion compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -45,7 +45,7 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "de.cantorgymnasium.meincantor" minSdkVersion 20 - targetSdkVersion 30 + targetSdkVersion rootProject.ext.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 729401f..de345fe 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,9 @@ + android:showWhenLocked="true" android:turnScreenOn="true" + android:windowSoftInputMode="adjustResize"> + + + + + + + + + + + CG + + + + + + + + + + + + + + + diff --git a/assets/images/meincantor-big.png b/assets/images/meincantor-big.png new file mode 100644 index 0000000..4830a8c Binary files /dev/null and b/assets/images/meincantor-big.png differ diff --git a/assets/images/meincantor-big.svg b/assets/images/meincantor-big.svg new file mode 100644 index 0000000..43834c7 --- /dev/null +++ b/assets/images/meincantor-big.svg @@ -0,0 +1,136 @@ + + + + + + + + + + + + + CG + + + + + + + + + + + + + + + diff --git a/lib/Settings/Pages/appearance_settings.dart b/lib/Settings/Pages/appearance_settings.dart index a379840..9360819 100644 --- a/lib/Settings/Pages/appearance_settings.dart +++ b/lib/Settings/Pages/appearance_settings.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class AppearanceSettings extends StatelessWidget { @@ -13,7 +12,7 @@ class AppearanceSettings extends StatelessWidget { ), body: ListView( padding: const EdgeInsets.fromLTRB(5, 5, 5, 5), - children: [], + children: const [], )); } } diff --git a/lib/Settings/Pages/dev_settings.dart b/lib/Settings/Pages/dev_settings.dart index 4e40844..4883132 100644 --- a/lib/Settings/Pages/dev_settings.dart +++ b/lib/Settings/Pages/dev_settings.dart @@ -1,12 +1,29 @@ +import 'package:background_fetch/background_fetch.dart'; import 'package:flutter/material.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:meincantor/background_fetch.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import '../../login.dart'; +import 'package:meincantor/login.dart'; -class DevSettings extends StatelessWidget { +class DevSettings extends StatefulWidget { const DevSettings({Key? key}) : super(key: key); + @override + State createState() => _DevSettingsState(); +} + +class _DevSettingsState extends State { + int _status = 0; + + void _onClickStatus() async { + int status = await BackgroundFetch.status; + print('[BackgroundFetch] status: $status'); + setState(() { + _status = status; + }); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -14,41 +31,98 @@ class DevSettings extends StatelessWidget { title: const Text("Entwickler-Einstellungen"), centerTitle: true, ), - body: ListView( - padding: const EdgeInsets.fromLTRB(20, 20, 20, 20), - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 0, 0), - child: TextField( - decoration: const InputDecoration( - icon: Icon(MdiIcons.keyOutline), - border: OutlineInputBorder(), - labelText: 'MeinCantor API-Schlüssel', - ), - onSubmitted: (String value) async { - SharedPreferences prefs = - await SharedPreferences.getInstance(); - String apiKey = value; - await prefs.setString('api_key', apiKey); - final snackBar = SnackBar( - content: - Text('Neuer API-Schlüssel gesetzt: $apiKey')); - ScaffoldMessenger.of(context).showSnackBar(snackBar); - })), - const Divider(), - Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 0, 0), - child: OutlinedButton( - onPressed: () async { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => Login()), - ); - }, - child: const Text("Benutzerdaten neu laden"), - ) - ) - ], - )); + body: LayoutBuilder(builder: (context, constraints) { + double widgetWidth = constraints.maxWidth; + + int factor; + + if (widgetWidth <= 600) { + factor = 1; + } else if (widgetWidth <= 1400) { + factor = 2; + } else if (widgetWidth <= 2000) { + factor = 3; + } else { + factor = 1; + } + + return Center( + heightFactor: 1, + child: Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width / factor, + ), + child: ListView( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 20), + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(0, 0, 0, 0), + child: TextField( + decoration: const InputDecoration( + icon: Icon(MdiIcons.keyOutline), + border: OutlineInputBorder(), + labelText: 'MeinCantor API-Schlüssel', + ), + onSubmitted: (String value) async { + SharedPreferences prefs = + await SharedPreferences.getInstance(); + String apiKey = value; + await prefs.setString('api_key', apiKey); + final snackBar = SnackBar( + content: Text( + 'Neuer API-Schlüssel gesetzt: $apiKey')); + ScaffoldMessenger.of(context) + .showSnackBar(snackBar); + })), + const Divider(), + Padding( + padding: const EdgeInsets.fromLTRB(0, 0, 0, 0), + child: OutlinedButton( + onPressed: () async { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => Login()), + ); + }, + child: const Text("Benutzerdaten neu laden"), + )), + const Divider(), + Padding( + padding: const EdgeInsets.fromLTRB(0, 0, 0, 0), + child: OutlinedButton( + onPressed: () { + _onClickStatus(); + }, + child: const Text( + "Status der Hintergrundoperation anzeigen"), + )), + const Divider(), + Padding( + padding: const EdgeInsets.fromLTRB(10, 10, 10, 10), + child: Text(_status.toString())), + const Divider(), + Padding( + padding: const EdgeInsets.fromLTRB(0, 0, 0, 0), + child: OutlinedButton( + onPressed: () async { + await backgroundFetchTimetable(); + }, + child: const Text( + "Stundenplan im Hintergrund neu laden"), + )), + const Divider(), + Padding( + padding: const EdgeInsets.fromLTRB(0, 0, 0, 0), + child: OutlinedButton( + onPressed: () async { + await backgroundFetchArticles(); + }, + child: const Text( + "Schülerzeitung im Hintergrund neu laden"), + )), + ], + )), + ); + })); } } diff --git a/lib/Settings/Pages/info_settings.dart b/lib/Settings/Pages/info_settings.dart index f82c1f8..730e44e 100644 --- a/lib/Settings/Pages/info_settings.dart +++ b/lib/Settings/Pages/info_settings.dart @@ -1,6 +1,3 @@ -import 'dart:io'; - -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:meincantor/const.dart'; @@ -17,79 +14,136 @@ class InfoSettings extends StatelessWidget { title: const Text("Informationen"), centerTitle: true, ), - body: ListView( - padding: const EdgeInsets.fromLTRB(5, 5, 5, 5), - children: [ - const ListTile( - leading: Icon(Icons.info_outlined), - title: Text("Version"), - subtitle: Text(version)), - ListTile( - leading: Icon(Icons.person_outlined), - title: Text("Autor"), - subtitle: Text(author), - onTap: () => launch("https://git.cantorgymnasium.de/denyskon"), - ), - ListTile( - leading: const Icon(Icons.source_outlined), - title: const Text("Quellcode"), - subtitle: Linkify( - onOpen: (link) async { - if (await canLaunch(link.url)) { - await launch(link.url); - } else { - throw 'Could not launch $link'; - } - }, - text: "https://git.cantorgymnasium.de/cantortechnik/meincantor-app", - linkStyle: const TextStyle(color: Palette.accent), + body: LayoutBuilder(builder: (context, constraints) { + double widgetWidth = constraints.maxWidth; + + int factor; + + if (widgetWidth <= 600) { + factor = 1; + } else if (widgetWidth <= 1400) { + factor = 2; + } else if (widgetWidth <= 2000) { + factor = 3; + } else { + factor = 1; + } + + return Center( + heightFactor: 1, + child: Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width / factor, ), - ), - ListTile( - leading: const Icon(Icons.settings_backup_restore_outlined), - title: const Text("Änderungsverlauf"), - subtitle: const Text("Was ist neu?"), - onTap: () { - showModalBottomSheet( - isScrollControlled: true, - context: context, - builder: (BuildContext context) { - return SizedBox( - height: 400, - child: Column( - children: [ - AppBar( - title: const Text("Änderungsverlauf"), - ), - const Padding( - padding: EdgeInsets.all(10), - child: Text("1.0 --\nErste Release-Version!"), - ), - ], + child: ListView( + padding: const EdgeInsets.fromLTRB(5, 5, 5, 5), + children: [ + const Padding( + padding: EdgeInsets.all(5), + child: ListTile( + leading: Icon(Icons.info_outlined), + title: Text("Version"), + subtitle: Text(version)), + ), + Padding( + padding: const EdgeInsets.all(5), + child: ListTile( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15.0)), + leading: const Icon(Icons.person_outlined), + title: const Text("Autor"), + subtitle: const Text(author), + onTap: () => + launch("https://git.cantorgymnasium.de/denyskon"), ), - ); - }, - ); - }, - ), - ListTile( - leading: const Icon(Icons.copyright_outlined), - title: const Text("Lizenzen"), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => LicensePage( - applicationIcon: Image.asset( - "assets/images/meincantor_r.png", - height: 64, - width: 64), - applicationVersion: version, - )), - ); - }, - ), - ], - )); + ), + Padding( + padding: const EdgeInsets.all(5), + child: ListTile( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15.0)), + leading: const Icon(Icons.source_outlined), + title: const Text("Quellcode"), + subtitle: Linkify( + onOpen: (link) async { + if (await canLaunch(link.url)) { + await launch(link.url); + } else { + throw 'Could not launch $link'; + } + }, + text: + "https://git.cantorgymnasium.de/cantortechnik/meincantor-app", + linkStyle: const TextStyle(color: Palette.accent), + ), + onTap: () => launch( + "https://git.cantorgymnasium.de/cantortechnik/meincantor-app")), + ), + Padding( + padding: const EdgeInsets.all(5), + child: ListTile( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15.0)), + leading: + const Icon(Icons.settings_backup_restore_outlined), + title: const Text("Änderungsverlauf"), + subtitle: const Text("Was ist neu?"), + onTap: () { + showModalBottomSheet( + isScrollControlled: true, + context: context, + builder: (BuildContext context) { + return SizedBox( + height: 400, + child: Column( + children: [ + AppBar( + title: const Text("Änderungsverlauf"), + ), + const Padding( + padding: EdgeInsets.all(10), + child: Text( + "1.0.0 --\nErste Release-Version!"), + ), + ], + ), + ); + }, + ); + }, + ), + ), + Padding( + padding: const EdgeInsets.all(5), + child: ListTile( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15.0)), + leading: const Icon(Icons.copyright_outlined), + title: const Text("Lizenzen"), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => LicensePage( + applicationIcon: Padding( + padding: const EdgeInsets.all(5), + child: MediaQuery.of(context).platformBrightness == Brightness.light + ? Image.asset( + "assets/images/meincantor-big.png", + width: 196) + : Image.asset( + "assets/images/meincantor-big-dark.png", + width: 196) + ), + applicationVersion: version, + )), + ); + }, + ), + ), + ], + )), + ); + })); } } diff --git a/lib/Settings/Pages/plan_settings.dart b/lib/Settings/Pages/plan_settings.dart index 0e34b7b..1b8c39e 100644 --- a/lib/Settings/Pages/plan_settings.dart +++ b/lib/Settings/Pages/plan_settings.dart @@ -1,9 +1,9 @@ import 'dart:convert'; import 'package:meincantor/main.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:cyclop/cyclop.dart'; +import 'package:meincantor/networking.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -12,52 +12,6 @@ import 'package:meincantor/presets/subjects.dart'; import 'package:meincantor/presets/teachers.dart'; -class PlanSettings extends StatelessWidget { - const PlanSettings({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text("Plan"), - centerTitle: true, - ), - body: ListView( - padding: const EdgeInsets.fromLTRB(5, 5, 5, 5), - children: [ - ListTile( - leading: const Icon(Icons.list_alt_outlined, color: Colors.red), - trailing: const Icon(Icons.arrow_forward_ios, size: 16), - title: const Text("Kurse"), - subtitle: const Text("Konfiguration der Kurse (Whitelist)"), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const WhitelistSettings()), - ); - }, - ), - ListTile( - leading: - const Icon(Icons.color_lens_outlined, color: Colors.teal), - trailing: const Icon(Icons.arrow_forward_ios, size: 16), - title: const Text("Farben"), - subtitle: - const Text("Konfiguration der Farben für die Plankacheln"), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const PlanColorSettings()), - ); - }, - ), - ], - )); - } -} - class WhitelistSettings extends StatefulWidget { const WhitelistSettings({Key? key}) : super(key: key); @@ -77,7 +31,7 @@ class _WhitelistSettingsState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text("Farben"), + title: const Text("Kurse"), centerTitle: true, ), body: ListView( @@ -106,11 +60,36 @@ class _WhitelistSettingsState extends State { snapshot.data! as List; final _blacklisted = _blacklist.contains(id); - return ListTile( - leading: Checkbox( - value: - _blacklisted ? false : true, - onChanged: (state) async { + return Padding( + padding: const EdgeInsets.all(5), + child: ListTile( + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular( + 15.0)), + leading: Checkbox( + value: _blacklisted + ? false + : true, + onChanged: (state) async { + SharedPreferences prefs = + await SharedPreferences + .getInstance(); + setState(() { + _blacklisted + ? _blacklist + .remove(id) + : _blacklist.add(id); + }); + prefs.setString("blacklist", + jsonEncode(_blacklist)); + }, + activeColor: color), + title: Text( + subjects[subject] ?? subject), + subtitle: Text( + teachers[teacher] ?? teacher), + onTap: () async { SharedPreferences prefs = await SharedPreferences .getInstance(); @@ -122,23 +101,7 @@ class _WhitelistSettingsState extends State { prefs.setString("blacklist", jsonEncode(_blacklist)); }, - activeColor: color), - title: Text(subjects[subject] ?? ""), - subtitle: - Text(teachers[teacher] ?? ""), - onTap: () async { - SharedPreferences prefs = - await SharedPreferences - .getInstance(); - setState(() { - _blacklisted - ? _blacklist.remove(id) - : _blacklist.add(id); - }); - prefs.setString("blacklist", - jsonEncode(_blacklist)); - }, - ); + )); } else { return const LinearProgressIndicator(); } @@ -187,8 +150,12 @@ Future buildPlanColors(String lesson) async { Future> buildLessonsList() async { SharedPreferences prefs = await SharedPreferences.getInstance(); - String lessonsJson = prefs.getString("lessons")!; - List lessons = jsonDecode(lessonsJson); + String? lessonsJson = prefs.getString("lessons"); + if (lessonsJson == null || lessonsJson.isEmpty) { + await fetchLessonList(); + lessonsJson = prefs.getString("lessons"); + } + List lessons = jsonDecode(lessonsJson!); return lessons; } @@ -235,30 +202,37 @@ class _PlanColorSettingsState extends State { builder: (context, snapshot) { if (snapshot.hasData) { Color color = snapshot.data as Color; - return ListTile( - leading: ColorButton( - key: const Key('c1'), - color: color, - config: const ColorPickerConfig( - enableEyePicker: false), - onSwatchesChanged: (Set value) { - swatches = value; - }, - size: 32, - swatches: swatches, - onColorChanged: (Color value) async { - setState(() { - color = value; - }); - SharedPreferences prefs = - await SharedPreferences - .getInstance(); - prefs.setInt( - "color$subject", value.value); - }), - title: Text(subjects[subject] ?? ""), - subtitle: Text(teachers[teacher] ?? ""), - ); + return Padding( + padding: const EdgeInsets.all(5), + child: ListTile( + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(15.0)), + leading: ColorButton( + key: const Key('c1'), + color: color, + config: const ColorPickerConfig( + enableEyePicker: false), + onSwatchesChanged: + (Set value) { + swatches = value; + }, + size: 32, + swatches: swatches, + onColorChanged: (Color value) async { + setState(() { + color = value; + }); + SharedPreferences prefs = + await SharedPreferences + .getInstance(); + prefs.setInt( + "color$subject", value.value); + }), + title: Text(subjects[subject] ?? subject), + subtitle: + Text(teachers[teacher] ?? teacher), + )); } else { return (const LinearProgressIndicator()); } diff --git a/lib/Settings/Pages/service_settings.dart b/lib/Settings/Pages/service_settings.dart index d2d583f..6ca0056 100644 --- a/lib/Settings/Pages/service_settings.dart +++ b/lib/Settings/Pages/service_settings.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class ServiceSettings extends StatelessWidget { @@ -13,7 +12,7 @@ class ServiceSettings extends StatelessWidget { ), body: ListView( padding: const EdgeInsets.fromLTRB(5, 5, 5, 5), - children: [], + children: const [], )); } } diff --git a/lib/Settings/Pages/user_settings.dart b/lib/Settings/Pages/user_settings.dart index 3407fbf..717b0d9 100644 --- a/lib/Settings/Pages/user_settings.dart +++ b/lib/Settings/Pages/user_settings.dart @@ -1,14 +1,10 @@ -import 'package:cyclop/cyclop.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; -import 'package:meincantor/Settings/Pages/plan_settings.dart'; import 'package:meincantor/networking.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:webview_flutter/webview_flutter.dart'; -import 'dart:io' show Platform; -import '../../const.dart'; +import 'package:meincantor/const.dart'; +import 'package:webviewx/webviewx.dart'; Future getSettingsString(String key) async { SharedPreferences prefs = await SharedPreferences.getInstance(); @@ -30,104 +26,172 @@ class UserSettings extends StatelessWidget { title: const Text("Benutzereinstellungen"), centerTitle: true, ), - body: ListView( - padding: const EdgeInsets.fromLTRB(5, 5, 5, 5), - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(10, 10, 10, 10), - child: FutureBuilder( - future: Future.sync(() async { - SharedPreferences prefs = await SharedPreferences.getInstance(); - String? user = prefs.getString("user"); - if (user == null || user.isEmpty) { - user = ""; - } - String? name = prefs.getString("name"); - if (name == null || name.isEmpty) { - name = ""; - } - Map data = {"user": user, "name": name }; - return data; - }), - builder: (context, snapshot) { - if (snapshot.hasData) { - // .svg?text=${(snapshot.data! as Map)['name'][0]} - String url = "$avatarUrl/${(snapshot.data! as Map)['user']}"; - return Container( - width: 120.0, - height: 120.0, - decoration: BoxDecoration( - shape: BoxShape.circle, - image: DecorationImage( - fit: BoxFit.scaleDown, - image: NetworkImage(url) - ) - ) - ); - } else { - return const CircularProgressIndicator(); - } - }, - ), - ), - FutureBuilder( - future: getSettingsString("name"), - builder: (context, snapshot) { - if (snapshot.hasData) { - return TextField( - decoration: const InputDecoration( - border: OutlineInputBorder(), - labelText: 'Name', + body: LayoutBuilder(builder: (context, constraints) { + double widgetWidth = constraints.maxWidth; + + int factor; + + if (widgetWidth <= 600) { + factor = 1; + } else if (widgetWidth <= 1400) { + factor = 2; + } else if (widgetWidth <= 2000) { + factor = 3; + } else { + factor = 1; + } + + return Center( + heightFactor: 1, + child: Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width / factor, + ), + child: ListView( + padding: const EdgeInsets.fromLTRB(5, 5, 5, 5), + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(10, 10, 10, 10), + child: FutureBuilder( + future: Future.sync(() async { + SharedPreferences prefs = + await SharedPreferences.getInstance(); + String? user = prefs.getString("user"); + if (user == null || user.isEmpty) { + user = ""; + } + String? name = prefs.getString("name"); + if (name == null || name.isEmpty) { + name = ""; + } + Map data = {"user": user, "name": name}; + return data; + }), + builder: (context, snapshot) { + if (snapshot.hasData) { + String url = + "$avatarUrl/${(snapshot.data! as Map)['user']}"; + return Container( + width: 120.0, + height: 120.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + image: DecorationImage( + fit: BoxFit.scaleDown, + image: NetworkImage(url)))); + } else { + return const CircularProgressIndicator(); + } + }, ), - readOnly: true, - controller: - TextEditingController(text: snapshot.data as String), - ); - } else { - return (const Center(child: CircularProgressIndicator())); - } - }), - Padding( - padding: const EdgeInsets.fromLTRB(5, 20, 5, 5), - child: buildClassesChooser()), - ListTile( - leading: const Icon(MdiIcons.accountSettingsOutline), - trailing: const Icon(Icons.link, size: 16), - title: const Text("Account-Konsole"), - subtitle: const Text("Konto-Einstellungen öffnen"), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => AccountConsole()), - ); - }, - ), - ], - ) - ); + ), + Padding( + padding: const EdgeInsets.fromLTRB(5, 5, 5, 5), + child: FutureBuilder( + future: getSettingsString("name"), + builder: (context, snapshot) { + if (snapshot.hasData) { + return TextField( + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Name', + icon: Icon(MdiIcons.passport)), + readOnly: true, + controller: TextEditingController( + text: snapshot.data as String), + ); + } else { + return (const Center( + child: CircularProgressIndicator())); + } + }), + ), + const Divider(), + Padding( + padding: const EdgeInsets.fromLTRB(5, 5, 5, 5), + child: FutureBuilder( + future: getSettingsString("user"), + builder: (context, snapshot) { + if (snapshot.hasData) { + return TextField( + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Benutzername', + icon: Icon(MdiIcons.identifier)), + readOnly: true, + controller: TextEditingController( + text: snapshot.data as String), + ); + } else { + return (const Center( + child: CircularProgressIndicator())); + } + }), + ), + const Divider(), + Padding( + padding: const EdgeInsets.fromLTRB(5, 5, 5, 5), + child: FutureBuilder( + future: getSettingsString("email"), + builder: (context, snapshot) { + if (snapshot.hasData) { + return TextField( + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'E-Mail-Adresse', + icon: Icon(Icons.email_outlined)), + readOnly: true, + controller: TextEditingController( + text: snapshot.data as String), + ); + } else { + return (const Center( + child: CircularProgressIndicator())); + } + }), + ), + Padding( + padding: const EdgeInsets.fromLTRB(5, 20, 5, 5), + child: buildClassesChooser()), + ListTile( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15.0)), + leading: const Icon(MdiIcons.accountSettingsOutline), + trailing: const Icon(Icons.link, size: 16), + title: const Text("Account-Konsole"), + subtitle: const Text("Konto-Einstellungen öffnen"), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const AccountConsole()), + ); + }, + ), + ], + )), + ); + })); } } - class AccountConsole extends StatefulWidget { + const AccountConsole({Key? key}) : super(key: key); + @override AccountConsoleState createState() => AccountConsoleState(); } class AccountConsoleState extends State { - @override - void initState() { - super.initState(); - // Enable virtual display. - if (Platform.isAndroid) WebView.platform = AndroidWebView(); - } @override Widget build(BuildContext context) { - return const WebView( - initialUrl: 'https://mein.cantorgymnasium.de/auth/realms/GCG.MeinCantor/account/', + return WebViewX( + height: MediaQuery.of(context).size.height, + initialContent: 'https://mein.cantorgymnasium.de/auth/realms/GCG.MeinCantor/account/', + initialSourceType: SourceType.url, + javascriptMode: JavascriptMode.unrestricted, + width: MediaQuery.of(context).size.width ); } } - diff --git a/lib/Settings/dashboard.dart b/lib/Settings/dashboard.dart index 09672ac..0c83aba 100644 --- a/lib/Settings/dashboard.dart +++ b/lib/Settings/dashboard.dart @@ -1,13 +1,10 @@ -import 'package:meincantor/Settings/Pages/appearance_settings.dart'; -import 'package:meincantor/Settings/Pages/service_settings.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; -import 'Pages/dev_settings.dart'; -import 'Pages/info_settings.dart'; -import 'Pages/plan_settings.dart'; -import 'Pages/user_settings.dart'; +import 'package:meincantor/Settings/Pages/dev_settings.dart'; +import 'package:meincantor/Settings/Pages/info_settings.dart'; +import 'package:meincantor/Settings/Pages/plan_settings.dart'; +import 'package:meincantor/Settings/Pages/user_settings.dart'; class Settings extends StatelessWidget { const Settings({Key? key}) : super(key: key); @@ -22,84 +19,100 @@ class Settings extends StatelessWidget { body: ListView( padding: const EdgeInsets.fromLTRB(5, 5, 5, 5), children: [ - ListTile( - leading: const Icon(MdiIcons.accountSettingsOutline, - color: Colors.cyan), - trailing: const Icon(Icons.arrow_forward_ios, size: 16), - title: const Text("Benutzer"), - subtitle: const Text("Profilbild, Klasse & mehr"), - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const UserSettings()), - ); - }, + Padding( + padding: const EdgeInsets.all(5), + child: ListTile( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15.0)), + leading: const Icon(MdiIcons.accountSettingsOutline, + color: Colors.cyan), + trailing: const Icon(Icons.arrow_forward_ios, size: 16), + title: const Text("Benutzer"), + subtitle: const Text("Profilbild, Klasse & mehr"), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const UserSettings()), + ); + }, + ), ), - ListTile( - leading: - const Icon(MdiIcons.timetable, color: Colors.orangeAccent), - trailing: const Icon(Icons.arrow_forward_ios, size: 16), - title: const Text("Plan"), - subtitle: const Text("Kurse/Fächer, Farben & mehr"), - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const PlanSettings()), - ); - }, + Padding( + padding: const EdgeInsets.all(5), + child: ListTile( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15.0)), + leading: const Icon(Icons.list_alt_outlined, color: Colors.red), + trailing: const Icon(Icons.arrow_forward_ios, size: 16), + title: const Text("Kurse"), + subtitle: const Text("Konfiguration der Kurse"), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const WhitelistSettings()), + ); + }, + ), ), - ListTile( - leading: const Icon(Icons.color_lens_outlined, - color: Colors.pinkAccent), - trailing: const Icon(Icons.arrow_forward_ios, size: 16), - title: const Text("Aussehen"), - subtitle: const Text("Widgets, Design & mehr"), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const AppearanceSettings()), - ); - }, + Padding( + padding: const EdgeInsets.all(5), + child: ListTile( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15.0)), + leading: + const Icon(Icons.color_lens_outlined, color: Colors.teal), + trailing: const Icon(Icons.arrow_forward_ios, size: 16), + title: const Text("Farben"), + subtitle: + const Text("Konfiguration der Farben für die Plankacheln"), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const PlanColorSettings()), + ); + }, + ), ), - ListTile( - leading: const Icon(MdiIcons.server, color: Colors.lightGreen), - trailing: const Icon(Icons.arrow_forward_ios, size: 16), - title: const Text("Dienste"), - subtitle: const Text("Konten, Plattformen & mehr"), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const ServiceSettings()), - ); - }, + Padding( + padding: const EdgeInsets.all(5), + child: ListTile( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15.0)), + leading: const Icon(Icons.developer_mode_outlined, + color: Colors.deepOrangeAccent), + trailing: const Icon(Icons.arrow_forward_ios, size: 16), + title: const Text("Entwickleroptionen"), + subtitle: const Text("API, Benutzerdaten & mehr"), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const DevSettings()), + ); + }, + ), ), - ListTile( - leading: const Icon(Icons.developer_mode_outlined, - color: Colors.deepOrangeAccent), - trailing: const Icon(Icons.arrow_forward_ios, size: 16), - title: const Text("Entwickleroptionen"), - subtitle: const Text("API, Benutzerdaten & mehr"), - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const DevSettings()), - ); - }, - ), - ListTile( - leading: - const Icon(Icons.info_outlined, color: Colors.greenAccent), - trailing: const Icon(Icons.arrow_forward_ios, size: 16), - title: const Text("Informationen"), - subtitle: const Text("Version, Lizenzen & mehr"), - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const InfoSettings()), - ); - }, + Padding( + padding: const EdgeInsets.all(5), + child: ListTile( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15.0)), + leading: + const Icon(Icons.info_outlined, color: Colors.greenAccent), + trailing: const Icon(Icons.arrow_forward_ios, size: 16), + title: const Text("Informationen"), + subtitle: const Text("Version, Lizenzen & mehr"), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const InfoSettings()), + ); + }, + ), ), ], )); diff --git a/lib/background_fetch.dart b/lib/background_fetch.dart new file mode 100644 index 0000000..11b7da0 --- /dev/null +++ b/lib/background_fetch.dart @@ -0,0 +1,258 @@ +import 'dart:convert'; + +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:intl/intl.dart'; +import 'package:meincantor/timetable.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:http/http.dart' as http; + +import 'package:meincantor/networking.dart'; + +Future backgroundFetchTimetable() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + + String? todayTimetable = prefs.getString("todayTimetable"); + String? tomorrowTimetable = prefs.getString("tomorrowTimetable"); + + http.Response todayResponse = await fetchClassTimetable( + "/${DateFormat("yyyyMMdd").format(DateTime.now())}", null); + + http.Response tomorrowResponse = await fetchClassTimetable( + "/${DateFormat("yyyyMMdd").format(DateTime.now().add(const Duration(days: 1)))}", + null); + + if (todayResponse.statusCode == 200) { + if (todayTimetable != todayResponse.body) { + FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); + const AndroidNotificationDetails + androidPlatformChannelSpecificsTodayPlan = AndroidNotificationDetails( + 'de.cantorgymnasium.meincantor.today.plan', + 'Vertretungsplan für heute', + channelDescription: '', + importance: Importance.max, + priority: Priority.high, + ticker: 'ticker'); + const NotificationDetails platformChannelSpecificsTodayPlan = + NotificationDetails( + android: androidPlatformChannelSpecificsTodayPlan); + + const AndroidNotificationDetails + androidPlatformChannelSpecificsTodayInfo = AndroidNotificationDetails( + 'de.cantorgymnasium.meincantor.today.info', + 'Informationen für heute', + channelDescription: '', + importance: Importance.max, + priority: Priority.high, + ticker: 'ticker'); + const NotificationDetails platformChannelSpecificsTodayInfo = + NotificationDetails( + android: androidPlatformChannelSpecificsTodayInfo); + + List lessonsList = + ClassTimetable.fromJson(jsonDecode(todayResponse.body)).timetable; + List changedLessons = []; + for (var element in lessonsList) { + if (element.info.isNotEmpty) { + changedLessons.add(element.count); + } + } + String subtitle; + if (changedLessons.isNotEmpty && changedLessons.length > 1) { + subtitle = "Änderungen in den Stunden "; + for (var i in changedLessons) { + subtitle += "$i"; + if (changedLessons.indexOf(i) != changedLessons.length - 1) { + subtitle += ", "; + } + } + } else if (changedLessons.isNotEmpty && changedLessons.length == 1) { + subtitle = "Änderungen in Stunde ${changedLessons[0]}"; + } else { + subtitle = "Keine Änderungen im Plan gefunden!"; + } + await flutterLocalNotificationsPlugin.show( + 0, + 'Neuer Vertretungsplan für heute geladen!', + subtitle, + platformChannelSpecificsTodayPlan, + payload: 'item x'); + if ((jsonDecode(todayResponse.body)["info"] as String).isNotEmpty) { + await flutterLocalNotificationsPlugin.show( + 1, + 'Informationen für heute', + (jsonDecode(todayResponse.body)["info"] as String), + platformChannelSpecificsTodayInfo, + payload: 'item x'); + } + prefs.setString("todayTimetable", todayResponse.body); + } + } + + if (tomorrowResponse.statusCode == 200) { + if (tomorrowTimetable != tomorrowResponse.body) { + FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); + const AndroidNotificationDetails + androidPlatformChannelSpecificsTomorrowPlan = + AndroidNotificationDetails( + 'de.cantorgymnasium.meincantor.tomorrow.plan', + 'Vertretungsplan für morgen', + channelDescription: '', + importance: Importance.max, + priority: Priority.high, + ticker: 'ticker'); + const NotificationDetails platformChannelSpecificsTomorrowPlan = + NotificationDetails( + android: androidPlatformChannelSpecificsTomorrowPlan); + + const AndroidNotificationDetails + androidPlatformChannelSpecificsTomorrowInfo = + AndroidNotificationDetails( + 'de.cantorgymnasium.meincantor.tomorrow.info', + 'Informationen für morgen', + channelDescription: '', + importance: Importance.max, + priority: Priority.high, + ticker: 'ticker'); + const NotificationDetails platformChannelSpecificsTomorrowInfo = + NotificationDetails( + android: androidPlatformChannelSpecificsTomorrowInfo); + + List lessonsList = + ClassTimetable.fromJson(jsonDecode(tomorrowResponse.body)).timetable; + List changedLessons = []; + for (var element in lessonsList) { + if (element.info.isNotEmpty) { + changedLessons.add(element.count); + } + } + String subtitle; + if (changedLessons.isNotEmpty && changedLessons.length > 1) { + subtitle = "Änderungen in den Stunden "; + for (var i in changedLessons) { + subtitle += "$i"; + if (changedLessons.indexOf(i) != changedLessons.length - 1) { + subtitle += ", "; + } + } + } else if (changedLessons.isNotEmpty && changedLessons.length == 1) { + subtitle = "Änderungen in Stunde ${changedLessons[0]}"; + } else { + subtitle = "Keine Änderungen im Plan gefunden!"; + } + await flutterLocalNotificationsPlugin.show( + 2, + 'Neuer Vertretungsplan für morgen geladen!', + subtitle, + platformChannelSpecificsTomorrowPlan, + payload: 'item x'); + if ((jsonDecode(tomorrowResponse.body)["info"] as String).isNotEmpty) { + await flutterLocalNotificationsPlugin.show( + 3, + 'Informationen für morgen', + (jsonDecode(tomorrowResponse.body)["info"] as String), + platformChannelSpecificsTomorrowInfo, + payload: 'item x'); + } + prefs.setString("tomorrowTimetable", tomorrowResponse.body); + } + } +} + +Future backgroundFetchArticles() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + + String? articles = prefs.getString("articles"); + String? news = prefs.getString("news"); + + http.Response fetchedArticles = await getArticles(); + + http.Response fetchedNews = await getNews(); + + if (fetchedArticles.statusCode == 200) { + if (articles != fetchedArticles.body) { + FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); + const AndroidNotificationDetails androidPlatformChannelSpecificsSZ = + AndroidNotificationDetails( + 'de.cantorgymnasium.meincantor.sz', 'Schülerzeitung', + channelDescription: '', + importance: Importance.max, + priority: Priority.high, + ticker: 'ticker'); + const NotificationDetails platformChannelSpecificsSZ = + NotificationDetails(android: androidPlatformChannelSpecificsSZ); + + String subtitle; + List listFetchedArticles = jsonDecode(fetchedArticles.body)["data"]; + if (articles != null && articles.isNotEmpty) { + List listSavedArticles = jsonDecode(articles)["data"]; + int diff = listFetchedArticles.length - listSavedArticles.length; + diff == 1 + ? subtitle = "1 neuer Artikel!" + : diff > 1 + ? subtitle = "$diff neue Artikel!" + : subtitle = "Fehler beim Ermitteln der Änderungen!"; + } else { + int len = listFetchedArticles.length; + len == 1 + ? subtitle = "1 neuer Artikel!" + : len > 1 + ? subtitle = "$len neue Artikel!" + : subtitle = "Fehler beim Ermitteln der Änderungen!"; + } + + await flutterLocalNotificationsPlugin.show( + 4, + 'Neuer Inhalt von der Schülerzeitung verfügbar!', + subtitle, + platformChannelSpecificsSZ, + payload: 'item x'); + prefs.setString("articles", fetchedArticles.body); + } + } + + if (fetchedNews.statusCode == 200) { + if (news != fetchedNews.body) { + FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); + const AndroidNotificationDetails androidPlatformChannelSpecificsNews = + AndroidNotificationDetails( + 'de.cantorgymnasium.meincantor.news', 'Aktuelles', + channelDescription: '', + importance: Importance.max, + priority: Priority.high, + ticker: 'ticker'); + const NotificationDetails platformChannelSpecificsNews = + NotificationDetails(android: androidPlatformChannelSpecificsNews); + + String subtitle; + List listFetchedNews = jsonDecode(fetchedNews.body)["data"]; + if (news != null && news.isNotEmpty) { + List listSavedNews = jsonDecode(news)["data"]; + int diff = listFetchedNews.length - listSavedNews.length; + diff == 1 + ? subtitle = "1 neuer Artikel!" + : diff > 1 + ? subtitle = "$diff neue Artikel!" + : subtitle = "Fehler beim Ermitteln der Änderungen!"; + } else { + int len = listFetchedNews.length; + len == 1 + ? subtitle = "1 neuer Artikel!" + : len > 1 + ? subtitle = "$len neue Artikel!" + : subtitle = "Fehler beim Ermitteln der Änderungen!"; + } + + await flutterLocalNotificationsPlugin.show( + 5, + 'Neue Informationen verfügbar!', + subtitle, + platformChannelSpecificsNews, + payload: 'item x'); + prefs.setString("news", fetchedNews.body); + } + } +} diff --git a/lib/cache_manager.dart b/lib/cache_manager.dart index 4f384b9..1ebb3b3 100644 --- a/lib/cache_manager.dart +++ b/lib/cache_manager.dart @@ -3,10 +3,12 @@ import 'dart:convert'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:shared_preferences/shared_preferences.dart'; -Future getCachedTimetable(String ext) async { +Future getCachedTimetable(String ext, String? presetClassNum) async { SharedPreferences prefs = await SharedPreferences.getInstance(); String classNum; - if (prefs.getString('class_num') != null) { + if (presetClassNum != null) { + classNum = presetClassNum.replaceAll("/", "_"); + } else if (prefs.getString('class_num') != null) { classNum = prefs.getString('class_num')!.replaceAll("/", "_"); } else { classNum = '05_1'; diff --git a/lib/dashboard.dart b/lib/dashboard.dart index 486d737..5444e89 100644 --- a/lib/dashboard.dart +++ b/lib/dashboard.dart @@ -1,8 +1,9 @@ -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'dart:convert'; +import 'dart:math'; + +import 'package:background_fetch/background_fetch.dart'; +import 'package:meincantor/background_fetch.dart'; import 'package:meincantor/const.dart'; -import 'package:meincantor/raumuebersicht.dart'; -import 'package:meincantor/schulbibliothek.dart'; -import 'package:meincantor/schulcomputer.dart'; import 'package:meincantor/schuelerzeitung.dart'; import 'package:meincantor/Settings/dashboard.dart'; @@ -16,17 +17,9 @@ import 'package:google_fonts/google_fonts.dart'; import 'package:intl/intl.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:http/http.dart' as http; -import 'news.dart'; - -class Dashboard extends StatefulWidget { - const Dashboard({Key? key, this.restorationId}) : super(key: key); - - final String? restorationId; - - @override - State createState() => _DashboardState(); -} +import 'package:meincantor/news.dart'; Future getSettingsString(String key) async { SharedPreferences prefs = await SharedPreferences.getInstance(); @@ -44,77 +37,262 @@ Widget buildSettingsString(String key, TextStyle? style) { if (snapshot.hasData) { return Text(snapshot.data as String, style: style); } else { - return (const Center(child: CircularProgressIndicator())); + return const SizedBox.shrink(); } }); } +Future> getFavClasses() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + List? listJson = prefs.getStringList("favClasses"); + if (listJson == null || listJson.isEmpty) { + return []; + } else { + return listJson; + } +} + +List buildFavClasses( + BuildContext context, List favClasses, Function removeFavClass) { + if (favClasses.isEmpty) { + return [const SizedBox.shrink()]; + } else { + List list = []; + for (var element in favClasses) { + var card = SizedBox( + width: 170, + child: GestureDetector( + onTap: () async { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => DefaultTabController( + initialIndex: 0, + length: 3, + child: Scaffold( + appBar: AppBar( + leading: null, + elevation: 0, + title: Text("Klasse $element"), + bottom: const TabBar( + indicatorColor: Palette.accent, + enableFeedback: true, + indicatorPadding: EdgeInsets.all(5), + indicatorSize: TabBarIndicatorSize.label, + tabs: [ + Tab( + text: "Heute", + icon: Icon(Icons.calendar_today_outlined), + ), + Tab( + text: "Morgen", + icon: Icon(MdiIcons.calendarToday), + ), + Tab( + text: "Neuster Plan", + icon: Icon(Icons.calendar_view_day_outlined), + ), + ], + ), + ), + body: TabBarView( + children: [ + LayoutBuilder(builder: (context, constraints) { + double widgetWidth = constraints.maxWidth; + + int factor; + + if (widgetWidth <= 600) { + factor = 1; + } else if (widgetWidth <= 1400) { + factor = 2; + } else if (widgetWidth <= 2000) { + factor = 3; + } else { + factor = 1; + } + + return Center( + heightFactor: 1, + child: Container( + constraints: BoxConstraints( + maxWidth: + MediaQuery.of(context).size.width / + factor, + ), + child: buildTimetable( + fetchClassTimetable( + "/${DateFormat("yyyyMMdd").format(DateTime.now())}", + element), + "Vertretungsplan für heute"), + )); + }), + LayoutBuilder(builder: (context, constraints) { + double widgetWidth = constraints.maxWidth; + + int factor; + + if (widgetWidth <= 600) { + factor = 1; + } else if (widgetWidth <= 1400) { + factor = 2; + } else if (widgetWidth <= 2000) { + factor = 3; + } else { + factor = 1; + } + + return Center( + heightFactor: 1, + child: Container( + constraints: BoxConstraints( + maxWidth: + MediaQuery.of(context).size.width / + factor, + ), + child: buildTimetable( + fetchClassTimetable( + "/${DateFormat("yyyyMMdd").format(DateTime.now().add(const Duration(days: 1)))}", + element), + "Vertretungsplan für morgen"), + )); + }), + LayoutBuilder(builder: (context, constraints) { + double widgetWidth = constraints.maxWidth; + + int factor; + + if (widgetWidth <= 600) { + factor = 1; + } else if (widgetWidth <= 1400) { + factor = 2; + } else if (widgetWidth <= 2000) { + factor = 3; + } else { + factor = 1; + } + + return Center( + heightFactor: 1, + child: Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context) + .size + .width / + factor, + ), + child: buildTimetable( + fetchClassTimetable( + "/latest", element), + "aktueller Vertretungsplan"))); + }), + ], + ), + )))); + }, + onLongPress: () async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + List stringList = prefs.getStringList("favClasses")!; + stringList.remove(element); + prefs.setStringList("favClasses", stringList); + removeFavClass(element); + }, + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + gradient: LinearGradient( + begin: Alignment.topRight, + end: Alignment.bottomLeft, + colors: [ + Colors.primaries[ + Random().nextInt(Colors.primaries.length)], + Colors.primaries[ + Random().nextInt(Colors.primaries.length)], + ], + )), + child: Padding( + padding: const EdgeInsets.all(10), + child: Center( + child: Padding( + padding: const EdgeInsets.fromLTRB(15, 15, 15, 15), + child: Text( + element, + style: const TextStyle(color: Colors.white), + textScaleFactor: 2.0, + ), + ), + ), + )), + ), + ), + ); + list.add(card); + } + return (list); + } +} + +class Dashboard extends StatefulWidget { + const Dashboard({Key? key, this.restorationId}) : super(key: key); + + final String? restorationId; + + @override + State createState() => _DashboardState(); +} + class _DashboardState extends State with RestorationMixin { final RestorableInt _currentIndex = RestorableInt(0); + + @override + void initState() { + super.initState(); + initPlatformState(); + } + + Future initPlatformState() async { + // Configure BackgroundFetch. + int status = await BackgroundFetch.configure( + BackgroundFetchConfig( + minimumFetchInterval: 15, + stopOnTerminate: false, + enableHeadless: true, + startOnBoot: true, + requiresBatteryNotLow: false, + requiresCharging: false, + requiresStorageNotLow: false, + requiresDeviceIdle: false, + requiredNetworkType: NetworkType.ANY), (String taskId) async { + // <-- Event handler + // This is the fetch-event callback. + print("[BackgroundFetch] Event received $taskId"); + + await backgroundFetchTimetable(); + await backgroundFetchArticles(); + + // IMPORTANT: You must signal completion of your task or the OS can punish your app + // for taking too long in the background. + BackgroundFetch.finish(taskId); + }, (String taskId) async { + // <-- Task timeout handler. + // This task has exceeded its allowed running-time. You must stop what you're doing and immediately .finish(taskId) + print("[BackgroundFetch] TASK TIMEOUT taskId: $taskId"); + BackgroundFetch.finish(taskId); + }); + print('[BackgroundFetch] configure success: $status'); + + // If the widget was removed from the tree while the asynchronous platform + // message was in flight, we want to discard the reply rather than calling + // setState to update our non-existent appearance. + if (!mounted) return; + } + @override Widget build(BuildContext context) { - final drawerElements = ListView( - children: [ - UserAccountsDrawerHeader( - accountName: buildSettingsString('name', const TextStyle()), - accountEmail: buildSettingsString('user', const TextStyle()), - currentAccountPicture: FutureBuilder( - future: Future.sync(() async { - SharedPreferences prefs = await SharedPreferences.getInstance(); - String? user = prefs.getString("user"); - if (user == null || user.isEmpty) { - user = ""; - } - String? name = prefs.getString("name"); - if (name == null || name.isEmpty) { - name = ""; - } - Map data = {"user": user, "name": name }; - return data; - }), - builder: (context, snapshot) { - if (snapshot.hasData) { - // .svg?text=${(snapshot.data! as Map)['name'][0]} - String url = "$avatarUrl/${(snapshot.data! as Map)['user']}"; - return Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - image: DecorationImage( - fit: BoxFit.scaleDown, - image: NetworkImage(url) - ) - ) - ); - } else { - return const CircularProgressIndicator(); - } - }, - ) - ), - ListTile( - title: const Text("Einstellungen"), - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const Settings()), - ); - }, - leading: const Icon(Icons.settings_outlined), - ), - ListTile( - title: const Text("Abmelden"), - onTap: () async { - SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.setString('api_key', ""); - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => Login()), - ); - }, - leading: const Icon(Icons.exit_to_app_outlined), - ), - ], - ); var bottomNavBarItems = [ const BottomNavigationBarItem( icon: Icon(MdiIcons.homeOutline), @@ -122,19 +300,10 @@ class _DashboardState extends State with RestorationMixin { ), const BottomNavigationBarItem( icon: Icon(MdiIcons.timetable), label: "Vertretungsplan"), - const BottomNavigationBarItem( - icon: Icon(Icons.notifications_outlined), label: "Hinweise"), ]; return Scaffold( - appBar: AppBar( - title: const Text("GCG.MeinCantor"), - centerTitle: true, - ), body: _DashboardBottomNavView( key: UniqueKey(), item: bottomNavBarItems[_currentIndex.value]), - drawer: Drawer( - child: drawerElements, - ), bottomNavigationBar: BottomNavigationBar( showUnselectedLabels: false, items: bottomNavBarItems, @@ -157,67 +326,168 @@ class _DashboardState extends State with RestorationMixin { } } -class _DashboardBottomNavView extends StatelessWidget { +class _DashboardBottomNavView extends StatefulWidget { const _DashboardBottomNavView({Key? key, required this.item}) : super(key: key); final BottomNavigationBarItem item; + @override + // ignore: no_logic_in_create_state + State createState() => _DashboardBottomNavViewState(item); +} + +class _DashboardBottomNavViewState extends State<_DashboardBottomNavView> { + final BottomNavigationBarItem item; + _DashboardBottomNavViewState(this.item); + + List favClasses = []; + + void removeFavClass(String classNum) { + setState(() { + favClasses.remove(classNum); + }); + } + @override Widget build(BuildContext context) { + final drawerElements = ListView( + children: [ + UserAccountsDrawerHeader( + accountName: buildSettingsString('name', const TextStyle()), + accountEmail: buildSettingsString('email', const TextStyle()), + currentAccountPicture: FutureBuilder( + future: Future.sync(() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + String? user = prefs.getString("user"); + if (user == null || user.isEmpty) { + user = ""; + } + String? name = prefs.getString("name"); + if (name == null || name.isEmpty) { + name = ""; + } + Map data = {"user": user, "name": name}; + return data; + }), + builder: (context, snapshot) { + if (snapshot.hasData) { + String url = "$avatarUrl/${(snapshot.data! as Map)['user']}"; + return Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + image: DecorationImage( + fit: BoxFit.scaleDown, + image: NetworkImage(url)))); + } else { + return const CircularProgressIndicator(); + } + }, + )), + Padding( + padding: const EdgeInsets.all(5), + child: ListTile( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15.0)), + title: const Text("Einstellungen"), + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const Settings()), + ); + }, + leading: const Icon(Icons.settings_outlined), + ), + ), + Padding( + padding: const EdgeInsets.all(5), + child: ListTile( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15.0)), + title: const Text("Abmelden"), + onTap: () async { + return showDialog( + context: context, + barrierDismissible: false, // user must tap button! + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Abmelden'), + content: SingleChildScrollView( + child: ListBody( + children: const [ + Text( + 'Dabei werden alle persönlichen Enstellungen gelöscht!'), + ], + ), + ), + actions: [ + TextButton( + child: const Text('Bestätigen'), + onPressed: () async { + SharedPreferences prefs = + await SharedPreferences.getInstance(); + prefs.clear(); + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => Login()), + ); + }, + ), + ], + ); + }, + ); + }, + leading: const Icon(Icons.exit_to_app_outlined), + ), + ), + ], + ); if (item.label == "Startseite") { double _timeOfDayToDouble(TimeOfDay tod) => tod.hour + tod.minute / 60.0; int lessonCount; - if (_timeOfDayToDouble(TimeOfDay.now()) <= - _timeOfDayToDouble(const TimeOfDay(hour: 7, minute: 30))) { - lessonCount = 1; - } else if (_timeOfDayToDouble(TimeOfDay.now()) > - _timeOfDayToDouble(const TimeOfDay(hour: 7, minute: 30)) && - _timeOfDayToDouble(TimeOfDay.now()) <= - _timeOfDayToDouble(const TimeOfDay(hour: 8, minute: 20))) { - lessonCount = 2; - } else if (_timeOfDayToDouble(TimeOfDay.now()) > - _timeOfDayToDouble(const TimeOfDay(hour: 8, minute: 20)) && - _timeOfDayToDouble(TimeOfDay.now()) <= - _timeOfDayToDouble(const TimeOfDay(hour: 9, minute: 25))) { - lessonCount = 3; - } else if (_timeOfDayToDouble(TimeOfDay.now()) > - _timeOfDayToDouble(const TimeOfDay(hour: 9, minute: 25)) && - _timeOfDayToDouble(TimeOfDay.now()) <= - _timeOfDayToDouble(const TimeOfDay(hour: 10, minute: 15))) { - lessonCount = 4; - } else if (_timeOfDayToDouble(TimeOfDay.now()) > - _timeOfDayToDouble(const TimeOfDay(hour: 10, minute: 15)) && - _timeOfDayToDouble(TimeOfDay.now()) <= - _timeOfDayToDouble(const TimeOfDay(hour: 11, minute: 30))) { - lessonCount = 5; - } else if (_timeOfDayToDouble(TimeOfDay.now()) > - _timeOfDayToDouble(const TimeOfDay(hour: 11, minute: 30)) && - _timeOfDayToDouble(TimeOfDay.now()) <= - _timeOfDayToDouble(const TimeOfDay(hour: 12, minute: 20))) { - lessonCount = 6; - } else if (_timeOfDayToDouble(TimeOfDay.now()) > - _timeOfDayToDouble(const TimeOfDay(hour: 12, minute: 20)) && - _timeOfDayToDouble(TimeOfDay.now()) <= - _timeOfDayToDouble(const TimeOfDay(hour: 13, minute: 30))) { - lessonCount = 7; - } else if (_timeOfDayToDouble(TimeOfDay.now()) > - _timeOfDayToDouble(const TimeOfDay(hour: 13, minute: 30)) && - _timeOfDayToDouble(TimeOfDay.now()) <= - _timeOfDayToDouble(const TimeOfDay(hour: 14, minute: 20))) { - lessonCount = 8; - } else if (_timeOfDayToDouble(TimeOfDay.now()) > - _timeOfDayToDouble(const TimeOfDay(hour: 14, minute: 20)) && - _timeOfDayToDouble(TimeOfDay.now()) <= - _timeOfDayToDouble(const TimeOfDay(hour: 15, minute: 10))) { - lessonCount = 9; - } else if (_timeOfDayToDouble(TimeOfDay.now()) > - _timeOfDayToDouble(const TimeOfDay(hour: 15, minute: 10)) && - _timeOfDayToDouble(TimeOfDay.now()) <= - _timeOfDayToDouble(const TimeOfDay(hour: 16, minute: 00))) { - lessonCount = 10; - } else { - lessonCount = -1; - } + _timeOfDayToDouble(TimeOfDay.now()) <= + _timeOfDayToDouble(const TimeOfDay(hour: 7, minute: 30)) + ? lessonCount = 1 + : _timeOfDayToDouble(TimeOfDay.now()) > _timeOfDayToDouble(const TimeOfDay(hour: 7, minute: 30)) && + _timeOfDayToDouble(TimeOfDay.now()) <= + _timeOfDayToDouble(const TimeOfDay(hour: 8, minute: 20)) + ? lessonCount = 2 + : _timeOfDayToDouble(TimeOfDay.now()) > _timeOfDayToDouble(const TimeOfDay(hour: 8, minute: 20)) && + _timeOfDayToDouble(TimeOfDay.now()) <= + _timeOfDayToDouble( + const TimeOfDay(hour: 9, minute: 25)) + ? lessonCount = 3 + : _timeOfDayToDouble(TimeOfDay.now()) > _timeOfDayToDouble(const TimeOfDay(hour: 9, minute: 25)) && + _timeOfDayToDouble(TimeOfDay.now()) <= + _timeOfDayToDouble( + const TimeOfDay(hour: 10, minute: 15)) + ? lessonCount = 4 + : _timeOfDayToDouble(TimeOfDay.now()) > + _timeOfDayToDouble( + const TimeOfDay(hour: 10, minute: 15)) && + _timeOfDayToDouble(TimeOfDay.now()) <= + _timeOfDayToDouble( + const TimeOfDay(hour: 11, minute: 30)) + ? lessonCount = 5 + : _timeOfDayToDouble(TimeOfDay.now()) > _timeOfDayToDouble(const TimeOfDay(hour: 11, minute: 30)) && + _timeOfDayToDouble(TimeOfDay.now()) <= + _timeOfDayToDouble( + const TimeOfDay(hour: 12, minute: 20)) + ? lessonCount = 6 + : _timeOfDayToDouble(TimeOfDay.now()) > _timeOfDayToDouble(const TimeOfDay(hour: 12, minute: 20)) && + _timeOfDayToDouble(TimeOfDay.now()) <= + _timeOfDayToDouble(const TimeOfDay( + hour: 13, minute: 30)) + ? lessonCount = 7 + : _timeOfDayToDouble(TimeOfDay.now()) > _timeOfDayToDouble(const TimeOfDay(hour: 13, minute: 30)) && + _timeOfDayToDouble(TimeOfDay.now()) <= + _timeOfDayToDouble(const TimeOfDay(hour: 14, minute: 20)) + ? lessonCount = 8 + : _timeOfDayToDouble(TimeOfDay.now()) > _timeOfDayToDouble(const TimeOfDay(hour: 14, minute: 20)) && _timeOfDayToDouble(TimeOfDay.now()) <= _timeOfDayToDouble(const TimeOfDay(hour: 15, minute: 10)) + ? lessonCount = 9 + : _timeOfDayToDouble(TimeOfDay.now()) > _timeOfDayToDouble(const TimeOfDay(hour: 15, minute: 10)) && _timeOfDayToDouble(TimeOfDay.now()) <= _timeOfDayToDouble(const TimeOfDay(hour: 16, minute: 00)) + ? lessonCount = 10 + : lessonCount = -1; var view = SingleChildScrollView( child: Column(children: [ @@ -274,152 +544,40 @@ class _DashboardBottomNavView extends StatelessWidget { child: Wrap( children: [ SizedBox( - child: GestureDetector( - onTap: () async { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const SZ()), - ); - }, - child: Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - child: const Padding( - padding: EdgeInsets.all(10), - child: ListTile( - title: Padding( - padding: EdgeInsets.fromLTRB(0, 0, 0, 10), - child: Icon( - MdiIcons.newspaper, - color: Palette.accent, - size: 48, + width: 175, + child: GestureDetector( + onTap: () async { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SZ()), + ); + }, + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: const Padding( + padding: EdgeInsets.all(10), + child: ListTile( + title: Padding( + padding: EdgeInsets.fromLTRB(0, 0, 0, 10), + child: Icon( + MdiIcons.newspaper, + color: Palette.accent, + size: 48, + ), ), - ), - subtitle: Center( - child: Padding( - padding: EdgeInsets.fromLTRB(0, 10, 0, 0), - child: Text( - 'Schülerzeitung', + subtitle: Center( + child: Padding( + padding: EdgeInsets.fromLTRB(0, 10, 0, 0), + child: Text('Schülerzeitung'), ), ), ), - ), - )), - ), - width: 170, - ), + )), + )), SizedBox( - width: 170, - child: GestureDetector( - onTap: () async { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const SB()), - ); - }, - child: Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - child: const Padding( - padding: EdgeInsets.all(10), - child: ListTile( - title: Padding( - padding: EdgeInsets.fromLTRB(0, 0, 0, 10), - child: Icon( - MdiIcons.libraryShelves, - color: Palette.accent, - size: 48, - ), - ), - subtitle: Center( - child: Padding( - padding: EdgeInsets.fromLTRB(0, 10, 0, 0), - child: Text( - 'Schulbibliothek', - ), - ), - ), - ), - )), - ), - ), - SizedBox( - width: 170, - child: GestureDetector( - onTap: () async { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const SC()), - ); - }, - child: Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - child: const Padding( - padding: EdgeInsets.all(10), - child: ListTile( - title: Padding( - padding: EdgeInsets.fromLTRB(0, 0, 0, 10), - child: Icon( - MdiIcons.laptop, - color: Palette.accent, - size: 48, - ), - ), - subtitle: Center( - child: Padding( - padding: EdgeInsets.fromLTRB(0, 10, 0, 0), - child: Text( - 'Schulcomputer', - ), - ), - ), - ), - )), - ), - ), - SizedBox( - width: 170, - child: GestureDetector( - onTap: () async { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const RoomOverview()), - ); - }, - child: Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - child: const Padding( - padding: EdgeInsets.all(10), - child: ListTile( - title: Padding( - padding: EdgeInsets.fromLTRB(0, 0, 0, 10), - child: Icon( - MdiIcons.door, - color: Palette.accent, - size: 48, - ), - ), - subtitle: Center( - child: Padding( - padding: EdgeInsets.fromLTRB(0, 10, 0, 0), - child: Text( - 'Raumübersicht', - ), - ), - ), - ), - )), - ), - ), - SizedBox( - width: 170, + width: 175, child: GestureDetector( onTap: () async { Navigator.push( @@ -457,77 +615,342 @@ class _DashboardBottomNavView extends StatelessWidget { ], ), ), + Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 20), + child: Column( + children: [ + const ListTile(title: Text("Favorisierte Klassen")), + FutureBuilder( + future: getFavClasses(), + builder: (context, snapshot) { + if (snapshot.hasData) { + favClasses = snapshot.data! as List; + return Wrap(children: [ + ...(buildFavClasses( + context, favClasses, removeFavClass)), + SizedBox( + width: 170, + child: GestureDetector( + onTap: () async { + showModalBottomSheet( + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(25.0), + topRight: Radius.circular(25.0)), + ), + context: context, + builder: (BuildContext context) { + return SizedBox( + height: 400, + child: ListView( + children: [ + ListTile( + title: const Text( + "Klasse hinzufügen", + style: TextStyle( + fontWeight: + FontWeight.bold)), + leading: + const Icon(Icons.arrow_back), + onTap: () { + Navigator.of(context).pop(); + }, + ), + FutureBuilder( + future: fetchClassesList(), + builder: (context, snapshot) { + if (snapshot.hasData) { + if (snapshot + .data!.statusCode == + 200) { + List classesList = + []; + for (var classNum + in jsonDecode(snapshot + .data!.body)) { + classesList + .add(ListTile( + title: Text(classNum), + onTap: () async { + SharedPreferences + prefs = + await SharedPreferences + .getInstance(); + if (prefs + .getStringList( + "favClasses") != + null && + prefs + .getStringList( + "favClasses")! + .contains( + classNum)) { + const snackBar = SnackBar( + content: Text( + 'Klasse bereits in den Favoriten')); + ScaffoldMessenger + .of( + context) + .showSnackBar( + snackBar); + } else if (prefs + .getStringList( + "favClasses") == + null) { + List + stringList = [ + classNum + ]; + prefs.setStringList( + "favClasses", + stringList); + } else { + List + stringList = + prefs.getStringList( + "favClasses")!; + stringList.add( + classNum); + prefs.setStringList( + "favClasses", + stringList); + setState(() { + favClasses = + stringList; + }); + } + }, + )); + classesList.add( + const Divider()); + } + return Column( + children: + classesList); + } else if (snapshot + .data!.statusCode == + 500) { + return const Padding( + padding: + EdgeInsets.fromLTRB( + 10, 10, 10, 10), + child: Center( + child: Text( + "Serverfehler. Bitte wende dich an den MeinCantor-Support.")), + ); + } else if (snapshot + .data!.statusCode == + 404) { + return const Padding( + padding: + EdgeInsets.fromLTRB( + 10, 10, 10, 10), + child: Center( + child: Text( + "Keine Verbindung mit dem MeinCantor-Server möglich. Bitte prüfe deine Internetverbindung und deine DNS-Einstellungen oder wende dich an den MeinCantor-Support")), + ); + } else { + return const Center( + child: Text( + "Uups... etwas ist schief gelaufen...")); + } + } else if (snapshot + .hasError) { + return const Center( + child: Text( + "Uups... etwas ist schief gelaufen...")); + } else { + return const Center( + child: + CircularProgressIndicator()); + } + }) + ], + ), + ); + }, + ); + }, + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: const Padding( + padding: EdgeInsets.all(6), + child: ListTile( + title: Center(child: Text("+")), + subtitle: Center( + child: Text( + 'Klasse hinzufügen', + textScaleFactor: 0.9, + ), + ), + ), + )), + ), + ) + ]); + } else if (snapshot.hasError) { + return const Center( + child: + Text("Uups... etwas ist schief gelaufen...")); + } else { + return const CircularProgressIndicator(); + } + }), + ], + )) ]), ); - return view; + return Scaffold( + appBar: AppBar( + title: const Text("GCG.MeinCantor"), + centerTitle: true, + ), + drawer: Drawer( + child: drawerElements, + ), + body: LayoutBuilder(builder: (context, constraints) { + double widgetWidth = constraints.maxWidth; + + int factor; + + if (widgetWidth <= 600) { + factor = 1; + } else if (widgetWidth <= 1400) { + factor = 2; + } else if (widgetWidth <= 2000) { + factor = 3; + } else { + factor = 1; + } + + return Center( + heightFactor: 1, + child: Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width / factor, + ), + child: view), + ); + }), + ); } else if (item.label == "Vertretungsplan") { - return LayoutBuilder(builder: (context, constraints) { - double widgetWidth = constraints.maxWidth; + List children = [ + LayoutBuilder(builder: (context, constraints) { + double widgetWidth = constraints.maxWidth; - int factor; + int factor; - if (widgetWidth <= 600) { - factor = 1; - } else if (widgetWidth <= 1400) { - factor = 2; - } else if (widgetWidth <= 2000) { - factor = 3; - } else { - factor = 1; - } + if (widgetWidth <= 600) { + factor = 1; + } else if (widgetWidth <= 1400) { + factor = 2; + } else if (widgetWidth <= 2000) { + factor = 3; + } else { + factor = 1; + } - // print(screenType); - return Center( - child: Container( - constraints: BoxConstraints( - // minHeight: 500, //minimum height - // minWidth: 300, // minimum width - //maximum height set to 100% of vertical height - maxWidth: MediaQuery.of(context).size.width / factor, - //maximum width set to 100% of width - ), - child: DefaultTabController( - initialIndex: 0, - length: 3, - child: Scaffold( - appBar: AppBar( - elevation: 0, - title: const TabBar( - tabs: [ - Tab( - text: "Heute", - icon: Icon(CupertinoIcons.calendar_today), - ), - Tab( - text: "Morgen", - icon: Icon(CupertinoIcons.calendar_today), - ), - Tab( - text: "Neuster Plan", - icon: Icon(CupertinoIcons.calendar), - ), - ], - ), - ), - body: TabBarView( - children: [ - buildTimetable( - fetchClassTimetable( - "/${DateFormat("yyyyMMdd").format(DateTime.now())}"), - "Vertretungsplan für heute"), - buildTimetable( - fetchClassTimetable( - "/${DateFormat("yyyyMMdd").format(DateTime.now().add(const Duration(days: 1)))}"), - "Vertretungsplan für morgen"), - buildTimetable(fetchClassTimetable("/latest"), - "aktueller Vertretungsplan") - ], - ), + return Center( + child: Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width / factor, ), + child: buildTimetable( + fetchClassTimetable( + "/${DateFormat("yyyyMMdd").format(DateTime.now())}", + null), + "Vertretungsplan für heute"), + ), + ); + }), + LayoutBuilder(builder: (context, constraints) { + double widgetWidth = constraints.maxWidth; + + int factor; + + if (widgetWidth <= 600) { + factor = 1; + } else if (widgetWidth <= 1400) { + factor = 2; + } else if (widgetWidth <= 2000) { + factor = 3; + } else { + factor = 1; + } + + return Center( + child: Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width / factor, + ), + child: buildTimetable( + fetchClassTimetable( + "/${DateFormat("yyyyMMdd").format(DateTime.now().add(const Duration(days: 1)))}", + null), + "Vertretungsplan für morgen"), + ), + ); + }), + LayoutBuilder(builder: (context, constraints) { + double widgetWidth = constraints.maxWidth; + + int factor; + + if (widgetWidth <= 600) { + factor = 1; + } else if (widgetWidth <= 1400) { + factor = 2; + } else if (widgetWidth <= 2000) { + factor = 3; + } else { + factor = 1; + } + + return Center( + child: Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width / factor, + ), + child: buildTimetable(fetchClassTimetable("/latest", null), + "aktueller Vertretungsplan")), + ); + }) + ]; + return DefaultTabController( + initialIndex: 0, + length: 3, + child: Scaffold( + appBar: AppBar( + title: const Text("GCG.MeinCantor"), + centerTitle: true, + bottom: const TabBar( + indicatorColor: Palette.accent, + enableFeedback: true, + indicatorPadding: EdgeInsets.all(5), + indicatorSize: TabBarIndicatorSize.label, + tabs: [ + Tab( + text: "Heute", + icon: Icon(Icons.calendar_today_outlined), + ), + Tab( + text: "Morgen", + icon: Icon(MdiIcons.calendarToday), + ), + Tab( + text: "Neuster Plan", + icon: Icon(Icons.calendar_view_day_outlined), + ), + ], ), ), - ); - }); + drawer: Drawer(child: drawerElements), + body: TabBarView(children: children), + ), + ); } else { return const Center(child: Text("Derzeit nichts hier...")); } diff --git a/lib/login.dart b/lib/login.dart index 434e9f7..a5c57c6 100644 --- a/lib/login.dart +++ b/lib/login.dart @@ -3,9 +3,10 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:http/http.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'networking.dart'; -import 'dashboard.dart'; +import 'package:meincantor/networking.dart'; +import 'package:meincantor/dashboard.dart'; Future checkKey() async { SharedPreferences prefs = await SharedPreferences.getInstance(); @@ -14,6 +15,7 @@ Future checkKey() async { } class Login extends StatelessWidget { + Login({Key? key}) : super(key: key); final userController = TextEditingController(); final passwordController = TextEditingController(); @@ -39,9 +41,6 @@ class Login extends StatelessWidget { } return Scaffold( - appBar: AppBar( - title: const Text("Anmelden"), - ), body: Center( child: SingleChildScrollView( child: Padding( @@ -54,8 +53,14 @@ class Login extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Image.asset("assets/images/meincantor_r.png", - height: 192, width: 192), + MediaQuery.of(context).platformBrightness == + Brightness.light + ? Image.asset( + "assets/images/meincantor-big.png", + width: 256) + : Image.asset( + "assets/images/meincantor-big-dark.png", + width: 256), const Divider(), AutofillGroup( child: Column( @@ -65,7 +70,7 @@ class Login extends StatelessWidget { AutofillHints.username ], decoration: const InputDecoration( - icon: Icon(CupertinoIcons.person), + icon: Icon(MdiIcons.identifier), border: OutlineInputBorder(), labelText: 'Benutzername', ), @@ -77,7 +82,7 @@ class Login extends StatelessWidget { AutofillHints.password ], decoration: const InputDecoration( - icon: Icon(CupertinoIcons.lock), + icon: Icon(MdiIcons.lock), border: OutlineInputBorder(), labelText: 'Passwort', ), @@ -89,9 +94,9 @@ class Login extends StatelessWidget { const Divider(), TextField( decoration: const InputDecoration( - icon: Icon(CupertinoIcons.lock), + icon: Icon(MdiIcons.twoFactorAuthentication), border: OutlineInputBorder(), - labelText: '2F2-Code (OTP) [falls aktiviert]', + labelText: '2F2-Code', ), obscureText: true, controller: otpController, @@ -109,18 +114,21 @@ class Login extends StatelessWidget { if (loginResponse.statusCode == 200) { String apiKey = jsonDecode(utf8.decode( loginResponse.bodyBytes))['token']; - await prefs.setString('api_key', apiKey); + prefs.setString('api_key', apiKey); dynamic userinfo = jsonDecode( await getUserInfo( userController.text, passwordController.text, otpController.text, devIdController.text)); - await prefs.setString('user', + prefs.setString('user', userinfo['preferred_username']); - await prefs.setString( - 'name', userinfo['name']); - await prefs.setString( + prefs.setString('user', + userinfo['preferred_username']); + prefs.setString('name', userinfo['name']); + prefs.setString( + 'email', userinfo['email']); + prefs.setString( 'class_num', userinfo['groups'][0] .replaceAll("_", "/")); diff --git a/lib/main.dart b/lib/main.dart index 261669d..cdb3a93 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,32 +1,52 @@ import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'dashboard.dart'; -import 'login.dart'; +import 'package:meincantor/dashboard.dart'; +import 'package:meincantor/login.dart'; import 'dart:math'; +import 'package:background_fetch/background_fetch.dart'; + +void backgroundFetchHeadlessTask(HeadlessTask task) async { + String taskId = task.taskId; + bool isTimeout = task.timeout; + if (isTimeout) { + // This task has exceeded its allowed running-time. + // You must stop what you're doing and immediately .finish(taskId) + print("[BackgroundFetch] Headless task timed-out: $taskId"); + BackgroundFetch.finish(taskId); + return; + } + print('[BackgroundFetch] Headless event received.'); + // Do your work here... + BackgroundFetch.finish(taskId); +} void main() async { - WidgetsFlutterBinding.ensureInitialized(); + runApp(const App()); FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); // initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('app_icon'); - final IOSInitializationSettings initializationSettingsIOS = + const IOSInitializationSettings initializationSettingsIOS = IOSInitializationSettings(); - final MacOSInitializationSettings initializationSettingsMacOS = + const MacOSInitializationSettings initializationSettingsMacOS = MacOSInitializationSettings(); - final InitializationSettings initializationSettings = InitializationSettings( + const InitializationSettings initializationSettings = InitializationSettings( android: initializationSettingsAndroid, iOS: initializationSettingsIOS, macOS: initializationSettingsMacOS); await flutterLocalNotificationsPlugin.initialize(initializationSettings); - runApp(const App()); + BackgroundFetch.registerHeadlessTask(backgroundFetchHeadlessTask); } -class App extends StatelessWidget { +class App extends StatefulWidget { const App({Key? key}) : super(key: key); + @override + _AppState createState() => _AppState(); +} +class _AppState extends State { MaterialColor generateMaterialColor(Color color) { return MaterialColor(color.value, { 50: tintColor(color, 0.5), diff --git a/lib/networking.dart b/lib/networking.dart index fff30a9..02d3867 100644 --- a/lib/networking.dart +++ b/lib/networking.dart @@ -1,10 +1,7 @@ import 'dart:convert'; import 'dart:io'; -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:meincantor/cache_manager.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/painting.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:http/http.dart' as http; import 'package:flutter/material.dart'; @@ -16,14 +13,16 @@ import 'package:meincantor/login.dart'; import 'package:meincantor/main.dart'; Future getArticles() async { - var uri = Uri.https(szUrl["url"]!, "/articles"); - final response = await http.get(uri); + var uri = Uri.https(szUrl["url"]! as String, "/api/articles"); + final response = + await http.get(uri, headers: szUrl["headers"]! as Map); return (response); } Future getNews() async { - var uri = Uri.https(szUrl["url"]!, "/aktuelles"); - final response = await http.get(uri); + var uri = Uri.https(szUrl["url"]! as String, "/api/aktuelles"); + final response = + await http.get(uri, headers: szUrl["headers"]! as Map); return (response); } @@ -50,21 +49,9 @@ Future getUserInfo( } } -Future fetchClassTimetable(String ext) async { - FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); - const AndroidNotificationDetails androidPlatformChannelSpecifics = - AndroidNotificationDetails('de.cantorgymnasium.meincantor', 'GCG.MeinCantor', - channelDescription: '', - importance: Importance.max, - priority: Priority.high, - ticker: 'ticker'); - const NotificationDetails platformChannelSpecifics = - NotificationDetails(android: androidPlatformChannelSpecifics); - await flutterLocalNotificationsPlugin.show( - 0, 'Neuer Vertretungsplan geladen!', 'Du hast folgende Vertretungen:\nSt. 8 Deutsch Frau Rinke, Raum 203\nSt. 4 Biologie Frau Borchert, Raum 107', platformChannelSpecifics, - payload: 'item x'); +Future fetchClassTimetable(String ext, String? classNum) async { try { - return (http.Response(await getCachedTimetable(ext), 200)); + return (http.Response(await getCachedTimetable(ext, classNum), 200)); } on HttpExceptionWithStatus catch (e) { return http.Response(e.message, e.statusCode); } on HttpException catch (e) { @@ -72,20 +59,6 @@ Future fetchClassTimetable(String ext) async { } on SocketException catch (e) { return http.Response(e.message, 404); } - /*SharedPreferences prefs = await SharedPreferences.getInstance(); - String classNum; - if (prefs.getString('class_num') != null) { - classNum = prefs.getString('class_num')!.replaceAll("/", "_"); - } else { - classNum = '05_1'; - } - var apiKey = prefs.getString('api_key'); - var uri = Uri.https("mein.cantorgymnasium.de", "/api/timetable/$classNum$ext"); - var headers = {"x-api-key": "$apiKey"}; - final response = http.get(uri, headers: headers).onError((error, stackTrace) { - return (http.Response("", 404)); - }); - return response;*/ } fetchLessonList() async { @@ -180,7 +153,7 @@ Widget buildTimetable(Future future, String info) { Widget buildTodayClassTimetableLesson(int count) { return FutureBuilder( future: fetchClassTimetable( - "/${DateFormat("yyyyMMdd").format(DateTime.now())}"), + "/${DateFormat("yyyyMMdd").format(DateTime.now())}", null), builder: (context, snapshot) { if (snapshot.hasData) { int statusCode = snapshot.data!.statusCode; @@ -281,7 +254,7 @@ Widget buildClassesChooser() { if (statusCode == 200) { List items = []; jsonDecode(utf8.decode(snapshot.data!.bodyBytes)).forEach((value) { - items.add(value..toString()); + items.add(value.toString()); }); return ClassesChooser(items: items); } else if (statusCode == 400) { @@ -322,7 +295,6 @@ class ClassesChooser extends StatefulWidget { class _ClassesChooserState extends State { final List items; - //final String dropdownValue; String? dropdownValue; _ClassesChooserState(this.items); @@ -348,12 +320,15 @@ class _ClassesChooserState extends State { border: OutlineInputBorder(), labelText: 'Klasse (05/1, 07/3, 10/2...)', ), - onChanged: (String? newValue) { - setState(() async { - dropdownValue = newValue!; - SharedPreferences prefs = await SharedPreferences.getInstance(); - String classNum = newValue; - await prefs.setString('class_num', classNum); + onChanged: (String? newValue) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + String classNum = newValue!; + prefs.setString('class_num', classNum); + prefs.remove("lessons"); + prefs.remove("todayTImetable"); + prefs.remove("tomorrowTimetable"); + setState(() { + dropdownValue = newValue; }); }, items: items.map>((String value) { diff --git a/lib/news.dart b/lib/news.dart index 0e3d141..e7e5189 100644 --- a/lib/news.dart +++ b/lib/news.dart @@ -30,162 +30,197 @@ class _NewsState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text("Aktuelles"), - centerTitle: true, - ), - body: FutureBuilder( - future: getNews(), - builder: (context, snapshot) { - if (snapshot.hasData) { - int statusCode = snapshot.data!.statusCode; - if (statusCode == 200) { - String data = utf8.decode(snapshot.data!.bodyBytes); - List articles = jsonDecode(data); - List articleTiles = []; - for (var element in articles) { - Color color = Colors.white70; - Widget card = FutureBuilder( - future: getNewsRead(), - builder: (context, snapshot) { - if (snapshot.hasData) { - List readList = snapshot.data! as List; - if (!readList.contains(element["id"])) { - return GestureDetector( - onTap: () async { - SharedPreferences prefs = - await SharedPreferences.getInstance(); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => Article.fromData( - element["title"], - element["content"], - element["author"], - element["published_at"]) - .widget), - ); - readList.add(element["id"]); - prefs.setString("newsRead", jsonEncode(readList)); - setState(() { - color = Colors.transparent; - }); - }, - child: Card( - color: color, - child: Padding( - padding: - const EdgeInsets.fromLTRB(10, 10, 10, 10), - child: FutureBuilder( - future: Future.delayed( - const Duration(seconds: 0)), - builder: (context, snapshot) { - if (element["summary"] != null && - (element["summary"] as String) - .isNotEmpty) { - return ListTile( - title: Text(element["title"], - style: const TextStyle( - fontWeight: FontWeight.bold)), - subtitle: Text( - element["summary"], - overflow: TextOverflow.ellipsis, - maxLines: 2, - softWrap: true, - ), - trailing: Text( - "${DateTime.parse(element["published_at"]).day.toString()}.${DateTime.parse(element["published_at"]).month.toString()}.${DateTime.parse(element["published_at"]).year.toString()}"), - ); - } else { - return ListTile( - title: Text(element["title"], - style: const TextStyle( - fontWeight: FontWeight.bold)), - trailing: Text( - "${DateTime.parse(element["published_at"]).day.toString()}.${DateTime.parse(element["published_at"]).month.toString()}.${DateTime.parse(element["published_at"]).year.toString()}"), - ); - } - }, - )), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - )), - ); - } else { - return GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => Article.fromData( - element["title"], - element["content"], - element["author"], - element["published_at"]) - .widget), - ); - }, - child: Card( - child: Padding( - padding: - const EdgeInsets.fromLTRB(10, 10, 10, 10), - child: FutureBuilder( - future: Future.delayed( - const Duration(seconds: 0)), - builder: (context, snapshot) { - if (element["summary"] != null && - (element["summary"] as String) - .isNotEmpty) { - return ListTile( - title: Text(element["title"], - style: const TextStyle( - fontWeight: FontWeight.bold)), - subtitle: Text( - element["summary"], - overflow: TextOverflow.ellipsis, - maxLines: 2, - softWrap: true, - ), - trailing: Text( - "${DateTime.parse(element["published_at"]).day.toString()}.${DateTime.parse(element["published_at"]).month.toString()}.${DateTime.parse(element["published_at"]).year.toString()}"), - ); - } else { - return ListTile( - title: Text(element["title"], - style: const TextStyle( - fontWeight: FontWeight.bold) - ), - trailing: Text( - "${DateTime.parse(element["published_at"]).day.toString()}.${DateTime.parse(element["published_at"]).month.toString()}.${DateTime.parse(element["published_at"]).year.toString()}"), - ); - } - }, - )), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - )), - ); - } - } else { - return const LinearProgressIndicator(); - } - }, - ); - articleTiles.add(card); - } - return ListView( - children: articleTiles.reversed.toList(), - ); - } else { - return (const Center( - child: Text("Uups... Irgendwas ist schief gelaufen"))); - } + appBar: AppBar( + title: const Text("Aktuelles"), + centerTitle: true, + ), + body: LayoutBuilder(builder: (context, constraints) { + double widgetWidth = constraints.maxWidth; + + int factor; + + if (widgetWidth <= 600) { + factor = 1; + } else if (widgetWidth <= 1400) { + factor = 2; + } else if (widgetWidth <= 2000) { + factor = 3; } else { - return (const Center(child: CircularProgressIndicator())); + factor = 1; } - }, - ), - ); + + return Center( + heightFactor: 1, + child: Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width / factor, + ), + child: FutureBuilder( + future: getNews(), + builder: (context, snapshot) { + if (snapshot.hasData) { + int statusCode = snapshot.data!.statusCode; + if (statusCode == 200) { + String data = utf8.decode(snapshot.data!.bodyBytes); + List articles = jsonDecode(data)["data"]; + List articleTiles = []; + for (var element in articles) { + int id = element["id"]; + element = element["attributes"]; + Color color = Colors.white70; + Widget card = FutureBuilder( + future: getNewsRead(), + builder: (context, snapshot) { + if (snapshot.hasData) { + List readList = + snapshot.data! as List; + if (!readList.contains(id)) { + return GestureDetector( + onTap: () async { + SharedPreferences prefs = + await SharedPreferences.getInstance(); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + Article.fromData( + element["title"], + element["content"], + element["author"], + element["publish_date"]) + .widget), + ); + readList.add(id); + prefs.setString( + "newsRead", jsonEncode(readList)); + setState(() { + color = Colors.transparent; + }); + }, + child: Card( + color: color, + child: Padding( + padding: const EdgeInsets.fromLTRB( + 10, 10, 10, 10), + child: FutureBuilder( + future: Future.delayed( + const Duration(seconds: 0)), + builder: (context, snapshot) { + if (element["summary"] != null && + (element["summary"] as String) + .isNotEmpty) { + return ListTile( + title: Text(element["title"], + style: const TextStyle( + fontWeight: + FontWeight.bold)), + subtitle: Text( + element["summary"], + overflow: + TextOverflow.ellipsis, + maxLines: 2, + softWrap: true, + ), + trailing: Text( + "${DateTime.parse(element["publish_date"]).day.toString()}.${DateTime.parse(element["publish_date"]).month.toString()}.${DateTime.parse(element["publish_date"]).year.toString()}"), + ); + } else { + return ListTile( + title: Text(element["title"], + style: const TextStyle( + fontWeight: + FontWeight.bold)), + trailing: Text( + "${DateTime.parse(element["publish_date"]).day.toString()}.${DateTime.parse(element["publish_date"]).month.toString()}.${DateTime.parse(element["publish_date"]).year.toString()}"), + ); + } + }, + )), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + )), + ); + } else { + return GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + Article.fromData( + element["title"], + element["content"], + element["author"], + element["publish_date"]) + .widget), + ); + }, + child: Card( + child: Padding( + padding: const EdgeInsets.fromLTRB( + 10, 10, 10, 10), + child: FutureBuilder( + future: Future.delayed( + const Duration(seconds: 0)), + builder: (context, snapshot) { + if (element["summary"] != null && + (element["summary"] as String) + .isNotEmpty) { + return ListTile( + title: Text(element["title"], + style: const TextStyle( + fontWeight: + FontWeight.bold)), + subtitle: Text( + element["summary"], + overflow: + TextOverflow.ellipsis, + maxLines: 2, + softWrap: true, + ), + trailing: Text( + "${DateTime.parse(element["publish_date"]).day.toString()}.${DateTime.parse(element["publish_date"]).month.toString()}.${DateTime.parse(element["publish_date"]).year.toString()}"), + ); + } else { + return ListTile( + title: Text(element["title"], + style: const TextStyle( + fontWeight: + FontWeight.bold)), + trailing: Text( + "${DateTime.parse(element["publish_date"]).day.toString()}.${DateTime.parse(element["publish_date"]).month.toString()}.${DateTime.parse(element["publish_date"]).year.toString()}"), + ); + } + }, + )), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + )), + ); + } + } else { + return const LinearProgressIndicator(); + } + }, + ); + articleTiles.add(card); + } + return ListView( + children: articleTiles.reversed.toList(), + ); + } else { + return (const Center( + child: + Text("Uups... Irgendwas ist schief gelaufen"))); + } + } else { + return (const Center(child: CircularProgressIndicator())); + } + }, + ), + ), + ); + })); } } diff --git a/lib/notifications.dart b/lib/notifications.dart deleted file mode 100644 index 8b13789..0000000 --- a/lib/notifications.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/lib/presets/colors.dart b/lib/presets/colors.dart index 8704493..f34417c 100644 --- a/lib/presets/colors.dart +++ b/lib/presets/colors.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; Map colors = { 'Bio': Colors.green, + 'bio1': Colors.green, + 'bioL1': Colors.green, 'Mat': Colors.indigo, 'matL1': Colors.indigo, 'matL2': Colors.indigo, @@ -10,20 +12,26 @@ Map colors = { 'deu1': Colors.red, 'deu2': Colors.red, 'deu3': Colors.red, + 'deuL1': Colors.red, 'Kun': Colors.deepPurple, 'kun1': Colors.deepPurple, 'kun2': Colors.deepPurple, 'kun3': Colors.deepPurple, 'Geo': Colors.brown, + 'geo1': Colors.brown, 'Lat': Colors.teal, 'lat1': Colors.teal, 'lat2': Colors.teal, 'lat3': Colors.teal, 'Che': Colors.lightGreen, + 'che1': Colors.lightGreen, + 'cheL1': Colors.lightGreen, 'Eng': Colors.amber, 'eng1': Colors.amber, 'eng2': Colors.amber, 'eng3': Colors.amber, + 'engL1': Colors.amber, + 'engL2': Colors.amber, 'Phy': Colors.cyan, 'phy1': Colors.cyan, 'phy2': Colors.cyan, diff --git a/lib/presets/subjects.dart b/lib/presets/subjects.dart index 0bd39d3..45283c5 100644 --- a/lib/presets/subjects.dart +++ b/lib/presets/subjects.dart @@ -1,30 +1,63 @@ dynamic subjects = { '---': '---', 'Bio': 'Biologie', + 'bio1': 'Biologie 1', + 'bioL1': 'Biologie Leistungskurs 1', 'Mat': 'Mathematik', 'matL1': 'Mathematik Leistungskurs 1', + 'matL2': 'Mathematik Leistungskurs 2', + 'matL3': 'Mathematik Leistungskurs 3', 'Kun': 'Kunst', + 'kun1': 'Kunst 1', 'Mus': 'Musik', + 'mus1': 'Musik 1', + 'mus2': 'Musik 2', 'Geo': 'Geographie', + 'geo1': 'Geographie 1', 'Ges': 'Geschichte', + 'ges1': 'Geschichte 1', + 'ges2': 'Geschichte 2', + 'ges3': 'Geschichte 3', 'Che': 'Chemie', + 'che1': 'Chemie 1', + 'cheL1': 'Chemie Leistungskurs 1', 'Lat': 'Latein', + 'lat1': 'Latein 1', 'Inf': 'Informatik', + 'inf1': 'Informatik 1', + 'inf2': 'Informatik 2', + 'infL1': 'Informatik Leistungskurs 1', 'Eng': 'Englisch', + 'eng1': 'Englisch 1', + 'eng2': 'Englisch 2', + 'engL1': 'Englisch Leistungskurs 1', + 'engL2': 'Englisch Leistungskurs 2', 'Frz': 'Französisch', 'frz1': 'Französisch 1', 'frz2': 'Französisch 2', 'frz3': 'Französisch 3', 'Phy': 'Physik', + 'phy1': 'Physik 1', + 'phyL1': 'Physik Leistungskurs 1', + 'phyL2': 'Physik Leistungskurs 2', + 'psy1': 'Psychologie 1', 'Spo': 'Sport', + 'spo1': 'Sport 1', + 'spo2': 'Sport 2', + 'spo3': 'Sport 3', 'Deu': 'Deutsch', 'deu1': 'Deutsch 1', 'deu2': 'Deutsch 2', 'deu3': 'Deutsch 3', + 'deuL1': 'Deutsch Leistungskurs 1', 'Lme': 'Lernmethoden', 'Eth': 'Ethik', + 'eth1': 'Ethik 1', 'EvR': 'Evangelische Religion', + 'evr1': 'Evangelische Religion 1', 'Soz': 'Sozialkunde', + 'soz1': 'Sozialkunde 1', + 'soz2': 'Sozialkunde 2', 'Ast': 'Astronomie', 'Spa': 'Spanisch', 'FK': 'Fachkurs', diff --git a/lib/raumuebersicht.dart b/lib/raumuebersicht.dart index 2cf97d9..0f5659b 100644 --- a/lib/raumuebersicht.dart +++ b/lib/raumuebersicht.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class RoomOverview extends StatelessWidget { diff --git a/lib/saved.dart b/lib/saved.dart new file mode 100644 index 0000000..01c82b4 --- /dev/null +++ b/lib/saved.dart @@ -0,0 +1,198 @@ +// SizedBox( + // width: 170, + // child: GestureDetector( + // onTap: () async { + // Navigator.push( + // context, + // MaterialPageRoute(builder: (context) => const SB()), + // ); + // }, + // child: Card( + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(10), + // ), + // child: const Padding( + // padding: EdgeInsets.all(10), + // child: ListTile( + // title: Padding( + // padding: EdgeInsets.fromLTRB(0, 0, 0, 10), + // child: Icon( + // MdiIcons.libraryShelves, + // color: Palette.accent, + // size: 48, + // ), + // ), + // subtitle: Center( + // child: Padding( + // padding: EdgeInsets.fromLTRB(0, 10, 0, 0), + // child: Text( + // 'Schulbibliothek', + // ), + // ), + // ), + // ), + // )), + // ), + // ), + // SizedBox( + // width: 170, + // child: GestureDetector( + // onTap: () async { + // Navigator.push( + // context, + // MaterialPageRoute(builder: (context) => const SC()), + // ); + // }, + // child: Card( + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(10), + // ), + // child: const Padding( + // padding: EdgeInsets.all(10), + // child: ListTile( + // title: Padding( + // padding: EdgeInsets.fromLTRB(0, 0, 0, 10), + // child: Icon( + // MdiIcons.laptop, + // color: Palette.accent, + // size: 48, + // ), + // ), + // subtitle: Center( + // child: Padding( + // padding: EdgeInsets.fromLTRB(0, 10, 0, 0), + // child: Text( + // 'Schulcomputer', + // ), + // ), + // ), + // ), + // )), + // ), + // ), + // SizedBox( + // width: 170, + // child: GestureDetector( + // onTap: () async { + // Navigator.push( + // context, + // MaterialPageRoute( + // builder: (context) => const RoomOverview()), + // ); + // }, + // child: Card( + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(10), + // ), + // child: const Padding( + // padding: EdgeInsets.all(10), + // child: ListTile( + // title: Padding( + // padding: EdgeInsets.fromLTRB(0, 0, 0, 10), + // child: Icon( + // MdiIcons.door, + // color: Palette.accent, + // size: 48, + // ), + // ), + // subtitle: Center( + // child: Padding( + // padding: EdgeInsets.fromLTRB(0, 10, 0, 0), + // child: Text( + // 'Raumübersicht', + // ), + // ), + // ), + // ), + // )), + // ), + // ), + + + + // ListTile( + // leading: + // const Icon(MdiIcons.timetable, color: Colors.orangeAccent), + // trailing: const Icon(Icons.arrow_forward_ios, size: 16), + // title: const Text("Plan"), + // subtitle: const Text("Kurse/Fächer, Farben & mehr"), + // onTap: () { + // Navigator.push( + // context, + // MaterialPageRoute(builder: (context) => const PlanSettings()), + // ); + // }, + // ), + // ListTile( + // leading: const Icon(Icons.color_lens_outlined, + // color: Colors.pinkAccent), + // trailing: const Icon(Icons.arrow_forward_ios, size: 16), + // title: const Text("Aussehen"), + // subtitle: const Text("Widgets, Design & mehr"), + // onTap: () { + // Navigator.push( + // context, + // MaterialPageRoute( + // builder: (context) => const AppearanceSettings()), + // ); + // }, + // ), + // ListTile( + // leading: const Icon(MdiIcons.server, color: Colors.lightGreen), + // trailing: const Icon(Icons.arrow_forward_ios, size: 16), + // title: const Text("Dienste"), + // subtitle: const Text("Konten, Plattformen & mehr"), + // onTap: () { + // Navigator.push( + // context, + // MaterialPageRoute( + // builder: (context) => const ServiceSettings()), + // ); + // }, + // ), + +// class PlanSettings extends StatelessWidget { +// const PlanSettings({Key? key}) : super(key: key); + +// @override +// Widget build(BuildContext context) { +// return Scaffold( +// appBar: AppBar( +// title: const Text("Plan"), +// centerTitle: true, +// ), +// body: ListView( +// padding: const EdgeInsets.fromLTRB(5, 5, 5, 5), +// children: [ +// ListTile( +// leading: const Icon(Icons.list_alt_outlined, color: Colors.red), +// trailing: const Icon(Icons.arrow_forward_ios, size: 16), +// title: const Text("Kurse"), +// subtitle: const Text("Konfiguration der Kurse"), +// onTap: () { +// Navigator.push( +// context, +// MaterialPageRoute( +// builder: (context) => const WhitelistSettings()), +// ); +// }, +// ), +// ListTile( +// leading: +// const Icon(Icons.color_lens_outlined, color: Colors.teal), +// trailing: const Icon(Icons.arrow_forward_ios, size: 16), +// title: const Text("Farben"), +// subtitle: +// const Text("Konfiguration der Farben für die Plankacheln"), +// onTap: () { +// Navigator.push( +// context, +// MaterialPageRoute( +// builder: (context) => const PlanColorSettings()), +// ); +// }, +// ), +// ], +// )); +// } +// } \ No newline at end of file diff --git a/lib/schuelerzeitung.dart b/lib/schuelerzeitung.dart index e211464..4de519f 100644 --- a/lib/schuelerzeitung.dart +++ b/lib/schuelerzeitung.dart @@ -5,6 +5,8 @@ import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:http/http.dart' as http; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:markdown/markdown.dart' as md; Future getSZread() async { SharedPreferences prefs = await SharedPreferences.getInstance(); @@ -30,161 +32,197 @@ class _SZState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text("Schülerzeitung"), - centerTitle: true, - ), - body: FutureBuilder( - future: getArticles(), - builder: (context, snapshot) { - if (snapshot.hasData) { - int statusCode = snapshot.data!.statusCode; - if (statusCode == 200) { - String data = utf8.decode(snapshot.data!.bodyBytes); - List articles = jsonDecode(data); - List articleTiles = []; - for (var element in articles) { - Color color = Colors.white70; - Widget card = FutureBuilder( - future: getSZread(), - builder: (context, snapshot) { - if (snapshot.hasData) { - List readList = snapshot.data! as List; - if (!readList.contains(element["id"])) { - return GestureDetector( - onTap: () async { - SharedPreferences prefs = - await SharedPreferences.getInstance(); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => Article.fromData( - element["title"], - element["content"], - element["author"], - element["published_at"]) - .widget), - ); - readList.add(element["id"]); - prefs.setString("SZread", jsonEncode(readList)); - setState(() { - color = Colors.transparent; - }); - }, - child: Card( - color: color, - child: Padding( - padding: - const EdgeInsets.fromLTRB(10, 10, 10, 10), - child: FutureBuilder( - future: Future.delayed( - const Duration(seconds: 0)), - builder: (context, snapshot) { - if (element["summary"] != null && - (element["summary"] as String) - .isNotEmpty) { - return ListTile( - title: Text(element["title"], - style: const TextStyle( - fontWeight: FontWeight.bold)), - subtitle: Text( - element["summary"], - overflow: TextOverflow.ellipsis, - maxLines: 2, - softWrap: true, - ), - trailing: Text( - "${DateTime.parse(element["published_at"]).day.toString()}.${DateTime.parse(element["published_at"]).month.toString()}.${DateTime.parse(element["published_at"]).year.toString()}"), - ); - } else { - return ListTile( - title: Text(element["title"], - style: const TextStyle( - fontWeight: FontWeight.bold)), - trailing: Text( - "${DateTime.parse(element["published_at"]).day.toString()}.${DateTime.parse(element["published_at"]).month.toString()}.${DateTime.parse(element["published_at"]).year.toString()}"), - ); - } - }, - )), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - )), - ); - } else { - return GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => Article.fromData( - element["title"], - element["content"], - element["author"], - element["published_at"]) - .widget), - ); - }, - child: Card( - child: Padding( - padding: - const EdgeInsets.fromLTRB(10, 10, 10, 10), - child: FutureBuilder( - future: Future.delayed( - const Duration(seconds: 0)), - builder: (context, snapshot) { - if (element["summary"] != null && - (element["summary"] as String) - .isNotEmpty) { - return ListTile( - title: Text(element["title"], - style: const TextStyle( - fontWeight: FontWeight.bold)), - subtitle: Text( - element["summary"], - overflow: TextOverflow.ellipsis, - maxLines: 2, - softWrap: true, - ), - trailing: Text( - "${DateTime.parse(element["published_at"]).day.toString()}.${DateTime.parse(element["published_at"]).month.toString()}.${DateTime.parse(element["published_at"]).year.toString()}"), - ); - } else { - return ListTile( - title: Text(element["title"], - style: const TextStyle( - fontWeight: FontWeight.bold)), - trailing: Text( - "${DateTime.parse(element["published_at"]).day.toString()}.${DateTime.parse(element["published_at"]).month.toString()}.${DateTime.parse(element["published_at"]).year.toString()}"), - ); - } - }, - )), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - )), - ); - } - } else { - return const LinearProgressIndicator(); - } - }, - ); - articleTiles.add(card); - } - return ListView( - children: articleTiles.reversed.toList(), - ); - } else { - return (const Center( - child: Text("Uups... Irgendwas ist schief gelaufen"))); - } + appBar: AppBar( + title: const Text("Schülerzeitung"), + centerTitle: true, + ), + body: LayoutBuilder(builder: (context, constraints) { + double widgetWidth = constraints.maxWidth; + + int factor; + + if (widgetWidth <= 600) { + factor = 1; + } else if (widgetWidth <= 1400) { + factor = 2; + } else if (widgetWidth <= 2000) { + factor = 3; } else { - return (const Center(child: CircularProgressIndicator())); + factor = 1; } - }, - ), - ); + + return Center( + heightFactor: 1, + child: Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width / factor, + ), + child: FutureBuilder( + future: getArticles(), + builder: (context, snapshot) { + if (snapshot.hasData) { + int statusCode = snapshot.data!.statusCode; + if (statusCode == 200) { + String data = utf8.decode(snapshot.data!.bodyBytes); + List articles = jsonDecode(data)["data"]; + List articleTiles = []; + for (var element in articles) { + int id = element["id"]; + element = element["attributes"]; + Color color = Colors.white70; + Widget card = FutureBuilder( + future: getSZread(), + builder: (context, snapshot) { + if (snapshot.hasData) { + List readList = + snapshot.data! as List; + if (!readList.contains(id)) { + return GestureDetector( + onTap: () async { + SharedPreferences prefs = + await SharedPreferences.getInstance(); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + Article.fromData( + element["title"], + element["content"], + element["author"], + element["publish_date"]) + .widget), + ); + readList.add(id); + prefs.setString( + "SZread", jsonEncode(readList)); + setState(() { + color = Colors.transparent; + }); + }, + child: Card( + color: color, + child: Padding( + padding: const EdgeInsets.fromLTRB( + 10, 10, 10, 10), + child: FutureBuilder( + future: Future.delayed( + const Duration(seconds: 0)), + builder: (context, snapshot) { + if (element["summary"] != null && + (element["summary"] as String) + .isNotEmpty) { + return ListTile( + title: Text(element["title"], + style: const TextStyle( + fontWeight: + FontWeight.bold)), + subtitle: Text( + element["summary"], + overflow: + TextOverflow.ellipsis, + maxLines: 2, + softWrap: true, + ), + trailing: Text( + "${DateTime.parse(element["publish_date"]).day.toString()}.${DateTime.parse(element["publish_date"]).month.toString()}.${DateTime.parse(element["publish_date"]).year.toString()}"), + ); + } else { + return ListTile( + title: Text(element["title"], + style: const TextStyle( + fontWeight: + FontWeight.bold)), + trailing: Text( + "${DateTime.parse(element["publish_date"]).day.toString()}.${DateTime.parse(element["publish_date"]).month.toString()}.${DateTime.parse(element["publish_date"]).year.toString()}"), + ); + } + }, + )), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + )), + ); + } else { + return GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + Article.fromData( + element["title"], + element["content"], + element["author"], + element["publish_date"]) + .widget), + ); + }, + child: Card( + child: Padding( + padding: const EdgeInsets.fromLTRB( + 10, 10, 10, 10), + child: FutureBuilder( + future: Future.delayed( + const Duration(seconds: 0)), + builder: (context, snapshot) { + if (element["summary"] != null && + (element["summary"] as String) + .isNotEmpty) { + return ListTile( + title: Text(element["title"], + style: const TextStyle( + fontWeight: + FontWeight.bold)), + subtitle: Text( + element["summary"], + overflow: + TextOverflow.ellipsis, + maxLines: 2, + softWrap: true, + ), + trailing: Text( + "${DateTime.parse(element["publish_date"]).day.toString()}.${DateTime.parse(element["publish_date"]).month.toString()}.${DateTime.parse(element["publish_date"]).year.toString()}"), + ); + } else { + return ListTile( + title: Text(element["title"], + style: const TextStyle( + fontWeight: + FontWeight.bold)), + trailing: Text( + "${DateTime.parse(element["publish_date"]).day.toString()}.${DateTime.parse(element["publish_date"]).month.toString()}.${DateTime.parse(element["publish_date"]).year.toString()}"), + ); + } + }, + )), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + )), + ); + } + } else { + return const LinearProgressIndicator(); + } + }, + ); + articleTiles.add(card); + } + return ListView( + children: articleTiles.reversed.toList(), + ); + } else { + return (const Center( + child: + Text("Uups... Irgendwas ist schief gelaufen"))); + } + } else { + return (const Center(child: CircularProgressIndicator())); + } + }, + ), + ), + ); + })); } } @@ -216,7 +254,14 @@ class Article { title: Text( "${DateTime.parse(publishDate).day.toString()}.${DateTime.parse(publishDate).month.toString()}.${DateTime.parse(publishDate).year.toString()}"), ), - MarkdownBody(data: content) + MarkdownBody( + data: content, + onTapLink: (text, url, title) { + launch(url!); + }, + extensionSet: md.ExtensionSet.commonMark, + imageDirectory: "https://cms.mein.cantorgymnasium.de", + ) ], ))); } diff --git a/lib/schulbibliothek.dart b/lib/schulbibliothek.dart index 81c4912..5073a90 100644 --- a/lib/schulbibliothek.dart +++ b/lib/schulbibliothek.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class SB extends StatelessWidget { diff --git a/lib/schulcomputer.dart b/lib/schulcomputer.dart index e5d7f97..7f754c5 100644 --- a/lib/schulcomputer.dart +++ b/lib/schulcomputer.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class SC extends StatelessWidget { diff --git a/lib/timetable.dart b/lib/timetable.dart index 681c00b..b62c255 100644 --- a/lib/timetable.dart +++ b/lib/timetable.dart @@ -3,7 +3,6 @@ import 'package:meincantor/presets/subjects.dart'; import 'package:meincantor/presets/colors.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -25,8 +24,10 @@ class ClassTimetableBuilder { onTap: () { showModalBottomSheet( isScrollControlled: true, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(25.0), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(25.0), + topRight: Radius.circular(25.0)), ), context: context, builder: (BuildContext context) { @@ -35,6 +36,8 @@ class ClassTimetableBuilder { child: ListView( children: [ ListTile( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(25.0)), title: const Text("Informationen", style: TextStyle(fontWeight: FontWeight.bold)), leading: const Icon(Icons.arrow_back), @@ -90,15 +93,15 @@ class LessonsListBuilder { title: Text(element.count.toString() + '.' + ' ' + element.name, style: TextStyle(color: element.fontColor)), subtitle: Row(children: [ - Icon(CupertinoIcons.person, color: element.fontColor), + Icon(Icons.person_outline, color: element.fontColor), const SizedBox(width: 5), Text(element.teacher, style: TextStyle(color: element.fontColor)), const Spacer(), - Icon(CupertinoIcons.home, color: element.fontColor), + Icon(MdiIcons.door, color: element.fontColor), const SizedBox(width: 5), Text(element.room, style: TextStyle(color: element.fontColor)) ]), - leading: Icon(CupertinoIcons.time, color: element.fontColor))); + leading: Icon(MdiIcons.clockOutline, color: element.fontColor))); if (element.info != '') { cardChildren.add(ListTile( title: Text(element.info, @@ -150,7 +153,7 @@ class TimetableInfo { } class ClassTimetable { - final List timetable; + final List timetable; ClassTimetable({required this.timetable}); factory ClassTimetable.fromJson(Map json) { List lessons = []; @@ -201,11 +204,10 @@ class ClassTimetable { lessons.add(TimetableLesson( value['St'], - value["Nr"], - subjects[subject] ?? subject.toString(), + value["Nr"] ?? 0, + subject == ' ' ? "---" : subjects[subject] ?? subject.toString(), teachers[teacher] ?? teacher.toString(), room.toString(), - value['If'].toString(), lessonColor, fontColor, info)); @@ -221,10 +223,9 @@ class TimetableLesson { final String name; final String teacher; final String room; - final String comment; final Future color; final Color fontColor; final String info; const TimetableLesson(this.count, this.id, this.name, this.teacher, this.room, - this.comment, this.color, this.fontColor, this.info); + this.color, this.fontColor, this.info); } diff --git a/pubspec.yaml b/pubspec.yaml index 4bd95bc..c183dfd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.8.0-dev +version: 1.0.0 environment: sdk: ">=2.12.0 <3.0.0" @@ -31,7 +31,7 @@ dependencies: 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 + # cupertino_icons: ^1.0.2 shared_preferences: ^2.0.6 http: ^0.13.3 google_fonts: ^2.1.0 @@ -45,7 +45,7 @@ dependencies: url_launcher: ^6.0.17 flutter_linkify: ^5.0.2 flutter_svg: ^1.0.0 - webview_flutter: ^3.0.0 + webviewx: ^0.2.1 flutter_local_notifications: ^10.0.0-dev.1 background_fetch: ^1.0.3 @@ -83,6 +83,8 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - assets/images/meincantor_r.png + - assets/images/meincantor-big.png + - assets/images/meincantor-big-dark.png # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see