diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index ee409c794..a13bc0273 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -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", diff --git a/src/renderer/features/player/components/player-config.tsx b/src/renderer/features/player/components/player-config.tsx new file mode 100644 index 000000000..b81b7f942 --- /dev/null +++ b/src/renderer/features/player/components/player-config.tsx @@ -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: ( + setQueueType(value as PlayerQueueType)} + size="sm" + value={queueType} + w="100%" + /> + ), + id: 'queueType', + label: t('player.queueType', { postProcess: 'sentenceCase' }), + }, + + ...(playbackSettings.type === PlayerType.WEB + ? [ + { + component: ( + 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: ( + + ), + id: 'crossfadeDuration', + label: t('setting.crossfadeDuration', { + postProcess: 'sentenceCase', + }), + }, + ] + : []), + + ...(playbackSettings.type === PlayerType.WEB + ? [ + { + component: ( + { + setSettings({ + playback: { + ...playbackSettings, + preservePitch: e.currentTarget.checked, + }, + }); + }} + /> + ), + id: 'preservePitch', + label: t('setting.preservePitch', { + postProcess: 'sentenceCase', + }), + }, + ] + : []), + + { + component: ( + 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 ( + + + { + e.stopPropagation(); + }} + size="sm" + tooltip={{ + label: t('common.setting_other', { postProcess: 'titleCase' }), + openDelay: 0, + }} + variant="subtle" + /> + + + + + + ); +}; diff --git a/src/renderer/features/player/components/right-controls.tsx b/src/renderer/features/player/components/right-controls.tsx index 336a7dfa1..e27a1f2c4 100644 --- a/src/renderer/features/player/components/right-controls.tsx +++ b/src/renderer/features/player/components/right-controls.tsx @@ -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 ( @@ -278,73 +204,7 @@ export const RightControls = () => { )} - - - { - e.stopPropagation(); - }} - size="sm" - tooltip={{ - label: t('player.playbackSpeed', { postProcess: 'sentenceCase' }), - openDelay: 0, - }} - variant="subtle" - /> - - - {playbackType === PlayerType.WEB && ( - - )} - handleSpeed(1)} - step={0.01} - styles={{ - markLabel: { - paddingTop: '0.5rem', - }, - root: { - margin: '1rem 1rem 2rem 1rem', - }, - }} - value={speed} - /> - - + { moveSelectedToNext: state.moveSelectedToNext, moveSelectedToTop: state.moveSelectedToTop, setCrossfadeDuration: state.setCrossfadeDuration, + setQueueType: state.setQueueType, setRepeat: state.setRepeat, setShuffle: state.setShuffle, setSpeed: state.setSpeed,