mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-17 10:00:46 -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-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>
|
||||||
|
|||||||
@@ -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} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 ? (
|
{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` : ''}`}>
|
||||||
<div className="panel h-full">
|
{isPortraitLayout ? (
|
||||||
<RoverQueuesPanel title="Rovers" />
|
<RawUserPilePanel hideNicknameForm hideHeader compact fillHeight className="h-full" />
|
||||||
</div>
|
) : (
|
||||||
|
<div className="panel h-full">
|
||||||
|
<RoverQueuesPanel title="Rovers" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!isPortraitLayout ? (
|
{!isPortraitLayout ? (
|
||||||
<div className={`${topBarItemClass} flex-1 min-h-0`}>
|
<div className={`${topBarItemClass} flex-1 min-h-0`}>
|
||||||
|
|||||||
Reference in New Issue
Block a user