1use std::{collections::HashMap, sync::Arc};
2
3use iocraft::prelude::*;
4
5use crate::{
6 common::ICONS,
7 ids::ThingsId,
8 store::{Task, ThingsStore},
9 ui::components::{
10 empty_text::EmptyText,
11 task_group::{TaskGroup, TaskGroupHeader},
12 tasks::TaskOptions,
13 },
14};
15
16const MAX_GROUP_ITEMS: usize = 3;
17const LIST_INDENT: u32 = 2;
18
19#[derive(Default)]
20struct AreaTaskGroup {
21 tasks: Vec<usize>,
22 projects: Vec<(ThingsId, Vec<usize>)>,
23 project_pos: HashMap<ThingsId, usize>,
24}
25
26#[derive(Default)]
27struct Grouped {
28 unscoped: Vec<usize>,
29 project_only: Vec<(ThingsId, Vec<usize>)>,
30 by_area: Vec<(ThingsId, AreaTaskGroup)>,
31}
32
33fn ensure_area_group(
34 grouped: &mut Grouped,
35 area_pos: &mut HashMap<ThingsId, usize>,
36 area_uuid: &ThingsId,
37) -> usize {
38 if let Some(i) = area_pos.get(area_uuid).copied() {
39 return i;
40 }
41 let i = grouped.by_area.len();
42 grouped
43 .by_area
44 .push((area_uuid.clone(), AreaTaskGroup::default()));
45 area_pos.insert(area_uuid.clone(), i);
46 i
47}
48
49fn ensure_project_group(
50 grouped: &mut Vec<(ThingsId, Vec<usize>)>,
51 project_pos: &mut HashMap<ThingsId, usize>,
52 project_uuid: &ThingsId,
53) -> usize {
54 if let Some(i) = project_pos.get(project_uuid).copied() {
55 return i;
56 }
57 let i = grouped.len();
58 grouped.push((project_uuid.clone(), Vec::new()));
59 project_pos.insert(project_uuid.clone(), i);
60 i
61}
62
63fn group_items(items: &[Task], store: &ThingsStore) -> Grouped {
64 let mut grouped = Grouped::default();
65 let mut project_only_pos: HashMap<ThingsId, usize> = HashMap::new();
66 let mut by_area_pos: HashMap<ThingsId, usize> = HashMap::new();
67
68 for (idx, task) in items.iter().enumerate() {
69 let project_uuid = store.effective_project_uuid(task);
70 let area_uuid = store.effective_area_uuid(task);
71
72 match (project_uuid, area_uuid) {
73 (Some(project_uuid), Some(area_uuid)) => {
74 let area_idx = ensure_area_group(&mut grouped, &mut by_area_pos, &area_uuid);
75 let area_group = &mut grouped.by_area[area_idx].1;
76 let project_idx = ensure_project_group(
77 &mut area_group.projects,
78 &mut area_group.project_pos,
79 &project_uuid,
80 );
81 area_group.projects[project_idx].1.push(idx);
82 }
83 (Some(project_uuid), None) => {
84 let project_idx = ensure_project_group(
85 &mut grouped.project_only,
86 &mut project_only_pos,
87 &project_uuid,
88 );
89 grouped.project_only[project_idx].1.push(idx);
90 }
91 (None, Some(area_uuid)) => {
92 let area_idx = ensure_area_group(&mut grouped, &mut by_area_pos, &area_uuid);
93 grouped.by_area[area_idx].1.tasks.push(idx);
94 }
95 (None, None) => grouped.unscoped.push(idx),
96 }
97 }
98
99 grouped
100}
101
102fn id_prefix_len(store: &ThingsStore, items: &[Task], grouped: &Grouped) -> usize {
103 let mut ids: Vec<ThingsId> = items.iter().map(|t| t.uuid.clone()).collect();
104 for (project_uuid, _) in &grouped.project_only {
105 ids.push(project_uuid.clone());
106 }
107 for (area_uuid, area_group) in &grouped.by_area {
108 ids.push(area_uuid.clone());
109 for (project_uuid, _) in &area_group.projects {
110 ids.push(project_uuid.clone());
111 }
112 }
113 store.unique_prefix_length(&ids)
114}
115
116fn limited(tasks: &[usize]) -> (&[usize], usize) {
117 let shown = tasks.len().min(MAX_GROUP_ITEMS);
118 (&tasks[..shown], tasks.len().saturating_sub(shown))
119}
120
121#[derive(Default, Props)]
122pub struct AnytimeViewProps<'a> {
123 pub items: Option<&'a Vec<Task>>,
124 pub detailed: bool,
125}
126
127#[component]
128pub fn AnytimeView<'a>(hooks: Hooks, props: &AnytimeViewProps<'a>) -> impl Into<AnyElement<'a>> {
129 let store = hooks.use_context::<Arc<ThingsStore>>().clone();
130 let Some(items) = props.items else {
131 return element! { Text(content: "") }.into_any();
132 };
133
134 let content: AnyElement<'a> = {
135 if items.is_empty() {
136 element! { EmptyText(content: "Anytime is empty.") }.into_any()
137 } else {
138 let grouped = group_items(items, store.as_ref());
139 let prefix_len = id_prefix_len(store.as_ref(), items, &grouped);
140 let options = TaskOptions {
141 detailed: props.detailed,
142 show_project: false,
143 show_area: false,
144 show_today_markers: true,
145 show_staged_today_marker: false,
146 };
147
148 struct BlockSpec<'b> {
149 header: Option<TaskGroupHeader>,
150 items: Vec<&'b Task>,
151 indent_under_header: u16,
152 hidden_count: usize,
153 extra_left: u32,
154 }
155
156 enum Entry<'b> {
157 Sep,
158 Block(BlockSpec<'b>),
159 }
160
161 let mut blocks: Vec<AnyElement<'a>> = Vec::new();
162 let mut entries: Vec<Entry<'a>> = Vec::new();
163 let mut first = true;
164
165 if !grouped.unscoped.is_empty() {
166 entries.push(Entry::Block(BlockSpec {
167 header: None,
168 items: grouped.unscoped.iter().map(|&i| &items[i]).collect(),
169 indent_under_header: 0u16,
170 hidden_count: 0usize,
171 extra_left: 0,
172 }));
173 first = false;
174 }
175
176 for (project_uuid, tasks) in &grouped.project_only {
177 if !first {
178 entries.push(Entry::Sep);
179 }
180 let (shown, hidden) = limited(tasks);
181 entries.push(Entry::Block(BlockSpec {
182 header: Some(TaskGroupHeader::Project {
183 project_uuid: project_uuid.clone(),
184 title: store.resolve_project_title(project_uuid),
185 id_prefix_len: prefix_len,
186 }),
187 items: shown.iter().map(|&i| &items[i]).collect(),
188 indent_under_header: 2u16,
189 hidden_count: hidden,
190 extra_left: 0,
191 }));
192 first = false;
193 }
194
195 for (area_uuid, area_group) in &grouped.by_area {
196 if !first {
197 entries.push(Entry::Sep);
198 }
199
200 let (shown_area_tasks, hidden_area_tasks) = limited(&area_group.tasks);
201
202 entries.push(Entry::Block(BlockSpec {
203 header: Some(TaskGroupHeader::Area {
204 area_uuid: area_uuid.clone(),
205 title: store.resolve_area_title(area_uuid),
206 id_prefix_len: prefix_len,
207 }),
208 items: shown_area_tasks.iter().map(|&i| &items[i]).collect(),
209 indent_under_header: 2u16,
210 hidden_count: hidden_area_tasks,
211 extra_left: 0,
212 }));
213
214 for (project_uuid, project_tasks) in &area_group.projects {
215 let (shown, hidden) = limited(project_tasks);
216 entries.push(Entry::Block(BlockSpec {
217 header: Some(TaskGroupHeader::Project {
218 project_uuid: project_uuid.clone(),
219 title: store.resolve_project_title(project_uuid),
220 id_prefix_len: prefix_len,
221 }),
222 items: shown.iter().map(|&i| &items[i]).collect(),
223 indent_under_header: 2u16,
224 hidden_count: hidden,
225 extra_left: 2,
226 }));
227 }
228
229 first = false;
230 }
231
232 for entry in entries {
233 match entry {
234 Entry::Sep => {
235 blocks.push(
236 element! { Text(content: "", wrap: TextWrap::NoWrap) }.into_any(),
237 );
238 }
239 Entry::Block(spec) => {
240 let block = element! {
241 TaskGroup(
242 header: spec.header,
243 items: spec.items,
244 id_prefix_len: prefix_len,
245 options: options,
246 indent_under_header: spec.indent_under_header,
247 hidden_count: spec.hidden_count,
248 )
249 }
250 .into_any();
251
252 if spec.extra_left > 0 {
253 blocks.push(
254 element! {
255 View(padding_left: spec.extra_left) { #(Some(block)) }
256 }
257 .into_any(),
258 );
259 } else {
260 blocks.push(block);
261 }
262 }
263 }
264 }
265
266 element! {
267 View(flex_direction: FlexDirection::Column, gap: 1) {
268 Text(
269 content: format!("{} Anytime ({} tasks)", ICONS.anytime, items.len()),
270 wrap: TextWrap::NoWrap,
271 color: Color::Cyan,
272 weight: Weight::Bold,
273 )
274 View(flex_direction: FlexDirection::Column, padding_left: LIST_INDENT) {
275 #(blocks)
276 }
277 }
278 }
279 .into_any()
280 }
281 };
282
283 content
284}