buncha stuff, charging complete sounds, room display button feedback, etc

This commit is contained in:
legop3
2026-06-10 01:46:03 -04:00
parent 2ce2f23b0a
commit 9826d4245f
15 changed files with 364 additions and 163 deletions
+1 -5
View File
@@ -41,11 +41,10 @@ import useUserIdentitySync from './hooks/useUserIdentitySync.js';
import GlobalObjectiveBanner from './components/GlobalObjectiveBanner/index.jsx';
import RoverQueuesPanel from './components/RoverQueuesPanel/index.jsx';
import VipPanel from './components/VipPanel/index.jsx';
import { useSessionActions, useSessionSelector } from './context/SessionContext.jsx';
import { useSessionSelector } from './context/SessionContext.jsx';
import ButtonBoxPanel from './components/ButtonBoxPanel/index.jsx';
import RewardRunOverlay from './components/RewardRunOverlay/index.jsx';
import SocketConnectionPill from './components/SocketConnectionPill/index.jsx';
import ReplayReadyPopup from './components/ReplaySourcesPanel/ReplayReadyPopup.jsx';
import { pageBackgroundClass, themeGapClass, themeStackClass } from './themeFlags.js';
function useLayoutMode() {
@@ -279,8 +278,6 @@ function AppWithProviders({ layout, isDesktop, fullscreen }) {
const swapMobileControlColumns = Boolean(pageSettings?.swapMobileControlColumns);
const fullscreenButtonSide = swapMobileControlColumns ? 'left' : 'right';
const showFloatingFullscreenButton = !isDesktop && (fullscreenIsIOS || fullscreenNativeSupported);
const latestRequestedReplay = useSessionSelector((state) => state.latestRequestedReplay);
const { clearReplayModal } = useSessionActions();
const [helpVisible, setHelpVisible] = useState(false);
const [quickstartVisible, setQuickstartVisible] = useState(false);
@@ -363,7 +360,6 @@ function AppWithProviders({ layout, isDesktop, fullscreen }) {
onOpenHelp={openHelpFromQuickstart}
onClose={closeQuickstart}
/>
<ReplayReadyPopup replay={latestRequestedReplay} onClose={clearReplayModal} />
{showFloatingFullscreenButton ? (
<FloatingFullscreenButton
side={fullscreenButtonSide}
@@ -84,6 +84,7 @@ export default function ReplayReadyPopup({ replay, onClose, variant = 'modal' })
const isPanel = variant === 'panel';
const isFloatingPanel = variant === 'floating-panel';
const isModal = !isPanel && !isFloatingPanel;
const title = String(replay?.title || 'Replay').trim() || 'Replay';
const messageUrl = normalizeUrl(replay?.messageUrl);
const meta = formatBytes(replay?.size) || null;
@@ -136,11 +137,12 @@ export default function ReplayReadyPopup({ replay, onClose, variant = 'modal' })
title={title}
meta={meta}
actions={actions}
fillHeight={isPanel}
fillHeight={isPanel || isModal}
clipOverflow={false}
bodyClassName={`${isPanel ? 'flex min-h-0 flex-1 flex-col' : ''} space-y-0.5 p-0.5 text-sm text-slate-200`}
className={isModal ? 'h-full w-full' : ''}
bodyClassName={`${isPanel || isModal ? 'flex min-h-0 flex-1 flex-col' : ''} space-y-0.5 p-0.5 text-sm text-slate-200`}
>
<div className={`${isPanel ? 'min-h-0 flex-1' : ''} overflow-hidden rounded bg-black`}>
<div className={`${isPanel || isModal ? 'min-h-0 flex-1' : ''} overflow-hidden rounded bg-black`}>
<video
key={videoUrl}
src={videoUrl}
@@ -149,7 +151,11 @@ export default function ReplayReadyPopup({ replay, onClose, variant = 'modal' })
preload="auto"
playsInline
{...videoLifecycleProps}
className={`${isPanel ? 'h-full min-h-[10rem]' : 'aspect-video max-h-[72vh]'} w-full bg-black`}
// Spectator pages use the modal variant as the primary replay viewer, so
// that video should fill the available viewport instead of behaving like
// a centered dialog preview. object-contain preserves the replay frame
// without cropping if the browser viewport is not the same aspect ratio.
className={`${isPanel ? 'h-full min-h-[10rem]' : isModal ? 'h-full object-contain' : 'aspect-video max-h-[72vh]'} w-full bg-black`}
/>
</div>
</CardFrame>
@@ -160,11 +166,12 @@ export default function ReplayReadyPopup({ replay, onClose, variant = 'modal' })
}
return (
<div className="fixed inset-0 z-[110] flex items-center justify-center bg-black/80 p-1" onClick={onClose} role="presentation">
<div className="fixed inset-0 z-[110] flex items-stretch justify-stretch bg-black" onClick={onClose} role="presentation">
<div
className="pointer-events-auto w-full max-w-4xl"
className="pointer-events-auto h-full w-full"
onClick={(event) => {
// The backdrop closes the popup, but clicks inside the card must leave video controls usable.
// The backdrop closes the popup, but clicks inside the fullscreen card
// must leave video controls and header actions usable.
event.stopPropagation();
}}
role="presentation"
@@ -27,7 +27,6 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei
const replaySources = useSessionSelector((state) => state.session?.replaySources ?? []);
const mode = useSessionSelector((state) => state.session?.mode || null);
const assignmentRoverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? null);
const selfSocketId = useSessionSelector((state) => state.session?.socketId || null);
const roster = useSessionSelector((state) => state.session?.roster ?? []);
const replayState = useSessionSelector((state) => state.session?.replay || null);
const latestReplay = useSessionSelector((state) => state.latestReplay);
@@ -48,17 +47,9 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei
activeJobId ? state.replayJobs?.[activeJobId] || null : null
));
const latestReplayJobId = latestReplay?.jobId || null;
const latestReplayRequesterSocketId = latestReplay?.requestedBy?.socketId || null;
const latestReplayRequestedBySelf = Boolean(
latestReplayJobId &&
selfSocketId &&
latestReplayRequesterSocketId &&
String(latestReplayRequesterSocketId) === String(selfSocketId),
);
const showPanelReplay = Boolean(
latestReplay?.url &&
latestReplayJobId &&
!latestReplayRequestedBySelf &&
dismissedPanelReplayId !== latestReplayJobId,
);
@@ -10,6 +10,7 @@ import RewardRunOverlay from '../../components/RewardRunOverlay/index.jsx';
import OnlinePeopleStrip from './components/OnlinePeopleStrip.jsx';
import DisplayRoverGrid from './components/DisplayRoverGrid.jsx';
import DisplayChatFeed from './components/DisplayChatFeed.jsx';
import DisplayNoticeOverlay from './components/DisplayNoticeOverlay.jsx';
export default function ServerDisplayContent() {
const { session } = useSession();
@@ -43,6 +44,7 @@ export default function ServerDisplayContent() {
<div className="min-h-0 flex-[1.28]">
<DisplayChatFeed />
</div>
<DisplayNoticeOverlay />
<RewardRunOverlay />
{/* Display is spectator-like: every Discord-hosted replay should take over
this physical-room board, not only replays requested by this browser. */}
@@ -0,0 +1,67 @@
// Display Notice Overlay
// Purpose: Shows server-side room-control feedback as huge temporary display text.
// Scope: Listens only on the /display page so normal driver and spectator pages are not interrupted.
import { useEffect, useRef, useState } from 'react';
import { useSocket } from '../../../context/SocketContext.jsx';
const DEFAULT_DURATION_MS = 4500;
function normalizeNotice(payload = {}) {
const text = String(payload?.text || '').trim();
if (!text) return null;
const durationMs = Number.isFinite(Number(payload?.durationMs))
? Math.max(1200, Math.min(15000, Number(payload.durationMs)))
: DEFAULT_DURATION_MS;
return {
id: payload?.id || `display-notice-${Date.now()}`,
text,
durationMs,
};
}
export default function DisplayNoticeOverlay() {
const socket = useSocket();
const [notice, setNotice] = useState(null);
const timerRef = useRef(null);
useEffect(() => {
function clearNoticeTimer() {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
}
function handleNotice(payload = {}) {
const next = normalizeNotice(payload);
if (!next) return;
// Home Assistant buttons can be pressed rapidly. The room display should
// reflect the most recent confirmed action instead of queueing stale room
// state messages after the action has already changed again.
clearNoticeTimer();
setNotice(next);
timerRef.current = setTimeout(() => {
setNotice((current) => (current?.id === next.id ? null : current));
timerRef.current = null;
}, next.durationMs);
}
socket.on('display:notice', handleNotice);
return () => {
clearNoticeTimer();
socket.off('display:notice', handleNotice);
};
}, [socket]);
if (!notice) return null;
return (
<div className="pointer-events-none fixed inset-0 z-[130] flex items-center justify-center bg-black px-[4vw] py-[4vh]">
<div className="max-w-[92vw] border-4 border-cyan-200 bg-slate-950 px-[3vw] py-[2.5vh] text-center">
<div className="whitespace-pre-wrap text-[clamp(3.2rem,11vh,11rem)] font-black leading-[0.95] text-white">
{notice.text}
</div>
</div>
</div>
);
}