mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
add new player config popover
This commit is contained in:
@@ -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={{
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user