diff --git a/refilc/lib/database/init.dart b/refilc/lib/database/init.dart index 80d0286..3691a8c 100644 --- a/refilc/lib/database/init.dart +++ b/refilc/lib/database/init.dart @@ -66,7 +66,12 @@ const userDataDB = DatabaseStruct("user_data", { // renamed teachers // non kreta data "renamed_teachers": String, // "subject_lesson_count": String, // non kreta data + // notifications and surprise grades // non kreta data "last_seen_grade": int, + "last_seen_surprisegrade": int, + "last_seen_absence": int, + "last_seen_message": int, + "last_seen_lesson": int, // goal planning // non kreta data "goal_plans": String, "goal_averages": String, @@ -129,7 +134,11 @@ Future initDB(DatabaseProvider database) async { // renamed teachers // non kreta data "renamed_teachers": "{}", // "subject_lesson_count": "{}", // non kreta data - "last_seen_grade": 0, + "last_seen_grade": DateTime.now().millisecondsSinceEpoch, + "last_seen_surprisegrade": 0, + "last_seen_absence": DateTime.now().millisecondsSinceEpoch, + "last_seen_message": DateTime.now().millisecondsSinceEpoch, + "last_seen_lesson": DateTime.now().millisecondsSinceEpoch, // goal planning // non kreta data "goal_plans": "{}", "goal_averages": "{}", @@ -207,3 +216,4 @@ Future migrateDB( print("INFO: Database migrated"); } } + diff --git a/refilc/lib/database/query.dart b/refilc/lib/database/query.dart index 4758ba2..aef1725 100644 --- a/refilc/lib/database/query.dart +++ b/refilc/lib/database/query.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:refilc/api/providers/database_provider.dart'; import 'package:refilc/models/linked_account.dart'; +import 'package:refilc/helpers/notification_helper.dart'; import 'package:refilc/models/self_note.dart'; import 'package:refilc/models/subject_lesson_count.dart'; import 'package:refilc/models/user.dart'; @@ -213,11 +214,11 @@ class UserDatabaseQuery { return lessonCount; } - Future lastSeenGrade({required String userId}) async { + Future lastSeen({required String userId, required LastSeenCategory category}) async { List userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]); if (userData.isEmpty) return DateTime(0); - int? lastSeenDate = userData.elementAt(0)["last_seen_grade"] as int?; + int? lastSeenDate = userData.elementAt(0)["last_seen_${category.name}"] as int?; if (lastSeenDate == null) return DateTime(0); DateTime lastSeen = DateTime.fromMillisecondsSinceEpoch(lastSeenDate); return lastSeen; diff --git a/refilc/lib/database/store.dart b/refilc/lib/database/store.dart index 18728b3..4b0f23f 100644 --- a/refilc/lib/database/store.dart +++ b/refilc/lib/database/store.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'package:refilc/models/linked_account.dart'; +import 'package:refilc/helpers/notification_helper.dart'; import 'package:refilc/models/self_note.dart'; import 'package:refilc/models/subject_lesson_count.dart'; import 'package:refilc_kreta_api/models/week.dart'; @@ -129,12 +130,13 @@ class UserDatabaseStore { where: "id = ?", whereArgs: [userId]); } - Future storeLastSeenGrade(DateTime date, - {required String userId}) async { + Future storeLastSeen(DateTime date, + {required String userId, required LastSeenCategory category}) async { int lastSeenDate = date.millisecondsSinceEpoch; - await db.update("user_data", {"last_seen_grade": lastSeenDate}, + await db.update("user_data", {"last_seen_${category.name}": lastSeenDate}, where: "id = ?", whereArgs: [userId]); } + // renamed things Future storeRenamedSubjects(Map subjects, diff --git a/refilc/lib/helpers/notification_helper.dart b/refilc/lib/helpers/notification_helper.dart index e21b4a3..fa1ed8a 100644 --- a/refilc/lib/helpers/notification_helper.dart +++ b/refilc/lib/helpers/notification_helper.dart @@ -1,8 +1,12 @@ +import 'package:flutter/foundation.dart'; import 'package:refilc/api/providers/database_provider.dart'; import 'package:refilc/api/providers/status_provider.dart'; import 'package:refilc/api/providers/user_provider.dart'; import 'package:refilc/models/settings.dart'; import 'package:refilc/helpers/notification_helper.i18n.dart'; +import 'package:refilc/models/user.dart'; +import 'package:refilc/utils/navigation_service.dart'; +import 'package:refilc/utils/service_locator.dart'; import 'package:refilc_kreta_api/client/api.dart'; import 'package:refilc_kreta_api/client/client.dart'; import 'package:refilc_kreta_api/models/absence.dart'; @@ -13,10 +17,19 @@ import 'package:refilc_kreta_api/providers/grade_provider.dart'; import 'package:refilc_kreta_api/providers/timetable_provider.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart' hide Message; -import 'package:i18n_extension/i18n_extension.dart'; +import 'package:i18n_extension/i18n_widget.dart'; import 'package:intl/intl.dart'; import 'package:refilc_kreta_api/models/message.dart'; +// if you want to add a new category, also add it to the DB or else the app will probably crash +enum LastSeenCategory { + grade, + surprisegrade, + absence, + message, + lesson +} // didn't know a better place for this + class NotificationsHelper { late DatabaseProvider database; late SettingsProvider settingsProvider; @@ -25,41 +38,19 @@ class NotificationsHelper { FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); - List combineLists( - List list1, - List list2, - K Function(T) keyExtractor, - ) { - Set uniqueKeys = {}; - 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); + String dayTitle = + DateFormat("EEEE", I18n.locale.languageCode).format(date); + dayTitle = dayTitle[0].toUpperCase() + + dayTitle.substring(1); // capitalize string + return dayTitle; } catch (e) { return "Unknown"; } } - + @pragma('vm:entry-point') void backgroundJob() async { // initialize providers @@ -69,540 +60,553 @@ class NotificationsHelper { userProvider = await database.query.getUsers(settingsProvider); if (userProvider.id != null && settingsProvider.notificationsEnabled) { - // refresh kreta login - final status = StatusProvider(); - kretaClient = KretaClient( - user: userProvider, settings: settingsProvider, status: status); - kretaClient.refreshLogin(); - if (settingsProvider.notificationsGradesEnabled) gradeNotification(); - if (settingsProvider.notificationsAbsencesEnabled) absenceNotification(); - if (settingsProvider.notificationsMessagesEnabled) messageNotification(); - if (settingsProvider.notificationsLessonsEnabled) lessonNotification(); + List users = userProvider.getUsers(); + + // Process notifications for each user asynchronously + await Future.forEach(users, (User user) async { + // Create a new instance of userProvider for each user + UserProvider userProviderForUser = await database.query.getUsers(settingsProvider); + userProviderForUser.setUser(user.id); + + // Refresh kreta login for current user + final status = StatusProvider(); + KretaClient kretaClientForUser = KretaClient( + user: userProviderForUser, settings: settingsProvider, status: status); + await kretaClientForUser.refreshLogin(); + + // Process notifications for current user + if (settingsProvider.notificationsGradesEnabled) await gradeNotification(userProviderForUser, kretaClientForUser); + if (settingsProvider.notificationsAbsencesEnabled) await absenceNotification(userProviderForUser, kretaClientForUser); + if (settingsProvider.notificationsMessagesEnabled) await messageNotification(userProviderForUser, kretaClientForUser); + if (settingsProvider.notificationsLessonsEnabled) await lessonNotification(userProviderForUser, kretaClientForUser); + }); } } - void gradeNotification() async { + Future gradeNotification(UserProvider currentuserProvider, KretaClient currentKretaClient) async { // fetch grades GradeProvider gradeProvider = GradeProvider( settings: settingsProvider, - user: userProvider, + user: currentuserProvider, database: database, - kreta: kretaClient); - gradeProvider.fetch(); - List grades = - await database.userQuery.getGrades(userId: userProvider.id ?? ""); - DateTime lastSeenGrade = - await database.userQuery.lastSeenGrade(userId: userProvider.id ?? ""); + kreta: currentKretaClient); + await gradeProvider.fetch(); + database.userQuery + .getGrades(userId: currentuserProvider.id!) + .then((grades) async { + DateTime lastSeenGrade = await database.userQuery.lastSeen( + userId: currentuserProvider.id!, category: LastSeenCategory.grade); - // loop through grades and see which hasn't been seen yet - for (Grade grade in grades) { - // 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 - // it worked in reverse, cuz someone added a * -1 to it, but it has been fixed now :D - // old code below - // if (grade.seenDate.isAfter(lastSeenGrade) && - // grade.date.difference(DateTime.now()).inDays * -1 < 7) { - // new code from here :P - if (grade.seenDate.isAfter(lastSeenGrade) && - grade.date.difference(DateTime.now()).inDays < 7) { - // send notificiation about new grade - AndroidNotificationDetails androidNotificationDetails = - AndroidNotificationDetails( - 'GRADES', - 'Jegyek', - channelDescription: 'Értesítés jegyek beírásakor', - importance: Importance.max, - priority: Priority.max, - color: settingsProvider.customAccentColor, - ticker: 'Jegyek', - ); - 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, + // loop through grades and see which hasn't been seen yet + for (Grade grade in grades) { + // 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.date.isAfter(lastSeenGrade) && + DateTime.now().difference(grade.date).inDays < 7) { + // send notificiation about new grade + AndroidNotificationDetails androidNotificationDetails = + AndroidNotificationDetails( + 'GRADES', + 'Jegyek', + channelDescription: 'Értesítés jegyek beírásakor', + importance: Importance.max, + priority: Priority.max, + color: settingsProvider.customAccentColor, + ticker: 'Jegyek', ); + NotificationDetails notificationDetails = + NotificationDetails(android: androidNotificationDetails); + if (currentuserProvider.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, + payload: "grades" + ); + } else { + // multiple users are added, also display student name + await flutterLocalNotificationsPlugin.show( + grade.id.hashCode, + "title_grade".i18n, + "body_grade_multiuser".i18n.fill( + [ + currentuserProvider.displayName!, + grade.value.value.toString(), + grade.subject.isRenamed && + settingsProvider.renamedSubjectsEnabled + ? grade.subject.renamedTo! + : grade.subject.name + ], + ), + notificationDetails, + payload: "grades" + ); + } } } } - } - // set grade seen status - gradeProvider.seenAll(); + // set grade seen status + database.userStore.storeLastSeen(DateTime.now(), userId: currentuserProvider.id!, category: LastSeenCategory.grade); + }); } - void absenceNotification() async { + Future absenceNotification(UserProvider currentuserProvider, KretaClient currentKretaClient) async { // get absences from api - List? absenceJson = await kretaClient - .getAPI(KretaAPI.absences(userProvider.instituteCode ?? "")); - List storedAbsences = - await database.userQuery.getAbsences(userId: userProvider.id!); + List? absenceJson = await currentKretaClient + .getAPI(KretaAPI.absences(currentuserProvider.instituteCode ?? "")); 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); - AndroidNotificationDetails androidNotificationDetails = - AndroidNotificationDetails( - 'ABSENCES', - 'Hiányzások', - channelDescription: 'Értesítés hiányzások beírásakor', - importance: Importance.max, - priority: Priority.max, - color: settingsProvider.customAccentColor, - ticker: 'Hiányzások', + DateTime lastSeenAbsence = await database.userQuery + .lastSeen(userId: currentuserProvider.id!, category: LastSeenCategory.absence); + // format api absences + List absences = + absenceJson.map((e) => Absence.fromJson(e)).toList(); + for (Absence absence in absences) { + if (absence.date.isAfter(lastSeenAbsence)) { + AndroidNotificationDetails androidNotificationDetails = + AndroidNotificationDetails( + 'ABSENCES', + 'Hiányzások', + channelDescription: 'Értesítés hiányzások beírásakor', + importance: Importance.max, + priority: Priority.max, + color: settingsProvider.customAccentColor, + ticker: 'Hiányzások', + ); + NotificationDetails notificationDetails = + NotificationDetails(android: androidNotificationDetails); + if (currentuserProvider.getUsers().length == 1) { + await flutterLocalNotificationsPlugin.show( + absence.id.hashCode, + "title_absence" + .i18n, // https://discord.com/channels/1111649116020285532/1153273625206591528 + "body_absence".i18n.fill( + [ + DateFormat("yyyy-MM-dd").format(absence.date), + absence.subject.isRenamed && + settingsProvider.renamedSubjectsEnabled + ? absence.subject.renamedTo! + : absence.subject.name + ], + ), + notificationDetails, + payload: "absences" + ); + } else { + await flutterLocalNotificationsPlugin.show( + absence.id.hashCode, + "title_absence" + .i18n, // https://discord.com/channels/1111649116020285532/1153273625206591528 + "body_absence_multiuser".i18n.fill( + [ + currentuserProvider.displayName!, + DateFormat("yyyy-MM-dd").format(absence.date), + absence.subject.isRenamed && + settingsProvider.renamedSubjectsEnabled + ? absence.subject.renamedTo! + : absence.subject.name + ], + ), + notificationDetails, + payload: "absences" ); - NotificationDetails notificationDetails = - NotificationDetails(android: androidNotificationDetails); - if (userProvider.getUsers().length == 1) { - await flutterLocalNotificationsPlugin.show( - absence.id.hashCode, - "title_absence" - .i18n, // https://discord.com/channels/1111649116020285532/1153273625206591528 - "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, // https://discord.com/channels/1111649116020285532/1153273625206591528 - "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!); + await database.userStore.storeLastSeen(DateTime.now(), + userId: currentuserProvider.id!, category: LastSeenCategory.absence); } - 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)); + Future messageNotification(UserProvider currentuserProvider, KretaClient currentKretaClient) async { + // get messages from api + List? messageJson = + await currentKretaClient.getAPI(KretaAPI.messages("beerkezett")); + if (messageJson == null) { + return; } - }(); - })); - - 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); - AndroidNotificationDetails androidNotificationDetails = - AndroidNotificationDetails( - 'MESSAGES', - 'Üzenetek', - channelDescription: 'Értesítés kapott üzenetekkor', - importance: Importance.max, - priority: Priority.max, - color: settingsProvider.customAccentColor, - ticker: 'Üzenetek', - ); - 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); - 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: settingsProvider.customAccentColor, - ticker: 'Órák', - ); - 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; - } - } + // format api messages to correct format + // Parse messages + List messages = []; + await Future.wait(List.generate(messageJson.length, (index) { + return () async { + Map message = messageJson.cast()[index]; + Map? innerMessageJson = await currentKretaClient + .getAPI(KretaAPI.message(message["azonosito"].toString())); + await Future.delayed(const Duration(seconds: 1)); + if (innerMessageJson != null) { + messages.add(Message.fromJson(innerMessageJson, + forceType: MessageType.inbox)); } - } 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; - } + }(); + })); + + DateTime lastSeenMessage = await database.userQuery.lastSeen( + userId: currentuserProvider.id!, category: LastSeenCategory.message); + + for (Message message in messages) { + if (message.date.isAfter(lastSeenMessage)) { + AndroidNotificationDetails androidNotificationDetails = + AndroidNotificationDetails( + 'MESSAGES', + 'Üzenetek', + channelDescription: 'Értesítés kapott üzenetekkor', + importance: Importance.max, + priority: Priority.max, + color: settingsProvider.customAccentColor, + ticker: 'Üzenetek', + ); + NotificationDetails notificationDetails = + NotificationDetails(android: androidNotificationDetails); + if (currentuserProvider.getUsers().length == 1) { + await flutterLocalNotificationsPlugin.show( + message.id.hashCode, + message.author, + message.content.replaceAll(RegExp(r'<[^>]*>'), ''), + notificationDetails, + payload: "messages", + ); + } else { + await flutterLocalNotificationsPlugin.show( + message.id.hashCode, + "(${currentuserProvider.displayName!}) ${message.author}", + message.content.replaceAll(RegExp(r'<[^>]*>'), ''), + notificationDetails, + payload: "messages", + ); + } + } + } + await database.userStore.storeLastSeen(DateTime.now(), + userId: currentuserProvider.id!, category: LastSeenCategory.message); + } + + Future lessonNotification(UserProvider currentuserProvider, KretaClient currentKretaClient) async { + // get lessons from api + TimetableProvider timetableProvider = TimetableProvider( + user: currentuserProvider, database: database, kreta: currentKretaClient); + await timetableProvider.restoreUser(); + await timetableProvider.fetch(week: Week.current()); + List apilessons = + timetableProvider.getWeek(Week.current()) ?? []; + + DateTime lastSeenLesson = await database.userQuery.lastSeen( + userId: currentuserProvider.id!, category: LastSeenCategory.lesson); + Lesson? latestLesson; + + for (Lesson lesson in apilessons) { + if((lesson.status?.name != "Elmaradt" || lesson.substituteTeacher?.name != "") && lesson.date.isAfter(latestLesson?.start ?? DateTime(1970))) { + latestLesson = lesson; + } + if (lesson.date.isAfter(lastSeenLesson)) { + 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: settingsProvider.customAccentColor, + ticker: 'Órák', + ); + NotificationDetails notificationDetails = + NotificationDetails(android: androidNotificationDetails); + if (currentuserProvider.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, + payload: "timetable" + ); + 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, + payload: "timetable" + ); + break; + } + default: + { + await flutterLocalNotificationsPlugin.show( + lesson.id.hashCode, + "title_lesson".i18n, + "body_lesson_canceled".i18n.fill( + [ + lesson.lessonIndex, + lesson.name, + dayTitle(lesson.date) + ], + ), + notificationDetails, + payload: "timetable" + ); + break; + } + } + } else if (lesson.substituteTeacher?.name != "" && lesson.substituteTeacher != null) { + 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, + payload: "timetable", + ); + 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, + payload: "timetable", + ); + 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, + payload: "timetable", + ); + 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; - } + } else { + if (lesson.status?.name == "Elmaradt") { + switch (I18n.localeStr) { + case "en_en": + { + await flutterLocalNotificationsPlugin.show( + lesson.id.hashCode, + "title_lesson".i18n, + "body_lesson_canceled_multiuser".i18n.fill( + [ + currentuserProvider.displayName!, + lesson.lessonIndex, + lesson.name, + dayTitle(lesson.date) + ], + ), + notificationDetails, + payload: "timetable", + ); + break; + } + case "hu_hu": + { + await flutterLocalNotificationsPlugin.show( + lesson.id.hashCode, + "title_lesson".i18n, + "body_lesson_canceled_multiuser".i18n.fill( + [ + currentuserProvider.displayName!, + dayTitle(lesson.date), + lesson.lessonIndex, + lesson.name + ], + ), + notificationDetails, + payload: "timetable", + ); + break; + } + default: + { + await flutterLocalNotificationsPlugin.show( + lesson.id.hashCode, + "title_lesson".i18n, + "body_lesson_canceled_multiuser".i18n.fill( + [ + currentuserProvider.displayName!, + lesson.lessonIndex, + lesson.name, + dayTitle(lesson.date) + ], + ), + notificationDetails, + payload: "timetable", + ); + break; + } + } + } else if (lesson.substituteTeacher?.name != "") { + switch (I18n.localeStr) { + case "en_en": + { + await flutterLocalNotificationsPlugin.show( + lesson.id.hashCode, + "title_lesson".i18n, + "body_lesson_substituted_multiuser".i18n.fill( + [ + currentuserProvider.displayName!, + lesson.lessonIndex, + lesson.name, + dayTitle(lesson.date), + lesson.substituteTeacher!.isRenamed + ? lesson.substituteTeacher!.renamedTo! + : lesson.substituteTeacher!.name + ], + ), + notificationDetails, + payload: "timetable", + ); + break; + } + case "hu_hu": + { + await flutterLocalNotificationsPlugin.show( + lesson.id.hashCode, + "title_lesson".i18n, + "body_lesson_substituted_multiuser".i18n.fill( + [ + currentuserProvider.displayName!, + dayTitle(lesson.date), + lesson.lessonIndex, + lesson.name, + lesson.substituteTeacher!.isRenamed + ? lesson.substituteTeacher!.renamedTo! + : lesson.substituteTeacher!.name + ], + ), + notificationDetails, + payload: "timetable", + ); + break; + } + default: + { + await flutterLocalNotificationsPlugin.show( + lesson.id.hashCode, + "title_lesson".i18n, + "body_lesson_substituted_multiuser".i18n.fill( + [ + currentuserProvider.displayName!, + lesson.lessonIndex, + lesson.name, + dayTitle(lesson.date), + lesson.substituteTeacher!.isRenamed + ? lesson.substituteTeacher!.renamedTo! + : lesson.substituteTeacher!.name + ], + ), + notificationDetails, + payload: "timetable", + ); + break; + } + } } } } } + // lesson.date does not contain time, only the date + await database.userStore.storeLastSeen(latestLesson?.start ?? DateTime.now(), + userId: currentuserProvider.id!, category: LastSeenCategory.lesson); + } + + // Called when the user taps a notification + void onDidReceiveNotificationResponse(NotificationResponse notificationResponse) async { + final String? payload = notificationResponse.payload; + if (notificationResponse.payload != null) { + debugPrint('notification payload: $payload'); + } + switch(payload) { + case "timetable": + locator().navigateTo("timetable"); + break; + case "grades": + locator().navigateTo("grades"); + break; + case "messages": + locator().navigateTo("messages"); + break; + case "absences": + locator().navigateTo("absences"); + break; + case "settings": + locator().navigateTo("settings"); + break; + default: + break; + } + } + + // Set all notification categories to seen + Future setAllCategoriesSeen(UserProvider userProvider) async { + if(userProvider.id != null) { + for(LastSeenCategory category in LastSeenCategory.values) { + await database.userStore.storeLastSeen(DateTime.now(), userId: userProvider.id!, category: category); + } + } } - // 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!); } - } -} diff --git a/refilc/lib/helpers/notification_helper.i18n.dart b/refilc/lib/helpers/notification_helper.i18n.dart index 37e6ff0..49db9f7 100644 --- a/refilc/lib/helpers/notification_helper.i18n.dart +++ b/refilc/lib/helpers/notification_helper.i18n.dart @@ -1,4 +1,5 @@ import 'package:i18n_extension/i18n_extension.dart'; +import 'package:refilc/api/providers/database_provider.dart'; extension Localization on String { static final _t = Translations.byLocale("hu_hu") + @@ -43,8 +44,19 @@ extension Localization on String { "body_lesson_substituted_multiuser": "(%s) Lektion Nr. %s (%s) wird am %s durch %s ersetzt" }, }; + String get i18n { + // very hacky way to get app language in notifications + // i18n does not like being in background functions (it cannot retrieve locale sometimes) + final DatabaseProvider databaseProvider = DatabaseProvider(); + databaseProvider.init().then((value) { + databaseProvider.query.getSettings(databaseProvider).then((settings) { + return localize(this, _t, locale: "${settings.language}_${settings.language.toUpperCase()}"); + }); + }); - String get i18n => localize(this, _t); + + return 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/refilc/lib/main.dart b/refilc/lib/main.dart index 91b569f..2d341e9 100644 --- a/refilc/lib/main.dart +++ b/refilc/lib/main.dart @@ -10,6 +10,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:refilc/app.dart'; import 'package:flutter/services.dart'; +import 'package:refilc/utils/navigation_service.dart'; +import 'package:refilc/utils/service_locator.dart'; import 'package:refilc_mobile_ui/screens/error_screen.dart'; import 'package:refilc_mobile_ui/screens/error_report_screen.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; @@ -23,12 +25,20 @@ void main() async { // ignore: deprecated_member_use binding.renderView.automaticSystemUiAdjustment = false; SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); + // navigation + setupLocator(); + // Startup Startup startup = Startup(); await startup.start(); // Custom error page ErrorWidget.builder = errorBuilder; + + // initialize stripe key + stripe.Stripe.publishableKey = + 'pk_test_51Oo7iUBS0FxsTGxKjGZSQqzDKWHY5ZFYM9XeI0qSdIh2w8jWy6GhHlYpT7GLTzgpl1xhE5YP4BXpA4gMZqPmgMId00cGFYFzbh'; + BackgroundFetch.registerHeadlessTask(backgroundHeadlessTask); @@ -53,6 +63,9 @@ class Startup { settings = await database.query.getSettings(database); user = await database.query.getUsers(settings); + // Set all notification categories to seen to avoid having notifications that the user has already seen in the app + NotificationsHelper().setAllCategoriesSeen(user); + late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin; // Notifications setup if (!kIsWeb) { @@ -113,6 +126,7 @@ class Startup { // Initialize notifications await flutterLocalNotificationsPlugin.initialize( initializationSettings, + onDidReceiveNotificationResponse: NotificationsHelper().onDidReceiveNotificationResponse, ); } diff --git a/refilc/lib/utils/navigation_service.dart b/refilc/lib/utils/navigation_service.dart new file mode 100644 index 0000000..ee8a479 --- /dev/null +++ b/refilc/lib/utils/navigation_service.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; + +class NavigationService { + final GlobalKey navigatorKey = GlobalKey(); + + Future navigateTo(String routeName) { + return navigatorKey.currentState!.pushNamed(routeName); + } +} \ No newline at end of file diff --git a/refilc/lib/utils/service_locator.dart b/refilc/lib/utils/service_locator.dart new file mode 100644 index 0000000..0ca94b7 --- /dev/null +++ b/refilc/lib/utils/service_locator.dart @@ -0,0 +1,8 @@ +import 'package:get_it/get_it.dart'; +import 'package:refilc/utils/navigation_service.dart'; + +GetIt locator = GetIt.instance; + +void setupLocator() { + locator.registerLazySingleton(() => NavigationService()); +} \ No newline at end of file diff --git a/refilc/pubspec.yaml b/refilc/pubspec.yaml index 842ea9f..fbc9620 100644 --- a/refilc/pubspec.yaml +++ b/refilc/pubspec.yaml @@ -77,6 +77,8 @@ dependencies: extension_google_sign_in_as_googleapis_auth: ^2.0.12 maps_launcher: ^2.2.0 google_fonts: ^6.1.0 + flutter_stripe: ^10.0.0 + get_it: ^7.6.7 dev_dependencies: flutter_lints: ^3.0.1 @@ -166,6 +168,7 @@ flutter: weight: 500 + flutter_launcher_icons: image_path: assets/icons/ic_android.png android: true diff --git a/refilc_kreta_api/lib/models/absence.dart b/refilc_kreta_api/lib/models/absence.dart index 232f671..5baad76 100644 --- a/refilc_kreta_api/lib/models/absence.dart +++ b/refilc_kreta_api/lib/models/absence.dart @@ -50,6 +50,7 @@ class Absence { DateTime lessonStart; DateTime lessonEnd; int? lessonIndex; + bool isSeen = json["isSeen"] ?? false; if (json["Ora"] != null) { lessonStart = json["Ora"]["KezdoDatum"] != null ? DateTime.parse(json["Ora"]["KezdoDatum"]).toLocal() @@ -62,7 +63,6 @@ class Absence { lessonStart = DateTime(0); lessonEnd = DateTime(0); } - return Absence( id: json["Uid"], date: json["Datum"] != null @@ -89,7 +89,7 @@ class Absence { lessonIndex: lessonIndex, group: json["OsztalyCsoport"] != null ? json["OsztalyCsoport"]["Uid"] : "", - isSeen: false, + isSeen: json["isSeen"] ?? false, json: json, ); } diff --git a/refilc_kreta_api/lib/providers/grade_provider.dart b/refilc_kreta_api/lib/providers/grade_provider.dart index c3918ba..3ab9701 100644 --- a/refilc_kreta_api/lib/providers/grade_provider.dart +++ b/refilc_kreta_api/lib/providers/grade_provider.dart @@ -1,5 +1,6 @@ import 'package:refilc/api/providers/user_provider.dart'; import 'package:refilc/api/providers/database_provider.dart'; +import 'package:refilc/helpers/notification_helper.dart'; import 'package:refilc/models/settings.dart'; import 'package:refilc/models/user.dart'; import 'package:refilc_kreta_api/client/api.dart'; @@ -49,7 +50,7 @@ class GradeProvider with ChangeNotifier { String? userId = _user.id; if (userId != null) { final userStore = _database.userStore; - userStore.storeLastSeenGrade(DateTime.now(), userId: userId); + userStore.storeLastSeen(DateTime.now(), userId: userId, category: LastSeenCategory.surprisegrade); _lastSeen = DateTime.now(); } } @@ -58,7 +59,7 @@ class GradeProvider with ChangeNotifier { String? userId = _user.id; if (userId != null) { final userStore = _database.userStore; - userStore.storeLastSeenGrade(DateTime(1969), userId: userId); + userStore.storeLastSeen(DateTime(1969), userId: userId, category: LastSeenCategory.surprisegrade); _lastSeen = DateTime(1969); } } @@ -74,7 +75,7 @@ class GradeProvider with ChangeNotifier { await convertBySettings(); _groupAvg = await userQuery.getGroupAverages(userId: userId); notifyListeners(); - DateTime lastSeenDB = await userQuery.lastSeenGrade(userId: userId); + DateTime lastSeenDB = await userQuery.lastSeen(userId: userId, category: LastSeenCategory.surprisegrade); if (lastSeenDB.millisecondsSinceEpoch == 0 || lastSeenDB.year == 0 || !_settings.gradeOpeningFun) { diff --git a/refilc_mobile_ui/lib/screens/navigation/navigation_screen.dart b/refilc_mobile_ui/lib/screens/navigation/navigation_screen.dart index 820b87f..b12c5a7 100644 --- a/refilc_mobile_ui/lib/screens/navigation/navigation_screen.dart +++ b/refilc_mobile_ui/lib/screens/navigation/navigation_screen.dart @@ -5,6 +5,8 @@ import 'package:refilc/helpers/quick_actions.dart'; import 'package:refilc/icons/filc_icons.dart'; import 'package:refilc/models/settings.dart'; import 'package:refilc/theme/observer.dart'; +import 'package:refilc/utils/navigation_service.dart'; +import 'package:refilc/utils/service_locator.dart'; import 'package:refilc_kreta_api/client/client.dart'; import 'package:refilc_kreta_api/providers/grade_provider.dart'; import 'package:refilc_mobile_ui/common/system_chrome.dart'; @@ -45,7 +47,7 @@ class NavigationScreenState extends State with WidgetsBindingObserver { late NavigationRoute selected; List initializers = []; - final _navigatorState = GlobalKey(); + final _navigatorState = locator().navigatorKey; late SettingsProvider settings; late NewsProvider newsProvider; diff --git a/refilc_mobile_ui/lib/screens/settings/notifications_screen.dart b/refilc_mobile_ui/lib/screens/settings/notifications_screen.dart index 6066691..5aeae16 100644 --- a/refilc_mobile_ui/lib/screens/settings/notifications_screen.dart +++ b/refilc_mobile_ui/lib/screens/settings/notifications_screen.dart @@ -1,12 +1,17 @@ +import 'package:flutter/foundation.dart'; +import 'package:refilc/api/providers/database_provider.dart'; +import 'package:refilc/api/providers/user_provider.dart'; +import 'package:refilc/helpers/notification_helper.dart'; import 'package:refilc/models/settings.dart'; +import 'package:refilc/models/user.dart'; import 'package:refilc/theme/colors/colors.dart'; import 'package:refilc_mobile_ui/common/beta_chip.dart'; -import 'package:refilc_mobile_ui/common/panel/panel.dart'; import 'package:refilc_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 'package:refilc_mobile_ui/common/splitted_panel/splitted_panel.dart'; import 'notifications_screen.i18n.dart'; class MenuNotifications extends StatelessWidget { @@ -53,6 +58,19 @@ class MenuNotifications extends StatelessWidget { class NotificationsScreen extends StatelessWidget { const NotificationsScreen({super.key}); + // Set all notification categories as seen to avoid spamming the user with notifications when they turn on notifications + void setAll(BuildContext context, DateTime date) { + DatabaseProvider database = + Provider.of(context, listen: false); + User? user = Provider.of(context, listen: false).user; + if (user != null) { + for (LastSeenCategory category in LastSeenCategory.values) { + database.userStore + .storeLastSeen(date, userId: user.id, category: category); + } + } + } + @override Widget build(BuildContext context) { SettingsProvider settings = Provider.of(context); @@ -69,151 +87,188 @@ class NotificationsScreen extends StatelessWidget { body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0), - child: Panel( - child: Column( - children: [ - Material( - type: MaterialType.transparency, - child: SwitchListTile( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0)), - value: settings.notificationsGradesEnabled, - onChanged: (v) => - settings.update(notificationsGradesEnabled: v), - title: Row( - children: [ - Icon( - FeatherIcons.bookmark, - color: settings.notificationsGradesEnabled - ? Theme.of(context).colorScheme.secondary - : AppColors.of(context).text.withOpacity(.25), - ), - 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, - ), - ), - ), - ), - ], + child: Column( + children: [ + SplittedPanel( + padding: const EdgeInsets.only(top: 8.0), + cardPadding: const EdgeInsets.all(4.0), + isSeparated: true, + children: [ + PanelButton( + padding: const EdgeInsets.only(left: 14.0, right: 6.0), + onPressed: () { + settings.update( + notificationsGradesEnabled: + !settings.notificationsGradesEnabled); + setAll(context, DateTime.now()); + }, + title: Text( + "grades".i18n, + style: TextStyle( + color: AppColors.of(context).text.withOpacity( + settings.notificationsGradesEnabled ? .95 : .25), + ), + ), + leading: Icon( + FeatherIcons.bookmark, + size: 22.0, + color: AppColors.of(context).text.withOpacity( + settings.notificationsGradesEnabled ? .95 : .25), + ), + trailing: Switch( + onChanged: (v) => + settings.update(notificationsGradesEnabled: v), + value: settings.notificationsGradesEnabled, + activeColor: Theme.of(context).colorScheme.secondary, + ), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12.0), + bottom: Radius.circular(12.0), ), ), - ), - Material( - type: MaterialType.transparency, - child: SwitchListTile( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0)), - value: settings.notificationsAbsencesEnabled, - onChanged: (v) => - settings.update(notificationsAbsencesEnabled: v), - title: Row( - children: [ - Icon( - FeatherIcons.clock, - color: settings.notificationsAbsencesEnabled - ? Theme.of(context).colorScheme.secondary - : AppColors.of(context).text.withOpacity(.25), - ), - const SizedBox(width: 14.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, - ), - ), - ), - ), - ], + ], + ), + SplittedPanel( + padding: const EdgeInsets.only(top: 8.0), + cardPadding: const EdgeInsets.all(4.0), + isSeparated: true, + children: [ + PanelButton( + padding: const EdgeInsets.only(left: 14.0, right: 6.0), + onPressed: () { + settings.update( + notificationsAbsencesEnabled: + !settings.notificationsAbsencesEnabled); + setAll(context, DateTime.now()); + }, + title: Text( + "absences".i18n, + style: TextStyle( + color: AppColors.of(context).text.withOpacity( + settings.notificationsAbsencesEnabled ? .95 : .25), + ), + ), + leading: Icon( + FeatherIcons.clock, + size: 22.0, + color: AppColors.of(context).text.withOpacity( + settings.notificationsAbsencesEnabled ? .95 : .25), + ), + trailing: Switch( + onChanged: (v) => + settings.update(notificationsAbsencesEnabled: v), + value: settings.notificationsAbsencesEnabled, + activeColor: Theme.of(context).colorScheme.secondary, + ), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12.0), + bottom: Radius.circular(12.0), ), ), - ), - Material( - type: MaterialType.transparency, - child: SwitchListTile( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0)), - value: settings.notificationsMessagesEnabled, - onChanged: (v) => - settings.update(notificationsMessagesEnabled: v), - title: Row( - children: [ - Icon( - FeatherIcons.messageSquare, - color: settings.notificationsMessagesEnabled - ? Theme.of(context).colorScheme.secondary - : AppColors.of(context).text.withOpacity(.25), - ), - const SizedBox(width: 14.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, - ), - ), - ), - ), - ], + ], + ), + SplittedPanel( + padding: const EdgeInsets.only(top: 8.0), + cardPadding: const EdgeInsets.all(4.0), + isSeparated: true, + children: [ + PanelButton( + padding: const EdgeInsets.only(left: 14.0, right: 6.0), + onPressed: () { + settings.update( + notificationsMessagesEnabled: + !settings.notificationsMessagesEnabled); + setAll(context, DateTime.now()); + }, + title: Text( + "messages".i18n, + style: TextStyle( + color: AppColors.of(context).text.withOpacity( + settings.notificationsMessagesEnabled ? .95 : .25), + ), + ), + leading: Icon( + FeatherIcons.messageSquare, + size: 22.0, + color: AppColors.of(context).text.withOpacity( + settings.notificationsMessagesEnabled ? .95 : .25), + ), + trailing: Switch( + onChanged: (v) => + settings.update(notificationsMessagesEnabled: v), + value: settings.notificationsMessagesEnabled, + activeColor: Theme.of(context).colorScheme.secondary, + ), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12.0), + bottom: Radius.circular(12.0), ), ), - ), - Material( - type: MaterialType.transparency, - child: SwitchListTile( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0)), - value: settings.notificationsLessonsEnabled, - onChanged: (v) => - settings.update(notificationsLessonsEnabled: v), - title: Row( - children: [ - Icon( - FeatherIcons.calendar, - color: settings.notificationsLessonsEnabled - ? Theme.of(context).colorScheme.secondary - : AppColors.of(context).text.withOpacity(.25), - ), - const SizedBox(width: 14.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, - ), - ), - ), - ), - ], + ], + ), + SplittedPanel( + padding: const EdgeInsets.only(top: 8.0), + cardPadding: const EdgeInsets.all(4.0), + isSeparated: true, + children: [ + PanelButton( + padding: const EdgeInsets.only(left: 14.0, right: 6.0), + onPressed: () { + settings.update( + notificationsLessonsEnabled: + !settings.notificationsLessonsEnabled); + setAll(context, DateTime.now()); + }, + title: Text( + "lessons".i18n, + style: TextStyle( + color: AppColors.of(context).text.withOpacity( + settings.notificationsLessonsEnabled ? .95 : .25), + ), + ), + leading: Icon( + FeatherIcons.bookmark, + size: 22.0, + color: AppColors.of(context).text.withOpacity( + settings.notificationsLessonsEnabled ? .95 : .25), + ), + trailing: Switch( + onChanged: (v) => + settings.update(notificationsLessonsEnabled: v), + value: settings.notificationsLessonsEnabled, + activeColor: Theme.of(context).colorScheme.secondary, + ), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12.0), + bottom: Radius.circular(12.0), ), ), - ), - ], - ), + ], + ), + // only used for debugging, pressing **will** cause notification spam + kDebugMode + ? SplittedPanel( + padding: const EdgeInsets.only(top: 9.0), + cardPadding: const EdgeInsets.all(4.0), + isSeparated: true, + children: [ + PanelButton( + onPressed: () => setAll( + context, DateTime(1970, 1, 1, 0, 0, 0, 0, 0)), + title: Text("set_all_as_unseen".i18n), + leading: Icon( + FeatherIcons.mail, + size: 22.0, + color: AppColors.of(context).text.withOpacity(0.95), + ), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12.0), + bottom: Radius.circular(4.0)), + ) + ], + ) + : const SizedBox.shrink(), + ], ), ), ), diff --git a/refilc_mobile_ui/lib/screens/settings/notifications_screen.i18n.dart b/refilc_mobile_ui/lib/screens/settings/notifications_screen.i18n.dart index c9d0087..310779f 100644 --- a/refilc_mobile_ui/lib/screens/settings/notifications_screen.i18n.dart +++ b/refilc_mobile_ui/lib/screens/settings/notifications_screen.i18n.dart @@ -8,7 +8,8 @@ extension SettingsLocalization on String { "grades": "Grades", "absences": "Absences", "messages": "Messages", - "lessons": "Lessons" + "lessons": "Lessons", + "set_all_as_unseen": "Set all as unseen", }, "hu_hu": { @@ -16,14 +17,16 @@ extension SettingsLocalization on String { "grades": "Jegyek", "absences": "Hiányzások", "messages": "Üzenetek", - "lessons": "Órák" + "lessons": "Órák", + "set_all_as_unseen": "Összes kategória beállítása olvasatlannak", }, "de_de": { "notifications_screen": "Mitteilung", "grades": "Noten", "absences": "Fehlen", "messages": "Nachrichten", - "lessons": "Unterricht" + "lessons": "Unterricht", + "set_all_as_unseen": "Alle als ungelesen einstellen", }, }; diff --git a/refilc_mobile_ui/pubspec.yaml b/refilc_mobile_ui/pubspec.yaml index dfc0a70..2fd214e 100644 --- a/refilc_mobile_ui/pubspec.yaml +++ b/refilc_mobile_ui/pubspec.yaml @@ -66,9 +66,10 @@ dependencies: google_fonts: ^6.1.0 flutter_any_logo: ^1.1.1 custom_sliding_segmented_control: ^1.8.1 - + get_it: ^7.6.7 + dev_dependencies: flutter_lints: ^3.0.1 flutter: - uses-material-design: true \ No newline at end of file + uses-material-design: true