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