diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsDevicesDialog.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsDevicesDialog.tsx index c4fb824f..b41e3672 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsDevicesDialog.tsx +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsDevicesDialog.tsx @@ -73,7 +73,7 @@ export function SettingsDeviceDialog() { videoInput: z.string(), }) - const { handleSubmit, control, setValue } = useForm({ + const { handleSubmit, control, setValue, watch } = useForm({ defaultValues: { audioInput: '', audioOutput: '', @@ -82,6 +82,8 @@ export function SettingsDeviceDialog() { resolver: zodResolver(schema), }) + const formValues = watch() + useEffect(() => { initDevices() }, []) @@ -101,10 +103,21 @@ export function SettingsDeviceDialog() { return storeValue || '' } - setValue('audioInput', getEffectiveValue('audioInput')) - setValue('audioOutput', getEffectiveValue('audioOutput')) - setValue('videoInput', getEffectiveValue('videoInput')) - }, [account?.preferredDevices]) + const audioInputValue = getEffectiveValue('audioInput') + const audioOutputValue = getEffectiveValue('audioOutput') + const videoInputValue = getEffectiveValue('videoInput') + + // Only set values if they are actually different to prevent unnecessary re-renders + if (formValues.audioInput !== audioInputValue) { + setValue('audioInput', audioInputValue, { shouldDirty: false }) + } + if (formValues.audioOutput !== audioOutputValue) { + setValue('audioOutput', audioOutputValue, { shouldDirty: false }) + } + if (formValues.videoInput !== videoInputValue) { + setValue('videoInput', videoInputValue, { shouldDirty: false }) + } + }, [account?.preferredDevices, setValue, formValues.audioInput, formValues.audioOutput, formValues.videoInput]) useEffect(() => { const handleStorageChange = (e: StorageEvent) => { @@ -116,7 +129,13 @@ export function SettingsDeviceDialog() { if (changedDeviceType && e.newValue) { console.log(`localStorage changed for ${changedDeviceType}:`, e.newValue) - setValue(changedDeviceType, e.newValue) + try { + const parsed = JSON.parse(e.newValue) + const deviceId = parsed.deviceId || e.newValue + setValue(changedDeviceType, deviceId, { shouldDirty: false }) + } catch { + setValue(changedDeviceType, e.newValue, { shouldDirty: false }) + } } } @@ -126,8 +145,8 @@ export function SettingsDeviceDialog() { const getDeviceById = useCallback( (type: DeviceType, id: string): MediaDeviceInfo | undefined => { - if (!devices) return undefined - return devices[type].find((d) => d.deviceId === id) + if (!devices || !devices[type] || !id) return undefined + return devices[type].find((d) => d && d.deviceId === id) }, [devices], ) @@ -135,21 +154,31 @@ export function SettingsDeviceDialog() { const initDevices = async () => { const devices = await getMediaDevices() Log.info('Available devices:', devices) - setDevices(devices) + if (devices?.audioOutput) { + Log.info('Audio output device IDs:', devices.audioOutput.map(d => ({ id: d.deviceId, label: d.label }))) + } + if (devices) { + setDevices(devices) + } } async function getMediaDevices() { try { + if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { + console.warn('Media devices API not available') + return { audioInput: [], audioOutput: [], videoInput: [] } + } + const devices = await navigator.mediaDevices.enumerateDevices() - const audioInput = devices.filter((device) => device.kind === 'audioinput') - const audioOutput = devices.filter((device) => device.kind === 'audiooutput') - const videoInput = devices.filter((device) => device.kind === 'videoinput') + const audioInput = devices.filter((device) => device && device.kind === 'audioinput') + const audioOutput = devices.filter((device) => device && device.kind === 'audiooutput') + const videoInput = devices.filter((device) => device && device.kind === 'videoinput') return { audioInput, audioOutput, videoInput } } catch (err) { console.error('Error reading audio and video devices:', err) - return null + return { audioInput: [], audioOutput: [], videoInput: [] } } } @@ -197,30 +226,51 @@ export function SettingsDeviceDialog() { {fieldLabels[name]} -
+
{ const selectedDevice = getDeviceById(name, value) + + // If selected device is not found in current devices list, it might have been disconnected + if (value && !selectedDevice && devices?.[name] && devices[name].length > 0) { + console.warn(`[${name}] Selected device ${value} not found in current devices, device may have been disconnected`) + } return ( { + items={devices?.[name]?.map((device) => { + if (!device) return null return ( { - console.log('change device:', device.deviceId) - onChange(device.deviceId) + key={device.deviceId || `device-${Math.random()}`} + onClick={(e) => { + e?.preventDefault() + e?.stopPropagation() + + if (value !== device.deviceId && device.deviceId) { + console.log(`[${name}] change device:`, device.deviceId, 'current:', value) + console.log(`[${name}] device label:`, device.label) + + // Use setTimeout to prevent potential re-render loops and add extra validation + setTimeout(() => { + // Double check the value hasn't changed in the meantime + if (device.deviceId && device.deviceId !== value) { + onChange(device.deviceId) + } + }, 0) + } else { + console.log(`[${name}] clicked same device, ignoring:`, device.deviceId) + } }} >
- {device.label} + {device.label || 'Unknown device'} ) - })} + }).filter(Boolean) || []} className='w-full' > @@ -265,7 +315,7 @@ export function SettingsDeviceDialog() {
) }, - [account?.preferredDevices, devices], + [devices, icons, fieldLabels, control, getDeviceById], ) return ( @@ -281,58 +331,58 @@ export function SettingsDeviceDialog() {
-
- {/* Dialog content */} -
- {/* Title */} -

- {t('TopBar.Preferred devices')} -

- - {/* Form */} -
- {/* Input field with clear button next to it */} - - - - - {/* Inline notification */} - {isDeviceUnavailable && ( - -

{t('Devices.Inline warning message devices')}

-
- )} - {/* Action buttons */} -
- - - -
- +
+ {/* Dialog content */} +
+ {/* Title */} +

+ {t('TopBar.Preferred devices')} +

+ + {/* Form */} +
+ {/* Input field with clear button next to it */} + + + + + {/* Inline notification */} + {isDeviceUnavailable && ( + +

{t('Devices.Inline warning message devices')}

+
+ )} + {/* Action buttons */} +
+ + + +
+ +
-
) diff --git a/src/renderer/src/components/Nethesis/dropdown/DropdownItem.tsx b/src/renderer/src/components/Nethesis/dropdown/DropdownItem.tsx index d50fa687..f215e104 100644 --- a/src/renderer/src/components/Nethesis/dropdown/DropdownItem.tsx +++ b/src/renderer/src/components/Nethesis/dropdown/DropdownItem.tsx @@ -10,7 +10,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { IconDefinition } from '@fortawesome/fontawesome-common-types' export interface DropdownItemProps extends Omit, 'className'> { - onClick?: () => void + onClick?: (e?: React.MouseEvent) => void icon?: IconDefinition centered?: boolean variantTop?: boolean