mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-17 08:54:27 +02:00
reorganize and redesign settings
This commit is contained in:
@@ -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' })}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user