mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-19 00:46:35 +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:
@@ -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 (
|
||||
<>
|
||||
<ActionIcon
|
||||
icon={muted ? 'volumeMute' : volume > 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"
|
||||
/>
|
||||
<ContextMenu>
|
||||
<ContextMenu.Target>
|
||||
{/*
|
||||
* 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.
|
||||
*/}
|
||||
<div style={{ alignItems: 'center', display: 'flex' }}>
|
||||
<ActionIcon
|
||||
icon={muted ? 'volumeMute' : volume > 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"
|
||||
/>
|
||||
</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 ? (
|
||||
<CustomPlayerbarSlider
|
||||
max={100}
|
||||
|
||||
Reference in New Issue
Block a user