From 13e52ab3ecd5616ee00fa7a030ebda927eeba911 Mon Sep 17 00:00:00 2001 From: Julian Chang Date: Thu, 1 Oct 2026 00:47:15 -0400 Subject: [PATCH] feat: use gallery images for package card banners --- .../src/view/app/browser/index/cards.rs | 6 +++++- .../oneclient_app/src/view/app/browser/mod.rs | 16 ++++++++++++---- .../src/packages/provider/curseforge.rs | 9 +++++++++ .../src/packages/provider/modrinth.rs | 11 +++++++++++ packages/oneclient_content/src/packages/types.rs | 4 ++++ packages/oneclient_core/src/clusters/modpack.rs | 4 +++- 6 files changed, 44 insertions(+), 6 deletions(-) diff --git a/packages/oneclient_app/src/view/app/browser/index/cards.rs b/packages/oneclient_app/src/view/app/browser/index/cards.rs index b42ad035..58a4518d 100644 --- a/packages/oneclient_app/src/view/app/browser/index/cards.rs +++ b/packages/oneclient_app/src/view/app/browser/index/cards.rs @@ -102,6 +102,10 @@ impl Component for PackageCard { let provider = self.item.provider; let package_type = self.package_type.clone(); let icon_url = self.item.icon_url.clone(); + let banner = match self.item.banner_url.clone() { + Some(url) => PackageBanner::new(Some(url), BANNER_H).sharp(), + None => PackageBanner::new(icon_url.clone(), BANNER_H), + }; let cluster_id = self.cluster_id; let a11y_id = use_a11y(); @@ -134,7 +138,7 @@ impl Component for PackageCard { .height(Size::px(BANNER_H)) .overflow(Overflow::Clip) // No installed badge, the control at the foot of the card already states it - .child(PackageBanner::new(icon_url.clone(), BANNER_H).backdrop_only()), + .child(banner.backdrop_only()), ) .child( rect() diff --git a/packages/oneclient_app/src/view/app/browser/mod.rs b/packages/oneclient_app/src/view/app/browser/mod.rs index 81705475..54c4830e 100644 --- a/packages/oneclient_app/src/view/app/browser/mod.rs +++ b/packages/oneclient_app/src/view/app/browser/mod.rs @@ -289,6 +289,7 @@ pub(crate) struct PackageBanner { icon_url: Option, height: f32, backdrop_only: bool, + sharp: bool, key: DiffKey, } @@ -298,6 +299,7 @@ impl PackageBanner { icon_url, height, backdrop_only: false, + sharp: false, key: DiffKey::None, } } @@ -307,6 +309,11 @@ impl PackageBanner { self.backdrop_only = true; self } + + pub fn sharp(mut self) -> Self { + self.sharp = true; + self + } } impl KeyExt for PackageBanner { @@ -319,7 +326,7 @@ impl Component for PackageBanner { fn render(&self) -> impl IntoElement { let h = self.height; let icon = h * 0.62; - let query = use_cached_image(self.icon_url.clone(), 512); + let query = use_cached_image(self.icon_url.clone(), if self.sharp { 384 } else { 256 }); let loaded = loaded_image(self.icon_url.as_deref(), &query); let banner = rect() @@ -350,7 +357,7 @@ impl Component for PackageBanner { ) .layer(Layer::Relative(1)), ) - .child( + .maybe_child((!self.sharp).then(|| { rect() .position(Position::new_absolute().top(0.).left(0.)) .width(Size::fill()) @@ -358,8 +365,9 @@ impl Component for PackageBanner { .backdrop_blur(12.) .background(BANNER_BG.with_a(120)) .overflow(Overflow::Clip) - .layer(Layer::Relative(3)), - ) + .layer(Layer::Relative(3)) + .into_element() + })) .maybe_child((!backdrop_only).then(|| { rect() .width(Size::px(icon)) diff --git a/packages/oneclient_content/src/packages/provider/curseforge.rs b/packages/oneclient_content/src/packages/provider/curseforge.rs index 3cdf810b..2f820806 100644 --- a/packages/oneclient_content/src/packages/provider/curseforge.rs +++ b/packages/oneclient_content/src/packages/provider/curseforge.rs @@ -488,6 +488,8 @@ struct CfMod { struct CfScreenshot { url: String, #[serde(default)] + thumbnail_url: Option, + #[serde(default)] title: Option, } @@ -629,6 +631,12 @@ impl From for ProjectSummary { .logo .and_then(|l| l.thumbnail_url.or(l.url)) .filter(|s| !s.is_empty()), + banner_url: m + .screenshots + .into_iter() + .next() + .map(|s| s.thumbnail_url.filter(|t| !t.is_empty()).unwrap_or(s.url)) + .filter(|s| !s.is_empty()), downloads: m.download_count, created: m.date_created, updated: m.date_modified, @@ -668,6 +676,7 @@ impl CfMod { .map(|s| GalleryImage { url: s.url, title: s.title.filter(|t| !t.is_empty()), + featured: false, }) .collect(), license: None, diff --git a/packages/oneclient_content/src/packages/provider/modrinth.rs b/packages/oneclient_content/src/packages/provider/modrinth.rs index 84eb4aa8..56d0bdc5 100644 --- a/packages/oneclient_content/src/packages/provider/modrinth.rs +++ b/packages/oneclient_content/src/packages/provider/modrinth.rs @@ -130,6 +130,10 @@ impl PackageProvider for ModrinthProvider { game_versions: Vec, downloads: u64, icon_url: Option, + #[serde(default)] + featured_gallery: Option, + #[serde(default)] + gallery: Vec, date_created: DateTime, date_modified: DateTime, } @@ -152,6 +156,10 @@ impl PackageProvider for ModrinthProvider { summary: h.description, author: h.author, icon_url: h.icon_url, + banner_url: h + .featured_gallery + .or_else(|| h.gallery.into_iter().next()) + .filter(|url| !url.is_empty()), downloads: h.downloads, created: h.date_created, updated: h.date_modified, @@ -534,6 +542,8 @@ struct ModrinthGalleryItem { raw_url: Option, #[serde(default)] title: Option, + #[serde(default)] + featured: bool, } #[derive(Deserialize)] @@ -627,6 +637,7 @@ impl ModrinthProject { .map(|g| GalleryImage { url: g.raw_url.filter(|url| !url.is_empty()).unwrap_or(g.url), title: g.title.filter(|t| !t.is_empty()), + featured: g.featured, }) .collect(), license, diff --git a/packages/oneclient_content/src/packages/types.rs b/packages/oneclient_content/src/packages/types.rs index 6c668659..ce354bf7 100644 --- a/packages/oneclient_content/src/packages/types.rs +++ b/packages/oneclient_content/src/packages/types.rs @@ -70,6 +70,8 @@ pub struct ProjectSummary { pub summary: String, pub author: String, pub icon_url: Option, + #[serde(default)] + pub banner_url: Option, pub downloads: u64, pub created: DateTime, pub updated: DateTime, @@ -103,6 +105,8 @@ pub struct ProjectMember { pub struct GalleryImage { pub url: String, pub title: Option, + #[serde(default)] + pub featured: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/packages/oneclient_core/src/clusters/modpack.rs b/packages/oneclient_core/src/clusters/modpack.rs index 3e06f874..442c3c15 100644 --- a/packages/oneclient_core/src/clusters/modpack.rs +++ b/packages/oneclient_core/src/clusters/modpack.rs @@ -376,7 +376,9 @@ async fn apply_project_cover( fn cover_url(project: &ProjectDetail) -> Option { project .gallery - .first() + .iter() + .find(|image| image.featured) + .or_else(|| project.gallery.first()) .map(|image| image.url.clone()) .or_else(|| project.icon_url.clone()) .filter(|url| !url.is_empty())