Skip to main content

void/app/
timer_ops.rs

1use super::*;
2use crate::model::TimerMode;
3
4impl App {
5    pub(crate) fn maybe_complete_task_estimate(&mut self, task_id: Option<u64>) {
6        let Some(id) = task_id else {
7            return;
8        };
9        let estimated = self
10            .data
11            .tasks
12            .iter()
13            .find(|t| t.id == id)
14            .map(|t| (t.title.clone(), t.actual_minutes, t.estimated_minutes));
15        let Some((title, actual, estimate)) = estimated else {
16            return;
17        };
18        if actual < estimate {
19            return;
20        }
21        match self.data.estimate_complete {
22            EstimateCompleteBehavior::Nudge => {
23                self.set_status(
24                    format!("Estimate reached for \"{title}\" — mark done?"),
25                    false,
26                );
27            }
28            EstimateCompleteBehavior::AutoDone => {
29                self.persist_data(|db, data| storage::mark_task_done(db, data, id));
30                if self.active_task == Some(id) {
31                    self.active_task = None;
32                    self.data.active_task_id = None;
33                    self.persist(|db| db.persist_active_task(None));
34                }
35                self.bump_data();
36                self.set_status(format!("\"{title}\" auto-completed (estimate met)."), false);
37                self.check_queue_empty();
38            }
39            EstimateCompleteBehavior::None => {}
40        }
41    }
42
43    pub fn end_session(&mut self) {
44        if self.timer.state == TimerState::Running {
45            self.pause_timer();
46        }
47        let today = storage::today_focus_minutes(&self.data);
48        let goal = self.data.daily_goal_minutes;
49        let queue_note = if self.queue_empty() {
50            "all tasks done"
51        } else {
52            "tasks remain"
53        };
54        self.set_status(
55            format!(
56                "Session ended — today {}/{} min · goal streak {} days · {queue_note}",
57                today, goal, self.data.goal_streak_days
58            ),
59            false,
60        );
61    }
62
63    pub fn on_tick(&mut self) {
64        let _ = storage::ensure_today_reset(&self.db, &mut self.data);
65        let just_finished = self.timer.tick();
66        if just_finished {
67            self.on_timer_finished(false);
68        }
69        if self.status.is_some()
70            && !self.status_error
71            && self.last_status_set.elapsed() > Duration::from_secs(4)
72        {
73            self.status = None;
74        }
75    }
76
77    pub(crate) fn on_timer_finished(&mut self, skipped: bool) {
78        let mode = self.timer.mode;
79        if mode == TimerMode::Focus {
80            let mins = self.elapsed_minutes(skipped);
81            let task_id = self.active_task;
82            self.persist_data(|db, data| {
83                storage::record_focus_session(db, data, mins, task_id, mode)
84            });
85            self.maybe_complete_task_estimate(task_id);
86            if self.data.sound_enabled {
87                sound::play_finish();
88            }
89            if self.data.notify_on_finish {
90                let msg = if skipped {
91                    format!("Logged {} min (skipped early)", mins)
92                } else {
93                    format!("+{} min logged — time for a break", mins)
94                };
95                let kind = if skipped {
96                    sound::NotifyKind::SessionSkipped
97                } else {
98                    sound::NotifyKind::FocusComplete
99                };
100                sound::notify_typed(kind, "Void · Focus complete", &msg);
101            }
102            self.set_status(
103                format!(
104                    "Focus {}: +{} min",
105                    if skipped { "skipped" } else { "complete" },
106                    mins
107                ),
108                false,
109            );
110            self.maybe_advance_task();
111            self.bump_data();
112            if !skipped {
113                self.persist_timer_state();
114            }
115            self.advance_to_break();
116        } else if mode == TimerMode::Custom {
117            let mins = self.elapsed_minutes(skipped);
118            let task_id = self.active_task;
119            self.persist_data(|db, data| {
120                storage::record_focus_session(db, data, mins, task_id, mode)
121            });
122            if self.data.sound_enabled {
123                sound::play_finish();
124            }
125            self.set_status(format!("Custom session complete: +{} min", mins), false);
126            self.bump_data();
127            self.timer.configure(TimerMode::Focus);
128            self.persist_timer_state();
129        } else {
130            let break_mins = self.elapsed_minutes(false);
131            self.persist_data(|db, data| storage::record_break_session(db, data, mode, break_mins));
132            if self.data.sound_enabled {
133                sound::play_finish();
134            }
135            if self.data.notify_on_finish {
136                sound::notify_typed(
137                    sound::NotifyKind::BreakComplete,
138                    "Void · Break over",
139                    "Break finished — ready to focus again",
140                );
141            }
142            self.set_status("Break finished. Ready for focus.", false);
143            self.bump_data();
144            self.advance_to_focus();
145        }
146    }
147
148    pub(crate) fn advance_to_break(&mut self) {
149        let long_break = self.timer.completed_focus_sessions > 0
150            && self
151                .timer
152                .completed_focus_sessions
153                .is_multiple_of(self.timer.config.long_break_every);
154        let next = if long_break {
155            TimerMode::LongBreak
156        } else {
157            TimerMode::ShortBreak
158        };
159        self.timer.configure(next);
160        self.persist_timer_state();
161        if self.data.auto_start_breaks {
162            self.timer.start();
163            if self.data.sound_enabled {
164                sound::play_start();
165            }
166            self.set_status(format!("{} started.", next.label()), false);
167        }
168    }
169
170    pub(crate) fn advance_to_focus(&mut self) {
171        self.timer.configure(TimerMode::Focus);
172        self.persist_timer_state();
173        if self.queue_empty() && self.data.empty_queue_behavior == EmptyQueueBehavior::PauseTimer {
174            self.set_status("All tasks done — timer waiting. [E] end session", false);
175            return;
176        }
177        self.auto_pick_task_if_needed();
178        if self.data.auto_start_focus {
179            self.timer.start();
180            if self.data.sound_enabled {
181                sound::play_start();
182            }
183            self.set_status("Focus started.", false);
184        }
185    }
186
187    pub fn toggle_timer(&mut self) {
188        if self.timer.state == TimerState::Running {
189            self.pause_timer();
190        } else {
191            self.start_timer();
192        }
193    }
194
195    pub fn start_timer(&mut self) {
196        if self.timer.state == TimerState::Running {
197            return;
198        }
199        if self.timer.state == TimerState::Finished {
200            self.timer.reset();
201        }
202        if self.timer.mode == TimerMode::Focus {
203            self.auto_pick_task_if_needed();
204        }
205        self.timer.start();
206        if self.data.sound_enabled {
207            sound::play_start();
208        }
209        self.set_status("Timer started.", false);
210    }
211
212    pub fn pause_timer(&mut self) {
213        if self.timer.state != TimerState::Running {
214            return;
215        }
216        let elapsed = self.timer.current_elapsed_seconds();
217        self.timer.pause();
218        if self.data.sound_enabled {
219            sound::play_pause();
220        }
221        let active_minutes = (elapsed / 60).max(1);
222        self.set_status(
223            format!(
224                "Paused at {} ({} min in).",
225                self.timer.format_remaining(),
226                active_minutes
227            ),
228            false,
229        );
230    }
231
232    pub fn reset_timer(&mut self) {
233        self.timer.reset();
234        self.set_status("Timer reset.", false);
235    }
236
237    pub fn cycle_mode(&mut self) {
238        if self.timer.state == TimerState::Running || self.timer.state == TimerState::Paused {
239            self.set_status("Stop the timer before changing mode.", true);
240            return;
241        }
242        let next = match self.timer.mode {
243            TimerMode::Focus => TimerMode::ShortBreak,
244            TimerMode::ShortBreak => TimerMode::LongBreak,
245            TimerMode::LongBreak => TimerMode::Custom,
246            TimerMode::Custom => TimerMode::Focus,
247        };
248        self.timer.configure(next);
249        self.set_status(format!("Mode: {}", next.label()), false);
250    }
251
252    pub fn adjust_minutes(&mut self, delta: i32) {
253        if self.timer.state == TimerState::Running || self.timer.state == TimerState::Paused {
254            self.set_status("Stop the timer before adjusting duration.", true);
255            return;
256        }
257        match self.timer.mode {
258            TimerMode::Focus => {
259                let cur = self.timer.config.focus_minutes as i32 + delta;
260                let v = cur.clamp(1, 240) as u32;
261                self.timer.set_focus_minutes(v);
262                self.data.focus_minutes = v;
263            }
264            TimerMode::ShortBreak => {
265                let cur = self.timer.config.short_break_minutes as i32 + delta;
266                let v = cur.clamp(1, 60) as u32;
267                self.timer.config.short_break_minutes = v;
268                self.data.short_break_minutes = v;
269                self.timer.total_seconds = self.timer.duration_seconds();
270            }
271            TimerMode::LongBreak => {
272                let cur = self.timer.config.long_break_minutes as i32 + delta;
273                let v = cur.clamp(1, 120) as u32;
274                self.timer.config.long_break_minutes = v;
275                self.data.long_break_minutes = v;
276                self.timer.total_seconds = self.timer.duration_seconds();
277            }
278            TimerMode::Custom => {
279                let cur = self.timer.custom_minutes as i32 + delta;
280                let v = cur.clamp(1, 240) as u32;
281                self.timer.set_custom_minutes(v);
282            }
283        }
284        if let Err(e) = self.db.persist_timer_settings(&self.data) {
285            self.set_status(format!("Save error: {e}"), true);
286        }
287        self.set_status(
288            format!(
289                "{} length: {} min",
290                self.timer.mode.label(),
291                self.timer.duration_seconds() / 60
292            ),
293            false,
294        );
295    }
296}