diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index cf5a4b9f1..9332059e2 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -784,6 +784,7 @@ "artistConfiguration_description": "Configure what items are shown, and in what order, on the album artist page", "artistReleaseTypeConfiguration": "Artist release type configuration", "artistReleaseTypeConfiguration_description": "Configure what release types are shown, and in what order, on the album artist page", + "audioDeviceDefault": "System default", "audioDevice_description": "Select the audio device to use for playback", "audioDevice": "Audio device", "audioExclusiveMode_description": "Enable exclusive output mode. In this mode, the system is usually locked out, and only mpv will be able to output audio. Visualizer system audio capture will not work while this is enabled", diff --git a/src/renderer/features/player/components/right-controls.tsx b/src/renderer/features/player/components/right-controls.tsx index 555da28a2..998d64467 100644 --- a/src/renderer/features/player/components/right-controls.tsx +++ b/src/renderer/features/player/components/right-controls.tsx @@ -7,6 +7,7 @@ import { PlayerConfig } from '/@/renderer/features/player/components/player-conf import { CustomPlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider'; import { SleepTimerButton } from '/@/renderer/features/player/components/sleep-timer-button'; import { usePlayer } from '/@/renderer/features/player/context/player-context'; +import { useAudioDevices } from '/@/renderer/features/settings/components/playback/audio-settings'; import { useSetRating } from '/@/renderer/features/shared/hooks/use-set-rating'; import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation'; import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation'; @@ -21,6 +22,8 @@ import { useFullScreenPlayerStore, useGeneralSettings, useHotkeySettings, + usePlaybackSettings, + usePlaybackType, usePlayerData, usePlayerMuted, usePlayerSong, @@ -35,6 +38,7 @@ import { import { useFullScreenPlayerStoreActions } from '/@/renderer/store/full-screen-player.store'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { Button } from '/@/shared/components/button/button'; +import { ContextMenu } from '/@/shared/components/context-menu/context-menu'; import { Flex } from '/@/shared/components/flex/flex'; import { Group } from '/@/shared/components/group/group'; import { NumberInput } from '/@/shared/components/number-input/number-input'; @@ -50,6 +54,7 @@ import { useMediaQuery } from '/@/shared/hooks/use-media-query'; import { useThrottledCallback } from '/@/shared/hooks/use-throttled-callback'; import { useThrottledValue } from '/@/shared/hooks/use-throttled-value'; import { LibraryItem, QueueSong, ServerType } from '/@/shared/types/domain-types'; +import { PlayerType } from '/@/shared/types/types'; const calculateVolumeUp = (volume: number, volumeWheelStep: number) => { let volumeToSet: number; @@ -506,6 +511,28 @@ const VolumeButton = () => { const { decreaseVolume, increaseVolume, mediaToggleMute, setVolume } = usePlayer(); const isMinWidth = useMediaQuery('(max-width: 480px)'); + const playbackType = usePlaybackType(); + const playbackSettings = usePlaybackSettings(); + const { setSettings } = useSettingsStoreActions(); + const audioDevices = useAudioDevices(playbackType); + + const currentAudioDeviceId = + playbackType === PlayerType.LOCAL + ? playbackSettings.mpvAudioDeviceId + : playbackSettings.audioDeviceId; + + const handleSelectAudioDevice = useCallback( + (deviceId: null | string) => { + setSettings({ + playback: + playbackType === PlayerType.LOCAL + ? { mpvAudioDeviceId: deviceId } + : { audioDeviceId: deviceId }, + }); + }, + [playbackType, setSettings], + ); + const [sliderValue, setSliderValue] = useState(volume); const throttledVolume = useThrottledValue(sliderValue, 100); @@ -561,24 +588,54 @@ const VolumeButton = () => { return ( <> - 50 ? 'volumeMax' : 'volumeNormal'} - iconProps={{ - color: muted ? 'muted' : undefined, - size: 'xl', - }} - onClick={(e) => { - e.stopPropagation(); - handleMute(); - }} - onWheel={handleVolumeWheel} - size="sm" - tooltip={{ - label: muted ? t('player.muted') : volume, - openDelay: 0, - }} - variant="subtle" - /> + + + {/* + * ActionIcon renders a Mantine Tooltip wrapper, which does not + * forward the onContextMenu/ref that Radix injects via asChild to + * the underlying button. Wrap in a real DOM node so right-click + * reliably opens the menu. + */} +
+ 50 ? 'volumeMax' : 'volumeNormal'} + iconProps={{ + color: muted ? 'muted' : undefined, + size: 'xl', + }} + onClick={(e) => { + e.stopPropagation(); + handleMute(); + }} + onWheel={handleVolumeWheel} + size="sm" + tooltip={{ + label: muted ? t('player.muted') : volume, + openDelay: 0, + }} + variant="subtle" + /> +
+
+ + handleSelectAudioDevice(null)} + > + {t('setting.audioDeviceDefault', { defaultValue: 'System default' })} + + {audioDevices.length > 0 && } + {audioDevices.map((device) => ( + handleSelectAudioDevice(device.value)} + > + {device.label || device.value} + + ))} + +
{!isMinWidth ? (