mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
better popuping vs not
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
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="Roomba Rover" />
|
||||
<title>Roomba Rover</title>
|
||||
<script type="module" crossorigin src="/assets/index-BysYkDl7.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Dr65gZI2.css">
|
||||
<script type="module" crossorigin src="/assets/index-DZt_FmKq.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BJwNeHyz.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -55,7 +55,7 @@ function createBusEventHandler(deps) {
|
||||
await sendToChannel(channelId, `${prefix}${content || ''}`.trim(), { embeds: payloadEmbeds, files: Array.isArray(files) ? files : undefined }, { parse: [], roles: pingRoleId ? [pingRoleId] : [] }, !pingRoleId);
|
||||
}
|
||||
|
||||
async function sendReplayToChannel(channelId, requester, sources = [], explicitTitle = '', includeSidebar = true, jobId = null) {
|
||||
async function sendReplayToChannel(channelId, requester, sources = [], explicitTitle = '', includeSidebar = true, jobId = null, requestedBy = null) {
|
||||
if (!channelId) throw new Error('Replay channel not configured');
|
||||
const job = createReplayJob({
|
||||
id: jobId,
|
||||
@@ -64,6 +64,7 @@ function createBusEventHandler(deps) {
|
||||
title: explicitTitle,
|
||||
sources,
|
||||
includeSidebar,
|
||||
requestedBy,
|
||||
});
|
||||
jobStatus.emit(job, 'accepted', { message: buildAcceptedMessage(job) });
|
||||
const progressMessage = await sendToChannel(channelId, buildAcceptedMessage(job), {}, DEFAULT_ALLOWED_MENTIONS);
|
||||
@@ -108,6 +109,7 @@ function createBusEventHandler(deps) {
|
||||
payload?.title || '',
|
||||
payload?.includeSidebar !== false,
|
||||
payload?.jobId || null,
|
||||
payload?.requestedBy || null,
|
||||
).catch((err) => {
|
||||
logger.warn('Replay send failed', { error: err.message });
|
||||
});
|
||||
|
||||
@@ -115,6 +115,7 @@ function buildDiscordReplayMediaPayload({ message, attachment, job }) {
|
||||
status: 'ready',
|
||||
title: job.title,
|
||||
requester: job.requester,
|
||||
requestedBy: job.requestedBy || null,
|
||||
url: attachment.url,
|
||||
proxyUrl: attachment.proxyURL || attachment.proxyUrl || null,
|
||||
messageUrl: message?.url || null,
|
||||
@@ -131,7 +132,7 @@ function buildDiscordReplayMediaPayload({ message, attachment, job }) {
|
||||
};
|
||||
}
|
||||
|
||||
function createReplayJob({ id = null, requester = 'Discord', source = 'discord', title = '', sources = [], includeSidebar = true } = {}) {
|
||||
function createReplayJob({ id = null, requester = 'Discord', source = 'discord', title = '', sources = [], includeSidebar = true, requestedBy = null } = {}) {
|
||||
const normalizedSources = normalizeReplaySources(sources);
|
||||
const resolvedTitle = buildReplayTitle({ explicitTitle: title, sources: normalizedSources });
|
||||
return {
|
||||
@@ -141,6 +142,7 @@ function createReplayJob({ id = null, requester = 'Discord', source = 'discord',
|
||||
title: resolvedTitle,
|
||||
sources: normalizedSources,
|
||||
includeSidebar: includeSidebar !== false,
|
||||
requestedBy: requestedBy && typeof requestedBy === 'object' ? { ...requestedBy } : null,
|
||||
createdAt: nowTs(),
|
||||
};
|
||||
}
|
||||
@@ -152,6 +154,7 @@ function createJobStatusEmitter({ io, logger, sanitizeMentions }) {
|
||||
status,
|
||||
title: job.title,
|
||||
requester: job.requester,
|
||||
requestedBy: job.requestedBy || null,
|
||||
sources: normalizeReplaySources(job.sources),
|
||||
message: extra.message ? sanitizeMentions(extra.message) : undefined,
|
||||
media: extra.media || undefined,
|
||||
|
||||
@@ -9,7 +9,7 @@ const SEGMENT_ROOT = path.join(resolveDataDir(), 'replay-segments');
|
||||
const SEGMENT_SECONDS = Math.max(1, Number.parseInt(process.env.REPLAY_SEGMENT_SECONDS || '1', 10));
|
||||
const BUFFER_SECONDS = Math.max(20, Number.parseInt(process.env.REPLAY_BUFFER_SECONDS || '45', 10));
|
||||
const CLEANUP_INTERVAL_MS = Math.max(500, Number.parseInt(process.env.REPLAY_TICK_MS || '2000', 10));
|
||||
const BUILD_DURATION_MS = Math.max(5000, Number.parseInt(process.env.REPLAY_DURATION_MS || '35000', 10));
|
||||
const BUILD_DURATION_MS = Math.max(5000, Number.parseInt(process.env.REPLAY_DURATION_MS || '20000', 10));
|
||||
const BUILD_GUARD_MS = Math.max(200, Number.parseInt(process.env.REPLAY_GUARD_MS || '1200', 10));
|
||||
const TARGET_FPS = Math.max(10, Number.parseInt(process.env.REPLAY_TARGET_FPS || '30', 10));
|
||||
const MAX_WIDTH = Math.max(320, Number.parseInt(process.env.REPLAY_MAX_WIDTH || '1280', 10));
|
||||
|
||||
+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