11import 'dart:async' ;
22import 'package:flutter/material.dart' ;
3+ import 'package:audioplayers/audioplayers.dart' ;
34
45import '../../core/notifications/notification_service.dart' ;
56import '../../shared/storage/alarm_storage.dart' ;
67import 'alarm_model.dart' ;
8+ import 'alarm_tile.dart' ;
9+ import '../../shared/widgets/app_model.dart' ;
710
811class AlarmScreen extends StatefulWidget {
912 const AlarmScreen ({super .key});
@@ -14,60 +17,69 @@ class AlarmScreen extends StatefulWidget {
1417
1518class _AlarmScreenState extends State <AlarmScreen > {
1619 final List <AlarmModel > alarms = [];
20+
1721 Timer ? _ticker;
22+ bool _dialogVisible = false ;
23+
24+ // Initialize the audio player
25+ final AudioPlayer _audioPlayer = AudioPlayer ();
1826
1927 @override
2028 void initState () {
2129 super .initState ();
2230 _loadAlarms ();
23- _ticker = Timer .periodic (const Duration (minutes: 1 ), (_) {
24- if (mounted) setState (() {});
25- });
31+ // Update UI every 30 seconds to check alarm times and refresh "remaining time"
32+ _ticker = Timer .periodic (const Duration (seconds: 30 ), (_) => _tick ());
2633 }
2734
2835 @override
2936 void dispose () {
3037 _ticker? .cancel ();
38+ _audioPlayer.dispose (); // Dispose of the player to release resources
3139 super .dispose ();
3240 }
3341
3442 Future <void > _loadAlarms () async {
3543 final stored = await AlarmStorage .load ();
36- if (! mounted) return ;
3744 alarms.addAll (stored);
38- setState (() {});
45+ _tick (); // Check immediately upon load
3946 }
4047
4148 Future <void > _persist () async {
4249 await AlarmStorage .save (alarms);
4350 }
4451
45- Duration _remainingTime (AlarmModel alarm) {
46- return alarm.scheduledAt.difference (DateTime .now ());
52+ void _tick () {
53+ final now = DateTime .now ();
54+ bool changed = false ;
55+
56+ for (final alarm in alarms) {
57+ if (alarm.active && now.isAfter (alarm.scheduledAt)) {
58+ alarm.active = false ;
59+ changed = true ;
60+
61+ if (mounted && ! _dialogVisible) {
62+ _showAlarmDialog (alarm);
63+ }
64+ }
65+ }
66+
67+ if (changed) _persist ();
68+ if (mounted) setState (() {});
4769 }
4870
4971 Future <void > _addAlarm () async {
5072 final allowed = await NotificationService .requestExactAlarmPermission ();
51- if (! allowed) {
52- if (! mounted) return ;
53- ScaffoldMessenger .of (context).showSnackBar (
54- const SnackBar (
55- content: Text ('Enable exact alarm permission in system settings' ),
56- ),
57- );
58- return ;
59- }
73+ if (! allowed || ! mounted) return ;
6074
61- if (! mounted) return ;
6275 final picked = await showTimePicker (
6376 context: context,
6477 initialTime: TimeOfDay .now (),
6578 );
66-
6779 if (picked == null ) return ;
6880
6981 final now = DateTime .now ();
70- DateTime scheduled = DateTime (
82+ var scheduled = DateTime (
7183 now.year,
7284 now.month,
7385 now.day,
@@ -79,7 +91,7 @@ class _AlarmScreenState extends State<AlarmScreen> {
7991 scheduled = scheduled.add (const Duration (days: 1 ));
8092 }
8193
82- final int id = scheduled.millisecondsSinceEpoch ~ / 1000 ;
94+ final id = scheduled.millisecondsSinceEpoch ~ / 1000 ;
8395
8496 await NotificationService .scheduleAlarm (
8597 id: id,
@@ -96,32 +108,145 @@ class _AlarmScreenState extends State<AlarmScreen> {
96108 await _persist ();
97109 }
98110
99- Future <void > _toggleAlarm (int index, bool value) async {
100- final alarm = alarms[index];
101- alarm.active = value;
111+ Future <void > _editAlarm (int index) async {
112+ final oldAlarm = alarms[index];
113+
114+ // 1. Show Time Picker initialized with the EXISTING time
115+ final picked = await showTimePicker (
116+ context: context,
117+ initialTime: oldAlarm.time,
118+ );
119+
120+ // If user cancelled, do nothing
121+ if (picked == null ) return ;
122+
123+ // 2. Calculate new schedule
124+ final now = DateTime .now ();
125+ var scheduled = DateTime (
126+ now.year,
127+ now.month,
128+ now.day,
129+ picked.hour,
130+ picked.minute,
131+ );
132+
133+ if (scheduled.isBefore (now)) {
134+ scheduled = scheduled.add (const Duration (days: 1 ));
135+ }
136+
137+ // 3. Create new ID based on new time
138+ final newId = scheduled.millisecondsSinceEpoch ~ / 1000 ;
139+
140+ // 4. Cancel the OLD notification
141+ await NotificationService .cancel (oldAlarm.id);
142+
143+ // 5. Schedule the NEW notification
144+ await NotificationService .scheduleAlarm (
145+ id: newId,
146+ dateTime: scheduled,
147+ title: '⏰ Alarm' ,
148+ body: 'It\' s time!' ,
149+ );
150+
151+ // 6. Update the list
152+ setState (() {
153+ alarms[index] = AlarmModel (
154+ time: picked,
155+ id: newId,
156+ scheduledAt: scheduled,
157+ active: true ,
158+ );
159+ });
160+
161+ await _persist ();
162+ }
163+
164+ Future <void > _toggle (int index, bool value) async {
165+ var alarm = alarms[index];
102166
103167 if (value) {
168+ var scheduled = alarm.scheduledAt;
169+
170+ // Reschedule for tomorrow if the time has already passed today
171+ if (scheduled.isBefore (DateTime .now ())) {
172+ scheduled = scheduled.add (const Duration (days: 1 ));
173+ alarm = alarms[index] = AlarmModel (
174+ time: alarm.time,
175+ id: scheduled.millisecondsSinceEpoch ~ / 1000 ,
176+ scheduledAt: scheduled,
177+ active: true ,
178+ );
179+ }
180+
104181 await NotificationService .scheduleAlarm (
105182 id: alarm.id,
106183 dateTime: alarm.scheduledAt,
107184 title: '⏰ Alarm' ,
108185 body: 'It\' s time!' ,
109186 );
187+
188+ alarm.active = true ;
110189 } else {
111190 await NotificationService .cancel (alarm.id);
191+ alarm.active = false ;
112192 }
113193
114194 setState (() {});
115195 await _persist ();
116196 }
117197
118- Future <void > _deleteAlarm (int index) async {
198+ Future <void > _delete (int index) async {
119199 await NotificationService .cancel (alarms[index].id);
120200 alarms.removeAt (index);
121201 setState (() {});
122202 await _persist ();
123203 }
124204
205+ Future <void > _showAlarmDialog (AlarmModel alarm) async {
206+ _dialogVisible = true ;
207+
208+ // 1. Play the sound in a loop
209+ try {
210+ await _audioPlayer.setReleaseMode (ReleaseMode .loop);
211+ await _audioPlayer.play (AssetSource ('sounds/alarm_clock_1.mp3' ));
212+ } catch (e) {
213+ debugPrint ("Error playing sound: $e " );
214+ }
215+
216+ if (! mounted) return ;
217+
218+ // 2. Show the modal
219+ await AppModal .show (
220+ context: context,
221+ title: '⏰ Alarm' ,
222+ content: Text ('It\' s ${alarm .time .format (context )}' ),
223+ dismissible: false , // Forces user to interact
224+ actions: [
225+ TextButton (
226+ onPressed: () {
227+ // Stop sound
228+ _audioPlayer.stop ();
229+
230+ Navigator .pop (context);
231+ _dialogVisible = false ;
232+ },
233+ child: const Text ('Dismiss' ),
234+ ),
235+ TextButton (
236+ onPressed: () {
237+ // Stop sound
238+ _audioPlayer.stop ();
239+
240+ // TODO: Implement actual snooze logic (e.g., reschedule for +5 mins)
241+ Navigator .pop (context);
242+ _dialogVisible = false ;
243+ },
244+ child: const Text ('Snooze (5 min)' ),
245+ ),
246+ ],
247+ );
248+ }
249+
125250 @override
126251 Widget build (BuildContext context) {
127252 return Scaffold (
@@ -135,44 +260,13 @@ class _AlarmScreenState extends State<AlarmScreen> {
135260 : ListView .builder (
136261 padding: const EdgeInsets .all (16 ),
137262 itemCount: alarms.length,
138- itemBuilder: (context, index) {
139- final alarm = alarms[index];
140- final remaining = _remainingTime (alarm);
141-
142- return Dismissible (
143- key: ValueKey (alarm.id),
144- direction: DismissDirection .endToStart,
145- onDismissed: (_) => _deleteAlarm (index),
146- background: Container (
147- alignment: Alignment .centerRight,
148- padding: const EdgeInsets .only (right: 20 ),
149- color: Colors .red,
150- child: const Icon (Icons .delete, color: Colors .white),
151- ),
152- child: Card (
153- shape: RoundedRectangleBorder (
154- borderRadius: BorderRadius .circular (16 ),
155- ),
156- child: ListTile (
157- title: Text (
158- alarm.time.format (context),
159- style: const TextStyle (
160- fontSize: 28 ,
161- fontWeight: FontWeight .bold,
162- ),
163- ),
164- subtitle: alarm.active && remaining.inMinutes > 0
165- ? Text (
166- '${remaining .inHours }h ${remaining .inMinutes % 60 }m remaining' ,
167- style: const TextStyle (color: Colors .grey),
168- )
169- : const Text ('Inactive' ),
170- trailing: Switch (
171- value: alarm.active,
172- onChanged: (v) => _toggleAlarm (index, v),
173- ),
174- ),
175- ),
263+ itemBuilder: (_, i) {
264+ return AlarmTile (
265+ key: ValueKey (alarms[i].id),
266+ alarm: alarms[i],
267+ onDelete: () => _delete (i),
268+ onEdit: () => _editAlarm (i), // Pass the edit function here
269+ onToggle: (val) => _toggle (i, val),
176270 );
177271 },
178272 ),
0 commit comments