From c233a80337078ece00a0afb724f54e157c7018f2 Mon Sep 17 00:00:00 2001 From: Jaret Burkett Date: Sun, 28 Sep 2025 14:13:10 -0600 Subject: [PATCH] Reqorked visibility toggle on samples, should help when dealing with more samples --- ui/src/components/SampleImageCard.tsx | 115 +++++++++++++++----------- ui/src/components/SampleImages.tsx | 6 +- 2 files changed, 72 insertions(+), 49 deletions(-) diff --git a/ui/src/components/SampleImageCard.tsx b/ui/src/components/SampleImageCard.tsx index e28bb014..d5234114 100644 --- a/ui/src/components/SampleImageCard.tsx +++ b/ui/src/components/SampleImageCard.tsx @@ -10,6 +10,10 @@ interface SampleImageCardProps { className?: string; onDelete?: () => 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 = ({ @@ -20,70 +24,87 @@ const SampleImageCard: React.FC = ({ children, className = '', onClick = () => {}, + observerRoot = null, + rootMargin = '200px 0px', }) => { const cardRef = useRef(null); - const [isVisible, setIsVisible] = useState(false); - const [loaded, setLoaded] = useState(false); + const videoRef = useRef(null); + const [isVisible, setIsVisible] = useState(false); + const [loaded, setLoaded] = useState(false); + // Observe both enter and exit useEffect(() => { - // Create intersection observer to check visibility + const el = cardRef.current; + if (!el) return; + const observer = new IntersectionObserver( entries => { - if (entries[0].isIntersecting) { - setIsVisible(true); - observer.disconnect(); + for (const entry of entries) { + if (entry.target === el) { + setIsVisible(entry.isIntersecting); + } } }, - { threshold: 0.1 }, + { + root: observerRoot ?? null, + threshold: 0.01, + rootMargin, + }, ); - if (cardRef.current) { - observer.observe(cardRef.current); + observer.observe(el); + 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 () => { - observer.disconnect(); - }; - }, []); + useEffect(() => { + console.log('SampleImageCard isVisible', isVisible, imageUrl); + }, [isVisible, imageUrl]); - const handleLoad = (): void => { - setLoaded(true); - }; + const handleLoad = () => setLoaded(true); return (
- {/* Square image container */} -
+
- {isVisible && ( - <> - {isVideo(imageUrl) ? ( -
diff --git a/ui/src/components/SampleImages.tsx b/ui/src/components/SampleImages.tsx index be4e3524..85d50c03 100644 --- a/ui/src/components/SampleImages.tsx +++ b/ui/src/components/SampleImages.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { useMemo, useState, useRef } from 'react'; import useSampleImages from '@/hooks/useSampleImages'; import SampleImageCard from './SampleImageCard'; import { Job } from '@prisma/client'; @@ -68,6 +68,7 @@ interface SampleImagesProps { export default function SampleImages({ job }: SampleImagesProps) { const { sampleImages, status, refreshSampleImages } = useSampleImages(job.id, 5000); const [selectedSamplePath, setSelectedSamplePath] = useState(null); + const containerRef = useRef(null); const numSamples = useMemo(() => { if (job?.job_config) { const jobConfig = JSON.parse(job.job_config) as JobConfig; @@ -232,7 +233,7 @@ export default function SampleImages({ job }: SampleImagesProps) { }, [job]); return ( -
+
{PageInfoContent} {sampleImages && ( @@ -245,6 +246,7 @@ export default function SampleImages({ job }: SampleImagesProps) { sampleImages={sampleImages} alt="Sample Image" onClick={() => setSelectedSamplePath(sample)} + observerRoot={containerRef.current} /> ))}