// GCG.MeinCantor - Die Schulplattform für Cantorianer. // Copyright (C) 2021-2022 Georg-Cantor-Gymnasium Halle (Saale) // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published // by the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . 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/schuelerzeitung.dart'; import 'package:meincantor/Settings/dashboard.dart'; import 'package:meincantor/main.dart'; import 'package:meincantor/networking.dart'; import 'package:meincantor/login.dart'; import 'package:flutter/material.dart'; 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 'package:meincantor/news.dart'; Future getSettingsString(String key) async { SharedPreferences prefs = await SharedPreferences.getInstance(); String? value = prefs.getString(key); if (value == null || value.isEmpty) { value = ""; } return value; } Widget buildSettingsString(String key, TextStyle? style) { return FutureBuilder( future: getSettingsString(key), builder: (context, snapshot) { if (snapshot.hasData) { return Text(snapshot.data as String, style: style); } else { 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) { var bottomNavBarItems = [ const BottomNavigationBarItem( icon: Icon(MdiIcons.homeOutline), label: "Startseite", ), const BottomNavigationBarItem( icon: Icon(MdiIcons.timetable), label: "Vertretungsplan"), ]; return Scaffold( body: _DashboardBottomNavView( key: UniqueKey(), item: bottomNavBarItems[_currentIndex.value]), bottomNavigationBar: BottomNavigationBar( showUnselectedLabels: false, items: bottomNavBarItems, currentIndex: _currentIndex.value, onTap: (index) { setState(() { _currentIndex.value = index; }); }, ), ); } @override String? get restorationId => widget.restorationId; @override void restoreState(RestorationBucket? oldBucket, bool initialRestore) { registerForRestoration(_currentIndex, 'bottom_navigation_tab_index'); } } 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; _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: [ Row( mainAxisSize: MainAxisSize.max, children: [ Padding( padding: const EdgeInsets.fromLTRB(20, 30, 20, 5), child: Text( 'Hallo,', style: GoogleFonts.robotoSlab( fontSize: 20, ), ), ), ], ), Row( mainAxisSize: MainAxisSize.max, children: [ Padding( padding: const EdgeInsets.fromLTRB(20, 5, 20, 20), child: buildSettingsString( "name", GoogleFonts.robotoSlab( fontSize: 28, fontWeight: FontWeight.w800, ), ), ) ], ), Row( mainAxisSize: MainAxisSize.max, children: [ Padding( padding: const EdgeInsets.fromLTRB(20, 10, 0, 10), child: Text( 'Deine nächste Unterrichtsstunde:', style: GoogleFonts.robotoSlab( fontSize: 20, fontWeight: FontWeight.w100, ), ), ) ], ), Padding( padding: const EdgeInsets.fromLTRB(20, 20, 20, 10), child: buildTodayClassTimetableLesson(lessonCount), ), Padding( padding: const EdgeInsets.fromLTRB(20, 20, 20, 10), child: Wrap( children: [ SizedBox( 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'), ), ), ), )), )), SizedBox( width: 175, child: GestureDetector( onTap: () async { Navigator.push( context, MaterialPageRoute(builder: (context) => const News()), ); }, 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.newspaperVariantOutline, color: Palette.accent, size: 48, ), ), subtitle: Center( child: Padding( padding: EdgeInsets.fromLTRB(0, 10, 0, 0), child: Text( 'Aktuelles', ), ), ), ), )), ), ) ], ), ), 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 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") { List 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( 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...")); } } }