From 27ebfeba1cbfcb3dcb4e5cc4c43c9bc15fd61d9a Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 27 Apr 2026 18:17:25 +1000 Subject: [PATCH] [UI] Smooth image loading (#11815) * Smooth image loading * Add "thumbnail" prop * Hide thumbnail once loaded --- .../src/components/details/DetailsImage.tsx | 4 +- .../src/components/images/ApiImage.tsx | 52 ++++++++++++++++++- .../src/components/nav/PageDetail.tsx | 3 ++ src/frontend/src/pages/part/PartDetail.tsx | 2 + 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/frontend/src/components/details/DetailsImage.tsx b/src/frontend/src/components/details/DetailsImage.tsx index 2df961ff35..d7201db566 100644 --- a/src/frontend/src/components/details/DetailsImage.tsx +++ b/src/frontend/src/components/details/DetailsImage.tsx @@ -42,6 +42,7 @@ import { StylishText } from '../items/StylishText'; export type DetailImageProps = { appRole?: UserRoles; src: string; + thumbnail?: string; apiPath: string; refresh?: () => void; imageActions?: DetailImageButtonProps; @@ -465,7 +466,7 @@ export function DetailsImage(props: Readonly) { const expandImage = (event: any) => { cancelEvent(event); modals.open({ - children: , + children: , withCloseButton: false }); }; @@ -484,6 +485,7 @@ export function DetailsImage(props: Readonly) { <> void; + thumbnail?: string; } /** @@ -19,14 +20,61 @@ interface ApiImageProps extends ImageProps { export function ApiImage(props: Readonly) { const { getHost } = useLocalState.getState(); + const [isLoaded, setIsLoaded] = useState(false); + const highResRef = useRef(null); + const imageUrl = useMemo(() => { return generateUrl(props.src, getHost()); }, [getHost, props.src]); + const thumbnailUrl = useMemo(() => { + if (props.thumbnail) { + return generateUrl(props.thumbnail, getHost()); + } else { + return null; + } + }, [getHost, props.thumbnail]); + + // Hook for progressive loading of the high-res image + useEffect(() => { + setIsLoaded(false); + + const img = new window.Image(); + img.src = imageUrl; + img.onload = () => setIsLoaded(true); + + return () => { + img.onload = null; + }; + }, [imageUrl]); + return ( + {thumbnailUrl && !isLoaded && ( + + )} {imageUrl ? ( - + ) : ( )} diff --git a/src/frontend/src/components/nav/PageDetail.tsx b/src/frontend/src/components/nav/PageDetail.tsx index 922ccbfdf4..e151b911f2 100644 --- a/src/frontend/src/components/nav/PageDetail.tsx +++ b/src/frontend/src/components/nav/PageDetail.tsx @@ -17,6 +17,7 @@ interface PageDetailInterface { badges?: ReactNode[]; breadcrumbs?: Breadcrumb[]; lastCrumb?: Breadcrumb[]; + thumbnailUrl?: string; breadcrumbAction?: () => void; actions?: ReactNode[]; editAction?: () => void; @@ -35,6 +36,7 @@ export function PageDetail({ subtitle, badges, imageUrl, + thumbnailUrl, breadcrumbs, lastCrumb: last_crumb, breadcrumbAction, @@ -108,6 +110,7 @@ export function PageDetail({ {imageUrl && (