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}