Skip to main content

modde_ui/views/
mod_details.rs

1//! Right-rail "mod details" panel — rendered at the bottom of the left nav
2//! sidebar when a mod with Nexus metadata is selected in the mod list.
3//!
4//! The state is populated asynchronously from the Nexus v1 REST API (basic
5//! metadata + primary `picture_url`) plus the v2 GraphQL endpoint (full image
6//! gallery). See `crates/modde-ui/src/app.rs` for the fetch flow.
7
8use iced::widget::image;
9use modde_core::NexusModId;
10
11/// Live state for the currently-selected mod's detail panel.
12#[derive(Debug, Clone)]
13pub struct ModDetailsState {
14    /// Nexus mod id — used to reject stale async results when the user
15    /// clicks on a different mod before the previous fetch completes.
16    pub nexus_mod_id: NexusModId,
17    /// Nexus game domain (e.g. `"skyrimspecialedition"`).
18    pub game_domain: String,
19    /// Full URL to the mod page on nexusmods.com — the "Open in Nexus" link
20    /// opens this in the system browser.
21    pub mod_page_url: String,
22
23    /// Loaded metadata. Until the initial fetch returns, these carry
24    /// whatever we knew locally from `EnabledMod` (`display_name`, version).
25    pub name: String,
26    pub author: String,
27    pub version: String,
28    pub summary: Option<String>,
29
30    /// True between sending the initial `get_mod` request and receiving the
31    /// response. The panel renders a "Loading…" placeholder in this state.
32    pub loading: bool,
33    /// If set, the initial fetch failed — we render the error text instead
34    /// of the metadata block.
35    pub error: Option<String>,
36
37    /// Image URLs for the gallery. Index 0 is typically the primary
38    /// `picture_url`. Empty until at least the v1 response arrives.
39    pub gallery: Vec<String>,
40    /// Which gallery index is currently displayed. Clicking the thumbnail
41    /// advances this (mod `gallery.len()`).
42    pub gallery_index: usize,
43    /// Decoded bytes of the image at `gallery_index`, ready for rendering.
44    /// `None` while the image is being fetched.
45    pub thumbnail: Option<image::Handle>,
46
47    /// User's current endorsement status for this mod. Values from Nexus:
48    /// `"Undecided"`, `"Abstained"`, `"Endorsed"`. `None` until fetched.
49    pub endorse_status: Option<String>,
50    /// Total endorsements on the mod (not user-specific).
51    pub endorsement_count: u64,
52    /// Whether the current user is tracking this mod. `None` = not yet
53    /// fetched, `Some(true)` = tracked, `Some(false)` = not tracked.
54    pub is_tracked: Option<bool>,
55    /// True while an endorse/track request is in flight. Disables both
56    /// buttons to prevent double-submits.
57    pub action_pending: bool,
58}
59
60impl ModDetailsState {
61    /// Construct the initial "loading" state as soon as a Nexus-tracked mod
62    /// is selected, before any HTTP requests complete.
63    #[must_use]
64    pub fn loading(
65        nexus_mod_id: NexusModId,
66        game_domain: String,
67        name: String,
68        version: String,
69    ) -> Self {
70        let mod_page_url = format!("https://www.nexusmods.com/{game_domain}/mods/{nexus_mod_id}");
71        Self {
72            nexus_mod_id,
73            game_domain,
74            mod_page_url,
75            name,
76            author: String::new(),
77            version,
78            summary: None,
79            loading: true,
80            error: None,
81            gallery: Vec::new(),
82            gallery_index: 0,
83            thumbnail: None,
84            endorse_status: None,
85            endorsement_count: 0,
86            is_tracked: None,
87            action_pending: false,
88        }
89    }
90
91    /// The URL of the image currently displayed in the thumbnail slot, if any.
92    pub fn current_image_url(&self) -> Option<&str> {
93        self.gallery
94            .get(self.gallery_index)
95            .map(std::string::String::as_str)
96    }
97}