prehnite/app/window/license_info_window/
mod.rs

1use crate::app::resources::app_icon_handle;
2use crate::app::window::{Window, WindowMessage};
3use crate::widget::styles::container::{focusable, not_focused_rect_box};
4use crate::widget::{copy_button, hideable};
5use iced::alignment::Horizontal;
6use iced::widget::pane_grid::Axis;
7use iced::widget::text::Wrapping;
8use iced::widget::{button, pane_grid, scrollable, span, text_input, Container, MouseArea};
9use iced::window::Id;
10use iced::{padding, widget, Alignment, Background, Color, Element, Length, Task};
11use license::license_bundle;
12use opener::open_browser;
13use prehnite_core::font::material_symbol::ARROW_UPWARD;
14use prehnite_core::font::widget::material_symbol;
15use prehnite_core::i18n::{i18n, i18n_w};
16use prehnite_core::license_bundle::Package;
17use prehnite_core::util::alert::alert_i18n;
18use prehnite_core::widget::font::{ftext, get_font};
19use prehnite_core::widget::text::TextBuilder;
20use prehnite_core::MessageLevel;
21use std::collections::{BTreeMap, BTreeSet};
22use tracing::error;
23use tracing::log::warn;
24
25pub mod license;
26
27const ROOT_PACKAGE_NAME: &str = "prehnite";
28
29#[derive(Clone, Debug)]
30pub enum LicenseInfoWindowMessage {
31    LoadLicenseBundle,
32    UpdateLicenseBundle(BTreeMap<String, Package>),
33    PkgBack,
34    PkgHome,
35    SearchTextOnChanged(String),
36    ChangeTarget(String),
37    ChangeSelectedTarget(String),
38    PageChanged,
39    OpenWelcomeMessage,
40    SetClipboard(String),
41    LinkOnClick(String),
42}
43
44impl From<LicenseInfoWindowMessage> for WindowMessage {
45    fn from(value: LicenseInfoWindowMessage) -> Self {
46        WindowMessage::LicenseInfoWindowMessage(value)
47    }
48}
49
50#[derive(Debug)]
51enum WindowPane {
52    List,
53    Detail,
54}
55
56#[derive(Debug)]
57pub struct LicenseInfoWindow {
58    packages: Option<BTreeMap<String, Package>>,
59    window_id: Option<Id>,
60    dep_package_list: BTreeSet<String>,
61    selected_package: String,
62    search_text_history: Vec<String>,
63    target_package_history: Vec<String>,
64    pane_state: pane_grid::State<WindowPane>,
65}
66
67impl LicenseInfoWindow {
68    fn software_list_row(&self, pkg_name: String) -> Element<'_, LicenseInfoWindowMessage> {
69        let pkg_name2 = pkg_name.clone();
70        MouseArea::new(
71            Container::new(widget::row![
72                ftext(pkg_name.clone()),
73                widget::space().width(Length::Fill),
74                iced_aw::badge(ftext(format!(
75                    "{}",
76                    match &self.packages {
77                        None => {
78                            0
79                        }
80                        Some(v) => {
81                            v.get(&pkg_name)
82                                .map(|v| v.dependencies.len())
83                                .unwrap_or_default()
84                        }
85                    }
86                )))
87                .style(|t, _| {
88                    iced_aw::badge::Style {
89                        background: Background::Color(t.palette().primary),
90                        border_radius: None,
91                        border_width: 0.0,
92                        border_color: None,
93                        text_color: t.palette().text,
94                    }
95                })
96                .width(32)
97                .align_x(Alignment::Center)
98                .align_y(Alignment::Center)
99            ])
100            .padding(5)
101            .width(Length::Fill)
102            .style(focusable(pkg_name.clone() == self.selected_package)),
103        )
104        .on_press(LicenseInfoWindowMessage::ChangeSelectedTarget(
105            pkg_name2.clone(),
106        ))
107        .on_double_click(LicenseInfoWindowMessage::ChangeTarget(pkg_name2))
108        .into()
109    }
110
111    fn software_list_pane(&self) -> Element<'_, LicenseInfoWindowMessage> {
112        widget::column![
113            widget::row![
114                button(material_symbol(ARROW_UPWARD))
115                    .style(button::text)
116                    .on_press_maybe(
117                        Some(LicenseInfoWindowMessage::PkgBack)
118                            .filter(|_| self.search_text_history.len() > 1)
119                    ),
120                text_input(
121                    i18n("search").as_str(),
122                    self.search_text_history
123                        .last()
124                        .cloned()
125                        .unwrap_or_default()
126                        .as_str()
127                )
128                .font(get_font())
129                .on_input(LicenseInfoWindowMessage::SearchTextOnChanged),
130                button(widget::image(app_icon_handle()).width(20).height(20))
131                    .style(button::text)
132                    .on_press(LicenseInfoWindowMessage::PkgHome)
133            ],
134            scrollable(widget::column(
135                self.dep_package_list
136                    .iter()
137                    .filter(|v| self
138                        .search_text_history
139                        .last()
140                        .map(|x| v.contains(x))
141                        .unwrap_or_default())
142                    .map(|v| self.software_list_row(v.clone()))
143            ))
144            .spacing(1)
145        ]
146        .into()
147    }
148
149    fn software_details_pane(&self) -> Element<'_, LicenseInfoWindowMessage> {
150        let package = match &self.packages {
151            None => return i18n_w("unknown").into(),
152            Some(v) => match v.get(&self.selected_package) {
153                None => return i18n_w("unknown").into(),
154                Some(v) => v.clone(),
155            },
156        };
157        let txt_header = TextBuilder::with_font().wrapping(Wrapping::None);
158        let txt_value = TextBuilder::with_font().wrapping(Wrapping::None);
159        widget::column![
160            scrollable(
161                widget::row![
162                    widget::column![
163                        txt_header.text(i18n("package-name")),
164                        txt_header.text(i18n("package-authors")),
165                        txt_header.text(i18n("package-homepage")),
166                        txt_header.text(i18n("package-repository")),
167                        txt_header.text(i18n("package-license")),
168                    ]
169                    .align_x(Horizontal::Center)
170                    .width(Length::Shrink),
171                    widget::space().width(20),
172                    widget::column![
173                        widget::row![
174                            txt_value.text(package.name.clone()),
175                            hideable(
176                                copy_button().on_press(LicenseInfoWindowMessage::SetClipboard(
177                                    package.name.clone(),
178                                )),
179                                !package.name.is_empty()
180                            )
181                        ],
182                        widget::row![
183                            txt_value.text({
184                                let v = package.authors.join(", ");
185                                if v.is_empty() { "-".to_string() } else { v }
186                            }),
187                            hideable(
188                                copy_button().on_press(LicenseInfoWindowMessage::SetClipboard(
189                                    package.authors.join(", "),
190                                )),
191                                !package.authors.is_empty()
192                            )
193                        ],
194                        txt_value
195                            .rich([package
196                                .homepage
197                                .clone()
198                                .map(|v| span(v).color(Color::from_rgb8(0, 0, 0xEE)).link(0))
199                                .unwrap_or(span::<i32, _>("-"))])
200                            .on_link_click(move |_| LicenseInfoWindowMessage::LinkOnClick(
201                                package.homepage.clone().unwrap_or_default()
202                            )),
203                        txt_value
204                            .rich([package
205                                .repository
206                                .clone()
207                                .map(|v| span(v).color(Color::from_rgb8(0, 0, 0xEE)).link(0))
208                                .unwrap_or(span::<i32, _>("-"))])
209                            .on_link_click(move |_| LicenseInfoWindowMessage::LinkOnClick(
210                                package.repository.clone().unwrap_or_default()
211                            )),
212                        widget::row![
213                            txt_value.text(package.license_info.clone()),
214                            hideable(
215                                copy_button().on_press(LicenseInfoWindowMessage::SetClipboard(
216                                    package.license_info.clone(),
217                                )),
218                                !package.license_info.is_empty()
219                            )
220                        ]
221                    ],
222                ]
223                .padding(padding::bottom(10))
224            )
225            .horizontal(),
226            widget::rule::horizontal(1),
227            widget::column(package.licenses.into_iter().map(|v| {
228                widget::column![widget::text(v.full_text), widget::rule::horizontal(1),].into()
229            }),),
230        ]
231        .into()
232    }
233
234    #[tracing::instrument]
235    fn update_impl(&mut self, msg: LicenseInfoWindowMessage) -> Task<LicenseInfoWindowMessage> {
236        match msg {
237            LicenseInfoWindowMessage::LoadLicenseBundle => {
238                return Task::future(async {
239                    LicenseInfoWindowMessage::UpdateLicenseBundle(
240                        license_bundle()
241                            .into_iter()
242                            .map(|v| (v.name.clone(), v))
243                            .collect(),
244                    )
245                });
246            }
247            LicenseInfoWindowMessage::UpdateLicenseBundle(v) => {
248                self.packages = Some(v);
249                return Task::done(LicenseInfoWindowMessage::PageChanged);
250            }
251            LicenseInfoWindowMessage::PkgBack => {
252                if self.search_text_history.len() > 1 {
253                    self.search_text_history.pop();
254                }
255                if self.target_package_history.len() > 1 {
256                    self.target_package_history.pop();
257                }
258                return Task::done(LicenseInfoWindowMessage::PageChanged);
259            }
260            LicenseInfoWindowMessage::SearchTextOnChanged(v) => {
261                match self.search_text_history.last_mut() {
262                    None => {}
263                    Some(x) => *x = v,
264                };
265            }
266            LicenseInfoWindowMessage::ChangeTarget(v) => {
267                match &self.packages {
268                    None => {}
269                    Some(packages) => {
270                        if packages
271                            .get(&v)
272                            .map(|pkg| pkg.dependencies.is_empty())
273                            .unwrap_or_default()
274                        {
275                            return Task::none();
276                        }
277                    }
278                };
279                self.target_package_history.push(v);
280                self.search_text_history.push("".to_string());
281                return Task::done(LicenseInfoWindowMessage::PageChanged);
282            }
283            LicenseInfoWindowMessage::PageChanged => {
284                self.dep_package_list = match &self.packages {
285                    None => None,
286                    Some(v) => v
287                        .get(
288                            &self
289                                .target_package_history
290                                .last()
291                                .cloned()
292                                .unwrap_or_default(),
293                        )
294                        .cloned(),
295                }
296                .map(|v| v.dependencies.clone())
297                .unwrap_or_default();
298            }
299            LicenseInfoWindowMessage::PkgHome => {
300                self.target_package_history.clear();
301                self.search_text_history.clear();
302                self.target_package_history
303                    .push(ROOT_PACKAGE_NAME.to_string());
304                self.search_text_history.push("".to_string());
305                self.selected_package = ROOT_PACKAGE_NAME.to_string();
306                return Task::done(LicenseInfoWindowMessage::PageChanged);
307            }
308            LicenseInfoWindowMessage::ChangeSelectedTarget(v) => {
309                self.selected_package = v;
310            }
311            LicenseInfoWindowMessage::OpenWelcomeMessage => {
312                return alert_i18n(
313                    self.window_id,
314                    ("info", "license-info_message"),
315                    MessageLevel::Info,
316                );
317            }
318            LicenseInfoWindowMessage::SetClipboard(v) => {
319                return iced::clipboard::write(v);
320            }
321            LicenseInfoWindowMessage::LinkOnClick(url) => {
322                open_browser(url)
323                    .inspect_err(|e| warn!("Failed to open browser. E: {e:?}"))
324                    .ok();
325            }
326        }
327        Task::none()
328    }
329}
330
331impl Window for LicenseInfoWindow {
332    fn new() -> Self
333    where
334        Self: Sized,
335    {
336        let (mut pane_state, pane) = pane_grid::State::new(WindowPane::List);
337        match pane_state.split(Axis::Vertical, pane, WindowPane::Detail) {
338            None => {}
339            Some((_, split)) => pane_state.resize(split, 0.33),
340        }
341        Self {
342            packages: None,
343            window_id: None,
344            dep_package_list: BTreeSet::new(),
345            selected_package: ROOT_PACKAGE_NAME.to_string(),
346            search_text_history: vec!["".to_string()],
347            target_package_history: vec![ROOT_PACKAGE_NAME.to_string()],
348            pane_state,
349        }
350    }
351
352    fn init_task() -> Task<WindowMessage>
353    where
354        Self: Sized,
355    {
356        Task::done(LicenseInfoWindowMessage::LoadLicenseBundle.into()).chain(Task::done(
357            LicenseInfoWindowMessage::OpenWelcomeMessage.into(),
358        ))
359    }
360
361    fn update(&mut self, message: WindowMessage) -> Task<WindowMessage> {
362        if let WindowMessage::LicenseInfoWindowMessage(message) = message {
363            self.update_impl(message).map(|v| v.into())
364        } else {
365            error!("Invalid message received.");
366            Task::none()
367        }
368    }
369
370    fn view(&'_ self) -> Element<'_, WindowMessage> {
371        widget::pane_grid(&self.pane_state, |_, state, _| {
372            pane_grid::Content::new(match state {
373                WindowPane::List => Element::from(
374                    Container::new(self.software_list_pane().map(|v| v.into()))
375                        .style(not_focused_rect_box),
376                ),
377                WindowPane::Detail => scrollable(
378                    Container::new(self.software_details_pane().map(|v| v.into()))
379                        .style(not_focused_rect_box)
380                        .padding(5),
381                )
382                .spacing(1)
383                .into(),
384            })
385        })
386        .into()
387    }
388
389    fn title(&'_ self) -> String {
390        i18n("license-info")
391    }
392
393    fn set_window_id(&mut self, window_id: Id) {
394        self.window_id = Some(window_id);
395    }
396}