mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
bwah
This commit is contained in:
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
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/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>
|
||||
<script type="module" crossorigin src="/assets/index-CSIsAzyE.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-17XZRVBQ.css">
|
||||
<script type="module" crossorigin src="/assets/index-Bt8dATWQ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CNy3e6VY.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -42,6 +42,12 @@ function resolveRoverName(roverId) {
|
||||
|
||||
function isPrivateClosedRoverId(roverId) {
|
||||
if (!roverId) return false;
|
||||
/*
|
||||
PTZ uses the existing rover badge fields so chat rows can reuse RoverLabel,
|
||||
but it is not a private rover. Let PTZ-badged messages broadcast normally
|
||||
instead of falling into the closed-private rover path for unknown ids.
|
||||
*/
|
||||
if (String(roverId) === ptzCameraService.PTZ_CAMERA_ID) return false;
|
||||
return roverManager.canReplayRoverId(roverId) !== true;
|
||||
}
|
||||
|
||||
|
||||
@@ -664,7 +664,6 @@ function revokeOperator(reason = 'release') {
|
||||
function activateOperator(socket) {
|
||||
revokeOperator('handoff');
|
||||
removeFromQueue(socket.id);
|
||||
releaseRoverOwnershipForPtz(socket);
|
||||
state.operatorSocketId = socket.id;
|
||||
state.deadline = Date.now() + getTurnDurationMs();
|
||||
turnTimer = setTimeout(handleTurnDeadline, getTurnDurationMs());
|
||||
@@ -731,6 +730,15 @@ async function claim(socket) {
|
||||
socket.emit('ptzCamera:dockRequired', payload);
|
||||
throw new Error(leave.message);
|
||||
}
|
||||
/*
|
||||
Joining PTZ is the point where rover ownership must end, even when another
|
||||
user is currently operating the camera and this socket only enters the
|
||||
waiting queue. PTZ queue membership is still a camera session: the user is
|
||||
waiting for a camera turn, not continuing as a rover driver until their turn
|
||||
arrives. Releasing here also keeps chat badges, assignment UI, and command
|
||||
routing from presenting a "rover plus camera queue" hybrid state.
|
||||
*/
|
||||
releaseRoverOwnershipForPtz(socket);
|
||||
if (state.operatorSocketId) {
|
||||
if (!state.queue.includes(socket.id)) state.queue.push(socket.id);
|
||||
emitChange('queue-join');
|
||||
|
||||
@@ -214,6 +214,7 @@ function TtsControls({
|
||||
function ChatComposer({
|
||||
allowSpectatorInput = false,
|
||||
hideSpectatorNotice = false,
|
||||
inputTarget = 'panel',
|
||||
}) {
|
||||
const {
|
||||
sendMessage,
|
||||
@@ -310,7 +311,7 @@ function ChatComposer({
|
||||
setTypingActive(false);
|
||||
}
|
||||
}}
|
||||
ref={(el) => registerInputRef(el, { target: 'panel' })}
|
||||
ref={(el) => registerInputRef(el, { target: inputTarget })}
|
||||
placeholder={canChat ? 'Type a message…' : hideSpectatorNotice ? '' : 'Spectators cannot chat'}
|
||||
disabled={!canChat}
|
||||
/>
|
||||
@@ -346,6 +347,7 @@ export default function ChatPanel({
|
||||
allowSpectatorInput = false,
|
||||
title = 'Chat and speech',
|
||||
minimal = false,
|
||||
inputTarget = 'panel',
|
||||
}) {
|
||||
const effectiveHideInput = minimal || hideInput;
|
||||
const effectiveTitle = minimal ? '' : title;
|
||||
@@ -362,6 +364,7 @@ export default function ChatPanel({
|
||||
<MemoizedChatComposer
|
||||
allowSpectatorInput={allowSpectatorInput}
|
||||
hideSpectatorNotice={hideSpectatorNotice}
|
||||
inputTarget={inputTarget}
|
||||
/>
|
||||
)}
|
||||
</CardFrame>
|
||||
|
||||
@@ -347,13 +347,15 @@ function PtzMediaPane({ ptz, open }) {
|
||||
const turnModel = useMemo(() => buildPtzTurnModel(ptz, selfId), [ptz, selfId]);
|
||||
|
||||
return (
|
||||
<div className="relative h-full min-h-0 w-full overflow-hidden bg-black">
|
||||
{isOperator ? (
|
||||
<PtzLiveVideo enabled={open} startMuted={false} label={ptz?.name || 'PTZ Camera'} />
|
||||
) : (
|
||||
<PtzSnapshotPreview feed={snapshot} label={ptz?.name || 'PTZ Camera'} />
|
||||
)}
|
||||
<TurnsOverlay turnModel={turnModel} />
|
||||
<div className="flex h-full min-h-0 w-full items-center justify-center overflow-hidden bg-black">
|
||||
<div className="relative aspect-video max-h-full w-full max-w-full overflow-hidden bg-black">
|
||||
{isOperator ? (
|
||||
<PtzLiveVideo enabled={open} startMuted={false} label={ptz?.name || 'PTZ Camera'} />
|
||||
) : (
|
||||
<PtzSnapshotPreview feed={snapshot} label={ptz?.name || 'PTZ Camera'} />
|
||||
)}
|
||||
<TurnsOverlay turnModel={turnModel} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -366,6 +368,7 @@ function PtzDesktopFullscreen({ ptz, releasePending }) {
|
||||
<PtzMediaPane ptz={ptz} open />
|
||||
</main>
|
||||
<aside className="flex min-h-0 flex-col gap-0.5 overflow-y-auto bg-neutral-950 text-sm">
|
||||
<PtzQueueSummary ptz={ptz} />
|
||||
{ptz?.isOperator ? (
|
||||
<PtzLightingControls ptz={ptz} />
|
||||
) : (
|
||||
@@ -376,14 +379,13 @@ function PtzDesktopFullscreen({ ptz, releasePending }) {
|
||||
<PtzControlReference />
|
||||
<PtzStatePanel ptz={ptz} />
|
||||
<ReplaySourcesPanel panelId="ptz-controller-replay" />
|
||||
<CardFrame title="Position presets" bodyClassName="p-1 text-xs text-slate-500">
|
||||
Presets will live here.
|
||||
</CardFrame>
|
||||
</aside>
|
||||
</div>
|
||||
<div className="grid min-h-0 grid-cols-[minmax(0,1.6fr)_minmax(16rem,0.7fr)] gap-0.5 overflow-hidden">
|
||||
<ChatPanel fillHeight title="Chat" />
|
||||
<PtzQueueSummary ptz={ptz} />
|
||||
<ChatPanel fillHeight title="Chat" allowSpectatorInput inputTarget="overlay" />
|
||||
<CardFrame title="Position presets" fillHeight bodyClassName="p-1 text-xs text-slate-500">
|
||||
Presets will live here.
|
||||
</CardFrame>
|
||||
</div>
|
||||
{releasePending ? (
|
||||
<div className="pointer-events-none absolute bottom-1 right-1 rounded bg-black/80 px-2 py-1 text-xs text-slate-200">
|
||||
@@ -407,7 +409,7 @@ function PtzMobileFullscreen({ ptz, layout }) {
|
||||
<PtzMobileControlsPanel ptz={ptz} disabled={!ptz?.isOperator} />
|
||||
</section>
|
||||
<section className="grid gap-0.5 md:grid-cols-[minmax(0,1fr)_minmax(0,0.7fr)]">
|
||||
<ChatPanel title="Chat" />
|
||||
<ChatPanel title="Chat" allowSpectatorInput inputTarget="overlay" />
|
||||
<div className="space-y-0.5">
|
||||
<PtzQueueSummary ptz={ptz} />
|
||||
<PtzStatePanel ptz={ptz} compact />
|
||||
|
||||
@@ -61,6 +61,7 @@ export function ChatProvider({ children }) {
|
||||
const [isChatFocused, setIsChatFocused] = useState(false);
|
||||
const panelInputRef = useRef(null);
|
||||
const hudInputRef = useRef(null);
|
||||
const overlayInputRef = useRef(null);
|
||||
const audioRef = useRef(null);
|
||||
const typingRef = useRef(new Map());
|
||||
const typingAlertRef = useRef(new Map());
|
||||
@@ -236,7 +237,17 @@ export function ChatProvider({ children }) {
|
||||
);
|
||||
|
||||
const registerInputRef = useCallback((el, options = {}) => {
|
||||
const target = options?.target === 'hud' ? 'hud' : 'panel';
|
||||
const target = options?.target === 'overlay' ? 'overlay' : options?.target === 'hud' ? 'hud' : 'panel';
|
||||
/*
|
||||
Fullscreen surfaces such as PTZ need chat-key focus while they are mounted
|
||||
without stealing the normal HUD ref permanently. Keep overlay chat as a
|
||||
separate highest-priority slot so unmounting the portal only clears the
|
||||
overlay target and leaves the driver HUD/panel refs intact.
|
||||
*/
|
||||
if (target === 'overlay') {
|
||||
overlayInputRef.current = el;
|
||||
return;
|
||||
}
|
||||
if (target === 'hud') {
|
||||
hudInputRef.current = el;
|
||||
} else {
|
||||
@@ -246,11 +257,12 @@ export function ChatProvider({ children }) {
|
||||
|
||||
const focusChat = useCallback(() => {
|
||||
setIsChatFocused(true);
|
||||
(hudInputRef.current || panelInputRef.current)?.focus();
|
||||
(overlayInputRef.current || hudInputRef.current || panelInputRef.current)?.focus();
|
||||
}, []);
|
||||
|
||||
const blurChat = useCallback(() => {
|
||||
setIsChatFocused(false);
|
||||
overlayInputRef.current?.blur();
|
||||
hudInputRef.current?.blur();
|
||||
panelInputRef.current?.blur();
|
||||
}, []);
|
||||
|
||||
Reference in New Issue
Block a user