mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-11-14 18:58:49 -05:00
improvements and bug fixes
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// Room Camera Panel
|
||||
// Purpose: Defines the Room Camera Panel module and the local helpers/components used in this file.
|
||||
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSessionSelector } from '../../context/SessionContext.jsx';
|
||||
import { useSettingsNamespace } from '../../settings/index.js';
|
||||
import { useRoomCameraSnapshots } from '../../hooks/useRoomCameraSnapshots.js';
|
||||
@@ -18,6 +18,7 @@ function EmptyState() {
|
||||
}
|
||||
|
||||
const ORIENTATIONS = ['horizontal', 'vertical'];
|
||||
const CAMERA_VISIBILITY_ROOT_MARGIN = '0px';
|
||||
|
||||
function normalizeOrientation(value, fallback) {
|
||||
if (ORIENTATIONS.includes(value)) {
|
||||
@@ -26,6 +27,82 @@ function normalizeOrientation(value, fallback) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function useCameraPanelSubscriptionGate() {
|
||||
const panelRef = useRef(null);
|
||||
const [isPanelVisible, setIsPanelVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
/*
|
||||
Browser visibility can only be measured after React has mounted a real DOM
|
||||
node. Starting closed avoids a short subscribe/unsubscribe burst for room
|
||||
camera panels that mount below the fold.
|
||||
*/
|
||||
const panel = panelRef.current;
|
||||
if (!panel || typeof window === 'undefined' || typeof document === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let intersectsViewport = true;
|
||||
|
||||
const publishVisibility = () => {
|
||||
/*
|
||||
Server upload bandwidth is only saved when the socket subscription is
|
||||
actually disabled. Combining viewport intersection with page visibility
|
||||
means a scrolled-away panel and a hidden browser tab both stop receiving
|
||||
room-camera frame uploads from socketGateway.js.
|
||||
*/
|
||||
setIsPanelVisible(intersectsViewport && document.visibilityState !== 'hidden');
|
||||
};
|
||||
|
||||
const handlePageVisibilityChange = () => {
|
||||
/*
|
||||
IntersectionObserver is about layout visibility, while the Page
|
||||
Visibility API is about whether the tab itself can be seen. The latter
|
||||
matters here because a background tab can still keep a mounted panel and
|
||||
socket alive unless we explicitly close the subscription gate.
|
||||
*/
|
||||
publishVisibility();
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', handlePageVisibilityChange);
|
||||
|
||||
if (typeof IntersectionObserver !== 'function') {
|
||||
/*
|
||||
Without IntersectionObserver we cannot cheaply know whether the panel is
|
||||
clipped by a scroll container. Fall back to page visibility so browsers
|
||||
without the observer still behave correctly, just without scroll-based
|
||||
bandwidth savings.
|
||||
*/
|
||||
publishVisibility();
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handlePageVisibilityChange);
|
||||
};
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
/*
|
||||
A zero root margin keeps the bandwidth gate strict: the browser only
|
||||
subscribes once the panel intersects the viewport. If the first frame
|
||||
feels too delayed in real use, this constant can be widened later.
|
||||
*/
|
||||
intersectsViewport = Boolean(entry?.isIntersecting);
|
||||
publishVisibility();
|
||||
},
|
||||
{ root: null, rootMargin: CAMERA_VISIBILITY_ROOT_MARGIN, threshold: 0 },
|
||||
);
|
||||
|
||||
observer.observe(panel);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
document.removeEventListener('visibilitychange', handlePageVisibilityChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { panelRef, isPanelVisible };
|
||||
}
|
||||
|
||||
export default function RoomCameraPanel({
|
||||
defaultOrientation = 'horizontal',
|
||||
orientation: forcedOrientation,
|
||||
@@ -34,7 +111,12 @@ export default function RoomCameraPanel({
|
||||
panelId = null,
|
||||
}) {
|
||||
const cameras = useSessionSelector((state) => state.session?.roomCameras || []);
|
||||
const feedMap = useRoomCameraSnapshots(cameras.map((camera) => ({ id: camera.id })));
|
||||
const cameraIds = useMemo(
|
||||
() => cameras.map((camera) => camera.id),
|
||||
[cameras],
|
||||
);
|
||||
const { panelRef, isPanelVisible } = useCameraPanelSubscriptionGate();
|
||||
const feedMap = useRoomCameraSnapshots(cameraIds, { enabled: isPanelVisible });
|
||||
const { value: orientationSettings, save: saveOrientationSettings } = useSettingsNamespace('roomCameraPanels', {});
|
||||
const [orientation, setOrientation] = useState(() =>
|
||||
normalizeOrientation(
|
||||
@@ -42,17 +124,19 @@ export default function RoomCameraPanel({
|
||||
'horizontal',
|
||||
),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!panelId) return;
|
||||
const stored = orientationSettings?.[panelId];
|
||||
if (!stored) return;
|
||||
setOrientation(normalizeOrientation(stored, 'horizontal'));
|
||||
// only respond to changes for this panel id
|
||||
}, [panelId, orientationSettings?.[panelId]]);
|
||||
const storedOrientation = panelId ? orientationSettings?.[panelId] : null;
|
||||
const effectiveOrientation = forcedOrientation
|
||||
? normalizeOrientation(forcedOrientation, 'horizontal')
|
||||
: orientation;
|
||||
: normalizeOrientation(
|
||||
/*
|
||||
Settings are external state, so derive from them during render instead
|
||||
of mirroring them into local state from an effect. Local state remains
|
||||
useful as the immediate value after clicking the layout toggle, while
|
||||
stored settings win once the settings provider has loaded or saved.
|
||||
*/
|
||||
storedOrientation || orientation,
|
||||
'horizontal',
|
||||
);
|
||||
const containerClass =
|
||||
effectiveOrientation === 'vertical' ? 'flex flex-col gap-0.5' : 'grid gap-0.5 md:grid-cols-2';
|
||||
const showLayoutToggle = !hideLayoutToggle && !forcedOrientation && cameras.length > 0;
|
||||
@@ -64,7 +148,11 @@ export default function RoomCameraPanel({
|
||||
};
|
||||
|
||||
if (cameras.length === 0) {
|
||||
return <EmptyState />;
|
||||
return (
|
||||
<div ref={panelRef}>
|
||||
<EmptyState />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const actions = showLayoutToggle ? (
|
||||
@@ -86,27 +174,28 @@ export default function RoomCameraPanel({
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<CardFrame
|
||||
title="Room cameras"
|
||||
|
||||
actions={actions}
|
||||
hideHeader={hideHeader}
|
||||
bodyClassName="space-y-0.5 text-base"
|
||||
>
|
||||
<div className={containerClass}>
|
||||
{cameras.map((camera) => {
|
||||
const feed = feedMap[camera.id] || null;
|
||||
return (
|
||||
<article key={camera.id} className="w-full space-y-0.5 p-0.5">
|
||||
{/* <header className="space-y-0.5">
|
||||
<p className="text-lg font-semibold text-white">{camera.name || camera.id}</p>
|
||||
{camera.description && <p className="text-xs text-slate-500">{camera.description}</p>}
|
||||
</header> */}
|
||||
<RoomCameraFeed feed={feed} label={camera.name || camera.id} />
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardFrame>
|
||||
<div ref={panelRef}>
|
||||
<CardFrame
|
||||
title="Room cameras"
|
||||
actions={actions}
|
||||
hideHeader={hideHeader}
|
||||
bodyClassName="space-y-0.5 text-base"
|
||||
>
|
||||
<div className={containerClass}>
|
||||
{cameras.map((camera) => {
|
||||
const feed = feedMap[camera.id] || null;
|
||||
return (
|
||||
<article key={camera.id} className="w-full space-y-0.5 p-0.5">
|
||||
{/* <header className="space-y-0.5">
|
||||
<p className="text-lg font-semibold text-white">{camera.name || camera.id}</p>
|
||||
{camera.description && <p className="text-xs text-slate-500">{camera.description}</p>}
|
||||
</header> */}
|
||||
<RoomCameraFeed feed={feed} label={camera.name || camera.id} />
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardFrame>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user