mirror of
https://github.com/inventree/InvenTree.git
synced 2026-04-28 13:54:25 +00:00
[UI] Smooth image loading (#11815)
* Smooth image loading * Add "thumbnail" prop * Hide thumbnail once loaded
This commit is contained in:
@@ -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<DetailImageProps>) {
|
||||
const expandImage = (event: any) => {
|
||||
cancelEvent(event);
|
||||
modals.open({
|
||||
children: <ApiImage src={img} />,
|
||||
children: <ApiImage src={img} thumbnail={props.thumbnail} />,
|
||||
withCloseButton: false
|
||||
});
|
||||
};
|
||||
@@ -484,6 +485,7 @@ export function DetailsImage(props: Readonly<DetailImageProps>) {
|
||||
<>
|
||||
<ApiImage
|
||||
src={img}
|
||||
thumbnail={props.thumbnail}
|
||||
mah={IMAGE_DIMENSION}
|
||||
maw={IMAGE_DIMENSION}
|
||||
onClick={expandImage}
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
* Image caching is handled automagically by the browsers cache
|
||||
*/
|
||||
import { Image, type ImageProps, Skeleton, Stack } from '@mantine/core';
|
||||
import { useMemo } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { generateUrl } from '../../functions/urls';
|
||||
import { useLocalState } from '../../states/LocalState';
|
||||
|
||||
interface ApiImageProps extends ImageProps {
|
||||
onClick?: (event: any) => void;
|
||||
thumbnail?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -19,14 +20,61 @@ interface ApiImageProps extends ImageProps {
|
||||
export function ApiImage(props: Readonly<ApiImageProps>) {
|
||||
const { getHost } = useLocalState.getState();
|
||||
|
||||
const [isLoaded, setIsLoaded] = useState<boolean>(false);
|
||||
const highResRef = useRef<HTMLImageElement>(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 (
|
||||
<Stack>
|
||||
{thumbnailUrl && !isLoaded && (
|
||||
<Image
|
||||
{...props}
|
||||
src={thumbnailUrl}
|
||||
fit='contain'
|
||||
style={{
|
||||
position: 'absolute',
|
||||
filter: 'blur(1px)',
|
||||
transform: 'scale(0.95)',
|
||||
opacity: isLoaded ? 0 : 1,
|
||||
transition: 'opacity 0.2s ease'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{imageUrl ? (
|
||||
<Image {...props} src={imageUrl} fit='contain' />
|
||||
<Image
|
||||
ref={highResRef}
|
||||
{...props}
|
||||
src={imageUrl}
|
||||
fit='contain'
|
||||
style={{
|
||||
opacity: isLoaded ? 1 : 0,
|
||||
transition: 'opacity 0.2s ease'
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton h={props?.h ?? props.w} w={props?.w ?? props.h} />
|
||||
)}
|
||||
|
||||
@@ -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 && (
|
||||
<ApiImage
|
||||
src={imageUrl}
|
||||
thumbnail={thumbnailUrl}
|
||||
radius='sm'
|
||||
miw={42}
|
||||
mah={42}
|
||||
|
||||
@@ -714,6 +714,7 @@ export default function PartDetail() {
|
||||
deleteFile: true
|
||||
}}
|
||||
src={part.image}
|
||||
thumbnail={part.thumbnail}
|
||||
apiPath={apiUrl(ApiEndpoints.part_list, part.pk)}
|
||||
refresh={refreshInstance}
|
||||
pk={part.pk}
|
||||
@@ -1225,6 +1226,7 @@ export default function PartDetail() {
|
||||
}
|
||||
subtitle={part.description}
|
||||
imageUrl={part.image}
|
||||
thumbnailUrl={part.thumbnail}
|
||||
badges={badges}
|
||||
breadcrumbs={
|
||||
user.hasViewRole(UserRoles.part_category)
|
||||
|
||||
Reference in New Issue
Block a user