= 0.8.0-dev =

- code cleanup
- caching
- black-/whitelist
- sz/news fixes
- added settings options
...
This commit is contained in:
Denys Konovalov 2021-12-13 13:39:06 +01:00
parent 649e226c66
commit e86c1bad1d
28 changed files with 1017 additions and 279 deletions

@ -44,7 +44,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "de.cantorgymnasium.meincantor"
minSdkVersion 18
minSdkVersion 20
targetSdkVersion 30
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName

23
android/app/proguard-rules.pro vendored Normal file

@ -0,0 +1,23 @@
## Gson rules
# Gson uses generic type information stored in a class file when working with fields. Proguard
# removes such information by default, so configure it to keep all of it.
-keepattributes Signature
# For using GSON @Expose annotation
-keepattributes *Annotation*
# Gson specific classes
-dontwarn sun.misc.**
#-keep class com.google.gson.stream.** { *; }
# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
-keep class * extends com.google.gson.TypeAdapter
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
# Prevent R8 from leaving Data object members always null
-keepclassmembers,allowobfuscation class * {
@com.google.gson.annotations.SerializedName <fields>;
}

@ -10,6 +10,8 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
android:showWhenLocked="true"
android:turnScreenOn="true"
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
@ -23,10 +25,10 @@
screen fades out. A splash screen is useful to avoid any visual
gap between the end of Android's launch screen and the painting of
Flutter's first frame. -->
<meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable"
<!--meta-data
<android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background"
/>
/>-->
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@drawable/*" />

@ -7,6 +7,10 @@ import Flutter
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
}
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

@ -34,9 +34,7 @@ class DevSettings extends StatelessWidget {
content:
Text('Neuer API-Schlüssel gesetzt: $apiKey'));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
)
),
})),
const Divider(),
Padding(
padding: const EdgeInsets.fromLTRB(0, 0, 0, 0),
@ -51,7 +49,6 @@ class DevSettings extends StatelessWidget {
)
)
],
)
);
));
}
}

@ -1,6 +1,11 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:MeinCantor/const.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:meincantor/const.dart';
import 'package:meincantor/main.dart';
import 'package:url_launcher/url_launcher.dart';
class InfoSettings extends StatelessWidget {
const InfoSettings({Key? key}) : super(key: key);
@ -19,9 +24,31 @@ class InfoSettings extends StatelessWidget {
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),
),
),
ListTile(
leading: const Icon(Icons.settings_backup_restore_outlined),
title: const Text("Änderungsverlauf"),
subtitle: const Text("Was ist neu?"),
onTap: () {
showModalBottomSheet<void>(
isScrollControlled: true,
@ -30,14 +57,13 @@ class InfoSettings extends StatelessWidget {
return SizedBox(
height: 400,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
AppBar(
title: const Text("Änderungsverlauf"),
),
const Padding(
padding: EdgeInsets.all(10),
child: Text(""),
child: Text("1.0 --\nErste Release-Version!"),
),
],
),

@ -1,51 +1,20 @@
import 'dart:convert';
import 'package:MeinCantor/main.dart';
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';
import 'package:MeinCantor/presets/colors.dart';
import 'package:MeinCantor/presets/subjects.dart';
import 'package:meincantor/presets/colors.dart';
import 'package:meincantor/presets/subjects.dart';
import 'package:MeinCantor/presets/teachers.dart';
import 'package:meincantor/presets/teachers.dart';
class PlanSettings extends StatefulWidget {
class PlanSettings extends StatelessWidget {
const PlanSettings({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _PlanSettingsState();
}
Future<Color> buildPlanColors(lesson) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
if (!prefs.containsKey("color$lesson")) {
prefs.setInt("color$lesson", colors[lesson].value ?? Colors.grey.value);
}
await fetchLessonList();
Color colorDeu = Color(prefs.getInt("color$lesson")!);
return colorDeu;
}
Future<List<dynamic>> buildLessonsList() async {
await fetchLessonList();
SharedPreferences prefs = await SharedPreferences.getInstance();
String lessonsJson = prefs.getString("lessons")!;
List<dynamic> lessons = jsonDecode(lessonsJson);
return lessons;
}
class _PlanSettingsState extends State<PlanSettings> {
Set<Color> swatches = {
...Colors.primaries,
...Colors.accents,
Palette.accent,
Palette.primary
};
@override
Widget build(BuildContext context) {
return Scaffold(
@ -56,10 +25,202 @@ class _PlanSettingsState extends State<PlanSettings> {
body: ListView(
padding: const EdgeInsets.fromLTRB(5, 5, 5, 5),
children: [
const ListTile(
title: Text("Kurse/Fächer"),
leading: Icon(Icons.list_alt_outlined),
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);
@override
State<StatefulWidget> createState() => _WhitelistSettingsState();
}
class _WhitelistSettingsState extends State<WhitelistSettings> {
Set<Color> swatches = {
...Colors.primaries,
...Colors.accents,
Palette.accent,
Palette.primary
};
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Farben"),
centerTitle: true,
),
body: ListView(
padding: const EdgeInsets.fromLTRB(5, 5, 5, 5),
children: [
FutureBuilder(
future: buildLessonsList(),
builder: (context, snapshot) {
if (snapshot.hasData) {
List<Widget> children = [];
for (var element in (snapshot.data as List<dynamic>)) {
String subject = element['subject'];
String teacher = element['teacher'];
int id = element['id'];
children.add(
FutureBuilder(
future: buildPlanColors(subject),
builder: (context, snapshot) {
if (snapshot.hasData) {
Color color = snapshot.data as Color;
return FutureBuilder(
future: buildBlacklist(),
builder: (context, snapshot) {
if (snapshot.hasData) {
final _blacklist =
snapshot.data! as List<dynamic>;
final _blacklisted =
_blacklist.contains(id);
return ListTile(
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] ?? ""),
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();
}
});
} else {
return (const LinearProgressIndicator());
}
}),
);
}
return Column(
children: children,
);
} else {
return (const Center(child: CircularProgressIndicator()));
}
}),
],
));
}
}
class PlanColorSettings extends StatefulWidget {
const PlanColorSettings({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _PlanColorSettingsState();
}
Future<Color> buildPlanColors(String lesson) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String key = "color$lesson";
if (prefs.containsKey(key) == false) {
int colorValue;
if (colors.containsKey(lesson)) {
colorValue = colors[lesson].value;
} else {
colorValue = Colors.grey.value;
}
prefs.setInt(key, colorValue);
}
//await fetchLessonList();
Color color = Color(prefs.getInt(key)!);
return color;
}
Future<List<dynamic>> buildLessonsList() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String lessonsJson = prefs.getString("lessons")!;
List<dynamic> lessons = jsonDecode(lessonsJson);
return lessons;
}
Future<List> buildBlacklist() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
if (!prefs.containsKey("blacklist") ||
jsonDecode(prefs.getString("blacklist")!).isEmpty) {
return <int>[];
}
String blacklistJson = prefs.getString("blacklist")!;
List blacklist = jsonDecode(blacklistJson);
return blacklist;
}
class _PlanColorSettingsState extends State<PlanColorSettings> {
Set<Color> swatches = {
...Colors.primaries,
...Colors.accents,
Palette.accent,
Palette.primary
};
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Farben"),
centerTitle: true,
),
body: ListView(
padding: const EdgeInsets.fromLTRB(5, 5, 5, 5),
children: [
FutureBuilder(
future: buildLessonsList(),
builder: (context, snapshot) {

@ -1,7 +1,14 @@
import 'package:cyclop/cyclop.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:MeinCantor/networking.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';
Future<String> getSettingsString(String key) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
@ -28,17 +35,39 @@ class UserSettings extends StatelessWidget {
children: [
Padding(
padding: const EdgeInsets.fromLTRB(10, 10, 10, 10),
child: Container(
width: 128.0,
height: 128.0,
decoration: const BoxDecoration(
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:
AssetImage("assets/images/meincantor_r.png")
image: NetworkImage(url)
)
)
);
} else {
return const CircularProgressIndicator();
}
},
),
),
FutureBuilder(
@ -61,8 +90,44 @@ class UserSettings extends StatelessWidget {
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()),
);
},
),
],
)
);
}
}
class AccountConsole extends StatefulWidget {
@override
AccountConsoleState createState() => AccountConsoleState();
}
class AccountConsoleState extends State<AccountConsole> {
@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/',
);
}
}

@ -1,5 +1,5 @@
import 'package:MeinCantor/Settings/Pages/appearance_settings.dart';
import 'package:MeinCantor/Settings/Pages/service_settings.dart';
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';

20
lib/cache_manager.dart Normal file

@ -0,0 +1,20 @@
import 'dart:convert';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:shared_preferences/shared_preferences.dart';
Future<String> getCachedTimetable(String ext) async {
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 headers = {"x-api-key": "$apiKey"};
var file = await DefaultCacheManager().getSingleFile(
"https://mein.cantorgymnasium.de/api/timetable/$ext/$classNum",
headers: headers);
return (utf8.decode(await file.readAsBytes()));
}

@ -1,19 +1,24 @@
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';
import 'package:flutter_local_notifications/flutter_local_notifications.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';
import 'package:MeinCantor/main.dart';
import 'package:MeinCantor/networking.dart';
import 'package:MeinCantor/login.dart';
import 'package:meincantor/main.dart';
import 'package:meincantor/networking.dart';
import 'package:meincantor/login.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.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 'news.dart';
class Dashboard extends StatefulWidget {
const Dashboard({Key? key, this.restorationId}) : super(key: key);
@ -53,8 +58,39 @@ class _DashboardState extends State<Dashboard> with RestorationMixin {
UserAccountsDrawerHeader(
accountName: buildSettingsString('name', const TextStyle()),
accountEmail: buildSettingsString('user', const TextStyle()),
currentAccountPicture:
Image.asset("assets/images/meincantor_r.png")),
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: () {
@ -271,10 +307,10 @@ class _DashboardBottomNavView extends StatelessWidget {
),
)),
),
width: 175,
width: 170,
),
SizedBox(
width: 175,
width: 170,
child: GestureDetector(
onTap: () async {
Navigator.push(
@ -310,7 +346,7 @@ class _DashboardBottomNavView extends StatelessWidget {
),
),
SizedBox(
width: 175,
width: 170,
child: GestureDetector(
onTap: () async {
Navigator.push(
@ -346,7 +382,7 @@ class _DashboardBottomNavView extends StatelessWidget {
),
),
SizedBox(
width: 175,
width: 170,
child: GestureDetector(
onTap: () async {
Navigator.push(
@ -381,6 +417,42 @@ class _DashboardBottomNavView extends StatelessWidget {
),
)),
),
),
SizedBox(
width: 170,
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',
),
),
),
),
)),
),
)
],
),
@ -439,9 +511,16 @@ class _DashboardBottomNavView extends StatelessWidget {
),
body: TabBarView(
children: <Widget>[
buildTodayClassTimetable(),
buildTomorrowClassTimetable(),
buildClassTimetable(),
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")
],
),
),

@ -5,14 +5,14 @@
// ignore_for_file: directives_ordering
// 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:url_launcher_web/url_launcher_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);
UrlLauncherPlugin.registerWith(registrar);
registrar.registerMessageHandler();
}

@ -14,6 +14,7 @@ Future<bool> checkKey() async {
}
class Login extends StatelessWidget {
final userController = TextEditingController();
final passwordController = TextEditingController();
final otpController = TextEditingController();
@ -148,11 +149,7 @@ class Login extends StatelessWidget {
child: const Text("Anmelden"))
],
),
)
)
)
)
);
)))));
},
);
}

@ -1,11 +1,28 @@
import 'package:flutter/cupertino.dart';
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 'dart:math';
void main() => runApp(const App());
void main() async {
WidgetsFlutterBinding.ensureInitialized();
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 =
IOSInitializationSettings();
final MacOSInitializationSettings initializationSettingsMacOS =
MacOSInitializationSettings();
final InitializationSettings initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOS,
macOS: initializationSettingsMacOS);
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
runApp(const App());
}
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);

@ -1,14 +1,19 @@
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';
import 'package:intl/intl.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:MeinCantor/const.dart';
import 'package:MeinCantor/timetable.dart';
import 'package:MeinCantor/login.dart';
import 'package:MeinCantor/main.dart';
import 'package:meincantor/const.dart';
import 'package:meincantor/timetable.dart';
import 'package:meincantor/login.dart';
import 'package:meincantor/main.dart';
Future<http.Response> getArticles() async {
var uri = Uri.https(szUrl["url"]!, "/articles");
@ -16,6 +21,12 @@ Future<http.Response> getArticles() async {
return (response);
}
Future<http.Response> getNews() async {
var uri = Uri.https(szUrl["url"]!, "/aktuelles");
final response = await http.get(uri);
return (response);
}
Future<http.Response> getToken(
String user, String password, String otp, String devId) async {
var uri = Uri.https("mein.cantorgymnasium.de", "/login");
@ -40,7 +51,28 @@ Future<String> getUserInfo(
}
Future<http.Response> fetchClassTimetable(String ext) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
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');
try {
return (http.Response(await getCachedTimetable(ext), 200));
} on HttpExceptionWithStatus catch (e) {
return http.Response(e.message, e.statusCode);
} on HttpException catch (e) {
return http.Response(e.message, 500);
} 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("/", "_");
@ -53,45 +85,9 @@ Future<http.Response> fetchClassTimetable(String ext) async {
final response = http.get(uri, headers: headers).onError((error, stackTrace) {
return (http.Response("", 404));
});
return response;
return response;*/
}
/*Future<http.Response> fetchTodayClassTimetable() async {
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/today");
var headers = {"x-api-key": "$apiKey"};
final response = http.get(uri, headers: headers).onError((error, stackTrace) {
return (http.Response("", 404));
});
return response;
}
Future<http.Response> fetchTomorrowClassTimetable() async {
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/tomorrow");
var headers = {"x-api-key": "$apiKey"};
final response = http.get(uri, headers: headers).onError((error, stackTrace) {
return (http.Response("", 404));
});
return response;
}*/
fetchLessonList() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String classNum;
@ -114,18 +110,6 @@ fetchLessonList() async {
}
}
Widget buildClassTimetable() {
return buildTimetable(fetchClassTimetable(""), "aktueller Vertretungsplan");
}
Widget buildTodayClassTimetable() {
return buildTimetable(fetchClassTimetable("/today"), "Vertretungsplan für heute");
}
Widget buildTomorrowClassTimetable() {
return buildTimetable(fetchClassTimetable("/tomorrow"), "Vertretungsplan für morgen");
}
Widget buildTimetable(Future<http.Response> future, String info) {
return FutureBuilder<http.Response>(
future: future,
@ -134,7 +118,7 @@ Widget buildTimetable(Future<http.Response> future, String info) {
int statusCode = snapshot.data!.statusCode;
if (statusCode == 200) {
Widget timetableView = ClassTimetableBuilder.buildView(
jsonDecode(utf8.decode(snapshot.data!.bodyBytes)), context)
jsonDecode(snapshot.data!.body), context)
.view
.child;
return timetableView;
@ -142,8 +126,7 @@ Widget buildTimetable(Future<http.Response> future, String info) {
Navigator.push(
context, MaterialPageRoute(builder: (context) => Login()));
} else if (statusCode == 500) {
var chars = Runes(
'Es konnte kein $info gefunden werden. \u{1F937}');
var chars = Runes('Es konnte kein $info gefunden werden. \u{1F937}');
List<Widget> cardChildren = [];
cardChildren.add(ListTile(
title: Text(String.fromCharCodes(chars),
@ -182,8 +165,7 @@ Widget buildTimetable(Future<http.Response> future, String info) {
padding: const EdgeInsets.fromLTRB(20, 20, 20, 20),
child: ListView(
children: [card],
)
);
));
}
return Center(child: Text('Error $statusCode'));
} else if (snapshot.hasError) {
@ -197,13 +179,14 @@ Widget buildTimetable(Future<http.Response> future, String info) {
Widget buildTodayClassTimetableLesson(int count) {
return FutureBuilder<http.Response>(
future: fetchClassTimetable("/today"),
future: fetchClassTimetable(
"/${DateFormat("yyyyMMdd").format(DateTime.now())}"),
builder: (context, snapshot) {
if (snapshot.hasData) {
int statusCode = snapshot.data!.statusCode;
if (statusCode == 200) {
List<Widget> lessons = LessonsListBuilder.buildList(
jsonDecode(utf8.decode(snapshot.data!.bodyBytes)),
jsonDecode(snapshot.data!.body),
count: count)
.lessons;
if (lessons.isNotEmpty) {
@ -223,7 +206,7 @@ Widget buildTodayClassTimetableLesson(int count) {
child: Column(
children: cardChildren,
));
return card;
return Column(children: [card]);
}
} else if (statusCode == 400) {
Future.delayed(Duration.zero, () {
@ -264,11 +247,8 @@ Widget buildTodayClassTimetableLesson(int count) {
child: Column(
children: cardChildren,
));
return Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 20),
child: ListView(
return Column(
children: [card],
)
);
}
return Center(child: Text('Error $statusCode'));

224
lib/news.dart Normal file

@ -0,0 +1,224 @@
import 'dart:convert';
import 'package:meincantor/networking.dart';
import 'package:flutter/material.dart';
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';
Future<List> getNewsRead() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? newsReadString = prefs.getString("newsRead");
List<dynamic> newsRead;
if (newsReadString == null ||
(jsonDecode(newsReadString) as List<dynamic>).isEmpty) {
newsRead = [];
} else {
newsRead = jsonDecode(newsReadString) as List<dynamic>;
}
return newsRead;
}
class News extends StatefulWidget {
const News({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _NewsState();
}
class _NewsState extends State<News> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Aktuelles"),
centerTitle: true,
),
body: FutureBuilder<http.Response>(
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<Widget> articleTiles = [];
for (var element in articles) {
Color color = Colors.white70;
Widget card = FutureBuilder(
future: getNewsRead(),
builder: (context, snapshot) {
if (snapshot.hasData) {
List<dynamic> readList = snapshot.data! as List<dynamic>;
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")));
}
} else {
return (const Center(child: CircularProgressIndicator()));
}
},
),
);
}
}
class Article {
Widget widget;
//const Article({Key? key}) : super(key: key);
Article({required this.widget});
factory Article.fromData(
String title, String content, String author, String publishDate) {
return Article(
widget: Scaffold(
appBar: AppBar(
title: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal, child: Text(title)),
),
centerTitle: true,
),
body: ListView(
padding: const EdgeInsets.fromLTRB(15, 15, 15, 15),
children: [
ListTile(
leading: const Icon(MdiIcons.accountOutline),
title: Text(author),
),
ListTile(
leading: const Icon(MdiIcons.calendarOutline),
title: Text(
"${DateTime.parse(publishDate).day.toString()}.${DateTime.parse(publishDate).month.toString()}.${DateTime.parse(publishDate).year.toString()}"),
),
MarkdownBody(data: content)
],
)));
}
}

1
lib/notifications.dart Normal file

@ -0,0 +1 @@

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
dynamic colors = {
Map colors = {
'Bio': Colors.green,
'Mat': Colors.indigo,
'matL1': Colors.indigo,

@ -1,16 +1,32 @@
import 'dart:convert';
import 'package:MeinCantor/networking.dart';
import 'package:flutter/cupertino.dart';
import 'package:meincantor/networking.dart';
import 'package:flutter/material.dart';
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';
class SZ extends StatelessWidget {
Future<List> getSZread() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? szReadString = prefs.getString("SZread");
List<dynamic> szRead;
if (szReadString == null ||
(jsonDecode(szReadString) as List<dynamic>).isEmpty) {
szRead = [];
} else {
szRead = jsonDecode(szReadString) as List<dynamic>;
}
return szRead;
}
class SZ extends StatefulWidget {
const SZ({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _SZState();
}
class _SZState extends State<SZ> {
@override
Widget build(BuildContext context) {
return Scaffold(
@ -28,26 +44,131 @@ class SZ extends StatelessWidget {
List articles = jsonDecode(data);
List<Widget> articleTiles = [];
for (var element in articles) {
Card card = Card(
child: Column(children: [
Padding(
padding: const EdgeInsets.fromLTRB(10, 10, 10, 10),
child: ListTile(
Color color = Colors.white70;
Widget card = FutureBuilder(
future: getSZread(),
builder: (context, snapshot) {
if (snapshot.hasData) {
List<dynamic> readList = snapshot.data! as List<dynamic>;
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),
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)),
style: const TextStyle(
fontWeight: FontWeight.bold)),
subtitle: Text(
element["summary"])),
)
]),
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);
}
@ -55,14 +176,11 @@ class SZ extends StatelessWidget {
children: articleTiles.reversed.toList(),
);
} else {
return(const Center(
child: Text("Uups... Irgendwas ist schief gelaufen")
));
return (const Center(
child: Text("Uups... Irgendwas ist schief gelaufen")));
}
} else {
return(const Center(
child: CircularProgressIndicator()
));
return (const Center(child: CircularProgressIndicator()));
}
},
),
@ -74,20 +192,20 @@ class Article {
Widget widget;
//const Article({Key? key}) : super(key: key);
Article({required this.widget});
factory Article.fromData(String title, String content, String author, String publishDate, ) {
return Article(widget: Scaffold(
factory Article.fromData(
String title, String content, String author, String publishDate) {
return Article(
widget: Scaffold(
appBar: AppBar(
title: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Text(title)
),
scrollDirection: Axis.horizontal, child: Text(title)),
),
centerTitle: true,
),
body: ListView(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 20),
padding: const EdgeInsets.fromLTRB(15, 15, 15, 15),
children: [
ListTile(
leading: const Icon(MdiIcons.accountOutline),
@ -95,12 +213,11 @@ class Article {
),
ListTile(
leading: const Icon(MdiIcons.calendarOutline),
title: Text("${DateTime.parse(publishDate).day.toString()}.${DateTime.parse(publishDate).month.toString()}.${DateTime.parse(publishDate).year.toString()}"),
title: Text(
"${DateTime.parse(publishDate).day.toString()}.${DateTime.parse(publishDate).month.toString()}.${DateTime.parse(publishDate).year.toString()}"),
),
MarkdownBody(data: content)
],
)
)
);
)));
}
}

@ -1,12 +1,14 @@
import 'package:MeinCantor/presets/teachers.dart';
import 'package:MeinCantor/presets/subjects.dart';
import 'package:MeinCantor/presets/colors.dart';
import 'package:meincantor/presets/teachers.dart';
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';
import 'Settings/Pages/plan_settings.dart';
class ClassTimetableBuilder {
final RefreshIndicator view;
ClassTimetableBuilder({required this.view});
@ -61,8 +63,7 @@ class ClassTimetableBuilder {
physics: const AlwaysScrollableScrollPhysics(),
children: list,
),
)
);
));
}
}
@ -104,6 +105,11 @@ class LessonsListBuilder {
style: TextStyle(color: element.fontColor))));
}
Widget card = FutureBuilder(
future: buildBlacklist(),
builder: (context, snapshot) {
if (snapshot.hasData) {
if (!((snapshot.data as List<dynamic>).contains(element.id))) {
return FutureBuilder(
future: element.color,
builder: (context, snapshot) {
if (snapshot.hasData) {
@ -116,10 +122,18 @@ class LessonsListBuilder {
children: cardChildren,
));
} else {
return (const Center(child: CircularProgressIndicator()));
return (const Center(
child: CircularProgressIndicator()));
}
},
);
} else {
return const SizedBox.shrink();
}
} else {
return const LinearProgressIndicator();
}
});
children.add(card);
}
}
@ -187,6 +201,7 @@ class ClassTimetable {
lessons.add(TimetableLesson(
value['St'],
value["Nr"],
subjects[subject] ?? subject.toString(),
teachers[teacher] ?? teacher.toString(),
room.toString(),
@ -202,6 +217,7 @@ class ClassTimetable {
class TimetableLesson {
final int count;
final int id;
final String name;
final String teacher;
final String room;
@ -209,6 +225,6 @@ class TimetableLesson {
final Future<Color> color;
final Color fontColor;
final String info;
const TimetableLesson(this.count, this.name, this.teacher, this.room,
const TimetableLesson(this.count, this.id, this.name, this.teacher, this.room,
this.comment, this.color, this.fontColor, this.info);
}

@ -1,4 +1,4 @@
name: MeinCantor
name: meincantor
description: Die Schulplatform für Cantorianer.
# The following line prevents the package from being accidentally published to
@ -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.7.5-beta1.nightly2021-11-16
version: 0.8.0-dev
environment:
sdk: ">=2.12.0 <3.0.0"
@ -37,11 +37,17 @@ dependencies:
google_fonts: ^2.1.0
time: ^2.0.0
flutter_launcher_icons: ^0.9.1
fluttertoast: ^8.0.8
flutter_colorpicker: ^0.6.0
material_design_icons_flutter: ^5.0.5955-rc.1
cyclop: ^0.5.2
flutter_markdown: ^0.6.8
flutter_cache_manager: ^3.2.0
intl: ^0.17.0
url_launcher: ^6.0.17
flutter_linkify: ^5.0.2
flutter_svg: ^1.0.0
webview_flutter: ^3.0.0
flutter_local_notifications: ^10.0.0-dev.1
background_fetch: ^1.0.3
flutter_icons:
# image_path: "assets/images/icon-128x128.png"

@ -3,8 +3,8 @@
"short_name": "MeinCantor",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"background_color": "#1a1a37",
"theme_color": "#ffbc3b",
"description": "Die Schulplatform für Cantorianer.",
"orientation": "portrait-primary",
"prefer_related_applications": false,