Reqorked visibility toggle on samples, should help when dealing with more samples
This commit is contained in:
@@ -10,6 +10,10 @@ interface SampleImageCardProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
/** pass your scroll container element (e.g. containerRef.current) */
|
||||||
|
observerRoot?: Element | null;
|
||||||
|
/** optional: tweak pre-load buffer */
|
||||||
|
rootMargin?: string; // default '200px 0px'
|
||||||
}
|
}
|
||||||
|
|
||||||
const SampleImageCard: React.FC<SampleImageCardProps> = ({
|
const SampleImageCard: React.FC<SampleImageCardProps> = ({
|
||||||
@@ -20,70 +24,87 @@ const SampleImageCard: React.FC<SampleImageCardProps> = ({
|
|||||||
children,
|
children,
|
||||||
className = '',
|
className = '',
|
||||||
onClick = () => {},
|
onClick = () => {},
|
||||||
|
observerRoot = null,
|
||||||
|
rootMargin = '200px 0px',
|
||||||
}) => {
|
}) => {
|
||||||
const cardRef = useRef<HTMLDivElement>(null);
|
const cardRef = useRef<HTMLDivElement>(null);
|
||||||
const [isVisible, setIsVisible] = useState<boolean>(false);
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||||
const [loaded, setLoaded] = useState<boolean>(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
const [loaded, setLoaded] = useState(false);
|
||||||
|
|
||||||
|
// Observe both enter and exit
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Create intersection observer to check visibility
|
const el = cardRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
entries => {
|
entries => {
|
||||||
if (entries[0].isIntersecting) {
|
for (const entry of entries) {
|
||||||
setIsVisible(true);
|
if (entry.target === el) {
|
||||||
observer.disconnect();
|
setIsVisible(entry.isIntersecting);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ threshold: 0.1 },
|
{
|
||||||
|
root: observerRoot ?? null,
|
||||||
|
threshold: 0.01,
|
||||||
|
rootMargin,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (cardRef.current) {
|
observer.observe(el);
|
||||||
observer.observe(cardRef.current);
|
return () => observer.disconnect();
|
||||||
|
}, [observerRoot, rootMargin]);
|
||||||
|
|
||||||
|
// Pause video when leaving viewport
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isVideo(imageUrl)) return;
|
||||||
|
const v = videoRef.current;
|
||||||
|
if (!v) return;
|
||||||
|
if (!isVisible && !v.paused) {
|
||||||
|
try {
|
||||||
|
v.pause();
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
|
}, [isVisible, imageUrl]);
|
||||||
|
|
||||||
return () => {
|
useEffect(() => {
|
||||||
observer.disconnect();
|
console.log('SampleImageCard isVisible', isVisible, imageUrl);
|
||||||
};
|
}, [isVisible, imageUrl]);
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleLoad = (): void => {
|
const handleLoad = () => setLoaded(true);
|
||||||
setLoaded(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-col ${className}`}>
|
<div className={`flex flex-col ${className}`}>
|
||||||
{/* Square image container */}
|
<div ref={cardRef} className="relative w-full cursor-pointer" style={{ paddingBottom: '100%' }} onClick={onClick}>
|
||||||
<div
|
|
||||||
ref={cardRef}
|
|
||||||
className="relative w-full cursor-pointer"
|
|
||||||
style={{ paddingBottom: '100%' }} // Make it square
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
<div className="absolute inset-0 rounded-t-lg shadow-md">
|
<div className="absolute inset-0 rounded-t-lg shadow-md">
|
||||||
{isVisible && (
|
{isVisible ? (
|
||||||
<>
|
isVideo(imageUrl) ? (
|
||||||
{isVideo(imageUrl) ? (
|
<video
|
||||||
<video
|
ref={videoRef}
|
||||||
src={`/api/img/${encodeURIComponent(imageUrl)}`}
|
src={`/api/img/${encodeURIComponent(imageUrl)}`}
|
||||||
className={`w-full h-full object-cover`}
|
className="w-full h-full object-cover"
|
||||||
autoPlay={false}
|
preload="none"
|
||||||
loop
|
playsInline
|
||||||
muted
|
muted
|
||||||
playsInline
|
loop
|
||||||
/>
|
controls={false}
|
||||||
) : (
|
/>
|
||||||
<img
|
) : (
|
||||||
src={`/api/img/${encodeURIComponent(imageUrl)}`}
|
<img
|
||||||
alt={alt}
|
src={`/api/img/${encodeURIComponent(imageUrl)}`}
|
||||||
onLoad={handleLoad}
|
alt={alt}
|
||||||
className={`w-full h-full object-cover transition-opacity duration-300 ${
|
onLoad={handleLoad}
|
||||||
loaded ? 'opacity-100' : 'opacity-0'
|
loading="lazy"
|
||||||
}`}
|
decoding="async"
|
||||||
/>
|
className={`w-full h-full object-cover transition-opacity duration-300 ${
|
||||||
)}
|
loaded ? 'opacity-100' : 'opacity-0'
|
||||||
</>
|
}`}
|
||||||
)}
|
/>
|
||||||
{children && <div className="absolute inset-0 flex items-center justify-center">{children}</div>}
|
)
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{children && isVisible && <div className="absolute inset-0 flex items-center justify-center">{children}</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState, useRef } from 'react';
|
||||||
import useSampleImages from '@/hooks/useSampleImages';
|
import useSampleImages from '@/hooks/useSampleImages';
|
||||||
import SampleImageCard from './SampleImageCard';
|
import SampleImageCard from './SampleImageCard';
|
||||||
import { Job } from '@prisma/client';
|
import { Job } from '@prisma/client';
|
||||||
@@ -68,6 +68,7 @@ interface SampleImagesProps {
|
|||||||
export default function SampleImages({ job }: SampleImagesProps) {
|
export default function SampleImages({ job }: SampleImagesProps) {
|
||||||
const { sampleImages, status, refreshSampleImages } = useSampleImages(job.id, 5000);
|
const { sampleImages, status, refreshSampleImages } = useSampleImages(job.id, 5000);
|
||||||
const [selectedSamplePath, setSelectedSamplePath] = useState<string | null>(null);
|
const [selectedSamplePath, setSelectedSamplePath] = useState<string | null>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const numSamples = useMemo(() => {
|
const numSamples = useMemo(() => {
|
||||||
if (job?.job_config) {
|
if (job?.job_config) {
|
||||||
const jobConfig = JSON.parse(job.job_config) as JobConfig;
|
const jobConfig = JSON.parse(job.job_config) as JobConfig;
|
||||||
@@ -232,7 +233,7 @@ export default function SampleImages({ job }: SampleImagesProps) {
|
|||||||
}, [job]);
|
}, [job]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div ref={containerRef} className="absolute top-[80px] left-0 right-0 bottom-0 overflow-y-auto">
|
||||||
<div className="pb-4">
|
<div className="pb-4">
|
||||||
{PageInfoContent}
|
{PageInfoContent}
|
||||||
{sampleImages && (
|
{sampleImages && (
|
||||||
@@ -245,6 +246,7 @@ export default function SampleImages({ job }: SampleImagesProps) {
|
|||||||
sampleImages={sampleImages}
|
sampleImages={sampleImages}
|
||||||
alt="Sample Image"
|
alt="Sample Image"
|
||||||
onClick={() => setSelectedSamplePath(sample)}
|
onClick={() => setSelectedSamplePath(sample)}
|
||||||
|
observerRoot={containerRef.current}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user