This commit is contained in:
legop3
2026-04-18 22:55:16 -04:00
parent cd70258a21
commit f4182073ee
8 changed files with 103 additions and 36 deletions
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-C1njEZEY.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-7nrE9xLC.css">
<script type="module" crossorigin src="/assets/index-CEe1q-KX.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C3BucANX.css">
</head>
<body>
<div id="root"></div>
+57 -8
View File
@@ -1,9 +1,12 @@
import { useEffect, useMemo, useState } from 'react';
import { useSession } from '../context/SessionContext.jsx';
import { useSocket } from '../context/SocketContext.jsx';
import ChatMessageRow from './ChatMessageRow.jsx';
import ChatTypingRow from './ChatTypingRow.jsx';
import ButtonBoxTile from './ButtonBoxTile.jsx';
const LIFETIME_MS = 3000;
const BUTTONBOX_LIFETIME_MS = 3500;
const DEFAULT_COLOR = '#2196f3';
function buildKey(alert) {
@@ -13,9 +16,37 @@ function buildKey(alert) {
}
export default function AlertFeed({ scale = 1 }) {
const { alerts } = useSession();
const { alerts, session, pushAlert } = useSession();
const socket = useSocket();
const [now, setNow] = useState(() => Date.now());
const latest = useMemo(() => alerts.slice(-3).map((alert) => ({ alert, key: buildKey(alert) })), [alerts]);
useEffect(() => {
function onButtonIncrement(payload = {}) {
const buttonId = Number(payload.buttonId);
if (!Number.isFinite(buttonId) || buttonId < 1 || buttonId > 4) return;
const buttons = Array.isArray(session?.buttonBox?.buttons) ? session.buttonBox.buttons : [];
const button = buttons.find((entry) => Number(entry?.id) === buttonId) || {};
const count = Number.isFinite(payload.count) ? payload.count : Number(button.count) || 0;
const goal = Number.isFinite(button.goal) ? button.goal : 0;
const rewardNumber = Number.isFinite(button.rewardNumber) ? button.rewardNumber : '?';
const rewardName =
typeof button.rewardName === 'string' && button.rewardName.trim()
? button.rewardName.trim()
: 'Unassigned';
pushAlert({
id: `buttonbox-active-${buttonId}`,
kind: 'buttonbox-active',
lifetimeMs: BUTTONBOX_LIFETIME_MS,
payload: { buttonId, count, goal, rewardNumber, rewardName },
});
}
socket.on('buttonBox:increment', onButtonIncrement);
return () => {
socket.off('buttonBox:increment', onButtonIncrement);
};
}, [pushAlert, session?.buttonBox?.buttons, socket]);
const latest = useMemo(() => alerts.slice(-12).map((alert) => ({ alert, key: buildKey(alert) })), [alerts]);
useEffect(() => {
if (!latest.length) return undefined;
@@ -23,12 +54,15 @@ export default function AlertFeed({ scale = 1 }) {
return () => clearInterval(interval);
}, [latest.length]);
const visible = latest
.map((item) => ({
...item,
age: now - (item.alert.receivedAt ?? item.alert.timestamp ?? 0),
}))
.filter((item) => item.age <= LIFETIME_MS);
const visibleByKey = new Map();
latest.forEach((item) => {
const age = now - (item.alert.receivedAt ?? item.alert.timestamp ?? 0);
const lifetimeMs = Number.isFinite(item.alert.lifetimeMs) ? item.alert.lifetimeMs : LIFETIME_MS;
if (age <= lifetimeMs) {
visibleByKey.set(item.key, { ...item, age });
}
});
const visible = Array.from(visibleByKey.values()).slice(-3);
if (!visible.length) return null;
@@ -66,6 +100,21 @@ function hexToRgb(hex) {
}
function AlertToast({ alert }) {
if (alert.kind === 'buttonbox-active' && alert.payload) {
const payload = alert.payload;
return (
<div className="pointer-events-none w-[12.5rem]">
<ButtonBoxTile
buttonId={payload.buttonId}
count={payload.count}
goal={payload.goal}
rewardNumber={payload.rewardNumber}
rewardName={payload.rewardName}
className="bg-cyan-900/45"
/>
</div>
);
}
if (alert.kind === 'chat' && alert.payload) {
return <ChatMessageRow message={alert.payload} />;
}
+9 -13
View File
@@ -3,6 +3,7 @@ import { useSession } from '../context/SessionContext.jsx';
import { useSocket } from '../context/SocketContext.jsx';
import { useSettingsNamespace } from '../settings/index.js';
import { AUDIO_SETTINGS_DEFAULTS } from '../settings/namespaces.js';
import ButtonBoxTile from './ButtonBoxTile.jsx';
const FLASH_MS = 420;
const REWARD_FLASH_MS = 1200;
@@ -139,20 +140,15 @@ export default function ButtonBoxPanel() {
const rewardActive = Boolean(rewardFlash[id]);
return (
<article
<ButtonBoxTile
key={id}
className={[
'surface px-0.5 py-0.5 text-center',
incActive ? 'bg-cyan-900/45' : '',
rewardActive ? 'bg-fuchsia-900/45' : '',
]
.filter(Boolean)
.join(' ')}
>
<p className="text-xs text-slate-400">Button {id}</p>
<p className="text-sm font-semibold text-white">{count} / {goal}</p>
<p className="truncate text-[0.7rem] text-slate-300">#{rewardNumber} {rewardName}</p>
</article>
buttonId={id}
count={count}
goal={goal}
rewardNumber={rewardNumber}
rewardName={rewardName}
className={[incActive ? 'bg-cyan-900/45' : '', rewardActive ? 'bg-fuchsia-900/45' : ''].filter(Boolean).join(' ')}
/>
);
})}
</div>
+16
View File
@@ -0,0 +1,16 @@
export default function ButtonBoxTile({
buttonId,
count,
goal,
rewardNumber,
rewardName,
className = '',
}) {
return (
<article className={['surface px-0.5 py-0.5 text-center', className].filter(Boolean).join(' ')}>
<p className="text-xs text-slate-400">Button {buttonId}</p>
<p className="text-sm font-semibold text-white">{count} / {goal}</p>
<p className="truncate text-[0.7rem] text-slate-300">#{rewardNumber} {rewardName}</p>
</article>
);
}
+11 -5
View File
@@ -177,8 +177,10 @@ function SpectatorContent() {
{isPortraitLayout ? (
<div className={`${topBarItemClass} ${portraitItemHeight} flex flex-col gap-0.5`}>
<CommunityGoalBanner layout="desktop" dismissable={false} className="text-sm" />
<div className="min-h-0 flex-1">
<RawUserPilePanel hideNicknameForm hideHeader compact fillHeight className="h-full" />
<div className="min-h-0 overflow-y-auto">
<div className="panel h-full">
<RoverQueuesPanel title="Rovers" />
</div>
</div>
</div>
) : (
@@ -187,9 +189,13 @@ function SpectatorContent() {
</div>
)}
<div className={`${topBarItemClass} ${isPortraitLayout ? `${portraitItemHeight} overflow-y-auto` : ''}`}>
<div className="panel h-full">
<RoverQueuesPanel title="Rovers" />
</div>
{isPortraitLayout ? (
<RawUserPilePanel hideNicknameForm hideHeader compact fillHeight className="h-full" />
) : (
<div className="panel h-full">
<RoverQueuesPanel title="Rovers" />
</div>
)}
</div>
{!isPortraitLayout ? (
<div className={`${topBarItemClass} flex-1 min-h-0`}>