import { Edit, Plus, Trash2, Speaker } from 'lucide-react'; import { useState } from 'react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { apiClient } from '@/lib/api/client'; import { isTauri } from '@/lib/tauri'; import { invoke } from '@tauri-apps/api/core'; interface AudioDevice { id: string; name: string; is_default: boolean; } export function AudioTab() { const [createDialogOpen, setCreateDialogOpen] = useState(false); const [editingChannel, setEditingChannel] = useState(null); const queryClient = useQueryClient(); const { data: channels, isLoading: channelsLoading } = useQuery({ queryKey: ['channels'], queryFn: () => apiClient.listChannels(), }); const { data: devices, isLoading: devicesLoading } = useQuery({ queryKey: ['audio-devices'], queryFn: async () => { if (!isTauri()) { return []; } try { const result = await invoke('list_audio_output_devices'); return result; } catch (error) { console.error('Failed to list audio devices:', error); return []; } }, enabled: isTauri(), }); const { data: profiles } = useQuery({ queryKey: ['profiles'], queryFn: () => apiClient.listProfiles(), }); const createChannel = useMutation({ mutationFn: (data: { name: string; device_ids: string[] }) => apiClient.createChannel(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['channels'] }); setCreateDialogOpen(false); }, }); const updateChannel = useMutation({ mutationFn: ({ channelId, data, }: { channelId: string; data: { name?: string; device_ids?: string[] }; }) => apiClient.updateChannel(channelId, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['channels'] }); queryClient.invalidateQueries({ queryKey: ['profile-channels'] }); setEditingChannel(null); }, }); const deleteChannel = useMutation({ mutationFn: (channelId: string) => apiClient.deleteChannel(channelId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['channels'] }); queryClient.invalidateQueries({ queryKey: ['profile-channels'] }); }, }); const { data: channelVoices } = useQuery({ queryKey: ['channel-voices', editingChannel], queryFn: async () => { if (!editingChannel) return { profile_ids: [] }; return apiClient.getChannelVoices(editingChannel); }, enabled: !!editingChannel, }); const setChannelVoices = useMutation({ mutationFn: ({ channelId, profileIds }: { channelId: string; profileIds: string[] }) => apiClient.setChannelVoices(channelId, profileIds), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['channel-voices'] }); queryClient.invalidateQueries({ queryKey: ['profile-channels'] }); }, }); if (channelsLoading || devicesLoading) { return (
Loading...
); } return (

Audio Channels

{channels?.map((channel) => (
{channel.name} {channel.is_default && ( (Default) )} {!channel.is_default && (
)}
{channel.device_ids.length > 0 ? (
    {channel.device_ids.map((deviceId) => { const device = devices?.find((d) => d.id === deviceId); return (
  • {device?.name || deviceId || 'Default Speakers'}
  • ); })}
) : ( Default Speakers )}
))}

Available Devices

{devices && devices.length > 0 ? ( devices.map((device) => (
{device.name} {device.is_default && ( (default) )}
)) ) : (
{isTauri() ? 'No audio devices found' : 'Audio device selection requires Tauri'}
)}
{/* Create Channel Dialog */} { createChannel.mutate({ name, device_ids: deviceIds }); }} /> {/* Edit Channel Dialog */} {editingChannel && ( !open && setEditingChannel(null)} channel={channels?.find((c) => c.id === editingChannel)!} devices={devices || []} profiles={profiles || []} channelVoices={channelVoices?.profile_ids || []} onUpdate={(name, deviceIds) => { updateChannel.mutate({ channelId: editingChannel, data: { name, device_ids: deviceIds }, }); }} onSetVoices={(profileIds) => { setChannelVoices.mutate({ channelId: editingChannel, profileIds, }); }} /> )}
); } function ChannelVoicesList({ channelId }: { channelId: string }) { const { data: voices } = useQuery({ queryKey: ['channel-voices', channelId], queryFn: () => apiClient.getChannelVoices(channelId), }); const { data: profiles } = useQuery({ queryKey: ['profiles'], queryFn: () => apiClient.listProfiles(), }); const voiceNames = voices?.profile_ids .map((id) => profiles?.find((p) => p.id === id)?.name) .filter(Boolean) || []; return (
{voiceNames.length > 0 ? ( {voiceNames.join(', ')} ) : ( No voices assigned )}
); } interface CreateChannelDialogProps { open: boolean; onOpenChange: (open: boolean) => void; devices: AudioDevice[]; onCreate: (name: string, deviceIds: string[]) => void; } function CreateChannelDialog({ open, onOpenChange, devices, onCreate, }: CreateChannelDialogProps) { const [name, setName] = useState(''); const [selectedDevices, setSelectedDevices] = useState([]); const handleSubmit = () => { if (name.trim()) { onCreate(name.trim(), selectedDevices); setName(''); setSelectedDevices([]); } }; return ( Create Audio Channel Create a new audio channel (bus) to route voices to specific output devices.
setName(e.target.value)} placeholder="e.g., Virtual Cable, Broadcast" />
{selectedDevices.length > 0 && (
{selectedDevices.map((deviceId) => { const device = devices.find((d) => d.id === deviceId); return (
{device?.name || deviceId}
); })}
)}
); } interface EditChannelDialogProps { open: boolean; onOpenChange: (open: boolean) => void; channel: { id: string; name: string; device_ids: string[]; }; devices: AudioDevice[]; profiles: Array<{ id: string; name: string }>; channelVoices: string[]; onUpdate: (name: string, deviceIds: string[]) => void; onSetVoices: (profileIds: string[]) => void; } function EditChannelDialog({ open, onOpenChange, channel, devices, profiles, channelVoices, onUpdate, onSetVoices, }: EditChannelDialogProps) { const [name, setName] = useState(channel.name); const [selectedDevices, setSelectedDevices] = useState(channel.device_ids); const [selectedVoices, setSelectedVoices] = useState(channelVoices); const handleSubmit = () => { if (name.trim()) { onUpdate(name.trim(), selectedDevices); onSetVoices(selectedVoices); } }; return ( Edit Channel Update channel settings and voice assignments.
setName(e.target.value)} />
{selectedDevices.length > 0 && (
{selectedDevices.map((deviceId) => { const device = devices.find((d) => d.id === deviceId); return (
{device?.name || deviceId}
); })}
)}
{selectedVoices.length > 0 && (
{selectedVoices.map((profileId) => { const profile = profiles.find((p) => p.id === profileId); return (
{profile?.name || profileId}
); })}
)}
); }