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-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Roomba Rover" />
|
<meta name="apple-mobile-web-app-title" content="Roomba Rover" />
|
||||||
<title>Roomba Rover</title>
|
<title>Roomba Rover</title>
|
||||||
<script type="module" crossorigin src="/assets/index-BysYkDl7.js"></script>
|
<script type="module" crossorigin src="/assets/index-DZt_FmKq.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-Dr65gZI2.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-BJwNeHyz.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<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);
|
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');
|
if (!channelId) throw new Error('Replay channel not configured');
|
||||||
const job = createReplayJob({
|
const job = createReplayJob({
|
||||||
id: jobId,
|
id: jobId,
|
||||||
@@ -64,6 +64,7 @@ function createBusEventHandler(deps) {
|
|||||||
title: explicitTitle,
|
title: explicitTitle,
|
||||||
sources,
|
sources,
|
||||||
includeSidebar,
|
includeSidebar,
|
||||||
|
requestedBy,
|
||||||
});
|
});
|
||||||
jobStatus.emit(job, 'accepted', { message: buildAcceptedMessage(job) });
|
jobStatus.emit(job, 'accepted', { message: buildAcceptedMessage(job) });
|
||||||
const progressMessage = await sendToChannel(channelId, buildAcceptedMessage(job), {}, DEFAULT_ALLOWED_MENTIONS);
|
const progressMessage = await sendToChannel(channelId, buildAcceptedMessage(job), {}, DEFAULT_ALLOWED_MENTIONS);
|
||||||
@@ -108,6 +109,7 @@ function createBusEventHandler(deps) {
|
|||||||
payload?.title || '',
|
payload?.title || '',
|
||||||
payload?.includeSidebar !== false,
|
payload?.includeSidebar !== false,
|
||||||
payload?.jobId || null,
|
payload?.jobId || null,
|
||||||
|
payload?.requestedBy || null,
|
||||||
).catch((err) => {
|
).catch((err) => {
|
||||||
logger.warn('Replay send failed', { error: err.message });
|
logger.warn('Replay send failed', { error: err.message });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ function buildDiscordReplayMediaPayload({ message, attachment, job }) {
|
|||||||
status: 'ready',
|
status: 'ready',
|
||||||
title: job.title,
|
title: job.title,
|
||||||
requester: job.requester,
|
requester: job.requester,
|
||||||
|
requestedBy: job.requestedBy || null,
|
||||||
url: attachment.url,
|
url: attachment.url,
|
||||||
proxyUrl: attachment.proxyURL || attachment.proxyUrl || null,
|
proxyUrl: attachment.proxyURL || attachment.proxyUrl || null,
|
||||||
messageUrl: message?.url || 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 normalizedSources = normalizeReplaySources(sources);
|
||||||
const resolvedTitle = buildReplayTitle({ explicitTitle: title, sources: normalizedSources });
|
const resolvedTitle = buildReplayTitle({ explicitTitle: title, sources: normalizedSources });
|
||||||
return {
|
return {
|
||||||
@@ -141,6 +142,7 @@ function createReplayJob({ id = null, requester = 'Discord', source = 'discord',
|
|||||||
title: resolvedTitle,
|
title: resolvedTitle,
|
||||||
sources: normalizedSources,
|
sources: normalizedSources,
|
||||||
includeSidebar: includeSidebar !== false,
|
includeSidebar: includeSidebar !== false,
|
||||||
|
requestedBy: requestedBy && typeof requestedBy === 'object' ? { ...requestedBy } : null,
|
||||||
createdAt: nowTs(),
|
createdAt: nowTs(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -152,6 +154,7 @@ function createJobStatusEmitter({ io, logger, sanitizeMentions }) {
|
|||||||
status,
|
status,
|
||||||
title: job.title,
|
title: job.title,
|
||||||
requester: job.requester,
|
requester: job.requester,
|
||||||
|
requestedBy: job.requestedBy || null,
|
||||||
sources: normalizeReplaySources(job.sources),
|
sources: normalizeReplaySources(job.sources),
|
||||||
message: extra.message ? sanitizeMentions(extra.message) : undefined,
|
message: extra.message ? sanitizeMentions(extra.message) : undefined,
|
||||||
media: extra.media || 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 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 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 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 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 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));
|
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 swapMobileControlColumns = Boolean(pageSettings?.swapMobileControlColumns);
|
||||||
const fullscreenButtonSide = swapMobileControlColumns ? 'left' : 'right';
|
const fullscreenButtonSide = swapMobileControlColumns ? 'left' : 'right';
|
||||||
const showFloatingFullscreenButton = !isDesktop && (fullscreenIsIOS || fullscreenNativeSupported);
|
const showFloatingFullscreenButton = !isDesktop && (fullscreenIsIOS || fullscreenNativeSupported);
|
||||||
const latestReplay = useSessionSelector((state) => state.latestReplay);
|
const latestRequestedReplay = useSessionSelector((state) => state.latestRequestedReplay);
|
||||||
const { clearLatestReplay } = useSessionActions();
|
const { clearReplayModal } = useSessionActions();
|
||||||
const [helpVisible, setHelpVisible] = useState(false);
|
const [helpVisible, setHelpVisible] = useState(false);
|
||||||
const [quickstartVisible, setQuickstartVisible] = useState(false);
|
const [quickstartVisible, setQuickstartVisible] = useState(false);
|
||||||
|
|
||||||
@@ -363,7 +363,7 @@ function AppWithProviders({ layout, isDesktop, fullscreen }) {
|
|||||||
onOpenHelp={openHelpFromQuickstart}
|
onOpenHelp={openHelpFromQuickstart}
|
||||||
onClose={closeQuickstart}
|
onClose={closeQuickstart}
|
||||||
/>
|
/>
|
||||||
<ReplayReadyPopup replay={latestReplay} onClose={clearLatestReplay} />
|
<ReplayReadyPopup replay={latestRequestedReplay} onClose={clearReplayModal} />
|
||||||
{showFloatingFullscreenButton ? (
|
{showFloatingFullscreenButton ? (
|
||||||
<FloatingFullscreenButton
|
<FloatingFullscreenButton
|
||||||
side={fullscreenButtonSide}
|
side={fullscreenButtonSide}
|
||||||
|
|||||||
@@ -15,10 +15,11 @@ function formatBytes(value) {
|
|||||||
return `${(size / 1024 / 1024).toFixed(1)} MB`;
|
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);
|
const videoUrl = normalizeUrl(replay?.url);
|
||||||
if (!videoUrl) return null;
|
if (!videoUrl) return null;
|
||||||
|
|
||||||
|
const isPanel = variant === 'panel';
|
||||||
const title = String(replay?.title || 'Replay').trim() || 'Replay';
|
const title = String(replay?.title || 'Replay').trim() || 'Replay';
|
||||||
const messageUrl = normalizeUrl(replay?.messageUrl);
|
const messageUrl = normalizeUrl(replay?.messageUrl);
|
||||||
const meta = formatBytes(replay?.size) || null;
|
const meta = formatBytes(replay?.size) || null;
|
||||||
@@ -38,12 +39,35 @@ export default function ReplayReadyPopup({ replay, onClose }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
const card = (
|
||||||
<div
|
<CardFrame
|
||||||
className="fixed inset-0 z-[110] flex items-center justify-center bg-black/80 p-1"
|
title={title}
|
||||||
onClick={onClose}
|
meta={meta}
|
||||||
role="presentation"
|
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
|
<div
|
||||||
className="pointer-events-auto w-full max-w-4xl"
|
className="pointer-events-auto w-full max-w-4xl"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
@@ -52,19 +76,7 @@ export default function ReplayReadyPopup({ replay, onClose }) {
|
|||||||
}}
|
}}
|
||||||
role="presentation"
|
role="presentation"
|
||||||
>
|
>
|
||||||
<CardFrame title={title} meta={meta} actions={actions} clipOverflow={false} bodyClassName="space-y-0.5 p-0.5 text-sm text-slate-200">
|
{card}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useSessionActions, useSessionSelector } from '../../context/SessionCont
|
|||||||
import { useSettingsNamespace } from '../../settings/index.js';
|
import { useSettingsNamespace } from '../../settings/index.js';
|
||||||
import CardFrame from '../CardFrame/index.jsx';
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
import RoverLabel from '../RoverLabel/index.jsx';
|
import RoverLabel from '../RoverLabel/index.jsx';
|
||||||
|
import ReplayReadyPopup from './ReplayReadyPopup.jsx';
|
||||||
|
|
||||||
function normalizeSources(list = []) {
|
function normalizeSources(list = []) {
|
||||||
return list
|
return list
|
||||||
@@ -26,8 +27,10 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei
|
|||||||
const replaySources = useSessionSelector((state) => state.session?.replaySources ?? []);
|
const replaySources = useSessionSelector((state) => state.session?.replaySources ?? []);
|
||||||
const mode = useSessionSelector((state) => state.session?.mode || null);
|
const mode = useSessionSelector((state) => state.session?.mode || null);
|
||||||
const assignmentRoverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? 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 roster = useSessionSelector((state) => state.session?.roster ?? []);
|
||||||
const replayState = useSessionSelector((state) => state.session?.replay || null);
|
const replayState = useSessionSelector((state) => state.session?.replay || null);
|
||||||
|
const latestReplay = useSessionSelector((state) => state.latestReplay);
|
||||||
const { triggerReplay } = useSessionActions();
|
const { triggerReplay } = useSessionActions();
|
||||||
const sources = normalizeSources(replaySources || []);
|
const sources = normalizeSources(replaySources || []);
|
||||||
const { value: settings, save: saveSettings } = useSettingsNamespace('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 [includeSidebar, setIncludeSidebar] = useState(true);
|
||||||
const [remainingMs, setRemainingMs] = useState(0);
|
const [remainingMs, setRemainingMs] = useState(0);
|
||||||
const [activeJobId, setActiveJobId] = useState(null);
|
const [activeJobId, setActiveJobId] = useState(null);
|
||||||
|
const [dismissedPanelReplayId, setDismissedPanelReplayId] = useState(null);
|
||||||
const activeReplayJob = useSessionSelector((state) => (
|
const activeReplayJob = useSessionSelector((state) => (
|
||||||
activeJobId ? state.replayJobs?.[activeJobId] || null : null
|
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 defaults = useMemo(() => {
|
||||||
const roverId = assignmentRoverId;
|
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' : '';
|
const listWrapClass = fillHeight ? 'flex-1 min-h-0 overflow-y-auto' : '';
|
||||||
|
|
||||||
|
if (showPanelReplay) {
|
||||||
|
return (
|
||||||
|
<ReplayReadyPopup
|
||||||
|
replay={latestReplay}
|
||||||
|
variant="panel"
|
||||||
|
onClose={() => setDismissedPanelReplayId(latestReplayJobId)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardFrame title="Replay Sources" fillHeight={fillHeight} bodyClassName="space-y-0.5 text-sm">
|
<CardFrame title="Replay Sources" fillHeight={fillHeight} bodyClassName="space-y-0.5 text-sm">
|
||||||
<div className={`grid gap-0.5 md:grid-cols-2 ${listWrapClass}`}>
|
<div className={`grid gap-0.5 md:grid-cols-2 ${listWrapClass}`}>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const INITIAL_STATE = {
|
|||||||
alerts: [],
|
alerts: [],
|
||||||
replayJobs: {},
|
replayJobs: {},
|
||||||
latestReplay: null,
|
latestReplay: null,
|
||||||
|
latestRequestedReplay: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SessionContext = createContext(null);
|
const SessionContext = createContext(null);
|
||||||
@@ -155,6 +156,9 @@ export function SessionProvider({ children }) {
|
|||||||
if (!payload?.jobId || !payload?.url) return;
|
if (!payload?.jobId || !payload?.url) return;
|
||||||
setState((prev) => {
|
setState((prev) => {
|
||||||
const previous = prev.replayJobs?.[payload.jobId] || {};
|
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 = {
|
const nextJob = {
|
||||||
...previous,
|
...previous,
|
||||||
...payload,
|
...payload,
|
||||||
@@ -168,12 +172,18 @@ export function SessionProvider({ children }) {
|
|||||||
...(prev.replayJobs || {}),
|
...(prev.replayJobs || {}),
|
||||||
[payload.jobId]: nextJob,
|
[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.
|
// this state is intentionally short-lived and does not become a replay library.
|
||||||
latestReplay: {
|
latestReplay: {
|
||||||
...payload,
|
...payload,
|
||||||
receivedAt: Date.now(),
|
receivedAt: Date.now(),
|
||||||
},
|
},
|
||||||
|
latestRequestedReplay: requestedByThisBrowser
|
||||||
|
? {
|
||||||
|
...payload,
|
||||||
|
receivedAt: Date.now(),
|
||||||
|
}
|
||||||
|
: prev.latestRequestedReplay,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -307,6 +317,13 @@ export function SessionProvider({ children }) {
|
|||||||
})),
|
})),
|
||||||
clearLatestReplay: () =>
|
clearLatestReplay: () =>
|
||||||
setState((prev) => (prev.latestReplay ? { ...prev, latestReplay: null } : prev)),
|
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],
|
[emitWithAck, setState],
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user