==Initial version==

- many hardcoded placeholders
- TODO:
  - update README
  - update license & copyright info
  - cleanup code
  - merge code parts to server side
  - muuuuuuuch more...
This commit is contained in:
Denys Konovalov 2021-08-27 19:24:30 +02:00
parent 38c9e85166
commit 20a50d66f7
11 changed files with 1177 additions and 0 deletions

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="meincantor_app_icon_background">#1A1A37</color>
</resources>

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="512"
height="512"
viewBox="0 0 135.46666 135.46667"
version="1.1"
id="svg826"
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
sodipodi:docname="meincantor-round.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview828"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
units="px"
inkscape:zoom="0.69130191"
inkscape:cx="155.50369"
inkscape:cy="289.3092"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs823" />
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1">
<circle
style="fill:#1a1a37;fill-opacity:1;stroke:#1a1a37;stroke-width:0.450144"
id="path909"
cx="67.733337"
cy="67.73333"
r="67.508263" />
<path
style="fill:#ffbc3b;fill-opacity:1;stroke-width:0.101646"
d="m 64.24865,118.49569 c -7.57703,-0.68874 -13.07166,-2.17953 -19.26191,-5.2261 -5.07555,-2.49796 -9.21616,-5.48508 -13.36935,-9.64492 -4.24296,-4.249748 -7.18643,-8.372728 -9.76459,-13.677468 -2.38531,-4.90795 -3.83281,-9.66485 -4.68595,-15.39936 -0.22167,-1.49001 -0.23434,-1.86236 -0.23348,-6.8611 0.001,-5.75731 0.0112,-5.8938 0.70967,-9.50389 1.41498,-7.31325 4.31986,-14.09787 8.56776,-20.0108 1.08578,-1.51136 1.26945,-1.74578 2.75696,-3.5186 1.18989,-1.41811 4.6051,-4.79494 6.00686,-5.93934 3.49341,-2.85204 6.3554,-4.71248 10.01212,-6.5084 5.63599,-2.76798 11.01697,-4.33808 17.27981,-5.04202 1.95056,-0.21925 6.49813,-0.3028 8.6399,-0.15874 10.85994,0.73043 21.22025,4.91354 29.47732,11.90184 2.31994,1.96346 5.11657,4.85756 7.09311,7.34031 1.57267,1.97545 4.42364,6.27134 4.29746,6.47549 -0.0302,0.0489 -0.7301,0.46721 -1.55528,0.92956 -0.82517,0.46235 -3.16985,1.77924 -5.2104,2.92642 -2.04054,1.14718 -4.41905,2.48273 -5.285584,2.96789 -0.866532,0.48516 -2.853543,1.5992 -4.415576,2.47565 -1.56203,0.87645 -2.89756,1.57148 -2.96783,1.54451 -0.0703,-0.027 -0.20565,-0.20004 -0.30085,-0.38461 -0.0952,-0.18456 -0.48635,-0.80286 -0.86925,-1.37398 -4.1041,-6.12164 -10.66138,-10.54941 -17.7976,-12.01774 -2.2308,-0.45901 -3.29135,-0.55802 -5.9723,-0.55754 -2.92886,5.9e-4 -4.34012,0.16701 -6.9277,0.81733 -7.54602,1.89648 -14.13772,6.9369 -17.91778,13.70103 -2.42291,4.33561 -3.62944,8.99167 -3.62434,13.98646 0.005,4.49962 0.87661,8.34811 2.79831,12.34998 1.52084,3.16708 3.34414,5.69426 5.87291,8.14011 4.37146,4.22814 9.64698,6.83554 15.74744,7.78309 1.95735,0.30402 6.10417,0.32974 7.9137,0.0491 5.50205,-0.85338 10.03859,-2.80989 14.13339,-6.09541 2.48209,-1.99153 5.02262,-4.91055 6.59534,-7.57789 l 0.29239,-0.4959 -8.29976,-4.8114 c -14.86737,-8.61866 -16.12525,-9.35129 -16.22306,-9.44874 -0.0607,-0.0605 4.47285,-0.0962 12.22224,-0.0962 h 12.3188 l 0.0261,7.12925 0.0261,7.12924 1.01647,0.57703 c 0.55905,0.31738 2.20571,1.24258 3.659248,2.05601 2.917256,1.63257 9.810502,5.49793 12.783782,7.16846 1.04922,0.5895 1.92802,1.10474 1.95288,1.14498 0.11707,0.18942 -1.95992,3.3969 -3.62221,5.59375 -6.11193,8.077428 -14.10926,13.942658 -23.64336,17.339998 -3.49711,1.24615 -7.65379,2.19049 -11.61375,2.63848 -1.40508,0.15896 -7.48873,0.2887 -8.63815,0.18422 z"
id="path6437" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="512"
height="512"
viewBox="0 0 135.46666 135.46666"
version="1.1"
id="svg825"
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
sodipodi:docname="meincantor.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview827"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
units="px"
inkscape:zoom="0.34565095"
inkscape:cx="392.01396"
inkscape:cy="324.0263"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs822" />
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#1a1a37;stroke:#1a1a37;stroke-width:0.407031;fill-opacity:1;stroke-opacity:1"
id="rect908"
width="135.05963"
height="135.05963"
x="0.20351535"
y="0.20351535" />
<path
style="fill:#ffbc3b;stroke-width:0.0677639;fill-opacity:1"
d="m 65.410221,101.57495 c -5.05135,-0.45916 -8.71443,-1.45302 -12.84127,-3.484075 -3.38371,-1.66531 -6.14411,-3.65673 -8.91291,-6.42995 -2.82864,-2.83317 -4.79095,-5.58183 -6.50973,-9.11832 -1.59021,-3.27197 -2.55521,-6.44324 -3.12397,-10.26624 -0.14777,-0.99334 -0.15622,-1.24158 -0.15565,-4.57407 6.6e-4,-3.83821 0.007,-3.9292 0.47311,-6.33593 0.94332,-4.87551 2.87992,-9.39859 5.71184,-13.34054 0.72385,-1.00758 0.84631,-1.16386 1.83798,-2.34574 0.79326,-0.94541 3.07007,-3.19663 4.00458,-3.95956 2.32894,-1.90136 4.23693,-3.14166 6.67475,-4.33893 3.75732,-1.84533 7.34464,-2.89206 11.51987,-3.36136 1.30038,-0.14616 4.3321,-0.20186 5.75995,-0.10583 7.23996,0.48696 14.14684,3.2757 19.65155,7.93458 1.54663,1.30896 3.41106,3.23837 4.72874,4.89353 1.04845,1.31697 2.9491,4.1809 2.86499,4.317 -0.0202,0.0326 -0.48674,0.31148 -1.03686,0.61971 -0.55012,0.30823 -2.11324,1.18616 -3.47361,1.95095 -1.36035,0.76478 -2.94603,1.65515 -3.52372,1.97859 -0.57769,0.32344 -1.90236,1.06613 -2.94372,1.65043 -1.04136,0.58431 -1.93171,1.04766 -1.97855,1.02968 -0.0469,-0.018 -0.13711,-0.13336 -0.20057,-0.2564 -0.0635,-0.12305 -0.32424,-0.53524 -0.5795,-0.916 -2.73607,-4.08109 -7.10759,-7.03294 -11.86507,-8.01183 -1.48721,-0.30601 -2.19424,-0.37201 -3.98154,-0.37169 -1.95258,3.9e-4 -2.89342,0.11134 -4.61847,0.54488 -5.03068,1.26433 -9.42516,4.62461 -11.94519,9.13404 -1.61528,2.8904 -2.41964,5.99444 -2.41623,9.3243 0.003,2.99976 0.5844,5.56542 1.86554,8.23333 1.01389,2.11139 2.22943,3.79617 3.91527,5.42675 2.91431,2.81875 6.43132,4.55701 10.4983,5.18872 1.30491,0.20268 4.06945,0.21983 5.27581,0.0327 3.66804,-0.56892 6.69239,-1.87326 9.42226,-4.06361 1.65472,-1.32769 3.34842,-3.27371 4.39689,-5.05193 l 0.19494,-0.33061 -5.53317,-3.20759 c -9.9116,-5.74579 -10.75019,-6.2342 -10.81539,-6.29917 -0.0405,-0.0403 2.9819,-0.0641 8.14817,-0.0641 h 8.21253 l 0.0174,4.75284 0.0174,4.75283 0.67764,0.38469 c 0.37271,0.21158 1.47048,0.82838 2.43951,1.37067 1.94484,1.08838 6.54034,3.66529 8.52252,4.77898 0.69949,0.39299 1.28535,0.73649 1.30193,0.76332 0.078,0.12627 -1.30662,2.26459 -2.41482,3.72917 -4.07462,5.38495 -9.40618,9.29511 -15.76224,11.56 -2.33141,0.830765 -5.10253,1.460335 -7.74251,1.758995 -0.93672,0.10597 -4.99249,0.19246 -5.75877,0.12281 z"
id="path6437" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

414
lib/Dashboard.dart Normal file

@ -0,0 +1,414 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'Settings.dart';
import 'networking.dart';
import 'Login.dart';
class Dashboard extends StatefulWidget {
const Dashboard({
Key? key,
this.restorationId
}): super(key: key);
final String? restorationId;
@override
State<StatefulWidget> createState() => _DashboardState();
}
Future<String> getSettingsString(String key) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? value = await prefs.getString(key);
if(value == null || value.isEmpty) {
value = "";
}
print(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(Center(child: CircularProgressIndicator()));
}
}
);
}
class _DashboardState extends State<Dashboard> with RestorationMixin {
final RestorableInt _currentIndex = RestorableInt(0);
@override
Widget build(BuildContext context) {
final drawerElements = ListView(
children: [
UserAccountsDrawerHeader(
accountName: buildSettingsString('name', TextStyle()),
accountEmail: buildSettingsString('user', TextStyle()),
currentAccountPicture: const CircularProgressIndicator(backgroundColor: Colors.black,),
),
ListTile(
title: Text("Einstellungen"),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => Settings()),
);
},
leading: Icon(CupertinoIcons.settings),
),
ListTile(
title: Text("Abmelden"),
onTap: () async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString('api_key', "");
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => Login()),
);
},
leading: Icon(Icons.exit_to_app_outlined),
),
AboutListTile(
child: Text("Info"),
icon: Icon(CupertinoIcons.info),
applicationVersion: "0.5.0-alpha1",
applicationIcon: Image.asset("assets/images/meincantor_r.png", height: 64, width: 64),
applicationName: "MeinCantor",
aboutBoxChildren: [
Text("MeinCantor ist die Schulplatform für Schüler des Georg-Cantor-Gymnasiums in Halle (Saale)."),
Divider(),
Text("Copyright © 2021 Denys Konovalov")
],
// applicationIcon: Image.,
),
],
);
var bottomNavBarItems = [
BottomNavigationBarItem(
icon: const Icon(CupertinoIcons.home),
label: "Startseite",
),
BottomNavigationBarItem(icon: const Icon(CupertinoIcons.rectangle_grid_1x2), label: "Vertretungsplan"),
];
return Scaffold(
appBar: AppBar(
title: Text("GCG.MeinCantor"),
centerTitle: true,
),
body: _DashboardBottomNavView(
key: UniqueKey(),
item: bottomNavBarItems[_currentIndex.value]
),
drawer: Drawer(
child: drawerElements,
),
bottomNavigationBar: BottomNavigationBar(
showUnselectedLabels: false,
items: bottomNavBarItems,
currentIndex: _currentIndex.value,
onTap: (index) {
setState(() {
_currentIndex.value = index;
});
//print(index);
},
),
);
}
@override
String? get restorationId => widget.restorationId;
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_currentIndex, 'bottom_navigation_tab_index');
}
}
class _DashboardBottomNavView extends StatelessWidget {
_DashboardBottomNavView({
Key? key,
required this.item
}) : super(key:key);
final BottomNavigationBarItem item;
@override
Widget build(BuildContext context) {
return materialCard(item.label);
}
Widget materialCard(String? label) {
if (label == "Startseite") {
var view = SingleChildScrollView(
child: Column(
children: [
Row(
mainAxisSize: MainAxisSize.max,
children: [
Padding(
padding: EdgeInsets.fromLTRB(20, 30, 20, 5),
child: Text(
'Hallo,',
style: GoogleFonts.robotoSlab(
fontSize: 20,
),
),
),
],
),
Row(
mainAxisSize: MainAxisSize.max,
children: [
Padding(
padding: EdgeInsets.fromLTRB(20, 5, 20, 20),
child: buildSettingsString("name", GoogleFonts.robotoSlab(
fontSize: 28,
fontWeight: FontWeight.w800,
),
),
)
],
),
Row(
mainAxisSize: MainAxisSize.max,
children: [
Padding(
padding: EdgeInsets.fromLTRB(20, 10, 0, 10),
child: Text(
'Deine nächste Unterrichtsstunde:',
style: GoogleFonts.robotoSlab(
fontSize: 20,
fontWeight: FontWeight.w100,
),
),
)
],
),
Padding(
padding: EdgeInsets.fromLTRB(20, 20, 20, 10),
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0),
),
color: Colors.red,
child: Column(
children: [ListTile(
title: Text('3' + '.' + ' ' + 'Deutsch', style: TextStyle(color: Colors.white)),
subtitle: Row(
children: [Icon(CupertinoIcons.person, color: Colors.white), SizedBox(width: 5), Text("Herr Jünemann", style: TextStyle(color: Colors.white)), Spacer(), Icon(CupertinoIcons.home, color: Colors.white), SizedBox(width: 5), Text("106", style: TextStyle(color: Colors.white))]
),
leading:
Icon(CupertinoIcons.time, color: Colors.white)
)],
)
),
),
Row(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Padding(
padding: EdgeInsets.fromLTRB(20, 20, 20, 10),
child: Column(
children: [
Card(
clipBehavior: Clip.antiAliasWithSaveLayer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Row(
mainAxisSize: MainAxisSize.max,
children: [
Padding(
padding:
EdgeInsets.fromLTRB(20, 20, 10, 10),
child: Icon(
CupertinoIcons.news,
color: Color(0xFFFFBC3B),
size: 48,
),
)
],
),
Row(
mainAxisSize: MainAxisSize.max,
children: [
Align(
alignment: Alignment(0, 0),
child: Padding(
padding:
EdgeInsets.fromLTRB(15, 50, 0, 15),
child: Text(
'Schülerzeitung',
),
),
),
],
)
],
),
),
Card(
clipBehavior: Clip.antiAliasWithSaveLayer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Row(
mainAxisSize: MainAxisSize.max,
children: [
Padding(
padding:
EdgeInsets.fromLTRB(20, 20, 10, 10),
child: Icon(
CupertinoIcons.book,
color: Color(0xFFFFBC3B),
size: 48,
),
)
],
),
Row(
mainAxisSize: MainAxisSize.max,
children: [
Padding(
padding: EdgeInsets.fromLTRB(15, 50, 0, 15),
child: Text(
'Schulbibliothek',
),
)
],
)
],
),
),
Card(
clipBehavior: Clip.antiAliasWithSaveLayer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Row(
mainAxisSize: MainAxisSize.max,
children: [
Padding(
padding:
EdgeInsets.fromLTRB(20, 20, 10, 10),
child: Icon(
CupertinoIcons.device_laptop,
color: Color(0xFFFFBC3B),
size: 48,
),
)
],
),
Row(
mainAxisSize: MainAxisSize.max,
children: [
Padding(
padding: EdgeInsets.fromLTRB(15, 50, 0, 15),
child: Text(
'Schulcomputer',
),
)
],
)
],
),
),
Card(
clipBehavior: Clip.antiAliasWithSaveLayer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Row(
mainAxisSize: MainAxisSize.max,
children: [
Padding(
padding:
EdgeInsets.fromLTRB(20, 20, 10, 10),
child: Icon(
CupertinoIcons.house_alt,
color: Color(0xFFFFBC3B),
size: 48,
),
)
],
),
Row(
mainAxisSize: MainAxisSize.max,
children: [
Padding(
padding: EdgeInsets.fromLTRB(15, 50, 0, 15),
child: Text(
'Raumübersicht',
),
)
],
)
],
),
)
],
),
),
)
]
),
],
)
);
return view;
} else if (label == "Vertretungsplan") {
return LayoutBuilder(
builder: (context, constraints) {
double widgetWidth = constraints.maxWidth;
double widgetHeight = constraints.maxHeight;
var factor;
if (widgetWidth <= 600) {
factor = 1;
} else if (widgetWidth <= 1400) {
factor = 2;
} else if (widgetWidth <= 2000) {
factor = 3;
}
// 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: buildClassTimetable(),
),
);
}
);
} else {
return Text("Such page does not exist.");
}
}
}

145
lib/Login.dart Normal file

@ -0,0 +1,145 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'networking.dart';
import 'Dashboard.dart';
Future<bool> checkKey() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? api_key = await prefs.getString('api_key');
return api_key != null && api_key.isNotEmpty;
}
class Login extends StatelessWidget {
final userController = TextEditingController();
final passwordController = TextEditingController();
final otpController = TextEditingController();
final devIdController = TextEditingController();
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
double widgetWidth = constraints.maxWidth;
double widgetHeight = constraints.maxHeight;
var factor;
if (widgetWidth <= 600) {
factor = 1;
} else if (widgetWidth <= 1400) {
factor = 2;
} else if (widgetWidth <= 2000) {
factor = 3;
}
return Scaffold(
appBar: AppBar(
title: Text("Anmelden"),
),
body: Center(
child: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.fromLTRB(20, 20, 20, 20),
child: Container(
constraints: BoxConstraints(
maxWidth:
MediaQuery.of(context).size.width / factor,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset("assets/images/meincantor_r.png",
height: 192, width: 192),
Divider(),
AutofillGroup(
child: Column(
children: [
TextField(
autofillHints: [AutofillHints.username],
decoration: InputDecoration(
icon: Icon(CupertinoIcons.person),
border: OutlineInputBorder(),
labelText: 'Benutzername',
),
controller: userController,
),
Divider(),
TextField(
autofillHints: [AutofillHints.password],
decoration: InputDecoration(
icon: Icon(CupertinoIcons.lock),
border: OutlineInputBorder(),
labelText: 'Passwort',
),
obscureText: true,
controller: passwordController,
),
],
)),
Divider(),
TextField(
decoration: InputDecoration(
icon: Icon(CupertinoIcons.lock),
border: OutlineInputBorder(),
labelText: '2F2-Code (OTP) [falls aktiviert]',
),
obscureText: true,
controller: otpController,
),
Divider(),
TextField(
decoration: InputDecoration(
icon: Icon(CupertinoIcons.device_laptop),
border: OutlineInputBorder(),
labelText: 'Gerätebezeichnung',
),
controller: devIdController,
),
Divider(),
OutlinedButton(
onPressed: () async {
SharedPreferences prefs =
await SharedPreferences.getInstance();
String api_key = await getToken(
userController.text,
passwordController.text,
otpController.text,
devIdController.text);
print('Set new API key to $api_key');
await prefs.setString('api_key', api_key);
dynamic userinfo = jsonDecode(
await getUserInfo(
userController.text,
passwordController.text,
otpController.text,
devIdController.text));
await prefs.setString(
'user', userinfo['preferred_username']);
await prefs.setString(
'name', userinfo['name']);
if (prefs.getString('api_key') != null &&
prefs
.getString('api_key')!
.isNotEmpty) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => Dashboard()),
);
}
},
child: Text("Anmelden"))
],
),
)
)
)
)
);
},
);
}
}

38
lib/Settings.dart Normal file

@ -0,0 +1,38 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'networking.dart';
class Settings extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Einstellungen"),
),
body: ListView(
padding: EdgeInsets.fromLTRB(20, 20, 20, 20),
children: [
Padding(
padding: EdgeInsets.fromLTRB(0, 0, 0, 0),
child: TextField(
decoration: InputDecoration(
icon: Icon(CupertinoIcons.lock),
border: OutlineInputBorder(),
labelText: 'API Key (POST /login)',
),
onSubmitted: (String value) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String api_key = value;
print('Set new API key to $api_key');
await prefs.setString('api_key', api_key);
}
)
),
Divider(),
buildClassesChooser()
],
)
);
}
}

174
lib/Timetable.dart Normal file

@ -0,0 +1,174 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
class ClassTimetableBuilder {
final ListView view;
ClassTimetableBuilder({required this.view});
factory ClassTimetableBuilder.buildView(Map<String, dynamic> json) {
List<Widget> children = [];
ClassTimetable.fromJson(json).timetable.forEach((element) {
List<Widget> cardChildren = [];
cardChildren.add(ListTile(
title: Text(element.count.toString() + '.' + ' ' + element.name,
style: TextStyle(color: element.fontColor)),
subtitle: Row(
children: [
Icon(CupertinoIcons.person, color: element.fontColor),
SizedBox(width: 5),
Text(element.teacher,
style: TextStyle(color: element.fontColor)),
Spacer(),
Icon(CupertinoIcons.home, color: element.fontColor),
SizedBox(width: 5),
Text(element.room, style: TextStyle(color: element.fontColor))
]
),
leading:
Icon(CupertinoIcons.time, color: element.fontColor)
));
if (element.info != '') {
cardChildren.add(ListTile(title: Text(
element.info, style: TextStyle(color: element.fontColor))));
}
Card card = Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0),
),
color: element.color,
child: Column(
children: cardChildren,
)
);
children.add(card);
});
return ClassTimetableBuilder(
view: ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: children
)
);
}
}
class ClassTimetable {
final List timetable;
ClassTimetable({required this.timetable});
factory ClassTimetable.fromJson(Map<String, dynamic> json){
print(json);
List<TimetableLesson> lessons = [];
json['courses'].forEach((value){
var color;
var name;
var teacher;
var room;
dynamic teachers = {'Poli':'Herr Polity', 'Zura':'Frau Zuralski', 'Enzi': 'Frau Enzian', 'Bütt':'Frau Büttner', 'Brod':'Herr Brode', 'Rink':'Frau Rinke', 'Schk':'Frau Schmidt', 'Rudo':'Frau Rudolph', 'Kipp':'Frau Kipping', 'Bach':'Frau Bachran', 'Bad':'Herr Bader', 'Prei':'Frau Preiß', 'Scha':'Frau Schapitz', '&nbsp;':'', 'Link':'Herr Linke', 'Stei':'Herr Stein', 'Tupp':'Frau Tuppack', 'Hoff':'Frau Hoffman', 'Knol':'Frau Knoll', 'Bet':'Frau Bethin', 'Schu':'Frau Schulz', 'Seid':'Frau Seidel', 'Krug':'Frau Krug', 'Laer':'Frau Langer', 'Youn':'Frau Younso', 'Härt':'Frau Härtig', 'Bros':'Frau Brosig', 'Ber':'Frau Bernhardt', 'Stüb':'Frau Stüber', 'Bor':'Frau Borchert', 'Dubb':'Frau Dubberstein', 'Tren':'Frau Trentsch', 'Meit':'Herr Meitzner', 'Stol':'Frau Stolpe', 'Jac':'Frau Jacob', 'Jüne':'Herr Jünemann', 'Bert':'Frau Berthelmann', 'Felk':'Frau Felke', 'Kimm':'Herr Kimmel', 'PM1': 'Pädagosische(r) Mitarbeiter(in) 1', 'Schet':'Herr Schetler', 'Mani':'Herr Manigk', 'Segg':'Frau Seggern', 'Opel':'Frau Opel-Fritzler'};
if(value['Fa'].runtimeType != String){
name = value['Fa']['#text'];
}
else {
name = value['Fa'];
}
if(value['Le'].runtimeType != String){
teacher = value['Le']['#text'];
}
else {
teacher = value['Le'];
}
print(value['Ra']);
if(value['Ra'].runtimeType != String && value['Ra'].runtimeType != int){
if(value['Ra']['#text'] == '&nbsp;') {
room = '';
} else {
room = value['Ra']['#text'];
}
} else if(value['Ra'] == '&nbsp;') {
room = '';
} else {
room = value['Ra'];
}
var fontColor;
if(name == '---') {
fontColor = Colors.red;
} else {
fontColor = Colors.white;
}
var info;
if(value['If'].runtimeType != String) {
info = '';
} else {
info = value['If'];
}
if(name == 'Mat'){
color = Colors.indigo;
}
else if(name == 'Deu'){
color = Colors.red;
}
else if(name == 'Kun'){
color = Colors.deepPurple;
}
else if(name == 'Bio'){
color = Colors.green;
}
else if(name == 'Geo'){
color = Colors.brown;
}
else if(name == 'Lat'){
color = Colors.teal;
}
else if(name == 'Che'){
color = Colors.lightGreen;
}
else if(name == 'Eng'){
color = Colors.amber;
}
else if(name == 'Phy'){
color = Colors.cyan;
}
else if(name == 'Inf'){
color = Colors.tealAccent[400];
}
else if(name == 'Mus'){
color = Colors.deepOrange;
}
else if(name == 'Lme'){
color = Colors.amber[700];
}
else if(name == 'Ges'){
color = Colors.grey;
}
else if(name == 'Spa'){
color = Colors.redAccent;
}
else if(name == 'Frz'){
color = Colors.amberAccent[700];
}
else if(name == '---') {
color = Colors.white;
}
else {
color = Colors.grey[700];
}
dynamic names = {'Bio':'Biologie', 'Mat':'Mathematik', 'Kun':'Kunst', 'Mus':'Musik', 'Geo':'Geographie', 'Ges':'Geschichte', 'Che':'Chemie', 'Lat':'Latein', 'Inf':'Informatik', 'Eng':'Englisch', 'Frz':'Französisch', 'Phy':'Physik', '---':'---', 'Spo':'Sport', 'Deu':'Deutsch', 'Lme':'Lernmethoden', 'Eth':'Ethik', 'EvR':'Evangelische Religion', 'Spa':'Spanisch', 'Soz':'Sozialkunde', 'Ast':'Astronomie'};
dynamic colors = {'Bio':Colors.green, 'Mat':Colors.blue};
lessons.add(TimetableLesson(value['St'], names[name].toString(), teachers[teacher].toString(), room.toString(), value['If'].toString(), color, fontColor, info));
});
return ClassTimetable(
timetable: lessons
);
}
}
class TimetableLesson {
final int count;
final String name;
final String teacher;
final String room;
final String comment;
final Color color;
final Color fontColor;
final String info;
const TimetableLesson(this.count, this.name, this.teacher, this.room, this.comment, this.color, this.fontColor, this.info);
}

@ -0,0 +1,17 @@
//
// Generated file. Do not edit.
//
// ignore_for_file: lines_longer_than_80_chars
import 'package:fluttertoast/fluttertoast_web.dart';
import 'package:shared_preferences_web/shared_preferences_web.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
// ignore: public_member_api_docs
void registerPlugins(Registrar registrar) {
FluttertoastWebPlugin.registerWith(registrar);
SharedPreferencesPlugin.registerWith(registrar);
registrar.registerMessageHandler();
}

84
lib/main.dart Normal file

@ -0,0 +1,84 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'Dashboard.dart';
import 'Login.dart';
import 'dart:math';
void main() => runApp(App());
class App extends StatelessWidget {
MaterialColor generateMaterialColor(Color color) {
return MaterialColor(color.value, {
50: tintColor(color, 0.5),
100: tintColor(color, 0.4),
200: tintColor(color, 0.3),
300: tintColor(color, 0.2),
400: tintColor(color, 0.1),
500: tintColor(color, 0),
600: tintColor(color, -0.1),
700: tintColor(color, -0.2),
800: tintColor(color, -0.3),
900: tintColor(color, -0.4),
});
}
int tintValue(int value, double factor) =>
max(0, min((value + ((255 - value) * factor)).round(), 255));
Color tintColor(Color color, double factor) => Color.fromRGBO(
tintValue(color.red, factor),
tintValue(color.green, factor),
tintValue(color.blue, factor),
1);
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primaryColor: Palette.primary,
colorScheme: ColorScheme.fromSwatch(
primarySwatch: generateMaterialColor(Palette.accent),
).copyWith(
secondary: generateMaterialColor(Palette.accent),
),
),
darkTheme: ThemeData.from(colorScheme: ColorScheme.dark(primary: Palette.accent)),
title: "GCG.MeinCantor",
home: buildHomePage(),
/*
routes: <String, WidgetBuilder>{
"/": (_) => Dashboard(),
"/login": (_) => Login()
},
*/
);
}
}
Future<bool> apiKeyEmpty() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? api_key = await prefs.getString("api_key");
return api_key == null || api_key.isEmpty;
}
Widget buildHomePage() {
return FutureBuilder(
future: apiKeyEmpty(),
builder: (context, snapshot) {
if(snapshot.data == true) {
return Login();
} else if(snapshot.data == false) {
return Dashboard();
} else {
return Center(child: CircularProgressIndicator());
}
}
);
}
class Palette {
static const Color primary = Color(0xFF1A1A37);
static const Color accent = Color(0xFFFFBC3B);
}

194
lib/networking.dart Normal file

@ -0,0 +1,194 @@
import 'dart:convert';
import 'main.dart';
import 'package:flutter/cupertino.dart';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'Timetable.dart';
import 'Login.dart';
Future<String> getToken(
String user, String password, String otp, String devId) async {
var uri = Uri.http("localhost:3000", "/login");
String body =
'{"user":"$user", "password": "$password", "otp": "$otp", "devid": "$devId"}';
print(uri);
final response = await http.post(uri, body: body);
if (response.statusCode == 200) {
return jsonDecode(utf8.decode(response.bodyBytes))['token'];
} else if(response.statusCode == 401) {
var body = response.body;
Fluttertoast.showToast(
msg: "Fehler: $body",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.CENTER,
timeInSecForIosWeb: 1,
backgroundColor: Colors.red,
textColor: Colors.white,
fontSize: 16.0
);
throw Exception('Failed to log in');
} else {
throw Exception('Undefined error');
}
}
Future<String> getUserInfo(
String user, String password, String otp, String devId) async {
var uri = Uri.http("localhost:3000", "/api/userinfo");
String body =
'{"user":"$user", "password": "$password", "otp": "$otp", "devid": "$devId"}';
print(uri);
final response = await http.post(uri, body: body);
if (response.statusCode == 200) {
return utf8.decode(response.bodyBytes);
} else {
throw Exception('Failed to log in');
}
}
Future<http.Response> fetchClassTimetable() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
var class_num;
print(prefs.getString('class_num'));
if (prefs.getString('class_num') != null) {
class_num = prefs.getString('class_num')!;
} else {
class_num = '07_1';
}
var api_key = prefs.getString('api_key');
var uri = Uri.https("mein.cantorgymnasium.de", "/api/timetable/$class_num");
var headers = {"x-api-key": "$api_key"};
print(uri);
final response = http.get(uri, headers: headers);
return response;
}
Widget buildClassTimetable() {
return FutureBuilder<http.Response>(
future: fetchClassTimetable(),
builder: (context, snapshot) {
if (snapshot.hasData) {
int statusCode = snapshot.data!.statusCode;
if (statusCode == 200) {
ListView timetableView = ClassTimetableBuilder.buildView(
jsonDecode(utf8.decode(snapshot.data!.bodyBytes)))
.view;
return timetableView;
}
else if(statusCode == 400) {
Navigator.push(context, MaterialPageRoute(builder: (context) => Login()));
}
return Center(child: Text('Error $statusCode'));
} else if (snapshot.hasError) {
return Text('$snapshot.error');
} else {
return Center(child: CircularProgressIndicator());
}
},
);
}
Future<http.Response> fetchClassesList() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
/*
var class_num;
print(prefs.getString('class_num'));
if(prefs.getString('class_num') != null){
class_num = prefs.getString('class_num')!;
} else {
class_num = '07_1';
}
*/
// await prefs.setString('api_key', "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJHZW9yZy1DYW50b3ItR3ltbmFzaXVtIEhhbGxlKFNhYWxlKSIsInVzZXIiOiJkZW55cy5rb25vdmFsb3ZAcG0ubWUiLCJyb2xlcyI6WyJTdHVkZW50IiwiQWRtaW4iXSwiYmxhY2tsaXN0IjpbIi9jbGFzc2VzIl0sIndoaXRlbGlzdCI6WyIvaGVsbG8vc2Vuc2l0aXZlIl0sImppZCI6IkFwcERldiBBbHBoYSBkdW1teSBrZXlAMDIvMDgvMjAyMSAxOTowODowOSIsImV4cCI6MTY1OTQ2NzI4OX0.a7Q83PK3ybeV7Bui-_rX1o6IZx1cNa6vsvUGG-kfqtc");
var api_key = prefs.getString('api_key');
var uri = Uri.https("mein.cantorgymnasium.de", "/api/classes");
var headers = {"x-api-key": "$api_key"};
print(uri);
final response = http.get(uri, headers: headers);
return response;
}
Widget buildClassesChooser() {
return FutureBuilder<http.Response>(
future: fetchClassesList(),
builder: (context, snapshot) {
if (snapshot.hasData) {
int statusCode = snapshot.data!.statusCode;
if (statusCode == 200) {
// List<Widget> children = [];
//ClassTimetable.fromJson(jsonDecode(utf8.decode(snapshot.data!.bodyBytes))).timetable.forEach((element) {
//});
List<String> items = [];
jsonDecode(utf8.decode(snapshot.data!.bodyBytes)).forEach((value) {
items.add(value..toString());
});
return ClassesChooser(items: items);
}
return Text('$statusCode');
} else if (snapshot.hasError) {
return Text('$snapshot.error');
} else {
return Center(child: CircularProgressIndicator());
}
},
);
}
class ClassesChooser extends StatefulWidget {
final List<String> items;
const ClassesChooser({Key? key, required this.items}) : super(key: key);
@override
State<ClassesChooser> createState() => _ClassesChooserState(items);
}
class _ClassesChooserState extends State<ClassesChooser> {
final List<String> items;
//final String dropdownValue;
var dropdownValue;
_ClassesChooserState(this.items);
@override
Widget build(BuildContext context) {
// var dropdown_items =
return DropdownButtonFormField<String>(
// value: dropdownValue,
/*icon: const Icon(Icons.arrow_downward),
iconSize: 24,
elevation: 16,*/
// style: const TextStyle(color: Color(0xFFFFBC3B)),
/*underline: Container(
height: 2,
color: Color(0xFFFFBC3B),
)
*/
decoration: InputDecoration(
icon: Icon(CupertinoIcons.number),
border: OutlineInputBorder(),
labelText: 'Klasse (05_1, 07_3, 10_2...)',
),
// icon: Icon(CupertinoIcons.number),
onChanged: (String? newValue) {
setState(() async {
dropdownValue = newValue!;
SharedPreferences prefs = await SharedPreferences.getInstance();
String class_num = newValue;
print('Set new class to $class_num');
await prefs.setString('class_num', class_num);
});
},
items: items.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
);
}
}