feat(frontend): always-visible Enable audio + Enable mic controls
Surface audio-playback unlock and mic publish as persistent buttons in the pod control row (alongside Test audio / Test PodMan voice / Share screen), replacing the conditional 'Sound is off' banner that only appeared after a refresh. Enable mic calls setMicrophoneEnabled, which triggers the browser permission prompt so users can verify their mic is set up right. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,8 @@ import {
|
|||||||
GitBranchIcon,
|
GitBranchIcon,
|
||||||
ExternalLinkIcon,
|
ExternalLinkIcon,
|
||||||
MessageSquareIcon,
|
MessageSquareIcon,
|
||||||
|
MicIcon,
|
||||||
|
MicOffIcon,
|
||||||
MonitorUpIcon,
|
MonitorUpIcon,
|
||||||
PanelLeftIcon,
|
PanelLeftIcon,
|
||||||
PanelRightIcon,
|
PanelRightIcon,
|
||||||
@@ -17,6 +19,7 @@ import {
|
|||||||
SparklesIcon,
|
SparklesIcon,
|
||||||
TriangleAlertIcon,
|
TriangleAlertIcon,
|
||||||
Volume2Icon,
|
Volume2Icon,
|
||||||
|
VolumeXIcon,
|
||||||
WorkflowIcon,
|
WorkflowIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
@@ -113,6 +116,7 @@ export function PodView({
|
|||||||
const [testingVoice, setTestingVoice] = useState(false);
|
const [testingVoice, setTestingVoice] = useState(false);
|
||||||
const [note, setNote] = useState<string | null>(null);
|
const [note, setNote] = useState<string | null>(null);
|
||||||
const [audioBlocked, setAudioBlocked] = useState(false);
|
const [audioBlocked, setAudioBlocked] = useState(false);
|
||||||
|
const [micOn, setMicOn] = useState(false);
|
||||||
const [leftStreamOpen, setLeftStreamOpen] = useState(() =>
|
const [leftStreamOpen, setLeftStreamOpen] = useState(() =>
|
||||||
readStoredBool('podman.myStreamOpen', true),
|
readStoredBool('podman.myStreamOpen', true),
|
||||||
);
|
);
|
||||||
@@ -168,6 +172,7 @@ export function PodView({
|
|||||||
// are actually heard.
|
// are actually heard.
|
||||||
const onPlaybackChanged = () => setAudioBlocked(!room.canPlaybackAudio);
|
const onPlaybackChanged = () => setAudioBlocked(!room.canPlaybackAudio);
|
||||||
onPlaybackChanged();
|
onPlaybackChanged();
|
||||||
|
setMicOn(room.localParticipant.isMicrophoneEnabled);
|
||||||
|
|
||||||
room
|
room
|
||||||
.on(RoomEvent.ParticipantConnected, refresh)
|
.on(RoomEvent.ParticipantConnected, refresh)
|
||||||
@@ -204,6 +209,20 @@ export function PodView({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Publish/unpublish the local mic. Enabling triggers the browser permission
|
||||||
|
// prompt, so the button doubles as a "is my mic set up right?" check.
|
||||||
|
async function toggleMic() {
|
||||||
|
if (!room) return;
|
||||||
|
setNote(null);
|
||||||
|
try {
|
||||||
|
const next = !micOn;
|
||||||
|
await room.localParticipant.setMicrophoneEnabled(next);
|
||||||
|
setMicOn(next);
|
||||||
|
} catch (e) {
|
||||||
|
setNote(`Could not toggle mic: ${(e as Error).message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
screenTrackRef.current?.stop();
|
screenTrackRef.current?.stop();
|
||||||
@@ -356,7 +375,31 @@ export function PodView({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2 sm:flex sm:items-center">
|
<div className="grid grid-cols-2 gap-2 sm:flex sm:flex-wrap sm:items-center">
|
||||||
|
<Button
|
||||||
|
variant={audioBlocked ? 'default' : 'outline'}
|
||||||
|
onClick={() => void enableSound()}
|
||||||
|
disabled={!room}
|
||||||
|
>
|
||||||
|
{audioBlocked ? (
|
||||||
|
<VolumeXIcon data-icon="inline-start" />
|
||||||
|
) : (
|
||||||
|
<Volume2Icon data-icon="inline-start" />
|
||||||
|
)}
|
||||||
|
{audioBlocked ? 'Enable audio' : 'Audio on'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={micOn ? 'default' : 'outline'}
|
||||||
|
onClick={() => void toggleMic()}
|
||||||
|
disabled={!room}
|
||||||
|
>
|
||||||
|
{micOn ? (
|
||||||
|
<MicIcon data-icon="inline-start" />
|
||||||
|
) : (
|
||||||
|
<MicOffIcon data-icon="inline-start" />
|
||||||
|
)}
|
||||||
|
{micOn ? 'Mic on' : 'Enable mic'}
|
||||||
|
</Button>
|
||||||
<Button variant="outline" onClick={onToggleBeat} disabled={!room}>
|
<Button variant="outline" onClick={onToggleBeat} disabled={!room}>
|
||||||
<Volume2Icon data-icon="inline-start" />
|
<Volume2Icon data-icon="inline-start" />
|
||||||
{beat.on ? (beat.mine ? 'Stop audio' : `Stop (${beat.by})`) : 'Test audio'}
|
{beat.on ? (beat.mine ? 'Stop audio' : `Stop (${beat.by})`) : 'Test audio'}
|
||||||
@@ -399,21 +442,6 @@ export function PodView({
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{room && audioBlocked && (
|
|
||||||
<Alert className="flex items-center justify-between gap-3">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<Volume2Icon />
|
|
||||||
<div>
|
|
||||||
<AlertTitle>Sound is off</AlertTitle>
|
|
||||||
<AlertDescription>Click to hear PodMan's voice alerts.</AlertDescription>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button size="sm" onClick={() => void enableSound()}>
|
|
||||||
Enable sound
|
|
||||||
</Button>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<main className="grid flex-1 gap-5 min-[1800px]:grid-cols-[minmax(0,1fr)_340px]">
|
<main className="grid flex-1 gap-5 min-[1800px]:grid-cols-[minmax(0,1fr)_340px]">
|
||||||
<section className="flex min-w-0 flex-col gap-5">
|
<section className="flex min-w-0 flex-col gap-5">
|
||||||
<Card className="flex-1">
|
<Card className="flex-1">
|
||||||
|
|||||||
Reference in New Issue
Block a user