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 { .image-container.is-round {
&::before { border-radius: 50%;
border-radius: 50%;
}
} }
.favorite-badge { .favorite-badge {
@@ -785,9 +785,9 @@ const PosterItemCard = ({
<> <>
<ItemImage <ItemImage
className={clsx(styles.image, { [styles.isRound]: isRound })} className={clsx(styles.image, { [styles.isRound]: isRound })}
id={data?.id} id={(data as { imageId: string })?.imageId}
itemType={itemType} itemType={itemType}
src={(data as Album | AlbumArtist | Playlist | Song)?.imageUrl} src={(data as { imageUrl: string })?.imageUrl}
/> />
{isFavorite && <div className={styles.favoriteBadge} />} {isFavorite && <div className={styles.favoriteBadge} />}
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>} {hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
@@ -11,6 +11,25 @@ import {
import { BaseImage, ImageProps } from '/@/shared/components/image/image'; import { BaseImage, ImageProps } from '/@/shared/components/image/image';
import { LibraryItem } from '/@/shared/types/domain-types'; 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 = ( const BaseItemImage = (
props: Omit<ImageProps, 'src'> & { props: Omit<ImageProps, 'src'> & {
id?: null | string; id?: null | string;
@@ -27,7 +46,7 @@ const BaseItemImage = (
size: 300, size: 300,
}); });
return <BaseImage src={imageUrl} {...rest} />; return <BaseImage src={imageUrl} unloaderIcon={getUnloaderIcon(props.itemType)} {...rest} />;
}; };
export const ItemImage = memo(BaseItemImage); export const ItemImage = memo(BaseItemImage);
+5
View File
@@ -160,7 +160,12 @@ export const AppIcon = {
edit: LuPencilLine, edit: LuPencilLine,
ellipsisHorizontal: LuEllipsis, ellipsisHorizontal: LuEllipsis,
ellipsisVertical: LuEllipsisVertical, ellipsisVertical: LuEllipsisVertical,
emptyAlbumImage: LuDisc3,
emptyArtistImage: LuUser,
emptyGenreImage: LuFlag,
emptyImage: LuDisc3, emptyImage: LuDisc3,
emptyPlaylistImage: LuListMusic,
emptySongImage: LuMusic,
error: LuShieldAlert, error: LuShieldAlert,
externalLink: LuExternalLink, externalLink: LuExternalLink,
favorite: LuHeart, favorite: LuHeart,
+34 -44
View File
@@ -11,7 +11,7 @@ import { Img } from 'react-image';
import styles from './image.module.css'; 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 { Skeleton } from '/@/shared/components/skeleton/skeleton';
import { useInViewport } from '/@/shared/hooks/use-in-viewport'; import { useInViewport } from '/@/shared/hooks/use-in-viewport';
@@ -23,6 +23,7 @@ export interface ImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 's
includeUnloader?: boolean; includeUnloader?: boolean;
src: string | string[] | undefined; src: string | string[] | undefined;
thumbHash?: string; thumbHash?: string;
unloaderIcon?: keyof typeof AppIcon;
} }
interface ImageContainerProps extends HTMLAttributes<HTMLDivElement> { interface ImageContainerProps extends HTMLAttributes<HTMLDivElement> {
@@ -36,6 +37,7 @@ interface ImageLoaderProps {
interface ImageUnloaderProps { interface ImageUnloaderProps {
className?: string; className?: string;
icon?: keyof typeof AppIcon;
} }
export const FALLBACK_SVG = export const FALLBACK_SVG =
@@ -49,52 +51,40 @@ export function BaseImage({
includeLoader = true, includeLoader = true,
includeUnloader = true, includeUnloader = true,
src, src,
unloaderIcon = 'emptyImage',
...props ...props
}: ImageProps) { }: ImageProps) {
const { inViewport, ref } = useInViewport(); 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 ( return (
<ImageContainer className={containerClassName}> <ImageContainer
<ImageUnloader /> 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> </ImageContainer>
); );
} }
@@ -131,10 +121,10 @@ export function ImageLoader({ className }: ImageLoaderProps) {
); );
} }
export function ImageUnloader({ className }: ImageUnloaderProps) { export function ImageUnloader({ className, icon = 'emptyImage' }: ImageUnloaderProps) {
return ( return (
<div className={clsx(styles.unloader, className)}> <div className={clsx(styles.unloader, className)}>
<Icon color="default" icon="emptyImage" size="25%" /> <Icon color="default" icon={icon} size="25%" />
</div> </div>
); );
} }