feat: added placeholder image for images that couldnt be decoded; fixed overflowing titles in mods view (#802)

fix: drop per-render image fallback logging and narrow its memoization cost

fix: keep the backdrop fallback, the cache is what fails
This commit is contained in:
Maciek 2026-09-13 00:54:10 +02:00 • committed by GitHub
parent ba74bedf92
commit 576c93c96b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 71 additions and 32 deletions

View file

@ -8,6 +8,7 @@ use oneclient_common::{VersionKey, parse_mc_version};
use crate::AppAssets; use crate::AppAssets;
use crate::hooks::{loaded_image, use_cached_image, use_version_metadata}; use crate::hooks::{loaded_image, use_cached_image, use_version_metadata};
use crate::layout::HOME_BACKGROUND_ASSET; use crate::layout::HOME_BACKGROUND_ASSET;
use crate::ui::ImageFallbackExt;
/// The size cluster cards request so it is usually already cached when anything else wants it /// The size cluster cards request so it is usually already cached when anything else wants it
pub const ART_PREVIEW_EDGE: u32 = 512; pub const ART_PREVIEW_EDGE: u32 = 512;
@ -121,6 +122,7 @@ impl Component for DynamicArt {
.height(Size::fill()) .height(Size::fill())
.aspect_ratio(AspectRatio::Max) .aspect_ratio(AspectRatio::Max)
.image_cover(ImageCover::Center) .image_cover(ImageCover::Center)
.fallback(rect().width(Size::fill()).height(Size::fill()))
.into_element() .into_element()
} }
} }

View file

@ -7,7 +7,7 @@ use crate::components::{Icon, IconType, toggle_controlled};
use crate::hooks::{ClusterAction, loaded_image, use_cached_image, use_cluster_mutation}; use crate::hooks::{ClusterAction, loaded_image, use_cached_image, use_cluster_mutation};
use crate::routes::Route; use crate::routes::Route;
use crate::theme::colors; use crate::theme::colors;
use crate::ui::border_all_color; use crate::ui::{ImageFallbackExt, border_all_color};
use crate::utils::format_size; use crate::utils::format_size;
pub(crate) const CARD_BG: Color = Color::from_rgb(26, 34, 41); pub(crate) const CARD_BG: Color = Color::from_rgb(26, 34, 41);
@ -268,6 +268,7 @@ pub(crate) fn grid_card(
.font_size(14.) .font_size(14.)
.font_weight(FontWeight::MEDIUM) .font_weight(FontWeight::MEDIUM)
.max_lines(1) .max_lines(1)
.text_overflow(TextOverflow::Ellipsis)
.width(Size::fill()) .width(Size::fill())
.color(CARD_NAME.with_a(content_alpha)), .color(CARD_NAME.with_a(content_alpha)),
) )
@ -277,6 +278,7 @@ pub(crate) fn grid_card(
.text(format!("by {}", item.author)) .text(format!("by {}", item.author))
.font_size(10.) .font_size(10.)
.max_lines(1) .max_lines(1)
.text_overflow(TextOverflow::Ellipsis)
.width(Size::fill()) .width(Size::fill())
.color(colors::fg_secondary().with_a(content_alpha)), .color(colors::fg_secondary().with_a(content_alpha)),
) )
@ -368,6 +370,7 @@ fn package_info(
.child( .child(
rect() rect()
.horizontal() .horizontal()
.width(Size::fill())
.cross_align(Alignment::Center) .cross_align(Alignment::Center)
.spacing(8.) .spacing(8.)
.child( .child(
@ -376,6 +379,8 @@ fn package_info(
.font_size(15.) .font_size(15.)
.font_weight(FontWeight::MEDIUM) .font_weight(FontWeight::MEDIUM)
.max_lines(1) .max_lines(1)
.text_overflow(TextOverflow::Ellipsis)
.max_width(Size::percent(60.))
.color(CARD_NAME), .color(CARD_NAME),
) )
.child(if remote { .child(if remote {
@ -391,6 +396,9 @@ fn package_info(
label() label()
.text(format!("by {}", item.author)) .text(format!("by {}", item.author))
.font_size(10.) .font_size(10.)
.max_lines(1)
.text_overflow(TextOverflow::Ellipsis)
.width(Size::fill())
.color(colors::fg_secondary()), .color(colors::fg_secondary()),
) )
}) })
@ -400,6 +408,7 @@ fn package_info(
.text(item.description.clone()) .text(item.description.clone())
.font_size(11.) .font_size(11.)
.max_lines(2) .max_lines(2)
.text_overflow(TextOverflow::Ellipsis)
.width(Size::fill()) .width(Size::fill())
.color(colors::fg_secondary()), .color(colors::fg_secondary()),
) )
@ -422,6 +431,7 @@ pub(crate) fn package_icon(
.height(Size::px(size)) .height(Size::px(size))
.aspect_ratio(AspectRatio::Min) .aspect_ratio(AspectRatio::Min)
.corner_radius(CornerRadius::new_all(8.)) .corner_radius(CornerRadius::new_all(8.))
.fallback(icon_box(IconType::DotsGrid, size))
.into_element(), .into_element(),
None if icon_url.is_some() => icon_box(IconType::DotsGrid, size), None if icon_url.is_some() => icon_box(IconType::DotsGrid, size),

View file

@ -172,3 +172,14 @@ pub fn entrance_motion_layer(
) )
.into_element() .into_element()
} }
pub trait ImageFallbackExt: Sized {
fn fallback(self, placeholder: impl IntoElement) -> Self;
}
impl ImageFallbackExt for ImageViewer {
fn fallback(self, placeholder: impl IntoElement) -> Self {
let placeholder = placeholder.into_element();
self.error_renderer(move |_: String| placeholder.clone())
}
}

View file

@ -7,7 +7,7 @@ use crate::components::{
}; };
use crate::hooks::{loaded_image, use_cached_image}; use crate::hooks::{loaded_image, use_cached_image};
use crate::theme::colors; use crate::theme::colors;
use crate::ui::border_all_color; use crate::ui::{ImageFallbackExt, border_all_color};
use crate::utils::{format_duration_hm, plural}; use crate::utils::{format_duration_hm, plural};
use super::{card, card_header}; use super::{card, card_header};
@ -371,6 +371,7 @@ impl Component for ServerIcon {
.height(Size::px(size)) .height(Size::px(size))
.aspect_ratio(AspectRatio::Min) .aspect_ratio(AspectRatio::Min)
.corner_radius(CornerRadius::new_all(6.)) .corner_radius(CornerRadius::new_all(6.))
.fallback(server_icon_placeholder(self.is_ip, size))
.into_element(), .into_element(),
None => server_icon_placeholder(self.is_ip, size), None => server_icon_placeholder(self.is_ip, size),
} }

View file

@ -13,7 +13,7 @@ use oneclient_core::{BundleFileKind, BundleWithUpdateStatus, LinkedArtifactInfo}
use crate::components::{Icon, IconType}; use crate::components::{Icon, IconType};
use crate::hooks::{loaded_image, use_cached_image}; use crate::hooks::{loaded_image, use_cached_image};
use crate::theme::colors; use crate::theme::colors;
use crate::ui::border_all_color; use crate::ui::{ImageFallbackExt, border_all_color};
const BANNER_BG: Color = Color::from_rgb(21, 28, 34); const BANNER_BG: Color = Color::from_rgb(21, 28, 34);
@ -251,29 +251,36 @@ impl Component for Thumbnail {
let query = use_cached_image(self.icon_url.clone(), 256); let query = use_cached_image(self.icon_url.clone(), 256);
let loaded = loaded_image(self.icon_url.as_deref(), &query); let loaded = loaded_image(self.icon_url.as_deref(), &query);
let placeholder = thumbnail_placeholder(size, radius, 0.4);
match loaded { match loaded {
Some((url, bytes)) => ImageViewer::new((url, bytes)) Some((url, bytes)) => ImageViewer::new((url, bytes))
.width(Size::px(size)) .width(Size::px(size))
.height(Size::px(size)) .height(Size::px(size))
.aspect_ratio(AspectRatio::Min) .aspect_ratio(AspectRatio::Min)
.corner_radius(CornerRadius::new_all(radius)) .corner_radius(CornerRadius::new_all(radius))
.fallback(placeholder)
.into_element(), .into_element(),
None => rect() None => placeholder,
.center()
.width(Size::px(size))
.height(Size::px(size))
.corner_radius(CornerRadius::new_all(radius))
.background(colors::component_bg())
.child(
Icon::new(IconType::DotsGrid)
.size(size * 0.4)
.color(colors::fg_secondary()),
)
.into_element(),
} }
} }
} }
fn thumbnail_placeholder(size: f32, radius: f32, icon_ratio: f32) -> Element {
rect()
.center()
.width(Size::px(size))
.height(Size::px(size))
.corner_radius(CornerRadius::new_all(radius))
.background(colors::component_bg())
.child(
Icon::new(IconType::DotsGrid)
.size(size * icon_ratio)
.color(colors::fg_secondary()),
)
.into_element()
}
#[derive(PartialEq)] #[derive(PartialEq)]
pub(crate) struct PackageBanner { pub(crate) struct PackageBanner {
icon_url: Option<String>, icon_url: Option<String>,
@ -311,6 +318,8 @@ impl Component for PackageBanner {
.overflow(Overflow::Clip) .overflow(Overflow::Clip)
.background(BANNER_BG); .background(BANNER_BG);
let icon_placeholder = thumbnail_placeholder(icon, 10., 0.45);
match loaded { match loaded {
Some((url, bytes)) => banner Some((url, bytes)) => banner
.child( .child(
@ -324,7 +333,8 @@ impl Component for PackageBanner {
.width(Size::fill()) .width(Size::fill())
.height(Size::fill()) .height(Size::fill())
.aspect_ratio(AspectRatio::Max) .aspect_ratio(AspectRatio::Max)
.image_cover(ImageCover::Center), .image_cover(ImageCover::Center)
.fallback(rect().width(Size::fill()).height(Size::fill())),
) )
.layer(Layer::Relative(1)), .layer(Layer::Relative(1)),
) )
@ -347,23 +357,12 @@ impl Component for PackageBanner {
.width(Size::px(icon)) .width(Size::px(icon))
.height(Size::px(icon)) .height(Size::px(icon))
.aspect_ratio(AspectRatio::Min) .aspect_ratio(AspectRatio::Min)
.corner_radius(CornerRadius::new_all(10.)), .corner_radius(CornerRadius::new_all(10.))
.fallback(icon_placeholder),
) )
.layer(Layer::Relative(5)), .layer(Layer::Relative(5)),
), ),
None => banner.child( None => banner.child(icon_placeholder),
rect()
.center()
.width(Size::px(icon))
.height(Size::px(icon))
.corner_radius(CornerRadius::new_all(10.))
.background(colors::component_bg())
.child(
Icon::new(IconType::DotsGrid)
.size(icon * 0.45)
.color(colors::fg_secondary()),
),
),
} }
} }
} }

View file

@ -9,7 +9,7 @@ use crate::components::{
}; };
use crate::hooks::{loaded_image, query_is_busy, use_cached_image, use_view_state}; use crate::hooks::{loaded_image, query_is_busy, use_cached_image, use_view_state};
use crate::theme::colors; use crate::theme::colors;
use crate::ui::{border_all_color, flow_grid}; use crate::ui::{ImageFallbackExt, border_all_color, flow_grid};
const MAX_COL_W: f32 = 400.; const MAX_COL_W: f32 = 400.;
const GRID_GAP: f32 = 16.; const GRID_GAP: f32 = 16.;
@ -118,6 +118,7 @@ impl Component for GalleryTile {
.height(Size::fill()) .height(Size::fill())
.aspect_ratio(AspectRatio::Max) .aspect_ratio(AspectRatio::Max)
.image_cover(ImageCover::Center) .image_cover(ImageCover::Center)
.fallback(broken_image(32.))
.into_element() .into_element()
})); }));
@ -202,6 +203,7 @@ impl Component for GalleryViewer {
.width(Size::fill()) .width(Size::fill())
.height(Size::fill()) .height(Size::fill())
.aspect_ratio(AspectRatio::Min) .aspect_ratio(AspectRatio::Min)
.fallback(broken_image(48.))
.into_element() .into_element()
})) }))
// A full-size fetch separate from the tile's cached copy a failed fetch is not loading // A full-size fetch separate from the tile's cached copy a failed fetch is not loading
@ -265,6 +267,19 @@ impl Component for GalleryViewer {
} }
} }
fn broken_image(size: f32) -> Element {
rect()
.center()
.width(Size::fill())
.height(Size::fill())
.child(
Icon::new(IconType::FileX02)
.size(size)
.color(colors::fg_secondary()),
)
.into_element()
}
fn chevron_btn( fn chevron_btn(
icon: IconType, icon: IconType,
enabled: bool, enabled: bool,

View file

@ -4,7 +4,7 @@ use oneclient_content::packages::ProviderId;
use crate::components::{Icon, provider_badge}; use crate::components::{Icon, provider_badge};
use crate::hooks::{loaded_image, use_cached_image}; use crate::hooks::{loaded_image, use_cached_image};
use crate::theme::colors; use crate::theme::colors;
use crate::ui::border_all_color; use crate::ui::{ImageFallbackExt, border_all_color};
use crate::utils::format_size; use crate::utils::format_size;
const CARD_BG: Color = Color::from_rgb(26, 34, 41); const CARD_BG: Color = Color::from_rgb(26, 34, 41);
@ -36,6 +36,7 @@ impl Component for OnboardingModCard {
.height(Size::px(44.)) .height(Size::px(44.))
.aspect_ratio(AspectRatio::Min) .aspect_ratio(AspectRatio::Min)
.corner_radius(CornerRadius::new_all(8.)) .corner_radius(CornerRadius::new_all(8.))
.fallback(icon_box(self.provider))
.into_element(), .into_element(),
None => icon_box(self.provider).into_element(), None => icon_box(self.provider).into_element(),