feat: use gallery images for package card banners

This commit is contained in:
Julian Chang 2026-10-01 00:47:15 -04:00
parent cd122e877e
commit 13e52ab3ec
No known key found for this signature in database
6 changed files with 44 additions and 6 deletions

View file

@ -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()

View file

@ -289,6 +289,7 @@ pub(crate) struct PackageBanner {
icon_url: Option<String>,
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))

View file

@ -488,6 +488,8 @@ struct CfMod {
struct CfScreenshot {
url: String,
#[serde(default)]
thumbnail_url: Option<String>,
#[serde(default)]
title: Option<String>,
}
@ -629,6 +631,12 @@ impl From<CfMod> 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,

View file

@ -130,6 +130,10 @@ impl PackageProvider for ModrinthProvider {
game_versions: Vec<String>,
downloads: u64,
icon_url: Option<String>,
#[serde(default)]
featured_gallery: Option<String>,
#[serde(default)]
gallery: Vec<String>,
date_created: DateTime<Utc>,
date_modified: DateTime<Utc>,
}
@ -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<String>,
#[serde(default)]
title: Option<String>,
#[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,

View file

@ -70,6 +70,8 @@ pub struct ProjectSummary {
pub summary: String,
pub author: String,
pub icon_url: Option<String>,
#[serde(default)]
pub banner_url: Option<String>,
pub downloads: u64,
pub created: DateTime<Utc>,
pub updated: DateTime<Utc>,
@ -103,6 +105,8 @@ pub struct ProjectMember {
pub struct GalleryImage {
pub url: String,
pub title: Option<String>,
#[serde(default)]
pub featured: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]

View file

@ -376,7 +376,9 @@ async fn apply_project_cover(
fn cover_url(project: &ProjectDetail) -> Option<String> {
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())