add notification for absences

This commit is contained in:
hihihaha 2023-08-29 13:45:52 +02:00
parent ec04a0ff81
commit bc4e4e9b5a
7 changed files with 296 additions and 98 deletions

View File

@ -20,7 +20,7 @@ const settingsDB = DatabaseStruct("settings", {
"grade_color4": int, "grade_color5": int, // grade colors "grade_color4": int, "grade_color5": int, // grade colors
"vibration_strength": int, "ab_weeks": int, "swap_ab_weeks": int, "vibration_strength": int, "ab_weeks": int, "swap_ab_weeks": int,
"notifications": int, "notifications_bitfield": 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, "x_filc_id": String, "graph_class_avg": int, "presentation_mode": int,
"bell_delay": int, "bell_delay_enabled": int, "bell_delay": int, "bell_delay_enabled": int,
"grade_opening_fun": int, "icon_pack": String, "premium_scopes": String, "grade_opening_fun": int, "icon_pack": String, "premium_scopes": String,

View File

@ -3,6 +3,7 @@ import 'dart:ui';
import 'package:filcnaplo/api/providers/database_provider.dart'; import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/api/providers/status_provider.dart'; import 'package:filcnaplo/api/providers/status_provider.dart';
import 'package:filcnaplo/api/providers/user_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/models/settings.dart';
import 'package:filcnaplo/helpers/notification_helper.i18n.dart'; import 'package:filcnaplo/helpers/notification_helper.i18n.dart';
import 'package:filcnaplo_kreta_api/client/api.dart'; import 'package:filcnaplo_kreta_api/client/api.dart';
@ -19,12 +20,36 @@ class NotificationsHelper {
late UserProvider userProvider; late UserProvider userProvider;
late KretaClient kretaClient; late KretaClient kretaClient;
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
List<T> combineLists<T, K>(List<T> list1, List<T> list2, K Function(T) keyExtractor,) {
Set<K> uniqueKeys = Set<K>();
List<T> 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') @pragma('vm:entry-point')
void backgroundJob() async { void backgroundJob() async {
// initialize providers // initialize providers
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin(); FlutterLocalNotificationsPlugin();
DatabaseProvider database = DatabaseProvider(); database = DatabaseProvider();
var db = await initDB(database);
await database.init(); await database.init();
settingsProvider = settingsProvider =
await database.query.getSettings(database); await database.query.getSettings(database);
@ -36,8 +61,8 @@ class NotificationsHelper {
kretaClient = KretaClient( kretaClient = KretaClient(
user: userProvider, settings: settingsProvider, status: status); user: userProvider, settings: settingsProvider, status: status);
kretaClient.refreshLogin(); kretaClient.refreshLogin();
gradeNotification(); if(settingsProvider.notificationsGradesEnabled) gradeNotification();
absenceNotification(); if(settingsProvider.notificationsAbsencesEnabled) absenceNotification();
} }
} }
@ -70,31 +95,31 @@ class NotificationsHelper {
ticker: 'Jegyek'); ticker: 'Jegyek');
const NotificationDetails notificationDetails = NotificationDetails(android: androidNotificationDetails); const NotificationDetails notificationDetails = NotificationDetails(android: androidNotificationDetails);
if(userProvider.getUsers().length == 1) { if(userProvider.getUsers().length == 1) {
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
grade.id.hashCode, grade.id.hashCode,
"title_grade".i18n, "title_grade".i18n,
"body_grade".i18n.fill([ "body_grade".i18n.fill([
grade.value.value.toString(), grade.value.value.toString(),
grade.subject.isRenamed && grade.subject.isRenamed &&
settingsProvider.renamedSubjectsEnabled settingsProvider.renamedSubjectsEnabled
? grade.subject.renamedTo! ? grade.subject.renamedTo!
: grade.subject.name : grade.subject.name
]), ]),
notificationDetails); notificationDetails);
} else { // multiple users are added, also display student name } else { // multiple users are added, also display student name
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
grade.id.hashCode, grade.id.hashCode,
"title_grade".i18n, "title_grade".i18n,
"body_grade_multiuser".i18n.fill([ "body_grade_multiuser".i18n.fill([
userProvider.displayName!, userProvider.displayName!,
grade.value.value.toString(), grade.value.value.toString(),
grade.subject.isRenamed && grade.subject.isRenamed &&
settingsProvider.renamedSubjectsEnabled settingsProvider.renamedSubjectsEnabled
? grade.subject.renamedTo! ? grade.subject.renamedTo!
: grade.subject.name : grade.subject.name
]), ]),
notificationDetails); notificationDetails);
} }
} }
} }
} }
@ -102,50 +127,72 @@ class NotificationsHelper {
gradeProvider.seenAll(); gradeProvider.seenAll();
} }
void absenceNotification() async { void absenceNotification() async {
// get absences from api
List? absenceJson = await kretaClient.getAPI(KretaAPI.absences(userProvider.instituteCode ?? "")); List? absenceJson = await kretaClient.getAPI(KretaAPI.absences(userProvider.instituteCode ?? ""));
List<Absence> storedAbsences = await database.userQuery.getAbsences(userId: userProvider.id!); List<Absence> storedAbsences = await database.userQuery.getAbsences(userId: userProvider.id!);
if(absenceJson == null) { if(absenceJson == null) {
return; return;
} }
List<Absence> absences = absenceJson.map((e) => Absence.fromJson(e)).toList(); // format api absences to correct format while preserving hasSeen value
List<Absence> 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<Absence> modifiedAbsences = [];
if(absences != storedAbsences) { if(absences != storedAbsences) {
// remove absences that are not new
absences.removeWhere((element) => storedAbsences.contains(element)); absences.removeWhere((element) => storedAbsences.contains(element));
for(Absence absence in absences) { for(Absence absence in absences) {
const AndroidNotificationDetails androidNotificationDetails = if(!absence.hasSeen) {
AndroidNotificationDetails('ABSENCES', 'Hiányzások', absence.hasSeen = true;
channelDescription: 'Értesítés hiányzások beírásakor', modifiedAbsences.add(absence);
importance: Importance.max, const AndroidNotificationDetails androidNotificationDetails =
priority: Priority.max, AndroidNotificationDetails('ABSENCES', 'Hiányzások',
color: const Color(0xFF3D7BF4), channelDescription: 'Értesítés hiányzások beírásakor',
ticker: 'Hiányzások'); importance: Importance.max,
const NotificationDetails notificationDetails = NotificationDetails(android: androidNotificationDetails); priority: Priority.max,
if(userProvider.getUsers().length == 1) { color: const Color(0xFF3D7BF4),
await flutterLocalNotificationsPlugin.show( ticker: 'Hiányzások');
absence.id.hashCode, const NotificationDetails notificationDetails = NotificationDetails(android: androidNotificationDetails);
"title_absence".i18n, if(userProvider.getUsers().length == 1) {
"body_absence".i18n.fill([ await flutterLocalNotificationsPlugin.show(
DateFormat("yyyy-MM-dd").format(absence.date), absence.id.hashCode,
absence.subject.isRenamed && "title_absence".i18n,
settingsProvider.renamedSubjectsEnabled "body_absence".i18n.fill([
? absence.subject.renamedTo! DateFormat("yyyy-MM-dd").format(absence.date),
: absence.subject.name absence.subject.isRenamed &&
]), settingsProvider.renamedSubjectsEnabled
notificationDetails); ? absence.subject.renamedTo!
} else { : absence.subject.name
await flutterLocalNotificationsPlugin.show( ]),
absence.id.hashCode, notificationDetails);
"title_absence".i18n, } else {
"body_absence_multiuser".i18n.fill([ await flutterLocalNotificationsPlugin.show(
userProvider.displayName!, absence.id.hashCode,
DateFormat("yyyy-MM-dd").format(absence.date), "title_absence".i18n,
absence.subject.isRenamed && "body_absence_multiuser".i18n.fill([
settingsProvider.renamedSubjectsEnabled userProvider.displayName!,
? absence.subject.renamedTo! DateFormat("yyyy-MM-dd").format(absence.date),
: absence.subject.name absence.subject.isRenamed &&
]), settingsProvider.renamedSubjectsEnabled
notificationDetails); ? absence.subject.renamedTo!
: absence.subject.name
]),
notificationDetails);
}
} }
} }
} }
// combine modified absences and storedabsences list and save them to the database
List<Absence> combinedAbsences = combineLists(
modifiedAbsences,
storedAbsences,
(Absence absence) => absence.id,
);
await database.userStore.storeAbsences(combinedAbsences, userId: userProvider.id!);
} }
} }

View File

@ -31,6 +31,8 @@ class SettingsProvider extends ChangeNotifier {
bool _newsEnabled; bool _newsEnabled;
String _seenNews; String _seenNews;
bool _notificationsEnabled; bool _notificationsEnabled;
bool _notificationsGradesEnabled;
bool _notificationsAbsencesEnabled;
/* /*
notificationsBitfield values: notificationsBitfield values:
@ -84,6 +86,8 @@ class SettingsProvider extends ChangeNotifier {
required bool newsEnabled, required bool newsEnabled,
required String seenNews, required String seenNews,
required bool notificationsEnabled, required bool notificationsEnabled,
required bool notificationsGradesEnabled,
required bool notificationsAbsencesEnabled,
required int notificationsBitfield, required int notificationsBitfield,
required bool developerMode, required bool developerMode,
required int notificationPollInterval, required int notificationPollInterval,
@ -122,6 +126,8 @@ class SettingsProvider extends ChangeNotifier {
_newsEnabled = newsEnabled, _newsEnabled = newsEnabled,
_seenNews = seenNews, _seenNews = seenNews,
_notificationsEnabled = notificationsEnabled, _notificationsEnabled = notificationsEnabled,
_notificationsGradesEnabled = notificationsGradesEnabled,
_notificationsAbsencesEnabled = notificationsAbsencesEnabled,
_notificationsBitfield = notificationsBitfield, _notificationsBitfield = notificationsBitfield,
_developerMode = developerMode, _developerMode = developerMode,
_notificationPollInterval = notificationPollInterval, _notificationPollInterval = notificationPollInterval,
@ -178,6 +184,8 @@ class SettingsProvider extends ChangeNotifier {
newsEnabled: map["news"] == 1, newsEnabled: map["news"] == 1,
seenNews: map["seen_news"], seenNews: map["seen_news"],
notificationsEnabled: map["notifications"] == 1, notificationsEnabled: map["notifications"] == 1,
notificationsGradesEnabled: map["notifications_grades"] == 1,
notificationsAbsencesEnabled: map["notifications_absences"] == 1,
notificationsBitfield: map["notifications_bitfield"], notificationsBitfield: map["notifications_bitfield"],
notificationPollInterval: map["notification_poll_interval"], notificationPollInterval: map["notification_poll_interval"],
developerMode: map["developer_mode"] == 1, developerMode: map["developer_mode"] == 1,
@ -220,6 +228,8 @@ class SettingsProvider extends ChangeNotifier {
"news": _newsEnabled ? 1 : 0, "news": _newsEnabled ? 1 : 0,
"seen_news": _seenNews, "seen_news": _seenNews,
"notifications": _notificationsEnabled ? 1 : 0, "notifications": _notificationsEnabled ? 1 : 0,
"notifications_grades": _notificationsGradesEnabled ? 1 : 0,
"notifications_absences": _notificationsAbsencesEnabled ? 1 : 0,
"notifications_bitfield": _notificationsBitfield, "notifications_bitfield": _notificationsBitfield,
"developer_mode": _developerMode ? 1 : 0, "developer_mode": _developerMode ? 1 : 0,
"grade_color1": _gradeColors[0].value, "grade_color1": _gradeColors[0].value,
@ -273,6 +283,8 @@ class SettingsProvider extends ChangeNotifier {
newsEnabled: true, newsEnabled: true,
seenNews: '', seenNews: '',
notificationsEnabled: true, notificationsEnabled: true,
notificationsGradesEnabled: true,
notificationsAbsencesEnabled: true,
notificationsBitfield: 255, notificationsBitfield: 255,
developerMode: false, developerMode: false,
notificationPollInterval: 1, notificationPollInterval: 1,
@ -314,6 +326,8 @@ class SettingsProvider extends ChangeNotifier {
bool get newsEnabled => _newsEnabled; bool get newsEnabled => _newsEnabled;
List<String> get seenNews => _seenNews.split(','); List<String> get seenNews => _seenNews.split(',');
bool get notificationsEnabled => _notificationsEnabled; bool get notificationsEnabled => _notificationsEnabled;
bool get notificationsGradesEnabled => _notificationsGradesEnabled;
bool get notificationsAbsencesEnabled => _notificationsAbsencesEnabled;
int get notificationsBitfield => _notificationsBitfield; int get notificationsBitfield => _notificationsBitfield;
bool get developerMode => _developerMode; bool get developerMode => _developerMode;
int get notificationPollInterval => _notificationPollInterval; int get notificationPollInterval => _notificationPollInterval;
@ -357,6 +371,8 @@ class SettingsProvider extends ChangeNotifier {
bool? newsEnabled, bool? newsEnabled,
String? seenNewsId, String? seenNewsId,
bool? notificationsEnabled, bool? notificationsEnabled,
bool? notificationsGradesEnabled,
bool? notificationsAbsencesEnabled,
int? notificationsBitfield, int? notificationsBitfield,
bool? developerMode, bool? developerMode,
int? notificationPollInterval, int? notificationPollInterval,
@ -408,6 +424,14 @@ class SettingsProvider extends ChangeNotifier {
notificationsEnabled != _notificationsEnabled) { notificationsEnabled != _notificationsEnabled) {
_notificationsEnabled = notificationsEnabled; _notificationsEnabled = notificationsEnabled;
} }
if (notificationsGradesEnabled != null &&
notificationsGradesEnabled != _notificationsGradesEnabled) {
_notificationsGradesEnabled = notificationsGradesEnabled;
}
if (notificationsAbsencesEnabled != null &&
notificationsAbsencesEnabled != _notificationsAbsencesEnabled) {
_notificationsAbsencesEnabled = notificationsAbsencesEnabled;
}
if (notificationsBitfield != null && if (notificationsBitfield != null &&
notificationsBitfield != _notificationsBitfield) { notificationsBitfield != _notificationsBitfield) {
_notificationsBitfield = notificationsBitfield; _notificationsBitfield = notificationsBitfield;

View File

@ -18,6 +18,14 @@ class Absence {
DateTime lessonEnd; DateTime lessonEnd;
int? lessonIndex; int? lessonIndex;
String group; 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({ Absence({
required this.id, required this.id,
@ -35,6 +43,7 @@ class Absence {
this.lessonIndex, this.lessonIndex,
required this.group, required this.group,
this.json, this.json,
this.hasSeen = false,
}); });
factory Absence.fromJson(Map json) { factory Absence.fromJson(Map json) {
@ -80,6 +89,7 @@ class Absence {
lessonIndex: lessonIndex, lessonIndex: lessonIndex,
group: group:
json["OsztalyCsoport"] != null ? json["OsztalyCsoport"]["Uid"] : "", json["OsztalyCsoport"] != null ? json["OsztalyCsoport"]["Uid"] : "",
hasSeen: false,
json: json, json: json,
); );
} }

View File

@ -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<SettingsProvider>(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),
),
),
),
],
),
)
]),
))));
}
}

View File

@ -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<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@ -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_tile.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/accounts/account_view.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/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/privacy_view.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/settings_helper.dart'; import 'package:filcnaplo_mobile_ui/screens/settings/settings_helper.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
@ -453,41 +454,7 @@ class _SettingsScreenState extends State<SettingsScreen>
), ),
Material( Material(
type: MaterialType.transparency, type: MaterialType.transparency,
child: SwitchListTile( child: MenuNotifications(settings: settings)
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),
),
), ),
], ],
), ),