mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-11-15 03:02:17 -05:00
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
// Shared spectator replay handling keeps disabled pages from retaining old popups.
|
||||
import { useEffect } from 'react';
|
||||
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
|
||||
import ReplayReadyPopup from '../ReplaySourcesPanel/ReplayReadyPopup.jsx';
|
||||
|
||||
export default function SpectatorReplayPopup({ enabled, ready }) {
|
||||
const replay = useSessionSelector((state) => state.latestReplay);
|
||||
const { clearLatestReplay } = useSessionActions();
|
||||
|
||||
useEffect(() => {
|
||||
// Consume arrivals while disabled so enabling popups only shows future replays.
|
||||
if (ready && !enabled && replay) clearLatestReplay();
|
||||
}, [ready, enabled, replay, clearLatestReplay]);
|
||||
|
||||
return ready && enabled ? <ReplayReadyPopup replay={replay} onClose={clearLatestReplay} /> : null;
|
||||
}
|
||||
+8
-15
@@ -1,18 +1,11 @@
|
||||
// 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.
|
||||
// Spectator Settings
|
||||
// Purpose: Provides a shared gear menu for page-defined spectator options.
|
||||
// Scope: Pages supply their options and own preference persistence.
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { FaCog } from 'react-icons/fa';
|
||||
import CardFrame from '../../../components/CardFrame/index.jsx';
|
||||
import CardFrame from '../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 }) {
|
||||
export default function SpectatorSettings({ options, preferences, onToggle }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const controlsRef = useRef(null);
|
||||
|
||||
@@ -45,7 +38,7 @@ export default function SpectatorViewControls({ preferences, onToggle }) {
|
||||
<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-label="Spectator settings"
|
||||
aria-expanded={open}
|
||||
aria-controls="spectator-view-controls"
|
||||
onClick={() => setOpen((current) => !current)}
|
||||
@@ -54,13 +47,13 @@ export default function SpectatorViewControls({ preferences, onToggle }) {
|
||||
</button>
|
||||
{open ? (
|
||||
<CardFrame
|
||||
title="Spectator view"
|
||||
title="Settings"
|
||||
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) => (
|
||||
{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"
|
||||
@@ -1,11 +1,13 @@
|
||||
// Server Display Content
|
||||
// Purpose: Composes the passive room display route used as a third spectator-style page.
|
||||
// Scope: Shows online names, rover driver/battery status, chat, replay overlays, reward overlays, and hidden audio.
|
||||
import { useSession, useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
|
||||
import { useSettingsNamespace } from '../../settings/index.js';
|
||||
import SpectatorSettings from '../../components/SpectatorSettings/index.jsx';
|
||||
import SpectatorReplayPopup from '../../components/SpectatorSettings/ReplayPopup.jsx';
|
||||
import { useSession } from '../../context/SessionContext.jsx';
|
||||
import { useSpectatorMode } from '../../hooks/useSpectatorMode.js';
|
||||
import useDefaultNickname from '../../hooks/useDefaultNickname.js';
|
||||
import useUserIdentitySync from '../../hooks/useUserIdentitySync.js';
|
||||
import ReplayReadyPopup from '../../components/ReplaySourcesPanel/ReplayReadyPopup.jsx';
|
||||
import RewardRunOverlay from '../../components/RewardRunOverlay/index.jsx';
|
||||
import OnlinePeopleStrip from './components/OnlinePeopleStrip.jsx';
|
||||
import DisplayRoverGrid from './components/DisplayRoverGrid.jsx';
|
||||
@@ -14,10 +16,15 @@ import DisplayNoticeOverlay from './components/DisplayNoticeOverlay.jsx';
|
||||
import DisplayPtzOperatorBadge from './components/DisplayPtzOperatorBadge.jsx';
|
||||
import AlertFeed from '../../components/AlertFeed/index.jsx';
|
||||
|
||||
const VIEW_OPTIONS = [{ key: 'showReplayPopups', label: 'Replay popups' }];
|
||||
|
||||
export default function ServerDisplayContent() {
|
||||
const { session } = useSession();
|
||||
const latestReplay = useSessionSelector((state) => state.latestReplay);
|
||||
const { clearLatestReplay } = useSessionActions();
|
||||
const { value: viewPreferences, save: saveViewPreferences, status: settingsStatus } = useSettingsNamespace(
|
||||
'displayPage',
|
||||
{ showReplayPopups: true },
|
||||
);
|
||||
const updateViewPreference = (key, enabled) => saveViewPreferences({ [key]: enabled });
|
||||
const inLockdown = session?.mode === 'lockdown';
|
||||
|
||||
useDefaultNickname();
|
||||
@@ -37,20 +44,21 @@ export default function ServerDisplayContent() {
|
||||
|
||||
return (
|
||||
<div className="display-page flex h-screen w-screen flex-col overflow-hidden bg-black text-slate-100">
|
||||
<div className="flex h-[8vh] min-h-16 shrink-0 overflow-hidden">
|
||||
<div className="flex h-[clamp(4rem,7.5vmin,6.5rem)] shrink-0 overflow-hidden pr-10">
|
||||
<OnlinePeopleStrip users={session?.users || []} />
|
||||
<DisplayPtzOperatorBadge />
|
||||
</div>
|
||||
<div className="min-h-0 flex-[0.72]">
|
||||
<div className="max-h-[65%] shrink-0 overflow-y-auto">
|
||||
<DisplayRoverGrid roster={session?.roster || []} session={session} />
|
||||
</div>
|
||||
<div className="min-h-0 flex-[1.28]">
|
||||
<div className="min-h-0 flex-1">
|
||||
<DisplayChatFeed />
|
||||
</div>
|
||||
<DisplayNoticeOverlay />
|
||||
<RewardRunOverlay />
|
||||
<ReplayReadyPopup replay={latestReplay} onClose={clearLatestReplay} />
|
||||
<AlertFeed scale={2} opacity={1}/>
|
||||
<SpectatorSettings options={VIEW_OPTIONS} preferences={viewPreferences} onToggle={updateViewPreference} />
|
||||
<SpectatorReplayPopup enabled={viewPreferences?.showReplayPopups !== false} ready={settingsStatus !== 'loading'} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import ChatPanel from '../../../components/ChatPanel/index.jsx';
|
||||
|
||||
export default function DisplayChatFeed() {
|
||||
const displayChatScale = 4.6;
|
||||
const displayChatScale = 4;
|
||||
|
||||
return (
|
||||
<section className="h-full min-h-0 overflow-hidden border-t border-slate-800 bg-black">
|
||||
|
||||
@@ -24,7 +24,7 @@ export default function DisplayPtzOperatorBadge() {
|
||||
like it enters from the right edge of the top bar while avoiding the
|
||||
previous problem where it covered content in the bottom-right corner.
|
||||
*/}
|
||||
<div className="flex h-full min-w-0 items-center justify-center gap-[1vw] px-[1.2vw] text-[clamp(2.1rem,5.1vh,5.6rem)] font-black leading-none tracking-normal text-white">
|
||||
<div className="flex h-full min-w-0 items-center justify-center gap-[1vw] px-[1.2vw] text-[clamp(1.25rem,3.2vmin,2.75rem)] font-black leading-none tracking-normal text-white">
|
||||
{/*
|
||||
The user explicitly requested uppercase "PTZ" here because the room
|
||||
display needs a terse, instantly recognizable camera marker. The name
|
||||
|
||||
@@ -45,7 +45,7 @@ export default function DisplayRoverCell({ rover, session }) {
|
||||
>
|
||||
<RoverHelpOverlay active={Boolean(rover?.needsHelp)} />
|
||||
<BatteryBar visual={visual} variant="background" orientation="vertical" />
|
||||
<div className="relative z-10 grid h-full min-h-0 grid-rows-[auto_minmax(0,1fr)_auto] gap-[0.55vh] p-[0.85vw] text-center">
|
||||
<div className="relative z-10 grid min-w-0 gap-[0.55vh] p-[0.85vw] text-center">
|
||||
<div className="grid min-w-0 grid-cols-[minmax(0,1fr)_auto] items-start gap-[0.8vw]">
|
||||
<div className="min-w-0">
|
||||
<RoverLabel
|
||||
@@ -71,8 +71,8 @@ export default function DisplayRoverCell({ rover, session }) {
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="grid min-h-0 min-w-0 grid-cols-[minmax(0,1.45fr)_minmax(0,0.85fr)] items-center gap-[1vw]">
|
||||
<div className="min-w-0">
|
||||
<div className="flex min-w-0 flex-wrap items-center justify-center gap-[1vw]">
|
||||
<div className="min-w-0 flex-[1.45_1_12rem]">
|
||||
<AutoFitText
|
||||
className={classNames(
|
||||
'font-black leading-none',
|
||||
@@ -84,7 +84,7 @@ export default function DisplayRoverCell({ rover, session }) {
|
||||
{driver?.label || 'Idle'}
|
||||
</AutoFitText>
|
||||
</div>
|
||||
<div className="min-w-0 bg-black/50 rounded-sm">
|
||||
<div className="min-w-0 max-w-[18rem] flex-[0.85_1_9rem] bg-black/50 rounded-sm text-center">
|
||||
<AutoFitText
|
||||
className={classNames(
|
||||
'font-black leading-none',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Display Rover Grid
|
||||
// Purpose: Fills the central 16:10 display band with rover driver/battery cells.
|
||||
// Purpose: Renders content-sized rows with rover driver/battery cells.
|
||||
// Scope: Keeps rover status presentation dense and label-free for room readability.
|
||||
import DisplayRoverCell from './DisplayRoverCell.jsx';
|
||||
import { gridClassForRoverCount } from '../utils.js';
|
||||
@@ -7,14 +7,14 @@ import { gridClassForRoverCount } from '../utils.js';
|
||||
export default function DisplayRoverGrid({ roster = [], session }) {
|
||||
if (!roster.length) {
|
||||
return (
|
||||
<section className="flex h-full min-h-0 items-center justify-center border-b border-slate-800 bg-black text-[clamp(2.5rem,7vh,7rem)] font-black text-slate-500">
|
||||
<section className="flex min-h-24 items-center justify-center border-b border-slate-800 bg-black text-[clamp(2.5rem,7vh,7rem)] font-black text-slate-500">
|
||||
No rovers
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={`grid h-full min-h-0 ${gridClassForRoverCount(roster.length)} auto-rows-fr gap-0.5 bg-slate-950 p-0.5`}>
|
||||
<section className={`grid items-stretch ${gridClassForRoverCount(roster.length)} auto-rows-fr gap-0.5 bg-slate-950 p-0.5`}>
|
||||
{roster.map((rover) => (
|
||||
<DisplayRoverCell key={rover.id} rover={rover} session={session} />
|
||||
))}
|
||||
|
||||
@@ -75,7 +75,7 @@ export default function OnlinePeopleStrip({ users = [] }) {
|
||||
<div
|
||||
ref={trackRef}
|
||||
className={classNames(
|
||||
'flex h-full w-max items-center whitespace-nowrap text-[clamp(2.5rem,5.3vh,5.8rem)] leading-none',
|
||||
'flex h-full w-max items-center whitespace-nowrap text-[clamp(2.25rem,5.5vmin,4.5rem)] leading-none',
|
||||
)}
|
||||
>
|
||||
<div ref={contentRef} className="flex h-full items-center">
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// Mini Summary Content
|
||||
// Purpose: Defines the Mini Summary 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 { useSettingsNamespace } from '../../settings/index.js';
|
||||
import SpectatorSettings from '../../components/SpectatorSettings/index.jsx';
|
||||
import SpectatorReplayPopup from '../../components/SpectatorSettings/ReplayPopup.jsx';
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSession } from '../../context/SessionContext.jsx';
|
||||
import { useVisualTelemetryFrames } from '../../context/TelemetryContext.jsx';
|
||||
@@ -150,8 +153,15 @@ function MiniPtzUserColumn({ label }) {
|
||||
);
|
||||
}
|
||||
|
||||
const VIEW_OPTIONS = [{ key: 'showReplayPopups', label: 'Replay popups' }];
|
||||
|
||||
export default function MiniSummaryContent() {
|
||||
const { session } = useSession();
|
||||
const { value: viewPreferences, save: saveViewPreferences, status: settingsStatus } = useSettingsNamespace(
|
||||
'miniPage',
|
||||
{ showReplayPopups: true },
|
||||
);
|
||||
const updateViewPreference = (key, enabled) => saveViewPreferences({ [key]: enabled });
|
||||
const spectatorReady = useSpectatorMode();
|
||||
useDefaultNickname();
|
||||
// Mini renders outside App.jsx, so it must run the same persisted identity
|
||||
@@ -405,6 +415,8 @@ export default function MiniSummaryContent() {
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
<SpectatorSettings options={VIEW_OPTIONS} preferences={viewPreferences} onToggle={updateViewPreference} />
|
||||
<SpectatorReplayPopup enabled={viewPreferences?.showReplayPopups !== false} ready={settingsStatus !== 'loading'} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user