prehnite/app/window/license_info_window/
mod.rs1use 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}