Skip to content

Commit 21c0723

Browse files
committed
ui
1 parent bdd6ab6 commit 21c0723

File tree

1 file changed

+139
-27
lines changed

1 file changed

+139
-27
lines changed

apps/desktop/src/components/editor-area/note-header/listen-button.tsx

Lines changed: 139 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import { Trans } from "@lingui/react/macro";
22
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";
414
import { useEffect, useState } from "react";
515

616
import SoundIndicator from "@/components/sound-indicator";
@@ -319,16 +329,14 @@ function RecordingControls({
319329

320330
return (
321331
<>
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
324334
isMuted={ongoingSessionMuted.micMuted}
325-
onClick={() => toggleMicMuted.mutate()}
326-
type="mic"
335+
onToggleMuted={() => toggleMicMuted.mutate()}
327336
/>
328-
<AudioControlButton
337+
<SpeakerButton
329338
isMuted={ongoingSessionMuted.speakerMuted}
330339
onClick={() => toggleSpeakerMuted.mutate()}
331-
type="speaker"
332340
/>
333341
</div>
334342

@@ -377,35 +385,139 @@ function RecordingControls({
377385
);
378386
}
379387

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({
382497
isMuted,
383498
onClick,
384499
disabled,
385500
}: {
386-
type: "mic" | "speaker";
387501
isMuted?: boolean;
388502
onClick: () => void;
389503
disabled?: boolean;
390504
}) {
391-
const Icon = type === "mic"
392-
? isMuted
393-
? MicOffIcon
394-
: MicIcon
395-
: isMuted
396-
? VolumeOffIcon
397-
: Volume2Icon;
505+
const Icon = isMuted ? VolumeOffIcon : Volume2Icon;
398506

399507
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>
410522
);
411523
}

0 commit comments

Comments
 (0)