prehnite/app/window/main_window/page/
item_list.rs

1use 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 /* headline-id */),
28    NewHeadline(Option<i64> /* parent-id */),
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_id */, Item>,
46    paragraph: IndexMap<i64 /* headline_id */, IndexMap<i64 /* item_id */, 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(_) => { /* handled by daemon */ }
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}