>(),
+ [(
+ "zoom".to_owned(),
+ Rating {
+ average: 4.25,
+ count: 12
+ }
+ )]
+ );
+ assert!(parse_ratings("not json").is_empty());
+ assert!(parse_ratings(r#"{"projects": []}"#).is_empty());
+ assert_eq!(
+ rate_url("zoom"),
+ "https://playhorizon.dev/api/mods/auth/login?back=/mods/zoom"
+ );
+}
diff --git a/launcher/src/mods_page.rs b/launcher/src/mods_page.rs
index 44f8998546..f74fc73c1f 100644
--- a/launcher/src/mods_page.rs
+++ b/launcher/src/mods_page.rs
@@ -175,6 +175,34 @@ fn count(n: u64) -> String {
}
}
+/// A registry project's rating: five pixel stars, the mean's nearest whole number of them lit, the
+/// mean and how many; None while the ratings aren't known.
+pub(super) fn stars(id: &str, t: &theme::Theme) -> Option {
+ let row = div().flex().items_center().gap(px(2.));
+ Some(match horizon::rating(id)? {
+ None => row.text_color(t.muted).child("No ratings yet"),
+ Some(r) => {
+ let lit = r.average.round() as usize;
+ row.children((0..5).map(|i| {
+ blocks::icon(
+ "star",
+ 12.,
+ if i < lit {
+ t.warn
+ } else {
+ t.muted.opacity(0.6)
+ },
+ )
+ }))
+ .child(div().ml(px(4.)).child(format!(
+ "{:.1} ({})",
+ r.average,
+ count(r.count)
+ )))
+ }
+ })
+}
+
/// A checksum as the page shows it: the algorithm and the first and last hex digits.
fn short_sum(checksum: &str) -> String {
let (algo, hex) = checksum.split_once(':').unwrap_or(("", checksum));
@@ -978,6 +1006,11 @@ impl Launcher {
.flex_col()
.child(div().truncate().child(p.summary.clone()))
.child(by.join(" ยท "))
+ .children(
+ (p.source == SourceId::Horizon)
+ .then(|| stars(&p.id, &t))
+ .flatten(),
+ )
.children(
support
.reason()
diff --git a/launcher/src/mods_project.rs b/launcher/src/mods_project.rs
index 97d23e0aa2..72a95b142c 100644
--- a/launcher/src/mods_project.rs
+++ b/launcher/src/mods_project.rs
@@ -491,6 +491,21 @@ impl Launcher {
.gap(px(24.))
.child(div().flex().child(back))
.child(head);
+ // Registry projects are rated on the site, signed in with GitHub (one rating per account).
+ if p.source == SourceId::Horizon {
+ let url = browser::horizon::rate_url(&p.id);
+ page = page.child(
+ div()
+ .flex()
+ .items_center()
+ .gap(px(16.))
+ .children(super::mods_page::stars(&p.id, &t).map(|s| text(theme::UI).child(s)))
+ .child(
+ button("mods-rate", "Sign in with GitHub to rate", Kind::Ghost, &t)
+ .on_click(move |_, _, cx| cx.open_url(&url)),
+ ),
+ );
+ }
if let Some(why) = support.reason() {
page = page.child(
text(theme::UI)