This commit is contained in:
legop3
2026-03-17 19:07:59 -04:00
parent 7934e1faf5
commit f5d7d2d82c
4 changed files with 57 additions and 72 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -11,7 +11,7 @@
<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="Multi Roomba Rover" /> <meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
<title>Multi Roomba Rover</title> <title>Multi Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-CIRCS_69.js"></script> <script type="module" crossorigin src="/assets/index-D4q08ieD.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-WqYsCIRI.css"> <link rel="stylesheet" crossorigin href="/assets/index-WqYsCIRI.css">
</head> </head>
<body> <body>
-1
View File
@@ -45,7 +45,6 @@ export default function VipPanel() {
audioForwardByRover={session?.audioForward || {}} audioForwardByRover={session?.audioForward || {}}
playUploadedAudio={playUploadedAudio} playUploadedAudio={playUploadedAudio}
stopUploadedAudio={stopUploadedAudio} stopUploadedAudio={stopUploadedAudio}
onMessage={setMessage}
/> />
) : ( ) : (
<VipVerificationCard <VipVerificationCard
@@ -1,5 +1,11 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { MAX_UPLOAD_BYTES, bytesToBase64, fieldClass } from './constants.js'; import {
MAX_UPLOAD_BYTES,
bytesToBase64,
fieldClass,
flowWrapClass,
innerFlowClass,
} from './constants.js';
export default function VipAudioForwardingCard({ export default function VipAudioForwardingCard({
roster = [], roster = [],
@@ -7,13 +13,12 @@ export default function VipAudioForwardingCard({
audioForwardByRover = {}, audioForwardByRover = {},
playUploadedAudio, playUploadedAudio,
stopUploadedAudio, stopUploadedAudio,
onMessage,
}) { }) {
const [selectedRoverId, setSelectedRoverId] = useState('');
const [selectedUpload, setSelectedUpload] = useState(null); const [selectedUpload, setSelectedUpload] = useState(null);
const [working, setWorking] = useState(false); const [working, setWorking] = useState(false);
const [message, setMessage] = useState('');
const singleRoverId = roster.length === 1 ? roster[0].id : ''; const singleRoverId = roster.length === 1 ? roster[0].id : '';
const targetRoverId = String(selectedRoverId || singleRoverId || ownRoverId || '').trim(); const targetRoverId = String(singleRoverId || ownRoverId || '').trim();
const selectedForwardState = useMemo( const selectedForwardState = useMemo(
() => (targetRoverId ? audioForwardByRover?.[targetRoverId] || null : null), () => (targetRoverId ? audioForwardByRover?.[targetRoverId] || null : null),
[audioForwardByRover, targetRoverId], [audioForwardByRover, targetRoverId],
@@ -22,19 +27,19 @@ export default function VipAudioForwardingCard({
const handleUploadPlay = async () => { const handleUploadPlay = async () => {
const roverId = targetRoverId; const roverId = targetRoverId;
if (!roverId) { if (!roverId) {
onMessage?.('Take control of a rover first.'); setMessage('Take control of a rover first.');
return; return;
} }
if (!selectedUpload) { if (!selectedUpload) {
onMessage?.('Select an audio file first.'); setMessage('Select an audio file first.');
return; return;
} }
if (selectedUpload.size > MAX_UPLOAD_BYTES) { if (selectedUpload.size > MAX_UPLOAD_BYTES) {
onMessage?.(`File too large (max ${MAX_UPLOAD_BYTES} bytes).`); setMessage(`File too large (max ${MAX_UPLOAD_BYTES} bytes).`);
return; return;
} }
setWorking(true); setWorking(true);
onMessage?.(''); setMessage('');
try { try {
const buffer = await selectedUpload.arrayBuffer(); const buffer = await selectedUpload.arrayBuffer();
const base64 = bytesToBase64(new Uint8Array(buffer)); const base64 = bytesToBase64(new Uint8Array(buffer));
@@ -44,9 +49,9 @@ export default function VipAudioForwardingCard({
mime: selectedUpload.type || '', mime: selectedUpload.type || '',
dataBase64: base64, dataBase64: base64,
}); });
onMessage?.(`Playing upload on ${roverId}.`); setMessage(`Playing upload on ${roverId}.`);
} catch (err) { } catch (err) {
onMessage?.(err.message || 'Failed to play upload.'); setMessage(err.message || 'Failed to play upload.');
} finally { } finally {
setWorking(false); setWorking(false);
} }
@@ -55,75 +60,56 @@ export default function VipAudioForwardingCard({
const handleUploadStop = async () => { const handleUploadStop = async () => {
const roverId = targetRoverId; const roverId = targetRoverId;
if (!roverId) { if (!roverId) {
onMessage?.('Take control of a rover first.'); setMessage('Take control of a rover first.');
return; return;
} }
setWorking(true); setWorking(true);
onMessage?.(''); setMessage('');
try { try {
await stopUploadedAudio?.(roverId); await stopUploadedAudio?.(roverId);
onMessage?.(`Stopped upload on ${roverId}.`); setMessage(`Stopped upload on ${roverId}.`);
} catch (err) { } catch (err) {
onMessage?.(err.message || 'Failed to stop upload.'); setMessage(err.message || 'Failed to stop upload.');
} finally { } finally {
setWorking(false); setWorking(false);
} }
}; };
return ( return (
<section className="surface space-y-0.5 text-sm text-slate-200"> <section className={`surface ${flowWrapClass}`}>
<p className="text-slate-300">VIP Audio Forwarding</p> <div className={innerFlowClass}>
{roster.length > 1 ? ( <p className="text-sm text-slate-300">VIP Audio Forwarding</p>
<label className="grid gap-0.5 text-xs text-slate-300"> <label className="grid w-full gap-0.5 text-xs text-slate-300">
<span>Target rover</span> <span>Audio file (mp3 / wav / ogg)</span>
<select <input
className={fieldClass} className={fieldClass}
value={selectedRoverId} type="file"
onChange={(event) => setSelectedRoverId(event.target.value)} accept=".mp3,.wav,.ogg,audio/mpeg,audio/wav,audio/ogg"
disabled={working} disabled={working || !targetRoverId}
> onChange={(event) => setSelectedUpload(event.target.files?.[0] || null)}
<option value="">Select rover</option> />
{roster.map((rover) => (
<option key={rover.id} value={rover.id}>
{rover.name || rover.id}
</option>
))}
</select>
</label> </label>
) : ( {selectedUpload ? (
<div className="surface-muted text-xs text-slate-300 text-center"> <div className="surface-muted mx-auto w-full max-w-sm text-xs text-slate-300 text-center">
Rover: {roster[0]?.name || roster[0]?.id || 'none selected'} {selectedUpload.name} ({selectedUpload.size} bytes)
</div>
) : null}
<div className="flex justify-center gap-0.5">
<button type="button" className="button-dark text-sm" disabled={working || !targetRoverId} onClick={handleUploadPlay}>
{working ? 'Working...' : 'Play Upload'}
</button>
<button type="button" className="button-dark text-sm" disabled={working || !targetRoverId} onClick={handleUploadStop}>
Stop
</button>
</div> </div>
)} {selectedForwardState ? (
<label className="grid gap-0.5 text-xs text-slate-300"> <div className="surface-muted mx-auto w-full max-w-sm text-xs text-slate-300 text-center">
<span>Audio file (mp3 / wav / ogg)</span> state: {selectedForwardState.state || 'idle'}
<input {selectedForwardState.error ? ` | error: ${selectedForwardState.error}` : ''}
className={fieldClass} </div>
type="file" ) : null}
accept=".mp3,.wav,.ogg,audio/mpeg,audio/wav,audio/ogg" {message ? <div className="text-xs text-slate-400 text-center">{message}</div> : null}
disabled={working || !targetRoverId}
onChange={(event) => setSelectedUpload(event.target.files?.[0] || null)}
/>
</label>
{selectedUpload ? (
<div className="surface-muted text-xs text-slate-300 text-center">
{selectedUpload.name} ({selectedUpload.size} bytes)
</div>
) : null}
<div className="flex justify-center gap-0.5">
<button type="button" className="button-dark text-sm" disabled={working || !targetRoverId} onClick={handleUploadPlay}>
{working ? 'Working...' : 'Play Upload'}
</button>
<button type="button" className="button-dark text-sm" disabled={working || !targetRoverId} onClick={handleUploadStop}>
Stop
</button>
</div> </div>
{selectedForwardState ? (
<div className="surface-muted text-xs text-slate-300 text-center">
state: {selectedForwardState.state || 'idle'}
{selectedForwardState.error ? ` | error: ${selectedForwardState.error}` : ''}
</div>
) : null}
</section> </section>
); );
} }