Skip to main content

modde_ui/views/
collections.rs

1use 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/// State for an in-progress collection download.
11#[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
29/// Render the collections browser view.
30pub 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}