diff --git a/crates/horizon-client-render/src/lib.rs b/crates/horizon-client-render/src/lib.rs index 359de0dce6..1454138334 100644 --- a/crates/horizon-client-render/src/lib.rs +++ b/crates/horizon-client-render/src/lib.rs @@ -63,6 +63,7 @@ pub mod menu_blur; pub mod mesher; pub mod minimap; pub mod mipmap; +pub mod mod_browser; pub mod mod_list; pub mod model_library; pub mod models; diff --git a/crates/horizon-client-render/src/mod_browser.rs b/crates/horizon-client-render/src/mod_browser.rs new file mode 100644 index 0000000000..4fe15b7845 --- /dev/null +++ b/crates/horizon-client-render/src/mod_browser.rs @@ -0,0 +1,189 @@ +//! What Options > Mods > Browse mods shows (docs/design/mod-hotload.md): the +//! registry's content mods this game installs, a search over them, and the one +//! whose page is open. The game fills it from a worker +//! (`horizon_client::mod_registry`, `game/mod_browser.rs`); the options screen +//! only reads it and asks for installs through its events. + +/// One project of the registry, as the list and its page show it. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Listing { + pub id: String, + pub name: String, + pub summary: String, + /// Markdown, shown as plain text. + pub description: String, + pub authors: Vec, + pub license: String, + pub categories: Vec, + pub tags: Vec, + /// The version this game would install, its date and changelog. + pub version: String, + pub published: String, + pub changelog: String, + pub downloads: u64, + /// The project's page on the website. + pub page: String, + /// The version installed in this game folder, if any. + pub installed: Option, +} + +impl Listing { + /// Whether a search for `query` (already lower case) finds it: its name, + /// id, summary, authors, categories or tags. + pub fn matches(&self, query: &str) -> bool { + let query = query.trim(); + query.is_empty() + || [&self.name, &self.id, &self.summary] + .into_iter() + .chain(&self.authors) + .chain(&self.categories) + .chain(&self.tags) + .any(|field| field.to_lowercase().contains(query)) + } + + /// Installed, but not the version the registry offers now. + pub fn has_update(&self) -> bool { + self.installed + .as_ref() + .is_some_and(|installed| *installed != self.version) + } +} + +/// Where the browser is. +#[derive(Clone, Debug, Default, PartialEq)] +pub enum Status { + /// Not asked yet. + #[default] + Idle, + /// The index is being read. + Loading, + /// The projects are listed. + Ready, + /// The index couldn't be read: why, in English (shown inside a translated line). + Failed(String), +} + +#[derive(Clone, Debug, Default)] +pub struct ModBrowser { + pub status: Status, + pub listings: Vec, + /// The project an install runs for. + pub installing: Option, + /// The last install's outcome, already translated, for the page it ran on. + pub message: Option<(String, bool)>, +} + +impl ModBrowser { + /// The listings a search for `query` finds, by index into `listings`. + pub fn found(&self, query: &str) -> Vec { + let query = query.to_lowercase(); + (0..self.listings.len()) + .filter(|&i| self.listings[i].matches(&query)) + .collect() + } + + pub fn listing(&self, id: &str) -> Option<&Listing> { + self.listings.iter().find(|l| l.id == id) + } +} + +/// A project's markdown description as lines of plain text for the page: +/// heading marks, emphasis and list bullets dropped, links shown as their text, +/// blank lines and fences left out. +pub fn plain_lines(markdown: &str) -> Vec { + let mut out = Vec::new(); + for line in markdown.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with("```") || line.starts_with("