11import 'dart:async' ;
22import 'dart:developer' ;
33
4+ import 'package:flutter/material.dart' ;
5+ import 'package:flutter_alarm_manager_poc/hive/service/settings_service.dart' ;
6+ import 'package:flutter_alarm_manager_poc/state/notification_behavior.dart' ;
7+
48// --- STATES ---
5- // Using a sealed class ensures that we can only be in one of the defined states.
69sealed class AlarmState {}
710
8- // The app is idle. No alarm is scheduled.
911final class AlarmIdle extends AlarmState {}
1012
11- // The cycle is running. An alarm is ALWAYS scheduled for a specific time.
13+ @immutable
1214final class AlarmActive extends AlarmState {
13- AlarmActive ({required this .scheduledAt});
15+ AlarmActive ({required this .scheduledAt, required this .behavior });
1416 final DateTime scheduledAt;
17+ final NotificationBehavior behavior;
18+
19+ @override
20+ bool operator == (Object other) =>
21+ identical (this , other) ||
22+ other is AlarmActive &&
23+ runtimeType == other.runtimeType &&
24+ scheduledAt == other.scheduledAt &&
25+ behavior == other.behavior;
26+
27+ @override
28+ int get hashCode => scheduledAt.hashCode ^ behavior.hashCode;
1529}
1630
1731// --- EVENTS ---
@@ -29,7 +43,12 @@ final class QuestionnaireFinished extends AlarmEvent {
2943
3044final class DebugScheduleRequested extends AlarmEvent {}
3145
32- // Helper enum to make the code more readable and type-safe.
46+ // New event to handle behavior changes.
47+ final class NotificationBehaviorChanged extends AlarmEvent {
48+ NotificationBehaviorChanged (this .newBehavior);
49+ final NotificationBehavior newBehavior;
50+ }
51+
3352enum QuestionnaireResult { answered, declined, snoozed }
3453
3554// --- THE STATE MACHINE ("BRAIN") ---
@@ -40,28 +59,16 @@ class AlarmStateManager {
4059
4160 // The initial state of the application.
4261 AlarmState _state = AlarmIdle ();
43-
44- // A broadcast stream controller allows multiple parts of the app (e.g., UI, interpreter)
45- // to listen to state changes.
4662 final _controller = StreamController <AlarmState >.broadcast ();
4763
48- // Public stream for widgets and services to listen to.
4964 Stream <AlarmState > get state => _controller.stream;
50-
51- // A way to get the current state synchronously if needed.
5265 AlarmState get currentState => _state;
5366
5467 void dispatch (AlarmEvent event) {
5568 log ('Dispatching event: ${event .runtimeType } from state: ${_state .runtimeType }' );
56-
57- // The new state is calculated based on the current state and the incoming event.
5869 final newState = _reduce (_state, event);
5970
60- // If the state has changed, update it and notify all listeners.
61- if (newState.runtimeType != _state.runtimeType ||
62- (newState is AlarmActive &&
63- _state is AlarmActive &&
64- newState.scheduledAt != (_state as AlarmActive ).scheduledAt)) {
71+ if (newState != _state) {
6572 _state = newState;
6673 _controller.add (_state);
6774 }
@@ -70,11 +77,19 @@ class AlarmStateManager {
7077 // The "reducer" function contains all the transition logic.
7178 // It's a pure function: given a state and an event, it returns a new state.
7279 AlarmState _reduce (AlarmState currentState, AlarmEvent event) {
80+ // Helper to get the current notification setting.
81+ NotificationBehavior getCurrentBehavior () {
82+ return SettingsService .instance.getNotificationBehavior ();
83+ }
84+
7385 switch (event) {
7486 case CycleStarted ():
7587 // Can only start a cycle if we are currently idle.
7688 if (currentState is AlarmIdle ) {
77- return AlarmActive (scheduledAt: _calculateNextWholeIntervalTime ());
89+ return AlarmActive (
90+ scheduledAt: _calculateNextWholeIntervalTime (),
91+ behavior: getCurrentBehavior (),
92+ );
7893 }
7994 case CycleCancelled ():
8095 // Can only cancel a cycle if one is active.
@@ -86,28 +101,41 @@ class AlarmStateManager {
86101 // The type of alarm depends on the user's answer.
87102 switch (event.result) {
88103 case QuestionnaireResult .answered:
89- return AlarmActive (scheduledAt: _calculateNextWholeIntervalTime ());
90104 case QuestionnaireResult .declined:
91- return AlarmActive (scheduledAt: _calculateNextWholeIntervalTime ());
105+ return AlarmActive (
106+ scheduledAt: _calculateNextWholeIntervalTime (),
107+ behavior: getCurrentBehavior (),
108+ );
92109 case QuestionnaireResult .snoozed:
93- return AlarmActive (scheduledAt: _calculateSnoozeTime ());
110+ return AlarmActive (
111+ scheduledAt: _calculateSnoozeTime (),
112+ behavior: getCurrentBehavior (),
113+ );
94114 }
95115 case DebugScheduleRequested ():
96116 // A special event for testing that schedules an alarm 10 seconds from now.
97117 return AlarmActive (
98- scheduledAt: DateTime .now ().add (const Duration (seconds: 10 )));
118+ scheduledAt: DateTime .now ().add (const Duration (seconds: 10 )),
119+ behavior: getCurrentBehavior (),
120+ );
121+ // Handle the new event to update the behavior of an active alarm.
122+ case NotificationBehaviorChanged (newBehavior: final behavior):
123+ if (currentState is AlarmActive ) {
124+ // Create a new state with the same time but new behavior.
125+ return AlarmActive (
126+ scheduledAt: currentState.scheduledAt,
127+ behavior: behavior,
128+ );
129+ }
99130 }
100- // If the event doesn't cause a state change, return the current state.
101131 return currentState;
102132 }
103133
104- // --- TIME CALCULATION LOGIC (moved from AlarmMethodChannel) ---
105134 DateTime _calculateNextWholeIntervalTime () {
106135 final now = DateTime .now ();
107136 DateTime nextAlarmTime;
108137
109138 if (now.hour >= 20 ) {
110- // Changed from 23 to 20 as per original logic
111139 nextAlarmTime = DateTime (now.year, now.month, now.day + 1 , 8 );
112140 } else if (now.hour < 8 ) {
113141 nextAlarmTime = DateTime (now.year, now.month, now.day, 8 );
0 commit comments