prehnite/app/window/main_window/page/
item_list.rs1use crate::widget::hideable;
2use crate::widget::styles::container::{focusable, not_focused_rect_box, rect_box, unborder};
3use iced::widget::pane_grid::{Axis, ResizeEvent};
4use iced::widget::{button, pane_grid, scrollable, space, Container, MouseArea};
5use iced::{padding, widget, Element, Length};
6use iced_aw::menu_items;
7use iced_aw::{menu_bar, Menu};
8use indexmap::IndexMap;
9use prehnite_core::db::schema::{Headline, Item, ItemType, Paragraph, ParagraphSummary};
10use prehnite_core::db::{acquire_book_or_alert, query};
11use prehnite_core::font::material_symbol;
12use prehnite_core::font::material_symbol::CIRCLE;
13use prehnite_core::font::widget::material_symbol;
14use prehnite_core::i18n::{i18n, i18n_w};
15use prehnite_core::widget::font::ftext;
16use tracing::error;
17use tracing_unwrap::ResultExt;
18
19#[derive(Clone, Debug)]
20pub enum ItemListMessage {
21 LoadItems,
22 ItemListPaneResized(ResizeEvent),
23 SetHeadlines(IndexMap<i64, Item>),
24 SetParagraph(IndexMap<i64, IndexMap<i64, Item>>),
25 ItemSelected(i64),
26 OpenEditor(i64),
27 NewParagraph(i64 ),
28 NewHeadline(Option<i64> ),
29 NewItemCreated(i64),
30 None,
31}
32
33pub enum ItemListActions {
34 Run(iced::Task<ItemListMessage>),
35}
36
37#[derive(Clone, Debug)]
38enum ItemListPane {
39 PaneList,
40 PaneDetails,
41}
42
43#[derive(Debug, Clone)]
44pub struct ItemList {
45 headlines: IndexMap<i64 , Item>,
46 paragraph: IndexMap<i64 , IndexMap<i64 , Item>>,
47 focused_item_id: Option<i64>,
48 per_page: u8,
49 page: u32,
50 item_list_pane: pane_grid::State<ItemListPane>,
51 not_opened: bool,
52}
53
54impl Default for ItemList {
55 fn default() -> Self {
56 let (mut item_list_pane, pane) = pane_grid::State::new(ItemListPane::PaneList);
57 item_list_pane.split(Axis::Vertical, pane, ItemListPane::PaneDetails);
58 Self {
59 headlines: Default::default(),
60 paragraph: Default::default(),
61 focused_item_id: None,
62 per_page: 10,
63 page: 0,
64 item_list_pane,
65 not_opened: false,
66 }
67 }
68}
69
70impl ItemList {
71 pub fn not_opened() -> Self {
72 Self {
73 not_opened: true,
74 ..Default::default()
75 }
76 }
77
78 #[tracing::instrument]
79 async fn load_headlines(page: u32, per_page: u8) -> ItemListMessage {
80 ItemListMessage::SetHeadlines(
81 query::fetch_root_headline_items(
82 acquire_book_or_alert().await.as_mut(),
83 per_page,
84 page,
85 )
86 .await
87 .unwrap_or_else(|e| {
88 error!("Failed to fetch headlines. Error: {e:#?}");
89 Default::default()
90 }),
91 )
92 }
93
94 #[tracing::instrument]
95 async fn load_paragraph(page: u32, per_page: u8) -> ItemListMessage {
96 let mut conn = acquire_book_or_alert().await;
97 let mut res = query::fetch_root_headline_related_paragraph(&mut conn, per_page, page)
98 .await
99 .unwrap_or_else(|e| {
100 error!("Failed to fetch paragraph of headline related. Error: {e:#?}");
101 Default::default()
102 });
103 for i in res.values_mut() {
104 for x in i.values_mut() {
105 match &mut x.item_type {
106 ItemType::Headline(_) => {}
107 ItemType::Paragraph(v) => {
108 if let Some(v) = v {
109 v.load_summary(&mut conn).await.unwrap_or_else(|e| {
110 error!(
111 "Failed to fetch references of paragraph related. Error: {e:#?}"
112 );
113 });
114 }
115 }
116 }
117 }
118 }
119 ItemListMessage::SetParagraph(res)
120 }
121
122 pub fn update(&mut self, msg: ItemListMessage) -> ItemListActions {
123 match msg {
124 ItemListMessage::ItemListPaneResized(ResizeEvent { split, ratio }) => {
125 if ratio > 0.33 && ratio < 0.66 {
126 self.item_list_pane.resize(split, ratio);
127 }
128 }
129 ItemListMessage::LoadItems => {
130 return ItemListActions::Run(
131 iced::Task::future(Self::load_headlines(self.page, self.per_page)).chain(
132 iced::Task::future(Self::load_paragraph(self.page, self.per_page)),
133 ),
134 );
135 }
136 ItemListMessage::SetHeadlines(v) => {
137 self.headlines = v;
138 }
139 ItemListMessage::SetParagraph(v) => self.paragraph = v,
140 ItemListMessage::ItemSelected(id) => self.focused_item_id = Some(id),
141 ItemListMessage::OpenEditor(_) => { }
142 ItemListMessage::NewParagraph(parent_item_id) => {
143 let headline_id = match self
144 .get_item_paragraph_or_headline(Some(parent_item_id))
145 .map(|v| &v.item_type)
146 {
147 Some(ItemType::Paragraph(Some(p))) => p.headline.id,
148 Some(ItemType::Headline(Some(h))) => h.id,
149 _ => return ItemListActions::Run(iced::Task::none()),
150 };
151 return ItemListActions::Run(iced::Task::future(async move {
152 let item = Item {
153 item_type: ItemType::Paragraph(None),
154 title: i18n("no-title"),
155 ..Default::default()
156 };
157 let mut conn = acquire_book_or_alert().await;
158 if let Some(item) = item.register(&mut *conn, false).await.ok_or_log() {
159 let paragraph = Paragraph {
160 item_id: item.id,
161 headline: Headline {
162 id: headline_id,
163 ..Default::default()
164 },
165 ..Paragraph::default()
166 };
167 if let Some(_) = paragraph.register(&mut *conn, true).await.ok_or_log() {
168 return ItemListMessage::NewItemCreated(item.id);
169 }
170 }
171 ItemListMessage::None
172 }));
173 }
174 ItemListMessage::NewHeadline(parent_item_id) => {
175 let headline_id = match self
176 .get_item_paragraph_or_headline(parent_item_id)
177 .map(|v| &v.item_type)
178 {
179 Some(ItemType::Paragraph(Some(p))) => Some(p.headline.id),
180 Some(ItemType::Headline(Some(h))) => Some(h.id),
181 None => None,
182 _ => return ItemListActions::Run(iced::Task::none()),
183 };
184 return ItemListActions::Run(iced::Task::future(async move {
185 let item = Item {
186 item_type: ItemType::Headline(None),
187 title: i18n("no-title"),
188 ..Default::default()
189 };
190 let mut conn = acquire_book_or_alert().await;
191 if let Some(item) = item.register(&mut *conn, false).await.ok_or_log() {
192 let headline = Headline {
193 item_id: item.id,
194 parent_id: headline_id,
195 ..Headline::default()
196 };
197 if let Some(_) = headline.register(&mut *conn, true).await.ok_or_log() {
198 return ItemListMessage::NewItemCreated(item.id);
199 }
200 }
201 ItemListMessage::None
202 }));
203 }
204 ItemListMessage::None => {}
205 ItemListMessage::NewItemCreated(id) => {
206 return ItemListActions::Run(
207 iced::Task::future(Self::load_headlines(self.page, self.per_page))
208 .chain(iced::Task::future(Self::load_paragraph(
209 self.page,
210 self.per_page,
211 )))
212 .chain(iced::Task::done(ItemListMessage::ItemSelected(id)))
213 .chain(iced::Task::done(ItemListMessage::OpenEditor(id))),
214 );
215 }
216 }
217 ItemListActions::Run(iced::Task::none())
218 }
219
220 pub fn summary<'a>(summary: ParagraphSummary) -> Element<'a, ItemListMessage> {
221 widget::row![material_symbol(CIRCLE), ftext(summary.title)].into()
222 }
223
224 pub fn item(item: &'_ Item, focused: bool) -> Element<'_, ItemListMessage> {
225 let paragraph = item.item_type.clone().paragraph_or_none();
226 let is_summary_visible = paragraph.is_some();
227 MouseArea::new(
228 Container::new(widget::column![
229 Container::new(widget::column![
230 ftext(item.title.clone()).size(match item.item_type {
231 ItemType::Headline(_) => 24,
232 ItemType::Paragraph(_) => 18,
233 }),
234 hideable(
235 widget::column(
236 paragraph
237 .and_then(|v| v.summary)
238 .map(|v| v
239 .into_iter()
240 .map(Self::summary)
241 .collect::<Vec<Element<'_, ItemListMessage>>>())
242 .unwrap_or_default()
243 )
244 .padding(padding::left(40)),
245 is_summary_visible
246 )
247 ])
248 .style(unborder(focusable(focused)))
249 .width(Length::Fill),
250 widget::rule::horizontal(1)
251 ])
252 .padding(padding::left(match item.item_type {
253 ItemType::Headline(_) => 0,
254 ItemType::Paragraph(_) => 20,
255 }))
256 .width(Length::Fill),
257 )
258 .on_press(ItemListMessage::ItemSelected(item.id))
259 .into()
260 }
261
262 pub fn item_list_panel(&'_ self) -> Element<'_, ItemListMessage> {
263 if self.not_opened {
264 widget::container(i18n_w("not-opened"))
265 .center(Length::Fill)
266 .into()
267 } else {
268 widget::column![
269 widget::column![
270 widget::row![
271 space().width(Length::Fill),
272 menu_bar!((
273 button(material_symbol(material_symbol::ADD).size(20))
274 .style(button::text),
275 {
276 Menu::new(menu_items!(
277 (button(i18n_w("new-parent-headline"))
278 .style(button::text)
279 .on_press(ItemListMessage::NewHeadline(None))),
280 (button(i18n_w("new-headline"))
281 .style(button::text)
282 .on_press_maybe(
283 self.focused_item_id
284 .map(|v| ItemListMessage::NewHeadline(Some(v)))
285 )),
286 (button(i18n_w("new-paragraph"))
287 .style(button::text)
288 .on_press_maybe(
289 self.focused_item_id
290 .map(|v| ItemListMessage::NewParagraph(v))
291 ))
292 ))
293 .width(Length::Shrink)
294 }
295 ))
296 ]
297 .width(Length::Fill),
298 widget::rule::horizontal(1)
299 ],
300 scrollable(
301 Container::new(widget::column(self.headlines.iter().map(|(id, itm)| {
302 widget::column![
303 Self::item(itm, Some(*id) == self.focused_item_id),
304 match self.paragraph.get(id) {
305 None => {
306 Element::from(space())
307 }
308 Some(v) => {
309 widget::column(v.iter().map(|(_, itm)| {
310 Self::item(itm, Some(itm.id) == self.focused_item_id)
311 }))
312 .into()
313 }
314 }
315 ]
316 .into()
317 })))
318 .width(Length::Fill)
319 .height(Length::Fill)
320 .padding(5)
321 .style(unborder(not_focused_rect_box))
322 )
323 .spacing(1)
324 ]
325 .into()
326 }
327 }
328
329 fn get_focused_item(&'_ self) -> Option<&'_ Item> {
330 self.get_item_paragraph_or_headline(self.focused_item_id)
331 }
332
333 fn get_item_paragraph_or_headline(&'_ self, id: Option<i64>) -> Option<&'_ Item> {
334 let id = id?;
335 self.headlines
336 .get(&id)
337 .or_else(|| self.paragraph.values().find_map(|v| v.get(&id)))
338 }
339
340 fn item_detail(item: &Item) -> Element<'_, ItemListMessage> {
341 widget::column![
342 i18n_w(item.item_type.as_ref()),
343 ftext(&item.title),
344 match item.item_type.clone() {
345 ItemType::Headline(_) => {
346 None
347 }
348 ItemType::Paragraph(p) => {
349 p.and_then(|p| p.accepted_draft.map(|d| Element::from(ftext(d.body))))
350 }
351 }
352 .unwrap_or(Element::new(widget::space()))
353 ]
354 .into()
355 }
356
357 pub fn item_detail_panel(&'_ self) -> Element<'_, ItemListMessage> {
358 scrollable(
359 Container::new(match self.get_focused_item() {
360 None => i18n_w("item-no-select").into(),
361 Some(item) => Self::item_detail(item),
362 })
363 .width(Length::Fill)
364 .height(Length::Fill)
365 .padding(5)
366 .style(unborder(rect_box)),
367 )
368 .spacing(1)
369 .into()
370 }
371
372 pub fn view(&'_ self) -> Element<'_, ItemListMessage> {
373 widget::pane_grid(&self.item_list_pane, |_, state, _| {
374 pane_grid::Content::new(match state {
375 ItemListPane::PaneList => self.item_list_panel(),
376 ItemListPane::PaneDetails => self.item_detail_panel(),
377 })
378 })
379 .spacing(2)
380 .on_resize(10, ItemListMessage::ItemListPaneResized)
381 .into()
382 }
383}