add new player config popover

This commit is contained in:
jeffvli
2025-11-17 13:07:53 -08:00
parent 60c7a4a9a1
commit 9113756923
4 changed files with 240 additions and 147 deletions
+3
View File
@@ -493,6 +493,9 @@
"queue_moveToBottom": "move selected to top",
"queue_moveToTop": "move selected to bottom",
"queue_remove": "remove selected",
"queueType": "queue type",
"queueType_default": "default",
"queueType_priority": "priority",
"repeat": "repeat",
"repeat_all": "repeat all",
"repeat_off": "repeat disabled",
@@ -0,0 +1,229 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { ListConfigTable } from '/@/renderer/features/shared/components/list-config-menu';
import {
usePlayerActions,
usePlayerData,
usePlayerProperties,
usePlayerQueueType,
usePlayerSpeed,
} from '/@/renderer/store';
import {
usePlaybackSettings,
useSettingsStore,
useSettingsStoreActions,
} from '/@/renderer/store/settings.store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Popover } from '/@/shared/components/popover/popover';
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
import { Slider } from '/@/shared/components/slider/slider';
import { Switch } from '/@/shared/components/switch/switch';
import { PlayerQueueType, PlayerStyle, PlayerType } from '/@/shared/types/types';
export const PlayerConfig = () => {
const { t } = useTranslation();
const { currentSong } = usePlayerData();
const speed = usePlayerSpeed();
const queueType = usePlayerQueueType();
const { crossfadeDuration, transitionType } = usePlayerProperties();
const { setCrossfadeDuration, setQueueType, setSpeed, setTransitionType } = usePlayerActions();
const playbackSettings = usePlaybackSettings();
const { setSettings } = useSettingsStoreActions();
const speedPreservePitch = useSettingsStore((state) => state.playback.preservePitch);
const options = useMemo(() => {
const formatPlaybackSpeedSliderLabel = (value: number) => {
const bpm = Number(currentSong?.bpm);
if (bpm > 0) {
return `${value} x / ${(bpm * value).toFixed(1)} BPM`;
}
return `${value} x`;
};
const allOptions = [
{
component: (
<SegmentedControl
data={[
{
label: t('player.queueType_default', { postProcess: 'titleCase' }),
value: PlayerQueueType.DEFAULT,
},
{
label: t('player.queueType_priority', { postProcess: 'titleCase' }),
value: PlayerQueueType.PRIORITY,
},
]}
onChange={(value) => setQueueType(value as PlayerQueueType)}
size="sm"
value={queueType}
w="100%"
/>
),
id: 'queueType',
label: t('player.queueType', { postProcess: 'sentenceCase' }),
},
...(playbackSettings.type === PlayerType.WEB
? [
{
component: (
<SegmentedControl
data={[
{
label: t('setting.playbackStyle', {
context: 'optionNormal',
postProcess: 'titleCase',
}),
value: PlayerStyle.GAPLESS,
},
{
label: t('setting.playbackStyle', {
context: 'optionCrossFade',
postProcess: 'titleCase',
}),
value: PlayerStyle.CROSSFADE,
},
]}
onChange={(value) => setTransitionType(value as PlayerStyle)}
size="sm"
value={transitionType}
w="100%"
/>
),
id: 'transitionType',
label: t('setting.playbackStyle', {
postProcess: 'sentenceCase',
}),
},
]
: []),
...(playbackSettings.type === PlayerType.WEB && transitionType === PlayerStyle.CROSSFADE
? [
{
component: (
<Slider
defaultValue={crossfadeDuration}
marks={[
{ label: '3', value: 3 },
{ label: '6', value: 6 },
{ label: '9', value: 9 },
{ label: '12', value: 12 },
{ label: '15', value: 15 },
]}
max={15}
min={3}
onChangeEnd={setCrossfadeDuration}
styles={{
root: {},
}}
w="100%"
/>
),
id: 'crossfadeDuration',
label: t('setting.crossfadeDuration', {
postProcess: 'sentenceCase',
}),
},
]
: []),
...(playbackSettings.type === PlayerType.WEB
? [
{
component: (
<Switch
defaultChecked={speedPreservePitch}
onChange={(e) => {
setSettings({
playback: {
...playbackSettings,
preservePitch: e.currentTarget.checked,
},
});
}}
/>
),
id: 'preservePitch',
label: t('setting.preservePitch', {
postProcess: 'sentenceCase',
}),
},
]
: []),
{
component: (
<Slider
label={formatPlaybackSpeedSliderLabel}
marks={[
{ label: '0.5', value: 0.5 },
{ label: '0.75', value: 0.75 },
{ label: '1', value: 1 },
{ label: '1.25', value: 1.25 },
{ label: '1.5', value: 1.5 },
{ label: '1.75', value: 1.75 },
{ label: '2', value: 2 },
]}
max={2}
min={0.5}
onChange={setSpeed}
onDoubleClick={() => setSpeed(1)}
step={0.01}
styles={{
markLabel: {},
root: {},
}}
value={speed}
w="100%"
/>
),
id: 'playbackSpeed',
label: t('player.playbackSpeed', { postProcess: 'sentenceCase' }),
},
];
return allOptions;
}, [
playbackSettings,
speedPreservePitch,
setSettings,
currentSong,
speed,
setSpeed,
queueType,
setQueueType,
transitionType,
setTransitionType,
crossfadeDuration,
setCrossfadeDuration,
t,
]);
return (
<Popover position="top-end" width={500} withArrow>
<Popover.Target>
<ActionIcon
icon="mediaSpeed"
iconProps={{
size: 'lg',
}}
onClick={(e) => {
e.stopPropagation();
}}
size="sm"
tooltip={{
label: t('common.setting_other', { postProcess: 'titleCase' }),
openDelay: 0,
}}
variant="subtle"
/>
</Popover.Target>
<Popover.Dropdown p="md">
<ListConfigTable options={options} />
</Popover.Dropdown>
</Popover>
);
};
@@ -1,8 +1,8 @@
import { useHotkeys, useMediaQuery } from '@mantine/hooks';
import isElectron from 'is-electron';
import { useCallback, useEffect, WheelEvent } from 'react';
import { useCallback, WheelEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { PlayerConfig } from '/@/renderer/features/player/components/player-config';
import { CustomPlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider';
import { usePlayer } from '/@/renderer/features/player/context/player-context';
import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
@@ -13,29 +13,17 @@ import {
useCurrentServer,
useGeneralSettings,
useHotkeySettings,
usePlaybackSettings,
usePlaybackType,
usePlayerData,
usePlayerMuted,
usePlayerSpeed,
usePlayerVolume,
useSettingsStore,
useSettingsStoreActions,
useSidebarStore,
} from '/@/renderer/store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
import { Flex } from '/@/shared/components/flex/flex';
import { Group } from '/@/shared/components/group/group';
import { Option } from '/@/shared/components/option/option';
import { Rating } from '/@/shared/components/rating/rating';
import { Slider } from '/@/shared/components/slider/slider';
import { Switch } from '/@/shared/components/switch/switch';
import { LibraryItem, QueueSong, ServerType, Song } from '/@/shared/types/domain-types';
import { PlayerType } from '/@/shared/types/types';
const ipc = isElectron() ? window.api.ipc : null;
const remote = isElectron() ? window.api.remote : null;
import { LibraryItem, QueueSong, ServerType } from '/@/shared/types/domain-types';
const calculateVolumeUp = (volume: number, volumeWheelStep: number) => {
let volumeToSet;
@@ -71,14 +59,9 @@ export const RightControls = () => {
const { setSideBar } = useAppStoreActions();
const { rightExpanded: isQueueExpanded } = useSidebarStore();
const { bindings } = useHotkeySettings();
const { setSettings } = useSettingsStoreActions();
const playbackSettings = usePlaybackSettings();
const playbackType = usePlaybackType();
const { volumeWheelStep } = useGeneralSettings();
const speed = usePlayerSpeed();
const volumeWidth = useSettingsStore((state) => state.general.volumeWidth);
const speedPreservePitch = useSettingsStore((state) => state.playback.preservePitch);
const { mediaToggleMute, setSpeed, setVolume } = usePlayer();
const { mediaToggleMute, setVolume } = usePlayer();
const updateRatingMutation = useSetRating({});
const addToFavoritesMutation = useCreateFavorite({});
const removeFromFavoritesMutation = useDeleteFavorite({});
@@ -101,8 +84,9 @@ export const RightControls = () => {
updateRatingMutation.mutate({
apiClientProps: { serverId: currentSong?._serverId || '' },
query: {
item: [currentSong],
id: [currentSong.id],
rating,
type: LibraryItem.SONG,
},
});
};
@@ -141,13 +125,6 @@ export const RightControls = () => {
mediaToggleMute();
}, [mediaToggleMute]);
const handleSpeed = useCallback(
(e: number) => {
setSpeed(e);
},
[setSpeed],
);
const handleVolumeSlider = useCallback(
(e: number) => {
setVolume(e);
@@ -173,14 +150,6 @@ export const RightControls = () => {
setSideBar({ rightExpanded: !isQueueExpanded });
};
const formatPlaybackSpeedSliderLabel = (value: number) => {
const bpm = Number(currentSong?.bpm);
if (bpm > 0) {
return `${value} x / ${(bpm * value).toFixed(1)} BPM`;
}
return `${value} x`;
};
const isSongDefined = Boolean(currentSong?.id);
const showRating =
isSongDefined &&
@@ -223,49 +192,6 @@ export const RightControls = () => {
[bindings.rate5.isGlobal ? '' : bindings.rate5.hotkey, () => handleUpdateRating(5)],
]);
useEffect(() => {
if (remote) {
remote.requestFavorite((_event, { favorite, id, serverId }) => {
const mutator = favorite ? addToFavoritesMutation : removeFromFavoritesMutation;
mutator.mutate({
apiClientProps: { serverId },
query: {
id: [id],
type: LibraryItem.SONG,
},
});
});
remote.requestRating((_event, { id, rating, serverId }) => {
updateRatingMutation.mutate({
apiClientProps: { serverId },
query: {
item: [
{
_serverId: currentSong?._serverId || '',
id,
itemType: LibraryItem.SONG,
} as Song, // This is not a type-safe cast, but it works because those are all the prop
],
rating,
},
});
});
return () => {
ipc?.removeAllListeners('request-favorite');
ipc?.removeAllListeners('request-rating');
};
}
return () => {};
}, [
addToFavoritesMutation,
currentSong?._serverId,
removeFromFavoritesMutation,
updateRatingMutation,
]);
return (
<Flex align="flex-end" direction="column" h="100%" px="1rem" py="0.5rem">
<Group h="calc(100% / 3)">
@@ -278,73 +204,7 @@ export const RightControls = () => {
)}
</Group>
<Group align="center" gap="xs" wrap="nowrap">
<DropdownMenu arrowOffset={12} offset={0} position="top-end" width={425} withArrow>
<DropdownMenu.Target>
<ActionIcon
icon="mediaSpeed"
iconProps={{
size: 'lg',
}}
onClick={(e) => {
e.stopPropagation();
}}
size="sm"
tooltip={{
label: t('player.playbackSpeed', { postProcess: 'sentenceCase' }),
openDelay: 0,
}}
variant="subtle"
/>
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
{playbackType === PlayerType.WEB && (
<Option>
<Option.Label>
{t('setting.preservePitch', {
postProcess: 'sentenceCase',
})}
</Option.Label>
<Option.Control>
<Switch
defaultChecked={speedPreservePitch}
onChange={(e) => {
setSettings({
playback: {
...playbackSettings,
preservePitch: e.currentTarget.checked,
},
});
}}
/>
</Option.Control>
</Option>
)}
<Slider
label={formatPlaybackSpeedSliderLabel}
marks={[
{ label: '0.5', value: 0.5 },
{ label: '0.75', value: 0.75 },
{ label: '1', value: 1 },
{ label: '1.25', value: 1.25 },
{ label: '1.5', value: 1.5 },
]}
max={1.5}
min={0.5}
onChange={handleSpeed}
onDoubleClick={() => handleSpeed(1)}
step={0.01}
styles={{
markLabel: {
paddingTop: '0.5rem',
},
root: {
margin: '1rem 1rem 2rem 1rem',
},
}}
value={speed}
/>
</DropdownMenu.Dropdown>
</DropdownMenu>
<PlayerConfig />
<ActionIcon
icon="favorite"
iconProps={{
+1
View File
@@ -1218,6 +1218,7 @@ export const usePlayerActions = () => {
moveSelectedToNext: state.moveSelectedToNext,
moveSelectedToTop: state.moveSelectedToTop,
setCrossfadeDuration: state.setCrossfadeDuration,
setQueueType: state.setQueueType,
setRepeat: state.setRepeat,
setShuffle: state.setShuffle,
setSpeed: state.setSpeed,