diff --git a/filcnaplo/lib/database/init.dart b/filcnaplo/lib/database/init.dart index 852b1dc..b256cdc 100644 --- a/filcnaplo/lib/database/init.dart +++ b/filcnaplo/lib/database/init.dart @@ -20,7 +20,7 @@ const settingsDB = DatabaseStruct("settings", { "grade_color4": int, "grade_color5": int, // grade colors "vibration_strength": int, "ab_weeks": int, "swap_ab_weeks": int, "notifications": int, "notifications_bitfield": int, - "notification_poll_interval": int, "notifications_grades":int, "notifications_absences":int, "notifications_messages": int, // notifications + "notification_poll_interval": int, "notifications_grades":int, "notifications_absences":int, "notifications_messages": int, "notifications_lessons":int, // notifications "x_filc_id": String, "graph_class_avg": int, "presentation_mode": int, "bell_delay": int, "bell_delay_enabled": int, "grade_opening_fun": int, "icon_pack": String, "premium_scopes": String, diff --git a/filcnaplo/lib/helpers/notification_helper.dart b/filcnaplo/lib/helpers/notification_helper.dart index 598be68..7ee3a39 100644 --- a/filcnaplo/lib/helpers/notification_helper.dart +++ b/filcnaplo/lib/helpers/notification_helper.dart @@ -10,9 +10,12 @@ import 'package:filcnaplo_kreta_api/client/api.dart'; import 'package:filcnaplo_kreta_api/client/client.dart'; import 'package:filcnaplo_kreta_api/models/absence.dart'; import 'package:filcnaplo_kreta_api/models/grade.dart'; +import 'package:filcnaplo_kreta_api/models/lesson.dart'; +import 'package:filcnaplo_kreta_api/models/week.dart'; import 'package:filcnaplo_kreta_api/providers/grade_provider.dart'; import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart' hide Message; +import 'package:i18n_extension/i18n_widget.dart'; import 'package:intl/intl.dart'; import 'package:filcnaplo_kreta_api/models/message.dart'; @@ -45,6 +48,15 @@ class NotificationsHelper { return combinedList; } + String dayTitle(DateTime date) { + try { + return DateFormat("EEEE", I18n.locale.languageCode) + .format(date); + } catch (e) { + return "Unknown"; + } + } + @pragma('vm:entry-point') void backgroundJob() async { // initialize providers @@ -65,7 +77,8 @@ class NotificationsHelper { kretaClient.refreshLogin(); if(settingsProvider.notificationsGradesEnabled) gradeNotification(); if(settingsProvider.notificationsAbsencesEnabled) absenceNotification(); - messageNotification(); + if(settingsProvider.notificationsMessagesEnabled) messageNotification(); + if(settingsProvider.notificationsLessonsEnabled) lessonNotification(); } } @@ -265,4 +278,247 @@ class NotificationsHelper { await database.userStore.storeMessages(combinedmessages, userId: userProvider.id!); } -} + void lessonNotification() async { + // get lesson from api + TimetableProvider timetableProvider = TimetableProvider( + user: userProvider, database: database, kreta: kretaClient); + List storedlessons = + timetableProvider.lessons[Week.current()] ?? []; + List? apilessons = timetableProvider.getWeek(Week.current()) ?? []; + for (Lesson lesson in apilessons) { + for (Lesson storedLesson in storedlessons) { + if (lesson.id == storedLesson.id) { + lesson.isSeen = storedLesson.isSeen; + } + } + } + List modifiedlessons = []; + if (apilessons != storedlessons) { + // remove lessons that are not new + apilessons.removeWhere((element) => storedlessons.contains(element)); + for (Lesson lesson in apilessons) { + if (!lesson.isSeen && lesson.isChanged) { + lesson.isSeen = true; + modifiedlessons.add(lesson); + const AndroidNotificationDetails androidNotificationDetails = + AndroidNotificationDetails('LESSONS', 'Órák', + channelDescription: + 'Értesítés órák elmaradásáról, helyettesítésről', + importance: Importance.max, + priority: Priority.max, + color: const Color(0xFF3D7BF4), + ticker: 'Órák'); + const NotificationDetails notificationDetails = + NotificationDetails(android: androidNotificationDetails); + if (userProvider.getUsers().length == 1) { + if (lesson.status?.name == "Elmaradt") { + switch (I18n.localeStr) { + case "en_en": + { + await flutterLocalNotificationsPlugin.show( + lesson.id.hashCode, + "title_lesson".i18n, + "body_lesson_canceled".i18n.fill([ + lesson.lessonIndex, + lesson.name, + dayTitle(lesson.date) + ]), + notificationDetails); + break; + } + case "hu_hu": + { + await flutterLocalNotificationsPlugin.show( + lesson.id.hashCode, + "title_lesson".i18n, + "body_lesson_canceled".i18n.fill([ + dayTitle(lesson.date), + lesson.lessonIndex, + lesson.name + ]), + notificationDetails); + break; + } + default: + { + await flutterLocalNotificationsPlugin.show( + lesson.id.hashCode, + "title_lesson".i18n, + "body_lesson_canceled".i18n.fill([ + lesson.lessonIndex, + lesson.name, + dayTitle(lesson.date) + ]), + notificationDetails); + break; + } + } + } else if (lesson.substituteTeacher?.name != "") { + switch (I18n.localeStr) { + case "en_en": + { + await flutterLocalNotificationsPlugin.show( + lesson.id.hashCode, + "title_lesson".i18n, + "body_lesson_substituted".i18n.fill([ + lesson.lessonIndex, + lesson.name, + dayTitle(lesson.date), + lesson.substituteTeacher!.isRenamed + ? lesson.substituteTeacher!.renamedTo! + : lesson.substituteTeacher!.name + ]), + notificationDetails); + break; + } + case "hu_hu": + { + await flutterLocalNotificationsPlugin.show( + lesson.id.hashCode, + "title_lesson".i18n, + "body_lesson_substituted".i18n.fill([ + dayTitle(lesson.date), + lesson.lessonIndex, + lesson.name, + lesson.substituteTeacher!.isRenamed + ? lesson.substituteTeacher!.renamedTo! + : lesson.substituteTeacher!.name + ]), + notificationDetails); + break; + } + default: + { + await flutterLocalNotificationsPlugin.show( + lesson.id.hashCode, + "title_lesson".i18n, + "body_lesson_substituted".i18n.fill([ + lesson.lessonIndex, + lesson.name, + dayTitle(lesson.date), + lesson.substituteTeacher!.isRenamed + ? lesson.substituteTeacher!.renamedTo! + : lesson.substituteTeacher!.name + ]), + notificationDetails); + break; + } + } + } + } else { + if (lesson.status?.name == "Elmaradt") { + switch (I18n.localeStr) { + case "en_en": + { + await flutterLocalNotificationsPlugin.show( + lesson.id.hashCode, + "title_lesson".i18n, + "body_lesson_canceled".i18n.fill([ + userProvider.displayName!, + lesson.lessonIndex, + lesson.name, + dayTitle(lesson.date) + ]), + notificationDetails); + break; + } + case "hu_hu": + { + await flutterLocalNotificationsPlugin.show( + lesson.id.hashCode, + "title_lesson".i18n, + "body_lesson_canceled".i18n.fill([ + userProvider.displayName!, + dayTitle(lesson.date), + lesson.lessonIndex, + lesson.name + ]), + notificationDetails); + break; + } + default: + { + await flutterLocalNotificationsPlugin.show( + lesson.id.hashCode, + "title_lesson".i18n, + "body_lesson_canceled".i18n.fill([ + userProvider.displayName!, + lesson.lessonIndex, + lesson.name, + dayTitle(lesson.date) + ]), + notificationDetails); + break; + } + } + } else if (lesson.substituteTeacher?.name != "") { + switch (I18n.localeStr) { + case "en_en": + { + await flutterLocalNotificationsPlugin.show( + lesson.id.hashCode, + "title_lesson".i18n, + "body_lesson_substituted".i18n.fill([ + userProvider.displayName!, + lesson.lessonIndex, + lesson.name, + dayTitle(lesson.date), + lesson.substituteTeacher!.isRenamed + ? lesson.substituteTeacher!.renamedTo! + : lesson.substituteTeacher!.name + ]), + notificationDetails); + break; + } + case "hu_hu": + { + await flutterLocalNotificationsPlugin.show( + lesson.id.hashCode, + "title_lesson".i18n, + "body_lesson_substituted".i18n.fill([ + userProvider.displayName!, + dayTitle(lesson.date), + lesson.lessonIndex, + lesson.name, + lesson.substituteTeacher!.isRenamed + ? lesson.substituteTeacher!.renamedTo! + : lesson.substituteTeacher!.name + ]), + notificationDetails); + break; + } + default: + { + await flutterLocalNotificationsPlugin.show( + lesson.id.hashCode, + "title_lesson".i18n, + "body_lesson_substituted".i18n.fill([ + userProvider.displayName!, + lesson.lessonIndex, + lesson.name, + dayTitle(lesson.date), + lesson.substituteTeacher!.isRenamed + ? lesson.substituteTeacher!.renamedTo! + : lesson.substituteTeacher!.name + ]), + notificationDetails); + break; + } + } + } + } + } + } + // combine modified lesson and storedlesson list and save them to the database + List combinedlessons = combineLists( + modifiedlessons, + storedlessons, + (Lesson message) => message.id, + ); + Map> timetableLessons = timetableProvider.lessons; + timetableLessons[Week.current()] = combinedlessons; + await database.userStore + .storeLessons(timetableLessons, userId: userProvider.id!); + } + } +} \ No newline at end of file diff --git a/filcnaplo/lib/helpers/notification_helper.i18n.dart b/filcnaplo/lib/helpers/notification_helper.i18n.dart index 70f54d9..839646b 100644 --- a/filcnaplo/lib/helpers/notification_helper.i18n.dart +++ b/filcnaplo/lib/helpers/notification_helper.i18n.dart @@ -10,6 +10,11 @@ extension Localization on String { "title_absence": "Absence recorded", "body_absence": "An absence was recorded on %s for %s", "body_absence_multiuser": "An absence was recorded for %s on %s for the subject %s", + "title_lesson": "Timetable modified", + "body_lesson_canceled": "Lesson #%s (%s) has been canceled on %s", + "body_lesson_canceled_multiuser": "(%s) Lesson #%s (%s) has been canceled on %s", + "body_lesson_substituted": "Lesson #%s (%s) on %s will be substituted by %s", + "body_lesson_substituted_multiuser": "(%s) Lesson #%s (%s) on %s will be substituted by %s" }, "hu_hu": { "title_grade": "Új jegy", @@ -18,6 +23,11 @@ extension Localization on String { "title_absence": "Új hiányzás", "body_absence": "Új hiányzást kaptál %s napon %s tantárgyból", "body_absence_multiuser": "%s tanuló új hiányzást kapott %s napon %s tantárgyból", + "title_lesson": "Órarend szerkesztve", + "body_lesson_canceled": "%s-i %s. óra (%s) elmarad", + "body_lesson_canceled_multiuser": "(%s) %s-i %s. óra (%s) elmarad", + "body_lesson_substituted": "%s-i %s. (%s) órát %s helyetessíti", + "body_lesson_substituted_multiuser": "(%s) %s-i %s. (%s) órát %s helyetessíti" }, "de_de": { "title_grade": "Neue Note", @@ -26,6 +36,11 @@ extension Localization on String { "title_absence": "Abwesenheit aufgezeichnet", "body_absence": "Auf %s für %s wurde eine Abwesenheit aufgezeichnet", "body_absence_multiuser": "Für %s wurde am %s für das Thema Mathematik eine Abwesenheit aufgezeichnet", + "title_lesson": "Fahrplan geändert", + "body_lesson_canceled": "Lektion Nr. %s (%s) wurde am %s abgesagt", + "body_lesson_canceled_multiuser": "(%s) Lektion Nr. %s (%s) wurde am %s abgesagt", + "body_lesson_substituted": "Lektion Nr. %s (%s) wird am %s durch %s ersetzt", + "body_lesson_substituted_multiuser": "(%s) Lektion Nr. %s (%s) wird am %s durch %s ersetzt" }, }; diff --git a/filcnaplo/lib/models/settings.dart b/filcnaplo/lib/models/settings.dart index 26969ba..9f59887 100644 --- a/filcnaplo/lib/models/settings.dart +++ b/filcnaplo/lib/models/settings.dart @@ -34,6 +34,7 @@ class SettingsProvider extends ChangeNotifier { bool _notificationsGradesEnabled; bool _notificationsAbsencesEnabled; bool _notificationsMessagesEnabled; + bool _notificationsLessonsEnabled; /* notificationsBitfield values: @@ -90,6 +91,7 @@ class SettingsProvider extends ChangeNotifier { required bool notificationsGradesEnabled, required bool notificationsAbsencesEnabled, required bool notificationsMessagesEnabled, + required bool notificationsLessonsEnabled, required int notificationsBitfield, required bool developerMode, required int notificationPollInterval, @@ -131,6 +133,7 @@ class SettingsProvider extends ChangeNotifier { _notificationsGradesEnabled = notificationsGradesEnabled, _notificationsAbsencesEnabled = notificationsAbsencesEnabled, _notificationsMessagesEnabled = notificationsMessagesEnabled, + _notificationsLessonsEnabled = notificationsLessonsEnabled, _notificationsBitfield = notificationsBitfield, _developerMode = developerMode, _notificationPollInterval = notificationPollInterval, @@ -190,6 +193,7 @@ class SettingsProvider extends ChangeNotifier { notificationsGradesEnabled: map["notifications_grades"] == 1, notificationsAbsencesEnabled: map["notifications_absences"] == 1, notificationsMessagesEnabled: map["notifications_messages"] == 1, + notificationsLessonsEnabled: map["notifications_lessons"] == 1, notificationsBitfield: map["notifications_bitfield"], notificationPollInterval: map["notification_poll_interval"], developerMode: map["developer_mode"] == 1, @@ -235,6 +239,7 @@ class SettingsProvider extends ChangeNotifier { "notifications_grades": _notificationsGradesEnabled ? 1 : 0, "notifications_absences": _notificationsAbsencesEnabled ? 1 : 0, "notifications_messages": _notificationsMessagesEnabled ? 1 : 0, + "notifications_lessons": _notificationsLessonsEnabled ? 1 : 0, "notifications_bitfield": _notificationsBitfield, "developer_mode": _developerMode ? 1 : 0, "grade_color1": _gradeColors[0].value, @@ -291,6 +296,7 @@ class SettingsProvider extends ChangeNotifier { notificationsGradesEnabled: true, notificationsAbsencesEnabled: true, notificationsMessagesEnabled: true, + notificationsLessonsEnabled: true, notificationsBitfield: 255, developerMode: false, notificationPollInterval: 1, @@ -335,6 +341,7 @@ class SettingsProvider extends ChangeNotifier { bool get notificationsGradesEnabled => _notificationsGradesEnabled; bool get notificationsAbsencesEnabled => _notificationsAbsencesEnabled; bool get notificationsMessagesEnabled => _notificationsMessagesEnabled; + bool get notificationsLessonsEnabled => _notificationsLessonsEnabled; int get notificationsBitfield => _notificationsBitfield; bool get developerMode => _developerMode; int get notificationPollInterval => _notificationPollInterval; @@ -381,6 +388,7 @@ class SettingsProvider extends ChangeNotifier { bool? notificationsGradesEnabled, bool? notificationsAbsencesEnabled, bool? notificationsMessagesEnabled, + bool? notificationsLessonsEnabled, int? notificationsBitfield, bool? developerMode, int? notificationPollInterval, @@ -444,6 +452,10 @@ class SettingsProvider extends ChangeNotifier { notificationsMessagesEnabled != _notificationsMessagesEnabled) { _notificationsMessagesEnabled = notificationsMessagesEnabled; } + if (notificationsLessonsEnabled != null && + notificationsLessonsEnabled != _notificationsLessonsEnabled) { + _notificationsLessonsEnabled = notificationsLessonsEnabled; + } if (notificationsBitfield != null && notificationsBitfield != _notificationsBitfield) { _notificationsBitfield = notificationsBitfield; diff --git a/filcnaplo_kreta_api/lib/models/lesson.dart b/filcnaplo_kreta_api/lib/models/lesson.dart index 0ac9ab2..e232b76 100644 --- a/filcnaplo_kreta_api/lib/models/lesson.dart +++ b/filcnaplo_kreta_api/lib/models/lesson.dart @@ -25,6 +25,7 @@ class Lesson { String name; bool online; bool isEmpty; + bool isSeen; Lesson({ this.status, @@ -49,7 +50,15 @@ class Lesson { this.online = false, this.isEmpty = false, this.json, + this.isSeen = false, }); + @override + bool operator ==(Object other) => + identical(this, other) || + other is Lesson && runtimeType == other.runtimeType && id == other.id; + + @override + int get hashCode => id.hashCode; factory Lesson.fromJson(Map json) { return Lesson( @@ -90,6 +99,7 @@ class Lesson { online: json["IsDigitalisOra"] ?? false, isEmpty: json['isEmpty'] ?? false, json: json, + isSeen: false ); } diff --git a/filcnaplo_kreta_api/lib/providers/timetable_provider.dart b/filcnaplo_kreta_api/lib/providers/timetable_provider.dart index 841a35f..1e31308 100644 --- a/filcnaplo_kreta_api/lib/providers/timetable_provider.dart +++ b/filcnaplo_kreta_api/lib/providers/timetable_provider.dart @@ -8,7 +8,7 @@ import 'package:filcnaplo_kreta_api/models/week.dart'; import 'package:flutter/material.dart'; class TimetableProvider with ChangeNotifier { - Map> _lessons = {}; + Map> lessons = {}; late final UserProvider _user; late final DatabaseProvider _database; late final KretaClient _kreta; @@ -29,7 +29,7 @@ class TimetableProvider with ChangeNotifier { // Load lessons from the database if (userId != null) { var dbLessons = await _database.userQuery.getLessons(userId: userId); - _lessons = dbLessons; + lessons = dbLessons; await convertBySettings(); } } @@ -45,7 +45,7 @@ class TimetableProvider with ChangeNotifier { ? await _database.userQuery.renamedTeachers(userId: _user.id!) : {}; - for (Lesson lesson in _lessons.values.expand((e) => e)) { + for (Lesson lesson in lessons.values.expand((e) => e)) { lesson.subject.renamedTo = renamedSubjects.isNotEmpty ? renamedSubjects[lesson.subject.id] : null; @@ -57,7 +57,7 @@ class TimetableProvider with ChangeNotifier { notifyListeners(); } - List? getWeek(Week week) => _lessons[week]; + List? getWeek(Week week) => lessons[week]; // Fetches Lessons from the Kreta API then stores them in the database Future fetch({Week? week}) async { @@ -68,11 +68,11 @@ class TimetableProvider with ChangeNotifier { List? lessonsJson = await _kreta .getAPI(KretaAPI.timetable(iss, start: week.start, end: week.end)); if (lessonsJson == null) throw "Cannot fetch Lessons for User ${user.id}"; - List lessons = lessonsJson.map((e) => Lesson.fromJson(e)).toList(); + List lessonsList = lessonsJson.map((e) => Lesson.fromJson(e)).toList(); - if (lessons.isEmpty && _lessons.isEmpty) return; + if (lessons.isEmpty && lessons.isEmpty) return; - _lessons[week] = lessons; + lessons[week] = lessonsList; await store(); await convertBySettings(); @@ -85,7 +85,7 @@ class TimetableProvider with ChangeNotifier { String userId = user.id; // -TODO: clear indexes with weeks outside of the current school year - await _database.userStore.storeLessons(_lessons, userId: userId); + await _database.userStore.storeLessons(lessons, userId: userId); } // Future setLessonCount(SubjectLessonCount lessonCount, {bool store = true}) async { diff --git a/filcnaplo_mobile_ui/lib/screens/settings/notifications_screen.dart b/filcnaplo_mobile_ui/lib/screens/settings/notifications_screen.dart index b1ea8a1..c8a2f49 100644 --- a/filcnaplo_mobile_ui/lib/screens/settings/notifications_screen.dart +++ b/filcnaplo_mobile_ui/lib/screens/settings/notifications_screen.dart @@ -142,6 +142,32 @@ class NotificationsScreen extends StatelessWidget { ), ], ), + ), + SwitchListTile( + value: settings.notificationsLessonsEnabled, + onChanged: (v) => {settings.update(notificationsLessonsEnabled: v)}, + title: Row( + children: [ + const SizedBox(width: 8), + settings.notificationsLessonsEnabled + ? const Icon(FeatherIcons.calendar) + : Icon(FeatherIcons.calendar, + color: + AppColors.of(context).text.withOpacity(.25)), + const SizedBox(width: 23.0), + Expanded( + child: Text( + "lessons".i18n, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16.0, + color: AppColors.of(context).text.withOpacity( + settings.notificationsLessonsEnabled ? 1.0 : .5), + ), + ), + ), + ], + ), ) ]), )))); diff --git a/filcnaplo_mobile_ui/lib/screens/settings/notifications_screen.i18n.dart b/filcnaplo_mobile_ui/lib/screens/settings/notifications_screen.i18n.dart index 6dccf41..c9d0087 100644 --- a/filcnaplo_mobile_ui/lib/screens/settings/notifications_screen.i18n.dart +++ b/filcnaplo_mobile_ui/lib/screens/settings/notifications_screen.i18n.dart @@ -8,6 +8,7 @@ extension SettingsLocalization on String { "grades": "Grades", "absences": "Absences", "messages": "Messages", + "lessons": "Lessons" }, "hu_hu": { @@ -15,12 +16,14 @@ extension SettingsLocalization on String { "grades": "Jegyek", "absences": "Hiányzások", "messages": "Üzenetek", + "lessons": "Órák" }, "de_de": { "notifications_screen": "Mitteilung", "grades": "Noten", "absences": "Fehlen", - "messages": "Nachrichten" + "messages": "Nachrichten", + "lessons": "Unterricht" }, };