mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
gawawa
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/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-CYuyI8Zg.js"></script>
|
<script type="module" crossorigin src="/assets/index-Dp_4HZ7O.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-CoyqmROJ.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-LTXnNyH3.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
+1
-1
@@ -206,7 +206,7 @@ function MobileFeatureTabs({
|
|||||||
</div>
|
</div>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel id="vip" keepMounted>
|
<TabPanel id="vip" keepMounted>
|
||||||
<VipPanel isActive={activeTab === 'vip'} />
|
<VipPanel isActive={activeTab === 'vip'} layout={layout} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel id="roomcontrols">
|
<TabPanel id="roomcontrols">
|
||||||
<div className={themeStackClass}>
|
<div className={themeStackClass}>
|
||||||
|
|||||||
@@ -425,7 +425,7 @@ export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
|
|||||||
|
|
||||||
{/* VIP tab */}
|
{/* VIP tab */}
|
||||||
<TabPanel id="vip" keepMounted>
|
<TabPanel id="vip" keepMounted>
|
||||||
<VipPanel isActive={activeTab === 'vip'} />
|
<VipPanel isActive={activeTab === 'vip'} layout={layout} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
{/* help tab */}
|
{/* help tab */}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import VipPrivateRoverAccessCard from '../vip/VipPrivateRoverAccessCard.jsx';
|
|||||||
import VipProfileImageCard from '../vip/VipProfileImageCard.jsx';
|
import VipProfileImageCard from '../vip/VipProfileImageCard.jsx';
|
||||||
import VipPtzCameraCard from '../vip/VipPtzCameraCard.jsx';
|
import VipPtzCameraCard from '../vip/VipPtzCameraCard.jsx';
|
||||||
|
|
||||||
export default function VipPanel() {
|
export default function VipPanel({ layout = 'desktop' }) {
|
||||||
const session = useSessionSelector((state) => state.session);
|
const session = useSessionSelector((state) => state.session);
|
||||||
const {
|
const {
|
||||||
identifySession,
|
identifySession,
|
||||||
@@ -70,7 +70,7 @@ export default function VipPanel() {
|
|||||||
<div className="lg:col-span-2">
|
<div className="lg:col-span-2">
|
||||||
{isVerified ? (
|
{isVerified ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<VipPtzCameraCard onMessage={setMessage} fullWidth />
|
<VipPtzCameraCard onMessage={setMessage} fullWidth layout={layout} />
|
||||||
<VipMidiBeeperCard />
|
<VipMidiBeeperCard />
|
||||||
<VipAudioUploadCard
|
<VipAudioUploadCard
|
||||||
ownRoverId={ownRoverId}
|
ownRoverId={ownRoverId}
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
// Purpose: Provides the verified-user entry point and fullscreen controller for the single Reolink PTZ camera.
|
// Purpose: Provides the verified-user entry point and fullscreen controller for the single Reolink PTZ camera.
|
||||||
// Scope: Owns PTZ UI state only; server-side PTZ ownership, rover handoff, and command authorization remain authoritative.
|
// Scope: Owns PTZ UI state only; server-side PTZ ownership, rover handoff, and command authorization remain authoritative.
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import CardFrame from '../CardFrame/index.jsx';
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
import GPIOToggleControl from '../GPIOToggleControl/index.jsx';
|
import GPIOToggleControl from '../GPIOToggleControl/index.jsx';
|
||||||
import ReplaySourcesPanel from '../ReplaySourcesPanel/index.jsx';
|
import ReplaySourcesPanel from '../ReplaySourcesPanel/index.jsx';
|
||||||
|
import KeyPill from './VipAudioUploadCard/KeyPill.jsx';
|
||||||
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
|
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
|
||||||
import { useControlSelector } from '../../controls/index.js';
|
import { useControlSelector } from '../../controls/index.js';
|
||||||
import { formatKeyLabel } from '../../controls/keymapUtils.js';
|
import { formatKeyLabel } from '../../controls/keymapUtils.js';
|
||||||
@@ -356,31 +358,34 @@ function keyLabelFor(keymap, actionId) {
|
|||||||
function PtzControlReference() {
|
function PtzControlReference() {
|
||||||
const keymap = useControlSelector((control) => control.state.keymap);
|
const keymap = useControlSelector((control) => control.state.keymap);
|
||||||
const rows = [
|
const rows = [
|
||||||
['Tilt up', keyLabelFor(keymap, 'driveForward')],
|
['Tilt up', 'driveForward'],
|
||||||
['Tilt down', keyLabelFor(keymap, 'driveBackward')],
|
['Tilt down', 'driveBackward'],
|
||||||
['Pan left', keyLabelFor(keymap, 'driveLeft')],
|
['Pan left', 'driveLeft'],
|
||||||
['Pan right', keyLabelFor(keymap, 'driveRight')],
|
['Pan right', 'driveRight'],
|
||||||
['Zoom in', keyLabelFor(keymap, 'cameraUp')],
|
['Zoom in', 'cameraUp'],
|
||||||
['Zoom out', keyLabelFor(keymap, 'cameraDown')],
|
['Zoom out', 'cameraDown'],
|
||||||
['Spotlight', keyLabelFor(keymap, 'headlightToggle')],
|
['Spotlight', 'headlightToggle'],
|
||||||
['Infrared mode', keyLabelFor(keymap, 'laserToggle')],
|
['Infrared mode', 'laserToggle'],
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardFrame title="Controls" bodyClassName="space-y-0.5 p-1 text-xs">
|
<CardFrame title="Controls" bodyClassName="space-y-0.5 p-1 text-xs">
|
||||||
{rows.map(([label, value]) => (
|
{rows.map(([label, actionId]) => (
|
||||||
<div key={label} className="surface flex items-center justify-between gap-1">
|
<div key={label} className="surface flex items-center justify-between gap-1">
|
||||||
<span className="text-slate-400">{label}</span>
|
<span className="text-slate-400">{label}</span>
|
||||||
<span className="truncate text-slate-100">{value}</span>
|
{/* Use the same key display component as the rest of the UI so PTZ
|
||||||
|
controls read as normal mapped controls instead of custom labels. */}
|
||||||
|
<KeyPill label={keyLabelFor(keymap, actionId)} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</CardFrame>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PtzController({ open, onClose }) {
|
function PtzController({ open, onClose, layout = 'desktop' }) {
|
||||||
const ptz = useSessionSelector((state) => state.session?.ptzCamera || null);
|
const ptz = useSessionSelector((state) => state.session?.ptzCamera || null);
|
||||||
const isOperator = Boolean(ptz?.isOperator);
|
const isOperator = Boolean(ptz?.isOperator);
|
||||||
|
const isMobile = layout === 'mobile-portrait' || layout === 'mobile-landscape';
|
||||||
const { ptzRelease } = useSessionActions();
|
const { ptzRelease } = useSessionActions();
|
||||||
const snapshot = usePtzCameraSnapshot({ enabled: open && !isOperator });
|
const snapshot = usePtzCameraSnapshot({ enabled: open && !isOperator });
|
||||||
const [releasePending, setReleasePending] = useState(false);
|
const [releasePending, setReleasePending] = useState(false);
|
||||||
@@ -397,20 +402,8 @@ function PtzController({ open, onClose }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const desktopSidebar = (
|
||||||
<div className="fixed inset-0 z-[110] h-[100dvh] w-[100vw] overflow-hidden bg-black text-slate-100">
|
<>
|
||||||
<CardFrame
|
|
||||||
hideHeader
|
|
||||||
fillHeight
|
|
||||||
clipOverflow={false}
|
|
||||||
className="h-[100dvh] w-[100vw] rounded-none border-0 !bg-black"
|
|
||||||
bodyClassName="grid h-full min-h-0 overflow-hidden grid-cols-[minmax(0,1fr)_12rem] md:grid-cols-[minmax(0,1fr)_20rem]"
|
|
||||||
>
|
|
||||||
<main className="relative min-h-0 min-w-0 bg-black">
|
|
||||||
{isOperator ? <PtzLiveVideo enabled /> : <PtzSnapshotPreview feed={snapshot} label={ptz?.name || 'PTZ Camera'} />}
|
|
||||||
</main>
|
|
||||||
<aside className="flex h-full min-h-0 items-stretch overflow-hidden border-l border-neutral-600 bg-neutral-950 text-sm">
|
|
||||||
<div className="flex min-h-0 w-full flex-col gap-0.5 overflow-y-auto p-0.5">
|
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
<PtzStatePanel
|
<PtzStatePanel
|
||||||
ptz={ptz}
|
ptz={ptz}
|
||||||
@@ -422,6 +415,36 @@ function PtzController({ open, onClose }) {
|
|||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
<PtzQueueList queue={ptz?.queue} operatorLabel={ptz?.operatorLabel} />
|
<PtzQueueList queue={ptz?.queue} operatorLabel={ptz?.operatorLabel} />
|
||||||
</div>
|
</div>
|
||||||
|
{isOperator ? (
|
||||||
|
<div className="shrink-0">
|
||||||
|
<PtzLightingControls ptz={ptz} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="shrink-0">
|
||||||
|
<CardFrame title="Controls" bodyClassName="p-1 text-xs text-slate-400">
|
||||||
|
Live PTZ controls unlock when your camera turn is active.
|
||||||
|
</CardFrame>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="shrink-0">
|
||||||
|
<PtzControlReference />
|
||||||
|
</div>
|
||||||
|
<div className="shrink-0">
|
||||||
|
<ReplaySourcesPanel panelId="ptz-controller-replay" />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const mobileSidebar = (
|
||||||
|
<>
|
||||||
|
<div className="shrink-0">
|
||||||
|
<PtzStatePanel
|
||||||
|
ptz={ptz}
|
||||||
|
onClose={onClose}
|
||||||
|
onRelease={isOperator ? releaseAndClose : null}
|
||||||
|
releaseDisabled={releasePending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{isOperator ? (
|
{isOperator ? (
|
||||||
<>
|
<>
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
@@ -439,19 +462,49 @@ function PtzController({ open, onClose }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
<PtzControlReference />
|
<PtzQueueList queue={ptz?.queue} operatorLabel={ptz?.operatorLabel} />
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
<ReplaySourcesPanel panelId="ptz-controller-replay" />
|
<ReplaySourcesPanel panelId="ptz-controller-replay-mobile" />
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const sidebarWidthClass = isMobile
|
||||||
|
? 'grid-cols-[minmax(0,1fr)_12rem]'
|
||||||
|
: 'grid-cols-[minmax(0,1fr)_20rem]';
|
||||||
|
|
||||||
|
const controller = (
|
||||||
|
<div className="fixed inset-0 z-[110] h-[100dvh] w-[100vw] overflow-hidden bg-black text-slate-100">
|
||||||
|
<CardFrame
|
||||||
|
hideHeader
|
||||||
|
fillHeight
|
||||||
|
clipOverflow={false}
|
||||||
|
className="h-[100dvh] w-[100vw] rounded-none border-0 !bg-black"
|
||||||
|
bodyClassName={`grid h-full min-h-0 overflow-hidden ${sidebarWidthClass}`}
|
||||||
|
>
|
||||||
|
<main className="relative min-h-0 min-w-0 bg-black">
|
||||||
|
{isOperator ? <PtzLiveVideo enabled /> : <PtzSnapshotPreview feed={snapshot} label={ptz?.name || 'PTZ Camera'} />}
|
||||||
|
</main>
|
||||||
|
<aside className="flex h-full min-h-0 items-stretch overflow-hidden border-l border-neutral-600 bg-neutral-950 text-sm">
|
||||||
|
<div className="flex min-h-0 w-full flex-col gap-0.5 overflow-y-auto p-0.5">
|
||||||
|
{isMobile ? mobileSidebar : desktopSidebar}
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</CardFrame>
|
</CardFrame>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
The controller is a true fullscreen surface, so mount it directly under
|
||||||
|
document.body instead of inside the VIP tab/card tree. That keeps tab panel
|
||||||
|
spacing, mobile banners, and parent overflow rules from creating visible
|
||||||
|
gaps around a fixed-position camera interface.
|
||||||
|
*/
|
||||||
|
return createPortal(controller, document.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function VipPtzCameraCard({ onMessage, fullWidth = false }) {
|
export default function VipPtzCameraCard({ onMessage, fullWidth = false, layout = 'desktop' }) {
|
||||||
const featureEnabled = useSessionSelector((state) => isFeatureEnabled(state, 'ptzCamera'));
|
const featureEnabled = useSessionSelector((state) => isFeatureEnabled(state, 'ptzCamera'));
|
||||||
const ptz = useSessionSelector((state) => state.session?.ptzCamera || null);
|
const ptz = useSessionSelector((state) => state.session?.ptzCamera || null);
|
||||||
const isVerified = useSessionSelector((state) => Boolean(state.session?.isVerified));
|
const isVerified = useSessionSelector((state) => Boolean(state.session?.isVerified));
|
||||||
@@ -556,7 +609,7 @@ export default function VipPtzCameraCard({ onMessage, fullWidth = false }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardFrame>
|
</CardFrame>
|
||||||
<PtzController open={controllerOpen} onClose={() => setControllerOpen(false)} />
|
<PtzController open={controllerOpen} onClose={() => setControllerOpen(false)} layout={layout} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user