diff --git a/src/Log4YM.Web/src/plugins/AnalogClockPlugin.tsx b/src/Log4YM.Web/src/plugins/AnalogClockPlugin.tsx index b7ccb57..e0c0467 100644 --- a/src/Log4YM.Web/src/plugins/AnalogClockPlugin.tsx +++ b/src/Log4YM.Web/src/plugins/AnalogClockPlugin.tsx @@ -18,13 +18,51 @@ export function AnalogClockPlugin() { const [time, setTime] = useState(new Date()); const containerRef = useRef(null); const { stationGrid } = useAppStore(); + const intervalRef = useRef | null>(null); - // Update time every second + // Update time every second, but only when page is visible useEffect(() => { - const interval = setInterval(() => { + const updateTime = () => { setTime(new Date()); - }, 1000); - return () => clearInterval(interval); + }; + + const startInterval = () => { + // Clear any existing interval + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + // Start new interval + intervalRef.current = setInterval(updateTime, 1000); + }; + + const stopInterval = () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }; + + const handleVisibilityChange = () => { + if (document.hidden) { + stopInterval(); + } else { + updateTime(); // Update immediately when becoming visible + startInterval(); + } + }; + + // Start interval initially if page is visible + if (!document.hidden) { + startInterval(); + } + + // Listen for visibility changes + document.addEventListener('visibilitychange', handleVisibilityChange); + + return () => { + stopInterval(); + document.removeEventListener('visibilitychange', handleVisibilityChange); + }; }, []); // Calculate sunrise/sunset times diff --git a/src/Log4YM.Web/src/plugins/GlobePlugin.tsx b/src/Log4YM.Web/src/plugins/GlobePlugin.tsx index 3718919..008f279 100644 --- a/src/Log4YM.Web/src/plugins/GlobePlugin.tsx +++ b/src/Log4YM.Web/src/plugins/GlobePlugin.tsx @@ -110,7 +110,6 @@ interface GlobeInstance { export function GlobeCore({ hideOverlays, hideCompass }: { hideOverlays?: boolean; hideCompass?: boolean }) { const containerRef = useRef(null); const globeRef = useRef(null); - const animationRef = useRef(null); const cameraAnimationRef = useRef(null); const lastTargetCoordsRef = useRef<{ lat: number; lng: number } | null>(null); @@ -121,6 +120,7 @@ export function GlobeCore({ hideOverlays, hideCompass }: { hideOverlays?: boolea const [currentAzimuth, setCurrentAzimuth] = useState(0); const [webglError, setWebglError] = useState(null); const [containerHeight, setContainerHeight] = useState(0); + const [globeReady, setGlobeReady] = useState(false); // Get current radio state if connected const selectedRadioState = selectedRadioId ? radioStates.get(selectedRadioId) : null; @@ -216,10 +216,6 @@ export function GlobeCore({ hideOverlays, hideCompass }: { hideOverlays?: boolea const numSegments = 50; const maxDistance = 18000; - // Subtle pulse effect for rotator beam - const pulseTime = Date.now() / 3000; - const pulseFactor = 0.6 + Math.sin(pulseTime * Math.PI * 2) * 0.1; - // Render rotator beam only when connected if (isConnected) { // Create left and right edge paths (amber accent) @@ -240,7 +236,7 @@ export function GlobeCore({ hideOverlays, hideCompass }: { hideOverlays?: boolea pathsData.push({ path: pathPoints, - color: `rgba(255, 180, 50, ${pulseFactor})`, // Amber accent for edges + color: 'rgba(255, 180, 50, 0.6)', // Amber accent for edges stroke: 3 }); } @@ -289,11 +285,7 @@ export function GlobeCore({ hideOverlays, hideCompass }: { hideOverlays?: boolea .ringsData([]); }, [stationLat, stationLon, getDestinationPoint]); - // Animation loop - use ref to avoid dependency on currentAzimuth - const currentAzimuthRef = useRef(currentAzimuth); - currentAzimuthRef.current = currentAzimuth; - - // Track rotator enabled status in ref for animation loop + // Track rotator enabled status in ref for click handler (avoids stale closure) const rotatorEnabledRef = useRef(rotatorEnabled); rotatorEnabledRef.current = rotatorEnabled; @@ -308,11 +300,6 @@ export function GlobeCore({ hideOverlays, hideCompass }: { hideOverlays?: boolea const commandedAzimuthRef = useRef(null); const displayedAzimuthRef = useRef(0); - const animateBeam = useCallback(() => { - renderBeam(currentAzimuthRef.current, rotatorEnabledRef.current); - animationRef.current = requestAnimationFrame(animateBeam); - }, [renderBeam]); - // Initialize globe - only runs once on mount useEffect(() => { if (!containerRef.current) return; @@ -509,7 +496,7 @@ export function GlobeCore({ hideOverlays, hideCompass }: { hideOverlays?: boolea resizeObserver.observe(containerRef.current); window.addEventListener('resize', debouncedResize); setTimeout(handleResize, 100); - animationRef.current = requestAnimationFrame(animateBeam); + setGlobeReady(true); } catch (e) { if (!isCancelled) { @@ -524,6 +511,8 @@ export function GlobeCore({ hideOverlays, hideCompass }: { hideOverlays?: boolea // Cleanup function return () => { isCancelled = true; + setGlobeReady(false); + globeRef.current = null; if (resizeObserver) { resizeObserver.disconnect(); } @@ -533,13 +522,24 @@ export function GlobeCore({ hideOverlays, hideCompass }: { hideOverlays?: boolea if (resizeTimeout) { clearTimeout(resizeTimeout); } - if (animationRef.current) { - cancelAnimationFrame(animationRef.current); - } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [stationLat, stationLon, stationGrid]); + // Render the beam whenever its inputs change. Replaces the old per-frame rAF + // loop that drove the GPU helper to >100% CPU even while idle. + useEffect(() => { + if (!globeReady) return; + renderBeam(currentAzimuth, rotatorEnabled); + }, [ + globeReady, + currentAzimuth, + rotatorEnabled, + focusedCallsignInfo?.latitude, + focusedCallsignInfo?.longitude, + renderBeam, + ]); + // Update beam when rotator position changes useEffect(() => { if (rotatorPosition?.currentAzimuth != null && typeof rotatorPosition.currentAzimuth === 'number') { diff --git a/src/Log4YM.Web/src/plugins/HeaderPlugin.tsx b/src/Log4YM.Web/src/plugins/HeaderPlugin.tsx index 05ccc80..c0c8637 100644 --- a/src/Log4YM.Web/src/plugins/HeaderPlugin.tsx +++ b/src/Log4YM.Web/src/plugins/HeaderPlugin.tsx @@ -57,8 +57,42 @@ export function HeaderPlugin() { }, []); useEffect(() => { - const timer = setInterval(() => setCurrentTime(new Date()), 1000); - return () => clearInterval(timer); + let timer: ReturnType | null = null; + + const updateTime = () => setCurrentTime(new Date()); + + const startTimer = () => { + if (timer) clearInterval(timer); + timer = setInterval(updateTime, 1000); + }; + + const stopTimer = () => { + if (timer) { + clearInterval(timer); + timer = null; + } + }; + + const handleVisibilityChange = () => { + if (document.hidden) { + stopTimer(); + } else { + updateTime(); // Update immediately when becoming visible + startTimer(); + } + }; + + // Start timer initially if page is visible + if (!document.hidden) { + startTimer(); + } + + document.addEventListener('visibilitychange', handleVisibilityChange); + + return () => { + stopTimer(); + document.removeEventListener('visibilitychange', handleVisibilityChange); + }; }, []); useEffect(() => { diff --git a/src/Log4YM.Web/src/plugins/LogEntryPlugin.tsx b/src/Log4YM.Web/src/plugins/LogEntryPlugin.tsx index b22fb42..e1427fb 100644 --- a/src/Log4YM.Web/src/plugins/LogEntryPlugin.tsx +++ b/src/Log4YM.Web/src/plugins/LogEntryPlugin.tsx @@ -133,12 +133,41 @@ export function LogEntryPlugin() { setQsoDate(formatDateForInput(now)); setQsoTime(formatTimeForInput(now)); }; - updateTime(); - timeIntervalRef.current = setInterval(updateTime, 1000); - return () => { + + const startInterval = () => { if (timeIntervalRef.current) { clearInterval(timeIntervalRef.current); } + timeIntervalRef.current = setInterval(updateTime, 1000); + }; + + const stopInterval = () => { + if (timeIntervalRef.current) { + clearInterval(timeIntervalRef.current); + timeIntervalRef.current = null; + } + }; + + const handleVisibilityChange = () => { + if (document.hidden) { + stopInterval(); + } else { + updateTime(); // Update immediately when becoming visible + startInterval(); + } + }; + + // Update immediately and start interval if page is visible + updateTime(); + if (!document.hidden) { + startInterval(); + } + + document.addEventListener('visibilitychange', handleVisibilityChange); + + return () => { + stopInterval(); + document.removeEventListener('visibilitychange', handleVisibilityChange); }; } }, [timeLocked]);