mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
optional sidebar
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# RULES FOR LOCKDOWN AUDIT
|
||||
@@ -0,0 +1,27 @@
|
||||
# REFACTOR RULES
|
||||
## server backend
|
||||
- there are a lot of services and some have gotten huge, like way too large to manage manually
|
||||
- all services should be converted into folders, with their files inside the folders
|
||||
- large functions of the service should be split off into more, smaller files within the service's folder
|
||||
- keep files relatively small and very clear and concise in their function. with titles commented at the top.
|
||||
|
||||
## webui frontend
|
||||
- similar situation to the server's service file bloat, but with large jsx component files
|
||||
- split up large jsx components and backing js files into folders containing smaller files
|
||||
- use same clear concise functional format, with title comments
|
||||
|
||||
|
||||
# REFACTOR TRACKING
|
||||
## server backend
|
||||
### COMPLETED SERVICES
|
||||
-
|
||||
|
||||
### LARGE CHANGES
|
||||
-
|
||||
|
||||
## webui frontend
|
||||
### COMPLETED COMPONENTS
|
||||
-
|
||||
|
||||
### LARGE CHANGES
|
||||
-
|
||||
File diff suppressed because one or more lines are too long
@@ -11,7 +11,7 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
||||
<title>Multi Roomba Rover</title>
|
||||
<script type="module" crossorigin src="/assets/index-lJfgnUIc.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-bndWXjZU.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DVTOmRBl.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -450,7 +450,7 @@ function buildReplayCaption({ requester, usedSources = [], missingSources = [],
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
async function sendReplayToChannel(channelId, requester, sources = [], explicitTitle = '') {
|
||||
async function sendReplayToChannel(channelId, requester, sources = [], explicitTitle = '', includeSidebar = true) {
|
||||
if (!channelId) {
|
||||
throw new Error('Replay channel not configured');
|
||||
}
|
||||
@@ -459,6 +459,7 @@ async function sendReplayToChannel(channelId, requester, sources = [], explicitT
|
||||
sources,
|
||||
title: resolvedTitle,
|
||||
requester,
|
||||
includeSidebar,
|
||||
});
|
||||
const filenameBase = sanitizeReplayTitleForFilename(resolvedTitle);
|
||||
const attachment = new AttachmentBuilder(buffer, { name: `${filenameBase}.mp4` });
|
||||
@@ -1619,7 +1620,13 @@ function handleBusEvent(event) {
|
||||
schedulePresenceRotation();
|
||||
break;
|
||||
case 'replay.requested':
|
||||
sendReplayToChannel(payload?.channelId, payload?.requester, payload?.sources || [], payload?.title || '').catch((err) => {
|
||||
sendReplayToChannel(
|
||||
payload?.channelId,
|
||||
payload?.requester,
|
||||
payload?.sources || [],
|
||||
payload?.title || '',
|
||||
payload?.includeSidebar !== false,
|
||||
).catch((err) => {
|
||||
logger.warn('Replay send failed', err.message);
|
||||
});
|
||||
break;
|
||||
|
||||
@@ -724,7 +724,7 @@ async function probeMaxFrameSize(paths) {
|
||||
return { maxWidth, maxHeight };
|
||||
}
|
||||
|
||||
async function buildReplayVideo({ sources = [], title = '', requester = '' } = {}) {
|
||||
async function buildReplayVideo({ sources = [], title = '', requester = '', includeSidebar = true } = {}) {
|
||||
if (!Array.isArray(sources) || !sources.length) {
|
||||
throw new Error('No replay sources selected');
|
||||
}
|
||||
@@ -843,22 +843,6 @@ async function buildReplayVideo({ sources = [], title = '', requester = '' } = {
|
||||
tileWidth = clampEven(tileWidth);
|
||||
tileHeight = clampEven(tileHeight);
|
||||
|
||||
const selectedRoverIds = usedSources
|
||||
.filter((entry) => entry?.type === 'rover')
|
||||
.map((entry) => String(entry.id));
|
||||
const driverBatteryLines = buildDriverBatterySnapshot(selectedRoverIds);
|
||||
const chatEvents = buildChatEventsForWindow(tStart, tEnd);
|
||||
const durationSec = BUILD_DURATION_MS / 1000;
|
||||
const sidebarPath = await renderSidebarVideo({
|
||||
tmpDir,
|
||||
title: resolvedTitle,
|
||||
durationSec,
|
||||
height: clampEven(tileHeight * layout.rows),
|
||||
windowStartMs: tStart,
|
||||
driverBatteryLines,
|
||||
chatEvents,
|
||||
});
|
||||
|
||||
const inputArgs = [];
|
||||
const filterParts = [];
|
||||
const layoutParts = [];
|
||||
@@ -872,9 +856,29 @@ async function buildReplayVideo({ sources = [], title = '', requester = '' } = {
|
||||
layoutParts.push(`${x}_${y}`);
|
||||
}
|
||||
|
||||
inputArgs.push('-i', sidebarPath);
|
||||
const sidebarInputIndex = normalizedVideos.length;
|
||||
const audioInputStart = normalizedVideos.length + 1;
|
||||
let audioInputStart = normalizedVideos.length;
|
||||
let sidebarInputIndex = -1;
|
||||
if (includeSidebar) {
|
||||
const selectedRoverIds = usedSources
|
||||
.filter((entry) => entry?.type === 'rover')
|
||||
.map((entry) => String(entry.id));
|
||||
const driverBatteryLines = buildDriverBatterySnapshot(selectedRoverIds);
|
||||
const chatEvents = buildChatEventsForWindow(tStart, tEnd);
|
||||
const durationSec = BUILD_DURATION_MS / 1000;
|
||||
const sidebarPath = await renderSidebarVideo({
|
||||
tmpDir,
|
||||
title: resolvedTitle,
|
||||
durationSec,
|
||||
height: clampEven(tileHeight * layout.rows),
|
||||
windowStartMs: tStart,
|
||||
driverBatteryLines,
|
||||
chatEvents,
|
||||
});
|
||||
inputArgs.push('-i', sidebarPath);
|
||||
sidebarInputIndex = normalizedVideos.length;
|
||||
audioInputStart = normalizedVideos.length + 1;
|
||||
}
|
||||
|
||||
for (const audioPath of normalizedAudios) {
|
||||
inputArgs.push('-i', audioPath);
|
||||
}
|
||||
@@ -887,7 +891,11 @@ async function buildReplayVideo({ sources = [], title = '', requester = '' } = {
|
||||
`xstack=inputs=${normalizedVideos.length}:layout=${layoutParts.join('|')}:fill=black[vgrid]`,
|
||||
);
|
||||
}
|
||||
filterParts.push(`[vgrid][${sidebarInputIndex}:v]hstack=inputs=2[vout]`);
|
||||
if (includeSidebar) {
|
||||
filterParts.push(`[vgrid][${sidebarInputIndex}:v]hstack=inputs=2[vout]`);
|
||||
} else {
|
||||
filterParts.push('[vgrid]null[vout]');
|
||||
}
|
||||
|
||||
if (normalizedAudios.length) {
|
||||
const audioRefs = normalizedAudios.map((_, idx) => `[${audioInputStart + idx}:a]`).join('');
|
||||
|
||||
@@ -20,6 +20,11 @@ function normalizeReplayTitle(value) {
|
||||
return value.trim().slice(0, 120);
|
||||
}
|
||||
|
||||
function normalizeIncludeSidebar(value) {
|
||||
if (typeof value === 'boolean') return value;
|
||||
return true;
|
||||
}
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
socket.on('replay:trigger', (payload = {}, cb = () => {}) => {
|
||||
if (getMode() === MODES.LOCKDOWN) {
|
||||
@@ -43,6 +48,7 @@ io.on('connection', (socket) => {
|
||||
}
|
||||
const requester = buildRequesterLabel(socket);
|
||||
const title = normalizeReplayTitle(payload?.title);
|
||||
const includeSidebar = normalizeIncludeSidebar(payload?.includeSidebar);
|
||||
const attempt = tryTriggerReplay({ by: { source: 'web', requester } });
|
||||
if (!attempt.ok) {
|
||||
cb({ error: 'Replay cooldown active', remainingMs: attempt.remainingMs, state: attempt.state });
|
||||
@@ -55,6 +61,7 @@ io.on('connection', (socket) => {
|
||||
channelId,
|
||||
requester,
|
||||
title,
|
||||
includeSidebar,
|
||||
sources,
|
||||
requestedBy: { socketId: socket.id },
|
||||
},
|
||||
|
||||
@@ -28,6 +28,7 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei
|
||||
const [success, setSuccess] = useState(null);
|
||||
const [title, setTitle] = useState('');
|
||||
const [titleDirty, setTitleDirty] = useState(false);
|
||||
const [includeSidebar, setIncludeSidebar] = useState(true);
|
||||
const replayState = session?.replay || null;
|
||||
const [remainingMs, setRemainingMs] = useState(0);
|
||||
|
||||
@@ -61,6 +62,15 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei
|
||||
setSelected(defaults);
|
||||
}, [settings?.[panelId], defaults, panelId]);
|
||||
|
||||
useEffect(() => {
|
||||
const saved = settings?.[`${panelId}:includeSidebar`];
|
||||
if (typeof saved === 'boolean') {
|
||||
setIncludeSidebar(saved);
|
||||
return;
|
||||
}
|
||||
setIncludeSidebar(true);
|
||||
}, [settings?.[`${panelId}:includeSidebar`], panelId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!titleDirty) {
|
||||
setTitle(defaultTitle);
|
||||
@@ -115,7 +125,7 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei
|
||||
return { type, id };
|
||||
});
|
||||
const resolvedTitle = String(title || '').trim() || defaultTitle;
|
||||
await triggerReplay(payload, resolvedTitle);
|
||||
await triggerReplay({ sources: payload, title: resolvedTitle, includeSidebar });
|
||||
setSuccess('Replay sent. Check the Discord replay channel.');
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
@@ -138,7 +148,11 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei
|
||||
<GroupList title="Room Cams" items={grouped.rooms} selected={selected} onToggle={toggleKey} />
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<label className="panel-muted block text-xs" htmlFor={`${panelId}-title`}>
|
||||
Replay title
|
||||
</label>
|
||||
<input
|
||||
id={`${panelId}-title`}
|
||||
type="text"
|
||||
className="field-input w-full text-xs"
|
||||
value={title}
|
||||
@@ -149,6 +163,19 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei
|
||||
placeholder={defaultTitle}
|
||||
maxLength={120}
|
||||
/>
|
||||
<label className="surface flex items-center gap-0.5 text-xs">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={includeSidebar}
|
||||
onChange={(event) => {
|
||||
const next = Boolean(event.target.checked);
|
||||
setIncludeSidebar(next);
|
||||
saveSettings((current) => ({ ...(current || {}), [`${panelId}:includeSidebar`]: next }));
|
||||
}}
|
||||
className="accent-emerald-400"
|
||||
/>
|
||||
<span>Include replay sidebar</span>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="button-dark w-full text-xs disabled:opacity-40"
|
||||
|
||||
Reference in New Issue
Block a user