mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
better popuping vs not
This commit is contained in:
+3
-3
@@ -279,8 +279,8 @@ function AppWithProviders({ layout, isDesktop, fullscreen }) {
|
||||
const swapMobileControlColumns = Boolean(pageSettings?.swapMobileControlColumns);
|
||||
const fullscreenButtonSide = swapMobileControlColumns ? 'left' : 'right';
|
||||
const showFloatingFullscreenButton = !isDesktop && (fullscreenIsIOS || fullscreenNativeSupported);
|
||||
const latestReplay = useSessionSelector((state) => state.latestReplay);
|
||||
const { clearLatestReplay } = useSessionActions();
|
||||
const latestRequestedReplay = useSessionSelector((state) => state.latestRequestedReplay);
|
||||
const { clearReplayModal } = useSessionActions();
|
||||
const [helpVisible, setHelpVisible] = useState(false);
|
||||
const [quickstartVisible, setQuickstartVisible] = useState(false);
|
||||
|
||||
@@ -363,7 +363,7 @@ function AppWithProviders({ layout, isDesktop, fullscreen }) {
|
||||
onOpenHelp={openHelpFromQuickstart}
|
||||
onClose={closeQuickstart}
|
||||
/>
|
||||
<ReplayReadyPopup replay={latestReplay} onClose={clearLatestReplay} />
|
||||
<ReplayReadyPopup replay={latestRequestedReplay} onClose={clearReplayModal} />
|
||||
{showFloatingFullscreenButton ? (
|
||||
<FloatingFullscreenButton
|
||||
side={fullscreenButtonSide}
|
||||
|
||||
@@ -15,10 +15,11 @@ function formatBytes(value) {
|
||||
return `${(size / 1024 / 1024).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
export default function ReplayReadyPopup({ replay, onClose }) {
|
||||
export default function ReplayReadyPopup({ replay, onClose, variant = 'modal' }) {
|
||||
const videoUrl = normalizeUrl(replay?.url);
|
||||
if (!videoUrl) return null;
|
||||
|
||||
const isPanel = variant === 'panel';
|
||||
const title = String(replay?.title || 'Replay').trim() || 'Replay';
|
||||
const messageUrl = normalizeUrl(replay?.messageUrl);
|
||||
const meta = formatBytes(replay?.size) || null;
|
||||
@@ -38,12 +39,35 @@ export default function ReplayReadyPopup({ replay, onClose }) {
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-[110] flex items-center justify-center bg-black/80 p-1"
|
||||
onClick={onClose}
|
||||
role="presentation"
|
||||
const card = (
|
||||
<CardFrame
|
||||
title={title}
|
||||
meta={meta}
|
||||
actions={actions}
|
||||
fillHeight={isPanel}
|
||||
clipOverflow={false}
|
||||
bodyClassName={`${isPanel ? '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`}>
|
||||
<video
|
||||
key={videoUrl}
|
||||
src={videoUrl}
|
||||
controls
|
||||
autoPlay
|
||||
preload="auto"
|
||||
playsInline
|
||||
className={`${isPanel ? 'h-full min-h-[10rem]' : 'aspect-video max-h-[72vh]'} w-full bg-black`}
|
||||
/>
|
||||
</div>
|
||||
</CardFrame>
|
||||
);
|
||||
|
||||
if (isPanel) {
|
||||
return <div className="flex h-full min-h-[14rem] flex-col">{card}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[110] flex items-center justify-center bg-black/80 p-1" onClick={onClose} role="presentation">
|
||||
<div
|
||||
className="pointer-events-auto w-full max-w-4xl"
|
||||
onClick={(event) => {
|
||||
@@ -52,19 +76,7 @@ export default function ReplayReadyPopup({ replay, onClose }) {
|
||||
}}
|
||||
role="presentation"
|
||||
>
|
||||
<CardFrame title={title} meta={meta} actions={actions} clipOverflow={false} bodyClassName="space-y-0.5 p-0.5 text-sm text-slate-200">
|
||||
<div className="overflow-hidden rounded bg-black">
|
||||
<video
|
||||
key={videoUrl}
|
||||
src={videoUrl}
|
||||
controls
|
||||
autoPlay
|
||||
preload="auto"
|
||||
playsInline
|
||||
className="aspect-video max-h-[72vh] w-full bg-black"
|
||||
/>
|
||||
</div>
|
||||
</CardFrame>
|
||||
{card}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useSessionActions, useSessionSelector } from '../../context/SessionCont
|
||||
import { useSettingsNamespace } from '../../settings/index.js';
|
||||
import CardFrame from '../CardFrame/index.jsx';
|
||||
import RoverLabel from '../RoverLabel/index.jsx';
|
||||
import ReplayReadyPopup from './ReplayReadyPopup.jsx';
|
||||
|
||||
function normalizeSources(list = []) {
|
||||
return list
|
||||
@@ -26,8 +27,10 @@ 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);
|
||||
const { triggerReplay } = useSessionActions();
|
||||
const sources = normalizeSources(replaySources || []);
|
||||
const { value: settings, save: saveSettings } = useSettingsNamespace('replaySources', {});
|
||||
@@ -40,9 +43,24 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei
|
||||
const [includeSidebar, setIncludeSidebar] = useState(true);
|
||||
const [remainingMs, setRemainingMs] = useState(0);
|
||||
const [activeJobId, setActiveJobId] = useState(null);
|
||||
const [dismissedPanelReplayId, setDismissedPanelReplayId] = useState(null);
|
||||
const activeReplayJob = useSessionSelector((state) => (
|
||||
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,
|
||||
);
|
||||
|
||||
const defaults = useMemo(() => {
|
||||
const roverId = assignmentRoverId;
|
||||
@@ -173,6 +191,16 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei
|
||||
|
||||
const listWrapClass = fillHeight ? 'flex-1 min-h-0 overflow-y-auto' : '';
|
||||
|
||||
if (showPanelReplay) {
|
||||
return (
|
||||
<ReplayReadyPopup
|
||||
replay={latestReplay}
|
||||
variant="panel"
|
||||
onClose={() => setDismissedPanelReplayId(latestReplayJobId)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CardFrame title="Replay Sources" fillHeight={fillHeight} bodyClassName="space-y-0.5 text-sm">
|
||||
<div className={`grid gap-0.5 md:grid-cols-2 ${listWrapClass}`}>
|
||||
|
||||
@@ -17,6 +17,7 @@ const INITIAL_STATE = {
|
||||
alerts: [],
|
||||
replayJobs: {},
|
||||
latestReplay: null,
|
||||
latestRequestedReplay: null,
|
||||
};
|
||||
|
||||
const SessionContext = createContext(null);
|
||||
@@ -155,6 +156,9 @@ export function SessionProvider({ children }) {
|
||||
if (!payload?.jobId || !payload?.url) return;
|
||||
setState((prev) => {
|
||||
const previous = prev.replayJobs?.[payload.jobId] || {};
|
||||
const selfSocketId = String(prev.session?.socketId || '').trim();
|
||||
const requesterSocketId = String(payload?.requestedBy?.socketId || '').trim();
|
||||
const requestedByThisBrowser = Boolean(selfSocketId && requesterSocketId && selfSocketId === requesterSocketId);
|
||||
const nextJob = {
|
||||
...previous,
|
||||
...payload,
|
||||
@@ -168,12 +172,18 @@ export function SessionProvider({ children }) {
|
||||
...(prev.replayJobs || {}),
|
||||
[payload.jobId]: nextJob,
|
||||
},
|
||||
// Only the current replay popup is retained. Discord is the media host, so
|
||||
// Only the latest replay media is retained. Discord is the media host, so
|
||||
// this state is intentionally short-lived and does not become a replay library.
|
||||
latestReplay: {
|
||||
...payload,
|
||||
receivedAt: Date.now(),
|
||||
},
|
||||
latestRequestedReplay: requestedByThisBrowser
|
||||
? {
|
||||
...payload,
|
||||
receivedAt: Date.now(),
|
||||
}
|
||||
: prev.latestRequestedReplay,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -307,6 +317,13 @@ export function SessionProvider({ children }) {
|
||||
})),
|
||||
clearLatestReplay: () =>
|
||||
setState((prev) => (prev.latestReplay ? { ...prev, latestReplay: null } : prev)),
|
||||
showReplayModal: (replay) =>
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
latestRequestedReplay: replay ? { ...replay, receivedAt: Date.now() } : null,
|
||||
})),
|
||||
clearReplayModal: () =>
|
||||
setState((prev) => (prev.latestRequestedReplay ? { ...prev, latestRequestedReplay: null } : prev)),
|
||||
}),
|
||||
[emitWithAck, setState],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user