132 lines
2.9 KiB
TypeScript
132 lines
2.9 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
memo,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
type ImgHTMLAttributes,
|
|
} from "react";
|
|
import { buildImageProxyUrl } from "@/lib/image-delivery";
|
|
|
|
type AgentPreviewImageProps = Omit<
|
|
ImgHTMLAttributes<HTMLImageElement>,
|
|
"src" | "loading" | "decoding"
|
|
> & {
|
|
src?: string | null;
|
|
previewWidth?: number;
|
|
previewQuality?: number;
|
|
previewFormat?: "webp" | "avif" | "jpeg" | "png";
|
|
rootMargin?: string;
|
|
eager?: boolean;
|
|
raw?: boolean;
|
|
};
|
|
|
|
const EMPTY_PIXEL =
|
|
"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
|
|
|
|
function buildPreviewUrl(params: {
|
|
src: string;
|
|
width: number;
|
|
quality: number;
|
|
format: "webp" | "avif" | "jpeg" | "png";
|
|
raw?: boolean;
|
|
}) {
|
|
if (params.raw) return params.src;
|
|
return (
|
|
buildImageProxyUrl(params.src, {
|
|
width: params.width,
|
|
quality: params.quality,
|
|
format: params.format,
|
|
}) || params.src
|
|
);
|
|
}
|
|
|
|
function AgentPreviewImageBase({
|
|
src,
|
|
previewWidth = 480,
|
|
previewQuality = 78,
|
|
previewFormat = "webp",
|
|
rootMargin = "640px 0px",
|
|
eager = false,
|
|
raw = false,
|
|
onError,
|
|
onLoad,
|
|
...props
|
|
}: AgentPreviewImageProps) {
|
|
const imageRef = useRef<HTMLImageElement | null>(null);
|
|
const [shouldLoad, setShouldLoad] = useState(eager);
|
|
const [useOriginal, setUseOriginal] = useState(false);
|
|
const normalizedSrc = String(src || "").trim();
|
|
|
|
useEffect(() => {
|
|
setUseOriginal(false);
|
|
setShouldLoad(eager);
|
|
}, [eager, normalizedSrc]);
|
|
|
|
useEffect(() => {
|
|
if (eager || shouldLoad || !normalizedSrc) return;
|
|
const image = imageRef.current;
|
|
if (!image) return;
|
|
|
|
if (!("IntersectionObserver" in window)) {
|
|
setShouldLoad(true);
|
|
return;
|
|
}
|
|
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
if (entries.some((entry) => entry.isIntersecting)) {
|
|
setShouldLoad(true);
|
|
observer.disconnect();
|
|
}
|
|
},
|
|
{ rootMargin, threshold: 0.01 }
|
|
);
|
|
observer.observe(image);
|
|
return () => observer.disconnect();
|
|
}, [eager, normalizedSrc, rootMargin, shouldLoad]);
|
|
|
|
const displaySrc = useMemo(() => {
|
|
if (!normalizedSrc || !shouldLoad) return EMPTY_PIXEL;
|
|
if (useOriginal) return normalizedSrc;
|
|
return buildPreviewUrl({
|
|
src: normalizedSrc,
|
|
width: previewWidth,
|
|
quality: previewQuality,
|
|
format: previewFormat,
|
|
raw,
|
|
});
|
|
}, [
|
|
normalizedSrc,
|
|
previewFormat,
|
|
previewQuality,
|
|
previewWidth,
|
|
raw,
|
|
shouldLoad,
|
|
useOriginal,
|
|
]);
|
|
|
|
return (
|
|
<img
|
|
{...props}
|
|
ref={imageRef}
|
|
src={displaySrc}
|
|
data-image-delivery="manual"
|
|
loading={eager ? "eager" : "lazy"}
|
|
decoding="async"
|
|
onError={(event) => {
|
|
if (normalizedSrc && !useOriginal && displaySrc !== normalizedSrc) {
|
|
setUseOriginal(true);
|
|
return;
|
|
}
|
|
onError?.(event);
|
|
}}
|
|
onLoad={onLoad}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export const AgentPreviewImage = memo(AgentPreviewImageBase);
|