diff --git a/.idea/naplo.iml b/.idea/naplo.iml index d6ebd48..ab2f482 100644 --- a/.idea/naplo.iml +++ b/.idea/naplo.iml @@ -2,7 +2,65 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/filcnaplo/android/app/src/main/res/drawable/ic_notification.png b/filcnaplo/android/app/src/main/res/drawable/ic_notification.png new file mode 100644 index 0000000..67fd370 Binary files /dev/null and b/filcnaplo/android/app/src/main/res/drawable/ic_notification.png differ diff --git a/filcnaplo/lib/helpers/notification_helper.dart b/filcnaplo/lib/helpers/notification_helper.dart new file mode 100644 index 0000000..c1cfcb6 --- /dev/null +++ b/filcnaplo/lib/helpers/notification_helper.dart @@ -0,0 +1,77 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:filcnaplo/api/providers/database_provider.dart'; +import 'package:filcnaplo/api/providers/status_provider.dart'; +import 'package:filcnaplo/api/providers/user_provider.dart'; +import 'package:filcnaplo/database/init.dart'; +import 'package:filcnaplo/models/settings.dart'; +import 'package:filcnaplo/helpers/notification_helper.i18n.dart'; +import 'package:filcnaplo_kreta_api/client/client.dart'; +import 'package:filcnaplo_kreta_api/models/grade.dart'; +import 'package:filcnaplo_kreta_api/providers/grade_provider.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; + +class NotificationsHelper { + @pragma('vm:entry-point') + void backgroundJob() async { + // initialize providers + FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); + DatabaseProvider database = DatabaseProvider(); + var db = await initDB(database); + await database.init(); + SettingsProvider settingsProvider = + await database.query.getSettings(database); + UserProvider userProvider = await database.query.getUsers(settingsProvider); + + if (userProvider.id != null && settingsProvider.notificationsEnabled) { + // refresh grades + final status = StatusProvider(); + final kretaClient = KretaClient( + user: userProvider, settings: settingsProvider, status: status); + kretaClient.refreshLogin(); + GradeProvider gradeProvider = GradeProvider( + settings: settingsProvider, + user: userProvider, + database: database, + kreta: kretaClient); + gradeProvider.fetch(); + List grades = + await database.userQuery.getGrades(userId: userProvider.id ?? ""); + DateTime lastSeenGrade = + await database.userQuery.lastSeenGrade(userId: userProvider.id ?? ""); + + // loop through grades and see which hasn't been seen yet + for (Grade grade in grades) { + // if the grade was added over a week ago, don't show it to avoid notification spam + if (grade.seenDate.isAfter(lastSeenGrade) && grade.date.difference(DateTime.now()).inDays * -1 < 7) { + // send notificiation about new grade + const AndroidNotificationDetails androidNotificationDetails = + AndroidNotificationDetails('GRADES', 'Jegyek', + channelDescription: 'Értesítés jegyek beírásakor', + importance: Importance.max, + priority: Priority.max, + color: const Color(0xFF3D7BF4), + ticker: 'Jegyek'); + const NotificationDetails notificationDetails = + NotificationDetails(android: androidNotificationDetails); + await flutterLocalNotificationsPlugin.show( + // probably shouldn't use a random int + Random().nextInt(432234 * 2), + "title".i18n, + "body".i18n.fill([ + grade.value.value.toString(), + grade.subject.isRenamed && + settingsProvider.renamedSubjectsEnabled + ? grade.subject.renamedTo! + : grade.subject.name + ]), + notificationDetails); + } + } + // set grade seen status + gradeProvider.seenAll(); + } + } +} diff --git a/filcnaplo/lib/helpers/notification_helper.i18n.dart b/filcnaplo/lib/helpers/notification_helper.i18n.dart new file mode 100644 index 0000000..5c96488 --- /dev/null +++ b/filcnaplo/lib/helpers/notification_helper.i18n.dart @@ -0,0 +1,24 @@ +import 'package:i18n_extension/i18n_extension.dart'; + +extension Localization on String { + static final _t = Translations.byLocale("hu_hu") + + { + "en_en": { + "title": "New grade", + "body": "You got a %s in %s" + }, + "hu_hu": { + "title": "Új jegy", + "body": "%s-st kaptál %s tantárgyból" + }, + "de_de": { + "title": "Neue Note", + "body": "Du hast eine %s in %s" + }, + }; + + String get i18n => localize(this, _t); + String fill(List params) => localizeFill(this, params); + String plural(int value) => localizePlural(value, this, _t); + String version(Object modifier) => localizeVersion(modifier, this, _t); +} diff --git a/filcnaplo/lib/main.dart b/filcnaplo/lib/main.dart index 8cbfb9e..9f62e1e 100644 --- a/filcnaplo/lib/main.dart +++ b/filcnaplo/lib/main.dart @@ -2,13 +2,16 @@ import 'package:background_fetch/background_fetch.dart'; import 'package:filcnaplo/api/providers/user_provider.dart'; import 'package:filcnaplo/api/providers/database_provider.dart'; import 'package:filcnaplo/database/init.dart'; +import 'package:filcnaplo/helpers/notification_helper.dart'; import 'package:filcnaplo/models/settings.dart'; +import 'package:filcnaplo_kreta_api/client/client.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:filcnaplo/app.dart'; import 'package:flutter/services.dart'; import 'package:filcnaplo_mobile_ui/screens/error_screen.dart'; import 'package:filcnaplo_mobile_ui/screens/error_report_screen.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; void main() async { // Initalize @@ -43,6 +46,53 @@ class Startup { await database.init(); settings = await database.query.getSettings(database); user = await database.query.getUsers(settings); + + // Notifications setup + initPlatformState(); + FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); + + // Get permission to show notifications + flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>()! + .requestPermission(); + await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + IOSFlutterLocalNotificationsPlugin>() + ?.requestPermissions( + alert: false, + badge: true, + sound: true, + ); + await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + MacOSFlutterLocalNotificationsPlugin>() + ?.requestPermissions( + alert: false, + badge: true, + sound: true, + ); + + // Platform specific settings + final DarwinInitializationSettings initializationSettingsDarwin = + DarwinInitializationSettings( + requestSoundPermission: true, + requestBadgePermission: true, + requestAlertPermission: false, + ); + const AndroidInitializationSettings initializationSettingsAndroid = + AndroidInitializationSettings('ic_notification'); + final InitializationSettings initializationSettings = InitializationSettings( + android: initializationSettingsAndroid, + iOS: initializationSettingsDarwin, + macOS: initializationSettingsDarwin + ); + + // Initialize notifications + await flutterLocalNotificationsPlugin.initialize( + initializationSettings, + ); } } @@ -71,11 +121,51 @@ Widget errorBuilder(FlutterErrorDetails details) { return Container(); }); } + Future initPlatformState() async { + // Configure BackgroundFetch. + int status = await BackgroundFetch.configure(BackgroundFetchConfig( + minimumFetchInterval: 15, + stopOnTerminate: false, + enableHeadless: true, + requiresBatteryNotLow: false, + requiresCharging: false, + requiresStorageNotLow: false, + requiresDeviceIdle: false, + requiredNetworkType: NetworkType.ANY, + startOnBoot: true + ), (String taskId) async { // <-- Event handler + print("[BackgroundFetch] Event received $taskId"); + NotificationsHelper().backgroundJob(); + BackgroundFetch.finish(taskId); + }, (String taskId) async { // <-- Task timeout handler. + print("[BackgroundFetch] TASK TIMEOUT taskId: $taskId"); + BackgroundFetch.finish(taskId); + }); + print('[BackgroundFetch] configure success: $status'); + BackgroundFetch.scheduleTask(TaskConfig( + taskId: "com.transistorsoft.refilcnotification", + delay: 900000, // 15 minutes + periodic: true, + forceAlarmManager: true, + stopOnTerminate: false, + enableHeadless: true + )); + } @pragma('vm:entry-point') void backgroundHeadlessTask(HeadlessTask task) { + String taskId = task.taskId; + bool isTimeout = task.timeout; + if (isTimeout) { + if (kDebugMode) { + print("[BackgroundFetch] Headless task timed-out: $taskId"); + } + BackgroundFetch.finish(taskId); + return; + } if (kDebugMode) { print('[BackgroundFetch] Headless event received.'); } + NotificationsHelper().backgroundJob(); BackgroundFetch.finish(task.taskId); } diff --git a/filcnaplo/lib/ui/filter/widgets.dart b/filcnaplo/lib/ui/filter/widgets.dart index 9632adf..bce82ae 100644 --- a/filcnaplo/lib/ui/filter/widgets.dart +++ b/filcnaplo/lib/ui/filter/widgets.dart @@ -89,6 +89,7 @@ Future> getFilterWidgets(FilterType activeData, // Grades case FilterType.grades: + gradeProvider.seenAll(); items = grade_filter.getWidgets( gradeProvider.grades, gradeProvider.lastSeenDate); if (settingsProvider.gradeOpeningFun) { diff --git a/filcnaplo_kreta_api/lib/providers/grade_provider.dart b/filcnaplo_kreta_api/lib/providers/grade_provider.dart index 11b7a43..b86c440 100644 --- a/filcnaplo_kreta_api/lib/providers/grade_provider.dart +++ b/filcnaplo_kreta_api/lib/providers/grade_provider.dart @@ -51,7 +51,6 @@ class GradeProvider with ChangeNotifier { final userStore = _database.userStore; userStore.storeLastSeenGrade(DateTime.now(), userId: userId); _lastSeen = DateTime.now(); - notifyListeners(); } } diff --git a/filcnaplo_mobile_ui/lib/screens/navigation/navigation_screen.dart b/filcnaplo_mobile_ui/lib/screens/navigation/navigation_screen.dart index 03afda9..caec818 100755 --- a/filcnaplo_mobile_ui/lib/screens/navigation/navigation_screen.dart +++ b/filcnaplo_mobile_ui/lib/screens/navigation/navigation_screen.dart @@ -139,7 +139,6 @@ class NavigationScreenState extends State void initState() { super.initState(); - initPlatformState(); HomeWidget.setAppGroupId('hu.refilc.naplo.group'); diff --git a/filcnaplo_mobile_ui/lib/screens/settings/settings_screen.dart b/filcnaplo_mobile_ui/lib/screens/settings/settings_screen.dart index ca4e26a..c7df96a 100755 --- a/filcnaplo_mobile_ui/lib/screens/settings/settings_screen.dart +++ b/filcnaplo_mobile_ui/lib/screens/settings/settings_screen.dart @@ -156,7 +156,7 @@ class _SettingsScreenState extends State void initState() { super.initState(); Future.delayed(Duration.zero, () { - futureRelease = Provider.of(context).installedVersion(); + futureRelease = Provider.of(context, listen: false).installedVersion(); }); _hideContainersController = AnimationController( vsync: this, duration: const Duration(milliseconds: 200)); @@ -437,6 +437,61 @@ class _SettingsScreenState extends State activeColor: Theme.of(context).colorScheme.secondary, ), ), + Material( + type: MaterialType.transparency, + child: SwitchListTile( + value: settings.notificationsEnabled, + activeColor: Theme.of(context).colorScheme.secondary, + contentPadding: const EdgeInsets.only(left: 12.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0)), + title: Row(children: [ + Icon(FeatherIcons.messageSquare, + color: settings.notificationsEnabled ? Theme.of(context).colorScheme.secondary : AppColors.of(context).text.withOpacity(.25)), + const SizedBox(width: 14.0), + Text( + "notifications".i18n, + style: TextStyle( + color: AppColors.of(context).text.withOpacity( + settings.notificationsEnabled ? 1.0 : .5), + fontWeight: FontWeight.w600, + fontSize: 16.0, + ), + ), + SizedBox( + width: 5, + ), + SizedBox( + height: 30, + child: AnimatedContainer( + duration: Duration(milliseconds: 200), + child: Padding( + padding: + const EdgeInsets.only(left: 10, right: 10), + child: Center( + child: Text("BETA", + style: TextStyle( + fontSize: 9.1, + color: AppColors.of(context) + .text + .withOpacity( + settings.notificationsEnabled + ? 1.0 + : .5), + fontWeight: FontWeight.w600, + overflow: TextOverflow.ellipsis))), + ), + decoration: BoxDecoration( + color: AppColors.of(context).filc.withOpacity( + settings.notificationsEnabled ? 1.0 : .5), + borderRadius: BorderRadius.circular(40)), + ), + ) + ]), + onChanged: (value) => + settings.update(notificationsEnabled: value), + ), + ), ], ), ),