Skip to content

Commit 865ab9b

Browse files
committed
feat,bfix: implement notification and fix test
1 parent e666d05 commit 865ab9b

9 files changed

Lines changed: 376 additions & 40 deletions

File tree

ios/Runner/AppDelegate.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import Flutter
88
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
99
) -> Bool {
1010
GeneratedPluginRegistrant.register(with: self)
11+
12+
if #available(iOS 10.0, *) {
13+
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
14+
}
15+
1116
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
1217
}
1318
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
2+
import 'package:flutter_native_timezone/flutter_native_timezone.dart';
3+
import 'package:timezone/data/latest.dart' as tz;
4+
import 'package:timezone/timezone.dart' as tz;
5+
6+
class NotificationService {
7+
static final _notification = FlutterLocalNotificationsPlugin();
8+
9+
static NotificationDetails _notificationDetails() {
10+
return const NotificationDetails(
11+
android: AndroidNotificationDetails(
12+
'channelId',
13+
'channelName',
14+
importance: Importance.max,
15+
),
16+
iOS: IOSNotificationDetails(),
17+
);
18+
}
19+
20+
static Future init() async {
21+
const settings = InitializationSettings(
22+
android: AndroidInitializationSettings('@mipmap/ic_launcher'),
23+
iOS: IOSInitializationSettings(),
24+
);
25+
26+
await _notification.initialize(
27+
settings,
28+
onSelectNotification: (payload) {},
29+
);
30+
31+
tz.initializeTimeZones();
32+
final locationName = await FlutterNativeTimezone.getLocalTimezone();
33+
tz.setLocalLocation(tz.getLocation(locationName));
34+
}
35+
36+
static Future setNotification({
37+
required DateTime scheduledDate,
38+
int id = 0,
39+
String? title,
40+
String? body,
41+
String? payload,
42+
}) async {
43+
_notification.zonedSchedule(
44+
id,
45+
title,
46+
body,
47+
tz.TZDateTime.from(scheduledDate, tz.local),
48+
_notificationDetails(),
49+
uiLocalNotificationDateInterpretation:
50+
UILocalNotificationDateInterpretation.absoluteTime,
51+
androidAllowWhileIdle: true,
52+
);
53+
}
54+
55+
static Future cancelNotification({String? name}) async {
56+
final pendingNotifications =
57+
await _notification.pendingNotificationRequests();
58+
59+
for (final notification in pendingNotifications) {
60+
if (notification.title == name) {
61+
await _notification.cancel(notification.id);
62+
return;
63+
}
64+
}
65+
}
66+
67+
static Future<List<String?>> getPendingNotification() async {
68+
final pendingNotifications =
69+
await _notification.pendingNotificationRequests();
70+
71+
return pendingNotifications.map((notif) => notif.title).toList();
72+
}
73+
}

lib/presentation/contests/bloc/contests_bloc.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
44

55
import '../../../data/constants/strings.dart';
66
import '../../../data/services/local/storage_service.dart';
7+
import '../../../data/services/remote/notification_service.dart';
78
import '../../../domain/models/contest.dart';
89
import '../../../domain/models/contest_filter.dart';
910
import '../../../domain/repositories/cp_repository.dart';
@@ -92,8 +93,9 @@ class ContestsBloc extends Bloc<ContestsEvent, ContestsState> {
9293
ContestFilter? _filter;
9394
List<Ongoing> _ongoing = [], _filteredOngoing = [];
9495
List<Upcoming> _upcoming = [], _filteredUpcoming = [];
96+
List<String?> pendingNotification = [];
9597

96-
void init() {
98+
void init() async {
9799
final exists = StorageService.exists(AppStrings.filterKey);
98100
if (!exists) {
99101
StorageService.filter = ContestFilter(
@@ -106,9 +108,14 @@ class ContestsBloc extends Bloc<ContestsEvent, ContestsState> {
106108
}
107109

108110
_filter = StorageService.filter;
111+
pendingNotification = await NotificationService.getPendingNotification();
109112
add(const FetchContests());
110113
}
111114

115+
bool reminderSet(String? title) {
116+
return pendingNotification.contains(title);
117+
}
118+
112119
void saveFilter() {
113120
StorageService.filter = _filter;
114121
add(const UpdateContestsList());

lib/presentation/contests/contests_screen.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,18 @@ class ContestsScreen extends StatelessWidget {
3333
return ListView.builder(
3434
itemCount: state.contests.length,
3535
itemBuilder: (context, index) {
36+
final isReminderSet = context
37+
.read<ContestsBloc>()
38+
.reminderSet(state.contests[index].name);
3639
if (state.contests[index] is Ongoing) {
3740
return ContestCard(
41+
isReminderSet: isReminderSet,
3842
ongoing: state.contests[index],
3943
);
4044
}
4145

4246
return ContestCard(
47+
isReminderSet: isReminderSet,
4348
upcoming: state.contests[index],
4449
);
4550
},

lib/presentation/contests/widgets/contest_card.dart

Lines changed: 210 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,47 @@
11
import 'package:flutter/material.dart';
2+
import 'package:flutter_bloc/flutter_bloc.dart';
23
import 'package:flutter_screenutil/flutter_screenutil.dart';
34
import 'package:flutter_svg/svg.dart';
5+
import 'package:flutter_web_browser/flutter_web_browser.dart';
46
import 'package:intl/intl.dart';
57

68
import '../../../data/constants/assets.dart';
9+
import '../../../data/constants/colors.dart';
710
import '../../../data/constants/styles.dart';
11+
import '../../../data/services/remote/notification_service.dart';
812
import '../../../domain/models/contest.dart';
913
import '../../../utils/contest_util.dart';
14+
import '../bloc/contests_bloc.dart';
1015

1116
class ContestCard extends StatelessWidget {
12-
const ContestCard({
17+
ContestCard({
18+
required this.isReminderSet,
1319
this.upcoming,
1420
this.ongoing,
1521
Key? key,
1622
}) : assert(upcoming != null || ongoing != null, ''),
17-
super(key: key);
23+
super(key: key) {
24+
_notifier = ValueNotifier(isReminderSet);
25+
}
1826
final Upcoming? upcoming;
1927
final Ongoing? ongoing;
28+
final bool isReminderSet;
29+
late final ValueNotifier<bool> _notifier;
30+
final ValueNotifier<int> _remindNotifer = ValueNotifier(2);
2031

2132
@override
2233
Widget build(BuildContext context) {
2334
return InkWell(
24-
onTap: () {},
35+
onTap: () {
36+
FlutterWebBrowser.openWebPage(
37+
url: upcoming?.url ?? ongoing!.url,
38+
customTabsOptions: const CustomTabsOptions(
39+
defaultColorSchemeParams: CustomTabsColorSchemeParams(
40+
toolbarColor: AppColors.primary,
41+
),
42+
),
43+
);
44+
},
2545
child: Padding(
2646
padding: EdgeInsets.symmetric(
2747
vertical: 10.h,
@@ -72,9 +92,193 @@ class ContestCard extends StatelessWidget {
7292
],
7393
),
7494
),
75-
IconButton(
76-
onPressed: () {},
77-
icon: SvgPicture.asset(AppAssets.bell),
95+
ValueListenableBuilder<bool>(
96+
valueListenable: _notifier,
97+
builder: (context, value, _) {
98+
return IconButton(
99+
onPressed: () {
100+
final _bloc = context.read<ContestsBloc>();
101+
showDialog(
102+
context: context,
103+
builder: (context) {
104+
return AlertDialog(
105+
titlePadding: EdgeInsets.zero,
106+
contentPadding: EdgeInsets.zero,
107+
shape: RoundedRectangleBorder(
108+
borderRadius: BorderRadius.vertical(
109+
top: Radius.circular(5.r),
110+
),
111+
),
112+
title: Container(
113+
padding: EdgeInsets.symmetric(vertical: 10.h),
114+
decoration: BoxDecoration(
115+
color: AppColors.grey7,
116+
borderRadius: BorderRadius.vertical(
117+
top: Radius.circular(5.r),
118+
),
119+
),
120+
child: Text(
121+
'Set Reminder',
122+
style: AppStyles.h6.copyWith(
123+
color: AppColors.primaryBlack,
124+
),
125+
textAlign: TextAlign.center,
126+
),
127+
),
128+
content: Visibility(
129+
visible: upcoming != null,
130+
replacement: Padding(
131+
padding: EdgeInsets.all(30.r),
132+
child: const Text(
133+
'The selected contest has already started. 😔 ',
134+
textAlign: TextAlign.center,
135+
),
136+
),
137+
child: SizedBox(
138+
height: 120.h,
139+
child: Column(
140+
children: [
141+
Padding(
142+
padding: EdgeInsets.symmetric(
143+
horizontal: 30.w,
144+
vertical: 10.h,
145+
),
146+
child: Text(
147+
'You will be reminded before the contest starts. Set the timer',
148+
style: AppStyles.h6.copyWith(
149+
color: AppColors.primaryBlack,
150+
fontSize: 12.sp,
151+
),
152+
textAlign: TextAlign.center,
153+
),
154+
),
155+
Row(
156+
mainAxisAlignment: MainAxisAlignment.center,
157+
children: [
158+
ValueListenableBuilder<int>(
159+
valueListenable: _remindNotifer,
160+
builder: (context, value, _) {
161+
return SizedBox(
162+
height: 60.h,
163+
width: 30.w,
164+
child: ListWheelScrollView(
165+
controller:
166+
FixedExtentScrollController(
167+
initialItem: value,
168+
),
169+
itemExtent: 25,
170+
useMagnifier: true,
171+
magnification: 1.3,
172+
onSelectedItemChanged: (val) {
173+
_remindNotifer.value = val;
174+
},
175+
children: List.generate(
176+
6,
177+
(index) {
178+
return Text(
179+
'${5 * (index + 1)}',
180+
style:
181+
AppStyles.h6.copyWith(
182+
color: index == value
183+
? AppColors.primary
184+
: AppColors
185+
.primaryBlack,
186+
),
187+
);
188+
},
189+
).toList(),
190+
),
191+
);
192+
},
193+
),
194+
SizedBox(width: 4.w),
195+
Text(
196+
'minutes',
197+
style: AppStyles.h6.copyWith(
198+
color: AppColors.primary,
199+
),
200+
),
201+
],
202+
),
203+
],
204+
),
205+
),
206+
),
207+
actions: [
208+
if (upcoming != null)
209+
TextButton(
210+
onPressed: () {
211+
_notifier.value = false;
212+
_bloc.pendingNotification
213+
.remove(upcoming!.name);
214+
215+
NotificationService.cancelNotification(
216+
name: upcoming!.name);
217+
218+
Navigator.pop(context);
219+
},
220+
child: Container(
221+
padding: EdgeInsets.symmetric(
222+
horizontal: 40.w,
223+
vertical: 10.h,
224+
),
225+
color: AppColors.grey10,
226+
child: Text(
227+
'Cancel',
228+
style: AppStyles.h6.copyWith(
229+
fontSize: 12.sp,
230+
),
231+
),
232+
),
233+
),
234+
TextButton(
235+
onPressed: () {
236+
if (upcoming != null) {
237+
_notifier.value = true;
238+
if (!_bloc.pendingNotification
239+
.contains(upcoming!.name)) {
240+
_bloc.pendingNotification
241+
.add(upcoming!.name);
242+
}
243+
NotificationService.setNotification(
244+
scheduledDate: upcoming!.startTime.subtract(
245+
Duration(
246+
minutes: (_remindNotifer.value + 1) * 6,
247+
),
248+
),
249+
title: upcoming!.name,
250+
body: 'Contest Starts at '
251+
'${DateFormat('hh:mm a dd, MMMM yyyy').format(upcoming!.startTime)}',
252+
);
253+
}
254+
255+
Navigator.pop(context);
256+
},
257+
child: Container(
258+
padding: EdgeInsets.symmetric(
259+
horizontal: 40.w,
260+
vertical: 10.h,
261+
),
262+
color: AppColors.primary,
263+
child: Text(
264+
'Okay',
265+
style: AppStyles.h6.copyWith(
266+
color: AppColors.white,
267+
fontSize: 12.sp,
268+
),
269+
),
270+
),
271+
),
272+
],
273+
);
274+
},
275+
);
276+
},
277+
icon: SvgPicture.asset(
278+
value ? AppAssets.selectedBell : AppAssets.bell,
279+
),
280+
);
281+
},
78282
),
79283
],
80284
),

0 commit comments

Comments
 (0)