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-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-C1njEZEY.js"></script> <script type="module" crossorigin src="/assets/index-CEe1q-KX.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-7nrE9xLC.css"> <link rel="stylesheet" crossorigin href="/assets/index-C3BucANX.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+57 -8
View File
@@ -1,9 +1,12 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useSession } from '../context/SessionContext.jsx'; import { useSession } from '../context/SessionContext.jsx';
import { useSocket } from '../context/SocketContext.jsx';
import ChatMessageRow from './ChatMessageRow.jsx'; import ChatMessageRow from './ChatMessageRow.jsx';
import ChatTypingRow from './ChatTypingRow.jsx'; import ChatTypingRow from './ChatTypingRow.jsx';
import ButtonBoxTile from './ButtonBoxTile.jsx';
const LIFETIME_MS = 3000; const LIFETIME_MS = 3000;
const BUTTONBOX_LIFETIME_MS = 3500;
const DEFAULT_COLOR = '#2196f3'; const DEFAULT_COLOR = '#2196f3';
function buildKey(alert) { function buildKey(alert) {
@@ -13,9 +16,37 @@ function buildKey(alert) {
} }
export default function AlertFeed({ scale = 1 }) { export default function AlertFeed({ scale = 1 }) {
const { alerts } = useSession(); const { alerts, session, pushAlert } = useSession();
const socket = useSocket();
const [now, setNow] = useState(() => Date.now()); 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(() => { useEffect(() => {
if (!latest.length) return undefined; if (!latest.length) return undefined;
@@ -23,12 +54,15 @@ export default function AlertFeed({ scale = 1 }) {
return () => clearInterval(interval); return () => clearInterval(interval);
}, [latest.length]); }, [latest.length]);
const visible = latest const visibleByKey = new Map();
.map((item) => ({ latest.forEach((item) => {
...item, const age = now - (item.alert.receivedAt ?? item.alert.timestamp ?? 0);
age: now - (item.alert.receivedAt ?? item.alert.timestamp ?? 0), const lifetimeMs = Number.isFinite(item.alert.lifetimeMs) ? item.alert.lifetimeMs : LIFETIME_MS;
})) if (age <= lifetimeMs) {
.filter((item) => item.age <= LIFETIME_MS); visibleByKey.set(item.key, { ...item, age });
}
});
const visible = Array.from(visibleByKey.values()).slice(-3);
if (!visible.length) return null; if (!visible.length) return null;
@@ -66,6 +100,21 @@ function hexToRgb(hex) {
} }
function AlertToast({ alert }) { 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) { if (alert.kind === 'chat' && alert.payload) {
return <ChatMessageRow message={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 { useSocket } from '../context/SocketContext.jsx';
import { useSettingsNamespace } from '../settings/index.js'; import { useSettingsNamespace } from '../settings/index.js';
import { AUDIO_SETTINGS_DEFAULTS } from '../settings/namespaces.js'; import { AUDIO_SETTINGS_DEFAULTS } from '../settings/namespaces.js';
import ButtonBoxTile from './ButtonBoxTile.jsx';
const FLASH_MS = 420; const FLASH_MS = 420;
const REWARD_FLASH_MS = 1200; const REWARD_FLASH_MS = 1200;
@@ -139,20 +140,15 @@ export default function ButtonBoxPanel() {
const rewardActive = Boolean(rewardFlash[id]); const rewardActive = Boolean(rewardFlash[id]);
return ( return (
<article <ButtonBoxTile
key={id} key={id}
className={[ buttonId={id}
'surface px-0.5 py-0.5 text-center', count={count}
incActive ? 'bg-cyan-900/45' : '', goal={goal}
rewardActive ? 'bg-fuchsia-900/45' : '', rewardNumber={rewardNumber}
] rewardName={rewardName}
.filter(Boolean) className={[incActive ? 'bg-cyan-900/45' : '', rewardActive ? 'bg-fuchsia-900/45' : ''].filter(Boolean).join(' ')}
.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>
); );
})} })}
</div> </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>
);
}
+8 -2
View File
@@ -177,8 +177,10 @@ function SpectatorContent() {
{isPortraitLayout ? ( {isPortraitLayout ? (
<div className={`${topBarItemClass} ${portraitItemHeight} flex flex-col gap-0.5`}> <div className={`${topBarItemClass} ${portraitItemHeight} flex flex-col gap-0.5`}>
<CommunityGoalBanner layout="desktop" dismissable={false} className="text-sm" /> <CommunityGoalBanner layout="desktop" dismissable={false} className="text-sm" />
<div className="min-h-0 flex-1"> <div className="min-h-0 overflow-y-auto">
<RawUserPilePanel hideNicknameForm hideHeader compact fillHeight className="h-full" /> <div className="panel h-full">
<RoverQueuesPanel title="Rovers" />
</div>
</div> </div>
</div> </div>
) : ( ) : (
@@ -187,9 +189,13 @@ function SpectatorContent() {
</div> </div>
)} )}
<div className={`${topBarItemClass} ${isPortraitLayout ? `${portraitItemHeight} overflow-y-auto` : ''}`}> <div className={`${topBarItemClass} ${isPortraitLayout ? `${portraitItemHeight} overflow-y-auto` : ''}`}>
{isPortraitLayout ? (
<RawUserPilePanel hideNicknameForm hideHeader compact fillHeight className="h-full" />
) : (
<div className="panel h-full"> <div className="panel h-full">
<RoverQueuesPanel title="Rovers" /> <RoverQueuesPanel title="Rovers" />
</div> </div>
)}
</div> </div>
{!isPortraitLayout ? ( {!isPortraitLayout ? (
<div className={`${topBarItemClass} flex-1 min-h-0`}> <div className={`${topBarItemClass} flex-1 min-h-0`}>