diff --git a/filcnaplo/lib/database/init.dart b/filcnaplo/lib/database/init.dart index 7c29324..fca6a84 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 "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 ba93892..76b9266 100644 --- a/filcnaplo/lib/helpers/notification_helper.dart +++ b/filcnaplo/lib/helpers/notification_helper.dart @@ -3,6 +3,7 @@ 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'; @@ -19,12 +20,36 @@ class NotificationsHelper { 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; + } @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 = await database.query.getSettings(database); @@ -36,8 +61,8 @@ class NotificationsHelper { kretaClient = KretaClient( user: userProvider, settings: settingsProvider, status: status); kretaClient.refreshLogin(); - gradeNotification(); - absenceNotification(); + if(settingsProvider.notificationsGradesEnabled) gradeNotification(); + if(settingsProvider.notificationsAbsencesEnabled) absenceNotification(); } } @@ -70,31 +95,31 @@ class NotificationsHelper { 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); - } + 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); + } } } } @@ -102,50 +127,72 @@ class NotificationsHelper { 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; } - List absences = absenceJson.map((e) => Absence.fromJson(e)).toList(); + // format api absences to correct format while preserving hasSeen value + List absences = absenceJson.map((e) { + Absence apiAbsence = Absence.fromJson(e); + Absence storedAbsence = storedAbsences.firstWhere( + (stored) => stored.id == apiAbsence.id, + orElse: () => apiAbsence); + apiAbsence.hasSeen = storedAbsence.hasSeen; + 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) { - 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); + if(!absence.hasSeen) { + absence.hasSeen = 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!); } } diff --git a/filcnaplo/lib/models/settings.dart b/filcnaplo/lib/models/settings.dart index 6673228..918a990 100644 --- a/filcnaplo/lib/models/settings.dart +++ b/filcnaplo/lib/models/settings.dart @@ -31,6 +31,8 @@ class SettingsProvider extends ChangeNotifier { bool _newsEnabled; String _seenNews; bool _notificationsEnabled; + bool _notificationsGradesEnabled; + bool _notificationsAbsencesEnabled; /* notificationsBitfield values: @@ -84,6 +86,8 @@ class SettingsProvider extends ChangeNotifier { required bool newsEnabled, required String seenNews, required bool notificationsEnabled, + required bool notificationsGradesEnabled, + required bool notificationsAbsencesEnabled, required int notificationsBitfield, required bool developerMode, required int notificationPollInterval, @@ -122,6 +126,8 @@ class SettingsProvider extends ChangeNotifier { _newsEnabled = newsEnabled, _seenNews = seenNews, _notificationsEnabled = notificationsEnabled, + _notificationsGradesEnabled = notificationsGradesEnabled, + _notificationsAbsencesEnabled = notificationsAbsencesEnabled, _notificationsBitfield = notificationsBitfield, _developerMode = developerMode, _notificationPollInterval = notificationPollInterval, @@ -178,6 +184,8 @@ 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, notificationsBitfield: map["notifications_bitfield"], notificationPollInterval: map["notification_poll_interval"], developerMode: map["developer_mode"] == 1, @@ -220,6 +228,8 @@ 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_bitfield": _notificationsBitfield, "developer_mode": _developerMode ? 1 : 0, "grade_color1": _gradeColors[0].value, @@ -273,6 +283,8 @@ class SettingsProvider extends ChangeNotifier { newsEnabled: true, seenNews: '', notificationsEnabled: true, + notificationsGradesEnabled: true, + notificationsAbsencesEnabled: true, notificationsBitfield: 255, developerMode: false, notificationPollInterval: 1, @@ -314,6 +326,8 @@ 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; int get notificationsBitfield => _notificationsBitfield; bool get developerMode => _developerMode; int get notificationPollInterval => _notificationPollInterval; @@ -357,6 +371,8 @@ class SettingsProvider extends ChangeNotifier { bool? newsEnabled, String? seenNewsId, bool? notificationsEnabled, + bool? notificationsGradesEnabled, + bool? notificationsAbsencesEnabled, int? notificationsBitfield, bool? developerMode, int? notificationPollInterval, @@ -408,6 +424,14 @@ class SettingsProvider extends ChangeNotifier { notificationsEnabled != _notificationsEnabled) { _notificationsEnabled = notificationsEnabled; } + if (notificationsGradesEnabled != null && + notificationsGradesEnabled != _notificationsGradesEnabled) { + _notificationsGradesEnabled = notificationsGradesEnabled; + } + if (notificationsAbsencesEnabled != null && + notificationsAbsencesEnabled != _notificationsAbsencesEnabled) { + _notificationsAbsencesEnabled = notificationsAbsencesEnabled; + } if (notificationsBitfield != null && notificationsBitfield != _notificationsBitfield) { _notificationsBitfield = notificationsBitfield; diff --git a/filcnaplo_kreta_api/lib/models/absence.dart b/filcnaplo_kreta_api/lib/models/absence.dart index d694c86..d869063 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 hasSeen; + @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.hasSeen = false, }); factory Absence.fromJson(Map json) { @@ -80,6 +89,7 @@ class Absence { lessonIndex: lessonIndex, group: json["OsztalyCsoport"] != null ? json["OsztalyCsoport"]["Uid"] : "", + hasSeen: false, json: json, ); } 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..358316a --- /dev/null +++ b/filcnaplo_mobile_ui/lib/screens/settings/notifications_screen.dart @@ -0,0 +1,122 @@ +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, nocolor: !settings.notificationsGradesEnabled,), + 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: 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), + ), + ), + ), + ], + ), + ) + ]), + )))); + } +} 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..3848402 --- /dev/null +++ b/filcnaplo_mobile_ui/lib/screens/settings/notifications_screen.i18n.dart @@ -0,0 +1,28 @@ +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" + + }, + "hu_hu": { + "notifications_screen": "Értesítések", + "grades": "Jegyek", + "absences": "Hiányzások" + }, + "de_de": { + "notifications_screen": "Mitteilung", + "grades": "Noten", + "absences": "Fehlen" + }, + }; + + 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) ), ], ),