mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
4:3 UI switchover
This commit is contained in:
Vendored
+3
-1
@@ -1,6 +1,6 @@
|
|||||||
#configuration for roverd
|
#configuration for roverd
|
||||||
name: dummy2
|
name: dummy2
|
||||||
serverUrl: ws://192.168.0.84:8080/rover
|
serverUrl: ws://127.0.0.1:8080/rover
|
||||||
serial:
|
serial:
|
||||||
device: /dev/ttyS0
|
device: /dev/ttyS0
|
||||||
baud: 115200
|
baud: 115200
|
||||||
@@ -18,3 +18,5 @@ media:
|
|||||||
service: mediamtx.service
|
service: mediamtx.service
|
||||||
healthUrl: http://127.0.0.1:9997/v3/paths/list
|
healthUrl: http://127.0.0.1:9997/v3/paths/list
|
||||||
healthInterval: 30s
|
healthInterval: 30s
|
||||||
|
nightVision:
|
||||||
|
enabled: false
|
||||||
|
|||||||
Vendored
+22
@@ -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
@@ -11,8 +11,8 @@
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
||||||
<title>Multi Roomba Rover</title>
|
<title>Multi Roomba Rover</title>
|
||||||
<script type="module" crossorigin src="/assets/index-DXkdiJPx.js"></script>
|
<script type="module" crossorigin src="/assets/index-BS8UNUHv.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BvvbAZLx.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-CxMMWICN.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
+2
-9
@@ -61,16 +61,9 @@ function useLayoutMode() {
|
|||||||
function DesktopLayout({ layout, onOpenHelpOverlay }) {
|
function DesktopLayout({ layout, onOpenHelpOverlay }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full gap-0.5 overflow-hidden">
|
<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 />
|
<DriverVideoPanel />
|
||||||
<div className="grid h-52 grid-cols-2 gap-0.5">
|
<TelemetryPanel />
|
||||||
<div className="h-full min-h-0">
|
|
||||||
<UserListPanel fillHeight />
|
|
||||||
</div>
|
|
||||||
<div className="h-full min-h-0">
|
|
||||||
<ChatPanel fillHeight />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<LogPanel />
|
<LogPanel />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5 overflow-y-auto">
|
<div className="flex min-w-0 flex-1 flex-col gap-0.5 overflow-y-auto">
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useControlSystem } from '../controls/index.js';
|
|||||||
import { formatKeyLabel } from '../controls/keymapUtils.js';
|
import { formatKeyLabel } from '../controls/keymapUtils.js';
|
||||||
import TopDownMap from './TopDownMap.jsx';
|
import TopDownMap from './TopDownMap.jsx';
|
||||||
import RoverRoster from './RoverRoster.jsx';
|
import RoverRoster from './RoverRoster.jsx';
|
||||||
import ReplaySourcesPanel from './ReplaySourcesPanel.jsx';
|
|
||||||
import DriveDockAction, { useDriveDockState } from './DriveDockAction.jsx';
|
import DriveDockAction, { useDriveDockState } from './DriveDockAction.jsx';
|
||||||
|
|
||||||
export function RoverRosterPanel({ title = 'Rovers' }) {
|
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 {
|
const {
|
||||||
state: { roverId, keymap },
|
state: { roverId, keymap },
|
||||||
} = useControlSystem();
|
} = useControlSystem();
|
||||||
@@ -57,7 +56,7 @@ export default function ControlSummary({ showRoster = true }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel-section">
|
<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="flex h-full w-full items-stretch justify-center">
|
||||||
<div className="aspect-square h-full w-full">
|
<div className="aspect-square h-full w-full">
|
||||||
<TopDownMap sensors={sensors} />
|
<TopDownMap sensors={sensors} />
|
||||||
@@ -70,14 +69,6 @@ export default function ControlSummary({ showRoster = true }) {
|
|||||||
{!hideInlineControls ? <InlineCameraTilt keymap={keymap} /> : null}
|
{!hideInlineControls ? <InlineCameraTilt keymap={keymap} /> : null}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useSession } from "../context/SessionContext";
|
import { useSession } from "../context/SessionContext";
|
||||||
import { FaDiscord } from "react-icons/fa";
|
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 { session } = useSession();
|
||||||
const discordInvite = session?.discord?.invite || null;
|
const discordInvite = session?.discord?.invite || null;
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ export default function DiscordInviteButton({text = "Join our Discord!"}) {
|
|||||||
href={discordInvite}
|
href={discordInvite}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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
|
// 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"
|
// 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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export default function DriverVideoPanel({layoutFormat = 'desktop'}) {
|
|||||||
idleSkipSeconds={idleSkipSeconds}
|
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>
|
<p>You are not assigned to a rover.</p>
|
||||||
{/* colored button to visit the spectator page */}
|
{/* colored button to visit the spectator page */}
|
||||||
<p className="mt-2">
|
<p className="mt-2">
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ function EntityRow({ entity, connected, onToggle }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => onToggle(entity.id)}
|
onClick={() => onToggle(entity.id)}
|
||||||
disabled={disableToggle}
|
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="min-w-0">
|
||||||
<div className="flex items-center gap-1 text-sm">
|
<div className="flex items-center gap-1 text-sm">
|
||||||
@@ -83,11 +83,11 @@ export default function HomeAssistantControls() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel-section space-y-0.5 text-base">
|
<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">
|
<header className="flex flex-wrap items-start justify-between gap-0.5 text-sm text-slate-400">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex flex-wrap items-center gap-1">
|
||||||
<p>Light Controls</p>
|
<p>Light Controls</p>
|
||||||
<span className="text-xs text-slate-500">{entities.length}</span>
|
<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 className="flex items-center gap-0.5">
|
||||||
<span>On</span>
|
<span>On</span>
|
||||||
{onKeyLabel ? <KeyPill label={onKeyLabel} /> : null}
|
{onKeyLabel ? <KeyPill label={onKeyLabel} /> : null}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useSession } from "../context/SessionContext";
|
import { useSession } from "../context/SessionContext";
|
||||||
import { FaCoffee } from "react-icons/fa";
|
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 { session } = useSession();
|
||||||
const kofiLink = session?.kofi?.link || null;
|
const kofiLink = session?.kofi?.link || null;
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ export default function KoFiButton({ text = "Support me on Ko-fi!" }) {
|
|||||||
href={kofiLink}
|
href={kofiLink}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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" />
|
<FaCoffee className="mr-1" />
|
||||||
{text}
|
{text}
|
||||||
|
|||||||
@@ -31,16 +31,20 @@ export default function NicknameForm({ compact = false }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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
|
<input
|
||||||
className="field-input flex-1"
|
className="field-input flex-1 min-w-0"
|
||||||
value={nicknameInput}
|
value={nicknameInput}
|
||||||
onChange={(e) => setNicknameInput(e.target.value)}
|
onChange={(e) => setNicknameInput(e.target.value)}
|
||||||
maxLength={32}
|
maxLength={32}
|
||||||
placeholder="Enter a nickname"
|
placeholder="Enter a nickname"
|
||||||
disabled={!canSetNickname}
|
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'}
|
{saving ? 'Saving…' : compact ? 'Set' : 'Save'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ function normalizeSources(list = []) {
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ReplaySourcesPanel({ panelId = 'replay-sources' }) {
|
export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHeight = false }) {
|
||||||
const { session, triggerReplay } = useSession();
|
const { session, triggerReplay } = useSession();
|
||||||
const sources = normalizeSources(session?.replaySources || []);
|
const sources = normalizeSources(session?.replaySources || []);
|
||||||
const { value: settings, save: saveSettings } = useSettingsNamespace('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 (
|
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">
|
<header className="panel-muted flex items-center justify-between text-xs">
|
||||||
<span>Replay Sources</span>
|
<span>Replay Sources</span>
|
||||||
<span>{sources.length}</span>
|
<span>{sources.length}</span>
|
||||||
</header>
|
</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="Rovers" items={grouped.rovers} selected={selected} onToggle={toggleKey} />
|
||||||
<GroupList title="Room Cams" items={grouped.rooms} selected={selected} onToggle={toggleKey} />
|
<GroupList title="Room Cams" items={grouped.rooms} selected={selected} onToggle={toggleKey} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,45 @@
|
|||||||
import TelemetryPanel from './TelemetryPanel.jsx';
|
import { RoverRosterPanel } from './ControlSummary.jsx';
|
||||||
import ControlSummary from './ControlSummary.jsx';
|
|
||||||
import RoomCameraPanel from './RoomCameraPanel.jsx';
|
import RoomCameraPanel from './RoomCameraPanel.jsx';
|
||||||
import HomeAssistantControls from './HomeAssistantControls.jsx';
|
import HomeAssistantControls from './HomeAssistantControls.jsx';
|
||||||
import SettingsPanel from './SettingsPanel.jsx';
|
import SettingsPanel from './SettingsPanel.jsx';
|
||||||
import HelpPanel from './HelpPanel.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 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 }) {
|
export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
|
||||||
return (
|
return (
|
||||||
@@ -18,10 +53,21 @@ export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
|
|||||||
<TabPanels>
|
<TabPanels>
|
||||||
<TabPanel id="telemetry">
|
<TabPanel id="telemetry">
|
||||||
<div className="space-y-0.5">
|
<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 />
|
<HomeAssistantControls />
|
||||||
<RoomCameraPanel defaultOrientation="horizontal" panelId="rightpane-telemetry" />
|
<RoomCameraPanel defaultOrientation="horizontal" panelId="rightpane-telemetry" />
|
||||||
<TelemetryPanel />
|
|
||||||
</div>
|
</div>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel id="help">
|
<TabPanel id="help">
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ export default function SessionSnapshot() {
|
|||||||
const { session } = useSession();
|
const { session } = useSession();
|
||||||
const payload = useMemo(() => JSON.stringify(session ?? {}, null, 2), [session]);
|
const payload = useMemo(() => JSON.stringify(session ?? {}, null, 2), [session]);
|
||||||
return (
|
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>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,20 @@ import NicknameForm from './NicknameForm.jsx';
|
|||||||
import DiscordInviteButton from './DiscordInviteButton.jsx';
|
import DiscordInviteButton from './DiscordInviteButton.jsx';
|
||||||
import KoFiButton from './KoFiButton.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) {
|
function roleColors(role) {
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case 'admin':
|
case 'admin':
|
||||||
@@ -28,7 +42,13 @@ function formatLabel(user, selfId) {
|
|||||||
return base;
|
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 { session, setNickname } = useSession();
|
||||||
const { value } = useSettingsNamespace('profile', { nickname: '' });
|
const { value } = useSettingsNamespace('profile', { nickname: '' });
|
||||||
const lastSyncedSocketRef = useRef(null);
|
const lastSyncedSocketRef = useRef(null);
|
||||||
@@ -93,9 +113,19 @@ export default function UserListPanel({ hideNicknameForm = false, hideHeader = f
|
|||||||
return Math.ceil(ms / 1000);
|
return Math.ceil(ms / 1000);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const baseListClass = fillHeight ? 'flex-1 min-h-0 overflow-y-auto' : 'h-48 overflow-y-auto';
|
const baseListClass = fillHeight
|
||||||
const turnsListClass = isTurnsMode && fillHeight ? 'max-h-40 overflow-y-auto' : baseListClass;
|
? 'flex-1 min-h-0 overflow-y-auto'
|
||||||
const usersListClass = isTurnsMode && fillHeight ? 'flex-1 min-h-0 overflow-y-auto' : baseListClass;
|
: 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 = () =>
|
const renderUserList = () =>
|
||||||
sorted.length === 0 ? (
|
sorted.length === 0 ? (
|
||||||
@@ -107,7 +137,7 @@ export default function UserListPanel({ hideNicknameForm = false, hideHeader = f
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={user.socketId}
|
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>
|
<p className={`font-semibold ${roleColors(user.role)}`}>{formatLabel(user, selfId)}</p>
|
||||||
{user.roverId ? (
|
{user.roverId ? (
|
||||||
@@ -131,13 +161,13 @@ export default function UserListPanel({ hideNicknameForm = false, hideHeader = f
|
|||||||
>
|
>
|
||||||
{!hideNicknameForm && (
|
{!hideNicknameForm && (
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<div className="flex items-stretch gap-0.5">
|
<div className="grid gap-0.5 md:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]">
|
||||||
<div className="flex w-1/2 min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="surface flex w-full items-center">
|
<div className="surface flex w-full items-center">
|
||||||
<NicknameForm />
|
<NicknameForm compact={compact} />
|
||||||
</div>
|
</div>
|
||||||
</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 />
|
<DiscordInviteButton />
|
||||||
<KoFiButton />
|
<KoFiButton />
|
||||||
</div>
|
</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' : ''}`}>
|
<div className={`space-y-0.5 ${fillHeight ? 'flex flex-1 min-h-0 flex-col' : ''}`}>
|
||||||
{!hideHeader && (
|
{!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>{isTurnsMode ? 'Turn queues' : 'Users'}</span>
|
||||||
<span className="text-xs text-slate-500">
|
<span className="text-xs text-slate-500">
|
||||||
{isTurnsMode ? Object.keys(turnQueues || {}).length : sorted.length}
|
{isTurnsMode ? Object.keys(turnQueues || {}).length : sorted.length}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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 ? (
|
{isTurnsMode ? (
|
||||||
Object.keys(turnQueues || {}).length === 0 ? (
|
Object.keys(turnQueues || {}).length === 0 ? (
|
||||||
<p className="text-sm text-slate-500">No turn queues yet.</p>
|
<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]
|
: queue[0]
|
||||||
: null;
|
: null;
|
||||||
return (
|
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">
|
<div className="flex items-center gap-1">
|
||||||
<p className="font-semibold text-slate-200">{rosterName(roverId)}</p>
|
<p className="font-semibold text-slate-200">{rosterName(roverId)}</p>
|
||||||
{remaining != null && (
|
{remaining != null && (
|
||||||
@@ -201,7 +231,7 @@ export default function UserListPanel({ hideNicknameForm = false, hideHeader = f
|
|||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
key={`${roverId}-${socketId}-${idx}`}
|
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`}>
|
<span className={`${roleColors(user.role)} font-semibold`}>
|
||||||
{formatLabel(user, selfId)}
|
{formatLabel(user, selfId)}
|
||||||
|
|||||||
@@ -368,7 +368,7 @@ export default function VideoTile({
|
|||||||
return (
|
return (
|
||||||
<div className={`flex flex-col gap-0.5 ${fitParent ? 'h-full' : ''}`}>
|
<div className={`flex flex-col gap-0.5 ${fitParent ? 'h-full' : ''}`}>
|
||||||
<div
|
<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 ? (
|
{usingSnapshot ? (
|
||||||
snapshotFeed?.objectUrl ? (
|
snapshotFeed?.objectUrl ? (
|
||||||
|
|||||||
@@ -180,11 +180,11 @@ function FitViewportFrame({ children }) {
|
|||||||
<div
|
<div
|
||||||
className="relative flex items-center justify-center overflow-hidden bg-black"
|
className="relative flex items-center justify-center overflow-hidden bg-black"
|
||||||
style={{
|
style={{
|
||||||
width: 'min(100%, calc(100vh * 16 / 9))',
|
width: 'min(100%, calc(100vh * 4 / 3))',
|
||||||
height: 'min(100%, calc(100vw * 9 / 16))',
|
height: 'min(100%, calc(100vw * 3 / 4))',
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
maxHeight: '100%',
|
maxHeight: '100%',
|
||||||
aspectRatio: '16 / 9',
|
aspectRatio: '4 / 3',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex h-full w-full items-center justify-center overflow-hidden">{children}</div>
|
<div className="flex h-full w-full items-center justify-center overflow-hidden">{children}</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user