4:3 UI switchover

This commit is contained in:
legop3
2026-01-19 03:00:47 -05:00
parent 5e7581c82d
commit 4d699e1c4c
20 changed files with 274 additions and 181 deletions
+3 -1
View File
@@ -1,6 +1,6 @@
#configuration for roverd
name: dummy2
serverUrl: ws://192.168.0.84:8080/rover
serverUrl: ws://127.0.0.1:8080/rover
serial:
device: /dev/ttyS0
baud: 115200
@@ -18,3 +18,5 @@ media:
service: mediamtx.service
healthUrl: http://127.0.0.1:9997/v3/paths/list
healthInterval: 30s
nightVision:
enabled: false
+22
View File
@@ -0,0 +1,22 @@
#configuration for roverd
name: dummy3
serverUrl: ws://127.0.0.1:8080/rover
serial:
device: /dev/ttyS0
baud: 115200
brc:
gpioPin: 25
pulseEvery: 1m
pulseWidth: 1s
battery:
full: 2068
warn: 1700
urgent: 1650
maxWheelSpeed: 350
media:
manage: false
service: mediamtx.service
healthUrl: http://127.0.0.1:9997/v3/paths/list
healthInterval: 30s
nightVision:
enabled: false
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
+2 -2
View File
@@ -11,8 +11,8 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
<title>Multi Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-DXkdiJPx.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BvvbAZLx.css">
<script type="module" crossorigin src="/assets/index-BS8UNUHv.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CxMMWICN.css">
</head>
<body>
<div id="root"></div>
+2 -9
View File
@@ -61,16 +61,9 @@ function useLayoutMode() {
function DesktopLayout({ layout, onOpenHelpOverlay }) {
return (
<div className="flex h-full gap-0.5 overflow-hidden">
<div className="flex min-w-0 flex-[1.8] flex-col gap-0.5 overflow-y-auto pr-0.5">
<div className="flex min-w-0 flex-[1.22] flex-col gap-0.5 overflow-y-auto pr-0.5">
<DriverVideoPanel />
<div className="grid h-52 grid-cols-2 gap-0.5">
<div className="h-full min-h-0">
<UserListPanel fillHeight />
</div>
<div className="h-full min-h-0">
<ChatPanel fillHeight />
</div>
</div>
<TelemetryPanel />
<LogPanel />
</div>
<div className="flex min-w-0 flex-1 flex-col gap-0.5 overflow-y-auto">
+2 -11
View File
@@ -5,7 +5,6 @@ import { useControlSystem } from '../controls/index.js';
import { formatKeyLabel } from '../controls/keymapUtils.js';
import TopDownMap from './TopDownMap.jsx';
import RoverRoster from './RoverRoster.jsx';
import ReplaySourcesPanel from './ReplaySourcesPanel.jsx';
import DriveDockAction, { useDriveDockState } from './DriveDockAction.jsx';
export function RoverRosterPanel({ title = 'Rovers' }) {
@@ -46,7 +45,7 @@ export function RoverRosterPanel({ title = 'Rovers' }) {
);
}
export default function ControlSummary({ showRoster = true }) {
export default function ControlSummary() {
const {
state: { roverId, keymap },
} = useControlSystem();
@@ -57,7 +56,7 @@ export default function ControlSummary({ showRoster = true }) {
return (
<section className="panel-section">
<div className="grid items-stretch gap-1 md:grid-cols-[minmax(0,2fr)_minmax(0,1fr)] md:min-h-[22rem]">
<div className="grid items-stretch gap-1 md:grid-cols-[minmax(0,1.1fr)_minmax(0,1fr)] md:min-h-[18rem]">
<div className="flex h-full w-full items-stretch justify-center">
<div className="aspect-square h-full w-full">
<TopDownMap sensors={sensors} />
@@ -70,14 +69,6 @@ export default function ControlSummary({ showRoster = true }) {
{!hideInlineControls ? <InlineCameraTilt keymap={keymap} /> : null}
</div>
</div>
{showRoster ? (
<div className="grid gap-0.5 md:grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)]">
<div>
<ReplaySourcesPanel />
</div>
<RoverRosterPanel />
</div>
) : null}
</section>
);
}
+2 -2
View File
@@ -1,7 +1,7 @@
import { useSession } from "../context/SessionContext";
import { FaDiscord } from "react-icons/fa";
export default function DiscordInviteButton({text = "Join our Discord!"}) {
export default function DiscordInviteButton({ text = "Join our Discord!", className = "" }) {
const { session } = useSession();
const discordInvite = session?.discord?.invite || null;
@@ -12,7 +12,7 @@ export default function DiscordInviteButton({text = "Join our Discord!"}) {
href={discordInvite}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center w-full px-0.5 py-0.5 text-sm font-medium text-white rainbow-animate-bg transition justify-center"
className={`inline-flex items-center w-full px-0.5 py-0.5 text-sm font-medium text-white rainbow-animate-bg transition justify-center ${className}`}
// animated rainbow backgound
// className="inline-flex items-center px-3 py-2 bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white rounded hover:from-indigo-600 hover:via-purple-600 hover:to-pink-600 transition"
>
+1 -1
View File
@@ -131,7 +131,7 @@ export default function DriverVideoPanel({layoutFormat = 'desktop'}) {
idleSkipSeconds={idleSkipSeconds}
/>
) : (
<div className="panel-muted content-center text-center text-sm text-slate-400 aspect-video">
<div className="panel-muted content-center text-center text-sm text-slate-400 aspect-[4/3]">
<p>You are not assigned to a rover.</p>
{/* colored button to visit the spectator page */}
<p className="mt-2">
@@ -34,7 +34,7 @@ function EntityRow({ entity, connected, onToggle }) {
type="button"
onClick={() => onToggle(entity.id)}
disabled={disableToggle}
className={`flex min-w-[12rem] flex-1 items-center justify-between gap-1 rounded px-1 py-0.5 text-left transition-colors ${toneStyles} disabled:opacity-60 disabled:hover:bg-inherit`}
className={`flex min-w-[10rem] flex-1 items-center justify-between gap-1 rounded px-1 py-0.5 text-left transition-colors ${toneStyles} disabled:opacity-60 disabled:hover:bg-inherit`}
>
<div className="min-w-0">
<div className="flex items-center gap-1 text-sm">
@@ -83,11 +83,11 @@ export default function HomeAssistantControls() {
return (
<section className="panel-section space-y-0.5 text-base">
<header className="flex items-center justify-between gap-0.5 text-sm text-slate-400">
<div className="flex items-center gap-1">
<header className="flex flex-wrap items-start justify-between gap-0.5 text-sm text-slate-400">
<div className="flex flex-wrap items-center gap-1">
<p>Light Controls</p>
<span className="text-xs text-slate-500">{entities.length}</span>
<div className="flex items-center gap-1 text-xs text-slate-300 background-black">
<div className="flex w-full flex-wrap items-center gap-1 text-xs text-slate-300">
<span className="flex items-center gap-0.5">
<span>On</span>
{onKeyLabel ? <KeyPill label={onKeyLabel} /> : null}
+2 -2
View File
@@ -1,7 +1,7 @@
import { useSession } from "../context/SessionContext";
import { FaCoffee } from "react-icons/fa";
export default function KoFiButton({ text = "Support me on Ko-fi!" }) {
export default function KoFiButton({ text = "Support me on Ko-fi!", className = "" }) {
const { session } = useSession();
const kofiLink = session?.kofi?.link || null;
@@ -12,7 +12,7 @@ export default function KoFiButton({ text = "Support me on Ko-fi!" }) {
href={kofiLink}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center w-full px-0.5 py-0.5 text-sm font-medium text-white kofi-animate-bg transition justify-center"
className={`inline-flex items-center w-full px-0.5 py-0.5 text-sm font-medium text-white kofi-animate-bg transition justify-center ${className}`}
>
<FaCoffee className="mr-1" />
{text}
+7 -3
View File
@@ -31,16 +31,20 @@ export default function NicknameForm({ compact = false }) {
}
return (
<form className="flex w-full gap-0.5" onSubmit={handleSave}>
<form className="grid w-full min-w-0 grid-cols-[minmax(0,1fr)_auto] gap-0.5" onSubmit={handleSave}>
<input
className="field-input flex-1"
className="field-input flex-1 min-w-0"
value={nicknameInput}
onChange={(e) => setNicknameInput(e.target.value)}
maxLength={32}
placeholder="Enter a nickname"
disabled={!canSetNickname}
/>
<button type="submit" disabled={!canSetNickname || saving} className="button-dark disabled:opacity-50">
<button
type="submit"
disabled={!canSetNickname || saving}
className="button-dark shrink-0 whitespace-nowrap disabled:opacity-50"
>
{saving ? 'Saving…' : compact ? 'Set' : 'Save'}
</button>
</form>
+6 -3
View File
@@ -16,7 +16,7 @@ function normalizeSources(list = []) {
.filter(Boolean);
}
export default function ReplaySourcesPanel({ panelId = 'replay-sources' }) {
export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHeight = false }) {
const { session, triggerReplay } = useSession();
const sources = normalizeSources(session?.replaySources || []);
const { value: settings, save: saveSettings } = useSettingsNamespace('replaySources', {});
@@ -100,13 +100,16 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources' }) {
}
};
const containerClass = fillHeight ? 'h-full flex flex-col' : '';
const listWrapClass = fillHeight ? 'flex-1 min-h-0 overflow-y-auto' : '';
return (
<section className="panel-section p-0.75 text-sm">
<section className={`panel-section p-0.75 text-sm ${containerClass}`}>
<header className="panel-muted flex items-center justify-between text-xs">
<span>Replay Sources</span>
<span>{sources.length}</span>
</header>
<div className="grid gap-0.5 md:grid-cols-2">
<div className={`grid gap-0.5 md:grid-cols-2 ${listWrapClass}`}>
<GroupList title="Rovers" items={grouped.rovers} selected={selected} onToggle={toggleKey} />
<GroupList title="Room Cams" items={grouped.rooms} selected={selected} onToggle={toggleKey} />
</div>
+50 -4
View File
@@ -1,10 +1,45 @@
import TelemetryPanel from './TelemetryPanel.jsx';
import ControlSummary from './ControlSummary.jsx';
import { RoverRosterPanel } from './ControlSummary.jsx';
import RoomCameraPanel from './RoomCameraPanel.jsx';
import HomeAssistantControls from './HomeAssistantControls.jsx';
import SettingsPanel from './SettingsPanel.jsx';
import HelpPanel from './HelpPanel.jsx';
import ChatPanel from './ChatPanel.jsx';
import UserListPanel, { NicknameLinksPanel } from './UserListPanel.jsx';
import ReplaySourcesPanel from './ReplaySourcesPanel.jsx';
import Tabs, { Tab, TabList, TabPanel, TabPanels } from './Tabs.jsx';
import TopDownMap from './TopDownMap.jsx';
import DriveDockAction, { useDriveDockState } from './DriveDockAction.jsx';
import { useTelemetryFrame } from '../context/TelemetryContext.jsx';
import { useControlSystem } from '../controls/index.js';
function TopDownMapPanel() {
const {
state: { roverId },
} = useControlSystem();
const frame = useTelemetryFrame(roverId);
const sensors = frame?.sensors || {};
return (
<section className="panel-section">
<div className="aspect-square w-full">
<TopDownMap sensors={sensors} />
</div>
</section>
);
}
function DriveDockPanel() {
const {
state: { roverId },
} = useControlSystem();
const driveDockState = useDriveDockState(roverId);
return (
<section className="panel-section flex h-full flex-col">
<DriveDockAction layout="desktop" expand driveDockState={driveDockState} />
</section>
);
}
export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
return (
@@ -18,10 +53,21 @@ export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
<TabPanels>
<TabPanel id="telemetry">
<div className="space-y-0.5">
<ControlSummary />
<div className="grid items-stretch gap-0.5 grid-cols-[minmax(0,1.35fr)_minmax(0,0.95fr)]">
<TopDownMapPanel />
<DriveDockPanel />
</div>
<div className="grid gap-0.5 grid-cols-[minmax(0,0.6fr)_minmax(0,1.1fr)_minmax(0,0.9fr)]">
<NicknameLinksPanel compact />
<RoverRosterPanel />
<ReplaySourcesPanel panelId="replay-sources-desktop" fillHeight />
</div>
<div className="grid items-stretch gap-0.5 grid-cols-[minmax(0,1.3fr)_minmax(0,0.7fr)] h-[14rem]">
<ChatPanel fillHeight />
<UserListPanel compact hideNicknameForm fillHeight />
</div>
<HomeAssistantControls />
<RoomCameraPanel defaultOrientation="horizontal" panelId="rightpane-telemetry" />
<TelemetryPanel />
</div>
</TabPanel>
<TabPanel id="help">
+4 -2
View File
@@ -5,9 +5,11 @@ export default function SessionSnapshot() {
const { session } = useSession();
const payload = useMemo(() => JSON.stringify(session ?? {}, null, 2), [session]);
return (
<div className="panel-section space-y-0.5 text-xs">
<div className="panel-section flex min-h-0 flex-col gap-0.5 text-xs">
<p className="text-sm text-slate-400">Session snapshot</p>
<pre className="surface h-64 overflow-y-auto font-mono text-[0.7rem] text-lime-300">{payload}</pre>
<pre className="surface min-h-0 flex-1 overflow-y-auto font-mono text-[0.7rem] text-lime-300">
{payload}
</pre>
</div>
);
}
+43 -13
View File
@@ -6,6 +6,20 @@ import NicknameForm from './NicknameForm.jsx';
import DiscordInviteButton from './DiscordInviteButton.jsx';
import KoFiButton from './KoFiButton.jsx';
export function NicknameLinksPanel({ compact = false }) {
return (
<section className="panel-section flex h-full min-h-0 flex-col gap-0.5 text-base">
<div className="surface flex w-full items-center">
<NicknameForm compact={compact} />
</div>
<div className="grid flex-1 min-h-0 gap-0.5 grid-rows-2">
<DiscordInviteButton className="h-full" />
<KoFiButton className="h-full" />
</div>
</section>
);
}
function roleColors(role) {
switch (role) {
case 'admin':
@@ -28,7 +42,13 @@ function formatLabel(user, selfId) {
return base;
}
export default function UserListPanel({ hideNicknameForm = false, hideHeader = false, className = '', fillHeight = false }) {
export default function UserListPanel({
hideNicknameForm = false,
hideHeader = false,
className = '',
fillHeight = false,
compact = false,
}) {
const { session, setNickname } = useSession();
const { value } = useSettingsNamespace('profile', { nickname: '' });
const lastSyncedSocketRef = useRef(null);
@@ -93,9 +113,19 @@ export default function UserListPanel({ hideNicknameForm = false, hideHeader = f
return Math.ceil(ms / 1000);
}, []);
const baseListClass = fillHeight ? 'flex-1 min-h-0 overflow-y-auto' : 'h-48 overflow-y-auto';
const turnsListClass = isTurnsMode && fillHeight ? 'max-h-40 overflow-y-auto' : baseListClass;
const usersListClass = isTurnsMode && fillHeight ? 'flex-1 min-h-0 overflow-y-auto' : baseListClass;
const baseListClass = fillHeight
? 'flex-1 min-h-0 overflow-y-auto'
: compact
? 'h-28 overflow-y-auto'
: 'h-48 overflow-y-auto';
const turnsListClass =
isTurnsMode && fillHeight
? 'max-h-40 overflow-y-auto'
: isTurnsMode && compact
? 'max-h-32 overflow-y-auto'
: baseListClass;
const usersListClass =
isTurnsMode && fillHeight ? 'flex-1 min-h-0 overflow-y-auto' : baseListClass;
const renderUserList = () =>
sorted.length === 0 ? (
@@ -107,7 +137,7 @@ export default function UserListPanel({ hideNicknameForm = false, hideHeader = f
return (
<div
key={user.socketId}
className="surface-muted flex items-center gap-1 text-sm"
className={`surface-muted flex items-center gap-1 ${compact ? 'py-0.25 text-[0.8rem]' : 'text-sm'}`}
>
<p className={`font-semibold ${roleColors(user.role)}`}>{formatLabel(user, selfId)}</p>
{user.roverId ? (
@@ -131,13 +161,13 @@ export default function UserListPanel({ hideNicknameForm = false, hideHeader = f
>
{!hideNicknameForm && (
<div className="space-y-0.5">
<div className="flex items-stretch gap-0.5">
<div className="flex w-1/2 min-w-0">
<div className="grid gap-0.5 md:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]">
<div className="min-w-0">
<div className="surface flex w-full items-center">
<NicknameForm />
<NicknameForm compact={compact} />
</div>
</div>
<div className="flex w-1/2 flex-col gap-0.5">
<div className="grid gap-0.5 sm:grid-cols-2 md:grid-cols-1">
<DiscordInviteButton />
<KoFiButton />
</div>
@@ -148,14 +178,14 @@ export default function UserListPanel({ hideNicknameForm = false, hideHeader = f
<div className={`space-y-0.5 ${fillHeight ? 'flex flex-1 min-h-0 flex-col' : ''}`}>
{!hideHeader && (
<div className="flex items-center justify-between text-sm text-slate-400">
<div className={`flex items-center justify-between text-sm text-slate-400 ${compact ? 'text-xs' : ''}`}>
<span>{isTurnsMode ? 'Turn queues' : 'Users'}</span>
<span className="text-xs text-slate-500">
{isTurnsMode ? Object.keys(turnQueues || {}).length : sorted.length}
</span>
</div>
)}
<div className={`surface space-y-0.25 ${isTurnsMode ? turnsListClass : baseListClass}`}>
<div className={`surface space-y-0.25 ${isTurnsMode ? turnsListClass : baseListClass} ${compact ? 'text-[0.8rem]' : ''}`}>
{isTurnsMode ? (
Object.keys(turnQueues || {}).length === 0 ? (
<p className="text-sm text-slate-500">No turn queues yet.</p>
@@ -173,7 +203,7 @@ export default function UserListPanel({ hideNicknameForm = false, hideHeader = f
: queue[0]
: null;
return (
<div key={roverId} className="surface-muted flex flex-col gap-0.25 text-sm">
<div key={roverId} className={`surface-muted flex flex-col gap-0.25 ${compact ? 'text-[0.8rem] py-0.25' : 'text-sm'}`}>
<div className="flex items-center gap-1">
<p className="font-semibold text-slate-200">{rosterName(roverId)}</p>
{remaining != null && (
@@ -201,7 +231,7 @@ export default function UserListPanel({ hideNicknameForm = false, hideHeader = f
return (
<span
key={`${roverId}-${socketId}-${idx}`}
className={`flex items-center gap-0.5 rounded px-1 text-[0.8rem] ${highlightClass}`}
className={`flex items-center gap-0.5 rounded px-1 ${compact ? 'text-[0.7rem]' : 'text-[0.8rem]'} ${highlightClass}`}
>
<span className={`${roleColors(user.role)} font-semibold`}>
{formatLabel(user, selfId)}
+1 -1
View File
@@ -368,7 +368,7 @@ export default function VideoTile({
return (
<div className={`flex flex-col gap-0.5 ${fitParent ? 'h-full' : ''}`}>
<div
className={`relative w-full overflow-hidden bg-black ${fitParent ? 'h-full flex-1' : 'aspect-video'}`}
className={`relative w-full overflow-hidden bg-black ${fitParent ? 'h-full flex-1' : 'aspect-[4/3]'}`}
>
{usingSnapshot ? (
snapshotFeed?.objectUrl ? (
+3 -3
View File
@@ -180,11 +180,11 @@ function FitViewportFrame({ children }) {
<div
className="relative flex items-center justify-center overflow-hidden bg-black"
style={{
width: 'min(100%, calc(100vh * 16 / 9))',
height: 'min(100%, calc(100vw * 9 / 16))',
width: 'min(100%, calc(100vh * 4 / 3))',
height: 'min(100%, calc(100vw * 3 / 4))',
maxWidth: '100%',
maxHeight: '100%',
aspectRatio: '16 / 9',
aspectRatio: '4 / 3',
}}
>
<div className="flex h-full w-full items-center justify-center overflow-hidden">{children}</div>