mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Display activity indicators when debug and mask/zone images load (#12411)
This commit is contained in:
		
							parent
							
								
									aaafd63b94
								
							
						
					
					
						commit
						2ebd2dfcc7
					
				| @ -4,6 +4,7 @@ import useSWR from "swr"; | ||||
| import ActivityIndicator from "../indicators/activity-indicator"; | ||||
| import { useResizeObserver } from "@/hooks/resize-observer"; | ||||
| import { isDesktop } from "react-device-detect"; | ||||
| import { cn } from "@/lib/utils"; | ||||
| 
 | ||||
| type CameraImageProps = { | ||||
|   className?: string; | ||||
| @ -20,7 +21,7 @@ export default function CameraImage({ | ||||
| }: CameraImageProps) { | ||||
|   const { data: config } = useSWR("config"); | ||||
|   const apiHost = useApiHost(); | ||||
|   const [hasLoaded, setHasLoaded] = useState(false); | ||||
|   const [imageLoaded, setImageLoaded] = useState(false); | ||||
|   const containerRef = useRef<HTMLDivElement | null>(null); | ||||
|   const imgRef = useRef<HTMLImageElement | null>(null); | ||||
| 
 | ||||
| @ -31,7 +32,7 @@ export default function CameraImage({ | ||||
|     useResizeObserver(containerRef); | ||||
| 
 | ||||
|   const requestHeight = useMemo(() => { | ||||
|     if (!config || containerHeight == 0 || !hasLoaded) { | ||||
|     if (!config || containerHeight == 0) { | ||||
|       return 360; | ||||
|     } | ||||
| 
 | ||||
| @ -39,47 +40,66 @@ export default function CameraImage({ | ||||
|       config.cameras[camera].detect.height, | ||||
|       Math.round(containerHeight * (isDesktop ? 1.1 : 1.25)), | ||||
|     ); | ||||
|   }, [config, camera, containerHeight, hasLoaded]); | ||||
|   }, [config, camera, containerHeight]); | ||||
| 
 | ||||
|   const isPortraitImage = useMemo(() => { | ||||
|     if (imgRef.current && containerWidth && containerHeight && hasLoaded) { | ||||
|       const { naturalHeight, naturalWidth } = imgRef.current; | ||||
|       return naturalWidth / naturalHeight < containerWidth / containerHeight; | ||||
|     } | ||||
|   }, [containerWidth, containerHeight, hasLoaded]); | ||||
|   const [isPortraitImage, setIsPortraitImage] = useState(false); | ||||
| 
 | ||||
|   useEffect(() => setHasLoaded(false), [camera]); | ||||
|   useEffect(() => { | ||||
|     setImageLoaded(false); | ||||
|     setIsPortraitImage(false); | ||||
|   }, [camera]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (!config || !imgRef.current) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     imgRef.current.src = `${apiHost}api/${name}/latest.webp?h=${requestHeight}${ | ||||
|     const newSrc = `${apiHost}api/${name}/latest.webp?h=${requestHeight}${ | ||||
|       searchParams ? `&${searchParams}` : "" | ||||
|     }`;
 | ||||
|   }, [apiHost, name, imgRef, searchParams, requestHeight, config]); | ||||
| 
 | ||||
|     if (imgRef.current.src !== newSrc) { | ||||
|       imgRef.current.src = newSrc; | ||||
|     } | ||||
|   }, [apiHost, name, searchParams, requestHeight, config, camera]); | ||||
| 
 | ||||
|   const handleImageLoad = () => { | ||||
|     if (imgRef.current && containerWidth && containerHeight) { | ||||
|       const { naturalWidth, naturalHeight } = imgRef.current; | ||||
|       setIsPortraitImage( | ||||
|         naturalWidth / naturalHeight < containerWidth / containerHeight, | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     setImageLoaded(true); | ||||
| 
 | ||||
|     if (onload) { | ||||
|       onload(); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <div className={className} ref={containerRef}> | ||||
|       {enabled ? ( | ||||
|         <img | ||||
|           ref={imgRef} | ||||
|           className={`object-contain ${isPortraitImage ? "h-full w-auto" : "h-auto w-full"} rounded-lg md:rounded-2xl`} | ||||
|           onLoad={() => { | ||||
|             setHasLoaded(true); | ||||
| 
 | ||||
|             if (onload) { | ||||
|               onload(); | ||||
|             } | ||||
|           }} | ||||
|           className={cn( | ||||
|             "object-contain", | ||||
|             imageLoaded | ||||
|               ? isPortraitImage | ||||
|                 ? "h-full w-auto" | ||||
|                 : "h-auto w-full" | ||||
|               : "invisible", | ||||
|             "rounded-lg md:rounded-2xl", | ||||
|           )} | ||||
|           onLoad={handleImageLoad} | ||||
|         /> | ||||
|       ) : ( | ||||
|         <div className="pt-6 text-center"> | ||||
|           Camera is disabled in config, no stream or snapshot available! | ||||
|         </div> | ||||
|       )} | ||||
|       {!hasLoaded && enabled ? ( | ||||
|       {!imageLoaded && enabled ? ( | ||||
|         <div className="absolute bottom-0 left-0 right-0 top-0 flex items-center justify-center"> | ||||
|           <ActivityIndicator /> | ||||
|         </div> | ||||
|  | ||||
| @ -5,6 +5,7 @@ import Konva from "konva"; | ||||
| import type { KonvaEventObject } from "konva/lib/Node"; | ||||
| import { Polygon, PolygonType } from "@/types/canvas"; | ||||
| import { useApiHost } from "@/api"; | ||||
| import ActivityIndicator from "@/components/indicators/activity-indicator"; | ||||
| 
 | ||||
| type PolygonCanvasProps = { | ||||
|   containerRef: RefObject<HTMLDivElement>; | ||||
| @ -29,6 +30,7 @@ export function PolygonCanvas({ | ||||
|   hoveredPolygonIndex, | ||||
|   selectedZoneMask, | ||||
| }: PolygonCanvasProps) { | ||||
|   const [isLoaded, setIsLoaded] = useState(false); | ||||
|   const [image, setImage] = useState<HTMLImageElement | undefined>(); | ||||
|   const imageRef = useRef<Konva.Image | null>(null); | ||||
|   const stageRef = useRef<Konva.Stage>(null); | ||||
| @ -36,13 +38,16 @@ export function PolygonCanvas({ | ||||
| 
 | ||||
|   const videoElement = useMemo(() => { | ||||
|     if (camera && width && height) { | ||||
|       setIsLoaded(false); | ||||
|       const element = new window.Image(); | ||||
|       element.width = width; | ||||
|       element.height = height; | ||||
|       element.src = `${apiHost}api/${camera}/latest.webp?cache=${Date.now()}`; | ||||
|       return element; | ||||
|     } | ||||
|   }, [camera, width, height, apiHost]); | ||||
|     // we know that these deps are correct
 | ||||
|     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||
|   }, [camera, apiHost]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (!videoElement) { | ||||
| @ -50,6 +55,7 @@ export function PolygonCanvas({ | ||||
|     } | ||||
|     const onload = function () { | ||||
|       setImage(videoElement); | ||||
|       setIsLoaded(true); | ||||
|     }; | ||||
|     videoElement.addEventListener("load", onload); | ||||
|     return () => { | ||||
| @ -218,6 +224,10 @@ export function PolygonCanvas({ | ||||
|     } | ||||
|   }, [activePolygonIndex, polygons, setPolygons]); | ||||
| 
 | ||||
|   if (!isLoaded) { | ||||
|     return <ActivityIndicator />; | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <Stage | ||||
|       ref={stageRef} | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user