diff --git a/lib/app/modules/addOrUpdateAlarm/controllers/add_or_update_alarm_controller.dart b/lib/app/modules/addOrUpdateAlarm/controllers/add_or_update_alarm_controller.dart index baf81538..40f4d907 100644 --- a/lib/app/modules/addOrUpdateAlarm/controllers/add_or_update_alarm_controller.dart +++ b/lib/app/modules/addOrUpdateAlarm/controllers/add_or_update_alarm_controller.dart @@ -127,6 +127,18 @@ class AddOrUpdateAlarmController extends GetxController { RxBool isWeekdaysSelected = false.obs; RxBool isCustomSelected = false.obs; RxBool isPlaying = false.obs; // Observable boolean to track playing state + + final Map previousSettings = {}.obs; + + + void storePreviousSetting(String key, dynamic value) { + previousSettings[key] = value; + } + + + dynamic getPreviousSetting(String key) { + return previousSettings[key]; + } // to check whether alarm data is updated or not Map initialValues = {}; @@ -155,6 +167,18 @@ class AddOrUpdateAlarmController extends GetxController { if (value == true) { isCustomSelected.value = false; isWeekdaysSelected.value = false; + + if (isFutureDate.value) { + isFutureDate.value = false; + Get.snackbar( + 'Specific Date Disabled', + 'Ring On specific date has been disabled since repeat pattern is selected.', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + } } } @@ -167,6 +191,18 @@ class AddOrUpdateAlarmController extends GetxController { if (value == true) { isCustomSelected.value = false; isDailySelected.value = false; + + if (isFutureDate.value) { + isFutureDate.value = false; + Get.snackbar( + 'Specific Date Disabled', + 'Ring On specific date has been disabled since repeat pattern is selected.', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + } } } @@ -175,6 +211,18 @@ class AddOrUpdateAlarmController extends GetxController { if (value == true) { isWeekdaysSelected.value = false; isDailySelected.value = false; + + if (isFutureDate.value) { + isFutureDate.value = false; + Get.snackbar( + 'Specific Date Disabled', + 'Ring On specific date has been disabled since repeat pattern is selected.', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + } } } @@ -740,6 +788,8 @@ class AddOrUpdateAlarmController extends GetxController { timeToAlarm.value = Utils.timeUntilAlarm( TimeOfDay.fromDateTime(selectedTime.value), repeatDays, + ringOn: isFutureDate.value, + alarmDate: selectedDate.value.toString().substring(0, 11), ); repeatDays.value = alarmRecord.value.days; @@ -850,6 +900,8 @@ class AddOrUpdateAlarmController extends GetxController { timeToAlarm.value = Utils.timeUntilAlarm( TimeOfDay.fromDateTime(selectedTime.value), repeatDays, + ringOn: isFutureDate.value, + alarmDate: selectedDate.value.toString().substring(0, 11), ); // store initial values of the variables @@ -891,12 +943,26 @@ class AddOrUpdateAlarmController extends GetxController { } void addListeners() { - // Updating UI to show time to alarm - selectedTime.listen((time) { - debugPrint('CHANGED CHANGED CHANGED CHANGED'); - timeToAlarm.value = - Utils.timeUntilAlarm(TimeOfDay.fromDateTime(time), repeatDays); - _compareAndSetChange('selectedTime', time); + ever(selectedTime, (DateTime time) { + if (time != initialValues['selectedTime']) { + debugPrint('CHANGED CHANGED CHANGED CHANGED'); + timeToAlarm.value = Utils.timeUntilAlarm( + TimeOfDay.fromDateTime(time), + repeatDays, + ringOn: isFutureDate.value, + alarmDate: selectedDate.value.toString().substring(0, 11) + ); + _compareAndSetChange('selectedTime', time); + } + }); + + ever(isFutureDate, (bool enabled) { + timeToAlarm.value = Utils.timeUntilAlarm( + TimeOfDay.fromDateTime(selectedTime.value), + repeatDays, + ringOn: enabled, + alarmDate: selectedDate.value.toString().substring(0, 11), + ); }); //Updating UI to show repeated days @@ -1261,8 +1327,47 @@ class AddOrUpdateAlarmController extends GetxController { lastDate: DateTime.now().add(const Duration(days: 355)), )) ?? DateTime.now(); - isFutureDate.value = - selectedDate.value.difference(DateTime.now()).inHours > 0; + + bool newFutureDate = selectedDate.value.difference(DateTime.now()).inHours > 0; + + + if (newFutureDate && !isFutureDate.value) { + + List oldRepeatDays = List.from(repeatDays); + + + for (int i = 0; i < repeatDays.length; i++) { + repeatDays[i] = false; + } + + + isDailySelected.value = false; + isWeekdaysSelected.value = false; + isCustomSelected.value = false; + + + if (oldRepeatDays.any((enabled) => enabled)) { + Get.snackbar( + 'Repeat Pattern Disabled', + 'Repeat pattern has been disabled since specific date is selected.', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + } + } + + + isFutureDate.value = newFutureDate; + + + timeToAlarm.value = Utils.timeUntilAlarm( + TimeOfDay.fromDateTime(selectedTime.value), + repeatDays, + ringOn: isFutureDate.value, + alarmDate: selectedDate.value.toString().substring(0, 11), + ); } void showToast({ @@ -1408,7 +1513,6 @@ class AddOrUpdateAlarmController extends GetxController { ); } } -} int orderedCountryCode(Country countryA, Country countryB) { // `??` for null safety of 'dialCode' @@ -1417,3 +1521,4 @@ class AddOrUpdateAlarmController extends GetxController { return int.parse(dialCodeA).compareTo(int.parse(dialCodeB)); } +} diff --git a/lib/app/modules/addOrUpdateAlarm/views/add_or_update_alarm_view.dart b/lib/app/modules/addOrUpdateAlarm/views/add_or_update_alarm_view.dart index 3a33c30b..c29c7f48 100644 --- a/lib/app/modules/addOrUpdateAlarm/views/add_or_update_alarm_view.dart +++ b/lib/app/modules/addOrUpdateAlarm/views/add_or_update_alarm_view.dart @@ -32,8 +32,8 @@ import 'package:ultimate_alarm_clock/app/modules/settings/controllers/theme_cont import 'package:ultimate_alarm_clock/app/utils/constants.dart'; import 'package:ultimate_alarm_clock/app/utils/utils.dart'; import '../controllers/add_or_update_alarm_controller.dart'; -import 'alarm_date_tile.dart'; import 'guardian_angel.dart'; +import 'scheduling_options_tile.dart'; class AddOrUpdateAlarmView extends GetView { AddOrUpdateAlarmView({super.key}) { @@ -792,15 +792,7 @@ class AddOrUpdateAlarmView extends GetView { () => controller.alarmSettingType.value == 0 ? Column( children: [ - AlarmDateTile( - controller: controller, - themeController: themeController, - ), - Divider( - color: themeController - .primaryDisabledTextColor.value, - ), - RepeatTile( + SchedulingOptionsTile( controller: controller, themeController: themeController, ), diff --git a/lib/app/modules/addOrUpdateAlarm/views/guardian_angel.dart b/lib/app/modules/addOrUpdateAlarm/views/guardian_angel.dart index 51842047..5d4be8cb 100644 --- a/lib/app/modules/addOrUpdateAlarm/views/guardian_angel.dart +++ b/lib/app/modules/addOrUpdateAlarm/views/guardian_angel.dart @@ -50,7 +50,7 @@ class GuardianAngel extends StatelessWidget { setSelectorButtonAsPrefixIcon: true, leadingPadding: 0, trailingSpace: false, - countryComparator: orderedCountryCode, + countryComparator: null, ), ), ), diff --git a/lib/app/modules/addOrUpdateAlarm/views/repeat_tile.dart b/lib/app/modules/addOrUpdateAlarm/views/repeat_tile.dart index 1c2a3432..4854cd3a 100644 --- a/lib/app/modules/addOrUpdateAlarm/views/repeat_tile.dart +++ b/lib/app/modules/addOrUpdateAlarm/views/repeat_tile.dart @@ -200,6 +200,18 @@ class RepeatTile extends StatelessWidget { onTap: () { Utils.hapticFeedback(); controller.repeatDays[dayIndex] = !controller.repeatDays[dayIndex]; + + if (controller.repeatDays[dayIndex] && controller.isFutureDate.value) { + controller.isFutureDate.value = false; + Get.snackbar( + 'Specific Date Disabled', + 'Ring On specific date has been disabled since repeat pattern is selected.', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + } }, child: Padding( padding: const EdgeInsets.only(left: 10.0), @@ -218,6 +230,18 @@ class RepeatTile extends StatelessWidget { onChanged: (value) { Utils.hapticFeedback(); controller.repeatDays[dayIndex] = value!; + + if (value && controller.isFutureDate.value) { + controller.isFutureDate.value = false; + Get.snackbar( + 'Specific Date Disabled', + 'Ring On specific date has been disabled since repeat pattern is selected.', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + } }, ), Text( diff --git a/lib/app/modules/addOrUpdateAlarm/views/scheduling_options_tile.dart b/lib/app/modules/addOrUpdateAlarm/views/scheduling_options_tile.dart new file mode 100644 index 00000000..43f90eda --- /dev/null +++ b/lib/app/modules/addOrUpdateAlarm/views/scheduling_options_tile.dart @@ -0,0 +1,422 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ultimate_alarm_clock/app/utils/constants.dart'; +import 'package:ultimate_alarm_clock/app/utils/utils.dart'; + +import '../../settings/controllers/theme_controller.dart'; +import '../controllers/add_or_update_alarm_controller.dart'; + +class SchedulingOptionsTile extends StatelessWidget { + const SchedulingOptionsTile({ + super.key, + required this.controller, + required this.themeController, + }); + + final AddOrUpdateAlarmController controller; + final ThemeController themeController; + + @override + Widget build(BuildContext context) { + return Obx(() => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Text( + "Schedule Alarm", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: themeController.primaryTextColor.value, + ), + ), + ), + + Container( + decoration: BoxDecoration( + color: themeController.secondaryBackgroundColor.value, + borderRadius: BorderRadius.circular(8), + ), + margin: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Expanded( + child: InkWell( + onTap: () { + if (controller.repeatDays.any((day) => day)) { + final List oldRepeatDays = List.from(controller.repeatDays); + final bool oldIsDailySelected = controller.isDailySelected.value; + final bool oldIsWeekdaysSelected = controller.isWeekdaysSelected.value; + final bool oldIsCustomSelected = controller.isCustomSelected.value; + + for (int i = 0; i < controller.repeatDays.length; i++) { + controller.repeatDays[i] = false; + } + + + controller.isDailySelected.value = false; + controller.isWeekdaysSelected.value = false; + controller.isCustomSelected.value = false; + + Get.snackbar( + 'Repeat Pattern Disabled', + 'Switching to one-time scheduling', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange, + colorText: Colors.white, + duration: const Duration(seconds: 4), + mainButton: TextButton( + child: const Text('UNDO', style: TextStyle(color: Colors.white)), + onPressed: () { + for (int i = 0; i < controller.repeatDays.length; i++) { + controller.repeatDays[i] = oldRepeatDays[i]; + } + controller.isDailySelected.value = oldIsDailySelected; + controller.isWeekdaysSelected.value = oldIsWeekdaysSelected; + controller.isCustomSelected.value = oldIsCustomSelected; + controller.isFutureDate.value = false; + }, + ), + ); + } + + controller.isFutureDate.value = true; + controller.datePicker(context); + }, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: controller.isFutureDate.value + ? kprimaryColor + : Colors.transparent, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + "One-time", + textAlign: TextAlign.center, + style: TextStyle( + color: controller.isFutureDate.value + ? Colors.black + : themeController.primaryTextColor.value, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + + Expanded( + child: InkWell( + onTap: () { + if (controller.isFutureDate.value) { + final bool oldIsFutureDate = controller.isFutureDate.value; + final DateTime oldSelectedDate = controller.selectedDate.value; + + + controller.isFutureDate.value = false; + + + Get.snackbar( + 'One-time Schedule Disabled', + 'Switching to recurring scheduling', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange, + colorText: Colors.white, + duration: const Duration(seconds: 4), + mainButton: TextButton( + child: const Text('UNDO', style: TextStyle(color: Colors.white)), + onPressed: () { + + controller.isFutureDate.value = oldIsFutureDate; + controller.selectedDate.value = oldSelectedDate; + + + for (int i = 0; i < controller.repeatDays.length; i++) { + controller.repeatDays[i] = false; + } + controller.isDailySelected.value = false; + controller.isWeekdaysSelected.value = false; + controller.isCustomSelected.value = false; + }, + ), + ); + } + + + if (!controller.repeatDays.any((day) => day)) { + controller.setIsWeekdaysSelected(true); + } + + + _showRepeatOptions(context); + }, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: controller.repeatDays.any((day) => day) + ? kprimaryColor + : Colors.transparent, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + "Recurring", + textAlign: TextAlign.center, + style: TextStyle( + color: controller.repeatDays.any((day) => day) + ? Colors.black + : themeController.primaryTextColor.value, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ], + ), + ), + + const SizedBox(height: 16), + + controller.isFutureDate.value + ? _buildOneTimeContent(context) + : _buildRecurringContent(context), + + const SizedBox(height: 8), + + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: Row( + children: [ + Icon( + Icons.access_time, + size: 16, + color: themeController.primaryDisabledTextColor.value, + ), + const SizedBox(width: 8), + Obx(() => Text( + "Rings in: ${controller.timeToAlarm}", + style: TextStyle( + fontSize: 12, + color: themeController.primaryDisabledTextColor.value, + ), + )), + ], + ), + ), + ], + )); + } + + Widget _buildOneTimeContent(BuildContext context) { + return ListTile( + onTap: () => controller.datePicker(context), + title: Text( + "Date", + style: TextStyle( + color: themeController.primaryTextColor.value, + ), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + controller.selectedDate.value.toString().substring(0, 11), + style: TextStyle( + color: themeController.primaryTextColor.value, + ), + ), + Icon( + Icons.chevron_right, + color: themeController.primaryTextColor.value, + ), + ], + ), + ); + } + + Widget _buildRecurringContent(BuildContext context) { + return ListTile( + title: Text( + "Pattern", + style: TextStyle( + color: themeController.primaryTextColor.value, + ), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + controller.daysRepeating.value, + style: TextStyle( + color: themeController.primaryTextColor.value, + ), + ), + Icon( + Icons.chevron_right, + color: themeController.primaryTextColor.value, + ), + ], + ), + onTap: () => _showRepeatOptions(context), + ); + } + + void _showRepeatOptions(BuildContext context) { + List repeatDays = List.filled(7, false); + + Get.bottomSheet( + BottomSheet( + onClosing: () {}, + backgroundColor: themeController.secondaryBackgroundColor.value, + builder: (BuildContext context) { + return SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + "Repeat Pattern", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: themeController.primaryTextColor.value, + ), + ), + ), + Divider(color: themeController.primaryDisabledTextColor.value), + _buildPatternOption( + "Daily", + controller.isDailySelected, + (value) { + Utils.hapticFeedback(); + controller.setIsDailySelected(value); + for (int i = 0; i < controller.repeatDays.length; i++) { + controller.repeatDays[i] = value; + } + }, + ), + Divider(color: themeController.primaryDisabledTextColor.value), + _buildPatternOption( + "Weekdays", + controller.isWeekdaysSelected, + (value) { + Utils.hapticFeedback(); + controller.setIsWeekdaysSelected(value); + for (int i = 0; i < controller.repeatDays.length; i++) { + controller.repeatDays[i] = value && i >= 0 && i <= 4; + } + }, + ), + Divider(color: themeController.primaryDisabledTextColor.value), + _buildPatternOption( + "Custom", + controller.isCustomSelected, + (value) { + Utils.hapticFeedback(); + controller.setIsCustomSelected(value); + if (value) { + // Show day picker dialog + Get.back(); + _showDayPickerDialog(context); + } + }, + ), + const SizedBox(height: 20), + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: kprimaryColor, + padding: const EdgeInsets.symmetric(vertical: 12), + ), + onPressed: () => Get.back(), + child: Text( + "Done", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + const SizedBox(height: 20), + ], + ), + ); + }, + ), + ); + } + + Widget _buildPatternOption(String title, RxBool value, Function(bool) onChanged) { + return Obx(() => ListTile( + title: Text( + title, + style: TextStyle( + color: themeController.primaryTextColor.value, + ), + ), + trailing: Checkbox( + value: value.value, + activeColor: kprimaryColor, + onChanged: (val) => onChanged(val!), + ), + onTap: () => onChanged(!value.value), + )); + } + + void _showDayPickerDialog(BuildContext context) { + Get.dialog( + AlertDialog( + backgroundColor: themeController.secondaryBackgroundColor.value, + title: Text( + "Select Days", + style: TextStyle( + color: themeController.primaryTextColor.value, + ), + ), + content: Container( + width: double.maxFinite, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildDayCheckbox(0, "Monday"), + _buildDayCheckbox(1, "Tuesday"), + _buildDayCheckbox(2, "Wednesday"), + _buildDayCheckbox(3, "Thursday"), + _buildDayCheckbox(4, "Friday"), + _buildDayCheckbox(5, "Saturday"), + _buildDayCheckbox(6, "Sunday"), + ], + ), + ), + actions: [ + TextButton( + child: Text("Done", style: TextStyle(color: kprimaryColor)), + onPressed: () => Get.back(), + ), + ], + ), + ); + } + + Widget _buildDayCheckbox(int dayIndex, String dayName) { + return Obx(() => CheckboxListTile( + title: Text( + dayName, + style: TextStyle( + color: themeController.primaryTextColor.value, + ), + ), + value: controller.repeatDays[dayIndex], + activeColor: kprimaryColor, + onChanged: (bool? value) { + Utils.hapticFeedback(); + controller.repeatDays[dayIndex] = value!; + }, + )); + } +} \ No newline at end of file diff --git a/lib/app/modules/home/controllers/home_controller.dart b/lib/app/modules/home/controllers/home_controller.dart index 449ca94f..3d6cef1d 100644 --- a/lib/app/modules/home/controllers/home_controller.dart +++ b/lib/app/modules/home/controllers/home_controller.dart @@ -327,6 +327,8 @@ class HomeController extends GetxController { String timeToAlarm = Utils.timeUntilAlarm( Utils.stringToTimeOfDay(latestAlarm.alarmTime), latestAlarm.days, + ringOn: latestAlarm.ringOn, + alarmDate: latestAlarm.alarmDate, ); alarmTime.value = 'Rings in $timeToAlarm'; // This function is necessary when alarms are deleted/enabled @@ -362,6 +364,8 @@ class HomeController extends GetxController { timeToAlarm = Utils.timeUntilAlarm( Utils.stringToTimeOfDay(latestAlarm.alarmTime), latestAlarm.days, + ringOn: latestAlarm.ringOn, + alarmDate: latestAlarm.alarmDate, ); alarmTime.value = 'Rings in $timeToAlarm'; @@ -377,6 +381,8 @@ class HomeController extends GetxController { timeToAlarm = Utils.timeUntilAlarm( Utils.stringToTimeOfDay(latestAlarm.alarmTime), latestAlarm.days, + ringOn: latestAlarm.ringOn, + alarmDate: latestAlarm.alarmDate, ); alarmTime.value = 'Rings in $timeToAlarm'; }); diff --git a/lib/app/utils/utils.dart b/lib/app/utils/utils.dart index 53c16127..50917fa1 100644 --- a/lib/app/utils/utils.dart +++ b/lib/app/utils/utils.dart @@ -270,8 +270,30 @@ class Utils { return deg * (pi / 180); } - static String timeUntilAlarm(TimeOfDay alarmTime, List days) { + static String timeUntilAlarm(TimeOfDay alarmTime, List days, {bool? ringOn, String? alarmDate}) { final now = DateTime.now(); + + if (ringOn == true && alarmDate != null) { + final targetDate = stringToDate(alarmDate); + final targetAlarm = DateTime( + targetDate.year, + targetDate.month, + targetDate.day, + alarmTime.hour, + alarmTime.minute, + ); + + Duration duration = targetAlarm.difference(now); + + if (duration.isNegative) { + return 'No upcoming alarms'; + } + + + return _formatAlarmDuration(duration); + } + + final todayAlarm = DateTime( now.year, now.month, @@ -321,6 +343,10 @@ class Utils { } } + return _formatAlarmDuration(duration); + } + + static String _formatAlarmDuration(Duration duration) { if (duration.inMinutes < 1) { return 'less than 1 minute'; } else if (duration.inHours < 24) {