mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
spectator page selectors
This commit is contained in:
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -78,8 +78,8 @@
|
|||||||
<script defer src="https://analytics.otter.land/script.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land"></script>
|
<script defer src="https://analytics.otter.land/script.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land"></script>
|
||||||
<script defer src="https://analytics.otter.land/recorder.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land" data-sample-rate="0.15" data-mask-level="moderate" data-max-duration="300000"></script>
|
<script defer src="https://analytics.otter.land/recorder.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land" data-sample-rate="0.15" data-mask-level="moderate" data-max-duration="300000"></script>
|
||||||
<title>Roomba Rover</title>
|
<title>Roomba Rover</title>
|
||||||
<script type="module" crossorigin src="/assets/index-CmNL6XEv.js"></script>
|
<script type="module" crossorigin src="/assets/index-DwzMEW-o.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-sm_EKOxJ.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-yu4pxjK3.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
1. improve spectator page, options on what to see and what not to see
|
1. improve spectator page, options on what to see and what not to see
|
||||||
2. assign rovers based on battery percentage, give people highest one
|
2. assign rovers based on battery percentage, give people highest one
|
||||||
3. verify centralized bandwidth-saving config in real external/LAN testing
|
3. add admin ui for VIP and private requests instead of only through discord
|
||||||
4. add admin ui for VIP and private requests instead of only through discord
|
4. pull page / tab title from session, use the name from profile of interinstance if enabled, if not just default to old one
|
||||||
5. pull page / tab title from session, use the name from profile of interinstance if enabled, if not just default to old one
|
5. make overcurrent limiter speed sensitive, slower fill at lower speeds
|
||||||
6. make overcurrent limiter speed sensitive, slower fill at lower speeds
|
6. add feature chat commands, like /neato start, /lift up, etc
|
||||||
7. add feature chat commands, like /neato start, /lift up, etc
|
7. add more background gap themes
|
||||||
8. add more background gap themes
|
8. fix this:
|
||||||
9. fix this:
|
|
||||||
`Jun 18 15:14:18 roombaserver.local node[216731]: /home/daniel/MultiRoombaRover/server/src/services/roverManager/socketHandlers.js:92
|
`Jun 18 15:14:18 roombaserver.local node[216731]: /home/daniel/MultiRoombaRover/server/src/services/roverManager/socketHandlers.js:92
|
||||||
Jun 18 15:14:18 roombaserver.local node[216731]: cb({ error: err.message });
|
Jun 18 15:14:18 roombaserver.local node[216731]: cb({ error: err.message });
|
||||||
Jun 18 15:14:18 roombaserver.local node[216731]: ^
|
Jun 18 15:14:18 roombaserver.local node[216731]: ^
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
||||||
import { useSession, useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
|
import { useSession, useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
|
||||||
import { useSpectatorMode } from '../../hooks/useSpectatorMode.js';
|
import { useSpectatorMode } from '../../hooks/useSpectatorMode.js';
|
||||||
|
import { useSettingsNamespace } from '../../settings/index.js';
|
||||||
import useDefaultNickname from '../../hooks/useDefaultNickname.js';
|
import useDefaultNickname from '../../hooks/useDefaultNickname.js';
|
||||||
import useUserIdentitySync from '../../hooks/useUserIdentitySync.js';
|
import useUserIdentitySync from '../../hooks/useUserIdentitySync.js';
|
||||||
import ChatPanel from '../../components/ChatPanel/index.jsx';
|
import ChatPanel from '../../components/ChatPanel/index.jsx';
|
||||||
@@ -17,11 +18,23 @@ import usePortraitLayout from './hooks/usePortraitLayout.js';
|
|||||||
import RoverRow from './components/RoverRow.jsx';
|
import RoverRow from './components/RoverRow.jsx';
|
||||||
import SecondaryRow from './components/SecondaryRow.jsx';
|
import SecondaryRow from './components/SecondaryRow.jsx';
|
||||||
import LogsRow from './components/LogsRow.jsx';
|
import LogsRow from './components/LogsRow.jsx';
|
||||||
|
import SpectatorViewControls from './components/SpectatorViewControls.jsx';
|
||||||
|
|
||||||
|
const SPECTATOR_VIEW_DEFAULTS = {
|
||||||
|
showSidebar: true,
|
||||||
|
showRovers: true,
|
||||||
|
showPtz: true,
|
||||||
|
showRoomCameras: true,
|
||||||
|
};
|
||||||
|
|
||||||
export default function SpectatorContent() {
|
export default function SpectatorContent() {
|
||||||
const { session } = useSession();
|
const { session } = useSession();
|
||||||
const latestReplay = useSessionSelector((state) => state.latestReplay);
|
const latestReplay = useSessionSelector((state) => state.latestReplay);
|
||||||
const { clearLatestReplay } = useSessionActions();
|
const { clearLatestReplay } = useSessionActions();
|
||||||
|
const { value: viewPreferences, save: saveViewPreferences } = useSettingsNamespace(
|
||||||
|
'spectatorPage',
|
||||||
|
SPECTATOR_VIEW_DEFAULTS,
|
||||||
|
);
|
||||||
const inLockdown = session?.mode === 'lockdown';
|
const inLockdown = session?.mode === 'lockdown';
|
||||||
const canUseSpectatorAccess = session?.bandwidthSavings?.canUseExternalSpectatorAccess !== false;
|
const canUseSpectatorAccess = session?.bandwidthSavings?.canUseExternalSpectatorAccess !== false;
|
||||||
const spectatorAccessMode = session?.bandwidthSavings?.externalSpectatorAccess || 'on';
|
const spectatorAccessMode = session?.bandwidthSavings?.externalSpectatorAccess || 'on';
|
||||||
@@ -34,6 +47,15 @@ export default function SpectatorContent() {
|
|||||||
useSpectatorMode();
|
useSpectatorMode();
|
||||||
const isPortraitLayout = usePortraitLayout();
|
const isPortraitLayout = usePortraitLayout();
|
||||||
const roster = session?.roster ?? [];
|
const roster = session?.roster ?? [];
|
||||||
|
// Treat absent keys as enabled so older settings cookies gain every new view
|
||||||
|
// option by default instead of unexpectedly hiding parts of the page.
|
||||||
|
const showSidebar = viewPreferences?.showSidebar !== false;
|
||||||
|
const showRovers = viewPreferences?.showRovers !== false;
|
||||||
|
const showPtz = viewPreferences?.showPtz !== false;
|
||||||
|
const showRoomCameras = viewPreferences?.showRoomCameras !== false;
|
||||||
|
const updateViewPreference = (key, enabled) => {
|
||||||
|
saveViewPreferences((current) => ({ ...(current || {}), [key]: Boolean(enabled) }));
|
||||||
|
};
|
||||||
|
|
||||||
if (inLockdown) {
|
if (inLockdown) {
|
||||||
return (
|
return (
|
||||||
@@ -68,7 +90,9 @@ export default function SpectatorContent() {
|
|||||||
|
|
||||||
const mainClass = isPortraitLayout
|
const mainClass = isPortraitLayout
|
||||||
? 'flex min-h-screen flex-col bg-black text-slate-100 md:h-screen md:overflow-hidden'
|
? 'flex min-h-screen flex-col bg-black text-slate-100 md:h-screen md:overflow-hidden'
|
||||||
: 'grid min-h-screen grid-cols-1 gap-0.5 bg-black text-slate-100 md:h-full md:min-h-0 md:grid-cols-[minmax(0,1fr)_18rem] lg:grid-cols-[minmax(0,1fr)_20rem]';
|
: showSidebar
|
||||||
|
? 'grid min-h-screen grid-cols-1 gap-0.5 bg-black text-slate-100 md:h-full md:min-h-0 md:grid-cols-[minmax(0,1fr)_18rem] lg:grid-cols-[minmax(0,1fr)_20rem]'
|
||||||
|
: 'grid min-h-screen grid-cols-1 gap-0.5 bg-black text-slate-100 md:h-full md:min-h-0';
|
||||||
const contentClass = isPortraitLayout
|
const contentClass = isPortraitLayout
|
||||||
? 'order-2 flex min-h-0 min-w-0 flex-1 flex-col gap-0.5 overflow-y-auto'
|
? 'order-2 flex min-h-0 min-w-0 flex-1 flex-col gap-0.5 overflow-y-auto'
|
||||||
: 'order-1 flex min-h-0 min-w-0 flex-col gap-0.5 md:overflow-y-auto';
|
: 'order-1 flex min-h-0 min-w-0 flex-col gap-0.5 md:overflow-y-auto';
|
||||||
@@ -81,7 +105,7 @@ export default function SpectatorContent() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-black text-slate-100 md:h-screen md:overflow-hidden">
|
<div className="min-h-screen bg-black text-slate-100 md:h-screen md:overflow-hidden">
|
||||||
<main className={mainClass}>
|
<main className={mainClass}>
|
||||||
<section className={sidebarClass}>
|
{showSidebar ? <section className={sidebarClass}>
|
||||||
{isPortraitLayout ? (
|
{isPortraitLayout ? (
|
||||||
<div className={`${topBarItemClass} ${portraitItemHeight} flex flex-col gap-0.5`}>
|
<div className={`${topBarItemClass} ${portraitItemHeight} flex flex-col gap-0.5`}>
|
||||||
<GlobalObjectiveBanner layout="desktop" dismissable={false} className="text-sm" />
|
<GlobalObjectiveBanner layout="desktop" dismissable={false} className="text-sm" />
|
||||||
@@ -112,12 +136,20 @@ export default function SpectatorContent() {
|
|||||||
<div className={`${topBarItemClass} ${isPortraitLayout ? portraitItemHeight : ''}`}>
|
<div className={`${topBarItemClass} ${isPortraitLayout ? portraitItemHeight : ''}`}>
|
||||||
<LogsRow className={`${isPortraitLayout ? 'h-full' : 'h-40'} overflow-hidden`} />
|
<LogsRow className={`${isPortraitLayout ? 'h-full' : 'h-40'} overflow-hidden`} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section> : null}
|
||||||
<section className={contentClass}>
|
<section className={contentClass}>
|
||||||
<RoverRow roster={roster} />
|
{/*
|
||||||
<SecondaryRow />
|
Conditional rendering is deliberate: CSS-only hiding would leave
|
||||||
|
WHEP peer connections and snapshot socket subscriptions active.
|
||||||
|
Unmounting delegates cleanup to the media components that own them.
|
||||||
|
*/}
|
||||||
|
{showRovers || showPtz ? (
|
||||||
|
<RoverRow roster={roster} showRovers={showRovers} showPtz={showPtz} />
|
||||||
|
) : null}
|
||||||
|
{showRoomCameras ? <SecondaryRow /> : null}
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
<SpectatorViewControls preferences={viewPreferences} onToggle={updateViewPreference} />
|
||||||
<AlertFeed />
|
<AlertFeed />
|
||||||
<RewardRunOverlay />
|
<RewardRunOverlay />
|
||||||
{/* Spectators do not have the replay request panel that normal web users see, so
|
{/* Spectators do not have the replay request panel that normal web users see, so
|
||||||
|
|||||||
@@ -4,14 +4,21 @@
|
|||||||
import RoverSpectatorCard from './RoverSpectatorCard.jsx';
|
import RoverSpectatorCard from './RoverSpectatorCard.jsx';
|
||||||
import PtzSpectatorCard from './PtzSpectatorCard.jsx';
|
import PtzSpectatorCard from './PtzSpectatorCard.jsx';
|
||||||
|
|
||||||
export default function RoverRow({ roster }) {
|
export default function RoverRow({ roster, showRovers = true, showPtz = true }) {
|
||||||
return (
|
return (
|
||||||
<section className="grid grid-cols-1 gap-0.5 md:grid-cols-2">
|
<section className="grid grid-cols-1 gap-0.5 md:grid-cols-2">
|
||||||
{roster.length === 0 ? <p className="col-span-full text-slate-400">No rovers registered.</p> : null}
|
{showRovers && roster.length === 0 ? <p className="col-span-full text-slate-400">No rovers registered.</p> : null}
|
||||||
{roster.map((rover) => (
|
{showRovers
|
||||||
<RoverSpectatorCard key={rover.id} rover={rover} />
|
? roster.map((rover) => (
|
||||||
))}
|
<RoverSpectatorCard key={rover.id} rover={rover} />
|
||||||
<PtzSpectatorCard />
|
))
|
||||||
|
: null}
|
||||||
|
{/*
|
||||||
|
PTZ intentionally remains the last grid item. With three rovers it
|
||||||
|
therefore occupies the fourth tile, preserving the spectator layout's
|
||||||
|
designed visual balance while still allowing independent unmounting.
|
||||||
|
*/}
|
||||||
|
{showPtz ? <PtzSpectatorCard /> : null}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user