1use super::*;
2use crate::storage;
3
4impl App {
5 pub fn pending_task_ids(&self) -> Vec<u64> {
6 storage::sorted_pending_tasks(&self.data)
7 .into_iter()
8 .map(|t| t.id)
9 .collect()
10 }
11
12 pub fn dashboard_selected_task_id(&self) -> Option<u64> {
13 let pending = self.dashboard_tasks();
14 if pending.is_empty() {
15 None
16 } else {
17 let idx = self.dashboard_task_selected.min(pending.len() - 1);
18 Some(pending[idx].id)
19 }
20 }
21
22 pub(crate) fn clamp_dashboard_task_selection(&mut self) {
23 let n = self.dashboard_tasks().len();
24 if n == 0 {
25 self.dashboard_task_selected = 0;
26 } else if self.dashboard_task_selected >= n {
27 self.dashboard_task_selected = n - 1;
28 }
29 }
30
31 pub(crate) fn clamp_task_selection_after_mutation(&mut self) {
32 let len = self.filtered_task_indices().len();
33 if len == 0 {
34 self.task_state.select(None);
35 } else {
36 let sel = self.task_state.selected().unwrap_or(0).min(len - 1);
37 self.task_state.select(Some(sel));
38 }
39 }
40
41 pub(crate) fn move_dashboard_task_selection(&mut self, delta: i32) {
42 let count = self.dashboard_tasks().len();
43 if count == 0 {
44 return;
45 }
46 let cur = self.dashboard_task_selected as i32;
47 let next = (cur + delta).rem_euclid(count as i32) as usize;
48 self.dashboard_task_selected = next;
49 }
50
51 pub fn pending_task_count(&self) -> u32 {
52 storage::pending_tasks(&self.data).count() as u32
53 }
54
55 pub fn active_task_pending_index(&self) -> Option<u32> {
56 let id = self.active_task?;
57 storage::sorted_pending_tasks(&self.data)
58 .iter()
59 .position(|t| t.id == id)
60 .map(|i| i as u32)
61 }
62
63 pub fn active_task_progress(&self) -> Option<f64> {
64 let id = self.active_task?;
65 let task = self.data.tasks.iter().find(|t| t.id == id)?;
66 Some(task.progress_ratio())
67 }
68
69 pub(crate) fn matches_filter(&self, t: &crate::model::Task) -> bool {
70 if !self.task_search.is_empty() {
71 let q = self.task_search.to_lowercase();
72 let title_match = t.title.to_lowercase().contains(&q);
73 let notes_match = t.notes.to_lowercase().contains(&q);
74 let tags_match = t.tags.iter().any(|tag| tag.to_lowercase().contains(&q));
75 if !title_match && !notes_match && !tags_match {
76 return false;
77 }
78 }
79 if let Some(ref tag) = self.active_tag_filter {
80 if !t.tags.iter().any(|t| t == tag) {
81 return false;
82 }
83 }
84 match self.task_filter {
85 TaskFilter::All => true,
86 TaskFilter::Pending => t.status != crate::model::TaskStatus::Done,
87 TaskFilter::Done => t.status == crate::model::TaskStatus::Done,
88 TaskFilter::Today => t.today && t.status != crate::model::TaskStatus::Done,
89 }
90 }
91
92 pub fn filtered_task_indices(&self) -> Vec<usize> {
93 self.data
94 .tasks
95 .iter()
96 .enumerate()
97 .filter(|(_, t)| self.matches_filter(t))
98 .map(|(i, _)| i)
99 .collect()
100 }
101
102 pub fn dashboard_tasks(&self) -> Vec<&crate::model::Task> {
103 let mut tasks: Vec<_> = self
104 .filtered_task_indices()
105 .into_iter()
106 .map(|i| &self.data.tasks[i])
107 .collect();
108 tasks.sort_by(|a, b| {
109 b.priority
110 .rank()
111 .cmp(&a.priority.rank())
112 .then(b.today.cmp(&a.today))
113 .then(a.sort_order.cmp(&b.sort_order))
114 });
115 tasks
116 }
117
118 pub fn set_active_task(&mut self, id: Option<u64>) {
119 if let Some(id) = id {
120 if self
121 .data
122 .tasks
123 .iter()
124 .find(|t| t.id == id)
125 .is_some_and(|t| t.status == crate::model::TaskStatus::Done)
126 {
127 self.set_status("That task is done — pick another.", true);
128 return;
129 }
130 self.persist_data(|db, data| storage::promote_task_on_activate(db, data, id));
131 }
132 self.active_task = id;
133 self.data.active_task_id = id;
134 self.persist(|db| db.persist_active_task(id));
135 }
136
137 pub fn cycle_active_task_status(&mut self) {
138 let Some(id) = self.active_task else {
139 self.set_status("No active task — set one on Tasks (Space).", true);
140 return;
141 };
142 self.persist_data(|db, data| storage::cycle_task_status(db, data, id));
143 let status = self
144 .data
145 .tasks
146 .iter()
147 .find(|t| t.id == id)
148 .map(|t| t.status);
149 let Some(status) = status else {
150 return;
151 };
152 if status == crate::model::TaskStatus::Done {
153 self.active_task = None;
154 self.data.active_task_id = None;
155 self.persist(|db| db.persist_active_task(None));
156 self.maybe_advance_task();
157 }
158 self.bump_data();
159 self.set_status(format!("Task status: {}", status.label()), false);
160 if status == crate::model::TaskStatus::Done {
161 self.check_queue_empty();
162 }
163 }
164
165 pub fn mark_active_task_done(&mut self) {
166 let Some(id) = self.active_task else {
167 self.set_status("No active task — set one on Tasks (Space).", true);
168 return;
169 };
170 self.persist_data(|db, data| storage::mark_task_done(db, data, id));
171 self.active_task = None;
172 self.data.active_task_id = None;
173 self.persist(|db| db.persist_active_task(None));
174 self.bump_data();
175 self.maybe_advance_task();
176 self.set_status("Task marked done.", false);
177 self.check_queue_empty();
178 }
179
180 pub(crate) fn auto_pick_task_if_needed(&mut self) {
181 if self.active_task.is_some() || !self.data.auto_pick_task {
182 return;
183 }
184 if let Some(id) = storage::pick_best_task(&self.data) {
185 self.set_active_task(Some(id));
186 }
187 }
188
189 pub(crate) fn maybe_advance_task(&mut self) {
190 if !self.data.auto_advance_task {
191 return;
192 }
193 let next = storage::advance_to_next_task(&self.data, self.active_task);
194 self.set_active_task(next);
195 if let Some(id) = next {
196 if let Some(t) = self.data.tasks.iter().find(|t| t.id == id) {
197 self.set_status(format!("Next task: {}", t.title), false);
198 }
199 }
200 }
201
202 pub fn start_focus_on_task(&mut self, id: u64) {
203 if self
204 .data
205 .tasks
206 .iter()
207 .find(|t| t.id == id)
208 .is_some_and(|t| t.status == crate::model::TaskStatus::Done)
209 {
210 self.set_status("That task is done — pick another.", true);
211 return;
212 }
213 self.set_active_task(Some(id));
214 self.tab = FocusTab::Dashboard;
215 if self.timer.mode != TimerMode::Focus {
216 self.timer.configure(TimerMode::Focus);
217 }
218 self.start_timer();
219 }
220
221 pub fn cycle_task_filter(&mut self) {
222 self.task_filter = self.task_filter.next();
223 self.task_state.select(Some(0));
224 self.set_status(format!("Filter: {}", self.task_filter.label()), false);
225 }
226}