mirror of
https://github.com/antebudimir/feishin.git
synced 2025-12-31 10:03:33 +00:00
- Add audio channels setting (auto/mono/stereo) to MPV properties - Implement UI controls in settings panel and player right controls - Enable immediate MPV property application when setting changes - Update default playback type to LOCAL and sample rate to 48kHz
445 lines
16 KiB
TypeScript
445 lines
16 KiB
TypeScript
import isElectron from 'is-electron';
|
|
import { useEffect, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
import {
|
|
SettingOption,
|
|
SettingsSection,
|
|
} from '/@/renderer/features/settings/components/settings-section';
|
|
import { usePlayerControls, usePlayerStore, useQueueControls } from '/@/renderer/store';
|
|
import {
|
|
SettingsState,
|
|
usePlaybackSettings,
|
|
useSettingsStore,
|
|
useSettingsStoreActions,
|
|
} from '/@/renderer/store/settings.store';
|
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
|
import { Group } from '/@/shared/components/group/group';
|
|
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
|
import { Select } from '/@/shared/components/select/select';
|
|
import { Stack } from '/@/shared/components/stack/stack';
|
|
import { Switch } from '/@/shared/components/switch/switch';
|
|
import { TextInput } from '/@/shared/components/text-input/text-input';
|
|
import { Text } from '/@/shared/components/text/text';
|
|
import { Textarea } from '/@/shared/components/textarea/textarea';
|
|
import { PlaybackType } from '/@/shared/types/types';
|
|
|
|
const localSettings = isElectron() ? window.api.localSettings : null;
|
|
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
|
|
|
|
export const getMpvSetting = (
|
|
key: keyof SettingsState['playback']['mpvProperties'],
|
|
value: any,
|
|
) => {
|
|
switch (key) {
|
|
case 'audioChannels':
|
|
return { 'audio-channels': value };
|
|
case 'audioExclusiveMode':
|
|
return { 'audio-exclusive': value || 'no' };
|
|
case 'audioSampleRateHz':
|
|
return { 'audio-samplerate': value };
|
|
case 'gaplessAudio':
|
|
return { 'gapless-audio': value || 'weak' };
|
|
case 'replayGainClip':
|
|
return { 'replaygain-clip': value || 'no' };
|
|
case 'replayGainFallbackDB':
|
|
return { 'replaygain-fallback': value };
|
|
case 'replayGainMode':
|
|
return { replaygain: value || 'no' };
|
|
case 'replayGainPreampDB':
|
|
return { 'replaygain-preamp': value || 0 };
|
|
default:
|
|
return { 'audio-format': value };
|
|
}
|
|
};
|
|
|
|
export const getMpvProperties = (settings: SettingsState['playback']['mpvProperties']) => {
|
|
const properties: Record<string, any> = {
|
|
'audio-channels': settings.audioChannels || 'stereo',
|
|
'audio-exclusive': settings.audioExclusiveMode || 'no',
|
|
'audio-samplerate':
|
|
settings.audioSampleRateHz === 0 ? undefined : settings.audioSampleRateHz,
|
|
'gapless-audio': settings.gaplessAudio || 'weak',
|
|
replaygain: settings.replayGainMode || 'no',
|
|
'replaygain-clip': settings.replayGainClip || 'no',
|
|
'replaygain-fallback': settings.replayGainFallbackDB,
|
|
'replaygain-preamp': settings.replayGainPreampDB || 0,
|
|
};
|
|
|
|
Object.keys(properties).forEach((key) =>
|
|
properties[key] === undefined ? delete properties[key] : {},
|
|
);
|
|
|
|
return properties;
|
|
};
|
|
|
|
export const MpvSettings = () => {
|
|
const { t } = useTranslation();
|
|
const settings = usePlaybackSettings();
|
|
const { setSettings } = useSettingsStoreActions();
|
|
const { pause } = usePlayerControls();
|
|
const { clearQueue } = useQueueControls();
|
|
|
|
const [mpvPath, setMpvPath] = useState(
|
|
(localSettings?.get('mpv_path') as string | undefined) || '',
|
|
);
|
|
|
|
const handleSetMpvPath = async (clear?: boolean) => {
|
|
if (clear) {
|
|
localSettings?.set('mpv_path', undefined);
|
|
setMpvPath('');
|
|
return;
|
|
}
|
|
|
|
const result = await localSettings?.openFileSelector();
|
|
|
|
if (result === null) {
|
|
localSettings?.set('mpv_path', undefined);
|
|
setMpvPath('');
|
|
return;
|
|
}
|
|
|
|
localSettings?.set('mpv_path', result);
|
|
setMpvPath(result);
|
|
};
|
|
|
|
useEffect(() => {
|
|
const getMpvPath = async () => {
|
|
if (!localSettings) return setMpvPath('');
|
|
const mpvPath = (await localSettings.get('mpv_path')) as string;
|
|
return setMpvPath(mpvPath);
|
|
};
|
|
|
|
getMpvPath();
|
|
}, []);
|
|
|
|
const handleSetMpvProperty = (
|
|
setting: keyof SettingsState['playback']['mpvProperties'],
|
|
value: any,
|
|
) => {
|
|
setSettings({
|
|
playback: {
|
|
...settings,
|
|
mpvProperties: {
|
|
...settings.mpvProperties,
|
|
[setting]: value,
|
|
},
|
|
},
|
|
});
|
|
|
|
const mpvSetting = getMpvSetting(setting, value);
|
|
|
|
mpvPlayer?.setProperties(mpvSetting);
|
|
};
|
|
|
|
const handleReloadMpv = () => {
|
|
pause();
|
|
clearQueue();
|
|
|
|
const extraParameters = useSettingsStore.getState().playback.mpvExtraParameters;
|
|
const properties: Record<string, any> = {
|
|
speed: usePlayerStore.getState().speed,
|
|
...getMpvProperties(useSettingsStore.getState().playback.mpvProperties),
|
|
};
|
|
mpvPlayer?.restart({
|
|
binaryPath: mpvPath || undefined,
|
|
extraParameters,
|
|
properties,
|
|
});
|
|
};
|
|
|
|
const handleSetExtraParameters = (data: string[]) => {
|
|
setSettings({
|
|
playback: {
|
|
...settings,
|
|
mpvExtraParameters: data,
|
|
},
|
|
});
|
|
};
|
|
|
|
const options: SettingOption[] = [
|
|
{
|
|
control: (
|
|
<Group gap="sm">
|
|
<ActionIcon
|
|
icon="refresh"
|
|
onClick={handleReloadMpv}
|
|
tooltip={{
|
|
label: t('common.reload', { postProcess: 'titleCase' }),
|
|
openDelay: 0,
|
|
}}
|
|
variant="subtle"
|
|
/>
|
|
<TextInput
|
|
onChange={(e) => {
|
|
setMpvPath(e.currentTarget.value);
|
|
|
|
// Transform backslashes to forward slashes
|
|
const transformedValue = e.currentTarget.value.replace(/\\/g, '/');
|
|
localSettings?.set('mpv_path', transformedValue);
|
|
}}
|
|
onClick={() => handleSetMpvPath()}
|
|
rightSection={
|
|
mpvPath && (
|
|
<ActionIcon
|
|
icon="x"
|
|
onClick={() => handleSetMpvPath(true)}
|
|
variant="transparent"
|
|
/>
|
|
)
|
|
}
|
|
value={mpvPath}
|
|
width={200}
|
|
/>
|
|
</Group>
|
|
),
|
|
description: t('setting.mpvExecutablePath', {
|
|
context: 'description',
|
|
postProcess: 'sentenceCase',
|
|
}),
|
|
isHidden: settings.type !== PlaybackType.LOCAL,
|
|
note: 'Restart required',
|
|
title: t('setting.mpvExecutablePath', { postProcess: 'sentenceCase' }),
|
|
},
|
|
{
|
|
control: (
|
|
<Stack gap="xs">
|
|
<Textarea
|
|
autosize
|
|
defaultValue={settings.mpvExtraParameters.join('\n')}
|
|
minRows={4}
|
|
onBlur={(e) => {
|
|
handleSetExtraParameters(e.currentTarget.value.split('\n'));
|
|
}}
|
|
placeholder={`(${t('setting.mpvExtraParameters', {
|
|
context: 'help',
|
|
postProcess: 'sentenceCase',
|
|
})}):\n--gapless-audio=weak\n--prefetch-playlist=yes`}
|
|
width={225}
|
|
/>
|
|
</Stack>
|
|
),
|
|
description: (
|
|
<Stack gap={0}>
|
|
<Text isMuted isNoSelect size="sm">
|
|
{t('setting.mpvExtraParameters', {
|
|
context: 'description',
|
|
postProcess: 'sentenceCase',
|
|
})}
|
|
</Text>
|
|
<Text size="sm">
|
|
<a
|
|
href="https://mpv.io/manual/stable/#audio"
|
|
rel="noreferrer"
|
|
target="_blank"
|
|
>
|
|
https://mpv.io/manual/stable/#audio
|
|
</a>
|
|
</Text>
|
|
</Stack>
|
|
),
|
|
isHidden: settings.type !== PlaybackType.LOCAL,
|
|
note: t('common.restartRequired', {
|
|
postProcess: 'sentenceCase',
|
|
}),
|
|
title: t('setting.mpvExtraParameters', {
|
|
postProcess: 'sentenceCase',
|
|
}),
|
|
},
|
|
];
|
|
|
|
const generalOptions: SettingOption[] = [
|
|
{
|
|
control: (
|
|
<Select
|
|
data={[
|
|
{ label: t('common.no', { postProcess: 'titleCase' }), value: 'no' },
|
|
{ label: t('common.yes', { postProcess: 'titleCase' }), value: 'yes' },
|
|
{
|
|
label: t('setting.gaplessAudio', {
|
|
context: 'optionWeak',
|
|
postProcess: 'sentenceCase',
|
|
}),
|
|
value: 'weak',
|
|
},
|
|
]}
|
|
defaultValue={settings.mpvProperties.gaplessAudio}
|
|
onChange={(e) => handleSetMpvProperty('gaplessAudio', e)}
|
|
/>
|
|
),
|
|
description: t('setting.gaplessAudio', {
|
|
context: 'description',
|
|
postProcess: 'sentenceCase',
|
|
}),
|
|
isHidden: settings.type !== PlaybackType.LOCAL,
|
|
title: t('setting.gaplessAudio', { postProcess: 'sentenceCase' }),
|
|
},
|
|
{
|
|
control: (
|
|
<Select
|
|
data={[
|
|
{ label: 'Mono', value: 'mono' },
|
|
{ label: 'Stereo', value: 'stereo' },
|
|
]}
|
|
defaultValue={settings.mpvProperties.audioChannels || 'stereo'}
|
|
onChange={(e) => handleSetMpvProperty('audioChannels', e)}
|
|
/>
|
|
),
|
|
description:
|
|
'Select the audio channel mode. Stereo is the default, mono downmixes to a single channel.',
|
|
isHidden: settings.type !== PlaybackType.LOCAL,
|
|
title: 'Audio Channels',
|
|
},
|
|
{
|
|
control: (
|
|
<NumberInput
|
|
defaultValue={settings.mpvProperties.audioSampleRateHz || undefined}
|
|
max={192000}
|
|
min={0}
|
|
onBlur={(e) => {
|
|
const value = Number(e.currentTarget.value);
|
|
// Setting a value of `undefined` causes an error for MPV. Use 0 instead
|
|
handleSetMpvProperty('audioSampleRateHz', value >= 8000 ? value : value);
|
|
}}
|
|
placeholder="48000"
|
|
rightSection={<Text size="xs">Hz</Text>}
|
|
width={100}
|
|
/>
|
|
),
|
|
description: t('setting.sampleRate', {
|
|
context: 'description',
|
|
postProcess: 'sentenceCase',
|
|
}),
|
|
note: 'Page refresh required for web player',
|
|
title: t('setting.sampleRate', { postProcess: 'sentenceCase' }),
|
|
},
|
|
{
|
|
control: (
|
|
<Switch
|
|
defaultChecked={settings.mpvProperties.audioExclusiveMode === 'yes'}
|
|
onChange={(e) =>
|
|
handleSetMpvProperty(
|
|
'audioExclusiveMode',
|
|
e.currentTarget.checked ? 'yes' : 'no',
|
|
)
|
|
}
|
|
/>
|
|
),
|
|
|
|
description: t('setting.audioExclusiveMode', {
|
|
context: 'description',
|
|
postProcess: 'sentenceCase',
|
|
}),
|
|
isHidden: settings.type !== PlaybackType.LOCAL,
|
|
title: t('setting.audioExclusiveMode', { postProcess: 'sentenceCase' }),
|
|
},
|
|
];
|
|
|
|
const replayGainOptions: SettingOption[] = [
|
|
{
|
|
control: (
|
|
<Select
|
|
data={[
|
|
{
|
|
label: t('setting.replayGainMode', {
|
|
context: 'optionNone',
|
|
postProcess: 'titleCase',
|
|
}),
|
|
value: 'no',
|
|
},
|
|
{
|
|
label: t('setting.replayGainMode', {
|
|
context: 'optionTrack',
|
|
postProcess: 'titleCase',
|
|
}),
|
|
value: 'track',
|
|
},
|
|
{
|
|
label: t('setting.replayGainMode', {
|
|
context: 'optionAlbum',
|
|
postProcess: 'titleCase',
|
|
}),
|
|
value: 'album',
|
|
},
|
|
]}
|
|
defaultValue={settings.mpvProperties.replayGainMode}
|
|
onChange={(e) => handleSetMpvProperty('replayGainMode', e)}
|
|
/>
|
|
),
|
|
description: t('setting.replayGainMode', {
|
|
context: 'description',
|
|
postProcess: 'sentenceCase',
|
|
ReplayGain: 'ReplayGain',
|
|
}),
|
|
note: t('common.restartRequired', { postProcess: 'sentenceCase' }),
|
|
title: t('setting.replayGainMode', {
|
|
postProcess: 'sentenceCase',
|
|
ReplayGain: 'ReplayGain',
|
|
}),
|
|
},
|
|
{
|
|
control: (
|
|
<NumberInput
|
|
defaultValue={settings.mpvProperties.replayGainPreampDB}
|
|
onChange={(e) => handleSetMpvProperty('replayGainPreampDB', e)}
|
|
width={75}
|
|
/>
|
|
),
|
|
description: t('setting.replayGainMode', {
|
|
context: 'description',
|
|
postProcess: 'sentenceCase',
|
|
ReplayGain: 'ReplayGain',
|
|
}),
|
|
title: t('setting.replayGainPreamp', {
|
|
postProcess: 'sentenceCase',
|
|
ReplayGain: 'ReplayGain',
|
|
}),
|
|
},
|
|
{
|
|
control: (
|
|
<Switch
|
|
defaultChecked={settings.mpvProperties.replayGainClip}
|
|
onChange={(e) =>
|
|
handleSetMpvProperty('replayGainClip', e.currentTarget.checked)
|
|
}
|
|
/>
|
|
),
|
|
description: t('setting.replayGainClipping', {
|
|
context: 'description',
|
|
postProcess: 'sentenceCase',
|
|
ReplayGain: 'ReplayGain',
|
|
}),
|
|
title: t('setting.replayGainClipping', {
|
|
postProcess: 'sentenceCase',
|
|
ReplayGain: 'ReplayGain',
|
|
}),
|
|
},
|
|
{
|
|
control: (
|
|
<NumberInput
|
|
defaultValue={settings.mpvProperties.replayGainFallbackDB}
|
|
onBlur={(e) =>
|
|
handleSetMpvProperty('replayGainFallbackDB', Number(e.currentTarget.value))
|
|
}
|
|
width={75}
|
|
/>
|
|
),
|
|
description: t('setting.replayGainFallback', {
|
|
postProcess: 'sentenceCase',
|
|
ReplayGain: 'ReplayGain',
|
|
}),
|
|
title: t('setting.replayGainFallback', {
|
|
postProcess: 'sentenceCase',
|
|
ReplayGain: 'ReplayGain',
|
|
}),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<>
|
|
<SettingsSection options={options} />
|
|
<SettingsSection options={generalOptions} />
|
|
<SettingsSection options={replayGainOptions} />
|
|
</>
|
|
);
|
|
};
|