mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +02:00
crossfade player enhancements, reorganize settings
This commit is contained in:
@@ -17,7 +17,7 @@ import {
|
|||||||
usePlayerProperties,
|
usePlayerProperties,
|
||||||
usePlayerVolume,
|
usePlayerVolume,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { PlayerStatus, PlayerStyle } from '/@/shared/types/types';
|
import { CrossfadeStyle, PlayerStatus, PlayerStyle } from '/@/shared/types/types';
|
||||||
|
|
||||||
const PLAY_PAUSE_FADE_DURATION = 300;
|
const PLAY_PAUSE_FADE_DURATION = 300;
|
||||||
const PLAY_PAUSE_FADE_INTERVAL = 10;
|
const PLAY_PAUSE_FADE_INTERVAL = 10;
|
||||||
@@ -26,7 +26,7 @@ export function WebPlayer() {
|
|||||||
const playerRef = useRef<null | WebPlayerEngineHandle>(null);
|
const playerRef = useRef<null | WebPlayerEngineHandle>(null);
|
||||||
const { num, player1, player2, status } = usePlayerData();
|
const { num, player1, player2, status } = usePlayerData();
|
||||||
const { mediaAutoNext, setTimestamp } = usePlayerActions();
|
const { mediaAutoNext, setTimestamp } = usePlayerActions();
|
||||||
const { crossfadeDuration, speed, transitionType } = usePlayerProperties();
|
const { crossfadeDuration, crossfadeStyle, speed, transitionType } = usePlayerProperties();
|
||||||
const isMuted = usePlayerMuted();
|
const isMuted = usePlayerMuted();
|
||||||
const volume = usePlayerVolume();
|
const volume = usePlayerVolume();
|
||||||
|
|
||||||
@@ -79,6 +79,7 @@ export function WebPlayer() {
|
|||||||
case PlayerStyle.CROSSFADE:
|
case PlayerStyle.CROSSFADE:
|
||||||
crossfadeHandler({
|
crossfadeHandler({
|
||||||
crossfadeDuration: crossfadeDuration,
|
crossfadeDuration: crossfadeDuration,
|
||||||
|
crossfadeStyle,
|
||||||
currentPlayer: playerRef.current.player1(),
|
currentPlayer: playerRef.current.player1(),
|
||||||
currentPlayerNum: num,
|
currentPlayerNum: num,
|
||||||
currentTime: e.playedSeconds,
|
currentTime: e.playedSeconds,
|
||||||
@@ -102,7 +103,7 @@ export function WebPlayer() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[crossfadeDuration, isTransitioning, num, transitionType, volume],
|
[crossfadeDuration, crossfadeStyle, isTransitioning, num, transitionType, volume],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onProgressPlayer2 = useCallback(
|
const onProgressPlayer2 = useCallback(
|
||||||
@@ -115,6 +116,7 @@ export function WebPlayer() {
|
|||||||
case PlayerStyle.CROSSFADE:
|
case PlayerStyle.CROSSFADE:
|
||||||
crossfadeHandler({
|
crossfadeHandler({
|
||||||
crossfadeDuration: crossfadeDuration,
|
crossfadeDuration: crossfadeDuration,
|
||||||
|
crossfadeStyle,
|
||||||
currentPlayer: playerRef.current.player2(),
|
currentPlayer: playerRef.current.player2(),
|
||||||
currentPlayerNum: num,
|
currentPlayerNum: num,
|
||||||
currentTime: e.playedSeconds,
|
currentTime: e.playedSeconds,
|
||||||
@@ -138,7 +140,7 @@ export function WebPlayer() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[crossfadeDuration, isTransitioning, num, transitionType, volume],
|
[crossfadeDuration, crossfadeStyle, isTransitioning, num, transitionType, volume],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOnEndedPlayer1 = useCallback(() => {
|
const handleOnEndedPlayer1 = useCallback(() => {
|
||||||
@@ -171,6 +173,22 @@ export function WebPlayer() {
|
|||||||
{
|
{
|
||||||
onPlayerSeekToTimestamp: (properties) => {
|
onPlayerSeekToTimestamp: (properties) => {
|
||||||
const timestamp = properties.timestamp;
|
const timestamp = properties.timestamp;
|
||||||
|
|
||||||
|
// Reset transition state if seeking during a crossfade transition
|
||||||
|
if (isTransitioning && transitionType === PlayerStyle.CROSSFADE) {
|
||||||
|
setIsTransitioning(false);
|
||||||
|
|
||||||
|
if (num === 1) {
|
||||||
|
playerRef.current?.player1()?.setVolume(volume);
|
||||||
|
playerRef.current?.player2()?.setVolume(0);
|
||||||
|
playerRef.current?.player2()?.ref?.getInternalPlayer()?.pause();
|
||||||
|
} else {
|
||||||
|
playerRef.current?.player2()?.setVolume(volume);
|
||||||
|
playerRef.current?.player1()?.setVolume(0);
|
||||||
|
playerRef.current?.player1()?.ref?.getInternalPlayer()?.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (num === 1) {
|
if (num === 1) {
|
||||||
playerRef.current?.player1()?.ref?.seekTo(timestamp);
|
playerRef.current?.player1()?.ref?.seekTo(timestamp);
|
||||||
} else {
|
} else {
|
||||||
@@ -179,6 +197,26 @@ export function WebPlayer() {
|
|||||||
},
|
},
|
||||||
onPlayerStatus: async (properties) => {
|
onPlayerStatus: async (properties) => {
|
||||||
const status = properties.status;
|
const status = properties.status;
|
||||||
|
|
||||||
|
// Reset crossfade transition if paused during a crossfade transition
|
||||||
|
if (
|
||||||
|
status === PlayerStatus.PAUSED &&
|
||||||
|
isTransitioning &&
|
||||||
|
transitionType === PlayerStyle.CROSSFADE
|
||||||
|
) {
|
||||||
|
setIsTransitioning(false);
|
||||||
|
|
||||||
|
if (num === 1) {
|
||||||
|
playerRef.current?.player1()?.setVolume(volume);
|
||||||
|
playerRef.current?.player2()?.setVolume(0);
|
||||||
|
playerRef.current?.player2()?.ref?.getInternalPlayer()?.pause();
|
||||||
|
} else {
|
||||||
|
playerRef.current?.player2()?.setVolume(volume);
|
||||||
|
playerRef.current?.player1()?.setVolume(0);
|
||||||
|
playerRef.current?.player1()?.ref?.getInternalPlayer()?.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (status === PlayerStatus.PAUSED) {
|
if (status === PlayerStatus.PAUSED) {
|
||||||
fadeAndSetStatus(volume, 0, PLAY_PAUSE_FADE_DURATION, PlayerStatus.PAUSED);
|
fadeAndSetStatus(volume, 0, PLAY_PAUSE_FADE_DURATION, PlayerStatus.PAUSED);
|
||||||
} else if (status === PlayerStatus.PLAYING) {
|
} else if (status === PlayerStatus.PLAYING) {
|
||||||
@@ -190,7 +228,7 @@ export function WebPlayer() {
|
|||||||
playerRef.current?.setVolume(volume);
|
playerRef.current?.setVolume(volume);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[volume, num, isTransitioning],
|
[volume, num, isTransitioning, transitionType],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -244,6 +282,7 @@ export function WebPlayer() {
|
|||||||
|
|
||||||
function crossfadeHandler(args: {
|
function crossfadeHandler(args: {
|
||||||
crossfadeDuration: number;
|
crossfadeDuration: number;
|
||||||
|
crossfadeStyle: CrossfadeStyle;
|
||||||
currentPlayer: {
|
currentPlayer: {
|
||||||
ref: null | ReactPlayer;
|
ref: null | ReactPlayer;
|
||||||
setVolume: (volume: number) => void;
|
setVolume: (volume: number) => void;
|
||||||
@@ -262,6 +301,7 @@ function crossfadeHandler(args: {
|
|||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
crossfadeDuration,
|
crossfadeDuration,
|
||||||
|
crossfadeStyle,
|
||||||
currentPlayer,
|
currentPlayer,
|
||||||
currentPlayerNum,
|
currentPlayerNum,
|
||||||
currentTime,
|
currentTime,
|
||||||
@@ -290,15 +330,57 @@ function crossfadeHandler(args: {
|
|||||||
|
|
||||||
const timeLeft = duration - currentTime;
|
const timeLeft = duration - currentTime;
|
||||||
|
|
||||||
// Calculate the volume levels based on time remaining
|
const progress = (crossfadeDuration - timeLeft) / crossfadeDuration;
|
||||||
const currentPlayerVolume = (timeLeft / crossfadeDuration) * volume;
|
|
||||||
const nextPlayerVolume = ((crossfadeDuration - timeLeft) / crossfadeDuration) * volume;
|
const { easeIn, easeOut } = getCrossfadeEasing(crossfadeStyle);
|
||||||
|
|
||||||
|
const easedProgressOut = easeOut(progress);
|
||||||
|
const easedProgressIn = easeIn(progress);
|
||||||
|
|
||||||
|
const currentPlayerVolume = (1 - easedProgressOut) * volume;
|
||||||
|
const nextPlayerVolume = easedProgressIn * volume;
|
||||||
|
|
||||||
// Set volumes for both players
|
// Set volumes for both players
|
||||||
currentPlayer.setVolume(currentPlayerVolume);
|
currentPlayer.setVolume(currentPlayerVolume);
|
||||||
nextPlayer.setVolume(nextPlayerVolume);
|
nextPlayer.setVolume(nextPlayerVolume);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Equal power easing - maintains constant power during crossfade
|
||||||
|
* Fade in: sin(π/2 * t)
|
||||||
|
* Fade out: 1 - cos(π/2 * t) so that (1 - result) = cos(π/2 * t)
|
||||||
|
*/
|
||||||
|
function equalPowerEaseIn(t: number): number {
|
||||||
|
const clampedT = Math.max(0, Math.min(1, t));
|
||||||
|
return Math.sin((Math.PI / 2) * clampedT);
|
||||||
|
}
|
||||||
|
|
||||||
|
function equalPowerEaseOut(t: number): number {
|
||||||
|
const clampedT = Math.max(0, Math.min(1, t));
|
||||||
|
return 1 - Math.cos((Math.PI / 2) * clampedT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exponential easing - natural exponential decay/rise
|
||||||
|
* Fade in: 1 - exp(-k * t) where k controls the curve steepness
|
||||||
|
* Fade out: exp(-k * t) normalized to go from 1 to 0
|
||||||
|
*/
|
||||||
|
function exponentialEaseIn(t: number): number {
|
||||||
|
const clampedT = Math.max(0, Math.min(1, t));
|
||||||
|
const k = 5;
|
||||||
|
return 1 - Math.exp(-k * clampedT);
|
||||||
|
}
|
||||||
|
|
||||||
|
function exponentialEaseOut(t: number): number {
|
||||||
|
const clampedT = Math.max(0, Math.min(1, t));
|
||||||
|
const k = 5;
|
||||||
|
// Exponential decay: exp(-k * t) goes from 1 (at t=0) to exp(-k) (at t=1)
|
||||||
|
// Normalize to go from 1 to 0
|
||||||
|
const startValue = Math.exp(0); // = 1
|
||||||
|
const endValue = Math.exp(-k);
|
||||||
|
return (Math.exp(-k * clampedT) - endValue) / (startValue - endValue);
|
||||||
|
}
|
||||||
|
|
||||||
function gaplessHandler(args: {
|
function gaplessHandler(args: {
|
||||||
currentTime: number;
|
currentTime: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
@@ -332,6 +414,40 @@ function gaplessHandler(args: {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCrossfadeEasing(style: CrossfadeStyle): {
|
||||||
|
easeIn: (t: number) => number;
|
||||||
|
easeOut: (t: number) => number;
|
||||||
|
} {
|
||||||
|
switch (style) {
|
||||||
|
case CrossfadeStyle.EQUAL_POWER:
|
||||||
|
return {
|
||||||
|
easeIn: equalPowerEaseIn,
|
||||||
|
easeOut: equalPowerEaseOut,
|
||||||
|
};
|
||||||
|
case CrossfadeStyle.EXPONENTIAL:
|
||||||
|
return {
|
||||||
|
easeIn: exponentialEaseIn,
|
||||||
|
easeOut: exponentialEaseOut,
|
||||||
|
};
|
||||||
|
case CrossfadeStyle.LINEAR:
|
||||||
|
return {
|
||||||
|
easeIn: linearEase,
|
||||||
|
easeOut: linearEase,
|
||||||
|
};
|
||||||
|
case CrossfadeStyle.S_CURVE:
|
||||||
|
return {
|
||||||
|
easeIn: sCurveEase,
|
||||||
|
easeOut: sCurveEase,
|
||||||
|
};
|
||||||
|
// Default to equal power for other styles
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
easeIn: equalPowerEaseIn,
|
||||||
|
easeOut: equalPowerEaseOut,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getDuration(ref: null | ReactPlayer | undefined) {
|
function getDuration(ref: null | ReactPlayer | undefined) {
|
||||||
return ref?.getInternalPlayer()?.duration || 0;
|
return ref?.getInternalPlayer()?.duration || 0;
|
||||||
}
|
}
|
||||||
@@ -344,3 +460,19 @@ function getDurationPadding(isFlac: boolean) {
|
|||||||
return 0.065;
|
return 0.065;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Linear easing - simple linear interpolation
|
||||||
|
*/
|
||||||
|
function linearEase(t: number): number {
|
||||||
|
return Math.max(0, Math.min(1, t));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* S-Curve easing (smoothstep) - smooth S-shaped curve
|
||||||
|
* Uses smoothstep function: t²(3 - 2t)
|
||||||
|
*/
|
||||||
|
function sCurveEase(t: number): number {
|
||||||
|
const clampedT = Math.max(0, Math.min(1, t));
|
||||||
|
return clampedT * clampedT * (3 - 2 * clampedT);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useMemo } from 'react';
|
import isElectron from 'is-electron';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { ListConfigTable } from '/@/renderer/features/shared/components/list-config-menu';
|
import { ListConfigTable } from '/@/renderer/features/shared/components/list-config-menu';
|
||||||
@@ -8,35 +9,61 @@ import {
|
|||||||
usePlayerProperties,
|
usePlayerProperties,
|
||||||
usePlayerQueueType,
|
usePlayerQueueType,
|
||||||
usePlayerSpeed,
|
usePlayerSpeed,
|
||||||
|
usePlayerStatus,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import {
|
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||||
BarAlign,
|
|
||||||
PlayerbarSliderType,
|
|
||||||
useGeneralSettings,
|
|
||||||
usePlaybackSettings,
|
|
||||||
usePlayerbarSlider,
|
|
||||||
useSettingsStore,
|
|
||||||
useSettingsStoreActions,
|
|
||||||
} from '/@/renderer/store/settings.store';
|
|
||||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { Popover } from '/@/shared/components/popover/popover';
|
import { Popover } from '/@/shared/components/popover/popover';
|
||||||
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
|
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
|
||||||
|
import { Select } from '/@/shared/components/select/select';
|
||||||
import { Slider } from '/@/shared/components/slider/slider';
|
import { Slider } from '/@/shared/components/slider/slider';
|
||||||
import { Switch } from '/@/shared/components/switch/switch';
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
import { PlayerQueueType, PlayerStyle, PlayerType } from '/@/shared/types/types';
|
import {
|
||||||
|
CrossfadeStyle,
|
||||||
|
PlayerQueueType,
|
||||||
|
PlayerStatus,
|
||||||
|
PlayerStyle,
|
||||||
|
PlayerType,
|
||||||
|
} from '/@/shared/types/types';
|
||||||
|
|
||||||
|
const ipc = isElectron() ? window.api.ipc : null;
|
||||||
|
|
||||||
|
const getAudioDevice = async () => {
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
return (devices || []).filter((dev: MediaDeviceInfo) => dev.kind === 'audiooutput');
|
||||||
|
};
|
||||||
|
|
||||||
export const PlayerConfig = () => {
|
export const PlayerConfig = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { currentSong } = usePlayerData();
|
const { currentSong } = usePlayerData();
|
||||||
const speed = usePlayerSpeed();
|
const speed = usePlayerSpeed();
|
||||||
const queueType = usePlayerQueueType();
|
const queueType = usePlayerQueueType();
|
||||||
const { crossfadeDuration, transitionType } = usePlayerProperties();
|
const status = usePlayerStatus();
|
||||||
const { setCrossfadeDuration, setQueueType, setSpeed, setTransitionType } = usePlayerActions();
|
const { crossfadeDuration, crossfadeStyle, transitionType } = usePlayerProperties();
|
||||||
|
const { setCrossfadeDuration, setCrossfadeStyle, setQueueType, setSpeed, setTransitionType } =
|
||||||
|
usePlayerActions();
|
||||||
const playbackSettings = usePlaybackSettings();
|
const playbackSettings = usePlaybackSettings();
|
||||||
const { setSettings } = useSettingsStoreActions();
|
const { setSettings } = useSettingsStoreActions();
|
||||||
const speedPreservePitch = useSettingsStore((state) => state.playback.preservePitch);
|
|
||||||
const playerbarSlider = usePlayerbarSlider();
|
const [audioDevices, setAudioDevices] = useState<{ label: string; value: string }[]>([]);
|
||||||
const generalSettings = useGeneralSettings();
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchAudioDevices = () => {
|
||||||
|
getAudioDevice()
|
||||||
|
.then((dev) =>
|
||||||
|
setAudioDevices(dev.map((d) => ({ label: d.label, value: d.deviceId }))),
|
||||||
|
)
|
||||||
|
.catch(() =>
|
||||||
|
toast.error({
|
||||||
|
message: t('error.audioDeviceFetchError', { postProcess: 'sentenceCase' }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (playbackSettings.type === PlayerType.WEB) {
|
||||||
|
fetchAudioDevices();
|
||||||
|
}
|
||||||
|
}, [playbackSettings.type, t]);
|
||||||
|
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
const formatPlaybackSpeedSliderLabel = (value: number) => {
|
const formatPlaybackSpeedSliderLabel = (value: number) => {
|
||||||
@@ -70,250 +97,163 @@ export const PlayerConfig = () => {
|
|||||||
id: 'queueType',
|
id: 'queueType',
|
||||||
label: t('player.queueType', { postProcess: 'titleCase' }),
|
label: t('player.queueType', { postProcess: 'titleCase' }),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: null,
|
||||||
|
id: 'divider-0',
|
||||||
|
isDivider: true,
|
||||||
|
label: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: (
|
||||||
|
<Select
|
||||||
|
comboboxProps={{ withinPortal: false }}
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
disabled: !isElectron(),
|
||||||
|
label: 'MPV',
|
||||||
|
value: PlayerType.LOCAL,
|
||||||
|
},
|
||||||
|
{ label: 'Web', value: PlayerType.WEB },
|
||||||
|
]}
|
||||||
|
defaultValue={playbackSettings.type}
|
||||||
|
disabled={status === PlayerStatus.PLAYING}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({
|
||||||
|
playback: { ...playbackSettings, type: e as PlayerType },
|
||||||
|
});
|
||||||
|
ipc?.send('settings-set', {
|
||||||
|
property: 'playbackType',
|
||||||
|
value: e,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
width="100%"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
id: 'audioPlayerType',
|
||||||
|
label: t('setting.audioPlayer', { postProcess: 'titleCase' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: (
|
||||||
|
<Select
|
||||||
|
clearable
|
||||||
|
comboboxProps={{ withinPortal: false }}
|
||||||
|
data={audioDevices}
|
||||||
|
defaultValue={playbackSettings.audioDeviceId}
|
||||||
|
disabled={playbackSettings.type !== PlayerType.WEB}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettings({
|
||||||
|
playback: {
|
||||||
|
...playbackSettings,
|
||||||
|
audioDeviceId: e,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
width="100%"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
id: 'audioDevice',
|
||||||
|
label: t('setting.audioDevice', { postProcess: 'titleCase' }),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: null,
|
component: null,
|
||||||
id: 'divider-1',
|
id: 'divider-1',
|
||||||
isDivider: true,
|
isDivider: true,
|
||||||
label: '',
|
label: '',
|
||||||
},
|
},
|
||||||
...(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: 'titleCase',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
|
|
||||||
...(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: 'titleCase',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
{
|
||||||
component: (
|
component: (
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
data={[
|
data={[
|
||||||
{
|
{
|
||||||
label: t('setting.playerbarSliderType', {
|
label: t('setting.playbackStyle', {
|
||||||
context: 'optionSlider',
|
context: 'optionNormal',
|
||||||
postProcess: 'titleCase',
|
postProcess: 'titleCase',
|
||||||
}),
|
}),
|
||||||
value: PlayerbarSliderType.SLIDER,
|
value: PlayerStyle.GAPLESS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('setting.playerbarSliderType', {
|
label: t('setting.playbackStyle', {
|
||||||
context: 'optionWaveform',
|
context: 'optionCrossFade',
|
||||||
postProcess: 'titleCase',
|
postProcess: 'titleCase',
|
||||||
}),
|
}),
|
||||||
value: PlayerbarSliderType.WAVEFORM,
|
value: PlayerStyle.CROSSFADE,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onChange={(value) => {
|
disabled={
|
||||||
setSettings({
|
!isElectron() ||
|
||||||
general: {
|
playbackSettings.type !== PlayerType.WEB ||
|
||||||
...generalSettings,
|
status === PlayerStatus.PLAYING
|
||||||
playerbarSlider: {
|
}
|
||||||
...playerbarSlider,
|
onChange={(value) => setTransitionType(value as PlayerStyle)}
|
||||||
type: value as PlayerbarSliderType,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
size="sm"
|
size="sm"
|
||||||
value={playerbarSlider?.type || PlayerbarSliderType.WAVEFORM}
|
value={transitionType}
|
||||||
w="100%"
|
w="100%"
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
id: 'playerbarSliderType',
|
id: 'transitionType',
|
||||||
label: t('setting.playerbarSlider', { postProcess: 'titleCase' }),
|
label: t('setting.playbackStyle', {
|
||||||
|
postProcess: 'titleCase',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: (
|
||||||
|
<Select
|
||||||
|
comboboxProps={{ withinPortal: false }}
|
||||||
|
data={[
|
||||||
|
{ label: 'Linear', value: CrossfadeStyle.LINEAR },
|
||||||
|
{ label: 'Equal Power', value: CrossfadeStyle.EQUAL_POWER },
|
||||||
|
{ label: 'S-Curve', value: CrossfadeStyle.S_CURVE },
|
||||||
|
{ label: 'Exponential', value: CrossfadeStyle.EXPONENTIAL },
|
||||||
|
]}
|
||||||
|
defaultValue={crossfadeStyle}
|
||||||
|
disabled={
|
||||||
|
playbackSettings.type !== PlayerType.WEB ||
|
||||||
|
transitionType !== PlayerStyle.CROSSFADE ||
|
||||||
|
status === PlayerStatus.PLAYING
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e) {
|
||||||
|
setCrossfadeStyle(e as CrossfadeStyle);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
width="100%"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
id: 'crossfadeStyle',
|
||||||
|
label: t('setting.crossfadeStyle', {
|
||||||
|
postProcess: 'titleCase',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: (
|
||||||
|
<Slider
|
||||||
|
defaultValue={crossfadeDuration}
|
||||||
|
disabled={
|
||||||
|
playbackSettings.type !== PlayerType.WEB ||
|
||||||
|
transitionType !== PlayerStyle.CROSSFADE ||
|
||||||
|
status === PlayerStatus.PLAYING
|
||||||
|
}
|
||||||
|
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: 'titleCase',
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
...(playerbarSlider?.type === PlayerbarSliderType.WAVEFORM
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
component: (
|
|
||||||
<SegmentedControl
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
label: t('setting.playerbarWaveformAlign', {
|
|
||||||
context: 'optionTop',
|
|
||||||
postProcess: 'titleCase',
|
|
||||||
}),
|
|
||||||
value: BarAlign.TOP,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('setting.playerbarWaveformAlign', {
|
|
||||||
context: 'optionCenter',
|
|
||||||
postProcess: 'titleCase',
|
|
||||||
}),
|
|
||||||
value: BarAlign.CENTER,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('setting.playerbarWaveformAlign', {
|
|
||||||
context: 'optionBottom',
|
|
||||||
postProcess: 'titleCase',
|
|
||||||
}),
|
|
||||||
value: BarAlign.BOTTOM,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onChange={(value) => {
|
|
||||||
setSettings({
|
|
||||||
general: {
|
|
||||||
...generalSettings,
|
|
||||||
playerbarSlider: {
|
|
||||||
...playerbarSlider,
|
|
||||||
barAlign: (value as BarAlign) || BarAlign.CENTER,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
size="sm"
|
|
||||||
value={playerbarSlider?.barAlign || BarAlign.CENTER}
|
|
||||||
w="100%"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
id: 'barAlign',
|
|
||||||
label: t('setting.playerbarWaveformAlign', {
|
|
||||||
postProcess: 'titleCase',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: (
|
|
||||||
<Slider
|
|
||||||
defaultValue={playerbarSlider?.barWidth ?? 2}
|
|
||||||
max={10}
|
|
||||||
min={0}
|
|
||||||
onChangeEnd={(value) => {
|
|
||||||
setSettings({
|
|
||||||
general: {
|
|
||||||
...generalSettings,
|
|
||||||
playerbarSlider: {
|
|
||||||
...playerbarSlider,
|
|
||||||
barWidth: value,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
step={1}
|
|
||||||
styles={{
|
|
||||||
root: {},
|
|
||||||
}}
|
|
||||||
w="100%"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
id: 'barWidth',
|
|
||||||
label: t('setting.playerbarWaveformBarWidth', {
|
|
||||||
postProcess: 'titleCase',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: (
|
|
||||||
<Slider
|
|
||||||
defaultValue={playerbarSlider?.barGap || 0}
|
|
||||||
max={10}
|
|
||||||
min={0}
|
|
||||||
onChangeEnd={(value) => {
|
|
||||||
setSettings({
|
|
||||||
general: {
|
|
||||||
...generalSettings,
|
|
||||||
playerbarSlider: {
|
|
||||||
...playerbarSlider,
|
|
||||||
barGap: value,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
step={1}
|
|
||||||
styles={{
|
|
||||||
root: {},
|
|
||||||
}}
|
|
||||||
w="100%"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
id: 'barGap',
|
|
||||||
label: t('setting.playerbarWaveformGap', { postProcess: 'titleCase' }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: (
|
|
||||||
<Slider
|
|
||||||
defaultValue={playerbarSlider?.barRadius ?? 4}
|
|
||||||
max={20}
|
|
||||||
min={0}
|
|
||||||
onChangeEnd={(value) => {
|
|
||||||
setSettings({
|
|
||||||
general: {
|
|
||||||
...generalSettings,
|
|
||||||
playerbarSlider: {
|
|
||||||
...playerbarSlider,
|
|
||||||
barRadius: value,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
step={1}
|
|
||||||
styles={{
|
|
||||||
root: {},
|
|
||||||
}}
|
|
||||||
w="100%"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
id: 'barRadius',
|
|
||||||
label: t('setting.playerbarWaveformRadius', {
|
|
||||||
postProcess: 'titleCase',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
{
|
||||||
component: null,
|
component: null,
|
||||||
id: 'divider-2',
|
id: 'divider-2',
|
||||||
@@ -323,6 +263,7 @@ export const PlayerConfig = () => {
|
|||||||
{
|
{
|
||||||
component: (
|
component: (
|
||||||
<Slider
|
<Slider
|
||||||
|
defaultValue={speed}
|
||||||
label={formatPlaybackSpeedSliderLabel}
|
label={formatPlaybackSpeedSliderLabel}
|
||||||
marks={[
|
marks={[
|
||||||
{ label: '0.5', value: 0.5 },
|
{ label: '0.5', value: 0.5 },
|
||||||
@@ -335,49 +276,26 @@ export const PlayerConfig = () => {
|
|||||||
]}
|
]}
|
||||||
max={2}
|
max={2}
|
||||||
min={0.5}
|
min={0.5}
|
||||||
onChange={setSpeed}
|
onChangeEnd={setSpeed}
|
||||||
onDoubleClick={() => setSpeed(1)}
|
onDoubleClick={() => setSpeed(1)}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
styles={{
|
styles={{
|
||||||
markLabel: {},
|
markLabel: {},
|
||||||
root: {},
|
root: {},
|
||||||
}}
|
}}
|
||||||
value={speed}
|
|
||||||
w="100%"
|
w="100%"
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
id: 'playbackSpeed',
|
id: 'playbackSpeed',
|
||||||
label: t('player.playbackSpeed', { postProcess: 'titleCase' }),
|
label: t('player.playbackSpeed', { postProcess: 'titleCase' }),
|
||||||
},
|
},
|
||||||
...(speed !== 1
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
component: (
|
|
||||||
<Switch
|
|
||||||
defaultChecked={speedPreservePitch}
|
|
||||||
onChange={(e) => {
|
|
||||||
setSettings({
|
|
||||||
playback: {
|
|
||||||
...playbackSettings,
|
|
||||||
preservePitch: e.currentTarget.checked,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
id: 'preservePitch',
|
|
||||||
label: t('setting.preservePitch', {
|
|
||||||
postProcess: 'titleCase',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return allOptions;
|
return allOptions;
|
||||||
}, [
|
}, [
|
||||||
playbackSettings,
|
playbackSettings,
|
||||||
speedPreservePitch,
|
audioDevices,
|
||||||
|
status,
|
||||||
setSettings,
|
setSettings,
|
||||||
currentSong,
|
currentSong,
|
||||||
speed,
|
speed,
|
||||||
@@ -388,8 +306,8 @@ export const PlayerConfig = () => {
|
|||||||
setTransitionType,
|
setTransitionType,
|
||||||
crossfadeDuration,
|
crossfadeDuration,
|
||||||
setCrossfadeDuration,
|
setCrossfadeDuration,
|
||||||
playerbarSlider,
|
crossfadeStyle,
|
||||||
generalSettings,
|
setCrossfadeStyle,
|
||||||
t,
|
t,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,17 @@ import {
|
|||||||
SettingsSection,
|
SettingsSection,
|
||||||
} from '/@/renderer/features/settings/components/settings-section';
|
} from '/@/renderer/features/settings/components/settings-section';
|
||||||
import {
|
import {
|
||||||
|
BarAlign,
|
||||||
GenreTarget,
|
GenreTarget,
|
||||||
|
PlayerbarSliderType,
|
||||||
SideQueueType,
|
SideQueueType,
|
||||||
useGeneralSettings,
|
useGeneralSettings,
|
||||||
|
usePlayerbarSlider,
|
||||||
useSettingsStoreActions,
|
useSettingsStoreActions,
|
||||||
} from '/@/renderer/store/settings.store';
|
} from '/@/renderer/store/settings.store';
|
||||||
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';
|
||||||
|
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
|
||||||
import { Select } from '/@/shared/components/select/select';
|
import { Select } from '/@/shared/components/select/select';
|
||||||
import { Slider } from '/@/shared/components/slider/slider';
|
import { Slider } from '/@/shared/components/slider/slider';
|
||||||
import { Switch } from '/@/shared/components/switch/switch';
|
import { Switch } from '/@/shared/components/switch/switch';
|
||||||
@@ -43,6 +47,7 @@ const SIDE_QUEUE_OPTIONS = [
|
|||||||
export const ControlSettings = () => {
|
export const ControlSettings = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const settings = useGeneralSettings();
|
const settings = useGeneralSettings();
|
||||||
|
const playerbarSlider = usePlayerbarSlider();
|
||||||
const { setSettings } = useSettingsStoreActions();
|
const { setSettings } = useSettingsStoreActions();
|
||||||
|
|
||||||
const controlOptions: SettingOption[] = [
|
const controlOptions: SettingOption[] = [
|
||||||
@@ -621,6 +626,202 @@ export const ControlSettings = () => {
|
|||||||
isHidden: false,
|
isHidden: false,
|
||||||
title: t('setting.playerbarOpenDrawer', { postProcess: 'sentenceCase' }),
|
title: t('setting.playerbarOpenDrawer', { postProcess: 'sentenceCase' }),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<SegmentedControl
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
label: t('setting.playerbarSliderType', {
|
||||||
|
context: 'optionSlider',
|
||||||
|
postProcess: 'titleCase',
|
||||||
|
}),
|
||||||
|
value: PlayerbarSliderType.SLIDER,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('setting.playerbarSliderType', {
|
||||||
|
context: 'optionWaveform',
|
||||||
|
postProcess: 'titleCase',
|
||||||
|
}),
|
||||||
|
value: PlayerbarSliderType.WAVEFORM,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onChange={(value) => {
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...settings,
|
||||||
|
playerbarSlider: {
|
||||||
|
...playerbarSlider,
|
||||||
|
type: value as PlayerbarSliderType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
value={playerbarSlider?.type || PlayerbarSliderType.WAVEFORM}
|
||||||
|
w="100%"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.playerbarSlider', {
|
||||||
|
context: 'description',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
isHidden: false,
|
||||||
|
title: t('setting.playerbarSlider', { postProcess: 'sentenceCase' }),
|
||||||
|
},
|
||||||
|
...(playerbarSlider?.type === PlayerbarSliderType.WAVEFORM
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<SegmentedControl
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
label: t('setting.playerbarWaveformAlign', {
|
||||||
|
context: 'optionTop',
|
||||||
|
postProcess: 'titleCase',
|
||||||
|
}),
|
||||||
|
value: BarAlign.TOP,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('setting.playerbarWaveformAlign', {
|
||||||
|
context: 'optionCenter',
|
||||||
|
postProcess: 'titleCase',
|
||||||
|
}),
|
||||||
|
value: BarAlign.CENTER,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('setting.playerbarWaveformAlign', {
|
||||||
|
context: 'optionBottom',
|
||||||
|
postProcess: 'titleCase',
|
||||||
|
}),
|
||||||
|
value: BarAlign.BOTTOM,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onChange={(value) => {
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...settings,
|
||||||
|
playerbarSlider: {
|
||||||
|
...playerbarSlider,
|
||||||
|
barAlign: (value as BarAlign) || BarAlign.CENTER,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
value={playerbarSlider?.barAlign || BarAlign.CENTER}
|
||||||
|
w="100%"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.playerbarWaveformAlign', {
|
||||||
|
context: 'description',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
isHidden: false,
|
||||||
|
title: t('setting.playerbarWaveformAlign', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Slider
|
||||||
|
defaultValue={playerbarSlider?.barWidth ?? 2}
|
||||||
|
max={10}
|
||||||
|
min={0}
|
||||||
|
onChangeEnd={(value) => {
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...settings,
|
||||||
|
playerbarSlider: {
|
||||||
|
...playerbarSlider,
|
||||||
|
barWidth: value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
step={1}
|
||||||
|
styles={{
|
||||||
|
root: {},
|
||||||
|
}}
|
||||||
|
w="120px"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.playerbarWaveformBarWidth', {
|
||||||
|
context: 'description',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
isHidden: false,
|
||||||
|
title: t('setting.playerbarWaveformBarWidth', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Slider
|
||||||
|
defaultValue={playerbarSlider?.barGap || 0}
|
||||||
|
max={10}
|
||||||
|
min={0}
|
||||||
|
onChangeEnd={(value) => {
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...settings,
|
||||||
|
playerbarSlider: {
|
||||||
|
...playerbarSlider,
|
||||||
|
barGap: value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
step={1}
|
||||||
|
styles={{
|
||||||
|
root: {},
|
||||||
|
}}
|
||||||
|
w="120px"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.playerbarWaveformGap', {
|
||||||
|
context: 'description',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
isHidden: false,
|
||||||
|
title: t('setting.playerbarWaveformGap', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Slider
|
||||||
|
defaultValue={playerbarSlider?.barRadius ?? 4}
|
||||||
|
max={20}
|
||||||
|
min={0}
|
||||||
|
onChangeEnd={(value) => {
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...settings,
|
||||||
|
playerbarSlider: {
|
||||||
|
...playerbarSlider,
|
||||||
|
barRadius: value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
step={1}
|
||||||
|
styles={{
|
||||||
|
root: {},
|
||||||
|
}}
|
||||||
|
w="120px"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.playerbarWaveformRadius', {
|
||||||
|
context: 'description',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
isHidden: false,
|
||||||
|
title: t('setting.playerbarWaveformRadius', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
];
|
];
|
||||||
|
|
||||||
return <SettingsSection options={controlOptions} />;
|
return <SettingsSection options={controlOptions} />;
|
||||||
|
|||||||
@@ -6,13 +6,12 @@ import {
|
|||||||
SettingOption,
|
SettingOption,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
} from '/@/renderer/features/settings/components/settings-section';
|
} from '/@/renderer/features/settings/components/settings-section';
|
||||||
import { usePlayerActions, usePlayerProperties, usePlayerStatus } from '/@/renderer/store';
|
import { usePlayerStatus } from '/@/renderer/store';
|
||||||
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||||
import { Select } from '/@/shared/components/select/select';
|
import { Select } from '/@/shared/components/select/select';
|
||||||
import { Slider } from '/@/shared/components/slider/slider';
|
|
||||||
import { Switch } from '/@/shared/components/switch/switch';
|
import { Switch } from '/@/shared/components/switch/switch';
|
||||||
import { toast } from '/@/shared/components/toast/toast';
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
import { CrossfadeStyle, PlayerStatus, PlayerStyle, PlayerType } from '/@/shared/types/types';
|
import { PlayerStatus, PlayerType } from '/@/shared/types/types';
|
||||||
|
|
||||||
const ipc = isElectron() ? window.api.ipc : null;
|
const ipc = isElectron() ? window.api.ipc : null;
|
||||||
|
|
||||||
@@ -27,9 +26,6 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
|
|||||||
const { setSettings } = useSettingsStoreActions();
|
const { setSettings } = useSettingsStoreActions();
|
||||||
const status = usePlayerStatus();
|
const status = usePlayerStatus();
|
||||||
|
|
||||||
const { crossfadeDuration, transitionType } = usePlayerProperties();
|
|
||||||
const { setCrossfadeDuration, setTransitionType } = usePlayerActions();
|
|
||||||
|
|
||||||
const [audioDevices, setAudioDevices] = useState<{ label: string; value: string }[]>([]);
|
const [audioDevices, setAudioDevices] = useState<{ label: string; value: string }[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -98,41 +94,6 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
|
|||||||
isHidden: !isElectron() || settings.type !== PlayerType.WEB,
|
isHidden: !isElectron() || settings.type !== PlayerType.WEB,
|
||||||
title: t('setting.audioDevice', { postProcess: 'sentenceCase' }),
|
title: t('setting.audioDevice', { postProcess: 'sentenceCase' }),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<Select
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
label: t('setting.playbackStyle', {
|
|
||||||
context: 'optionNormal',
|
|
||||||
postProcess: 'titleCase',
|
|
||||||
}),
|
|
||||||
value: PlayerStyle.GAPLESS,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('setting.playbackStyle', {
|
|
||||||
context: 'optionCrossFade',
|
|
||||||
postProcess: 'titleCase',
|
|
||||||
}),
|
|
||||||
value: PlayerStyle.CROSSFADE,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
defaultValue={transitionType}
|
|
||||||
disabled={settings.type !== PlayerType.WEB || status === PlayerStatus.PLAYING}
|
|
||||||
onChange={(e) => setTransitionType(e as PlayerStyle)}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description: t('setting.playbackStyle', {
|
|
||||||
context: 'description',
|
|
||||||
postProcess: 'sentenceCase',
|
|
||||||
}),
|
|
||||||
isHidden: settings.type !== PlayerType.WEB,
|
|
||||||
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
|
|
||||||
title: t('setting.playbackStyle', {
|
|
||||||
context: 'description',
|
|
||||||
postProcess: 'sentenceCase',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
control: (
|
control: (
|
||||||
<Switch
|
<Switch
|
||||||
@@ -174,71 +135,6 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
|
|||||||
postProcess: 'sentenceCase',
|
postProcess: 'sentenceCase',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<Slider
|
|
||||||
defaultValue={crossfadeDuration}
|
|
||||||
disabled={
|
|
||||||
settings.type !== PlayerType.WEB ||
|
|
||||||
settings.style !== PlayerStyle.CROSSFADE ||
|
|
||||||
status === PlayerStatus.PLAYING
|
|
||||||
}
|
|
||||||
max={15}
|
|
||||||
min={3}
|
|
||||||
onChangeEnd={(e) => setCrossfadeDuration(e)}
|
|
||||||
w={100}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description: t('setting.crossfadeDuration', {
|
|
||||||
context: 'description',
|
|
||||||
postProcess: 'sentenceCase',
|
|
||||||
}),
|
|
||||||
isHidden: settings.type !== PlayerType.WEB,
|
|
||||||
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
|
|
||||||
title: t('setting.crossfadeDuration', {
|
|
||||||
postProcess: 'sentenceCase',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<Select
|
|
||||||
data={[
|
|
||||||
{ label: 'Linear', value: CrossfadeStyle.LINEAR },
|
|
||||||
{ label: 'Constant Power', value: CrossfadeStyle.CONSTANT_POWER },
|
|
||||||
{
|
|
||||||
label: 'Constant Power (Slow cut)',
|
|
||||||
value: CrossfadeStyle.CONSTANT_POWER_SLOW_CUT,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Constant Power (Slow fade)',
|
|
||||||
value: CrossfadeStyle.CONSTANT_POWER_SLOW_FADE,
|
|
||||||
},
|
|
||||||
{ label: 'Dipped', value: CrossfadeStyle.DIPPED },
|
|
||||||
{ label: 'Equal Power', value: CrossfadeStyle.EQUALPOWER },
|
|
||||||
]}
|
|
||||||
defaultValue={settings.crossfadeStyle}
|
|
||||||
disabled={
|
|
||||||
settings.type !== PlayerType.WEB ||
|
|
||||||
settings.style !== PlayerStyle.CROSSFADE ||
|
|
||||||
status === PlayerStatus.PLAYING
|
|
||||||
}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (!e) return;
|
|
||||||
setSettings({
|
|
||||||
playback: { ...settings, crossfadeStyle: e as CrossfadeStyle },
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
width={200}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description: t('setting.crossfadeStyle', {
|
|
||||||
context: 'description',
|
|
||||||
postProcess: 'sentenceCase',
|
|
||||||
}),
|
|
||||||
isHidden: settings.type !== PlayerType.WEB,
|
|
||||||
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
|
|
||||||
title: t('setting.crossfadeStyle', { postProcess: 'sentenceCase' }),
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return <SettingsSection divider={!hasFancyAudio} options={audioOptions} />;
|
return <SettingsSection divider={!hasFancyAudio} options={audioOptions} />;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { idbStateStorage } from '/@/renderer/store/utils';
|
|||||||
import { shuffleInPlace } from '/@/renderer/utils/shuffle';
|
import { shuffleInPlace } from '/@/renderer/utils/shuffle';
|
||||||
import { PlayerData, QueueData, QueueSong, Song } from '/@/shared/types/domain-types';
|
import { PlayerData, QueueData, QueueSong, Song } from '/@/shared/types/domain-types';
|
||||||
import {
|
import {
|
||||||
|
CrossfadeStyle,
|
||||||
Play,
|
Play,
|
||||||
PlayerQueueType,
|
PlayerQueueType,
|
||||||
PlayerRepeat,
|
PlayerRepeat,
|
||||||
@@ -56,6 +57,7 @@ interface Actions {
|
|||||||
moveSelectedToNext: (items: QueueSong[]) => void;
|
moveSelectedToNext: (items: QueueSong[]) => void;
|
||||||
moveSelectedToTop: (items: QueueSong[]) => void;
|
moveSelectedToTop: (items: QueueSong[]) => void;
|
||||||
setCrossfadeDuration: (duration: number) => void;
|
setCrossfadeDuration: (duration: number) => void;
|
||||||
|
setCrossfadeStyle: (style: CrossfadeStyle) => void;
|
||||||
setQueueType: (queueType: PlayerQueueType) => void;
|
setQueueType: (queueType: PlayerQueueType) => void;
|
||||||
setRepeat: (repeat: PlayerRepeat) => void;
|
setRepeat: (repeat: PlayerRepeat) => void;
|
||||||
setShuffle: (shuffle: PlayerShuffle) => void;
|
setShuffle: (shuffle: PlayerShuffle) => void;
|
||||||
@@ -77,6 +79,7 @@ interface GroupedQueue {
|
|||||||
interface State {
|
interface State {
|
||||||
player: {
|
player: {
|
||||||
crossfadeDuration: number;
|
crossfadeDuration: number;
|
||||||
|
crossfadeStyle: CrossfadeStyle;
|
||||||
index: number;
|
index: number;
|
||||||
muted: boolean;
|
muted: boolean;
|
||||||
playerNum: 1 | 2;
|
playerNum: 1 | 2;
|
||||||
@@ -95,6 +98,7 @@ interface State {
|
|||||||
const initialState: State = {
|
const initialState: State = {
|
||||||
player: {
|
player: {
|
||||||
crossfadeDuration: 5,
|
crossfadeDuration: 5,
|
||||||
|
crossfadeStyle: CrossfadeStyle.EQUAL_POWER,
|
||||||
index: -1,
|
index: -1,
|
||||||
muted: false,
|
muted: false,
|
||||||
playerNum: 1,
|
playerNum: 1,
|
||||||
@@ -1048,6 +1052,11 @@ export const usePlayerStoreBase = create<PlayerState>()(
|
|||||||
state.player.crossfadeDuration = normalizedDuration;
|
state.player.crossfadeDuration = normalizedDuration;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
setCrossfadeStyle: (style: CrossfadeStyle) => {
|
||||||
|
set((state) => {
|
||||||
|
state.player.crossfadeStyle = style;
|
||||||
|
});
|
||||||
|
},
|
||||||
setQueueType: (queueType: PlayerQueueType) => {
|
setQueueType: (queueType: PlayerQueueType) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
// From default -> priority, move all items from default to priority
|
// From default -> priority, move all items from default to priority
|
||||||
@@ -1245,6 +1254,7 @@ export const usePlayerActions = () => {
|
|||||||
moveSelectedToNext: state.moveSelectedToNext,
|
moveSelectedToNext: state.moveSelectedToNext,
|
||||||
moveSelectedToTop: state.moveSelectedToTop,
|
moveSelectedToTop: state.moveSelectedToTop,
|
||||||
setCrossfadeDuration: state.setCrossfadeDuration,
|
setCrossfadeDuration: state.setCrossfadeDuration,
|
||||||
|
setCrossfadeStyle: state.setCrossfadeStyle,
|
||||||
setQueueType: state.setQueueType,
|
setQueueType: state.setQueueType,
|
||||||
setRepeat: state.setRepeat,
|
setRepeat: state.setRepeat,
|
||||||
setShuffle: state.setShuffle,
|
setShuffle: state.setShuffle,
|
||||||
@@ -1403,6 +1413,7 @@ export const usePlayerProperties = () => {
|
|||||||
return usePlayerStoreBase(
|
return usePlayerStoreBase(
|
||||||
useShallow((state) => ({
|
useShallow((state) => ({
|
||||||
crossfadeDuration: state.player.crossfadeDuration,
|
crossfadeDuration: state.player.crossfadeDuration,
|
||||||
|
crossfadeStyle: state.player.crossfadeStyle,
|
||||||
isMuted: state.player.muted,
|
isMuted: state.player.muted,
|
||||||
playerNum: state.player.playerNum,
|
playerNum: state.player.playerNum,
|
||||||
queueType: state.player.queueType,
|
queueType: state.player.queueType,
|
||||||
|
|||||||
@@ -25,14 +25,12 @@ import { sanitizeCss } from '/@/renderer/utils/sanitize';
|
|||||||
import { AppTheme } from '/@/shared/themes/app-theme-types';
|
import { AppTheme } from '/@/shared/themes/app-theme-types';
|
||||||
import { LibraryItem, LyricSource } from '/@/shared/types/domain-types';
|
import { LibraryItem, LyricSource } from '/@/shared/types/domain-types';
|
||||||
import {
|
import {
|
||||||
CrossfadeStyle,
|
|
||||||
FontType,
|
FontType,
|
||||||
ItemListKey,
|
ItemListKey,
|
||||||
ListDisplayType,
|
ListDisplayType,
|
||||||
ListPaginationType,
|
ListPaginationType,
|
||||||
Platform,
|
Platform,
|
||||||
Play,
|
Play,
|
||||||
PlayerStyle,
|
|
||||||
PlayerType,
|
PlayerType,
|
||||||
TableColumn,
|
TableColumn,
|
||||||
} from '/@/shared/types/types';
|
} from '/@/shared/types/types';
|
||||||
@@ -306,15 +304,11 @@ const ScrobbleSettingsSchema = z.object({
|
|||||||
|
|
||||||
const PlaybackSettingsSchema = z.object({
|
const PlaybackSettingsSchema = z.object({
|
||||||
audioDeviceId: z.string().nullable().optional(),
|
audioDeviceId: z.string().nullable().optional(),
|
||||||
crossfadeDuration: z.number(),
|
|
||||||
crossfadeStyle: z.nativeEnum(CrossfadeStyle),
|
|
||||||
mediaSession: z.boolean(),
|
mediaSession: z.boolean(),
|
||||||
mpvExtraParameters: z.array(z.string()),
|
mpvExtraParameters: z.array(z.string()),
|
||||||
mpvProperties: MpvSettingsSchema,
|
mpvProperties: MpvSettingsSchema,
|
||||||
muted: z.boolean(),
|
|
||||||
preservePitch: z.boolean(),
|
preservePitch: z.boolean(),
|
||||||
scrobble: ScrobbleSettingsSchema,
|
scrobble: ScrobbleSettingsSchema,
|
||||||
style: z.nativeEnum(PlayerStyle),
|
|
||||||
transcode: TranscodingConfigSchema,
|
transcode: TranscodingConfigSchema,
|
||||||
type: z.nativeEnum(PlayerType),
|
type: z.nativeEnum(PlayerType),
|
||||||
webAudio: z.boolean(),
|
webAudio: z.boolean(),
|
||||||
@@ -1090,8 +1084,6 @@ const initialState: SettingsState = {
|
|||||||
},
|
},
|
||||||
playback: {
|
playback: {
|
||||||
audioDeviceId: undefined,
|
audioDeviceId: undefined,
|
||||||
crossfadeDuration: 5,
|
|
||||||
crossfadeStyle: CrossfadeStyle.EQUALPOWER,
|
|
||||||
mediaSession: false,
|
mediaSession: false,
|
||||||
mpvExtraParameters: [],
|
mpvExtraParameters: [],
|
||||||
mpvProperties: {
|
mpvProperties: {
|
||||||
@@ -1104,7 +1096,6 @@ const initialState: SettingsState = {
|
|||||||
replayGainMode: 'no',
|
replayGainMode: 'no',
|
||||||
replayGainPreampDB: 0,
|
replayGainPreampDB: 0,
|
||||||
},
|
},
|
||||||
muted: false,
|
|
||||||
preservePitch: true,
|
preservePitch: true,
|
||||||
scrobble: {
|
scrobble: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -1112,7 +1103,6 @@ const initialState: SettingsState = {
|
|||||||
scrobbleAtDuration: 240,
|
scrobbleAtDuration: 240,
|
||||||
scrobbleAtPercentage: 75,
|
scrobbleAtPercentage: 75,
|
||||||
},
|
},
|
||||||
style: PlayerStyle.GAPLESS,
|
|
||||||
transcode: {
|
transcode: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
@@ -1289,10 +1279,7 @@ export const useTableSettings = (type: ItemListKey) =>
|
|||||||
|
|
||||||
export const useGeneralSettings = () => useSettingsStore((state) => state.general, shallow);
|
export const useGeneralSettings = () => useSettingsStore((state) => state.general, shallow);
|
||||||
|
|
||||||
export const usePlaybackType = () =>
|
export const usePlaybackType = () => useSettingsStore((state) => state.playback.type, shallow);
|
||||||
useSettingsStore((state) => {
|
|
||||||
return state.playback.type;
|
|
||||||
});
|
|
||||||
|
|
||||||
export const usePlayButtonBehavior = () =>
|
export const usePlayButtonBehavior = () =>
|
||||||
useSettingsStore((state) => state.general.playButtonBehavior, shallow);
|
useSettingsStore((state) => state.general.playButtonBehavior, shallow);
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ export interface SelectProps extends MantineSelectProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Select = ({
|
export const Select = ({
|
||||||
|
allowDeselect = false,
|
||||||
classNames,
|
classNames,
|
||||||
|
clearable = false,
|
||||||
maxWidth,
|
maxWidth,
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
width,
|
width,
|
||||||
@@ -19,14 +21,17 @@ export const Select = ({
|
|||||||
}: SelectProps) => {
|
}: SelectProps) => {
|
||||||
return (
|
return (
|
||||||
<MantineSelect
|
<MantineSelect
|
||||||
|
allowDeselect={allowDeselect || clearable}
|
||||||
classNames={{
|
classNames={{
|
||||||
dropdown: styles.dropdown,
|
dropdown: styles.dropdown,
|
||||||
input: styles.input,
|
input: styles.input,
|
||||||
label: styles.label,
|
label: styles.label,
|
||||||
option: styles.option,
|
option: styles.option,
|
||||||
root: styles.root,
|
root: styles.root,
|
||||||
|
section: styles.section,
|
||||||
...classNames,
|
...classNames,
|
||||||
}}
|
}}
|
||||||
|
clearable={false}
|
||||||
style={{ maxWidth, width }}
|
style={{ maxWidth, width }}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
withCheckIcon={false}
|
withCheckIcon={false}
|
||||||
|
|||||||
@@ -95,12 +95,10 @@ export enum AuthState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum CrossfadeStyle {
|
export enum CrossfadeStyle {
|
||||||
CONSTANT_POWER = 'constantPower',
|
EQUAL_POWER = 'equalPower',
|
||||||
CONSTANT_POWER_SLOW_CUT = 'constantPowerSlowCut',
|
EXPONENTIAL = 'exponential',
|
||||||
CONSTANT_POWER_SLOW_FADE = 'constantPowerSlowFade',
|
|
||||||
DIPPED = 'dipped',
|
|
||||||
EQUALPOWER = 'equalPower',
|
|
||||||
LINEAR = 'linear',
|
LINEAR = 'linear',
|
||||||
|
S_CURVE = 'sCurve',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum FontType {
|
export enum FontType {
|
||||||
|
|||||||
Reference in New Issue
Block a user