mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
new ptz ui
This commit is contained in:
@@ -5,17 +5,13 @@ import { useMemo, useState } from 'react';
|
||||
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
|
||||
import { useSharedClock } from '../../hooks/useSharedClock.js';
|
||||
import CardFrame from '../CardFrame/index.jsx';
|
||||
import RoverLabel from '../RoverLabel/index.jsx';
|
||||
import QueueTargetRow from '../QueueTargetRow/index.jsx';
|
||||
import { trackAnalyticsEvent } from '../../analytics/index.js';
|
||||
import { openExternalRover } from '../../lib/interInstanceTransfer.js';
|
||||
import { ExternalInstancesCompact } from '../InterInstancePanel/index.jsx';
|
||||
import { isFeatureEnabled } from '../../lib/features.js';
|
||||
import { useSettingsNamespace } from '../../settings/index.js';
|
||||
|
||||
function classNames(...values) {
|
||||
return values.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
function formatBattery(rover) {
|
||||
const percent = rover?.batteryState?.percentDisplay;
|
||||
if (percent == null) return '--';
|
||||
@@ -29,27 +25,6 @@ function batteryClass(rover) {
|
||||
return 'text-emerald-300';
|
||||
}
|
||||
|
||||
function roleColors(role) {
|
||||
switch (role) {
|
||||
case 'admin':
|
||||
case 'lockdown':
|
||||
return 'text-amber-300';
|
||||
case 'spectator':
|
||||
return 'text-slate-400';
|
||||
default:
|
||||
return 'text-sky-300';
|
||||
}
|
||||
}
|
||||
|
||||
function formatLabel(user, selfId) {
|
||||
if (!user) return '';
|
||||
const base = user.nickname || user.socketId?.slice(0, 6) || 'unknown';
|
||||
if (user.socketId && user.socketId === selfId) {
|
||||
return `${base} (you)`;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
export default function RoverQueuesPanel({
|
||||
title = 'Rovers',
|
||||
roster: rosterOverride = null,
|
||||
@@ -192,129 +167,59 @@ export default function RoverQueuesPanel({
|
||||
) : (
|
||||
<ul className="space-y-0.5 text-sm">
|
||||
{rosterItems.map((rover) => {
|
||||
const roverId = String(rover.id);
|
||||
const info = turnQueues?.[roverId] || null;
|
||||
const queue = info?.queue || [];
|
||||
const deadline = info?.idleDeadline || info?.deadline || null;
|
||||
const remainingSeconds =
|
||||
deadline && deadline > now ? Math.ceil((deadline - now) / 1000) : deadline ? 0 : null;
|
||||
const currentId = info?.current || null;
|
||||
const currentIdx = currentId ? queue.findIndex((id) => id === currentId) : -1;
|
||||
const nextId =
|
||||
queue.length > 1
|
||||
? currentIdx >= 0
|
||||
? queue[(currentIdx + 1) % queue.length]
|
||||
: queue[0]
|
||||
: null;
|
||||
const isSelfCurrent = Boolean(selfId && currentId && currentId === selfId);
|
||||
const isSelfNext = Boolean(selfId && nextId && nextId === selfId);
|
||||
const showTimer = remainingSeconds != null && (isSelfCurrent || isSelfNext);
|
||||
const isPrivateOpen = Boolean(rover?.private?.enabled && rover?.private?.open);
|
||||
const isGrantedClosedPrivate = Boolean(rover?.private?.enabled && !rover?.private?.open);
|
||||
const locked = Boolean(rover.locked);
|
||||
const lockedBlocked = locked && (externalMode || (!adminCapable && !isGrantedClosedPrivate));
|
||||
const lockLabel = rover.lockReason ? `locked: ${rover.lockReason}` : 'locked';
|
||||
const buttonLabel = pending[roverId]
|
||||
? '...'
|
||||
: lockedBlocked
|
||||
? lockLabel
|
||||
: externalMode
|
||||
? 'Open'
|
||||
: 'request';
|
||||
const canClickRow = canRequest && !lockedBlocked && !pending[roverId];
|
||||
return (
|
||||
<li
|
||||
key={rover.id}
|
||||
className={classNames(
|
||||
'surface flex flex-wrap items-start justify-between gap-0.5',
|
||||
canClickRow && 'cursor-pointer',
|
||||
locked
|
||||
? 'bg-red-900/40'
|
||||
: isPrivateOpen
|
||||
? 'bg-amber-700/35 border border-amber-200/30'
|
||||
: null,
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!canClickRow) return;
|
||||
handleRequest(rover.id);
|
||||
}}
|
||||
>
|
||||
{externalMode && rover?.snapshots?.latestUrl ? (
|
||||
<img
|
||||
src={rover.snapshots.latestUrl}
|
||||
alt=""
|
||||
className="h-8 w-10 shrink-0 rounded border border-slate-700 bg-black object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
) : null}
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||
<div className="flex items-center justify-between gap-0.5">
|
||||
<div className="flex min-w-0 items-center gap-0.5">
|
||||
<p className="min-w-0 flex items-center gap-0.5 whitespace-nowrap text-slate-200">
|
||||
<RoverLabel rover={rover} fallback={roverId} />
|
||||
{rover.description ? (
|
||||
<span className="min-w-0 flex-1 truncate text-[0.7rem] text-slate-400">
|
||||
{rover.description}
|
||||
</span>
|
||||
) : null}
|
||||
</p>
|
||||
{showTimer ? (
|
||||
<span className="rounded bg-slate-800 px-1 text-[0.7rem] text-slate-200">
|
||||
{isSelfCurrent ? `${remainingSeconds}s left` : `Your turn in ${remainingSeconds}s`}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<span className={classNames('text-[0.75rem] font-semibold', batteryClass(rover))}>
|
||||
{formatBattery(rover)}
|
||||
</span>
|
||||
</div>
|
||||
{queue.length === 0 ? (
|
||||
<p className="text-[0.7rem] text-slate-500">No queue.</p>
|
||||
) : (
|
||||
<div className="flex flex-wrap items-center gap-0.5">
|
||||
{queue.map((socketId, idx) => {
|
||||
const user = lookupUser(socketId);
|
||||
const isCurrent = socketId === currentId;
|
||||
const isNext = Boolean(nextId && socketId === nextId && !isCurrent);
|
||||
const highlightClass = isCurrent
|
||||
? 'bg-sky-600 text-white ring-2 ring-amber-300'
|
||||
: isNext
|
||||
? 'bg-emerald-700/60 text-emerald-100 ring-1 ring-emerald-300/70'
|
||||
: 'bg-slate-800 text-slate-200';
|
||||
return (
|
||||
<span
|
||||
key={`${roverId}-${socketId}-${idx}`}
|
||||
className={`flex items-center gap-0.5 rounded px-1 text-[0.7rem] ${highlightClass}`}
|
||||
>
|
||||
<span className={`${roleColors(user.role)} font-semibold`}>
|
||||
{formatLabel(user, selfId)}
|
||||
</span>
|
||||
{isCurrent && <span className="text-[0.65rem] text-slate-200">now</span>}
|
||||
{isNext && <span className="text-[0.65rem] text-emerald-100">next</span>}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{canRequest ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleRequest(rover.id);
|
||||
}}
|
||||
disabled={pending[roverId] || lockedBlocked}
|
||||
className={classNames(
|
||||
'button-dark disabled:opacity-40',
|
||||
locked && 'bg-red-600/70 text-white hover:bg-red-600',
|
||||
)}
|
||||
>
|
||||
{buttonLabel}
|
||||
</button>
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
const roverId = String(rover.id);
|
||||
const info = turnQueues?.[roverId] || null;
|
||||
const queue = info?.queue || [];
|
||||
const deadline = info?.idleDeadline || info?.deadline || null;
|
||||
const remainingSeconds =
|
||||
deadline && deadline > now ? Math.ceil((deadline - now) / 1000) : deadline ? 0 : null;
|
||||
const currentId = info?.current || null;
|
||||
const currentIdx = currentId ? queue.findIndex((id) => id === currentId) : -1;
|
||||
const nextId =
|
||||
queue.length > 1
|
||||
? currentIdx >= 0
|
||||
? queue[(currentIdx + 1) % queue.length]
|
||||
: queue[0]
|
||||
: null;
|
||||
const isSelfCurrent = Boolean(selfId && currentId && currentId === selfId);
|
||||
const isSelfNext = Boolean(selfId && nextId && nextId === selfId);
|
||||
const showTimer = remainingSeconds != null && (isSelfCurrent || isSelfNext);
|
||||
const isPrivateOpen = Boolean(rover?.private?.enabled && rover?.private?.open);
|
||||
const isGrantedClosedPrivate = Boolean(rover?.private?.enabled && !rover?.private?.open);
|
||||
const locked = Boolean(rover.locked);
|
||||
const lockedBlocked = locked && (externalMode || (!adminCapable && !isGrantedClosedPrivate));
|
||||
const lockLabel = rover.lockReason ? `locked: ${rover.lockReason}` : 'locked';
|
||||
const buttonLabel = pending[roverId]
|
||||
? '...'
|
||||
: lockedBlocked
|
||||
? lockLabel
|
||||
: externalMode
|
||||
? 'Open'
|
||||
: 'request';
|
||||
const canClickRow = canRequest && !lockedBlocked && !pending[roverId];
|
||||
return (
|
||||
<QueueTargetRow
|
||||
key={rover.id}
|
||||
target={{ ...rover, rover, roverId, id: roverId }}
|
||||
queue={queue}
|
||||
currentId={currentId}
|
||||
nextId={nextId}
|
||||
selfId={selfId}
|
||||
lookupUser={lookupUser}
|
||||
canClick={canClickRow}
|
||||
pending={Boolean(pending[roverId])}
|
||||
locked={locked}
|
||||
lockedBlocked={lockedBlocked}
|
||||
privateOpen={isPrivateOpen}
|
||||
buttonLabel={buttonLabel}
|
||||
batteryLabel={formatBattery(rover)}
|
||||
batteryClassName={batteryClass(rover)}
|
||||
timerLabel={showTimer ? (isSelfCurrent ? `${remainingSeconds}s left` : `Your turn in ${remainingSeconds}s`) : ''}
|
||||
thumbnailUrl={externalMode ? rover?.snapshots?.latestUrl : ''}
|
||||
onRequest={handleRequest}
|
||||
showAction={Boolean(canRequest)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user