diff --git a/android/app/build.gradle b/android/app/build.gradle index b94b7a5e..cafe7f90 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -35,9 +35,10 @@ if (keystorePropertiesFile.exists()) { android { - compileSdkVersion flutter.compileSdkVersion + compileSdkVersion 33 compileOptions { + coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } @@ -83,4 +84,7 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation platform('com.google.firebase:firebase-bom:29.2.1') + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2' + implementation 'androidx.window:window:1.0.0' + implementation 'androidx.window:window-java:1.0.0' } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 9e68f23b..02ece270 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,10 +1,12 @@ + + - - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/ic_launcher.png b/android/app/src/main/res/drawable/ic_launcher.png new file mode 100644 index 00000000..cf7d7d3b Binary files /dev/null and b/android/app/src/main/res/drawable/ic_launcher.png differ diff --git a/lib/core/dependency_injection/injection_container.dart b/lib/core/dependency_injection/injection_container.dart index 6af5dac6..13a4b50a 100644 --- a/lib/core/dependency_injection/injection_container.dart +++ b/lib/core/dependency_injection/injection_container.dart @@ -18,7 +18,9 @@ import 'package:dairy_app/features/auth/presentation/bloc/user_config/user_confi import 'package:dairy_app/features/notes/data/datasources/local%20data%20sources/local_data_source.dart'; import 'package:dairy_app/features/notes/data/datasources/local%20data%20sources/local_data_source_template.dart'; import 'package:dairy_app/features/notes/data/repositories/notes_repository.dart'; +import 'package:dairy_app/features/notes/data/repositories/notifications_repository.dart'; import 'package:dairy_app/features/notes/domain/repositories/notes_repository.dart'; +import 'package:dairy_app/features/notes/domain/repositories/notifications_repository.dart'; import 'package:dairy_app/features/notes/presentation/bloc/notes/notes_bloc.dart'; import 'package:dairy_app/features/notes/presentation/bloc/notes_fetch/notes_fetch_cubit.dart'; import 'package:dairy_app/features/notes/presentation/bloc/selectable_list/selectable_list_cubit.dart'; @@ -29,6 +31,7 @@ import 'package:dairy_app/features/sync/data/datasources/temeplates/key_value_da import 'package:dairy_app/features/sync/data/repositories/sync_repository.dart'; import 'package:dairy_app/features/sync/domain/repositories/sync_repository_template.dart'; import 'package:dairy_app/features/sync/presentation/bloc/notes_sync/notesync_cubit.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:get_it/get_it.dart'; import 'package:internet_connection_checker/internet_connection_checker.dart'; @@ -119,6 +122,21 @@ Future init() async { sl.registerSingleton( NotesRepository(notesLocalDataSource: sl(), authSessionBloc: sl())); + sl.registerSingletonAsync(() async { + final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + const AndroidInitializationSettings initializationSettingsAndroid = + AndroidInitializationSettings('@mipmap/ic_launcher'); + + const InitializationSettings initializationSettings = + InitializationSettings( + android: initializationSettingsAndroid, + ); + + await flutterLocalNotificationsPlugin.initialize(initializationSettings); + return NotificationsRepository( + flutterLocalNotificationsPlugin: flutterLocalNotificationsPlugin); + }); + //* Blocs sl.registerLazySingleton(() => NotesBloc(notesRepository: sl())); sl.registerLazySingleton(() => NotesFetchCubit( diff --git a/lib/core/pages/settings_page.dart b/lib/core/pages/settings_page.dart index 26efd93d..9e5c2bd6 100644 --- a/lib/core/pages/settings_page.dart +++ b/lib/core/pages/settings_page.dart @@ -10,6 +10,7 @@ import 'package:dairy_app/core/widgets/version_number.dart'; import 'package:dairy_app/features/auth/presentation/bloc/auth_session/auth_session_bloc.dart'; import 'package:dairy_app/features/auth/presentation/widgets/security_settings.dart'; import 'package:dairy_app/features/auth/presentation/widgets/setup_account.dart'; +import 'package:dairy_app/features/notes/presentation/widgets/daily_reminders.dart'; import 'package:dairy_app/features/sync/presentation/widgets/sync_settings.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -127,6 +128,8 @@ class _SettingsPageState extends State { const SizedBox(height: 15), const AutoSaveToggleButton(), const SizedBox(height: 10), + const DailyReminders(), + const SizedBox(height: 10), const ThemeDropdown(), const SizedBox(height: 15), const SendFeedBack(), diff --git a/lib/features/auth/core/constants.dart b/lib/features/auth/core/constants.dart index 168ef4c1..b0b082fb 100644 --- a/lib/features/auth/core/constants.dart +++ b/lib/features/auth/core/constants.dart @@ -10,6 +10,8 @@ class UserConfigConstants { static String isAutoSyncEnabled = "is_auto_sync_enabled"; static String isFingerPrintLoginEnabled = "is_finger_print_log_enabled"; static String isAutoSaveEnabled = "is_auto_save_enabled"; + static String isDailyReminderEnabled = "is_daily_reminder_enabled"; + static String reminderTime = "reminder_time"; } class SyncConstants { diff --git a/lib/features/auth/data/models/user_config_model.dart b/lib/features/auth/data/models/user_config_model.dart index 8342c841..0762c1de 100644 --- a/lib/features/auth/data/models/user_config_model.dart +++ b/lib/features/auth/data/models/user_config_model.dart @@ -1,5 +1,6 @@ import 'package:dairy_app/features/auth/core/constants.dart'; import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; /// class to store non-critical properties of user /// it is stored apart from user table, which stores critical properties of user @@ -13,17 +14,22 @@ class UserConfigModel extends Equatable { final bool? isAutoSyncEnabled; final bool? isFingerPrintLoginEnabled; final bool? isAutoSaveEnabled; + final bool? isDailyReminderEnabled; + final TimeOfDay? reminderTime; - const UserConfigModel( - {required this.userId, - this.preferredSyncOption, - this.lastGoogleDriveSync, - this.lastDropboxSync, - this.googleDriveUserInfo, - this.dropBoxUserInfo, - this.isAutoSyncEnabled, - this.isFingerPrintLoginEnabled, - this.isAutoSaveEnabled}); + const UserConfigModel({ + required this.userId, + this.preferredSyncOption, + this.lastGoogleDriveSync, + this.lastDropboxSync, + this.googleDriveUserInfo, + this.dropBoxUserInfo, + this.isAutoSyncEnabled, + this.isFingerPrintLoginEnabled, + this.isAutoSaveEnabled, + this.isDailyReminderEnabled, + this.reminderTime, + }); @override List get props => [ @@ -35,9 +41,40 @@ class UserConfigModel extends Equatable { dropBoxUserInfo, isAutoSyncEnabled, isFingerPrintLoginEnabled, - isAutoSaveEnabled + isAutoSaveEnabled, + isDailyReminderEnabled, + reminderTime ]; + static TimeOfDay? getTimeOfDayFromTimeString(String? timeString) { + if (timeString != null) { + final parts = timeString.split(':'); + + if (parts.length == 2) { + final hour = int.parse(parts[0]); + final minute = int.parse(parts[1]); + return TimeOfDay(hour: hour, minute: minute); + } + + return null; + } + return null; + } + + static String? getTimeOfDayToString(TimeOfDay? time) { + if (time == null) { + return null; + } + + final hour = time.hour.toString(); + final minute = time.minute.toString(); + final formattedHour = hour.length == 1 ? hour.padLeft(2, '0') : hour; + final formattedMinute = + minute.length == 1 ? minute.padLeft(2, '0') : minute; + + return '$formattedHour:$formattedMinute'; + } + factory UserConfigModel.fromJson(Map jsonMap) { return UserConfigModel( userId: jsonMap[UserConfigConstants.userId], @@ -57,7 +94,11 @@ class UserConfigModel extends Equatable { isFingerPrintLoginEnabled: jsonMap[UserConfigConstants.isFingerPrintLoginEnabled], isAutoSaveEnabled: jsonMap[UserConfigConstants.isAutoSaveEnabled], - ); // default theme is coral bubbles + isDailyReminderEnabled: + jsonMap[UserConfigConstants.isDailyReminderEnabled], + reminderTime: + getTimeOfDayFromTimeString(jsonMap[UserConfigConstants.reminderTime]), + ); } Map toJson() { @@ -73,6 +114,8 @@ class UserConfigModel extends Equatable { UserConfigConstants.isAutoSyncEnabled: isAutoSyncEnabled, UserConfigConstants.isFingerPrintLoginEnabled: isFingerPrintLoginEnabled, UserConfigConstants.isAutoSaveEnabled: isAutoSaveEnabled, + UserConfigConstants.isDailyReminderEnabled: isDailyReminderEnabled, + UserConfigConstants.reminderTime: getTimeOfDayToString(reminderTime) }; } } diff --git a/lib/features/notes/data/repositories/notifications_repository.dart b/lib/features/notes/data/repositories/notifications_repository.dart new file mode 100644 index 00000000..253a9047 --- /dev/null +++ b/lib/features/notes/data/repositories/notifications_repository.dart @@ -0,0 +1,102 @@ +import 'package:dairy_app/core/logger/logger.dart'; +import 'package:dairy_app/features/notes/domain/repositories/notifications_repository.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_timezone/flutter_timezone.dart'; +import 'package:timezone/data/latest_all.dart' as tz; +import 'package:timezone/timezone.dart' as tz; + +final log = printer("NotificationsRepository"); + +class NotificationsRepository implements INotificationsRepository { + late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin; + + NotificationsRepository({required this.flutterLocalNotificationsPlugin}); + + tz.TZDateTime nextInstanceOfTime(TimeOfDay time, tz.Location localTimeZOne) { + final tz.TZDateTime now = tz.TZDateTime.now(localTimeZOne); + + tz.TZDateTime scheduledDate = tz.TZDateTime( + tz.local, now.year, now.month, now.day, time.hour, time.minute); + if (scheduledDate.isBefore(now)) { + scheduledDate = scheduledDate.add(const Duration(days: 1)); + } + + log.i("Scheduling alarm at $scheduledDate"); + return scheduledDate; + } + + @override + Future zonedScheduleNotification(TimeOfDay time) async { + try { + final permissionsEnabled = await areNotificationsEnabled(); + + log.w("permissions enabled = $permissionsEnabled"); + + if (!permissionsEnabled) { + // We can request permission from within the App for >= Android 13 + final arePermissionsGranted = await requestPermission(); + if (!arePermissionsGranted) { + throw Exception("Notification permissions are not enabled"); + } + } + + // inititalize time zones + tz.initializeTimeZones(); + final String? timeZoneName = await FlutterTimezone.getLocalTimezone(); + tz.setLocalLocation(tz.getLocation(timeZoneName!)); + + log.i("Local timezone = ${tz.local}"); + + // cancel all previously scheduled notifications before scheduling new ones + cancelAllNotifications(); + + await flutterLocalNotificationsPlugin.zonedSchedule( + 0, + 'Time to Journal!', + 'Take a few minutes to reflect on your day in your diary', + nextInstanceOfTime(time, tz.local), + const NotificationDetails( + android: AndroidNotificationDetails( + 'daily_reminder', + 'Daily reminders', + importance: Importance.high, + channelDescription: 'Daily Reminder Notifications', + ), + ), + androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, + matchDateTimeComponents: DateTimeComponents.time, + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.absoluteTime); + } catch (e) { + log.e(e); + rethrow; + } + } + + Future areNotificationsEnabled() async { + final bool granted = await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>() + ?.areNotificationsEnabled() ?? + false; + return granted; + } + + Future requestPermission() async { + final AndroidFlutterLocalNotificationsPlugin? androidImplementation = + flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>(); + + final bool grantedNotificationPermission = + await androidImplementation?.requestNotificationsPermission() ?? false; + + return grantedNotificationPermission; + } + + @override + Future cancelAllNotifications() async { + log.i("Cancelling all scheduled notifications"); + await flutterLocalNotificationsPlugin.cancelAll(); + } +} diff --git a/lib/features/notes/domain/repositories/notifications_repository.dart b/lib/features/notes/domain/repositories/notifications_repository.dart new file mode 100644 index 00000000..d3979e4a --- /dev/null +++ b/lib/features/notes/domain/repositories/notifications_repository.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +abstract class INotificationsRepository { + /// Schedules daily notification at [time] + Future zonedScheduleNotification(TimeOfDay time); + + /// Cancels/removes all notifications that have been scheduled and those + /// that have already been presented. + Future cancelAllNotifications(); +} diff --git a/lib/features/notes/presentation/widgets/daily_reminders.dart b/lib/features/notes/presentation/widgets/daily_reminders.dart new file mode 100644 index 00000000..3903c597 --- /dev/null +++ b/lib/features/notes/presentation/widgets/daily_reminders.dart @@ -0,0 +1,160 @@ +import 'package:dairy_app/app/themes/theme_extensions/note_create_page_theme_extensions.dart'; +import 'package:dairy_app/app/themes/theme_extensions/settings_page_theme_extensions.dart'; +import 'package:dairy_app/core/dependency_injection/injection_container.dart'; +import 'package:dairy_app/core/utils/utils.dart'; +import 'package:dairy_app/features/auth/core/constants.dart'; +import 'package:dairy_app/features/auth/data/models/user_config_model.dart'; +import 'package:dairy_app/features/auth/presentation/bloc/user_config/user_config_cubit.dart'; +import 'package:dairy_app/features/notes/domain/repositories/notifications_repository.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:simple_accordion/widgets/AccordionHeaderItem.dart'; +import 'package:simple_accordion/widgets/AccordionItem.dart'; +import 'package:simple_accordion/widgets/AccordionWidget.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class DailyReminders extends StatelessWidget { + const DailyReminders({Key? key}) : super(key: key); + + String getSubtitle(bool? isDailyReminderEnabled, TimeOfDay? reminderTime, + BuildContext context) { + if (isDailyReminderEnabled == null || isDailyReminderEnabled == false) { + return AppLocalizations.of(context).notificationsNotEnabled; + } + + if (reminderTime == null) { + return AppLocalizations.of(context).notificationTimeNotEnabled; + } + + return AppLocalizations.of(context).youWillBeNotifiedAt( + UserConfigModel.getTimeOfDayToString(reminderTime)!); + } + + @override + Widget build(BuildContext context) { + final userConfigCubit = BlocProvider.of(context); + + final mainTextColor = Theme.of(context) + .extension()! + .mainTextColor; + + final inactiveTrackColor = Theme.of(context) + .extension()! + .inactiveTrackColor; + + final activeColor = + Theme.of(context).extension()!.activeColor; + + return BlocBuilder( + builder: (context, state) { + final isDailyReminderEnabled = + state.userConfigModel?.isDailyReminderEnabled; + + final reminderTime = state.userConfigModel?.reminderTime; + + final INotificationsRepository notificationsRepository = + sl(); + + return SimpleAccordion( + headerColor: mainTextColor, + headerTextStyle: TextStyle( + color: mainTextColor, + fontSize: 16, + ), + children: [ + AccordionHeaderItem( + title: AppLocalizations.of(context).dailyReminders, + children: [ + AccordionItem( + child: Padding( + padding: const EdgeInsets.only(top: 10), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SwitchListTile( + inactiveTrackColor: inactiveTrackColor, + activeColor: activeColor, + contentPadding: const EdgeInsets.all(0.0), + title: Text( + AppLocalizations.of(context).enableDailyReminders, + style: TextStyle(color: mainTextColor), + ), + subtitle: Text( + AppLocalizations.of(context).getDailyReminders, + style: TextStyle(color: mainTextColor), + ), + value: isDailyReminderEnabled ?? false, + onChanged: (value) async { + try { + if (value == true) { + if (reminderTime != null) { + await notificationsRepository + .zonedScheduleNotification(reminderTime); + } + } else { + await notificationsRepository + .cancelAllNotifications(); + } + + userConfigCubit.setUserConfig( + UserConfigConstants.isDailyReminderEnabled, + value, + ); + } on Exception catch (e) { + showToast( + e.toString().replaceAll("Exception: ", "")); + } + }, + ), + ListTile( + contentPadding: const EdgeInsets.only(right: 10.0), + title: Text( + AppLocalizations.of(context).chooseTime, + style: TextStyle(color: mainTextColor), + ), + subtitle: Text( + getSubtitle( + isDailyReminderEnabled, reminderTime, context), + style: TextStyle(color: mainTextColor), + ), + trailing: Icon( + Icons.alarm, + color: mainTextColor, + ), + onTap: () async { + TimeOfDay selectedTime = TimeOfDay.now(); + + final TimeOfDay? pickedTime = await showTimePicker( + context: context, + initialTime: selectedTime, + ); + + if (pickedTime != null) { + // Handle the selected time + userConfigCubit.setUserConfig( + UserConfigConstants.reminderTime, + UserConfigModel.getTimeOfDayToString( + pickedTime), + ); + try { + // if notifications are enabled, then new schedule new notification at this time + notificationsRepository + .zonedScheduleNotification(pickedTime); + } on Exception catch (e) { + showToast( + e.toString().replaceAll("Exception: ", "")); + } + } + }, + ) + ], + ), + ), + ), + ], + ) + ]); + }); + } +} diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 2b99e1f6..d0c7531f 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -76,6 +76,22 @@ "delete":"Delete", "cancel":"Cancel", "submit":"Submit", - "appDescription":"Discover diaryVault - a diary app designed to help you capture your thoughts, memories, and moments effortlessly. Available now on the Play Store!" - + "appDescription":"Discover diaryVault - a diary app designed to help you capture your thoughts, memories, and moments effortlessly. Available now on the Play Store!", + "notificationsNotEnabled":"Notifications are not enabled", + "notificationTimeNotEnabled":"You haven't selected a notification time", + "youWillBeNotifiedAt":"You will be notified at {time}", + "@youWillBeNotifiedAt":{ + "placeholders":{ + "time": { + "type": "String", + "example":"in sometime" + } + } + }, + "enableDailyReminders":"Enable Daily Reminders", + "dailyReminders":"Daily Reminders", + "getDailyReminders":"Get daily reminders at your chosen time to keep your journal up to date.", + "chooseTime":"Choose Time", + "notificationTitle1":"Time to Journal!", + "notificationDescription1":"Take a few minutes to reflect on your day in your diary" } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 15de9b7b..ddd33903 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,9 @@ import device_info_plus import file_selector_macos import firebase_auth import firebase_core +import flutter_local_notifications +import flutter_secure_storage_macos +import flutter_timezone import flutter_secure_storage_macos import flutter_tts import flutter_web_auth_2 @@ -27,6 +30,9 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) + FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) + FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterTtsPlugin.register(with: registry.registrar(forPlugin: "FlutterTtsPlugin")) FlutterWebAuth2Plugin.register(with: registry.registrar(forPlugin: "FlutterWebAuth2Plugin")) diff --git a/pubspec.lock b/pubspec.lock index e6a13915..730382e9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -257,6 +257,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.10.1" + dbus: + dependency: transitive + description: + name: dbus + sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" + url: "https://pub.dev" + source: hosted + version: "0.7.8" device_info_plus: dependency: transitive description: @@ -523,6 +531,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: "6d11ea777496061e583623aaf31923f93a9409ef8fcaeeefdd6cd78bf4fe5bb3" + url: "https://pub.dev" + source: hosted + version: "16.1.0" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" + url: "https://pub.dev" + source: hosted + version: "4.0.0+1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "7cf643d6d5022f3baed0be777b0662cce5919c0a7b86e700299f22dc4ae660ef" + url: "https://pub.dev" + source: hosted + version: "7.0.0+1" flutter_localizations: dependency: "direct main" description: flutter @@ -627,6 +659,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_timezone: + dependency: "direct main" + description: + name: flutter_timezone + sha256: "06b35132c98fa188db3c4b654b7e1af7ccd01dfe12a004d58be423357605fb24" + url: "https://pub.dev" + source: hosted + version: "1.0.8" flutter_tts: dependency: "direct main" description: @@ -1533,6 +1573,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.3" + timezone: + dependency: transitive + description: + name: timezone + sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0" + url: "https://pub.dev" + source: hosted + version: "0.9.2" timing: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c96da3df..d5e99690 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,15 +38,17 @@ dependencies: firebase_auth: ^4.7.3 flutter: sdk: flutter - flutter_localizations: - sdk: flutter flutter_bloc: ^8.0.1 flutter_dotenv: ^5.1.0 + flutter_local_notifications: ^16.1.0 + flutter_localizations: + sdk: flutter flutter_quill: path: ./packages/flutter_quill flutter_quill_extensions: path: ./packages/flutter_quill/flutter_quill_extensions flutter_secure_storage: ^9.0.0 + flutter_timezone: ^1.0.8 flutter_tts: ^3.8.3 flutter_web_auth_2: ^2.2.1 fluttertoast: ^8.0.9 @@ -95,11 +97,10 @@ dev_dependencies: # android_12: # image: 'assets/images/splash_screen.png' dependency_overrides: - ? analyzer - - # For information on the generic Dart part of this file, see the - # following page: https://dart.dev/tools/pub/pubspec - # The following section is specific to Flutter. + # For information on the generic Dart part of this file, see the + # following page: https://dart.dev/tools/pub/pubspec + # The following section is specific to Flutter. + analyzer: flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in @@ -135,4 +136,3 @@ flutter: # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages -