mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-11 21:19:59 +02:00
feat(player): right-click volume icon to switch audio output device (#2189)
* feat(player): add right-click audio output device menu to volume icon Right-clicking the volume/speaker icon in the player bar now opens a context menu listing available audio output devices, with the active device checkmarked and a System default reset option. Selecting a device writes to the existing audioDeviceId (web) or mpvAudioDeviceId (mpv) playback settings, so the choice is applied live and persisted across restarts via the settings store. Unlike the settings-page selector, the menu is not disabled during playback. * refactor(player): drop redundant audio menu label, document wrapper div Address review feedback: remove the self-explanatory ContextMenu.Label from the audio output menu, and add a comment explaining why the volume icon is wrapped in a div (Mantine Tooltip does not forward Radix's asChild onContextMenu/ref to the button).
This commit is contained in:
@@ -784,6 +784,7 @@
|
|||||||
"artistConfiguration_description": "Configure what items are shown, and in what order, on the album artist page",
|
"artistConfiguration_description": "Configure what items are shown, and in what order, on the album artist page",
|
||||||
"artistReleaseTypeConfiguration": "Artist release type configuration",
|
"artistReleaseTypeConfiguration": "Artist release type configuration",
|
||||||
"artistReleaseTypeConfiguration_description": "Configure what release types are shown, and in what order, on the album artist page",
|
"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_description": "Select the audio device to use for playback",
|
||||||
"audioDevice": "Audio device",
|
"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",
|
"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",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { PlayerConfig } from '/@/renderer/features/player/components/player-conf
|
|||||||
import { CustomPlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider';
|
import { CustomPlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider';
|
||||||
import { SleepTimerButton } from '/@/renderer/features/player/components/sleep-timer-button';
|
import { SleepTimerButton } from '/@/renderer/features/player/components/sleep-timer-button';
|
||||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
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 { useSetRating } from '/@/renderer/features/shared/hooks/use-set-rating';
|
||||||
import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
|
import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
|
||||||
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
|
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
|
||||||
@@ -21,6 +22,8 @@ import {
|
|||||||
useFullScreenPlayerStore,
|
useFullScreenPlayerStore,
|
||||||
useGeneralSettings,
|
useGeneralSettings,
|
||||||
useHotkeySettings,
|
useHotkeySettings,
|
||||||
|
usePlaybackSettings,
|
||||||
|
usePlaybackType,
|
||||||
usePlayerData,
|
usePlayerData,
|
||||||
usePlayerMuted,
|
usePlayerMuted,
|
||||||
usePlayerSong,
|
usePlayerSong,
|
||||||
@@ -35,6 +38,7 @@ import {
|
|||||||
import { useFullScreenPlayerStoreActions } from '/@/renderer/store/full-screen-player.store';
|
import { useFullScreenPlayerStoreActions } from '/@/renderer/store/full-screen-player.store';
|
||||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { Button } from '/@/shared/components/button/button';
|
import { Button } from '/@/shared/components/button/button';
|
||||||
|
import { ContextMenu } from '/@/shared/components/context-menu/context-menu';
|
||||||
import { Flex } from '/@/shared/components/flex/flex';
|
import { Flex } from '/@/shared/components/flex/flex';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
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 { useThrottledCallback } from '/@/shared/hooks/use-throttled-callback';
|
||||||
import { useThrottledValue } from '/@/shared/hooks/use-throttled-value';
|
import { useThrottledValue } from '/@/shared/hooks/use-throttled-value';
|
||||||
import { LibraryItem, QueueSong, ServerType } from '/@/shared/types/domain-types';
|
import { LibraryItem, QueueSong, ServerType } from '/@/shared/types/domain-types';
|
||||||
|
import { PlayerType } from '/@/shared/types/types';
|
||||||
|
|
||||||
const calculateVolumeUp = (volume: number, volumeWheelStep: number) => {
|
const calculateVolumeUp = (volume: number, volumeWheelStep: number) => {
|
||||||
let volumeToSet: number;
|
let volumeToSet: number;
|
||||||
@@ -506,6 +511,28 @@ const VolumeButton = () => {
|
|||||||
const { decreaseVolume, increaseVolume, mediaToggleMute, setVolume } = usePlayer();
|
const { decreaseVolume, increaseVolume, mediaToggleMute, setVolume } = usePlayer();
|
||||||
const isMinWidth = useMediaQuery('(max-width: 480px)');
|
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 [sliderValue, setSliderValue] = useState(volume);
|
||||||
|
|
||||||
const throttledVolume = useThrottledValue(sliderValue, 100);
|
const throttledVolume = useThrottledValue(sliderValue, 100);
|
||||||
@@ -561,24 +588,54 @@ const VolumeButton = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ActionIcon
|
<ContextMenu>
|
||||||
icon={muted ? 'volumeMute' : volume > 50 ? 'volumeMax' : 'volumeNormal'}
|
<ContextMenu.Target>
|
||||||
iconProps={{
|
{/*
|
||||||
color: muted ? 'muted' : undefined,
|
* ActionIcon renders a Mantine Tooltip wrapper, which does not
|
||||||
size: 'xl',
|
* forward the onContextMenu/ref that Radix injects via asChild to
|
||||||
}}
|
* the underlying button. Wrap in a real DOM node so right-click
|
||||||
onClick={(e) => {
|
* reliably opens the menu.
|
||||||
e.stopPropagation();
|
*/}
|
||||||
handleMute();
|
<div style={{ alignItems: 'center', display: 'flex' }}>
|
||||||
}}
|
<ActionIcon
|
||||||
onWheel={handleVolumeWheel}
|
icon={muted ? 'volumeMute' : volume > 50 ? 'volumeMax' : 'volumeNormal'}
|
||||||
size="sm"
|
iconProps={{
|
||||||
tooltip={{
|
color: muted ? 'muted' : undefined,
|
||||||
label: muted ? t('player.muted') : volume,
|
size: 'xl',
|
||||||
openDelay: 0,
|
}}
|
||||||
}}
|
onClick={(e) => {
|
||||||
variant="subtle"
|
e.stopPropagation();
|
||||||
/>
|
handleMute();
|
||||||
|
}}
|
||||||
|
onWheel={handleVolumeWheel}
|
||||||
|
size="sm"
|
||||||
|
tooltip={{
|
||||||
|
label: muted ? t('player.muted') : volume,
|
||||||
|
openDelay: 0,
|
||||||
|
}}
|
||||||
|
variant="subtle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ContextMenu.Target>
|
||||||
|
<ContextMenu.Content>
|
||||||
|
<ContextMenu.Item
|
||||||
|
isSelected={!currentAudioDeviceId}
|
||||||
|
onSelect={() => handleSelectAudioDevice(null)}
|
||||||
|
>
|
||||||
|
{t('setting.audioDeviceDefault', { defaultValue: 'System default' })}
|
||||||
|
</ContextMenu.Item>
|
||||||
|
{audioDevices.length > 0 && <ContextMenu.Divider />}
|
||||||
|
{audioDevices.map((device) => (
|
||||||
|
<ContextMenu.Item
|
||||||
|
isSelected={device.value === currentAudioDeviceId}
|
||||||
|
key={device.value}
|
||||||
|
onSelect={() => handleSelectAudioDevice(device.value)}
|
||||||
|
>
|
||||||
|
{device.label || device.value}
|
||||||
|
</ContextMenu.Item>
|
||||||
|
))}
|
||||||
|
</ContextMenu.Content>
|
||||||
|
</ContextMenu>
|
||||||
{!isMinWidth ? (
|
{!isMinWidth ? (
|
||||||
<CustomPlayerbarSlider
|
<CustomPlayerbarSlider
|
||||||
max={100}
|
max={100}
|
||||||
|
|||||||
Reference in New Issue
Block a user