mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-08 13:00:13 +02:00
crossfade player enhancements, reorganize settings
This commit is contained in:
@@ -17,7 +17,7 @@ import {
|
||||
usePlayerProperties,
|
||||
usePlayerVolume,
|
||||
} 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_INTERVAL = 10;
|
||||
@@ -26,7 +26,7 @@ export function WebPlayer() {
|
||||
const playerRef = useRef<null | WebPlayerEngineHandle>(null);
|
||||
const { num, player1, player2, status } = usePlayerData();
|
||||
const { mediaAutoNext, setTimestamp } = usePlayerActions();
|
||||
const { crossfadeDuration, speed, transitionType } = usePlayerProperties();
|
||||
const { crossfadeDuration, crossfadeStyle, speed, transitionType } = usePlayerProperties();
|
||||
const isMuted = usePlayerMuted();
|
||||
const volume = usePlayerVolume();
|
||||
|
||||
@@ -79,6 +79,7 @@ export function WebPlayer() {
|
||||
case PlayerStyle.CROSSFADE:
|
||||
crossfadeHandler({
|
||||
crossfadeDuration: crossfadeDuration,
|
||||
crossfadeStyle,
|
||||
currentPlayer: playerRef.current.player1(),
|
||||
currentPlayerNum: num,
|
||||
currentTime: e.playedSeconds,
|
||||
@@ -102,7 +103,7 @@ export function WebPlayer() {
|
||||
break;
|
||||
}
|
||||
},
|
||||
[crossfadeDuration, isTransitioning, num, transitionType, volume],
|
||||
[crossfadeDuration, crossfadeStyle, isTransitioning, num, transitionType, volume],
|
||||
);
|
||||
|
||||
const onProgressPlayer2 = useCallback(
|
||||
@@ -115,6 +116,7 @@ export function WebPlayer() {
|
||||
case PlayerStyle.CROSSFADE:
|
||||
crossfadeHandler({
|
||||
crossfadeDuration: crossfadeDuration,
|
||||
crossfadeStyle,
|
||||
currentPlayer: playerRef.current.player2(),
|
||||
currentPlayerNum: num,
|
||||
currentTime: e.playedSeconds,
|
||||
@@ -138,7 +140,7 @@ export function WebPlayer() {
|
||||
break;
|
||||
}
|
||||
},
|
||||
[crossfadeDuration, isTransitioning, num, transitionType, volume],
|
||||
[crossfadeDuration, crossfadeStyle, isTransitioning, num, transitionType, volume],
|
||||
);
|
||||
|
||||
const handleOnEndedPlayer1 = useCallback(() => {
|
||||
@@ -171,6 +173,22 @@ export function WebPlayer() {
|
||||
{
|
||||
onPlayerSeekToTimestamp: (properties) => {
|
||||
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) {
|
||||
playerRef.current?.player1()?.ref?.seekTo(timestamp);
|
||||
} else {
|
||||
@@ -179,6 +197,26 @@ export function WebPlayer() {
|
||||
},
|
||||
onPlayerStatus: async (properties) => {
|
||||
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) {
|
||||
fadeAndSetStatus(volume, 0, PLAY_PAUSE_FADE_DURATION, PlayerStatus.PAUSED);
|
||||
} else if (status === PlayerStatus.PLAYING) {
|
||||
@@ -190,7 +228,7 @@ export function WebPlayer() {
|
||||
playerRef.current?.setVolume(volume);
|
||||
},
|
||||
},
|
||||
[volume, num, isTransitioning],
|
||||
[volume, num, isTransitioning, transitionType],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -244,6 +282,7 @@ export function WebPlayer() {
|
||||
|
||||
function crossfadeHandler(args: {
|
||||
crossfadeDuration: number;
|
||||
crossfadeStyle: CrossfadeStyle;
|
||||
currentPlayer: {
|
||||
ref: null | ReactPlayer;
|
||||
setVolume: (volume: number) => void;
|
||||
@@ -262,6 +301,7 @@ function crossfadeHandler(args: {
|
||||
}) {
|
||||
const {
|
||||
crossfadeDuration,
|
||||
crossfadeStyle,
|
||||
currentPlayer,
|
||||
currentPlayerNum,
|
||||
currentTime,
|
||||
@@ -290,15 +330,57 @@ function crossfadeHandler(args: {
|
||||
|
||||
const timeLeft = duration - currentTime;
|
||||
|
||||
// Calculate the volume levels based on time remaining
|
||||
const currentPlayerVolume = (timeLeft / crossfadeDuration) * volume;
|
||||
const nextPlayerVolume = ((crossfadeDuration - timeLeft) / crossfadeDuration) * volume;
|
||||
const progress = (crossfadeDuration - timeLeft) / crossfadeDuration;
|
||||
|
||||
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
|
||||
currentPlayer.setVolume(currentPlayerVolume);
|
||||
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: {
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
@@ -332,6 +414,40 @@ function gaplessHandler(args: {
|
||||
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) {
|
||||
return ref?.getInternalPlayer()?.duration || 0;
|
||||
}
|
||||
@@ -344,3 +460,19 @@ function getDurationPadding(isFlac: boolean) {
|
||||
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 { ListConfigTable } from '/@/renderer/features/shared/components/list-config-menu';
|
||||
@@ -8,35 +9,61 @@ import {
|
||||
usePlayerProperties,
|
||||
usePlayerQueueType,
|
||||
usePlayerSpeed,
|
||||
usePlayerStatus,
|
||||
} from '/@/renderer/store';
|
||||
import {
|
||||
BarAlign,
|
||||
PlayerbarSliderType,
|
||||
useGeneralSettings,
|
||||
usePlaybackSettings,
|
||||
usePlayerbarSlider,
|
||||
useSettingsStore,
|
||||
useSettingsStoreActions,
|
||||
} from '/@/renderer/store/settings.store';
|
||||
import { usePlaybackSettings, 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 { Select } from '/@/shared/components/select/select';
|
||||
import { Slider } from '/@/shared/components/slider/slider';
|
||||
import { Switch } from '/@/shared/components/switch/switch';
|
||||
import { PlayerQueueType, PlayerStyle, PlayerType } from '/@/shared/types/types';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
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 = () => {
|
||||
const { t } = useTranslation();
|
||||
const { currentSong } = usePlayerData();
|
||||
const speed = usePlayerSpeed();
|
||||
const queueType = usePlayerQueueType();
|
||||
const { crossfadeDuration, transitionType } = usePlayerProperties();
|
||||
const { setCrossfadeDuration, setQueueType, setSpeed, setTransitionType } = usePlayerActions();
|
||||
const status = usePlayerStatus();
|
||||
const { crossfadeDuration, crossfadeStyle, transitionType } = usePlayerProperties();
|
||||
const { setCrossfadeDuration, setCrossfadeStyle, setQueueType, setSpeed, setTransitionType } =
|
||||
usePlayerActions();
|
||||
const playbackSettings = usePlaybackSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const speedPreservePitch = useSettingsStore((state) => state.playback.preservePitch);
|
||||
const playerbarSlider = usePlayerbarSlider();
|
||||
const generalSettings = useGeneralSettings();
|
||||
|
||||
const [audioDevices, setAudioDevices] = useState<{ label: string; value: string }[]>([]);
|
||||
|
||||
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 formatPlaybackSpeedSliderLabel = (value: number) => {
|
||||
@@ -70,250 +97,163 @@ export const PlayerConfig = () => {
|
||||
id: 'queueType',
|
||||
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,
|
||||
id: 'divider-1',
|
||||
isDivider: true,
|
||||
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: (
|
||||
<SegmentedControl
|
||||
data={[
|
||||
{
|
||||
label: t('setting.playerbarSliderType', {
|
||||
context: 'optionSlider',
|
||||
label: t('setting.playbackStyle', {
|
||||
context: 'optionNormal',
|
||||
postProcess: 'titleCase',
|
||||
}),
|
||||
value: PlayerbarSliderType.SLIDER,
|
||||
value: PlayerStyle.GAPLESS,
|
||||
},
|
||||
{
|
||||
label: t('setting.playerbarSliderType', {
|
||||
context: 'optionWaveform',
|
||||
label: t('setting.playbackStyle', {
|
||||
context: 'optionCrossFade',
|
||||
postProcess: 'titleCase',
|
||||
}),
|
||||
value: PlayerbarSliderType.WAVEFORM,
|
||||
value: PlayerStyle.CROSSFADE,
|
||||
},
|
||||
]}
|
||||
onChange={(value) => {
|
||||
setSettings({
|
||||
general: {
|
||||
...generalSettings,
|
||||
playerbarSlider: {
|
||||
...playerbarSlider,
|
||||
type: value as PlayerbarSliderType,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
disabled={
|
||||
!isElectron() ||
|
||||
playbackSettings.type !== PlayerType.WEB ||
|
||||
status === PlayerStatus.PLAYING
|
||||
}
|
||||
onChange={(value) => setTransitionType(value as PlayerStyle)}
|
||||
size="sm"
|
||||
value={playerbarSlider?.type || PlayerbarSliderType.WAVEFORM}
|
||||
value={transitionType}
|
||||
w="100%"
|
||||
/>
|
||||
),
|
||||
id: 'playerbarSliderType',
|
||||
label: t('setting.playerbarSlider', { postProcess: 'titleCase' }),
|
||||
id: 'transitionType',
|
||||
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,
|
||||
id: 'divider-2',
|
||||
@@ -323,6 +263,7 @@ export const PlayerConfig = () => {
|
||||
{
|
||||
component: (
|
||||
<Slider
|
||||
defaultValue={speed}
|
||||
label={formatPlaybackSpeedSliderLabel}
|
||||
marks={[
|
||||
{ label: '0.5', value: 0.5 },
|
||||
@@ -335,49 +276,26 @@ export const PlayerConfig = () => {
|
||||
]}
|
||||
max={2}
|
||||
min={0.5}
|
||||
onChange={setSpeed}
|
||||
onChangeEnd={setSpeed}
|
||||
onDoubleClick={() => setSpeed(1)}
|
||||
step={0.01}
|
||||
styles={{
|
||||
markLabel: {},
|
||||
root: {},
|
||||
}}
|
||||
value={speed}
|
||||
w="100%"
|
||||
/>
|
||||
),
|
||||
id: 'playbackSpeed',
|
||||
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;
|
||||
}, [
|
||||
playbackSettings,
|
||||
speedPreservePitch,
|
||||
audioDevices,
|
||||
status,
|
||||
setSettings,
|
||||
currentSong,
|
||||
speed,
|
||||
@@ -388,8 +306,8 @@ export const PlayerConfig = () => {
|
||||
setTransitionType,
|
||||
crossfadeDuration,
|
||||
setCrossfadeDuration,
|
||||
playerbarSlider,
|
||||
generalSettings,
|
||||
crossfadeStyle,
|
||||
setCrossfadeStyle,
|
||||
t,
|
||||
]);
|
||||
|
||||
|
||||
@@ -7,13 +7,17 @@ import {
|
||||
SettingsSection,
|
||||
} from '/@/renderer/features/settings/components/settings-section';
|
||||
import {
|
||||
BarAlign,
|
||||
GenreTarget,
|
||||
PlayerbarSliderType,
|
||||
SideQueueType,
|
||||
useGeneralSettings,
|
||||
usePlayerbarSlider,
|
||||
useSettingsStoreActions,
|
||||
} from '/@/renderer/store/settings.store';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
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 { Slider } from '/@/shared/components/slider/slider';
|
||||
import { Switch } from '/@/shared/components/switch/switch';
|
||||
@@ -43,6 +47,7 @@ const SIDE_QUEUE_OPTIONS = [
|
||||
export const ControlSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const settings = useGeneralSettings();
|
||||
const playerbarSlider = usePlayerbarSlider();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
const controlOptions: SettingOption[] = [
|
||||
@@ -621,6 +626,202 @@ export const ControlSettings = () => {
|
||||
isHidden: false,
|
||||
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} />;
|
||||
|
||||
@@ -6,13 +6,12 @@ import {
|
||||
SettingOption,
|
||||
SettingsSection,
|
||||
} 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 { Select } from '/@/shared/components/select/select';
|
||||
import { Slider } from '/@/shared/components/slider/slider';
|
||||
import { Switch } from '/@/shared/components/switch/switch';
|
||||
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;
|
||||
|
||||
@@ -27,9 +26,6 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const status = usePlayerStatus();
|
||||
|
||||
const { crossfadeDuration, transitionType } = usePlayerProperties();
|
||||
const { setCrossfadeDuration, setTransitionType } = usePlayerActions();
|
||||
|
||||
const [audioDevices, setAudioDevices] = useState<{ label: string; value: string }[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -98,41 +94,6 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
|
||||
isHidden: !isElectron() || settings.type !== PlayerType.WEB,
|
||||
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: (
|
||||
<Switch
|
||||
@@ -174,71 +135,6 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
|
||||
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} />;
|
||||
|
||||
@@ -15,6 +15,7 @@ import { idbStateStorage } from '/@/renderer/store/utils';
|
||||
import { shuffleInPlace } from '/@/renderer/utils/shuffle';
|
||||
import { PlayerData, QueueData, QueueSong, Song } from '/@/shared/types/domain-types';
|
||||
import {
|
||||
CrossfadeStyle,
|
||||
Play,
|
||||
PlayerQueueType,
|
||||
PlayerRepeat,
|
||||
@@ -56,6 +57,7 @@ interface Actions {
|
||||
moveSelectedToNext: (items: QueueSong[]) => void;
|
||||
moveSelectedToTop: (items: QueueSong[]) => void;
|
||||
setCrossfadeDuration: (duration: number) => void;
|
||||
setCrossfadeStyle: (style: CrossfadeStyle) => void;
|
||||
setQueueType: (queueType: PlayerQueueType) => void;
|
||||
setRepeat: (repeat: PlayerRepeat) => void;
|
||||
setShuffle: (shuffle: PlayerShuffle) => void;
|
||||
@@ -77,6 +79,7 @@ interface GroupedQueue {
|
||||
interface State {
|
||||
player: {
|
||||
crossfadeDuration: number;
|
||||
crossfadeStyle: CrossfadeStyle;
|
||||
index: number;
|
||||
muted: boolean;
|
||||
playerNum: 1 | 2;
|
||||
@@ -95,6 +98,7 @@ interface State {
|
||||
const initialState: State = {
|
||||
player: {
|
||||
crossfadeDuration: 5,
|
||||
crossfadeStyle: CrossfadeStyle.EQUAL_POWER,
|
||||
index: -1,
|
||||
muted: false,
|
||||
playerNum: 1,
|
||||
@@ -1048,6 +1052,11 @@ export const usePlayerStoreBase = create<PlayerState>()(
|
||||
state.player.crossfadeDuration = normalizedDuration;
|
||||
});
|
||||
},
|
||||
setCrossfadeStyle: (style: CrossfadeStyle) => {
|
||||
set((state) => {
|
||||
state.player.crossfadeStyle = style;
|
||||
});
|
||||
},
|
||||
setQueueType: (queueType: PlayerQueueType) => {
|
||||
set((state) => {
|
||||
// From default -> priority, move all items from default to priority
|
||||
@@ -1245,6 +1254,7 @@ export const usePlayerActions = () => {
|
||||
moveSelectedToNext: state.moveSelectedToNext,
|
||||
moveSelectedToTop: state.moveSelectedToTop,
|
||||
setCrossfadeDuration: state.setCrossfadeDuration,
|
||||
setCrossfadeStyle: state.setCrossfadeStyle,
|
||||
setQueueType: state.setQueueType,
|
||||
setRepeat: state.setRepeat,
|
||||
setShuffle: state.setShuffle,
|
||||
@@ -1403,6 +1413,7 @@ export const usePlayerProperties = () => {
|
||||
return usePlayerStoreBase(
|
||||
useShallow((state) => ({
|
||||
crossfadeDuration: state.player.crossfadeDuration,
|
||||
crossfadeStyle: state.player.crossfadeStyle,
|
||||
isMuted: state.player.muted,
|
||||
playerNum: state.player.playerNum,
|
||||
queueType: state.player.queueType,
|
||||
|
||||
@@ -25,14 +25,12 @@ import { sanitizeCss } from '/@/renderer/utils/sanitize';
|
||||
import { AppTheme } from '/@/shared/themes/app-theme-types';
|
||||
import { LibraryItem, LyricSource } from '/@/shared/types/domain-types';
|
||||
import {
|
||||
CrossfadeStyle,
|
||||
FontType,
|
||||
ItemListKey,
|
||||
ListDisplayType,
|
||||
ListPaginationType,
|
||||
Platform,
|
||||
Play,
|
||||
PlayerStyle,
|
||||
PlayerType,
|
||||
TableColumn,
|
||||
} from '/@/shared/types/types';
|
||||
@@ -306,15 +304,11 @@ const ScrobbleSettingsSchema = z.object({
|
||||
|
||||
const PlaybackSettingsSchema = z.object({
|
||||
audioDeviceId: z.string().nullable().optional(),
|
||||
crossfadeDuration: z.number(),
|
||||
crossfadeStyle: z.nativeEnum(CrossfadeStyle),
|
||||
mediaSession: z.boolean(),
|
||||
mpvExtraParameters: z.array(z.string()),
|
||||
mpvProperties: MpvSettingsSchema,
|
||||
muted: z.boolean(),
|
||||
preservePitch: z.boolean(),
|
||||
scrobble: ScrobbleSettingsSchema,
|
||||
style: z.nativeEnum(PlayerStyle),
|
||||
transcode: TranscodingConfigSchema,
|
||||
type: z.nativeEnum(PlayerType),
|
||||
webAudio: z.boolean(),
|
||||
@@ -1090,8 +1084,6 @@ const initialState: SettingsState = {
|
||||
},
|
||||
playback: {
|
||||
audioDeviceId: undefined,
|
||||
crossfadeDuration: 5,
|
||||
crossfadeStyle: CrossfadeStyle.EQUALPOWER,
|
||||
mediaSession: false,
|
||||
mpvExtraParameters: [],
|
||||
mpvProperties: {
|
||||
@@ -1104,7 +1096,6 @@ const initialState: SettingsState = {
|
||||
replayGainMode: 'no',
|
||||
replayGainPreampDB: 0,
|
||||
},
|
||||
muted: false,
|
||||
preservePitch: true,
|
||||
scrobble: {
|
||||
enabled: true,
|
||||
@@ -1112,7 +1103,6 @@ const initialState: SettingsState = {
|
||||
scrobbleAtDuration: 240,
|
||||
scrobbleAtPercentage: 75,
|
||||
},
|
||||
style: PlayerStyle.GAPLESS,
|
||||
transcode: {
|
||||
enabled: false,
|
||||
},
|
||||
@@ -1289,10 +1279,7 @@ export const useTableSettings = (type: ItemListKey) =>
|
||||
|
||||
export const useGeneralSettings = () => useSettingsStore((state) => state.general, shallow);
|
||||
|
||||
export const usePlaybackType = () =>
|
||||
useSettingsStore((state) => {
|
||||
return state.playback.type;
|
||||
});
|
||||
export const usePlaybackType = () => useSettingsStore((state) => state.playback.type, shallow);
|
||||
|
||||
export const usePlayButtonBehavior = () =>
|
||||
useSettingsStore((state) => state.general.playButtonBehavior, shallow);
|
||||
|
||||
@@ -11,7 +11,9 @@ export interface SelectProps extends MantineSelectProps {
|
||||
}
|
||||
|
||||
export const Select = ({
|
||||
allowDeselect = false,
|
||||
classNames,
|
||||
clearable = false,
|
||||
maxWidth,
|
||||
variant = 'default',
|
||||
width,
|
||||
@@ -19,14 +21,17 @@ export const Select = ({
|
||||
}: SelectProps) => {
|
||||
return (
|
||||
<MantineSelect
|
||||
allowDeselect={allowDeselect || clearable}
|
||||
classNames={{
|
||||
dropdown: styles.dropdown,
|
||||
input: styles.input,
|
||||
label: styles.label,
|
||||
option: styles.option,
|
||||
root: styles.root,
|
||||
section: styles.section,
|
||||
...classNames,
|
||||
}}
|
||||
clearable={false}
|
||||
style={{ maxWidth, width }}
|
||||
variant={variant}
|
||||
withCheckIcon={false}
|
||||
|
||||
@@ -95,12 +95,10 @@ export enum AuthState {
|
||||
}
|
||||
|
||||
export enum CrossfadeStyle {
|
||||
CONSTANT_POWER = 'constantPower',
|
||||
CONSTANT_POWER_SLOW_CUT = 'constantPowerSlowCut',
|
||||
CONSTANT_POWER_SLOW_FADE = 'constantPowerSlowFade',
|
||||
DIPPED = 'dipped',
|
||||
EQUALPOWER = 'equalPower',
|
||||
EQUAL_POWER = 'equalPower',
|
||||
EXPONENTIAL = 'exponential',
|
||||
LINEAR = 'linear',
|
||||
S_CURVE = 'sCurve',
|
||||
}
|
||||
|
||||
export enum FontType {
|
||||
|
||||
Reference in New Issue
Block a user