split lyrics settings by key (#1389)

This commit is contained in:
jeffvli
2025-12-25 01:29:31 -08:00
parent 5eb2cff6e9
commit 8205eeed22
16 changed files with 618 additions and 86 deletions
+1
View File
@@ -569,6 +569,7 @@
"scrobble": "scrobble",
"audio": "audio",
"lyrics": "lyrics",
"lyricsDisplay": "lyrics display",
"transcoding": "transcoding",
"discord": "discord",
"logger": "logger",
@@ -0,0 +1,388 @@
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 {
useLyricsDisplaySettings,
useLyricsSettings,
useSettingsStore,
useSettingsStoreActions,
} from '/@/renderer/store';
import { Fieldset } from '/@/shared/components/fieldset/fieldset';
import { MultiSelect } from '/@/shared/components/multi-select/multi-select';
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 { 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 { LyricSource } from '/@/shared/types/domain-types';
const localSettings = isElectron() ? window.api.localSettings : null;
interface LyricsSettingsFormProps {
settingsKey: string;
}
export const LyricsSettingsForm = ({ settingsKey }: LyricsSettingsFormProps) => {
const { t } = useTranslation();
const lyricsSettings = useLyricsSettings();
const displaySettings = useLyricsDisplaySettings(settingsKey);
const allLyricsDisplay = useSettingsStore((state) => state.lyricsDisplay);
const { setSettings } = useSettingsStoreActions();
const updateLyricsSetting = (updates: Partial<typeof lyricsSettings>) => {
setSettings({
lyrics: {
...lyricsSettings,
...updates,
},
});
};
const updateDisplaySetting = (updates: Partial<typeof displaySettings>) => {
setSettings({
lyricsDisplay: {
...allLyricsDisplay,
[settingsKey]: {
...displaySettings,
...updates,
},
},
});
};
const displayOptions: SettingOption[] = [
{
control: (
<NumberInput
onBlur={(e) => {
const value = Number(e.currentTarget.value);
updateDisplaySetting({ fontSize: value });
}}
rightSection={
<Text pr="md" size="sm">
px
</Text>
}
step={1}
value={displaySettings.fontSize}
width={100}
/>
),
description: '',
title: t(
`${t('page.fullscreenPlayer.config.lyricSize')} (${t('page.fullscreenPlayer.config.synchronized')})`,
{ postProcess: 'sentenceCase' },
),
},
{
control: (
<NumberInput
onBlur={(e) => {
const value = Number(e.currentTarget.value);
updateDisplaySetting({ fontSizeUnsync: value });
}}
rightSection={
<Text pr="md" size="sm">
px
</Text>
}
step={1}
value={displaySettings.fontSizeUnsync}
width={100}
/>
),
description: '',
title: t(
`${t('page.fullscreenPlayer.config.lyricSize')} (${t('page.fullscreenPlayer.config.unsynchronized')})`,
{ postProcess: 'sentenceCase' },
),
},
{
control: (
<NumberInput
onBlur={(e) => {
const value = Number(e.currentTarget.value);
updateDisplaySetting({ gap: value });
}}
rightSection={
<Text pr="md" size="sm">
px
</Text>
}
step={1}
value={displaySettings.gap}
width={100}
/>
),
description: '',
title: t(
`${t('page.fullscreenPlayer.config.lyricGap')} (${t('page.fullscreenPlayer.config.synchronized')})`,
{ postProcess: 'sentenceCase' },
),
},
{
control: (
<NumberInput
onBlur={(e) => {
const value = Number(e.currentTarget.value);
updateDisplaySetting({ gapUnsync: value });
}}
rightSection={
<Text pr="md" size="sm">
px
</Text>
}
step={1}
value={displaySettings.gapUnsync}
width={100}
/>
),
description: '',
title: t(
`${t('page.fullscreenPlayer.config.lyricGap')} (${t('page.fullscreenPlayer.config.unsynchronized')})`,
{ postProcess: 'sentenceCase' },
),
},
{
control: (
<SegmentedControl
data={[
{ label: t('common.left', { postProcess: 'titleCase' }), value: 'left' },
{
label: t('common.center', { postProcess: 'titleCase' }),
value: 'center',
},
{ label: t('common.right', { postProcess: 'titleCase' }), value: 'right' },
]}
onChange={(value) =>
updateLyricsSetting({ alignment: value as 'center' | 'left' | 'right' })
}
value={lyricsSettings.alignment}
/>
),
description: '',
title: t('page.fullscreenPlayer.config.lyricAlignment', {
postProcess: 'sentenceCase',
}),
},
{
control: (
<Switch
aria-label="Follow lyrics"
defaultChecked={lyricsSettings.follow}
onChange={(e) => updateLyricsSetting({ follow: e.currentTarget.checked })}
/>
),
description: '',
title: t('page.fullscreenPlayer.config.followCurrentLyric', {
postProcess: 'sentenceCase',
}),
},
{
control: (
<Switch
aria-label="Show match"
defaultChecked={lyricsSettings.showMatch}
onChange={(e) => updateLyricsSetting({ showMatch: e.currentTarget.checked })}
/>
),
description: '',
title: t('page.fullscreenPlayer.config.showLyricMatch', {
postProcess: 'sentenceCase',
}),
},
{
control: (
<Switch
aria-label="Show provider"
defaultChecked={lyricsSettings.showProvider}
onChange={(e) => updateLyricsSetting({ showProvider: e.currentTarget.checked })}
/>
),
description: '',
title: t('page.fullscreenPlayer.config.showLyricProvider', {
postProcess: 'sentenceCase',
}),
},
];
const lyricOptions: SettingOption[] = [
{
control: (
<Switch
aria-label="Prefer local lyrics"
defaultChecked={lyricsSettings.preferLocalLyrics}
onChange={(e) =>
updateLyricsSetting({ 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={lyricsSettings.fetch}
onChange={(e) => updateLyricsSetting({ 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={lyricsSettings.sources}
onChange={(e: string[]) => {
localSettings?.set('lyrics', e);
updateLyricsSetting({ 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={lyricsSettings.enableNeteaseTranslation}
onChange={(e) => {
const isChecked = e.currentTarget.checked;
updateLyricsSetting({ enableNeteaseTranslation: isChecked });
localSettings?.set('enableNeteaseTranslation', isChecked);
}}
/>
),
description: t('setting.neteaseTranslation', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !isElectron(),
title: t('setting.neteaseTranslation', { postProcess: 'sentenceCase' }),
},
{
control: (
<NumberInput
defaultValue={lyricsSettings.delayMs}
onBlur={(e) => {
const value = Number(e.currentTarget.value);
updateLyricsSetting({ 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) => {
updateLyricsSetting({ translationTargetLanguage: value });
}}
value={lyricsSettings.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) => {
updateLyricsSetting({ translationApiProvider: value });
}}
value={lyricsSettings.translationApiProvider}
/>
),
description: t('setting.translationApiProvider', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !isElectron(),
title: t('setting.translationApiProvider', { postProcess: 'sentenceCase' }),
},
{
control: (
<TextInput
onChange={(e) => {
updateLyricsSetting({ translationApiKey: e.currentTarget.value });
}}
value={lyricsSettings.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={lyricsSettings.enableAutoTranslation}
onChange={(e) =>
updateLyricsSetting({ enableAutoTranslation: e.currentTarget.checked })
}
/>
),
description: t('setting.enableAutoTranslation', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !isElectron(),
title: t('setting.enableAutoTranslation', { postProcess: 'sentenceCase' }),
},
];
return (
<Stack gap="md" p="md">
<Fieldset legend={t('page.setting.lyricsDisplay', { postProcess: 'sentenceCase' })}>
<SettingsSection options={displayOptions} />
</Fieldset>
<Fieldset legend={t('page.setting.lyrics', { postProcess: 'sentenceCase' })}>
<SettingsSection options={lyricOptions} />
</Fieldset>
</Stack>
);
};
@@ -0,0 +1,9 @@
import { ContextModalProps } from '@mantine/modals';
import { LyricsSettingsForm } from './lyrics-settings-form';
export const LyricsSettingsContextModal = ({
innerProps,
}: ContextModalProps<{ settingsKey: string }>) => {
return <LyricsSettingsForm settingsKey={innerProps.settingsKey} />;
};
@@ -22,6 +22,7 @@ interface LyricsActionsProps {
onTranslateLyric?: () => void;
onUpdateOffset: (offsetMs: number) => void;
setIndex: (idx: number) => void;
settingsKey?: string;
synced?: boolean;
}
@@ -35,6 +36,7 @@ export const LyricsActions = ({
onTranslateLyric,
onUpdateOffset,
setIndex,
settingsKey = 'default',
}: LyricsActionsProps) => {
const { t } = useTranslation();
const currentSong = usePlayerSong();
@@ -64,3 +64,13 @@
transparent 95%
);
}
.settings-icon {
z-index: 100;
opacity: 0;
transition: opacity 0.2s ease;
}
.lyrics-container:hover .settings-icon {
opacity: 1;
}
+21 -1
View File
@@ -18,10 +18,12 @@ import {
UnsynchronizedLyrics,
UnsynchronizedLyricsProps,
} from '/@/renderer/features/lyrics/unsynchronized-lyrics';
import { openLyricsSettingsModal } from '/@/renderer/features/lyrics/utils/open-lyrics-settings-modal';
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
import { ComponentErrorBoundary } from '/@/renderer/features/shared/components/component-error-boundary';
import { queryClient } from '/@/renderer/lib/react-query';
import { useLyricsSettings, usePlayerSong } from '/@/renderer/store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Center } from '/@/shared/components/center/center';
import { Group } from '/@/shared/components/group/group';
import { Spinner } from '/@/shared/components/spinner/spinner';
@@ -35,9 +37,10 @@ import {
type LyricsProps = {
fadeOutNoLyricsMessage?: boolean;
settingsKey?: string;
};
export const Lyrics = ({ fadeOutNoLyricsMessage = true }: LyricsProps) => {
export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default' }: LyricsProps) => {
const currentSong = usePlayerSong();
const {
enableAutoTranslation,
@@ -301,9 +304,23 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true }: LyricsProps) => {
}
}, [currentOffsetMs, lyrics, synced]);
const handleOpenSettings = () => {
openLyricsSettingsModal(settingsKey);
};
return (
<ComponentErrorBoundary>
<div className={styles.lyricsContainer}>
<ActionIcon
className={styles.settingsIcon}
icon="settings2"
iconProps={{ size: 'lg' }}
onClick={handleOpenSettings}
pos="absolute"
right={0}
top={0}
variant="transparent"
/>
{isLoadingLyrics ? (
<Spinner container />
) : (
@@ -335,11 +352,13 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true }: LyricsProps) => {
<SynchronizedLyrics
{...(lyrics as SynchronizedLyricsProps)}
offsetMs={currentOffsetMs}
settingsKey={settingsKey}
translatedLyrics={showTranslation ? translatedLyrics : null}
/>
) : (
<UnsynchronizedLyrics
{...(lyrics as UnsynchronizedLyricsProps)}
settingsKey={settingsKey}
translatedLyrics={showTranslation ? translatedLyrics : null}
/>
)}
@@ -362,6 +381,7 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true }: LyricsProps) => {
}
onUpdateOffset={handleUpdateOffset}
setIndex={setIndex}
settingsKey={settingsKey}
/>
</div>
</div>
@@ -6,6 +6,7 @@ import styles from './synchronized-lyrics.module.css';
import { LyricLine } from '/@/renderer/features/lyrics/lyric-line';
import {
useLyricsDisplaySettings,
useLyricsSettings,
usePlaybackType,
usePlayerActions,
@@ -22,6 +23,7 @@ const mpris = isElectron() && utils?.isLinux() ? window.api.mpris : null;
export interface SynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
lyrics: SynchronizedLyricsArray;
offsetMs?: number;
settingsKey?: string;
style?: React.CSSProperties;
translatedLyrics?: null | string;
}
@@ -32,12 +34,22 @@ export const SynchronizedLyrics = ({
name,
offsetMs,
remote,
settingsKey = 'default',
source,
style,
translatedLyrics,
}: SynchronizedLyricsProps) => {
const playbackType = usePlaybackType();
const settings = useLyricsSettings();
const lyricsSettings = useLyricsSettings();
const displaySettings = useLyricsDisplaySettings(settingsKey);
const settings = {
...lyricsSettings,
fontSize:
displaySettings.fontSize && displaySettings.fontSize !== 0
? displaySettings.fontSize
: 24,
gap: displaySettings.gap && displaySettings.gap !== 0 ? displaySettings.gap : 24,
};
const { mediaSeekToTimestamp } = usePlayerActions();
const status = usePlayerStatus();
const timestamp = usePlayerTimestamp();
@@ -3,11 +3,12 @@ import { useMemo } from 'react';
import styles from './unsynchronized-lyrics.module.css';
import { LyricLine } from '/@/renderer/features/lyrics/lyric-line';
import { useLyricsSettings } from '/@/renderer/store';
import { useLyricsDisplaySettings, useLyricsSettings } from '/@/renderer/store';
import { FullLyricsMetadata } from '/@/shared/types/domain-types';
export interface UnsynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
lyrics: string;
settingsKey?: string;
translatedLyrics?: null | string;
}
@@ -16,10 +17,23 @@ export const UnsynchronizedLyrics = ({
lyrics,
name,
remote,
settingsKey = 'default',
source,
translatedLyrics,
}: UnsynchronizedLyricsProps) => {
const settings = useLyricsSettings();
const lyricsSettings = useLyricsSettings();
const displaySettings = useLyricsDisplaySettings(settingsKey);
const settings = {
...lyricsSettings,
fontSizeUnsync:
displaySettings.fontSizeUnsync && displaySettings.fontSizeUnsync !== 0
? displaySettings.fontSizeUnsync
: 24,
gapUnsync:
displaySettings.gapUnsync && displaySettings.gapUnsync !== 0
? displaySettings.gapUnsync
: 24,
};
const lines = useMemo(() => {
return lyrics.split('\n');
}, [lyrics]);
@@ -0,0 +1,27 @@
import { openContextModal } from '@mantine/modals';
import i18n from '/@/i18n/i18n';
export const openLyricsSettingsModal = (settingsKey: string = 'default') => {
openContextModal({
innerProps: { settingsKey },
modalKey: 'lyricsSettings',
overlayProps: {
blur: 0,
opacity: 0,
},
size: 'xl',
styles: {
content: {
height: '90%',
maxWidth: '1400px',
minHeight: '600px',
width: '100%',
},
},
title: i18n.t('common.setting_other', { postProcess: 'titleCase' }),
transitionProps: {
transition: 'pop',
},
});
};
@@ -270,7 +270,7 @@ const LyricsPanel = () => {
return (
<div className={styles.lyricsSection}>
<PanelReorderControls panelType="lyrics" />
<Lyrics fadeOutNoLyricsMessage={false} />
<Lyrics fadeOutNoLyricsMessage={false} settingsKey="sidebar" />
</div>
);
};
@@ -330,7 +330,7 @@ const CombinedLyricsAndVisualizerPanel = () => {
return (
<div className={styles.lyricsSection}>
<PanelReorderControls panelType="lyrics" />
<Lyrics fadeOutNoLyricsMessage={true} />
<Lyrics fadeOutNoLyricsMessage={true} settingsKey="sidebar" />
<div
className={styles.visualizerOverlay}
style={{
@@ -22,6 +22,7 @@ import { useFastAverageColor } from '/@/renderer/hooks';
import {
useFullScreenPlayerStore,
useFullScreenPlayerStoreActions,
useLyricsDisplaySettings,
useLyricsSettings,
usePlayerData,
usePlayerSong,
@@ -235,19 +236,35 @@ const Controls = ({ isPageHovered }: ControlsProps) => {
} = useFullScreenPlayerStore();
const { setStore } = useFullScreenPlayerStoreActions();
const { setSettings } = useSettingsStoreActions();
const lyricConfig = useLyricsSettings();
const lyricsSettings = useLyricsSettings();
const displaySettings = useLyricsDisplaySettings('default');
const lyricConfig = { ...lyricsSettings, ...displaySettings };
const handleToggleFullScreenPlayer = () => {
setStore({ expanded: !expanded });
};
const handleLyricsSettings = (property: string, value: any) => {
setSettings({
lyrics: {
...useSettingsStore.getState().lyrics,
[property]: value,
},
});
const displayProperties = ['fontSize', 'fontSizeUnsync', 'gap', 'gapUnsync'];
if (displayProperties.includes(property)) {
const currentDisplay = useSettingsStore.getState().lyricsDisplay;
setSettings({
lyricsDisplay: {
...currentDisplay,
default: {
...currentDisplay.default,
[property]: value,
},
},
});
} else {
setSettings({
lyrics: {
...useSettingsStore.getState().lyrics,
[property]: value,
},
});
}
};
useHotkeys([['Escape', handleToggleFullScreenPlayer]]);
@@ -5,7 +5,12 @@ import styles from './mobile-fullscreen-player.module.css';
import { SONG_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns';
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
import { useLyricsSettings, useSettingsStore, useSettingsStoreActions } from '/@/renderer/store';
import {
useLyricsDisplaySettings,
useLyricsSettings,
useSettingsStore,
useSettingsStoreActions,
} from '/@/renderer/store';
import { useFullScreenPlayerStore, useFullScreenPlayerStoreActions } from '/@/renderer/store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Divider } from '/@/shared/components/divider/divider';
@@ -37,15 +42,31 @@ export const MobileFullscreenPlayerHeader = memo(
} = useFullScreenPlayerStore();
const { setStore } = useFullScreenPlayerStoreActions();
const { setSettings } = useSettingsStoreActions();
const lyricConfig = useLyricsSettings();
const lyricsSettings = useLyricsSettings();
const displaySettings = useLyricsDisplaySettings('default');
const lyricConfig = { ...lyricsSettings, ...displaySettings };
const handleLyricsSettings = (property: string, value: any) => {
setSettings({
lyrics: {
...useSettingsStore.getState().lyrics,
[property]: value,
},
});
const displayProperties = ['fontSize', 'fontSizeUnsync', 'gap', 'gapUnsync'];
if (displayProperties.includes(property)) {
const currentDisplay = useSettingsStore.getState().lyricsDisplay;
setSettings({
lyricsDisplay: {
...currentDisplay,
default: {
...currentDisplay.default,
[property]: value,
},
},
});
} else {
setSettings({
lyrics: {
...useSettingsStore.getState().lyrics,
[property]: value,
},
});
}
};
return (
@@ -1,5 +1,7 @@
import { openContextModal } from '@mantine/modals';
import i18n from '/@/i18n/i18n';
export const openVisualizerSettingsModal = () => {
openContextModal({
innerProps: {},
@@ -17,7 +19,7 @@ export const openVisualizerSettingsModal = () => {
width: '100%',
},
},
title: 'Visualizer Settings',
title: i18n.t('common.setting_other', { postProcess: 'titleCase' }),
transitionProps: {
transition: 'pop',
},
@@ -21,20 +21,22 @@ export const LyricSettings = () => {
const settings = useLyricsSettings();
const { setSettings } = useSettingsStoreActions();
const updateSetting = (updates: Partial<typeof settings>) => {
setSettings({
lyrics: {
...settings,
...updates,
},
});
};
const lyricOptions: SettingOption[] = [
{
control: (
<Switch
aria-label="Follow lyrics"
defaultChecked={settings.follow}
onChange={(e) => {
setSettings({
lyrics: {
...settings,
follow: e.currentTarget.checked,
},
});
}}
onChange={(e) => updateSetting({ follow: e.currentTarget.checked })}
/>
),
description: t('setting.followLyric', {
@@ -48,14 +50,7 @@ export const LyricSettings = () => {
<Switch
aria-label="Prefer local lyrics"
defaultChecked={settings.preferLocalLyrics}
onChange={(e) => {
setSettings({
lyrics: {
...settings,
preferLocalLyrics: e.currentTarget.checked,
},
});
}}
onChange={(e) => updateSetting({ preferLocalLyrics: e.currentTarget.checked })}
/>
),
description: t('setting.preferLocalLyrics', {
@@ -70,14 +65,7 @@ export const LyricSettings = () => {
<Switch
aria-label="Enable fetching lyrics"
defaultChecked={settings.fetch}
onChange={(e) => {
setSettings({
lyrics: {
...settings,
fetch: e.currentTarget.checked,
},
});
}}
onChange={(e) => updateSetting({ fetch: e.currentTarget.checked })}
/>
),
description: t('setting.lyricFetch', {
@@ -96,12 +84,7 @@ export const LyricSettings = () => {
defaultValue={settings.sources}
onChange={(e: string[]) => {
localSettings?.set('lyrics', e);
setSettings({
lyrics: {
...settings,
sources: e.map((source) => source as LyricSource),
},
});
updateSetting({ sources: e.map((source) => source as LyricSource) });
}}
width={300}
/>
@@ -120,12 +103,7 @@ export const LyricSettings = () => {
defaultChecked={settings.enableNeteaseTranslation}
onChange={(e) => {
const isChecked = e.currentTarget.checked;
setSettings({
lyrics: {
...settings,
enableNeteaseTranslation: e.currentTarget.checked,
},
});
updateSetting({ enableNeteaseTranslation: isChecked });
localSettings?.set('enableNeteaseTranslation', isChecked);
}}
/>
@@ -143,12 +121,7 @@ export const LyricSettings = () => {
defaultValue={settings.delayMs}
onBlur={(e) => {
const value = Number(e.currentTarget.value);
setSettings({
lyrics: {
...settings,
delayMs: value,
},
});
updateSetting({ delayMs: value });
}}
step={10}
width={100}
@@ -166,7 +139,7 @@ export const LyricSettings = () => {
<Select
data={languages}
onChange={(value) => {
setSettings({ lyrics: { ...settings, translationTargetLanguage: value } });
updateSetting({ translationTargetLanguage: value });
}}
value={settings.translationTargetLanguage}
/>
@@ -184,7 +157,7 @@ export const LyricSettings = () => {
clearable
data={['Microsoft Azure', 'Google Cloud']}
onChange={(value) => {
setSettings({ lyrics: { ...settings, translationApiProvider: value } });
updateSetting({ translationApiProvider: value });
}}
value={settings.translationApiProvider}
/>
@@ -200,9 +173,7 @@ export const LyricSettings = () => {
control: (
<TextInput
onChange={(e) => {
setSettings({
lyrics: { ...settings, translationApiKey: e.currentTarget.value },
});
updateSetting({ translationApiKey: e.currentTarget.value });
}}
value={settings.translationApiKey}
/>
@@ -219,14 +190,9 @@ export const LyricSettings = () => {
<Switch
aria-label="Enable auto translation"
defaultChecked={settings.enableAutoTranslation}
onChange={(e) => {
setSettings({
lyrics: {
...settings,
enableAutoTranslation: e.currentTarget.checked,
},
});
}}
onChange={(e) =>
updateSetting({ enableAutoTranslation: e.currentTarget.checked })
}
/>
),
description: t('setting.enableAutoTranslation', {
+2
View File
@@ -8,6 +8,7 @@ import { UpdatePlaylistContextModal } from '/@/renderer/features/playlists/compo
import { SettingsContextModal } from '/@/renderer/features/settings/components/settings-modal';
import { RouterErrorBoundary } from '/@/renderer/features/shared/components/router-error-boundary';
import { ShareItemContextModal } from '/@/renderer/features/sharing/components/share-item-context-modal';
import { LyricsSettingsContextModal } from '/@/renderer/features/lyrics/components/lyrics-settings-modal';
import { VisualizerSettingsContextModal } from '/@/renderer/features/visualizer/components/audiomotionanalyzer/visualizer-settings-modal';
import { AuthenticationOutlet } from '/@/renderer/layouts/authentication-outlet';
import { ResponsiveLayout } from '/@/renderer/layouts/responsive-layout';
@@ -99,6 +100,7 @@ export const AppRouter = () => {
shuffleAll: ShuffleAllContextModal,
updatePlaylist: UpdatePlaylistContextModal,
visualizerSettings: VisualizerSettingsContextModal,
lyricsSettings: LyricsSettingsContextModal,
}}
>
<RouterErrorBoundary>
+50 -9
View File
@@ -406,6 +406,13 @@ const HotkeysSettingsSchema = z.object({
globalMediaHotkeys: z.boolean(),
});
const LyricsDisplaySettingsSchema = z.object({
fontSize: z.number(),
fontSizeUnsync: z.number(),
gap: z.number(),
gapUnsync: z.number(),
});
const LyricsSettingsSchema = z.object({
alignment: z.enum(['center', 'left', 'right']),
delayMs: z.number(),
@@ -413,10 +420,6 @@ const LyricsSettingsSchema = z.object({
enableNeteaseTranslation: z.boolean(),
fetch: z.boolean(),
follow: z.boolean(),
fontSize: z.number(),
fontSizeUnsync: z.number(),
gap: z.number(),
gapUnsync: z.number(),
preferLocalLyrics: z.boolean(),
showMatch: z.boolean(),
showProvider: z.boolean(),
@@ -548,6 +551,7 @@ export const ValidationSettingsStateSchema = z.object({
hotkeys: HotkeysSettingsSchema,
lists: z.record(z.nativeEnum(ItemListKey), ItemListConfigSchema),
lyrics: LyricsSettingsSchema,
lyricsDisplay: z.record(z.string(), LyricsDisplaySettingsSchema),
playback: PlaybackSettingsSchema,
queryBuilder: QueryBuilderSettingsSchema,
remote: RemoteSettingsSchema,
@@ -1364,10 +1368,6 @@ const initialState: SettingsState = {
enableNeteaseTranslation: false,
fetch: true,
follow: true,
fontSize: 24,
fontSizeUnsync: 24,
gap: 24,
gapUnsync: 24,
preferLocalLyrics: true,
showMatch: true,
showProvider: true,
@@ -1376,6 +1376,14 @@ const initialState: SettingsState = {
translationApiProvider: '',
translationTargetLanguage: 'en',
},
lyricsDisplay: {
default: {
fontSize: 48,
fontSizeUnsync: 24,
gap: 32,
gapUnsync: 24,
},
},
playback: {
audioDeviceId: undefined,
audioFadeOnStatusChange: true,
@@ -1749,10 +1757,40 @@ export const useSettingsStore = createWithEqualityFn<SettingsSlice>()(
state.window.releaseChannel = 'beta';
}
if (version <= 17) {
// Migrate lyrics settings from record structure to separate lyrics and lyricsDisplay
if (
state.lyrics &&
typeof state.lyrics === 'object' &&
'default' in state.lyrics
) {
const oldLyrics = state.lyrics as any;
const defaultSettings = oldLyrics.default || oldLyrics;
// Extract display settings
const displaySettings = {
fontSize: defaultSettings.fontSize || 24,
fontSizeUnsync: defaultSettings.fontSizeUnsync || 24,
gap: defaultSettings.gap || 24,
gapUnsync: defaultSettings.gapUnsync || 24,
};
// Remove display properties from main settings
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { fontSize, fontSizeUnsync, gap, gapUnsync, ...mainSettings } =
defaultSettings;
state.lyrics = mainSettings;
state.lyricsDisplay = {
default: displaySettings,
};
}
}
return persistedState;
},
name: 'store_settings',
version: 17,
version: 18,
},
),
);
@@ -1780,6 +1818,9 @@ export const useMpvSettings = () =>
export const useLyricsSettings = () => useSettingsStore((state) => state.lyrics, shallow);
export const useLyricsDisplaySettings = (key: string = 'default') =>
useSettingsStore((state) => state.lyricsDisplay[key] || state.lyricsDisplay.default, shallow);
export const useRemoteSettings = () => useSettingsStore((state) => state.remote, shallow);
export const useFontSettings = () => useSettingsStore((state) => state.font, shallow);