improve image placeholders and loading

This commit is contained in:
jeffvli
2025-12-28 02:43:31 -08:00
parent 88711eac2f
commit dde4e1b33c
5 changed files with 62 additions and 50 deletions
@@ -56,9 +56,7 @@
}
.image-container.is-round {
&::before {
border-radius: 50%;
}
border-radius: 50%;
}
.favorite-badge {
@@ -785,9 +785,9 @@ const PosterItemCard = ({
<>
<ItemImage
className={clsx(styles.image, { [styles.isRound]: isRound })}
id={data?.id}
id={(data as { imageId: string })?.imageId}
itemType={itemType}
src={(data as Album | AlbumArtist | Playlist | Song)?.imageUrl}
src={(data as { imageUrl: string })?.imageUrl}
/>
{isFavorite && <div className={styles.favoriteBadge} />}
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
@@ -11,6 +11,25 @@ import {
import { BaseImage, ImageProps } from '/@/shared/components/image/image';
import { LibraryItem } from '/@/shared/types/domain-types';
const getUnloaderIcon = (itemType: LibraryItem) => {
switch (itemType) {
case LibraryItem.ALBUM:
return 'emptyAlbumImage';
case LibraryItem.ALBUM_ARTIST:
return 'emptyArtistImage';
case LibraryItem.ARTIST:
return 'emptyArtistImage';
case LibraryItem.GENRE:
return 'emptyGenreImage';
case LibraryItem.PLAYLIST:
return 'emptyPlaylistImage';
case LibraryItem.SONG:
return 'emptySongImage';
default:
return 'emptyImage';
}
};
const BaseItemImage = (
props: Omit<ImageProps, 'src'> & {
id?: null | string;
@@ -27,7 +46,7 @@ const BaseItemImage = (
size: 300,
});
return <BaseImage src={imageUrl} {...rest} />;
return <BaseImage src={imageUrl} unloaderIcon={getUnloaderIcon(props.itemType)} {...rest} />;
};
export const ItemImage = memo(BaseItemImage);
+5
View File
@@ -160,7 +160,12 @@ export const AppIcon = {
edit: LuPencilLine,
ellipsisHorizontal: LuEllipsis,
ellipsisVertical: LuEllipsisVertical,
emptyAlbumImage: LuDisc3,
emptyArtistImage: LuUser,
emptyGenreImage: LuFlag,
emptyImage: LuDisc3,
emptyPlaylistImage: LuListMusic,
emptySongImage: LuMusic,
error: LuShieldAlert,
externalLink: LuExternalLink,
favorite: LuHeart,
+34 -44
View File
@@ -11,7 +11,7 @@ import { Img } from 'react-image';
import styles from './image.module.css';
import { Icon } from '/@/shared/components/icon/icon';
import { AppIcon, Icon } from '/@/shared/components/icon/icon';
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
import { useInViewport } from '/@/shared/hooks/use-in-viewport';
@@ -23,6 +23,7 @@ export interface ImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 's
includeUnloader?: boolean;
src: string | string[] | undefined;
thumbHash?: string;
unloaderIcon?: keyof typeof AppIcon;
}
interface ImageContainerProps extends HTMLAttributes<HTMLDivElement> {
@@ -36,6 +37,7 @@ interface ImageLoaderProps {
interface ImageUnloaderProps {
className?: string;
icon?: keyof typeof AppIcon;
}
export const FALLBACK_SVG =
@@ -49,52 +51,40 @@ export function BaseImage({
includeLoader = true,
includeUnloader = true,
src,
unloaderIcon = 'emptyImage',
...props
}: ImageProps) {
const { inViewport, ref } = useInViewport();
if (src) {
return (
<Img
className={clsx(styles.image, className, {
[styles.animated]: enableAnimation,
})}
container={(children) => (
<ImageContainer
className={containerClassName}
enableAnimation={enableAnimation}
ref={ref}
{...imageContainerProps}
>
{children}
</ImageContainer>
)}
decoding="async"
fetchPriority={inViewport ? 'high' : 'low'}
loader={
includeLoader ? (
<ImageContainer className={containerClassName}>
<ImageLoader className={className} />
</ImageContainer>
) : null
}
loading={inViewport ? 'eager' : 'lazy'}
src={inViewport ? src : FALLBACK_SVG}
unloader={
includeUnloader ? (
<ImageContainer className={containerClassName}>
<ImageUnloader className={className} />
</ImageContainer>
) : null
}
{...props}
/>
);
}
return (
<ImageContainer className={containerClassName}>
<ImageUnloader />
<ImageContainer
className={containerClassName}
enableAnimation={enableAnimation}
ref={ref}
{...imageContainerProps}
>
{inViewport && src ? (
<Img
className={clsx(styles.image, className, {
[styles.animated]: enableAnimation,
})}
decoding="async"
fetchPriority="high"
loader={includeLoader ? <ImageLoader className={className} /> : null}
loading="eager"
src={src}
unloader={
includeUnloader ? (
<ImageUnloader className={className} icon={unloaderIcon} />
) : null
}
{...props}
/>
) : !src ? (
<ImageUnloader icon={unloaderIcon} />
) : (
<ImageLoader className={className} />
)}
</ImageContainer>
);
}
@@ -131,10 +121,10 @@ export function ImageLoader({ className }: ImageLoaderProps) {
);
}
export function ImageUnloader({ className }: ImageUnloaderProps) {
export function ImageUnloader({ className, icon = 'emptyImage' }: ImageUnloaderProps) {
return (
<div className={clsx(styles.unloader, className)}>
<Icon color="default" icon="emptyImage" size="25%" />
<Icon color="default" icon={icon} size="25%" />
</div>
);
}