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