From 576c93c96b8547197169453fb1bb204cbb232c27 Mon Sep 17 00:00:00 2001 From: Maciek <108873945+rozwader@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:54:10 +0200 Subject: [PATCH] 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 --- .../src/components/dynamic_art.rs | 2 + .../src/components/package_row.rs | 12 +++- packages/oneclient_app/src/ui.rs | 11 ++++ .../src/view/app/analytics/servers.rs | 3 +- .../oneclient_app/src/view/app/browser/mod.rs | 55 +++++++++---------- .../src/view/app/browser/package/gallery.rs | 17 +++++- .../src/view/onboarding/bundles/row.rs | 3 +- 7 files changed, 71 insertions(+), 32 deletions(-) diff --git a/packages/oneclient_app/src/components/dynamic_art.rs b/packages/oneclient_app/src/components/dynamic_art.rs index f533c1e7..261814aa 100644 --- a/packages/oneclient_app/src/components/dynamic_art.rs +++ b/packages/oneclient_app/src/components/dynamic_art.rs @@ -8,6 +8,7 @@ use oneclient_common::{VersionKey, parse_mc_version}; use crate::AppAssets; use crate::hooks::{loaded_image, use_cached_image, use_version_metadata}; 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 pub const ART_PREVIEW_EDGE: u32 = 512; @@ -121,6 +122,7 @@ impl Component for DynamicArt { .height(Size::fill()) .aspect_ratio(AspectRatio::Max) .image_cover(ImageCover::Center) + .fallback(rect().width(Size::fill()).height(Size::fill())) .into_element() } } diff --git a/packages/oneclient_app/src/components/package_row.rs b/packages/oneclient_app/src/components/package_row.rs index 1a733baa..9f40bae7 100644 --- a/packages/oneclient_app/src/components/package_row.rs +++ b/packages/oneclient_app/src/components/package_row.rs @@ -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::routes::Route; use crate::theme::colors; -use crate::ui::border_all_color; +use crate::ui::{ImageFallbackExt, border_all_color}; use crate::utils::format_size; 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_weight(FontWeight::MEDIUM) .max_lines(1) + .text_overflow(TextOverflow::Ellipsis) .width(Size::fill()) .color(CARD_NAME.with_a(content_alpha)), ) @@ -277,6 +278,7 @@ pub(crate) fn grid_card( .text(format!("by {}", item.author)) .font_size(10.) .max_lines(1) + .text_overflow(TextOverflow::Ellipsis) .width(Size::fill()) .color(colors::fg_secondary().with_a(content_alpha)), ) @@ -368,6 +370,7 @@ fn package_info( .child( rect() .horizontal() + .width(Size::fill()) .cross_align(Alignment::Center) .spacing(8.) .child( @@ -376,6 +379,8 @@ fn package_info( .font_size(15.) .font_weight(FontWeight::MEDIUM) .max_lines(1) + .text_overflow(TextOverflow::Ellipsis) + .max_width(Size::percent(60.)) .color(CARD_NAME), ) .child(if remote { @@ -391,6 +396,9 @@ fn package_info( label() .text(format!("by {}", item.author)) .font_size(10.) + .max_lines(1) + .text_overflow(TextOverflow::Ellipsis) + .width(Size::fill()) .color(colors::fg_secondary()), ) }) @@ -400,6 +408,7 @@ fn package_info( .text(item.description.clone()) .font_size(11.) .max_lines(2) + .text_overflow(TextOverflow::Ellipsis) .width(Size::fill()) .color(colors::fg_secondary()), ) @@ -422,6 +431,7 @@ pub(crate) fn package_icon( .height(Size::px(size)) .aspect_ratio(AspectRatio::Min) .corner_radius(CornerRadius::new_all(8.)) + .fallback(icon_box(IconType::DotsGrid, size)) .into_element(), None if icon_url.is_some() => icon_box(IconType::DotsGrid, size), diff --git a/packages/oneclient_app/src/ui.rs b/packages/oneclient_app/src/ui.rs index 4b3a9a6b..6875f759 100644 --- a/packages/oneclient_app/src/ui.rs +++ b/packages/oneclient_app/src/ui.rs @@ -172,3 +172,14 @@ pub fn entrance_motion_layer( ) .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()) + } +} diff --git a/packages/oneclient_app/src/view/app/analytics/servers.rs b/packages/oneclient_app/src/view/app/analytics/servers.rs index 263343dc..c08d9827 100644 --- a/packages/oneclient_app/src/view/app/analytics/servers.rs +++ b/packages/oneclient_app/src/view/app/analytics/servers.rs @@ -7,7 +7,7 @@ use crate::components::{ }; use crate::hooks::{loaded_image, use_cached_image}; 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 super::{card, card_header}; @@ -371,6 +371,7 @@ impl Component for ServerIcon { .height(Size::px(size)) .aspect_ratio(AspectRatio::Min) .corner_radius(CornerRadius::new_all(6.)) + .fallback(server_icon_placeholder(self.is_ip, size)) .into_element(), None => server_icon_placeholder(self.is_ip, size), } diff --git a/packages/oneclient_app/src/view/app/browser/mod.rs b/packages/oneclient_app/src/view/app/browser/mod.rs index fde529ae..5d6f4510 100644 --- a/packages/oneclient_app/src/view/app/browser/mod.rs +++ b/packages/oneclient_app/src/view/app/browser/mod.rs @@ -13,7 +13,7 @@ use oneclient_core::{BundleFileKind, BundleWithUpdateStatus, LinkedArtifactInfo} use crate::components::{Icon, IconType}; use crate::hooks::{loaded_image, use_cached_image}; 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); @@ -251,29 +251,36 @@ impl Component for Thumbnail { let query = use_cached_image(self.icon_url.clone(), 256); let loaded = loaded_image(self.icon_url.as_deref(), &query); + let placeholder = thumbnail_placeholder(size, radius, 0.4); + match loaded { Some((url, bytes)) => ImageViewer::new((url, bytes)) .width(Size::px(size)) .height(Size::px(size)) .aspect_ratio(AspectRatio::Min) .corner_radius(CornerRadius::new_all(radius)) + .fallback(placeholder) .into_element(), - None => 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 * 0.4) - .color(colors::fg_secondary()), - ) - .into_element(), + None => placeholder, } } } +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)] pub(crate) struct PackageBanner { icon_url: Option, @@ -311,6 +318,8 @@ impl Component for PackageBanner { .overflow(Overflow::Clip) .background(BANNER_BG); + let icon_placeholder = thumbnail_placeholder(icon, 10., 0.45); + match loaded { Some((url, bytes)) => banner .child( @@ -324,7 +333,8 @@ impl Component for PackageBanner { .width(Size::fill()) .height(Size::fill()) .aspect_ratio(AspectRatio::Max) - .image_cover(ImageCover::Center), + .image_cover(ImageCover::Center) + .fallback(rect().width(Size::fill()).height(Size::fill())), ) .layer(Layer::Relative(1)), ) @@ -347,23 +357,12 @@ impl Component for PackageBanner { .width(Size::px(icon)) .height(Size::px(icon)) .aspect_ratio(AspectRatio::Min) - .corner_radius(CornerRadius::new_all(10.)), + .corner_radius(CornerRadius::new_all(10.)) + .fallback(icon_placeholder), ) .layer(Layer::Relative(5)), ), - None => banner.child( - 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()), - ), - ), + None => banner.child(icon_placeholder), } } } diff --git a/packages/oneclient_app/src/view/app/browser/package/gallery.rs b/packages/oneclient_app/src/view/app/browser/package/gallery.rs index 8eae22da..0183aa00 100644 --- a/packages/oneclient_app/src/view/app/browser/package/gallery.rs +++ b/packages/oneclient_app/src/view/app/browser/package/gallery.rs @@ -9,7 +9,7 @@ use crate::components::{ }; use crate::hooks::{loaded_image, query_is_busy, use_cached_image, use_view_state}; 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 GRID_GAP: f32 = 16.; @@ -118,6 +118,7 @@ impl Component for GalleryTile { .height(Size::fill()) .aspect_ratio(AspectRatio::Max) .image_cover(ImageCover::Center) + .fallback(broken_image(32.)) .into_element() })); @@ -202,6 +203,7 @@ impl Component for GalleryViewer { .width(Size::fill()) .height(Size::fill()) .aspect_ratio(AspectRatio::Min) + .fallback(broken_image(48.)) .into_element() })) // 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( icon: IconType, enabled: bool, diff --git a/packages/oneclient_app/src/view/onboarding/bundles/row.rs b/packages/oneclient_app/src/view/onboarding/bundles/row.rs index 479bf341..705696ae 100644 --- a/packages/oneclient_app/src/view/onboarding/bundles/row.rs +++ b/packages/oneclient_app/src/view/onboarding/bundles/row.rs @@ -4,7 +4,7 @@ use oneclient_content::packages::ProviderId; use crate::components::{Icon, provider_badge}; use crate::hooks::{loaded_image, use_cached_image}; use crate::theme::colors; -use crate::ui::border_all_color; +use crate::ui::{ImageFallbackExt, border_all_color}; use crate::utils::format_size; const CARD_BG: Color = Color::from_rgb(26, 34, 41); @@ -36,6 +36,7 @@ impl Component for OnboardingModCard { .height(Size::px(44.)) .aspect_ratio(AspectRatio::Min) .corner_radius(CornerRadius::new_all(8.)) + .fallback(icon_box(self.provider)) .into_element(), None => icon_box(self.provider).into_element(),