modde_ui/views/
collections.rs1use crate::views::selectable_text::text;
2use iced::widget::{button, column, container, progress_bar, row, scrollable, text_input};
3use iced::{Alignment, Element, Length};
4
5use modde_core::manifest::collection::CollectionManifest;
6
7use crate::action_button::{ButtonAction, DescribedButtonExt};
8use crate::app::Message;
9
10#[derive(Debug, Clone)]
12pub struct CollectionDownload {
13 pub slug: String,
14 pub bytes_downloaded: u64,
15 pub bytes_total: u64,
16}
17
18impl CollectionDownload {
19 #[must_use]
20 pub fn progress_fraction(&self) -> f32 {
21 if self.bytes_total == 0 {
22 0.0
23 } else {
24 self.bytes_downloaded as f32 / self.bytes_total as f32
25 }
26 }
27}
28
29pub fn view<'a>(
31 search_query: &'a str,
32 collections: &'a [CollectionManifest],
33 active_downloads: &'a [CollectionDownload],
34) -> Element<'a, Message> {
35 let title_bar = row![
36 text("Nexus Collections").size(20),
37 iced::widget::space::horizontal(),
38 ]
39 .align_y(Alignment::Center);
40
41 let search_bar = text_input("Search collections...", search_query)
42 .on_input(Message::SearchCollections)
43 .on_submit(Message::SearchCollections(search_query.to_string()))
44 .padding(8)
45 .width(Length::Fill);
46
47 let content: Element<Message> = if collections.is_empty() {
48 container(
49 text("No collections loaded. Enter a search term above to browse Nexus Collections.")
50 .size(14),
51 )
52 .padding(20)
53 .width(Length::Fill)
54 .center_x(Length::Fill)
55 .into()
56 } else {
57 let cards = collections
58 .iter()
59 .fold(column![].spacing(8), |col, collection| {
60 let download_state = active_downloads.iter().find(|d| d.slug == collection.slug);
61
62 let card = collection_card(collection, download_state);
63 col.push(card)
64 });
65
66 scrollable(cards).height(Length::Fill).into()
67 };
68
69 column![
70 title_bar,
71 search_bar,
72 iced::widget::rule::horizontal(1),
73 content,
74 ]
75 .spacing(8)
76 .padding(16)
77 .width(Length::Fill)
78 .height(Length::Fill)
79 .into()
80}
81
82fn collection_card<'a>(
83 collection: &'a CollectionManifest,
84 download: Option<&'a CollectionDownload>,
85) -> Element<'a, Message> {
86 let mod_count = collection.mods.len();
87 let endorsements = collection.endorsements;
88
89 let header = row![
90 text(&collection.name).size(16),
91 iced::widget::space::horizontal(),
92 text(format!("v{}", collection.version.version)).size(12),
93 ]
94 .align_y(Alignment::Center);
95
96 let meta = row![
97 text(format!("by {}", collection.author.name)).size(12),
98 text(" | ").size(12),
99 text(format!("{mod_count} mod(s)")).size(12),
100 text(" | ").size(12),
101 text(format!("{endorsements} endorsement(s)")).size(12),
102 ]
103 .spacing(0);
104
105 let summary = collection
106 .summary
107 .as_deref()
108 .unwrap_or("No description available.");
109 let description = text(summary).size(13);
110
111 let action_row: Element<Message> = if let Some(dl) = download {
112 let pct = dl.progress_fraction() * 100.0;
113 column![
114 progress_bar(0.0..=100.0, pct).girth(8),
115 text(format!(
116 "Downloading: {:.1}% ({} / {} bytes)",
117 pct, dl.bytes_downloaded, dl.bytes_total
118 ))
119 .size(11),
120 ]
121 .spacing(4)
122 .into()
123 } else {
124 let slug = collection.slug.clone();
125 let version = collection.version.version.clone();
126 button(text("Install").size(14))
127 .style(button::primary)
128 .padding([6, 14])
129 .on_action(ButtonAction::InstallCollection { slug, version })
130 };
131
132 container(
133 column![header, meta, description, action_row,]
134 .spacing(6)
135 .padding(12),
136 )
137 .width(Length::Fill)
138 .style(container::rounded_box)
139 .into()
140}