diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml new file mode 100644 index 0000000..86624d6 --- /dev/null +++ b/.idea/libraries/Dart_Packages.xml @@ -0,0 +1,1286 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Dart_SDK.xml b/.idea/libraries/Dart_SDK.xml new file mode 100644 index 0000000..563d1c4 --- /dev/null +++ b/.idea/libraries/Dart_SDK.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..de791dd --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/filcnaplo/lib/database/init.dart b/filcnaplo/lib/database/init.dart index 7c29324..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 + "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 568bb62..7ee3a39 100644 --- a/filcnaplo/lib/helpers/notification_helper.dart +++ b/filcnaplo/lib/helpers/notification_helper.dart @@ -1,35 +1,90 @@ -import 'dart:math'; import 'dart:ui'; import 'package:filcnaplo/api/providers/database_provider.dart'; import 'package:filcnaplo/api/providers/status_provider.dart'; import 'package:filcnaplo/api/providers/user_provider.dart'; +import 'package:filcnaplo/database/init.dart'; import 'package:filcnaplo/models/settings.dart'; import 'package:filcnaplo/helpers/notification_helper.i18n.dart'; +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:flutter_local_notifications/flutter_local_notifications.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'; class NotificationsHelper { + late DatabaseProvider database; + late SettingsProvider settingsProvider; + late UserProvider userProvider; + late KretaClient kretaClient; + FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + + List combineLists(List list1, List list2, K Function(T) keyExtractor,) { + Set uniqueKeys = Set(); + List combinedList = []; + + for (T item in list1) { + K key = keyExtractor(item); + if (!uniqueKeys.contains(key)) { + uniqueKeys.add(key); + combinedList.add(item); + } + } + + for (T item in list2) { + K key = keyExtractor(item); + if (!uniqueKeys.contains(key)) { + uniqueKeys.add(key); + combinedList.add(item); + } + } + + 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 FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); - DatabaseProvider database = DatabaseProvider(); + database = DatabaseProvider(); + var db = await initDB(database); await database.init(); - SettingsProvider settingsProvider = + settingsProvider = await database.query.getSettings(database); - UserProvider userProvider = await database.query.getUsers(settingsProvider); + userProvider = await database.query.getUsers(settingsProvider); if (userProvider.id != null && settingsProvider.notificationsEnabled) { - // refresh grades + // refresh kreta login final status = StatusProvider(); - final kretaClient = KretaClient( + kretaClient = KretaClient( user: userProvider, settings: settingsProvider, status: status); kretaClient.refreshLogin(); - GradeProvider gradeProvider = GradeProvider( + if(settingsProvider.notificationsGradesEnabled) gradeNotification(); + if(settingsProvider.notificationsAbsencesEnabled) absenceNotification(); + if(settingsProvider.notificationsMessagesEnabled) messageNotification(); + if(settingsProvider.notificationsLessonsEnabled) lessonNotification(); + } + } + + void gradeNotification() async { + // fetch grades + GradeProvider gradeProvider = GradeProvider( settings: settingsProvider, user: userProvider, database: database, @@ -45,39 +100,425 @@ class NotificationsHelper { // if grade is not a normal grade (1-5), don't show it if ([1, 2, 3, 4, 5].contains(grade.value.value)) { // if the grade was added over a week ago, don't show it to avoid notification spam - if (grade.seenDate.isAfter(lastSeenGrade) && - grade.date.difference(DateTime.now()).inDays * -1 < 7) { + if (grade.seenDate.isAfter(lastSeenGrade) && grade.date.difference(DateTime.now()).inDays * -1 < 7) { // send notificiation about new grade const AndroidNotificationDetails androidNotificationDetails = - AndroidNotificationDetails( - 'GRADES', - 'Jegyek', - channelDescription: 'Értesítés jegyek beírásakor', - importance: Importance.max, - priority: Priority.max, - color: Color(0xFF3D7BF4), - ticker: 'Jegyek', - groupKey: 'refilc.notifications.GRADES_GROUP', - ); - const NotificationDetails notificationDetails = - NotificationDetails(android: androidNotificationDetails); - await flutterLocalNotificationsPlugin.show( - // probably shouldn't use a random int - Random().nextInt(432234 * 2), - "title".i18n, - "body".i18n.fill([ - grade.value.value.toString(), - grade.subject.isRenamed && - settingsProvider.renamedSubjectsEnabled - ? grade.subject.renamedTo! - : grade.subject.name - ]), - notificationDetails); - } - } + AndroidNotificationDetails('GRADES', 'Jegyek', + channelDescription: 'Értesítés jegyek beírásakor', + importance: Importance.max, + priority: Priority.max, + color: const Color(0xFF3D7BF4), + ticker: 'Jegyek'); + const NotificationDetails notificationDetails = NotificationDetails(android: androidNotificationDetails); + if(userProvider.getUsers().length == 1) { + await flutterLocalNotificationsPlugin.show( + grade.id.hashCode, + "title_grade".i18n, + "body_grade".i18n.fill([ + grade.value.value.toString(), + grade.subject.isRenamed && + settingsProvider.renamedSubjectsEnabled + ? grade.subject.renamedTo! + : grade.subject.name + ]), + notificationDetails); + } else { // multiple users are added, also display student name + await flutterLocalNotificationsPlugin.show( + grade.id.hashCode, + "title_grade".i18n, + "body_grade_multiuser".i18n.fill([ + userProvider.displayName!, + grade.value.value.toString(), + grade.subject.isRenamed && + settingsProvider.renamedSubjectsEnabled + ? grade.subject.renamedTo! + : grade.subject.name + ]), + notificationDetails); + } + } + } } // set grade seen status gradeProvider.seenAll(); + } + void absenceNotification() async { + // get absences from api + List? absenceJson = await kretaClient.getAPI(KretaAPI.absences(userProvider.instituteCode ?? "")); + List storedAbsences = await database.userQuery.getAbsences(userId: userProvider.id!); + if(absenceJson == null) { + return; + } + // format api absences to correct format while preserving isSeen value + List absences = absenceJson.map((e) { + Absence apiAbsence = Absence.fromJson(e); + Absence storedAbsence = storedAbsences.firstWhere( + (stored) => stored.id == apiAbsence.id, + orElse: () => apiAbsence); + apiAbsence.isSeen = storedAbsence.isSeen; + return apiAbsence; + }).toList(); + List modifiedAbsences = []; + if(absences != storedAbsences) { + // remove absences that are not new + absences.removeWhere((element) => storedAbsences.contains(element)); + for(Absence absence in absences) { + if(!absence.isSeen) { + absence.isSeen = true; + modifiedAbsences.add(absence); + const AndroidNotificationDetails androidNotificationDetails = + AndroidNotificationDetails('ABSENCES', 'Hiányzások', + channelDescription: 'Értesítés hiányzások beírásakor', + importance: Importance.max, + priority: Priority.max, + color: const Color(0xFF3D7BF4), + ticker: 'Hiányzások'); + const NotificationDetails notificationDetails = NotificationDetails(android: androidNotificationDetails); + if(userProvider.getUsers().length == 1) { + await flutterLocalNotificationsPlugin.show( + absence.id.hashCode, + "title_absence".i18n, + "body_absence".i18n.fill([ + DateFormat("yyyy-MM-dd").format(absence.date), + absence.subject.isRenamed && + settingsProvider.renamedSubjectsEnabled + ? absence.subject.renamedTo! + : absence.subject.name + ]), + notificationDetails); + } else { + await flutterLocalNotificationsPlugin.show( + absence.id.hashCode, + "title_absence".i18n, + "body_absence_multiuser".i18n.fill([ + userProvider.displayName!, + DateFormat("yyyy-MM-dd").format(absence.date), + absence.subject.isRenamed && + settingsProvider.renamedSubjectsEnabled + ? absence.subject.renamedTo! + : absence.subject.name + ]), + notificationDetails); + } + } + } + } + // combine modified absences and storedabsences list and save them to the database + List combinedAbsences = combineLists( + modifiedAbsences, + storedAbsences, + (Absence absence) => absence.id, + ); + await database.userStore.storeAbsences(combinedAbsences, userId: userProvider.id!); + } + + void messageNotification() async { + // get messages from api + List? messageJson = await kretaClient.getAPI(KretaAPI.messages("beerkezett")); + List storedmessages = await database.userQuery.getMessages(userId: userProvider.id!); + if(messageJson == null) { + return; + } + // format api messages to correct format while preserving isSeen value + // Parse messages + List messages = []; + await Future.wait(List.generate(messageJson.length, (index) { + return () async { + Map message = messageJson!.cast()[index]; + Map? innerMessageJson = await kretaClient.getAPI(KretaAPI.message(message["azonosito"].toString())); + if (innerMessageJson != null) messages.add(Message.fromJson(innerMessageJson, forceType: MessageType.inbox)); + }(); + })); + + for(Message message in messages) { + for(Message storedMessage in storedmessages) { + if(message.id == storedMessage.id) { + message.isSeen = storedMessage.isSeen; + } + } + } + List modifiedmessages = []; + if(messages != storedmessages) { + // remove messages that are not new + messages.removeWhere((element) => storedmessages.contains(element)); + for(Message message in messages) { + if(!message.isSeen) { + message.isSeen = true; + modifiedmessages.add(message); + const AndroidNotificationDetails androidNotificationDetails = + AndroidNotificationDetails('MESSAGES', 'Üzenetek', + channelDescription: 'Értesítés kapott üzenetekkor', + importance: Importance.max, + priority: Priority.max, + color: const Color(0xFF3D7BF4), + ticker: 'Üzenetek'); + const NotificationDetails notificationDetails = NotificationDetails(android: androidNotificationDetails); + if(userProvider.getUsers().length == 1) { + await flutterLocalNotificationsPlugin.show( + message.id.hashCode, + message.author, + message.content.replaceAll(RegExp(r'<[^>]*>'), ''), + notificationDetails); + } else { + await flutterLocalNotificationsPlugin.show( + message.id.hashCode, + "(${userProvider.displayName!}) ${message.author}", + message.content.replaceAll(RegExp(r'<[^>]*>'), ''), + notificationDetails); + } + } + } + } + // combine modified messages and storedmessages list and save them to the database + List combinedmessages = combineLists( + modifiedmessages, + storedmessages, + (Message message) => message.id, + ); + 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 5c96488..37e6ff0 100644 --- a/filcnaplo/lib/helpers/notification_helper.i18n.dart +++ b/filcnaplo/lib/helpers/notification_helper.i18n.dart @@ -4,16 +4,43 @@ extension Localization on String { static final _t = Translations.byLocale("hu_hu") + { "en_en": { - "title": "New grade", - "body": "You got a %s in %s" + "title_grade": "New grade", + "body_grade": "You got a %s in %s", + "body_grade_multiuser": "%s got a %s in %s", + "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": "Új jegy", - "body": "%s-st kaptál %s tantárgyból" + "title_grade": "Új jegy", + "body_grade": "%s-st kaptál %s tantárgyból", + "body_grade_multiuser": "%s tanuló %s-st kapott %s tantárgyból", + "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 helyettesíti", + "body_lesson_substituted_multiuser": "(%s) %s-i %s. (%s) órát %s helyettesíti" }, "de_de": { - "title": "Neue Note", - "body": "Du hast eine %s in %s" + "title_grade": "Neue Note", + "body_grade": "Du hast eine %s in %s", + "body_grade_multiuser": "%s hast eine %s in %s", + "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 6673228..9f59887 100644 --- a/filcnaplo/lib/models/settings.dart +++ b/filcnaplo/lib/models/settings.dart @@ -31,6 +31,10 @@ class SettingsProvider extends ChangeNotifier { bool _newsEnabled; String _seenNews; bool _notificationsEnabled; + bool _notificationsGradesEnabled; + bool _notificationsAbsencesEnabled; + bool _notificationsMessagesEnabled; + bool _notificationsLessonsEnabled; /* notificationsBitfield values: @@ -84,6 +88,10 @@ class SettingsProvider extends ChangeNotifier { required bool newsEnabled, required String seenNews, required bool notificationsEnabled, + required bool notificationsGradesEnabled, + required bool notificationsAbsencesEnabled, + required bool notificationsMessagesEnabled, + required bool notificationsLessonsEnabled, required int notificationsBitfield, required bool developerMode, required int notificationPollInterval, @@ -122,6 +130,10 @@ class SettingsProvider extends ChangeNotifier { _newsEnabled = newsEnabled, _seenNews = seenNews, _notificationsEnabled = notificationsEnabled, + _notificationsGradesEnabled = notificationsGradesEnabled, + _notificationsAbsencesEnabled = notificationsAbsencesEnabled, + _notificationsMessagesEnabled = notificationsMessagesEnabled, + _notificationsLessonsEnabled = notificationsLessonsEnabled, _notificationsBitfield = notificationsBitfield, _developerMode = developerMode, _notificationPollInterval = notificationPollInterval, @@ -178,6 +190,10 @@ class SettingsProvider extends ChangeNotifier { newsEnabled: map["news"] == 1, seenNews: map["seen_news"], notificationsEnabled: map["notifications"] == 1, + 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, @@ -220,6 +236,10 @@ class SettingsProvider extends ChangeNotifier { "news": _newsEnabled ? 1 : 0, "seen_news": _seenNews, "notifications": _notificationsEnabled ? 1 : 0, + "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, @@ -273,6 +293,10 @@ class SettingsProvider extends ChangeNotifier { newsEnabled: true, seenNews: '', notificationsEnabled: true, + notificationsGradesEnabled: true, + notificationsAbsencesEnabled: true, + notificationsMessagesEnabled: true, + notificationsLessonsEnabled: true, notificationsBitfield: 255, developerMode: false, notificationPollInterval: 1, @@ -314,6 +338,10 @@ class SettingsProvider extends ChangeNotifier { bool get newsEnabled => _newsEnabled; List get seenNews => _seenNews.split(','); bool get notificationsEnabled => _notificationsEnabled; + 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; @@ -357,6 +385,10 @@ class SettingsProvider extends ChangeNotifier { bool? newsEnabled, String? seenNewsId, bool? notificationsEnabled, + bool? notificationsGradesEnabled, + bool? notificationsAbsencesEnabled, + bool? notificationsMessagesEnabled, + bool? notificationsLessonsEnabled, int? notificationsBitfield, bool? developerMode, int? notificationPollInterval, @@ -408,6 +440,22 @@ class SettingsProvider extends ChangeNotifier { notificationsEnabled != _notificationsEnabled) { _notificationsEnabled = notificationsEnabled; } + if (notificationsGradesEnabled != null && + notificationsGradesEnabled != _notificationsGradesEnabled) { + _notificationsGradesEnabled = notificationsGradesEnabled; + } + if (notificationsAbsencesEnabled != null && + notificationsAbsencesEnabled != _notificationsAbsencesEnabled) { + _notificationsAbsencesEnabled = notificationsAbsencesEnabled; + } + if (notificationsMessagesEnabled != null && + notificationsMessagesEnabled != _notificationsMessagesEnabled) { + _notificationsMessagesEnabled = notificationsMessagesEnabled; + } + if (notificationsLessonsEnabled != null && + notificationsLessonsEnabled != _notificationsLessonsEnabled) { + _notificationsLessonsEnabled = notificationsLessonsEnabled; + } if (notificationsBitfield != null && notificationsBitfield != _notificationsBitfield) { _notificationsBitfield = notificationsBitfield; diff --git a/filcnaplo/lib/ui/filter/widgets.dart b/filcnaplo/lib/ui/filter/widgets.dart index bce82ae..54589d7 100644 --- a/filcnaplo/lib/ui/filter/widgets.dart +++ b/filcnaplo/lib/ui/filter/widgets.dart @@ -89,7 +89,9 @@ Future> getFilterWidgets(FilterType activeData, // Grades case FilterType.grades: - gradeProvider.seenAll(); + if(!settingsProvider.gradeOpeningFun) { + gradeProvider.seenAll(); + } items = grade_filter.getWidgets( gradeProvider.grades, gradeProvider.lastSeenDate); if (settingsProvider.gradeOpeningFun) { diff --git a/filcnaplo/lib/ui/widgets/grade/grade_tile.dart b/filcnaplo/lib/ui/widgets/grade/grade_tile.dart index cfb9365..50e0d3a 100644 --- a/filcnaplo/lib/ui/widgets/grade/grade_tile.dart +++ b/filcnaplo/lib/ui/widgets/grade/grade_tile.dart @@ -192,6 +192,7 @@ class GradeValueWidget extends StatelessWidget { this.outline = false, this.complemented = false, this.nocolor = false, + this.color, }) : super(key: key); final GradeValue value; @@ -202,6 +203,7 @@ class GradeValueWidget extends StatelessWidget { final bool outline; final bool complemented; final bool nocolor; + final Color? color; @override Widget build(BuildContext context) { @@ -209,7 +211,7 @@ class GradeValueWidget extends StatelessWidget { bool isSubjectView = SubjectGradesContainer.of(context) != null; Color color = - gradeColor(context: context, value: value.value, nocolor: nocolor); + this.color ?? gradeColor(context: context, value: value.value, nocolor: nocolor); Widget valueText; final percentage = value.percentage; diff --git a/filcnaplo_kreta_api/lib/models/absence.dart b/filcnaplo_kreta_api/lib/models/absence.dart index d694c86..9997351 100644 --- a/filcnaplo_kreta_api/lib/models/absence.dart +++ b/filcnaplo_kreta_api/lib/models/absence.dart @@ -18,6 +18,14 @@ class Absence { DateTime lessonEnd; int? lessonIndex; String group; + bool isSeen; + @override + bool operator ==(Object other) => + identical(this, other) || + other is Absence && runtimeType == other.runtimeType && id == other.id; + + @override + int get hashCode => id.hashCode; Absence({ required this.id, @@ -35,6 +43,7 @@ class Absence { this.lessonIndex, required this.group, this.json, + this.isSeen = false, }); factory Absence.fromJson(Map json) { @@ -80,6 +89,7 @@ class Absence { lessonIndex: lessonIndex, group: json["OsztalyCsoport"] != null ? json["OsztalyCsoport"]["Uid"] : "", + isSeen: false, json: json, ); } 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/models/message.dart b/filcnaplo_kreta_api/lib/models/message.dart index c086830..97cd5f3 100644 --- a/filcnaplo_kreta_api/lib/models/message.dart +++ b/filcnaplo_kreta_api/lib/models/message.dart @@ -16,6 +16,7 @@ class Message { MessageType? type; List recipients; List attachments; + bool isSeen; Message({ required this.id, @@ -32,7 +33,15 @@ class Message { this.replyId, this.conversationId, this.json, + this.isSeen = false, }); + @override + bool operator ==(Object other) => + identical(this, other) || + other is Message && runtimeType == other.runtimeType && id == other.id; + + @override + int get hashCode => id.hashCode; factory Message.fromJson(Map json, {MessageType? forceType}) { Map message = json["uzenet"]; @@ -69,6 +78,7 @@ class Message { replyId: message["elozoUzenetAzonosito"], conversationId: message["beszelgetesAzonosito"], 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 new file mode 100644 index 0000000..c8a2f49 --- /dev/null +++ b/filcnaplo_mobile_ui/lib/screens/settings/notifications_screen.dart @@ -0,0 +1,175 @@ +import 'package:filcnaplo/models/settings.dart'; +import 'package:filcnaplo/theme/colors/colors.dart'; +import 'package:filcnaplo/ui/widgets/grade/grade_tile.dart'; +import 'package:filcnaplo_kreta_api/models/grade.dart'; +import 'package:filcnaplo_mobile_ui/common/panel/panel.dart'; +import 'package:filcnaplo_mobile_ui/common/panel/panel_button.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_feather_icons/flutter_feather_icons.dart'; +import 'package:provider/provider.dart'; +import 'notifications_screen.i18n.dart'; + +class MenuNotifications extends StatelessWidget { + const MenuNotifications({Key? key, required this.settings}) : super(key: key); + + final SettingsProvider settings; + + @override + Widget build(BuildContext context) { + return PanelButton( + padding: const EdgeInsets.only(left: 14.0), + onPressed: () { + Navigator.of(context, rootNavigator: true).push( + CupertinoPageRoute(builder: (context) => NotificationsScreen()), + ); + }, + title: Text( + "notifications_screen".i18n, + style: TextStyle( + color: AppColors.of(context) + .text + .withOpacity(settings.notificationsEnabled ? 1.0 : .5)), + ), + leading: settings.notificationsEnabled + ? const Icon(FeatherIcons.messageSquare) + : Icon(FeatherIcons.messageSquare, + color: AppColors.of(context).text.withOpacity(.25)), + trailingDivider: true, + trailing: Switch( + onChanged: (v) async { + settings.update(notificationsEnabled: v); + }, + value: settings.notificationsEnabled, + activeColor: Theme.of(context).colorScheme.secondary, + ), + ); + } +} + +class NotificationsScreen extends StatelessWidget { + NotificationsScreen({super.key}); + late SettingsProvider settings; + + @override + Widget build(BuildContext context) { + settings = Provider.of(context); + return Scaffold( + appBar: AppBar( + surfaceTintColor: Theme.of(context).scaffoldBackgroundColor, + leading: BackButton(color: AppColors.of(context).text), + title: Text( + "notifications_screen".i18n, + style: TextStyle(color: AppColors.of(context).text), + ), + ), + body: Padding( + padding: + const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0), + child: SingleChildScrollView( + child: Panel( + child: Column(children: [ + SwitchListTile( + value: settings.notificationsGradesEnabled, + onChanged: (v) => {settings.update(notificationsGradesEnabled: v)}, + title: Row( + children: [ + GradeValueWidget(GradeValue(5, "", "", 100), fill: true, size: 30, color: settings.gradeColors[4].withOpacity( + settings.notificationsGradesEnabled ? 1.0 : .5)), + const SizedBox(width: 14.0), + Expanded( + child: Text( + "grades".i18n, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16.0, + color: AppColors.of(context).text.withOpacity( + settings.notificationsGradesEnabled ? 1.0 : .5), + ), + ), + ), + ], + ), + ), + SwitchListTile( + value: settings.notificationsAbsencesEnabled, + onChanged: (v) => {settings.update(notificationsAbsencesEnabled: v)}, + title: Row( + children: [ + const SizedBox(width: 8), + settings.notificationsAbsencesEnabled + ? const Icon(FeatherIcons.clock) + : Icon(FeatherIcons.clock, + color: + AppColors.of(context).text.withOpacity(.25)), + const SizedBox(width: 23.0), + Expanded( + child: Text( + "absences".i18n, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16.0, + color: AppColors.of(context).text.withOpacity( + settings.notificationsAbsencesEnabled ? 1.0 : .5), + ), + ), + ), + ], + ), + ), + SwitchListTile( + value: settings.notificationsMessagesEnabled, + onChanged: (v) => {settings.update(notificationsMessagesEnabled: v)}, + title: Row( + children: [ + const SizedBox(width: 8), + settings.notificationsMessagesEnabled + ? const Icon(FeatherIcons.messageSquare) + : Icon(FeatherIcons.messageSquare, + color: + AppColors.of(context).text.withOpacity(.25)), + const SizedBox(width: 23.0), + Expanded( + child: Text( + "messages".i18n, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16.0, + color: AppColors.of(context).text.withOpacity( + settings.notificationsMessagesEnabled ? 1.0 : .5), + ), + ), + ), + ], + ), + ), + 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 new file mode 100644 index 0000000..c9d0087 --- /dev/null +++ b/filcnaplo_mobile_ui/lib/screens/settings/notifications_screen.i18n.dart @@ -0,0 +1,34 @@ +import 'package:i18n_extension/i18n_extension.dart'; + +extension SettingsLocalization on String { + static final _t = Translations.byLocale("hu_hu") + + { + "en_en": { + "notifications_screen": "Notifications", + "grades": "Grades", + "absences": "Absences", + "messages": "Messages", + "lessons": "Lessons" + + }, + "hu_hu": { + "notifications_screen": "Értesítések", + "grades": "Jegyek", + "absences": "Hiányzások", + "messages": "Üzenetek", + "lessons": "Órák" + }, + "de_de": { + "notifications_screen": "Mitteilung", + "grades": "Noten", + "absences": "Fehlen", + "messages": "Nachrichten", + "lessons": "Unterricht" + }, + }; + + String get i18n => localize(this, _t); + String fill(List params) => localizeFill(this, params); + String plural(int value) => localizePlural(value, this, _t); + String version(Object modifier) => localizeVersion(modifier, this, _t); +} diff --git a/filcnaplo_mobile_ui/lib/screens/settings/settings_screen.dart b/filcnaplo_mobile_ui/lib/screens/settings/settings_screen.dart index dcbd8ba..d0f7774 100755 --- a/filcnaplo_mobile_ui/lib/screens/settings/settings_screen.dart +++ b/filcnaplo_mobile_ui/lib/screens/settings/settings_screen.dart @@ -27,6 +27,7 @@ import 'package:filcnaplo_mobile_ui/screens/news/news_screen.dart'; import 'package:filcnaplo_mobile_ui/screens/settings/accounts/account_tile.dart'; import 'package:filcnaplo_mobile_ui/screens/settings/accounts/account_view.dart'; import 'package:filcnaplo_mobile_ui/screens/settings/debug/subject_icon_gallery.dart'; +import 'package:filcnaplo_mobile_ui/screens/settings/notifications_screen.dart'; import 'package:filcnaplo_mobile_ui/screens/settings/privacy_view.dart'; import 'package:filcnaplo_mobile_ui/screens/settings/settings_helper.dart'; import 'package:flutter/cupertino.dart'; @@ -453,41 +454,7 @@ class _SettingsScreenState extends State ), Material( type: MaterialType.transparency, - child: SwitchListTile( - value: settings.notificationsEnabled, - activeColor: Theme.of(context).colorScheme.secondary, - contentPadding: const EdgeInsets.only(left: 12.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0)), - title: Row( - children: [ - Icon(FeatherIcons.messageSquare, - color: settings.notificationsEnabled - ? Theme.of(context).colorScheme.secondary - : AppColors.of(context) - .text - .withOpacity(.25)), - const SizedBox(width: 14.0), - Text( - "notifications".i18n, - style: TextStyle( - color: AppColors.of(context).text.withOpacity( - settings.notificationsEnabled ? 1.0 : .5), - fontWeight: FontWeight.w600, - fontSize: 16.0, - ), - ), - const SizedBox( - width: 5, - ), - BetaChip( - disabled: !settings.notificationsEnabled, - ), - ], - ), - onChanged: (value) => - settings.update(notificationsEnabled: value), - ), + child: MenuNotifications(settings: settings) ), ], ),