reorganize and redesign settings

This commit is contained in:
jeffvli
2025-11-23 18:15:38 -08:00
parent 7cc5ccd2c5
commit a2926ef47e
26 changed files with 629 additions and 540 deletions
@@ -20,7 +20,7 @@ const getAudioDevice = async () => {
return (devices || []).filter((dev: MediaDeviceInfo) => dev.kind === 'audiooutput');
};
export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) => {
export const AudioSettings = () => {
const { t } = useTranslation();
const settings = usePlaybackSettings();
const { setSettings } = useSettingsStoreActions();
@@ -137,5 +137,10 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
},
];
return <SettingsSection divider={!hasFancyAudio} options={audioOptions} />;
return (
<SettingsSection
options={audioOptions}
title={t('page.setting.audio', { postProcess: 'sentenceCase' })}
/>
);
};
@@ -1,242 +0,0 @@
import isElectron from 'is-electron';
import { useTranslation } from 'react-i18next';
import { languages } from '/@/i18n/i18n';
import {
SettingOption,
SettingsSection,
} from '/@/renderer/features/settings/components/settings-section';
import { useLyricsSettings, useSettingsStoreActions } from '/@/renderer/store';
import { MultiSelect } from '/@/shared/components/multi-select/multi-select';
import { NumberInput } from '/@/shared/components/number-input/number-input';
import { Select } from '/@/shared/components/select/select';
import { Switch } from '/@/shared/components/switch/switch';
import { TextInput } from '/@/shared/components/text-input/text-input';
import { LyricSource } from '/@/shared/types/domain-types';
const localSettings = isElectron() ? window.api.localSettings : null;
export const LyricSettings = () => {
const { t } = useTranslation();
const settings = useLyricsSettings();
const { setSettings } = useSettingsStoreActions();
const lyricOptions: SettingOption[] = [
{
control: (
<Switch
aria-label="Follow lyrics"
defaultChecked={settings.follow}
onChange={(e) => {
setSettings({
lyrics: {
...settings,
follow: e.currentTarget.checked,
},
});
}}
/>
),
description: t('setting.followLyric', {
context: 'description',
postProcess: 'sentenceCase',
}),
title: t('setting.followLyric', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
aria-label="Prefer local lyrics"
defaultChecked={settings.preferLocalLyrics}
onChange={(e) => {
setSettings({
lyrics: {
...settings,
preferLocalLyrics: e.currentTarget.checked,
},
});
}}
/>
),
description: t('setting.preferLocalLyrics', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !isElectron(),
title: t('setting.preferLocalLyrics', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
aria-label="Enable fetching lyrics"
defaultChecked={settings.fetch}
onChange={(e) => {
setSettings({
lyrics: {
...settings,
fetch: e.currentTarget.checked,
},
});
}}
/>
),
description: t('setting.lyricFetch', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !isElectron(),
title: t('setting.lyricFetch', { postProcess: 'sentenceCase' }),
},
{
control: (
<MultiSelect
aria-label="Lyric providers"
clearable
data={Object.values(LyricSource)}
defaultValue={settings.sources}
onChange={(e: string[]) => {
localSettings?.set('lyrics', e);
setSettings({
lyrics: {
...settings,
sources: e.map((source) => source as LyricSource),
},
});
}}
width={300}
/>
),
description: t('setting.lyricFetchProvider', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !isElectron(),
title: t('setting.lyricFetchProvider', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
aria-label="Enable NetEase translations"
defaultChecked={settings.enableNeteaseTranslation}
onChange={(e) => {
const isChecked = e.currentTarget.checked;
setSettings({
lyrics: {
...settings,
enableNeteaseTranslation: e.currentTarget.checked,
},
});
localSettings?.set('enableNeteaseTranslation', isChecked);
}}
/>
),
description: t('setting.neteaseTranslation', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !isElectron(),
title: t('setting.neteaseTranslation', { postProcess: 'sentenceCase' }),
},
{
control: (
<NumberInput
defaultValue={settings.delayMs}
onBlur={(e) => {
const value = Number(e.currentTarget.value);
setSettings({
lyrics: {
...settings,
delayMs: value,
},
});
}}
step={10}
width={100}
/>
),
description: t('setting.lyricOffset', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !isElectron(),
title: t('setting.lyricOffset', { postProcess: 'sentenceCase' }),
},
{
control: (
<Select
data={languages}
onChange={(value) => {
setSettings({ lyrics: { ...settings, translationTargetLanguage: value } });
}}
value={settings.translationTargetLanguage}
/>
),
description: t('setting.translationTargetLanguage', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !isElectron(),
title: t('setting.translationTargetLanguage', { postProcess: 'sentenceCase' }),
},
{
control: (
<Select
clearable
data={['Microsoft Azure', 'Google Cloud']}
onChange={(value) => {
setSettings({ lyrics: { ...settings, translationApiProvider: value } });
}}
value={settings.translationApiProvider}
/>
),
description: t('setting.translationApiProvider', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !isElectron(),
title: t('setting.translationApiProvider', { postProcess: 'sentenceCase' }),
},
{
control: (
<TextInput
onChange={(e) => {
setSettings({
lyrics: { ...settings, translationApiKey: e.currentTarget.value },
});
}}
value={settings.translationApiKey}
/>
),
description: t('setting.translationApiKey', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !isElectron(),
title: t('setting.translationApiKey', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
aria-label="Enable auto translation"
defaultChecked={settings.enableAutoTranslation}
onChange={(e) => {
setSettings({
lyrics: {
...settings,
enableAutoTranslation: e.currentTarget.checked,
},
});
}}
/>
),
description: t('setting.enableAutoTranslation', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !isElectron(),
title: t('setting.enableAutoTranslation', { postProcess: 'sentenceCase' }),
},
];
return <SettingsSection divider={false} options={lyricOptions} />;
};
@@ -1,48 +0,0 @@
import isElectron from 'is-electron';
import { useTranslation } from 'react-i18next';
import {
SettingOption,
SettingsSection,
} from '/@/renderer/features/settings/components/settings-section';
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
import { Switch } from '/@/shared/components/switch/switch';
import { PlayerType } from '/@/shared/types/types';
const isWindows = isElectron() ? window.api.utils.isWindows() : null;
const isDesktop = isElectron();
const ipc = isElectron() ? window.api.ipc : null;
export const MediaSessionSettings = () => {
const { t } = useTranslation();
const { mediaSession, type: playbackType } = usePlaybackSettings();
const { toggleMediaSession } = useSettingsStoreActions();
function handleMediaSessionChange() {
const current = mediaSession;
toggleMediaSession();
ipc?.send('settings-set', { property: 'mediaSession', value: !current });
}
const mediaSessionOptions: SettingOption[] = [
{
control: (
<Switch
aria-label="Toggle media Session"
defaultChecked={mediaSession}
disabled={!isWindows || !isDesktop || playbackType !== PlayerType.WEB}
onChange={handleMediaSessionChange}
/>
),
description: t('setting.mediaSession', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !isWindows || !isDesktop,
note: t('common.restartRequired', { postProcess: 'sentenceCase' }),
title: t('setting.mediaSession', { postProcess: 'sentenceCase' }),
},
];
return <SettingsSection divider options={mediaSessionOptions} />;
};
@@ -2,11 +2,9 @@ import isElectron from 'is-electron';
import { lazy, Suspense, useMemo } from 'react';
import { AudioSettings } from '/@/renderer/features/settings/components/playback/audio-settings';
import { LyricSettings } from '/@/renderer/features/settings/components/playback/lyric-settings';
import { MediaSessionSettings } from '/@/renderer/features/settings/components/playback/media-session-settings';
import { ScrobbleSettings } from '/@/renderer/features/settings/components/playback/scrobble-settings';
import { TranscodeSettings } from '/@/renderer/features/settings/components/playback/transcode-settings';
import { useSettingsStore } from '/@/renderer/store';
import { Divider } from '/@/shared/components/divider/divider';
import { Stack } from '/@/shared/components/stack/stack';
import { PlayerType } from '/@/shared/types/types';
@@ -29,12 +27,10 @@ export const PlaybackTab = () => {
return (
<Stack gap="md">
<AudioSettings hasFancyAudio={hasFancyAudio} />
<AudioSettings />
<Suspense fallback={<></>}>{hasFancyAudio && <MpvSettings />}</Suspense>
<Divider />
<TranscodeSettings />
<MediaSessionSettings />
<ScrobbleSettings />
<LyricSettings />
</Stack>
);
};
@@ -1,148 +0,0 @@
import { useTranslation } from 'react-i18next';
import {
SettingOption,
SettingsSection,
} from '/@/renderer/features/settings/components/settings-section';
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
import { NumberInput } from '/@/shared/components/number-input/number-input';
import { Slider } from '/@/shared/components/slider/slider';
import { Switch } from '/@/shared/components/switch/switch';
import { toast } from '/@/shared/components/toast/toast';
export const ScrobbleSettings = () => {
const { t } = useTranslation();
const settings = usePlaybackSettings();
const { setSettings } = useSettingsStoreActions();
const scrobbleOptions: SettingOption[] = [
{
control: (
<Switch
aria-label="Toggle scrobble"
defaultChecked={settings.scrobble.enabled}
onChange={(e) => {
setSettings({
playback: {
...settings,
scrobble: {
...settings.scrobble,
enabled: e.currentTarget.checked,
},
},
});
}}
/>
),
description: t('setting.scrobble', {
context: 'description',
postProcess: 'sentenceCase',
}),
title: t('setting.scrobble', { postProcess: 'sentenceCase' }),
},
{
control: (
<Slider
aria-label="Scrobble percentage"
defaultValue={settings.scrobble.scrobbleAtPercentage}
label={`${settings.scrobble.scrobbleAtPercentage}%`}
max={90}
min={25}
onChange={(e) => {
setSettings({
playback: {
...settings,
scrobble: {
...settings.scrobble,
scrobbleAtPercentage: e,
},
},
});
}}
w={100}
/>
),
description: t('setting.minimumScrobblePercentage', {
context: 'description',
postProcess: 'sentenceCase',
}),
title: t('setting.minimumScrobblePercentage', { postProcess: 'sentenceCase' }),
},
{
control: (
<NumberInput
aria-label="Scrobble duration in seconds"
defaultValue={settings.scrobble.scrobbleAtDuration}
max={1200}
min={0}
onChange={(e) => {
if (e === '') return;
setSettings({
playback: {
...settings,
scrobble: {
...settings.scrobble,
scrobbleAtDuration: Number(e),
},
},
});
}}
width={75}
/>
),
description: t('setting.minimumScrobbleSeconds', {
context: 'description',
postProcess: 'sentenceCase',
}),
title: t('setting.minimumScrobbleSeconds', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
aria-label="Toggle notify"
defaultChecked={settings.scrobble.notify}
onChange={async (e) => {
if (Notification.permission === 'denied') {
toast.error({
message: t('error.notificationDenied', {
postProcess: 'sentenceCase',
}),
});
return;
}
if (Notification.permission !== 'granted') {
const permissions = await Notification.requestPermission();
if (permissions !== 'granted') {
toast.error({
message: t('error.notificationDenied', {
postProcess: 'sentenceCase',
}),
});
return;
}
}
setSettings({
playback: {
...settings,
scrobble: {
...settings.scrobble,
notify: e.currentTarget.checked,
},
},
});
}}
/>
),
description: t('setting.notify', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !('Notification' in window),
title: t('setting.notify', { postProcess: 'sentenceCase' }),
},
];
return <SettingsSection options={scrobbleOptions} />;
};
@@ -86,5 +86,10 @@ export const TranscodeSettings = () => {
},
];
return <SettingsSection divider options={transcodeOptions} />;
return (
<SettingsSection
options={transcodeOptions}
title={t('page.setting.transcoding', { postProcess: 'sentenceCase' })}
/>
);
};