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 = {
|
export type DetailImageProps = {
|
||||||
appRole?: UserRoles;
|
appRole?: UserRoles;
|
||||||
src: string;
|
src: string;
|
||||||
|
thumbnail?: string;
|
||||||
apiPath: string;
|
apiPath: string;
|
||||||
refresh?: () => void;
|
refresh?: () => void;
|
||||||
imageActions?: DetailImageButtonProps;
|
imageActions?: DetailImageButtonProps;
|
||||||
@@ -465,7 +466,7 @@ export function DetailsImage(props: Readonly<DetailImageProps>) {
|
|||||||
const expandImage = (event: any) => {
|
const expandImage = (event: any) => {
|
||||||
cancelEvent(event);
|
cancelEvent(event);
|
||||||
modals.open({
|
modals.open({
|
||||||
children: <ApiImage src={img} />,
|
children: <ApiImage src={img} thumbnail={props.thumbnail} />,
|
||||||
withCloseButton: false
|
withCloseButton: false
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -484,6 +485,7 @@ export function DetailsImage(props: Readonly<DetailImageProps>) {
|
|||||||
<>
|
<>
|
||||||
<ApiImage
|
<ApiImage
|
||||||
src={img}
|
src={img}
|
||||||
|
thumbnail={props.thumbnail}
|
||||||
mah={IMAGE_DIMENSION}
|
mah={IMAGE_DIMENSION}
|
||||||
maw={IMAGE_DIMENSION}
|
maw={IMAGE_DIMENSION}
|
||||||
onClick={expandImage}
|
onClick={expandImage}
|
||||||
|
|||||||
@@ -4,13 +4,14 @@
|
|||||||
* Image caching is handled automagically by the browsers cache
|
* Image caching is handled automagically by the browsers cache
|
||||||
*/
|
*/
|
||||||
import { Image, type ImageProps, Skeleton, Stack } from '@mantine/core';
|
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 { generateUrl } from '../../functions/urls';
|
||||||
import { useLocalState } from '../../states/LocalState';
|
import { useLocalState } from '../../states/LocalState';
|
||||||
|
|
||||||
interface ApiImageProps extends ImageProps {
|
interface ApiImageProps extends ImageProps {
|
||||||
onClick?: (event: any) => void;
|
onClick?: (event: any) => void;
|
||||||
|
thumbnail?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,14 +20,61 @@ interface ApiImageProps extends ImageProps {
|
|||||||
export function ApiImage(props: Readonly<ApiImageProps>) {
|
export function ApiImage(props: Readonly<ApiImageProps>) {
|
||||||
const { getHost } = useLocalState.getState();
|
const { getHost } = useLocalState.getState();
|
||||||
|
|
||||||
|
const [isLoaded, setIsLoaded] = useState<boolean>(false);
|
||||||
|
const highResRef = useRef<HTMLImageElement>(null);
|
||||||
|
|
||||||
const imageUrl = useMemo(() => {
|
const imageUrl = useMemo(() => {
|
||||||
return generateUrl(props.src, getHost());
|
return generateUrl(props.src, getHost());
|
||||||
}, [getHost, props.src]);
|
}, [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 (
|
return (
|
||||||
<Stack>
|
<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 ? (
|
{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} />
|
<Skeleton h={props?.h ?? props.w} w={props?.w ?? props.h} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ interface PageDetailInterface {
|
|||||||
badges?: ReactNode[];
|
badges?: ReactNode[];
|
||||||
breadcrumbs?: Breadcrumb[];
|
breadcrumbs?: Breadcrumb[];
|
||||||
lastCrumb?: Breadcrumb[];
|
lastCrumb?: Breadcrumb[];
|
||||||
|
thumbnailUrl?: string;
|
||||||
breadcrumbAction?: () => void;
|
breadcrumbAction?: () => void;
|
||||||
actions?: ReactNode[];
|
actions?: ReactNode[];
|
||||||
editAction?: () => void;
|
editAction?: () => void;
|
||||||
@@ -35,6 +36,7 @@ export function PageDetail({
|
|||||||
subtitle,
|
subtitle,
|
||||||
badges,
|
badges,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
|
thumbnailUrl,
|
||||||
breadcrumbs,
|
breadcrumbs,
|
||||||
lastCrumb: last_crumb,
|
lastCrumb: last_crumb,
|
||||||
breadcrumbAction,
|
breadcrumbAction,
|
||||||
@@ -108,6 +110,7 @@ export function PageDetail({
|
|||||||
{imageUrl && (
|
{imageUrl && (
|
||||||
<ApiImage
|
<ApiImage
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
|
thumbnail={thumbnailUrl}
|
||||||
radius='sm'
|
radius='sm'
|
||||||
miw={42}
|
miw={42}
|
||||||
mah={42}
|
mah={42}
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ export default function PartDetail() {
|
|||||||
deleteFile: true
|
deleteFile: true
|
||||||
}}
|
}}
|
||||||
src={part.image}
|
src={part.image}
|
||||||
|
thumbnail={part.thumbnail}
|
||||||
apiPath={apiUrl(ApiEndpoints.part_list, part.pk)}
|
apiPath={apiUrl(ApiEndpoints.part_list, part.pk)}
|
||||||
refresh={refreshInstance}
|
refresh={refreshInstance}
|
||||||
pk={part.pk}
|
pk={part.pk}
|
||||||
@@ -1225,6 +1226,7 @@ export default function PartDetail() {
|
|||||||
}
|
}
|
||||||
subtitle={part.description}
|
subtitle={part.description}
|
||||||
imageUrl={part.image}
|
imageUrl={part.image}
|
||||||
|
thumbnailUrl={part.thumbnail}
|
||||||
badges={badges}
|
badges={badges}
|
||||||
breadcrumbs={
|
breadcrumbs={
|
||||||
user.hasViewRole(UserRoles.part_category)
|
user.hasViewRole(UserRoles.part_category)
|
||||||
|
|||||||
Reference in New Issue
Block a user