Merge branch 'main' into REFACTOR-participation-mode-ptz
Container image / image (push) Failing after 55s

This commit is contained in:
legop3
2026-09-27 18:11:37 -04:00
committed by GitHub
41 changed files with 4015 additions and 47 deletions
@@ -1,7 +1,7 @@
// Spectator Content
// Purpose: Defines the Spectator Content 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 { useSession, useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
import { useSession } from '../../context/SessionContext.jsx';
import { useSpectatorMode } from '../../hooks/useSpectatorMode.js';
import { useSettingsNamespace } from '../../settings/index.js';
import useDefaultNickname from '../../hooks/useDefaultNickname.js';
@@ -13,14 +13,23 @@ import RoverQueuesPanel from '../../components/RoverQueuesPanel/index.jsx';
import RawUserPilePanel from '../../components/RawUserPilePanel/index.jsx';
import ButtonBoxPanel from '../../components/ButtonBoxPanel/index.jsx';
import RewardRunOverlay from '../../components/RewardRunOverlay/index.jsx';
import ReplayReadyPopup from '../../components/ReplaySourcesPanel/ReplayReadyPopup.jsx';
import SpectatorReplayPopup from '../../components/SpectatorSettings/ReplayPopup.jsx';
import usePortraitLayout from './hooks/usePortraitLayout.js';
import RoverRow from './components/RoverRow.jsx';
import SecondaryRow from './components/SecondaryRow.jsx';
import LogsRow from './components/LogsRow.jsx';
import SpectatorViewControls from './components/SpectatorViewControls.jsx';
import SpectatorSettings from '../../components/SpectatorSettings/index.jsx';
const VIEW_OPTIONS = [
{ key: 'showSidebar', label: 'Sidebar' },
{ key: 'showRovers', label: 'Rovers' },
{ key: 'showPtz', label: 'PTZ camera' },
{ key: 'showRoomCameras', label: 'Room cameras' },
{ key: 'showReplayPopups', label: 'Replay popups' },
];
const SPECTATOR_VIEW_DEFAULTS = {
showReplayPopups: true,
showSidebar: true,
showRovers: true,
showPtz: true,
@@ -29,9 +38,7 @@ const SPECTATOR_VIEW_DEFAULTS = {
export default function SpectatorContent() {
const { session } = useSession();
const latestReplay = useSessionSelector((state) => state.latestReplay);
const { clearLatestReplay } = useSessionActions();
const { value: viewPreferences, save: saveViewPreferences } = useSettingsNamespace(
const { value: viewPreferences, save: saveViewPreferences, status: settingsStatus } = useSettingsNamespace(
'spectatorPage',
SPECTATOR_VIEW_DEFAULTS,
);
@@ -151,14 +158,10 @@ export default function SpectatorContent() {
{showRoomCameras ? <SecondaryRow /> : null}
</section>
</main>
<SpectatorViewControls preferences={viewPreferences} onToggle={updateViewPreference} />
<SpectatorSettings options={VIEW_OPTIONS} preferences={viewPreferences} onToggle={updateViewPreference} />
<AlertFeed />
<RewardRunOverlay />
{/* Spectators do not have the replay request panel that normal web users see, so
the spectator route presents the shared latestReplay value directly. Every
spectator should receive the fullscreen replay when completed media becomes
available, regardless of which browser or transport requested its creation. */}
<ReplayReadyPopup replay={latestReplay} onClose={clearLatestReplay} />
<SpectatorReplayPopup enabled={viewPreferences?.showReplayPopups !== false} ready={settingsStatus !== 'loading'} />
</div>
);
}
@@ -1,82 +0,0 @@
// Spectator View Controls
// Purpose: Lets each spectator choose which major page regions consume space and media bandwidth.
// Scope: Owns only the fixed gear menu UI; cookie persistence and layout decisions remain in SpectatorContent.
import { useEffect, useRef, useState } from 'react';
import { FaCog } from 'react-icons/fa';
import CardFrame from '../../../components/CardFrame/index.jsx';
const VIEW_OPTIONS = [
{ key: 'showSidebar', label: 'Sidebar' },
{ key: 'showRovers', label: 'Rovers' },
{ key: 'showPtz', label: 'PTZ camera' },
{ key: 'showRoomCameras', label: 'Room cameras' },
];
export default function SpectatorViewControls({ preferences, onToggle }) {
const [open, setOpen] = useState(false);
const controlsRef = useRef(null);
useEffect(() => {
if (!open) return undefined;
const handlePointerDown = (event) => {
/*
The popup floats above both the sidebar and camera grid, so clicking
elsewhere should dismiss it without changing any saved preference.
Keeping the gear and popup under one ref makes clicks on either safe.
*/
if (!controlsRef.current?.contains(event.target)) setOpen(false);
};
const handleKeyDown = (event) => {
// Escape mirrors normal dialog/menu behavior without trapping keyboard focus.
if (event.key === 'Escape') setOpen(false);
};
document.addEventListener('pointerdown', handlePointerDown);
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('pointerdown', handlePointerDown);
document.removeEventListener('keydown', handleKeyDown);
};
}, [open]);
return (
<div ref={controlsRef} className="fixed right-1 top-1 z-50 flex flex-col items-end gap-0.5">
<button
type="button"
className="button-dark flex h-8 w-8 items-center justify-center p-0 text-slate-100 shadow-lg"
aria-label="Spectator view settings"
aria-expanded={open}
aria-controls="spectator-view-controls"
onClick={() => setOpen((current) => !current)}
>
<FaCog aria-hidden="true" />
</button>
{open ? (
<CardFrame
title="Spectator view"
className="w-48 shadow-xl"
bodyClassName="space-y-0.5 p-0.5 text-sm"
clipOverflow={false}
>
<div id="spectator-view-controls" className="space-y-0.5">
{VIEW_OPTIONS.map((option) => (
<label
key={option.key}
className="surface-muted flex cursor-pointer items-center gap-0.5 px-1 py-0.75 text-slate-100"
>
<input
type="checkbox"
className="h-3.5 w-3.5 accent-sky-500"
checked={preferences[option.key] !== false}
onChange={(event) => onToggle(option.key, event.target.checked)}
/>
<span>{option.label}</span>
</label>
))}
</div>
</CardFrame>
) : null}
</div>
);
}