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