diff --git a/lib/components/beacon_card.dart b/lib/components/beacon_card.dart index 4cdb9526..bc2953a6 100644 --- a/lib/components/beacon_card.dart +++ b/lib/components/beacon_card.dart @@ -1,4 +1,5 @@ import 'package:beacon/components/active_beacon.dart'; +import 'package:beacon/components/dialog_boxes.dart'; import 'package:beacon/components/timer.dart'; import 'package:beacon/locator.dart'; import 'package:beacon/models/beacon/beacon.dart'; @@ -122,6 +123,17 @@ class BeaconCustomWidgets { 'Expires At: ${DateFormat("hh:mm a, d/M/y").format(DateTime.fromMillisecondsSinceEpoch(beacon.expiresAt)).toString()}', style: Style.commonTextStyle) : Container(), + SizedBox(height: 4.0), + TextButton.icon( + onPressed: () async { + return await DialogBoxes.setReminderDialogueBox( + context, + beacon, + ); + }, + icon: Icon(Icons.alarm), + label: Text('Set Reminders!'), + ), ], ) : (willStart) @@ -200,6 +212,17 @@ class BeaconCustomWidgets { 'Expires At: ${DateFormat("hh:mm a, d/M/y").format(DateTime.fromMillisecondsSinceEpoch(beacon.expiresAt)).toString()}', style: Style.commonTextStyle) : Container(), + SizedBox(height: 4.0), + TextButton.icon( + onPressed: () async { + return await DialogBoxes.setReminderDialogueBox( + context, + beacon, + ); + }, + icon: Icon(Icons.alarm), + label: Text('Set Reminders!'), + ), ], ) : Column( @@ -244,6 +267,7 @@ class BeaconCustomWidgets { 'Expired At: ${DateFormat("hh:mm a, d/M/y").format(DateTime.fromMillisecondsSinceEpoch(beacon.expiresAt)).toString()}', style: Style.commonTextStyle) : Container(), + SizedBox(height: 4.0), ], ), ], @@ -271,90 +295,91 @@ class BeaconCustomWidgets { static ListView getPlaceholder() { final BorderRadius borderRadius = BorderRadius.circular(10.0); return ListView.builder( - scrollDirection: Axis.vertical, - physics: BouncingScrollPhysics(), - itemCount: 3, - padding: const EdgeInsets.all(8.0), - itemBuilder: (BuildContext context, int index) { - return Container( - margin: const EdgeInsets.symmetric( - vertical: 10.0, - horizontal: 10.0, - ), - height: 110, - decoration: BoxDecoration( - color: kBlue, - shape: BoxShape.rectangle, - borderRadius: BorderRadius.circular(8.0), - boxShadow: [ - BoxShadow( - color: Colors.black26, - blurRadius: 10.0, - offset: Offset(0.0, 10.0), - ), - ], - ), - padding: - EdgeInsets.only(left: 16.0, right: 16.0, bottom: 10, top: 10), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 15.0, bottom: 10.0, right: 15.0), - child: ClipRRect( - borderRadius: borderRadius, - child: SkeletonAnimation( - child: Container( - height: 15.0, - decoration: BoxDecoration(color: shimmerSkeletonColor), - ), + scrollDirection: Axis.vertical, + physics: BouncingScrollPhysics(), + itemCount: 3, + padding: const EdgeInsets.all(8.0), + itemBuilder: (BuildContext context, int index) { + return Container( + margin: const EdgeInsets.symmetric( + vertical: 10.0, + horizontal: 10.0, + ), + height: 110, + decoration: BoxDecoration( + color: kBlue, + shape: BoxShape.rectangle, + borderRadius: BorderRadius.circular(8.0), + boxShadow: [ + BoxShadow( + color: Colors.black26, + blurRadius: 10.0, + offset: Offset(0.0, 10.0), + ), + ], + ), + padding: + EdgeInsets.only(left: 16.0, right: 16.0, bottom: 10, top: 10), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 15.0, bottom: 10.0, right: 15.0), + child: ClipRRect( + borderRadius: borderRadius, + child: SkeletonAnimation( + child: Container( + height: 15.0, + decoration: BoxDecoration(color: shimmerSkeletonColor), ), ), ), - Padding( - padding: const EdgeInsets.only( - left: 15.0, right: 30.0, bottom: 10.0), - child: ClipRRect( - borderRadius: borderRadius, - child: SkeletonAnimation( - child: Container( - height: 10.0, - decoration: BoxDecoration(color: shimmerSkeletonColor), - ), + ), + Padding( + padding: const EdgeInsets.only( + left: 15.0, right: 30.0, bottom: 10.0), + child: ClipRRect( + borderRadius: borderRadius, + child: SkeletonAnimation( + child: Container( + height: 10.0, + decoration: BoxDecoration(color: shimmerSkeletonColor), ), ), ), - Padding( - padding: const EdgeInsets.only( - left: 15.0, right: 45.0, bottom: 10.0), - child: ClipRRect( - borderRadius: BorderRadius.circular(10.0), - child: SkeletonAnimation( - child: Container( - height: 10.0, - decoration: BoxDecoration(color: shimmerSkeletonColor), - ), + ), + Padding( + padding: const EdgeInsets.only( + left: 15.0, right: 45.0, bottom: 10.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(10.0), + child: SkeletonAnimation( + child: Container( + height: 10.0, + decoration: BoxDecoration(color: shimmerSkeletonColor), ), ), ), - Padding( - padding: const EdgeInsets.only(left: 15.0, right: 60.0), - child: ClipRRect( - borderRadius: BorderRadius.circular(10.0), - child: SkeletonAnimation( - child: Container( - height: 10.0, - decoration: BoxDecoration(color: shimmerSkeletonColor), - ), + ), + Padding( + padding: const EdgeInsets.only(left: 15.0, right: 60.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(10.0), + child: SkeletonAnimation( + child: Container( + height: 10.0, + decoration: BoxDecoration(color: shimmerSkeletonColor), ), ), ), - ], - ), - ); - }); + ), + ], + ), + ); + }, + ); } } diff --git a/lib/components/dialog_boxes.dart b/lib/components/dialog_boxes.dart index 6824be78..19f7a1c6 100644 --- a/lib/components/dialog_boxes.dart +++ b/lib/components/dialog_boxes.dart @@ -1,5 +1,6 @@ import 'package:beacon/components/hike_button.dart'; import 'package:beacon/locator.dart'; +import 'package:beacon/models/beacon/beacon.dart'; import 'package:beacon/utilities/constants.dart'; import 'package:flutter/material.dart'; import 'package:sizer/sizer.dart'; @@ -95,4 +96,226 @@ class DialogBoxes { ), ); } + + static Future setReminderDialogueBox( + BuildContext context, + Beacon beacon, + ) async { + DateTime dateTime; + TimeOfDay timeOfDay; + var startsAtDate = TextEditingController(); + var startsAtTime = TextEditingController(); + var title = TextEditingController(); + var message = TextEditingController(); + return await showDialog( + context: context, + builder: (context) => Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + child: Container( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 2.h, + ), + Container( + child: Text( + 'Set reminders for yourself!', + style: TextStyle(color: kYellow, fontSize: 13.0), + ), + ), + SizedBox( + height: 2.h, + ), + Container( + color: kLightBlue, + height: 10.h, + child: Padding( + padding: const EdgeInsets.all(4.0), + child: InkWell( + onTap: () async { + dateTime = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime.now(), + lastDate: DateTime.fromMillisecondsSinceEpoch( + beacon.expiresAt), + ); + startsAtDate.text = + dateTime.toString().substring(0, 10); + }, + child: TextFormField( + enabled: false, + controller: startsAtDate, + onChanged: (value) { + startsAtDate.text = + dateTime.toString().substring(0, 10); + }, + decoration: InputDecoration( + alignLabelWithHint: true, + errorStyle: TextStyle(color: Colors.red[800]), + floatingLabelBehavior: FloatingLabelBehavior.always, + labelText: 'Date', + labelStyle: + TextStyle(fontSize: labelsize, color: kYellow), + hintStyle: + TextStyle(fontSize: 13, color: hintColor), + hintText: 'Choose a date to reminded at!', + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + ), + ), + ), + ), + ), + SizedBox( + height: 2.h, + ), + Container( + height: 10.h, + child: Padding( + padding: const EdgeInsets.all(4.0), + child: InkWell( + onTap: () async { + timeOfDay = await showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + ); + startsAtTime.text = + timeOfDay.toString().substring(10, 15); + }, + child: TextFormField( + enabled: false, + controller: startsAtTime, + onChanged: (value) { + startsAtTime.text = + timeOfDay.toString().substring(10, 15); + }, + decoration: InputDecoration( + alignLabelWithHint: true, + errorStyle: TextStyle(color: Colors.red[800]), + floatingLabelBehavior: FloatingLabelBehavior.always, + labelText: 'Time', + labelStyle: + TextStyle(fontSize: labelsize, color: kYellow), + hintStyle: + TextStyle(fontSize: 13, color: hintColor), + hintText: 'Choose a time to be reminded at!', + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + ), + ), + ), + ), + color: kLightBlue, + ), + SizedBox( + height: 2.h, + ), + Container( + height: 10.h, + child: Padding( + padding: const EdgeInsets.all(4.0), + child: TextFormField( + enabled: true, + controller: title, + decoration: InputDecoration( + alignLabelWithHint: true, + errorStyle: TextStyle(color: Colors.red[800]), + floatingLabelBehavior: FloatingLabelBehavior.always, + labelText: 'Title', + labelStyle: + TextStyle(fontSize: labelsize, color: kYellow), + hintStyle: TextStyle(fontSize: 13, color: hintColor), + hintText: 'A title for the reminder!', + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + ), + ), + ), + color: kLightBlue, + ), + SizedBox( + height: 2.h, + ), + Container( + height: 10.h, + child: Padding( + padding: const EdgeInsets.all(4.0), + child: TextFormField( + enabled: true, + controller: message, + decoration: InputDecoration( + alignLabelWithHint: true, + errorStyle: TextStyle(color: Colors.red[800]), + floatingLabelBehavior: FloatingLabelBehavior.always, + labelText: 'Message', + labelStyle: + TextStyle(fontSize: labelsize, color: kYellow), + hintStyle: TextStyle(fontSize: 13, color: hintColor), + hintText: 'A message for the reminder!', + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + ), + ), + ), + color: kLightBlue, + ), + SizedBox( + height: 2.h, + ), + HikeButton( + buttonWidth: optbwidth, + text: 'Done', + textSize: 18.0, + textColor: Colors.white, + buttonColor: kYellow, + onTap: () { + if (dateTime == null || timeOfDay == null) { + navigationService.showSnackBar("Enter date and time"); + return; + } + if (message.text.trim().isEmpty) { + navigationService.showSnackBar("Choose a message"); + return; + } + if (title.text.trim().isEmpty) { + navigationService.showSnackBar("Choose a title"); + return; + } + dateTime = DateTime( + dateTime.year, + dateTime.month, + dateTime.day, + timeOfDay.hour, + timeOfDay.minute, + ); + if (DateTime.fromMillisecondsSinceEpoch(beacon.expiresAt) + .isBefore(dateTime)) { + navigationService + .showSnackBar("Enter a valid date and time!!"); + return; + } + localNotif.scheduleNotificationForBeacon( + beacon, dateTime, title.text, message.text); + + Navigator.pop(context, dateTime); + }, + ), + SizedBox( + height: 2.h, + ), + ], + ), + ), + ), + ), + ), + ); + } } diff --git a/lib/services/local_notification.dart b/lib/services/local_notification.dart index d3503eee..4cd3a8c9 100644 --- a/lib/services/local_notification.dart +++ b/lib/services/local_notification.dart @@ -43,59 +43,40 @@ class LocalNotification { await flutterLocalNotificationsPlugin.cancelAll(); } - Future scheduleNotification(Beacon beacon) async { - await flutterLocalNotificationsPlugin.zonedSchedule( - beacon.id.hashCode, - 'Hike ' + beacon.title + ' has started', - 'Click here to join!', - tz.TZDateTime.from( - DateTime.fromMillisecondsSinceEpoch(beacon.startsAt), tz.local), - NotificationDetails( - android: AndroidNotificationDetails( - 'channel id', - 'channel name', - playSound: true, - priority: Priority.high, - importance: Importance.high, + Future scheduleNotificationForBeacon( + Beacon beacon, + DateTime scheduleAtDateTime, + String title, + String body, + ) async { + try { + await flutterLocalNotificationsPlugin.zonedSchedule( + (beacon.id + scheduleAtDateTime.toIso8601String()).hashCode, + title, + body, + tz.TZDateTime.from(scheduleAtDateTime, tz.local), + NotificationDetails( + android: AndroidNotificationDetails( + 'channel id', + 'channel name', + playSound: true, + priority: Priority.high, + importance: Importance.high, + ), + iOS: IOSNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + badgeNumber: 1, + ), ), - iOS: IOSNotificationDetails( - presentAlert: true, - presentBadge: true, - presentSound: true, - badgeNumber: 1, - ), - ), - uiLocalNotificationDateInterpretation: - UILocalNotificationDateInterpretation.absoluteTime, - androidAllowWhileIdle: true, - payload: beacon.id, - ); - await flutterLocalNotificationsPlugin.zonedSchedule( - beacon.id.hashCode, - 'Reminder: ' + beacon.title + ' will start in an hour', - 'Get Ready!', - tz.TZDateTime.from( - DateTime.fromMillisecondsSinceEpoch(beacon.startsAt), tz.local) - .subtract(Duration(hours: 1)), - NotificationDetails( - android: AndroidNotificationDetails( - 'channel id', - 'channel name', - playSound: true, - priority: Priority.high, - importance: Importance.high, - ), - iOS: IOSNotificationDetails( - presentAlert: true, - presentBadge: true, - presentSound: true, - badgeNumber: 1, - ), - ), - uiLocalNotificationDateInterpretation: - UILocalNotificationDateInterpretation.absoluteTime, - androidAllowWhileIdle: true, - payload: beacon.id, - ); + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.absoluteTime, + androidAllowWhileIdle: true, + payload: beacon.id, + ); + } catch (e) { + throw (e); + } } } diff --git a/lib/view_model/home_view_model.dart b/lib/view_model/home_view_model.dart index c998b906..404bc44a 100644 --- a/lib/view_model/home_view_model.dart +++ b/lib/view_model/home_view_model.dart @@ -1,3 +1,4 @@ +import 'package:beacon/components/dialog_boxes.dart'; import 'package:beacon/enums/view_state.dart'; import 'package:beacon/locator.dart'; import 'package:beacon/models/beacon/beacon.dart'; @@ -48,11 +49,37 @@ class HomeViewModel extends BaseModel { isLeader: true, )); } else { - localNotif.scheduleNotification(beacon); + //schedule has started notif. + localNotif.scheduleNotificationForBeacon( + beacon, + DateTime.fromMillisecondsSinceEpoch(beacon.startsAt), + 'Hike ' + beacon.title + ' has started', + 'Click here to join!'); setState(ViewState.idle); navigationService.showSnackBar( 'Beacon has not yet started! \nPlease come back at ${DateFormat("hh:mm a, d/M/y").format(DateTime.fromMillisecondsSinceEpoch(beacon.startsAt)).toString()}', ); + //show dialogue to ask if you want to be reminded. + DialogBoxes.setReminderDialogueBox( + navigationService.navigatorKey.currentContext, beacon) + .then((value) { + //if no reminder was scheduled then default to one hour before start time. + if (value == null && + DateTime.fromMillisecondsSinceEpoch(beacon.startsAt) + .subtract(Duration(hours: 1)) + .isAfter( + DateTime.now(), + )) { + localNotif.scheduleNotificationForBeacon( + beacon, + DateTime.fromMillisecondsSinceEpoch(beacon.startsAt).subtract( + Duration(hours: 1), + ), + 'Reminder: ' + beacon.title + ' will start in an hour', + 'Get Ready!', + ); + } + }); return; } } else { @@ -79,11 +106,38 @@ class HomeViewModel extends BaseModel { navigationService.pushScreen('/hikeScreen', arguments: HikeScreen(beacon, isLeader: false)); } else { - localNotif.scheduleNotification(beacon); + //schedule has started notif. + localNotif.scheduleNotificationForBeacon( + beacon, + DateTime.fromMillisecondsSinceEpoch(beacon.startsAt), + 'Hike ' + beacon.title + ' has started', + 'Click here to join!', + ); setState(ViewState.idle); navigationService.showSnackBar( 'Beacon has not yet started! \nPlease come back at ${DateFormat("hh:mm a, d/M/y").format(DateTime.fromMillisecondsSinceEpoch(beacon.startsAt)).toString()}', ); + //show dialogue to ask if you want to be reminded. + DialogBoxes.setReminderDialogueBox( + navigationService.navigatorKey.currentContext, beacon) + .then((value) { + //if no reminder was scheduled then default to one hour before start time. + if (value == null && + DateTime.fromMillisecondsSinceEpoch(beacon.startsAt) + .subtract(Duration(hours: 1)) + .isAfter( + DateTime.now(), + )) { + localNotif.scheduleNotificationForBeacon( + beacon, + DateTime.fromMillisecondsSinceEpoch(beacon.startsAt).subtract( + Duration(hours: 1), + ), + 'Reminder: ' + beacon.title + ' will start in an hour', + 'Get Ready!', + ); + } + }); return; } } else {