Merge pull request #29 from Monke14/notifications

Értesítések
This commit is contained in:
Márton Kiss 2023-08-30 00:47:45 +02:00 committed by GitHub
commit 91255182d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 2132 additions and 89 deletions

1286
.idea/libraries/Dart_Packages.xml generated Normal file

File diff suppressed because it is too large Load Diff

27
.idea/libraries/Dart_SDK.xml generated Normal file
View File

@ -0,0 +1,27 @@
<component name="libraryTable">
<library name="Dart SDK">
<CLASSES>
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/async" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/cli" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/collection" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/convert" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/core" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/developer" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/ffi" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/html" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/indexed_db" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/io" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/isolate" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/js" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/js_util" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/math" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/mirrors" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/svg" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/typed_data" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/web_audio" />
<root url="file:///opt/flutter/bin/cache/dart-sdk/lib/web_gl" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

4
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Android API 33, extension level 3 Platform" project-jdk-type="Android SDK" />
</project>

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_messages": int, "notifications_lessons":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

@ -1,34 +1,89 @@
import 'dart:math';
import 'dart:ui'; 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/client.dart'; import 'package:filcnaplo_kreta_api/client/client.dart';
import 'package:filcnaplo_kreta_api/models/absence.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart'; import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_kreta_api/models/lesson.dart';
import 'package:filcnaplo_kreta_api/models/week.dart';
import 'package:filcnaplo_kreta_api/providers/grade_provider.dart'; import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart' hide Message;
import 'package:i18n_extension/i18n_widget.dart';
import 'package:intl/intl.dart';
import 'package:filcnaplo_kreta_api/models/message.dart';
class NotificationsHelper { class NotificationsHelper {
late DatabaseProvider database;
late SettingsProvider settingsProvider;
late UserProvider userProvider;
late KretaClient kretaClient;
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;
}
String dayTitle(DateTime date) {
try {
return DateFormat("EEEE", I18n.locale.languageCode)
.format(date);
} catch (e) {
return "Unknown";
}
}
@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 = settingsProvider =
await database.query.getSettings(database); await database.query.getSettings(database);
UserProvider userProvider = await database.query.getUsers(settingsProvider); userProvider = await database.query.getUsers(settingsProvider);
if (userProvider.id != null && settingsProvider.notificationsEnabled) { if (userProvider.id != null && settingsProvider.notificationsEnabled) {
// refresh grades // refresh kreta login
final status = StatusProvider(); final status = StatusProvider();
final kretaClient = KretaClient( kretaClient = KretaClient(
user: userProvider, settings: settingsProvider, status: status); user: userProvider, settings: settingsProvider, status: status);
kretaClient.refreshLogin(); kretaClient.refreshLogin();
if(settingsProvider.notificationsGradesEnabled) gradeNotification();
if(settingsProvider.notificationsAbsencesEnabled) absenceNotification();
if(settingsProvider.notificationsMessagesEnabled) messageNotification();
if(settingsProvider.notificationsLessonsEnabled) lessonNotification();
}
}
void gradeNotification() async {
// fetch grades
GradeProvider gradeProvider = GradeProvider( GradeProvider gradeProvider = GradeProvider(
settings: settingsProvider, settings: settingsProvider,
user: userProvider, user: userProvider,
@ -45,27 +100,34 @@ class NotificationsHelper {
// if grade is not a normal grade (1-5), don't show it // if grade is not a normal grade (1-5), don't show it
if ([1, 2, 3, 4, 5].contains(grade.value.value)) { 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 the grade was added over a week ago, don't show it to avoid notification spam
if (grade.seenDate.isAfter(lastSeenGrade) && if (grade.seenDate.isAfter(lastSeenGrade) && grade.date.difference(DateTime.now()).inDays * -1 < 7) {
grade.date.difference(DateTime.now()).inDays * -1 < 7) {
// send notificiation about new grade // send notificiation about new grade
const AndroidNotificationDetails androidNotificationDetails = const AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails( AndroidNotificationDetails('GRADES', 'Jegyek',
'GRADES',
'Jegyek',
channelDescription: 'Értesítés jegyek beírásakor', channelDescription: 'Értesítés jegyek beírásakor',
importance: Importance.max, importance: Importance.max,
priority: Priority.max, priority: Priority.max,
color: Color(0xFF3D7BF4), color: const Color(0xFF3D7BF4),
ticker: 'Jegyek', ticker: 'Jegyek');
groupKey: 'refilc.notifications.GRADES_GROUP', const NotificationDetails notificationDetails = NotificationDetails(android: androidNotificationDetails);
); if(userProvider.getUsers().length == 1) {
const NotificationDetails notificationDetails =
NotificationDetails(android: androidNotificationDetails);
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
// probably shouldn't use a random int grade.id.hashCode,
Random().nextInt(432234 * 2), "title_grade".i18n,
"title".i18n, "body_grade".i18n.fill([
"body".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.value.value.toString(),
grade.subject.isRenamed && grade.subject.isRenamed &&
settingsProvider.renamedSubjectsEnabled settingsProvider.renamedSubjectsEnabled
@ -76,8 +138,387 @@ class NotificationsHelper {
} }
} }
} }
}
// set grade seen status // set grade seen status
gradeProvider.seenAll(); gradeProvider.seenAll();
} }
void absenceNotification() async {
// get absences from api
List? absenceJson = await kretaClient.getAPI(KretaAPI.absences(userProvider.instituteCode ?? ""));
List<Absence> storedAbsences = await database.userQuery.getAbsences(userId: userProvider.id!);
if(absenceJson == null) {
return;
}
// format api absences to correct format while preserving isSeen value
List<Absence> 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<Absence> modifiedAbsences = [];
if(absences != storedAbsences) {
// remove absences that are not new
absences.removeWhere((element) => storedAbsences.contains(element));
for(Absence absence in absences) {
if(!absence.isSeen) {
absence.isSeen = true;
modifiedAbsences.add(absence);
const AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails('ABSENCES', 'Hiányzások',
channelDescription: 'Értesítés hiányzások beírásakor',
importance: Importance.max,
priority: Priority.max,
color: const Color(0xFF3D7BF4),
ticker: 'Hiányzások');
const NotificationDetails notificationDetails = NotificationDetails(android: androidNotificationDetails);
if(userProvider.getUsers().length == 1) {
await flutterLocalNotificationsPlugin.show(
absence.id.hashCode,
"title_absence".i18n,
"body_absence".i18n.fill([
DateFormat("yyyy-MM-dd").format(absence.date),
absence.subject.isRenamed &&
settingsProvider.renamedSubjectsEnabled
? absence.subject.renamedTo!
: absence.subject.name
]),
notificationDetails);
} else {
await flutterLocalNotificationsPlugin.show(
absence.id.hashCode,
"title_absence".i18n,
"body_absence_multiuser".i18n.fill([
userProvider.displayName!,
DateFormat("yyyy-MM-dd").format(absence.date),
absence.subject.isRenamed &&
settingsProvider.renamedSubjectsEnabled
? absence.subject.renamedTo!
: absence.subject.name
]),
notificationDetails);
}
}
}
}
// combine modified absences and storedabsences list and save them to the database
List<Absence> combinedAbsences = combineLists(
modifiedAbsences,
storedAbsences,
(Absence absence) => absence.id,
);
await database.userStore.storeAbsences(combinedAbsences, userId: userProvider.id!);
}
void messageNotification() async {
// get messages from api
List? messageJson = await kretaClient.getAPI(KretaAPI.messages("beerkezett"));
List<Message> 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<Message> messages = [];
await Future.wait(List.generate(messageJson.length, (index) {
return () async {
Map message = messageJson!.cast<Map>()[index];
Map? innerMessageJson = await kretaClient.getAPI(KretaAPI.message(message["azonosito"].toString()));
if (innerMessageJson != null) messages.add(Message.fromJson(innerMessageJson, forceType: MessageType.inbox));
}();
}));
for(Message message in messages) {
for(Message storedMessage in storedmessages) {
if(message.id == storedMessage.id) {
message.isSeen = storedMessage.isSeen;
}
}
}
List<Message> modifiedmessages = [];
if(messages != storedmessages) {
// remove messages that are not new
messages.removeWhere((element) => storedmessages.contains(element));
for(Message message in messages) {
if(!message.isSeen) {
message.isSeen = true;
modifiedmessages.add(message);
const AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails('MESSAGES', 'Üzenetek',
channelDescription: 'Értesítés kapott üzenetekkor',
importance: Importance.max,
priority: Priority.max,
color: const Color(0xFF3D7BF4),
ticker: 'Üzenetek');
const NotificationDetails notificationDetails = NotificationDetails(android: androidNotificationDetails);
if(userProvider.getUsers().length == 1) {
await flutterLocalNotificationsPlugin.show(
message.id.hashCode,
message.author,
message.content.replaceAll(RegExp(r'<[^>]*>'), ''),
notificationDetails);
} else {
await flutterLocalNotificationsPlugin.show(
message.id.hashCode,
"(${userProvider.displayName!}) ${message.author}",
message.content.replaceAll(RegExp(r'<[^>]*>'), ''),
notificationDetails);
}
}
}
}
// combine modified messages and storedmessages list and save them to the database
List<Message> 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<Lesson> 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<Lesson> modifiedlessons = [];
if (apilessons != storedlessons) {
// remove lessons that are not new
apilessons.removeWhere((element) => storedlessons.contains(element));
for (Lesson lesson in apilessons) {
if (!lesson.isSeen && lesson.isChanged) {
lesson.isSeen = true;
modifiedlessons.add(lesson);
const AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails('LESSONS', 'Órák',
channelDescription:
'Értesítés órák elmaradásáról, helyettesítésről',
importance: Importance.max,
priority: Priority.max,
color: const Color(0xFF3D7BF4),
ticker: 'Órák');
const NotificationDetails notificationDetails =
NotificationDetails(android: androidNotificationDetails);
if (userProvider.getUsers().length == 1) {
if (lesson.status?.name == "Elmaradt") {
switch (I18n.localeStr) {
case "en_en":
{
await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode,
"title_lesson".i18n,
"body_lesson_canceled".i18n.fill([
lesson.lessonIndex,
lesson.name,
dayTitle(lesson.date)
]),
notificationDetails);
break;
}
case "hu_hu":
{
await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode,
"title_lesson".i18n,
"body_lesson_canceled".i18n.fill([
dayTitle(lesson.date),
lesson.lessonIndex,
lesson.name
]),
notificationDetails);
break;
}
default:
{
await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode,
"title_lesson".i18n,
"body_lesson_canceled".i18n.fill([
lesson.lessonIndex,
lesson.name,
dayTitle(lesson.date)
]),
notificationDetails);
break;
}
}
} else if (lesson.substituteTeacher?.name != "") {
switch (I18n.localeStr) {
case "en_en":
{
await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode,
"title_lesson".i18n,
"body_lesson_substituted".i18n.fill([
lesson.lessonIndex,
lesson.name,
dayTitle(lesson.date),
lesson.substituteTeacher!.isRenamed
? lesson.substituteTeacher!.renamedTo!
: lesson.substituteTeacher!.name
]),
notificationDetails);
break;
}
case "hu_hu":
{
await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode,
"title_lesson".i18n,
"body_lesson_substituted".i18n.fill([
dayTitle(lesson.date),
lesson.lessonIndex,
lesson.name,
lesson.substituteTeacher!.isRenamed
? lesson.substituteTeacher!.renamedTo!
: lesson.substituteTeacher!.name
]),
notificationDetails);
break;
}
default:
{
await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode,
"title_lesson".i18n,
"body_lesson_substituted".i18n.fill([
lesson.lessonIndex,
lesson.name,
dayTitle(lesson.date),
lesson.substituteTeacher!.isRenamed
? lesson.substituteTeacher!.renamedTo!
: lesson.substituteTeacher!.name
]),
notificationDetails);
break;
}
}
}
} else {
if (lesson.status?.name == "Elmaradt") {
switch (I18n.localeStr) {
case "en_en":
{
await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode,
"title_lesson".i18n,
"body_lesson_canceled".i18n.fill([
userProvider.displayName!,
lesson.lessonIndex,
lesson.name,
dayTitle(lesson.date)
]),
notificationDetails);
break;
}
case "hu_hu":
{
await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode,
"title_lesson".i18n,
"body_lesson_canceled".i18n.fill([
userProvider.displayName!,
dayTitle(lesson.date),
lesson.lessonIndex,
lesson.name
]),
notificationDetails);
break;
}
default:
{
await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode,
"title_lesson".i18n,
"body_lesson_canceled".i18n.fill([
userProvider.displayName!,
lesson.lessonIndex,
lesson.name,
dayTitle(lesson.date)
]),
notificationDetails);
break;
}
}
} else if (lesson.substituteTeacher?.name != "") {
switch (I18n.localeStr) {
case "en_en":
{
await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode,
"title_lesson".i18n,
"body_lesson_substituted".i18n.fill([
userProvider.displayName!,
lesson.lessonIndex,
lesson.name,
dayTitle(lesson.date),
lesson.substituteTeacher!.isRenamed
? lesson.substituteTeacher!.renamedTo!
: lesson.substituteTeacher!.name
]),
notificationDetails);
break;
}
case "hu_hu":
{
await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode,
"title_lesson".i18n,
"body_lesson_substituted".i18n.fill([
userProvider.displayName!,
dayTitle(lesson.date),
lesson.lessonIndex,
lesson.name,
lesson.substituteTeacher!.isRenamed
? lesson.substituteTeacher!.renamedTo!
: lesson.substituteTeacher!.name
]),
notificationDetails);
break;
}
default:
{
await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode,
"title_lesson".i18n,
"body_lesson_substituted".i18n.fill([
userProvider.displayName!,
lesson.lessonIndex,
lesson.name,
dayTitle(lesson.date),
lesson.substituteTeacher!.isRenamed
? lesson.substituteTeacher!.renamedTo!
: lesson.substituteTeacher!.name
]),
notificationDetails);
break;
}
}
}
}
}
}
// combine modified lesson and storedlesson list and save them to the database
List<Lesson> combinedlessons = combineLists(
modifiedlessons,
storedlessons,
(Lesson message) => message.id,
);
Map<Week, List<Lesson>> timetableLessons = timetableProvider.lessons;
timetableLessons[Week.current()] = combinedlessons;
await database.userStore
.storeLessons(timetableLessons, userId: userProvider.id!);
}
} }
} }

View File

@ -4,16 +4,43 @@ extension Localization on String {
static final _t = Translations.byLocale("hu_hu") + static final _t = Translations.byLocale("hu_hu") +
{ {
"en_en": { "en_en": {
"title": "New grade", "title_grade": "New grade",
"body": "You got a %s in %s" "body_grade": "You got a %s in %s",
"body_grade_multiuser": "%s got a %s in %s",
"title_absence": "Absence recorded",
"body_absence": "An absence was recorded on %s for %s",
"body_absence_multiuser": "An absence was recorded for %s on %s for the subject %s",
"title_lesson": "Timetable modified",
"body_lesson_canceled": "Lesson #%s (%s) has been canceled on %s",
"body_lesson_canceled_multiuser": "(%s) Lesson #%s (%s) has been canceled on %s",
"body_lesson_substituted": "Lesson #%s (%s) on %s will be substituted by %s",
"body_lesson_substituted_multiuser": "(%s) Lesson #%s (%s) on %s will be substituted by %s"
}, },
"hu_hu": { "hu_hu": {
"title": "Új jegy", "title_grade": "Új jegy",
"body": "%s-st kaptál %s tantárgyból" "body_grade": "%s-st kaptál %s tantárgyból",
"body_grade_multiuser": "%s tanuló %s-st kapott %s tantárgyból",
"title_absence": "Új hiányzás",
"body_absence": "Új hiányzást kaptál %s napon %s tantárgyból",
"body_absence_multiuser": "%s tanuló új hiányzást kapott %s napon %s tantárgyból",
"title_lesson": "Órarend szerkesztve",
"body_lesson_canceled": "%s-i %s. óra (%s) elmarad",
"body_lesson_canceled_multiuser": "(%s) %s-i %s. óra (%s) elmarad",
"body_lesson_substituted": "%s-i %s. (%s) órát %s helyettesíti",
"body_lesson_substituted_multiuser": "(%s) %s-i %s. (%s) órát %s helyettesíti"
}, },
"de_de": { "de_de": {
"title": "Neue Note", "title_grade": "Neue Note",
"body": "Du hast eine %s in %s" "body_grade": "Du hast eine %s in %s",
"body_grade_multiuser": "%s hast eine %s in %s",
"title_absence": "Abwesenheit aufgezeichnet",
"body_absence": "Auf %s für %s wurde eine Abwesenheit aufgezeichnet",
"body_absence_multiuser": "Für %s wurde am %s für das Thema Mathematik eine Abwesenheit aufgezeichnet",
"title_lesson": "Fahrplan geändert",
"body_lesson_canceled": "Lektion Nr. %s (%s) wurde am %s abgesagt",
"body_lesson_canceled_multiuser": "(%s) Lektion Nr. %s (%s) wurde am %s abgesagt",
"body_lesson_substituted": "Lektion Nr. %s (%s) wird am %s durch %s ersetzt",
"body_lesson_substituted_multiuser": "(%s) Lektion Nr. %s (%s) wird am %s durch %s ersetzt"
}, },
}; };

View File

@ -31,6 +31,10 @@ class SettingsProvider extends ChangeNotifier {
bool _newsEnabled; bool _newsEnabled;
String _seenNews; String _seenNews;
bool _notificationsEnabled; bool _notificationsEnabled;
bool _notificationsGradesEnabled;
bool _notificationsAbsencesEnabled;
bool _notificationsMessagesEnabled;
bool _notificationsLessonsEnabled;
/* /*
notificationsBitfield values: notificationsBitfield values:
@ -84,6 +88,10 @@ 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 bool notificationsMessagesEnabled,
required bool notificationsLessonsEnabled,
required int notificationsBitfield, required int notificationsBitfield,
required bool developerMode, required bool developerMode,
required int notificationPollInterval, required int notificationPollInterval,
@ -122,6 +130,10 @@ class SettingsProvider extends ChangeNotifier {
_newsEnabled = newsEnabled, _newsEnabled = newsEnabled,
_seenNews = seenNews, _seenNews = seenNews,
_notificationsEnabled = notificationsEnabled, _notificationsEnabled = notificationsEnabled,
_notificationsGradesEnabled = notificationsGradesEnabled,
_notificationsAbsencesEnabled = notificationsAbsencesEnabled,
_notificationsMessagesEnabled = notificationsMessagesEnabled,
_notificationsLessonsEnabled = notificationsLessonsEnabled,
_notificationsBitfield = notificationsBitfield, _notificationsBitfield = notificationsBitfield,
_developerMode = developerMode, _developerMode = developerMode,
_notificationPollInterval = notificationPollInterval, _notificationPollInterval = notificationPollInterval,
@ -178,6 +190,10 @@ 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,
notificationsMessagesEnabled: map["notifications_messages"] == 1,
notificationsLessonsEnabled: map["notifications_lessons"] == 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 +236,10 @@ 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_messages": _notificationsMessagesEnabled ? 1 : 0,
"notifications_lessons": _notificationsLessonsEnabled ? 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 +293,10 @@ class SettingsProvider extends ChangeNotifier {
newsEnabled: true, newsEnabled: true,
seenNews: '', seenNews: '',
notificationsEnabled: true, notificationsEnabled: true,
notificationsGradesEnabled: true,
notificationsAbsencesEnabled: true,
notificationsMessagesEnabled: true,
notificationsLessonsEnabled: true,
notificationsBitfield: 255, notificationsBitfield: 255,
developerMode: false, developerMode: false,
notificationPollInterval: 1, notificationPollInterval: 1,
@ -314,6 +338,10 @@ 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;
bool get notificationsMessagesEnabled => _notificationsMessagesEnabled;
bool get notificationsLessonsEnabled => _notificationsLessonsEnabled;
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 +385,10 @@ class SettingsProvider extends ChangeNotifier {
bool? newsEnabled, bool? newsEnabled,
String? seenNewsId, String? seenNewsId,
bool? notificationsEnabled, bool? notificationsEnabled,
bool? notificationsGradesEnabled,
bool? notificationsAbsencesEnabled,
bool? notificationsMessagesEnabled,
bool? notificationsLessonsEnabled,
int? notificationsBitfield, int? notificationsBitfield,
bool? developerMode, bool? developerMode,
int? notificationPollInterval, int? notificationPollInterval,
@ -408,6 +440,22 @@ 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 (notificationsMessagesEnabled != null &&
notificationsMessagesEnabled != _notificationsMessagesEnabled) {
_notificationsMessagesEnabled = notificationsMessagesEnabled;
}
if (notificationsLessonsEnabled != null &&
notificationsLessonsEnabled != _notificationsLessonsEnabled) {
_notificationsLessonsEnabled = notificationsLessonsEnabled;
}
if (notificationsBitfield != null && if (notificationsBitfield != null &&
notificationsBitfield != _notificationsBitfield) { notificationsBitfield != _notificationsBitfield) {
_notificationsBitfield = notificationsBitfield; _notificationsBitfield = notificationsBitfield;

View File

@ -89,7 +89,9 @@ Future<List<DateWidget>> getFilterWidgets(FilterType activeData,
// Grades // Grades
case FilterType.grades: case FilterType.grades:
if(!settingsProvider.gradeOpeningFun) {
gradeProvider.seenAll(); gradeProvider.seenAll();
}
items = grade_filter.getWidgets( items = grade_filter.getWidgets(
gradeProvider.grades, gradeProvider.lastSeenDate); gradeProvider.grades, gradeProvider.lastSeenDate);
if (settingsProvider.gradeOpeningFun) { if (settingsProvider.gradeOpeningFun) {

View File

@ -192,6 +192,7 @@ class GradeValueWidget extends StatelessWidget {
this.outline = false, this.outline = false,
this.complemented = false, this.complemented = false,
this.nocolor = false, this.nocolor = false,
this.color,
}) : super(key: key); }) : super(key: key);
final GradeValue value; final GradeValue value;
@ -202,6 +203,7 @@ class GradeValueWidget extends StatelessWidget {
final bool outline; final bool outline;
final bool complemented; final bool complemented;
final bool nocolor; final bool nocolor;
final Color? color;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -209,7 +211,7 @@ class GradeValueWidget extends StatelessWidget {
bool isSubjectView = SubjectGradesContainer.of(context) != null; bool isSubjectView = SubjectGradesContainer.of(context) != null;
Color color = Color color =
gradeColor(context: context, value: value.value, nocolor: nocolor); this.color ?? gradeColor(context: context, value: value.value, nocolor: nocolor);
Widget valueText; Widget valueText;
final percentage = value.percentage; final percentage = value.percentage;

View File

@ -18,6 +18,14 @@ class Absence {
DateTime lessonEnd; DateTime lessonEnd;
int? lessonIndex; int? lessonIndex;
String group; String group;
bool isSeen;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Absence && runtimeType == other.runtimeType && id == other.id;
@override
int get hashCode => id.hashCode;
Absence({ 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.isSeen = 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"] : "",
isSeen: false,
json: json, json: json,
); );
} }

View File

@ -25,6 +25,7 @@ class Lesson {
String name; String name;
bool online; bool online;
bool isEmpty; bool isEmpty;
bool isSeen;
Lesson({ Lesson({
this.status, this.status,
@ -49,7 +50,15 @@ class Lesson {
this.online = false, this.online = false,
this.isEmpty = false, this.isEmpty = false,
this.json, this.json,
this.isSeen = false,
}); });
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Lesson && runtimeType == other.runtimeType && id == other.id;
@override
int get hashCode => id.hashCode;
factory Lesson.fromJson(Map json) { factory Lesson.fromJson(Map json) {
return Lesson( return Lesson(
@ -90,6 +99,7 @@ class Lesson {
online: json["IsDigitalisOra"] ?? false, online: json["IsDigitalisOra"] ?? false,
isEmpty: json['isEmpty'] ?? false, isEmpty: json['isEmpty'] ?? false,
json: json, json: json,
isSeen: false
); );
} }

View File

@ -16,6 +16,7 @@ class Message {
MessageType? type; MessageType? type;
List<Recipient> recipients; List<Recipient> recipients;
List<Attachment> attachments; List<Attachment> attachments;
bool isSeen;
Message({ Message({
required this.id, required this.id,
@ -32,7 +33,15 @@ class Message {
this.replyId, this.replyId,
this.conversationId, this.conversationId,
this.json, this.json,
this.isSeen = false,
}); });
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Message && runtimeType == other.runtimeType && id == other.id;
@override
int get hashCode => id.hashCode;
factory Message.fromJson(Map json, {MessageType? forceType}) { factory Message.fromJson(Map json, {MessageType? forceType}) {
Map message = json["uzenet"]; Map message = json["uzenet"];
@ -69,6 +78,7 @@ class Message {
replyId: message["elozoUzenetAzonosito"], replyId: message["elozoUzenetAzonosito"],
conversationId: message["beszelgetesAzonosito"], conversationId: message["beszelgetesAzonosito"],
json: json, json: json,
isSeen: false,
); );
} }

View File

@ -8,7 +8,7 @@ import 'package:filcnaplo_kreta_api/models/week.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class TimetableProvider with ChangeNotifier { class TimetableProvider with ChangeNotifier {
Map<Week, List<Lesson>> _lessons = {}; Map<Week, List<Lesson>> lessons = {};
late final UserProvider _user; late final UserProvider _user;
late final DatabaseProvider _database; late final DatabaseProvider _database;
late final KretaClient _kreta; late final KretaClient _kreta;
@ -29,7 +29,7 @@ class TimetableProvider with ChangeNotifier {
// Load lessons from the database // Load lessons from the database
if (userId != null) { if (userId != null) {
var dbLessons = await _database.userQuery.getLessons(userId: userId); var dbLessons = await _database.userQuery.getLessons(userId: userId);
_lessons = dbLessons; lessons = dbLessons;
await convertBySettings(); await convertBySettings();
} }
} }
@ -45,7 +45,7 @@ class TimetableProvider with ChangeNotifier {
? await _database.userQuery.renamedTeachers(userId: _user.id!) ? await _database.userQuery.renamedTeachers(userId: _user.id!)
: {}; : {};
for (Lesson lesson in _lessons.values.expand((e) => e)) { for (Lesson lesson in lessons.values.expand((e) => e)) {
lesson.subject.renamedTo = renamedSubjects.isNotEmpty lesson.subject.renamedTo = renamedSubjects.isNotEmpty
? renamedSubjects[lesson.subject.id] ? renamedSubjects[lesson.subject.id]
: null; : null;
@ -57,7 +57,7 @@ class TimetableProvider with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
List<Lesson>? getWeek(Week week) => _lessons[week]; List<Lesson>? getWeek(Week week) => lessons[week];
// Fetches Lessons from the Kreta API then stores them in the database // Fetches Lessons from the Kreta API then stores them in the database
Future<void> fetch({Week? week}) async { Future<void> fetch({Week? week}) async {
@ -68,11 +68,11 @@ class TimetableProvider with ChangeNotifier {
List? lessonsJson = await _kreta List? lessonsJson = await _kreta
.getAPI(KretaAPI.timetable(iss, start: week.start, end: week.end)); .getAPI(KretaAPI.timetable(iss, start: week.start, end: week.end));
if (lessonsJson == null) throw "Cannot fetch Lessons for User ${user.id}"; if (lessonsJson == null) throw "Cannot fetch Lessons for User ${user.id}";
List<Lesson> lessons = lessonsJson.map((e) => Lesson.fromJson(e)).toList(); List<Lesson> lessonsList = lessonsJson.map((e) => Lesson.fromJson(e)).toList();
if (lessons.isEmpty && _lessons.isEmpty) return; if (lessons.isEmpty && lessons.isEmpty) return;
_lessons[week] = lessons; lessons[week] = lessonsList;
await store(); await store();
await convertBySettings(); await convertBySettings();
@ -85,7 +85,7 @@ class TimetableProvider with ChangeNotifier {
String userId = user.id; String userId = user.id;
// -TODO: clear indexes with weeks outside of the current school year // -TODO: clear indexes with weeks outside of the current school year
await _database.userStore.storeLessons(_lessons, userId: userId); await _database.userStore.storeLessons(lessons, userId: userId);
} }
// Future<void> setLessonCount(SubjectLessonCount lessonCount, {bool store = true}) async { // Future<void> setLessonCount(SubjectLessonCount lessonCount, {bool store = true}) async {

View File

@ -0,0 +1,175 @@
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo/ui/widgets/grade/grade_tile.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel_button.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:provider/provider.dart';
import 'notifications_screen.i18n.dart';
class MenuNotifications extends StatelessWidget {
const MenuNotifications({Key? key, required this.settings}) : super(key: key);
final SettingsProvider settings;
@override
Widget build(BuildContext context) {
return PanelButton(
padding: const EdgeInsets.only(left: 14.0),
onPressed: () {
Navigator.of(context, rootNavigator: true).push(
CupertinoPageRoute(builder: (context) => NotificationsScreen()),
);
},
title: Text(
"notifications_screen".i18n,
style: TextStyle(
color: AppColors.of(context)
.text
.withOpacity(settings.notificationsEnabled ? 1.0 : .5)),
),
leading: settings.notificationsEnabled
? const Icon(FeatherIcons.messageSquare)
: Icon(FeatherIcons.messageSquare,
color: AppColors.of(context).text.withOpacity(.25)),
trailingDivider: true,
trailing: Switch(
onChanged: (v) async {
settings.update(notificationsEnabled: v);
},
value: settings.notificationsEnabled,
activeColor: Theme.of(context).colorScheme.secondary,
),
);
}
}
class NotificationsScreen extends StatelessWidget {
NotificationsScreen({super.key});
late SettingsProvider settings;
@override
Widget build(BuildContext context) {
settings = Provider.of<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, color: settings.gradeColors[4].withOpacity(
settings.notificationsGradesEnabled ? 1.0 : .5)),
const SizedBox(width: 14.0),
Expanded(
child: Text(
"grades".i18n,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16.0,
color: AppColors.of(context).text.withOpacity(
settings.notificationsGradesEnabled ? 1.0 : .5),
),
),
),
],
),
),
SwitchListTile(
value: settings.notificationsAbsencesEnabled,
onChanged: (v) => {settings.update(notificationsAbsencesEnabled: v)},
title: Row(
children: [
const SizedBox(width: 8),
settings.notificationsAbsencesEnabled
? const Icon(FeatherIcons.clock)
: Icon(FeatherIcons.clock,
color:
AppColors.of(context).text.withOpacity(.25)),
const SizedBox(width: 23.0),
Expanded(
child: Text(
"absences".i18n,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16.0,
color: AppColors.of(context).text.withOpacity(
settings.notificationsAbsencesEnabled ? 1.0 : .5),
),
),
),
],
),
),
SwitchListTile(
value: settings.notificationsMessagesEnabled,
onChanged: (v) => {settings.update(notificationsMessagesEnabled: v)},
title: Row(
children: [
const SizedBox(width: 8),
settings.notificationsMessagesEnabled
? const Icon(FeatherIcons.messageSquare)
: Icon(FeatherIcons.messageSquare,
color:
AppColors.of(context).text.withOpacity(.25)),
const SizedBox(width: 23.0),
Expanded(
child: Text(
"messages".i18n,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16.0,
color: AppColors.of(context).text.withOpacity(
settings.notificationsMessagesEnabled ? 1.0 : .5),
),
),
),
],
),
),
SwitchListTile(
value: settings.notificationsLessonsEnabled,
onChanged: (v) => {settings.update(notificationsLessonsEnabled: v)},
title: Row(
children: [
const SizedBox(width: 8),
settings.notificationsLessonsEnabled
? const Icon(FeatherIcons.calendar)
: Icon(FeatherIcons.calendar,
color:
AppColors.of(context).text.withOpacity(.25)),
const SizedBox(width: 23.0),
Expanded(
child: Text(
"lessons".i18n,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16.0,
color: AppColors.of(context).text.withOpacity(
settings.notificationsLessonsEnabled ? 1.0 : .5),
),
),
),
],
),
)
]),
))));
}
}

View File

@ -0,0 +1,34 @@
import 'package:i18n_extension/i18n_extension.dart';
extension SettingsLocalization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"notifications_screen": "Notifications",
"grades": "Grades",
"absences": "Absences",
"messages": "Messages",
"lessons": "Lessons"
},
"hu_hu": {
"notifications_screen": "Értesítések",
"grades": "Jegyek",
"absences": "Hiányzások",
"messages": "Üzenetek",
"lessons": "Órák"
},
"de_de": {
"notifications_screen": "Mitteilung",
"grades": "Noten",
"absences": "Fehlen",
"messages": "Nachrichten",
"lessons": "Unterricht"
},
};
String get i18n => localize(this, _t);
String fill(List<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),
),
), ),
], ],
), ),