Merge pull request #102 from Monke14/notifications

Értesítések fix
This commit is contained in:
Márton Kiss 2024-03-10 20:34:08 +01:00 committed by GitHub
commit a199f919b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 814 additions and 689 deletions

View File

@ -66,7 +66,12 @@ const userDataDB = DatabaseStruct("user_data", {
// renamed teachers // non kreta data // renamed teachers // non kreta data
"renamed_teachers": String, "renamed_teachers": String,
// "subject_lesson_count": String, // non kreta data // "subject_lesson_count": String, // non kreta data
// notifications and surprise grades // non kreta data
"last_seen_grade": int, "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 planning // non kreta data
"goal_plans": String, "goal_plans": String,
"goal_averages": String, "goal_averages": String,
@ -129,7 +134,11 @@ Future<Database> initDB(DatabaseProvider database) async {
// renamed teachers // non kreta data // renamed teachers // non kreta data
"renamed_teachers": "{}", "renamed_teachers": "{}",
// "subject_lesson_count": "{}", // non kreta data // "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 planning // non kreta data
"goal_plans": "{}", "goal_plans": "{}",
"goal_averages": "{}", "goal_averages": "{}",
@ -207,3 +216,4 @@ Future<void> migrateDB(
print("INFO: Database migrated"); print("INFO: Database migrated");
} }
} }

View File

@ -1,6 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:refilc/api/providers/database_provider.dart'; import 'package:refilc/api/providers/database_provider.dart';
import 'package:refilc/models/linked_account.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/self_note.dart';
import 'package:refilc/models/subject_lesson_count.dart'; import 'package:refilc/models/subject_lesson_count.dart';
import 'package:refilc/models/user.dart'; import 'package:refilc/models/user.dart';
@ -213,11 +214,11 @@ class UserDatabaseQuery {
return lessonCount; return lessonCount;
} }
Future<DateTime> lastSeenGrade({required String userId}) async { Future<DateTime> lastSeen({required String userId, required LastSeenCategory category}) async {
List<Map> userData = List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]); await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return DateTime(0); 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); if (lastSeenDate == null) return DateTime(0);
DateTime lastSeen = DateTime.fromMillisecondsSinceEpoch(lastSeenDate); DateTime lastSeen = DateTime.fromMillisecondsSinceEpoch(lastSeenDate);
return lastSeen; return lastSeen;

View File

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:refilc/models/linked_account.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/self_note.dart';
import 'package:refilc/models/subject_lesson_count.dart'; import 'package:refilc/models/subject_lesson_count.dart';
import 'package:refilc_kreta_api/models/week.dart'; import 'package:refilc_kreta_api/models/week.dart';
@ -129,13 +130,14 @@ class UserDatabaseStore {
where: "id = ?", whereArgs: [userId]); where: "id = ?", whereArgs: [userId]);
} }
Future<void> storeLastSeenGrade(DateTime date, Future<void> storeLastSeen(DateTime date,
{required String userId}) async { {required String userId, required LastSeenCategory category}) async {
int lastSeenDate = date.millisecondsSinceEpoch; 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]); where: "id = ?", whereArgs: [userId]);
} }
// renamed things // renamed things
Future<void> storeRenamedSubjects(Map<String, String> subjects, Future<void> storeRenamedSubjects(Map<String, String> subjects,
{required String userId}) async { {required String userId}) async {

View File

@ -1,8 +1,12 @@
import 'package:flutter/foundation.dart';
import 'package:refilc/api/providers/database_provider.dart'; import 'package:refilc/api/providers/database_provider.dart';
import 'package:refilc/api/providers/status_provider.dart'; import 'package:refilc/api/providers/status_provider.dart';
import 'package:refilc/api/providers/user_provider.dart'; import 'package:refilc/api/providers/user_provider.dart';
import 'package:refilc/models/settings.dart'; import 'package:refilc/models/settings.dart';
import 'package:refilc/helpers/notification_helper.i18n.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/api.dart';
import 'package:refilc_kreta_api/client/client.dart'; import 'package:refilc_kreta_api/client/client.dart';
import 'package:refilc_kreta_api/models/absence.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:refilc_kreta_api/providers/timetable_provider.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart' import 'package:flutter_local_notifications/flutter_local_notifications.dart'
hide Message; hide Message;
import 'package:i18n_extension/i18n_extension.dart'; import 'package:i18n_extension/i18n_widget.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:refilc_kreta_api/models/message.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 { class NotificationsHelper {
late DatabaseProvider database; late DatabaseProvider database;
late SettingsProvider settingsProvider; late SettingsProvider settingsProvider;
@ -25,36 +38,14 @@ class NotificationsHelper {
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin(); FlutterLocalNotificationsPlugin();
List<T> combineLists<T, K>(
List<T> list1,
List<T> list2,
K Function(T) keyExtractor,
) {
Set<K> uniqueKeys = <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) { String dayTitle(DateTime date) {
try { 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) { } catch (e) {
return "Unknown"; return "Unknown";
} }
@ -69,43 +60,50 @@ class NotificationsHelper {
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 kreta login List<User> 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(); final status = StatusProvider();
kretaClient = KretaClient( KretaClient kretaClientForUser = KretaClient(
user: userProvider, settings: settingsProvider, status: status); user: userProviderForUser, settings: settingsProvider, status: status);
kretaClient.refreshLogin(); await kretaClientForUser.refreshLogin();
if (settingsProvider.notificationsGradesEnabled) gradeNotification();
if (settingsProvider.notificationsAbsencesEnabled) absenceNotification(); // Process notifications for current user
if (settingsProvider.notificationsMessagesEnabled) messageNotification(); if (settingsProvider.notificationsGradesEnabled) await gradeNotification(userProviderForUser, kretaClientForUser);
if (settingsProvider.notificationsLessonsEnabled) lessonNotification(); 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<void> gradeNotification(UserProvider currentuserProvider, KretaClient currentKretaClient) async {
// fetch grades // fetch grades
GradeProvider gradeProvider = GradeProvider( GradeProvider gradeProvider = GradeProvider(
settings: settingsProvider, settings: settingsProvider,
user: userProvider, user: currentuserProvider,
database: database, database: database,
kreta: kretaClient); kreta: currentKretaClient);
gradeProvider.fetch(); await gradeProvider.fetch();
List<Grade> grades = database.userQuery
await database.userQuery.getGrades(userId: userProvider.id ?? ""); .getGrades(userId: currentuserProvider.id!)
DateTime lastSeenGrade = .then((grades) async {
await database.userQuery.lastSeenGrade(userId: userProvider.id ?? ""); DateTime lastSeenGrade = await database.userQuery.lastSeen(
userId: currentuserProvider.id!, category: LastSeenCategory.grade);
// loop through grades and see which hasn't been seen yet // loop through grades and see which hasn't been seen yet
for (Grade grade in grades) { for (Grade grade in grades) {
// 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
// it worked in reverse, cuz someone added a * -1 to it, but it has been fixed now :D if (grade.date.isAfter(lastSeenGrade) &&
// old code below DateTime.now().difference(grade.date).inDays < 7) {
// 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 // send notificiation about new grade
AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails( AndroidNotificationDetails(
@ -119,7 +117,7 @@ class NotificationsHelper {
); );
NotificationDetails notificationDetails = NotificationDetails notificationDetails =
NotificationDetails(android: androidNotificationDetails); NotificationDetails(android: androidNotificationDetails);
if (userProvider.getUsers().length == 1) { if (currentuserProvider.getUsers().length == 1) {
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
grade.id.hashCode, grade.id.hashCode,
"title_grade".i18n, "title_grade".i18n,
@ -133,6 +131,7 @@ class NotificationsHelper {
], ],
), ),
notificationDetails, notificationDetails,
payload: "grades"
); );
} else { } else {
// multiple users are added, also display student name // multiple users are added, also display student name
@ -141,7 +140,7 @@ class NotificationsHelper {
"title_grade".i18n, "title_grade".i18n,
"body_grade_multiuser".i18n.fill( "body_grade_multiuser".i18n.fill(
[ [
userProvider.displayName!, currentuserProvider.displayName!,
grade.value.value.toString(), grade.value.value.toString(),
grade.subject.isRenamed && grade.subject.isRenamed &&
settingsProvider.renamedSubjectsEnabled settingsProvider.renamedSubjectsEnabled
@ -150,41 +149,31 @@ class NotificationsHelper {
], ],
), ),
notificationDetails, notificationDetails,
payload: "grades"
); );
} }
} }
} }
} }
// set grade seen status // set grade seen status
gradeProvider.seenAll(); database.userStore.storeLastSeen(DateTime.now(), userId: currentuserProvider.id!, category: LastSeenCategory.grade);
});
} }
void absenceNotification() async { Future<void> absenceNotification(UserProvider currentuserProvider, KretaClient currentKretaClient) async {
// get absences from api // get absences from api
List? absenceJson = await kretaClient List? absenceJson = await currentKretaClient
.getAPI(KretaAPI.absences(userProvider.instituteCode ?? "")); .getAPI(KretaAPI.absences(currentuserProvider.instituteCode ?? ""));
List<Absence> storedAbsences =
await database.userQuery.getAbsences(userId: userProvider.id!);
if (absenceJson == null) { if (absenceJson == null) {
return; return;
} }
// format api absences to correct format while preserving isSeen value DateTime lastSeenAbsence = await database.userQuery
List<Absence> absences = absenceJson.map((e) { .lastSeen(userId: currentuserProvider.id!, category: LastSeenCategory.absence);
Absence apiAbsence = Absence.fromJson(e); // format api absences
Absence storedAbsence = storedAbsences.firstWhere( List<Absence> absences =
(stored) => stored.id == apiAbsence.id, absenceJson.map((e) => Absence.fromJson(e)).toList();
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) { for (Absence absence in absences) {
if (!absence.isSeen) { if (absence.date.isAfter(lastSeenAbsence)) {
absence.isSeen = true;
modifiedAbsences.add(absence);
AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails( AndroidNotificationDetails(
'ABSENCES', 'ABSENCES',
@ -197,7 +186,7 @@ class NotificationsHelper {
); );
NotificationDetails notificationDetails = NotificationDetails notificationDetails =
NotificationDetails(android: androidNotificationDetails); NotificationDetails(android: androidNotificationDetails);
if (userProvider.getUsers().length == 1) { if (currentuserProvider.getUsers().length == 1) {
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
absence.id.hashCode, absence.id.hashCode,
"title_absence" "title_absence"
@ -212,6 +201,7 @@ class NotificationsHelper {
], ],
), ),
notificationDetails, notificationDetails,
payload: "absences"
); );
} else { } else {
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
@ -220,7 +210,7 @@ class NotificationsHelper {
.i18n, // https://discord.com/channels/1111649116020285532/1153273625206591528 .i18n, // https://discord.com/channels/1111649116020285532/1153273625206591528
"body_absence_multiuser".i18n.fill( "body_absence_multiuser".i18n.fill(
[ [
userProvider.displayName!, currentuserProvider.displayName!,
DateFormat("yyyy-MM-dd").format(absence.date), DateFormat("yyyy-MM-dd").format(absence.date),
absence.subject.isRenamed && absence.subject.isRenamed &&
settingsProvider.renamedSubjectsEnabled settingsProvider.renamedSubjectsEnabled
@ -229,60 +219,43 @@ class NotificationsHelper {
], ],
), ),
notificationDetails, notificationDetails,
payload: "absences"
); );
} }
} }
} }
} await database.userStore.storeLastSeen(DateTime.now(),
// combine modified absences and storedabsences list and save them to the database userId: currentuserProvider.id!, category: LastSeenCategory.absence);
List<Absence> combinedAbsences = combineLists(
modifiedAbsences,
storedAbsences,
(Absence absence) => absence.id,
);
await database.userStore
.storeAbsences(combinedAbsences, userId: userProvider.id!);
} }
void messageNotification() async { Future<void> messageNotification(UserProvider currentuserProvider, KretaClient currentKretaClient) async {
// get messages from api // get messages from api
List? messageJson = List? messageJson =
await kretaClient.getAPI(KretaAPI.messages("beerkezett")); await currentKretaClient.getAPI(KretaAPI.messages("beerkezett"));
List<Message> storedmessages =
await database.userQuery.getMessages(userId: userProvider.id!);
if (messageJson == null) { if (messageJson == null) {
return; return;
} }
// format api messages to correct format while preserving isSeen value // format api messages to correct format
// Parse messages // Parse messages
List<Message> messages = []; List<Message> messages = [];
await Future.wait(List.generate(messageJson.length, (index) { await Future.wait(List.generate(messageJson.length, (index) {
return () async { return () async {
Map message = messageJson.cast<Map>()[index]; Map message = messageJson.cast<Map>()[index];
Map? innerMessageJson = await kretaClient Map? innerMessageJson = await currentKretaClient
.getAPI(KretaAPI.message(message["azonosito"].toString())); .getAPI(KretaAPI.message(message["azonosito"].toString()));
await Future.delayed(const Duration(seconds: 1));
if (innerMessageJson != null) { if (innerMessageJson != null) {
messages.add( messages.add(Message.fromJson(innerMessageJson,
Message.fromJson(innerMessageJson, forceType: MessageType.inbox)); forceType: MessageType.inbox));
} }
}(); }();
})); }));
DateTime lastSeenMessage = await database.userQuery.lastSeen(
userId: currentuserProvider.id!, category: LastSeenCategory.message);
for (Message message in messages) { for (Message message in messages) {
for (Message storedMessage in storedmessages) { if (message.date.isAfter(lastSeenMessage)) {
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);
AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails( AndroidNotificationDetails(
'MESSAGES', 'MESSAGES',
@ -295,56 +268,47 @@ class NotificationsHelper {
); );
NotificationDetails notificationDetails = NotificationDetails notificationDetails =
NotificationDetails(android: androidNotificationDetails); NotificationDetails(android: androidNotificationDetails);
if (userProvider.getUsers().length == 1) { if (currentuserProvider.getUsers().length == 1) {
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
message.id.hashCode, message.id.hashCode,
message.author, message.author,
message.content.replaceAll(RegExp(r'<[^>]*>'), ''), message.content.replaceAll(RegExp(r'<[^>]*>'), ''),
notificationDetails, notificationDetails,
payload: "messages",
); );
} else { } else {
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
message.id.hashCode, message.id.hashCode,
"(${userProvider.displayName!}) ${message.author}", "(${currentuserProvider.displayName!}) ${message.author}",
message.content.replaceAll(RegExp(r'<[^>]*>'), ''), message.content.replaceAll(RegExp(r'<[^>]*>'), ''),
notificationDetails, notificationDetails,
payload: "messages",
); );
} }
} }
} }
} await database.userStore.storeLastSeen(DateTime.now(),
// combine modified messages and storedmessages list and save them to the database userId: currentuserProvider.id!, category: LastSeenCategory.message);
List<Message> combinedmessages = combineLists(
modifiedmessages,
storedmessages,
(Message message) => message.id,
);
await database.userStore
.storeMessages(combinedmessages, userId: userProvider.id!);
} }
void lessonNotification() async { Future<void> lessonNotification(UserProvider currentuserProvider, KretaClient currentKretaClient) async {
// get lesson from api // get lessons from api
TimetableProvider timetableProvider = TimetableProvider( TimetableProvider timetableProvider = TimetableProvider(
user: userProvider, database: database, kreta: kretaClient); user: currentuserProvider, database: database, kreta: currentKretaClient);
List<Lesson> storedlessons = await timetableProvider.restoreUser();
timetableProvider.lessons[Week.current()] ?? []; await timetableProvider.fetch(week: Week.current());
List? apilessons = timetableProvider.getWeek(Week.current()) ?? []; List<Lesson> apilessons =
timetableProvider.getWeek(Week.current()) ?? [];
DateTime lastSeenLesson = await database.userQuery.lastSeen(
userId: currentuserProvider.id!, category: LastSeenCategory.lesson);
Lesson? latestLesson;
for (Lesson lesson in apilessons) { for (Lesson lesson in apilessons) {
for (Lesson storedLesson in storedlessons) { if((lesson.status?.name != "Elmaradt" || lesson.substituteTeacher?.name != "") && lesson.date.isAfter(latestLesson?.start ?? DateTime(1970))) {
if (lesson.id == storedLesson.id) { latestLesson = lesson;
lesson.isSeen = storedLesson.isSeen;
} }
} if (lesson.date.isAfter(lastSeenLesson)) {
}
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);
AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails( AndroidNotificationDetails(
'LESSONS', 'LESSONS',
@ -358,7 +322,7 @@ class NotificationsHelper {
); );
NotificationDetails notificationDetails = NotificationDetails notificationDetails =
NotificationDetails(android: androidNotificationDetails); NotificationDetails(android: androidNotificationDetails);
if (userProvider.getUsers().length == 1) { if (currentuserProvider.getUsers().length == 1) {
if (lesson.status?.name == "Elmaradt") { if (lesson.status?.name == "Elmaradt") {
switch (I18n.localeStr) { switch (I18n.localeStr) {
case "en_en": case "en_en":
@ -374,6 +338,7 @@ class NotificationsHelper {
], ],
), ),
notificationDetails, notificationDetails,
payload: "timetable"
); );
break; break;
} }
@ -390,6 +355,7 @@ class NotificationsHelper {
], ],
), ),
notificationDetails, notificationDetails,
payload: "timetable"
); );
break; break;
} }
@ -406,11 +372,12 @@ class NotificationsHelper {
], ],
), ),
notificationDetails, notificationDetails,
payload: "timetable"
); );
break; break;
} }
} }
} else if (lesson.substituteTeacher?.name != "") { } else if (lesson.substituteTeacher?.name != "" && lesson.substituteTeacher != null) {
switch (I18n.localeStr) { switch (I18n.localeStr) {
case "en_en": case "en_en":
{ {
@ -428,6 +395,7 @@ class NotificationsHelper {
], ],
), ),
notificationDetails, notificationDetails,
payload: "timetable",
); );
break; break;
} }
@ -447,6 +415,7 @@ class NotificationsHelper {
], ],
), ),
notificationDetails, notificationDetails,
payload: "timetable",
); );
break; break;
} }
@ -466,6 +435,7 @@ class NotificationsHelper {
], ],
), ),
notificationDetails, notificationDetails,
payload: "timetable",
); );
break; break;
} }
@ -479,15 +449,16 @@ class NotificationsHelper {
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode, lesson.id.hashCode,
"title_lesson".i18n, "title_lesson".i18n,
"body_lesson_canceled".i18n.fill( "body_lesson_canceled_multiuser".i18n.fill(
[ [
userProvider.displayName!, currentuserProvider.displayName!,
lesson.lessonIndex, lesson.lessonIndex,
lesson.name, lesson.name,
dayTitle(lesson.date) dayTitle(lesson.date)
], ],
), ),
notificationDetails, notificationDetails,
payload: "timetable",
); );
break; break;
} }
@ -496,15 +467,16 @@ class NotificationsHelper {
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode, lesson.id.hashCode,
"title_lesson".i18n, "title_lesson".i18n,
"body_lesson_canceled".i18n.fill( "body_lesson_canceled_multiuser".i18n.fill(
[ [
userProvider.displayName!, currentuserProvider.displayName!,
dayTitle(lesson.date), dayTitle(lesson.date),
lesson.lessonIndex, lesson.lessonIndex,
lesson.name lesson.name
], ],
), ),
notificationDetails, notificationDetails,
payload: "timetable",
); );
break; break;
} }
@ -513,15 +485,16 @@ class NotificationsHelper {
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode, lesson.id.hashCode,
"title_lesson".i18n, "title_lesson".i18n,
"body_lesson_canceled".i18n.fill( "body_lesson_canceled_multiuser".i18n.fill(
[ [
userProvider.displayName!, currentuserProvider.displayName!,
lesson.lessonIndex, lesson.lessonIndex,
lesson.name, lesson.name,
dayTitle(lesson.date) dayTitle(lesson.date)
], ],
), ),
notificationDetails, notificationDetails,
payload: "timetable",
); );
break; break;
} }
@ -533,9 +506,9 @@ class NotificationsHelper {
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode, lesson.id.hashCode,
"title_lesson".i18n, "title_lesson".i18n,
"body_lesson_substituted".i18n.fill( "body_lesson_substituted_multiuser".i18n.fill(
[ [
userProvider.displayName!, currentuserProvider.displayName!,
lesson.lessonIndex, lesson.lessonIndex,
lesson.name, lesson.name,
dayTitle(lesson.date), dayTitle(lesson.date),
@ -545,6 +518,7 @@ class NotificationsHelper {
], ],
), ),
notificationDetails, notificationDetails,
payload: "timetable",
); );
break; break;
} }
@ -553,9 +527,9 @@ class NotificationsHelper {
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode, lesson.id.hashCode,
"title_lesson".i18n, "title_lesson".i18n,
"body_lesson_substituted".i18n.fill( "body_lesson_substituted_multiuser".i18n.fill(
[ [
userProvider.displayName!, currentuserProvider.displayName!,
dayTitle(lesson.date), dayTitle(lesson.date),
lesson.lessonIndex, lesson.lessonIndex,
lesson.name, lesson.name,
@ -565,6 +539,7 @@ class NotificationsHelper {
], ],
), ),
notificationDetails, notificationDetails,
payload: "timetable",
); );
break; break;
} }
@ -573,9 +548,9 @@ class NotificationsHelper {
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
lesson.id.hashCode, lesson.id.hashCode,
"title_lesson".i18n, "title_lesson".i18n,
"body_lesson_substituted".i18n.fill( "body_lesson_substituted_multiuser".i18n.fill(
[ [
userProvider.displayName!, currentuserProvider.displayName!,
lesson.lessonIndex, lesson.lessonIndex,
lesson.name, lesson.name,
dayTitle(lesson.date), dayTitle(lesson.date),
@ -585,6 +560,7 @@ class NotificationsHelper {
], ],
), ),
notificationDetails, notificationDetails,
payload: "timetable",
); );
break; break;
} }
@ -593,16 +569,44 @@ class NotificationsHelper {
} }
} }
} }
// combine modified lesson and storedlesson list and save them to the database // lesson.date does not contain time, only the date
List<Lesson> combinedlessons = combineLists( await database.userStore.storeLastSeen(latestLesson?.start ?? DateTime.now(),
modifiedlessons, userId: currentuserProvider.id!, category: LastSeenCategory.lesson);
storedlessons, }
(Lesson message) => message.id,
); // Called when the user taps a notification
Map<Week, List<Lesson>> timetableLessons = timetableProvider.lessons; void onDidReceiveNotificationResponse(NotificationResponse notificationResponse) async {
timetableLessons[Week.current()] = combinedlessons; final String? payload = notificationResponse.payload;
await database.userStore if (notificationResponse.payload != null) {
.storeLessons(timetableLessons, userId: userProvider.id!); debugPrint('notification payload: $payload');
}
switch(payload) {
case "timetable":
locator<NavigationService>().navigateTo("timetable");
break;
case "grades":
locator<NavigationService>().navigateTo("grades");
break;
case "messages":
locator<NavigationService>().navigateTo("messages");
break;
case "absences":
locator<NavigationService>().navigateTo("absences");
break;
case "settings":
locator<NavigationService>().navigateTo("settings");
break;
default:
break;
}
}
// Set all notification categories to seen
Future<void> 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);
}
}
} }
} }
}

View File

@ -1,4 +1,5 @@
import 'package:i18n_extension/i18n_extension.dart'; import 'package:i18n_extension/i18n_extension.dart';
import 'package:refilc/api/providers/database_provider.dart';
extension Localization on String { extension Localization on String {
static final _t = Translations.byLocale("hu_hu") + 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" "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<Object> params) => localizeFill(this, params); String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t); String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t); String version(Object modifier) => localizeVersion(modifier, this, _t);

View File

@ -10,6 +10,8 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:refilc/app.dart'; import 'package:refilc/app.dart';
import 'package:flutter/services.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_screen.dart';
import 'package:refilc_mobile_ui/screens/error_report_screen.dart'; import 'package:refilc_mobile_ui/screens/error_report_screen.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
@ -23,6 +25,9 @@ void main() async {
// ignore: deprecated_member_use // ignore: deprecated_member_use
binding.renderView.automaticSystemUiAdjustment = false; binding.renderView.automaticSystemUiAdjustment = false;
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
// navigation
setupLocator();
// Startup // Startup
Startup startup = Startup(); Startup startup = Startup();
await startup.start(); await startup.start();
@ -30,6 +35,11 @@ void main() async {
// Custom error page // Custom error page
ErrorWidget.builder = errorBuilder; ErrorWidget.builder = errorBuilder;
// initialize stripe key
stripe.Stripe.publishableKey =
'pk_test_51Oo7iUBS0FxsTGxKjGZSQqzDKWHY5ZFYM9XeI0qSdIh2w8jWy6GhHlYpT7GLTzgpl1xhE5YP4BXpA4gMZqPmgMId00cGFYFzbh';
BackgroundFetch.registerHeadlessTask(backgroundHeadlessTask); BackgroundFetch.registerHeadlessTask(backgroundHeadlessTask);
// Run App // Run App
@ -53,6 +63,9 @@ class Startup {
settings = await database.query.getSettings(database); settings = await database.query.getSettings(database);
user = await database.query.getUsers(settings); 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; late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
// Notifications setup // Notifications setup
if (!kIsWeb) { if (!kIsWeb) {
@ -113,6 +126,7 @@ class Startup {
// Initialize notifications // Initialize notifications
await flutterLocalNotificationsPlugin.initialize( await flutterLocalNotificationsPlugin.initialize(
initializationSettings, initializationSettings,
onDidReceiveNotificationResponse: NotificationsHelper().onDidReceiveNotificationResponse,
); );
} }

View File

@ -0,0 +1,9 @@
import 'package:flutter/material.dart';
class NavigationService {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
Future<dynamic> navigateTo(String routeName) {
return navigatorKey.currentState!.pushNamed(routeName);
}
}

View File

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

View File

@ -77,6 +77,8 @@ dependencies:
extension_google_sign_in_as_googleapis_auth: ^2.0.12 extension_google_sign_in_as_googleapis_auth: ^2.0.12
maps_launcher: ^2.2.0 maps_launcher: ^2.2.0
google_fonts: ^6.1.0 google_fonts: ^6.1.0
flutter_stripe: ^10.0.0
get_it: ^7.6.7
dev_dependencies: dev_dependencies:
flutter_lints: ^3.0.1 flutter_lints: ^3.0.1
@ -166,6 +168,7 @@ flutter:
weight: 500 weight: 500
flutter_launcher_icons: flutter_launcher_icons:
image_path: assets/icons/ic_android.png image_path: assets/icons/ic_android.png
android: true android: true

View File

@ -50,6 +50,7 @@ class Absence {
DateTime lessonStart; DateTime lessonStart;
DateTime lessonEnd; DateTime lessonEnd;
int? lessonIndex; int? lessonIndex;
bool isSeen = json["isSeen"] ?? false;
if (json["Ora"] != null) { if (json["Ora"] != null) {
lessonStart = json["Ora"]["KezdoDatum"] != null lessonStart = json["Ora"]["KezdoDatum"] != null
? DateTime.parse(json["Ora"]["KezdoDatum"]).toLocal() ? DateTime.parse(json["Ora"]["KezdoDatum"]).toLocal()
@ -62,7 +63,6 @@ class Absence {
lessonStart = DateTime(0); lessonStart = DateTime(0);
lessonEnd = DateTime(0); lessonEnd = DateTime(0);
} }
return Absence( return Absence(
id: json["Uid"], id: json["Uid"],
date: json["Datum"] != null date: json["Datum"] != null
@ -89,7 +89,7 @@ class Absence {
lessonIndex: lessonIndex, lessonIndex: lessonIndex,
group: group:
json["OsztalyCsoport"] != null ? json["OsztalyCsoport"]["Uid"] : "", json["OsztalyCsoport"] != null ? json["OsztalyCsoport"]["Uid"] : "",
isSeen: false, isSeen: json["isSeen"] ?? false,
json: json, json: json,
); );
} }

View File

@ -1,5 +1,6 @@
import 'package:refilc/api/providers/user_provider.dart'; import 'package:refilc/api/providers/user_provider.dart';
import 'package:refilc/api/providers/database_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/settings.dart';
import 'package:refilc/models/user.dart'; import 'package:refilc/models/user.dart';
import 'package:refilc_kreta_api/client/api.dart'; import 'package:refilc_kreta_api/client/api.dart';
@ -49,7 +50,7 @@ class GradeProvider with ChangeNotifier {
String? userId = _user.id; String? userId = _user.id;
if (userId != null) { if (userId != null) {
final userStore = _database.userStore; final userStore = _database.userStore;
userStore.storeLastSeenGrade(DateTime.now(), userId: userId); userStore.storeLastSeen(DateTime.now(), userId: userId, category: LastSeenCategory.surprisegrade);
_lastSeen = DateTime.now(); _lastSeen = DateTime.now();
} }
} }
@ -58,7 +59,7 @@ class GradeProvider with ChangeNotifier {
String? userId = _user.id; String? userId = _user.id;
if (userId != null) { if (userId != null) {
final userStore = _database.userStore; final userStore = _database.userStore;
userStore.storeLastSeenGrade(DateTime(1969), userId: userId); userStore.storeLastSeen(DateTime(1969), userId: userId, category: LastSeenCategory.surprisegrade);
_lastSeen = DateTime(1969); _lastSeen = DateTime(1969);
} }
} }
@ -74,7 +75,7 @@ class GradeProvider with ChangeNotifier {
await convertBySettings(); await convertBySettings();
_groupAvg = await userQuery.getGroupAverages(userId: userId); _groupAvg = await userQuery.getGroupAverages(userId: userId);
notifyListeners(); notifyListeners();
DateTime lastSeenDB = await userQuery.lastSeenGrade(userId: userId); DateTime lastSeenDB = await userQuery.lastSeen(userId: userId, category: LastSeenCategory.surprisegrade);
if (lastSeenDB.millisecondsSinceEpoch == 0 || if (lastSeenDB.millisecondsSinceEpoch == 0 ||
lastSeenDB.year == 0 || lastSeenDB.year == 0 ||
!_settings.gradeOpeningFun) { !_settings.gradeOpeningFun) {

View File

@ -5,6 +5,8 @@ import 'package:refilc/helpers/quick_actions.dart';
import 'package:refilc/icons/filc_icons.dart'; import 'package:refilc/icons/filc_icons.dart';
import 'package:refilc/models/settings.dart'; import 'package:refilc/models/settings.dart';
import 'package:refilc/theme/observer.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/client/client.dart';
import 'package:refilc_kreta_api/providers/grade_provider.dart'; import 'package:refilc_kreta_api/providers/grade_provider.dart';
import 'package:refilc_mobile_ui/common/system_chrome.dart'; import 'package:refilc_mobile_ui/common/system_chrome.dart';
@ -45,7 +47,7 @@ class NavigationScreenState extends State<NavigationScreen>
with WidgetsBindingObserver { with WidgetsBindingObserver {
late NavigationRoute selected; late NavigationRoute selected;
List<String> initializers = []; List<String> initializers = [];
final _navigatorState = GlobalKey<NavigatorState>(); final _navigatorState = locator<NavigationService>().navigatorKey;
late SettingsProvider settings; late SettingsProvider settings;
late NewsProvider newsProvider; late NewsProvider newsProvider;

View File

@ -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/settings.dart';
import 'package:refilc/models/user.dart';
import 'package:refilc/theme/colors/colors.dart'; import 'package:refilc/theme/colors/colors.dart';
import 'package:refilc_mobile_ui/common/beta_chip.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:refilc_mobile_ui/common/panel/panel_button.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:refilc_mobile_ui/common/splitted_panel/splitted_panel.dart';
import 'notifications_screen.i18n.dart'; import 'notifications_screen.i18n.dart';
class MenuNotifications extends StatelessWidget { class MenuNotifications extends StatelessWidget {
@ -53,6 +58,19 @@ class MenuNotifications extends StatelessWidget {
class NotificationsScreen extends StatelessWidget { class NotificationsScreen extends StatelessWidget {
const NotificationsScreen({super.key}); 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<DatabaseProvider>(context, listen: false);
User? user = Provider.of<UserProvider>(context, listen: false).user;
if (user != null) {
for (LastSeenCategory category in LastSeenCategory.values) {
database.userStore
.storeLastSeen(date, userId: user.id, category: category);
}
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
SettingsProvider settings = Provider.of<SettingsProvider>(context); SettingsProvider settings = Provider.of<SettingsProvider>(context);
@ -69,151 +87,188 @@ class NotificationsScreen extends StatelessWidget {
body: SingleChildScrollView( body: SingleChildScrollView(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0), padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
child: Panel(
child: Column( child: Column(
children: [ children: [
Material( SplittedPanel(
type: MaterialType.transparency, padding: const EdgeInsets.only(top: 8.0),
child: SwitchListTile( cardPadding: const EdgeInsets.all(4.0),
shape: RoundedRectangleBorder( isSeparated: true,
borderRadius: BorderRadius.circular(12.0)),
value: settings.notificationsGradesEnabled,
onChanged: (v) =>
settings.update(notificationsGradesEnabled: v),
title: Row(
children: [ children: [
Icon( PanelButton(
FeatherIcons.bookmark, padding: const EdgeInsets.only(left: 14.0, right: 6.0),
color: settings.notificationsGradesEnabled onPressed: () {
? Theme.of(context).colorScheme.secondary settings.update(
: AppColors.of(context).text.withOpacity(.25), notificationsGradesEnabled:
), !settings.notificationsGradesEnabled);
const SizedBox(width: 14.0), setAll(context, DateTime.now());
Expanded( },
child: Text( title: Text(
"grades".i18n, "grades".i18n,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16.0,
color: AppColors.of(context).text.withOpacity( color: AppColors.of(context).text.withOpacity(
settings.notificationsGradesEnabled settings.notificationsGradesEnabled ? .95 : .25),
? 1.0
: .5,
), ),
), ),
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),
), ),
), ),
], ],
), ),
), SplittedPanel(
), padding: const EdgeInsets.only(top: 8.0),
Material( cardPadding: const EdgeInsets.all(4.0),
type: MaterialType.transparency, isSeparated: true,
child: SwitchListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0)),
value: settings.notificationsAbsencesEnabled,
onChanged: (v) =>
settings.update(notificationsAbsencesEnabled: v),
title: Row(
children: [ children: [
Icon( PanelButton(
FeatherIcons.clock, padding: const EdgeInsets.only(left: 14.0, right: 6.0),
color: settings.notificationsAbsencesEnabled onPressed: () {
? Theme.of(context).colorScheme.secondary settings.update(
: AppColors.of(context).text.withOpacity(.25), notificationsAbsencesEnabled:
), !settings.notificationsAbsencesEnabled);
const SizedBox(width: 14.0), setAll(context, DateTime.now());
Expanded( },
child: Text( title: Text(
"absences".i18n, "absences".i18n,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16.0,
color: AppColors.of(context).text.withOpacity( color: AppColors.of(context).text.withOpacity(
settings.notificationsAbsencesEnabled settings.notificationsAbsencesEnabled ? .95 : .25),
? 1.0
: .5,
), ),
), ),
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),
), ),
), ),
], ],
), ),
), SplittedPanel(
), padding: const EdgeInsets.only(top: 8.0),
Material( cardPadding: const EdgeInsets.all(4.0),
type: MaterialType.transparency, isSeparated: true,
child: SwitchListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0)),
value: settings.notificationsMessagesEnabled,
onChanged: (v) =>
settings.update(notificationsMessagesEnabled: v),
title: Row(
children: [ children: [
Icon( PanelButton(
FeatherIcons.messageSquare, padding: const EdgeInsets.only(left: 14.0, right: 6.0),
color: settings.notificationsMessagesEnabled onPressed: () {
? Theme.of(context).colorScheme.secondary settings.update(
: AppColors.of(context).text.withOpacity(.25), notificationsMessagesEnabled:
), !settings.notificationsMessagesEnabled);
const SizedBox(width: 14.0), setAll(context, DateTime.now());
Expanded( },
child: Text( title: Text(
"messages".i18n, "messages".i18n,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16.0,
color: AppColors.of(context).text.withOpacity( color: AppColors.of(context).text.withOpacity(
settings.notificationsMessagesEnabled settings.notificationsMessagesEnabled ? .95 : .25),
? 1.0
: .5,
), ),
), ),
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),
), ),
), ),
], ],
), ),
), SplittedPanel(
), padding: const EdgeInsets.only(top: 8.0),
Material( cardPadding: const EdgeInsets.all(4.0),
type: MaterialType.transparency, isSeparated: true,
child: SwitchListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0)),
value: settings.notificationsLessonsEnabled,
onChanged: (v) =>
settings.update(notificationsLessonsEnabled: v),
title: Row(
children: [ children: [
Icon( PanelButton(
FeatherIcons.calendar, padding: const EdgeInsets.only(left: 14.0, right: 6.0),
color: settings.notificationsLessonsEnabled onPressed: () {
? Theme.of(context).colorScheme.secondary settings.update(
: AppColors.of(context).text.withOpacity(.25), notificationsLessonsEnabled:
), !settings.notificationsLessonsEnabled);
const SizedBox(width: 14.0), setAll(context, DateTime.now());
Expanded( },
child: Text( title: Text(
"lessons".i18n, "lessons".i18n,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16.0,
color: AppColors.of(context).text.withOpacity( color: AppColors.of(context).text.withOpacity(
settings.notificationsLessonsEnabled settings.notificationsLessonsEnabled ? .95 : .25),
? 1.0
: .5,
), ),
), ),
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(),
], ],
),
), ),
), ),
), ),

View File

@ -8,7 +8,8 @@ extension SettingsLocalization on String {
"grades": "Grades", "grades": "Grades",
"absences": "Absences", "absences": "Absences",
"messages": "Messages", "messages": "Messages",
"lessons": "Lessons" "lessons": "Lessons",
"set_all_as_unseen": "Set all as unseen",
}, },
"hu_hu": { "hu_hu": {
@ -16,14 +17,16 @@ extension SettingsLocalization on String {
"grades": "Jegyek", "grades": "Jegyek",
"absences": "Hiányzások", "absences": "Hiányzások",
"messages": "Üzenetek", "messages": "Üzenetek",
"lessons": "Órák" "lessons": "Órák",
"set_all_as_unseen": "Összes kategória beállítása olvasatlannak",
}, },
"de_de": { "de_de": {
"notifications_screen": "Mitteilung", "notifications_screen": "Mitteilung",
"grades": "Noten", "grades": "Noten",
"absences": "Fehlen", "absences": "Fehlen",
"messages": "Nachrichten", "messages": "Nachrichten",
"lessons": "Unterricht" "lessons": "Unterricht",
"set_all_as_unseen": "Alle als ungelesen einstellen",
}, },
}; };

View File

@ -66,6 +66,7 @@ dependencies:
google_fonts: ^6.1.0 google_fonts: ^6.1.0
flutter_any_logo: ^1.1.1 flutter_any_logo: ^1.1.1
custom_sliding_segmented_control: ^1.8.1 custom_sliding_segmented_control: ^1.8.1
get_it: ^7.6.7
dev_dependencies: dev_dependencies:
flutter_lints: ^3.0.1 flutter_lints: ^3.0.1