mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
waui
This commit is contained in:
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-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>
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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`}>
|
||||
|
||||
Reference in New Issue
Block a user