|
1 | 1 | import { Trans } from "@lingui/react/macro"; |
2 | 2 | import { useMutation, useQuery } from "@tanstack/react-query"; |
3 | | -import { MicIcon, MicOffIcon, PauseIcon, PlayIcon, StopCircleIcon, Volume2Icon, VolumeOffIcon } from "lucide-react"; |
| 3 | +import { |
| 4 | + CheckIcon, |
| 5 | + ChevronDownIcon, |
| 6 | + MicIcon, |
| 7 | + MicOffIcon, |
| 8 | + PauseIcon, |
| 9 | + PlayIcon, |
| 10 | + StopCircleIcon, |
| 11 | + Volume2Icon, |
| 12 | + VolumeOffIcon, |
| 13 | +} from "lucide-react"; |
4 | 14 | import { useEffect, useState } from "react"; |
5 | 15 |
|
6 | 16 | import SoundIndicator from "@/components/sound-indicator"; |
@@ -319,16 +329,14 @@ function RecordingControls({ |
319 | 329 |
|
320 | 330 | return ( |
321 | 331 | <> |
322 | | - <div className="flex w-full justify-between mb-3"> |
323 | | - <AudioControlButton |
| 332 | + <div className="flex gap-2 w-full justify-between mb-3"> |
| 333 | + <MicrophoneSelector |
324 | 334 | isMuted={ongoingSessionMuted.micMuted} |
325 | | - onClick={() => toggleMicMuted.mutate()} |
326 | | - type="mic" |
| 335 | + onToggleMuted={() => toggleMicMuted.mutate()} |
327 | 336 | /> |
328 | | - <AudioControlButton |
| 337 | + <SpeakerButton |
329 | 338 | isMuted={ongoingSessionMuted.speakerMuted} |
330 | 339 | onClick={() => toggleSpeakerMuted.mutate()} |
331 | | - type="speaker" |
332 | 340 | /> |
333 | 341 | </div> |
334 | 342 |
|
@@ -377,35 +385,139 @@ function RecordingControls({ |
377 | 385 | ); |
378 | 386 | } |
379 | 387 |
|
380 | | -function AudioControlButton({ |
381 | | - type, |
| 388 | +function MicrophoneSelector({ |
| 389 | + isMuted, |
| 390 | + onToggleMuted, |
| 391 | + disabled, |
| 392 | +}: { |
| 393 | + isMuted?: boolean; |
| 394 | + onToggleMuted: () => void; |
| 395 | + disabled?: boolean; |
| 396 | +}) { |
| 397 | + const [isOpen, setIsOpen] = useState(false); |
| 398 | + const [selectedDevice, setSelectedDevice] = useState<string>(""); |
| 399 | + |
| 400 | + const { data: devices = [], isLoading } = useQuery({ |
| 401 | + queryKey: ["microphone-devices"], |
| 402 | + queryFn: () => listenerCommands.listMicrophoneDevices(), |
| 403 | + refetchOnWindowFocus: false, |
| 404 | + }); |
| 405 | + |
| 406 | + useEffect(() => { |
| 407 | + if (!selectedDevice && devices.length > 0) { |
| 408 | + setSelectedDevice(devices[0]); |
| 409 | + } |
| 410 | + }, [devices, selectedDevice]); |
| 411 | + |
| 412 | + const Icon = isMuted ? MicOffIcon : MicIcon; |
| 413 | + |
| 414 | + return ( |
| 415 | + <div className="flex-1 min-w-0"> |
| 416 | + <Popover open={isOpen} onOpenChange={setIsOpen}> |
| 417 | + <div className="flex -space-x-px"> |
| 418 | + <Button |
| 419 | + variant="outline" |
| 420 | + className="rounded-r-none flex-1 min-w-0 h-10" |
| 421 | + disabled={disabled} |
| 422 | + onClick={onToggleMuted} |
| 423 | + > |
| 424 | + <Icon |
| 425 | + className={cn( |
| 426 | + "w-4 h-4 flex-shrink-0", |
| 427 | + isMuted ? "text-neutral-500" : "", |
| 428 | + disabled && "text-neutral-300", |
| 429 | + )} |
| 430 | + /> |
| 431 | + {!disabled && <SoundIndicator input="mic" size="long" />} |
| 432 | + </Button> |
| 433 | + |
| 434 | + <PopoverTrigger asChild> |
| 435 | + <Button |
| 436 | + variant="outline" |
| 437 | + className="rounded-l-none px-0.5 flex-shrink-0" |
| 438 | + disabled={disabled} |
| 439 | + > |
| 440 | + <ChevronDownIcon className="w-4 h-4" /> |
| 441 | + </Button> |
| 442 | + </PopoverTrigger> |
| 443 | + </div> |
| 444 | + |
| 445 | + <PopoverContent className="w-64 p-0" align="end"> |
| 446 | + <div className="p-2"> |
| 447 | + <div className="mb-2 px-2"> |
| 448 | + <span className="text-sm font-medium">Microphone</span> |
| 449 | + </div> |
| 450 | + |
| 451 | + {isLoading |
| 452 | + ? ( |
| 453 | + <div className="p-4 text-center"> |
| 454 | + <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-neutral-600 mx-auto"></div> |
| 455 | + <p className="text-sm text-neutral-500 mt-2">Loading devices...</p> |
| 456 | + </div> |
| 457 | + ) |
| 458 | + : devices.length === 0 |
| 459 | + ? ( |
| 460 | + <div className="p-4 text-center"> |
| 461 | + <p className="text-sm text-neutral-500">No microphones found</p> |
| 462 | + </div> |
| 463 | + ) |
| 464 | + : ( |
| 465 | + <div className="space-y-1"> |
| 466 | + {devices.map((device) => { |
| 467 | + const isSelected = device === selectedDevice; |
| 468 | + return ( |
| 469 | + <Button |
| 470 | + key={device} |
| 471 | + variant="ghost" |
| 472 | + className={cn( |
| 473 | + "w-full justify-start text-left h-8 px-2", |
| 474 | + isSelected && "bg-neutral-100", |
| 475 | + )} |
| 476 | + onClick={() => { |
| 477 | + setSelectedDevice(device); |
| 478 | + setIsOpen(false); |
| 479 | + }} |
| 480 | + > |
| 481 | + <Icon className="w-3 h-3 mr-2 flex-shrink-0" /> |
| 482 | + <span className="text-sm truncate flex-1">{device}</span> |
| 483 | + {isSelected && <CheckIcon className="w-3 h-3 ml-auto flex-shrink-0 text-green-600" />} |
| 484 | + </Button> |
| 485 | + ); |
| 486 | + })} |
| 487 | + </div> |
| 488 | + )} |
| 489 | + </div> |
| 490 | + </PopoverContent> |
| 491 | + </Popover> |
| 492 | + </div> |
| 493 | + ); |
| 494 | +} |
| 495 | + |
| 496 | +function SpeakerButton({ |
382 | 497 | isMuted, |
383 | 498 | onClick, |
384 | 499 | disabled, |
385 | 500 | }: { |
386 | | - type: "mic" | "speaker"; |
387 | 501 | isMuted?: boolean; |
388 | 502 | onClick: () => void; |
389 | 503 | disabled?: boolean; |
390 | 504 | }) { |
391 | | - const Icon = type === "mic" |
392 | | - ? isMuted |
393 | | - ? MicOffIcon |
394 | | - : MicIcon |
395 | | - : isMuted |
396 | | - ? VolumeOffIcon |
397 | | - : Volume2Icon; |
| 505 | + const Icon = isMuted ? VolumeOffIcon : Volume2Icon; |
398 | 506 |
|
399 | 507 | return ( |
400 | | - <Button |
401 | | - variant="ghost" |
402 | | - size="icon" |
403 | | - onClick={onClick} |
404 | | - className="w-full" |
405 | | - disabled={disabled} |
406 | | - > |
407 | | - <Icon className={cn(isMuted ? "text-neutral-500" : "", disabled && "text-neutral-300")} size={20} /> |
408 | | - {!disabled && <SoundIndicator input={type} size="long" />} |
409 | | - </Button> |
| 508 | + <div className="flex-1 min-w-0"> |
| 509 | + <Button |
| 510 | + variant="outline" |
| 511 | + onClick={onClick} |
| 512 | + className="w-full h-10" |
| 513 | + disabled={disabled} |
| 514 | + > |
| 515 | + <Icon |
| 516 | + className={cn("flex-shrink-0", isMuted ? "text-neutral-500" : "", disabled && "text-neutral-300")} |
| 517 | + size={16} |
| 518 | + /> |
| 519 | + {!disabled && <SoundIndicator input="speaker" size="long" />} |
| 520 | + </Button> |
| 521 | + </div> |
410 | 522 | ); |
411 | 523 | } |
0 commit comments