mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
replay in web UI
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useSession } from '../context/SessionContext.jsx';
|
||||
import { useSettingsNamespace } from '../settings/index.js';
|
||||
import { useRoomCameraSnapshots } from '../hooks/useRoomCameraSnapshots.js';
|
||||
@@ -29,7 +29,7 @@ export default function RoomCameraPanel({
|
||||
hideHeader = false,
|
||||
panelId = null,
|
||||
}) {
|
||||
const { session } = useSession();
|
||||
const { session, triggerReplay } = useSession();
|
||||
const cameras = session?.roomCameras || [];
|
||||
const feedMap = useRoomCameraSnapshots(cameras.map((camera) => ({ id: camera.id })));
|
||||
const { value: orientationSettings, save: saveOrientationSettings } = useSettingsNamespace('roomCameraPanels', {});
|
||||
@@ -53,6 +53,42 @@ export default function RoomCameraPanel({
|
||||
const containerClass =
|
||||
effectiveOrientation === 'vertical' ? 'flex flex-col gap-0.5' : 'grid gap-0.5 md:grid-cols-2';
|
||||
const showLayoutToggle = !hideLayoutToggle && !forcedOrientation && cameras.length > 0;
|
||||
const [replayBusy, setReplayBusy] = useState(false);
|
||||
const [replayError, setReplayError] = useState(null);
|
||||
const replayState = session?.replay || null;
|
||||
const replayRemainingMs = useMemo(() => {
|
||||
if (!replayState?.lastTriggeredAt || !replayState?.cooldownMs) return 0;
|
||||
const remaining = replayState.lastTriggeredAt + replayState.cooldownMs - Date.now();
|
||||
return Math.max(0, remaining);
|
||||
}, [replayState?.lastTriggeredAt, replayState?.cooldownMs, replayState?.remainingMs]);
|
||||
const [remainingMs, setRemainingMs] = useState(replayRemainingMs);
|
||||
const replayDisabled =
|
||||
replayBusy || session?.mode === 'lockdown' || !cameras.length || remainingMs > 0;
|
||||
|
||||
useEffect(() => {
|
||||
setRemainingMs(replayRemainingMs);
|
||||
if (!replayState?.lastTriggeredAt || !replayState?.cooldownMs) {
|
||||
return undefined;
|
||||
}
|
||||
const interval = setInterval(() => {
|
||||
const next = replayState.lastTriggeredAt + replayState.cooldownMs - Date.now();
|
||||
setRemainingMs(Math.max(0, next));
|
||||
}, 250);
|
||||
return () => clearInterval(interval);
|
||||
}, [replayState?.lastTriggeredAt, replayState?.cooldownMs, replayRemainingMs]);
|
||||
|
||||
const handleReplay = async () => {
|
||||
if (replayDisabled) return;
|
||||
setReplayError(null);
|
||||
setReplayBusy(true);
|
||||
try {
|
||||
await triggerReplay();
|
||||
} catch (err) {
|
||||
setReplayError(err.message);
|
||||
} finally {
|
||||
setReplayBusy(false);
|
||||
}
|
||||
};
|
||||
const applyOrientation = (next) => {
|
||||
setOrientation(next);
|
||||
if (panelId) {
|
||||
@@ -72,23 +108,39 @@ export default function RoomCameraPanel({
|
||||
<p>Room cameras</p>
|
||||
<span className="text-xs text-slate-500">{cameras.length}</span>
|
||||
</div>
|
||||
{showLayoutToggle && (
|
||||
<div className="flex items-center gap-0.5 text-xs">
|
||||
<span className="text-slate-500">Layout:</span>
|
||||
<div className="inline-flex overflow-hidden rounded border border-slate-700">
|
||||
{ORIENTATIONS.map((option) => (
|
||||
<button
|
||||
key={option}
|
||||
type="button"
|
||||
className={`px-1 py-0.5 ${effectiveOrientation === option ? 'bg-slate-600 text-white' : 'bg-transparent text-slate-400 hover:text-white'}`}
|
||||
onClick={() => applyOrientation(option)}
|
||||
>
|
||||
{option === 'vertical' ? 'Vertical' : 'Grid'}
|
||||
</button>
|
||||
))}
|
||||
<div className="flex flex-wrap items-center gap-1 text-xs">
|
||||
<button
|
||||
type="button"
|
||||
className={`rounded border px-1.5 py-0.5 ${
|
||||
replayDisabled
|
||||
? 'border-slate-700 text-slate-500'
|
||||
: 'border-slate-500 text-slate-200 hover:border-slate-300 hover:text-white'
|
||||
}`}
|
||||
onClick={handleReplay}
|
||||
disabled={replayDisabled}
|
||||
title={session?.mode === 'lockdown' ? 'Replay disabled in lockdown' : 'Send replay to Discord'}
|
||||
>
|
||||
{remainingMs > 0 ? `Replay (${Math.ceil(remainingMs / 1000)}s)` : replayBusy ? 'Replay…' : 'Replay'}
|
||||
</button>
|
||||
{replayError && <span className="text-amber-400">{replayError}</span>}
|
||||
{showLayoutToggle && (
|
||||
<div className="flex items-center gap-0.5">
|
||||
<span className="text-slate-500">Layout:</span>
|
||||
<div className="inline-flex overflow-hidden rounded border border-slate-700">
|
||||
{ORIENTATIONS.map((option) => (
|
||||
<button
|
||||
key={option}
|
||||
type="button"
|
||||
className={`px-1 py-0.5 ${effectiveOrientation === option ? 'bg-slate-600 text-white' : 'bg-transparent text-slate-400 hover:text-white'}`}
|
||||
onClick={() => applyOrientation(option)}
|
||||
>
|
||||
{option === 'vertical' ? 'Vertical' : 'Grid'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
)}
|
||||
<div className={containerClass}>
|
||||
|
||||
Reference in New Issue
Block a user