diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 39bd9159c..58f7b3424 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -569,6 +569,7 @@ "scrobble": "scrobble", "audio": "audio", "lyrics": "lyrics", + "lyricsDisplay": "lyrics display", "transcoding": "transcoding", "discord": "discord", "logger": "logger", diff --git a/src/renderer/features/lyrics/components/lyrics-settings-form.tsx b/src/renderer/features/lyrics/components/lyrics-settings-form.tsx new file mode 100644 index 000000000..7cada3cb2 --- /dev/null +++ b/src/renderer/features/lyrics/components/lyrics-settings-form.tsx @@ -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) => { + setSettings({ + lyrics: { + ...lyricsSettings, + ...updates, + }, + }); + }; + + const updateDisplaySetting = (updates: Partial) => { + setSettings({ + lyricsDisplay: { + ...allLyricsDisplay, + [settingsKey]: { + ...displaySettings, + ...updates, + }, + }, + }); + }; + + const displayOptions: SettingOption[] = [ + { + control: ( + { + const value = Number(e.currentTarget.value); + updateDisplaySetting({ fontSize: value }); + }} + rightSection={ + + px + + } + step={1} + value={displaySettings.fontSize} + width={100} + /> + ), + description: '', + title: t( + `${t('page.fullscreenPlayer.config.lyricSize')} (${t('page.fullscreenPlayer.config.synchronized')})`, + { postProcess: 'sentenceCase' }, + ), + }, + { + control: ( + { + const value = Number(e.currentTarget.value); + updateDisplaySetting({ fontSizeUnsync: value }); + }} + rightSection={ + + px + + } + step={1} + value={displaySettings.fontSizeUnsync} + width={100} + /> + ), + description: '', + title: t( + `${t('page.fullscreenPlayer.config.lyricSize')} (${t('page.fullscreenPlayer.config.unsynchronized')})`, + { postProcess: 'sentenceCase' }, + ), + }, + { + control: ( + { + const value = Number(e.currentTarget.value); + updateDisplaySetting({ gap: value }); + }} + rightSection={ + + px + + } + step={1} + value={displaySettings.gap} + width={100} + /> + ), + description: '', + title: t( + `${t('page.fullscreenPlayer.config.lyricGap')} (${t('page.fullscreenPlayer.config.synchronized')})`, + { postProcess: 'sentenceCase' }, + ), + }, + { + control: ( + { + const value = Number(e.currentTarget.value); + updateDisplaySetting({ gapUnsync: value }); + }} + rightSection={ + + px + + } + step={1} + value={displaySettings.gapUnsync} + width={100} + /> + ), + description: '', + title: t( + `${t('page.fullscreenPlayer.config.lyricGap')} (${t('page.fullscreenPlayer.config.unsynchronized')})`, + { postProcess: 'sentenceCase' }, + ), + }, + { + control: ( + + updateLyricsSetting({ alignment: value as 'center' | 'left' | 'right' }) + } + value={lyricsSettings.alignment} + /> + ), + description: '', + title: t('page.fullscreenPlayer.config.lyricAlignment', { + postProcess: 'sentenceCase', + }), + }, + { + control: ( + updateLyricsSetting({ follow: e.currentTarget.checked })} + /> + ), + description: '', + title: t('page.fullscreenPlayer.config.followCurrentLyric', { + postProcess: 'sentenceCase', + }), + }, + { + control: ( + updateLyricsSetting({ showMatch: e.currentTarget.checked })} + /> + ), + description: '', + title: t('page.fullscreenPlayer.config.showLyricMatch', { + postProcess: 'sentenceCase', + }), + }, + { + control: ( + updateLyricsSetting({ showProvider: e.currentTarget.checked })} + /> + ), + description: '', + title: t('page.fullscreenPlayer.config.showLyricProvider', { + postProcess: 'sentenceCase', + }), + }, + ]; + + const lyricOptions: SettingOption[] = [ + { + control: ( + + updateLyricsSetting({ preferLocalLyrics: e.currentTarget.checked }) + } + /> + ), + description: t('setting.preferLocalLyrics', { + context: 'description', + postProcess: 'sentenceCase', + }), + isHidden: !isElectron(), + title: t('setting.preferLocalLyrics', { postProcess: 'sentenceCase' }), + }, + { + control: ( + updateLyricsSetting({ fetch: e.currentTarget.checked })} + /> + ), + description: t('setting.lyricFetch', { + context: 'description', + postProcess: 'sentenceCase', + }), + isHidden: !isElectron(), + title: t('setting.lyricFetch', { postProcess: 'sentenceCase' }), + }, + { + control: ( + { + 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: ( + { + 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: ( + { + 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: ( + { + updateLyricsSetting({ translationApiProvider: value }); + }} + value={lyricsSettings.translationApiProvider} + /> + ), + description: t('setting.translationApiProvider', { + context: 'description', + postProcess: 'sentenceCase', + }), + isHidden: !isElectron(), + title: t('setting.translationApiProvider', { postProcess: 'sentenceCase' }), + }, + { + control: ( + { + 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: ( + + updateLyricsSetting({ enableAutoTranslation: e.currentTarget.checked }) + } + /> + ), + description: t('setting.enableAutoTranslation', { + context: 'description', + postProcess: 'sentenceCase', + }), + isHidden: !isElectron(), + title: t('setting.enableAutoTranslation', { postProcess: 'sentenceCase' }), + }, + ]; + + return ( + +
+ +
+
+ +
+
+ ); +}; diff --git a/src/renderer/features/lyrics/components/lyrics-settings-modal.tsx b/src/renderer/features/lyrics/components/lyrics-settings-modal.tsx new file mode 100644 index 000000000..caf2c433f --- /dev/null +++ b/src/renderer/features/lyrics/components/lyrics-settings-modal.tsx @@ -0,0 +1,9 @@ +import { ContextModalProps } from '@mantine/modals'; + +import { LyricsSettingsForm } from './lyrics-settings-form'; + +export const LyricsSettingsContextModal = ({ + innerProps, +}: ContextModalProps<{ settingsKey: string }>) => { + return ; +}; diff --git a/src/renderer/features/lyrics/lyrics-actions.tsx b/src/renderer/features/lyrics/lyrics-actions.tsx index 52222ef46..2020917ec 100644 --- a/src/renderer/features/lyrics/lyrics-actions.tsx +++ b/src/renderer/features/lyrics/lyrics-actions.tsx @@ -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(); diff --git a/src/renderer/features/lyrics/lyrics.module.css b/src/renderer/features/lyrics/lyrics.module.css index a073374dd..32b626660 100644 --- a/src/renderer/features/lyrics/lyrics.module.css +++ b/src/renderer/features/lyrics/lyrics.module.css @@ -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; +} diff --git a/src/renderer/features/lyrics/lyrics.tsx b/src/renderer/features/lyrics/lyrics.tsx index 84f61d96e..a09639564 100644 --- a/src/renderer/features/lyrics/lyrics.tsx +++ b/src/renderer/features/lyrics/lyrics.tsx @@ -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 (
+ {isLoadingLyrics ? ( ) : ( @@ -335,11 +352,13 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true }: LyricsProps) => { ) : ( )} @@ -362,6 +381,7 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true }: LyricsProps) => { } onUpdateOffset={handleUpdateOffset} setIndex={setIndex} + settingsKey={settingsKey} />
diff --git a/src/renderer/features/lyrics/synchronized-lyrics.tsx b/src/renderer/features/lyrics/synchronized-lyrics.tsx index 7de34f6ac..57d5c626a 100644 --- a/src/renderer/features/lyrics/synchronized-lyrics.tsx +++ b/src/renderer/features/lyrics/synchronized-lyrics.tsx @@ -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 { 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(); diff --git a/src/renderer/features/lyrics/unsynchronized-lyrics.tsx b/src/renderer/features/lyrics/unsynchronized-lyrics.tsx index 8ed6e4e43..036c0bf72 100644 --- a/src/renderer/features/lyrics/unsynchronized-lyrics.tsx +++ b/src/renderer/features/lyrics/unsynchronized-lyrics.tsx @@ -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 { 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]); diff --git a/src/renderer/features/lyrics/utils/open-lyrics-settings-modal.ts b/src/renderer/features/lyrics/utils/open-lyrics-settings-modal.ts new file mode 100644 index 000000000..ef6faa010 --- /dev/null +++ b/src/renderer/features/lyrics/utils/open-lyrics-settings-modal.ts @@ -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', + }, + }); +}; diff --git a/src/renderer/features/now-playing/components/sidebar-play-queue.tsx b/src/renderer/features/now-playing/components/sidebar-play-queue.tsx index 29c526a0d..480fc7100 100644 --- a/src/renderer/features/now-playing/components/sidebar-play-queue.tsx +++ b/src/renderer/features/now-playing/components/sidebar-play-queue.tsx @@ -270,7 +270,7 @@ const LyricsPanel = () => { return (
- +
); }; @@ -330,7 +330,7 @@ const CombinedLyricsAndVisualizerPanel = () => { return (
- +
{ } = 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]]); diff --git a/src/renderer/features/player/components/mobile-fullscreen-player-header.tsx b/src/renderer/features/player/components/mobile-fullscreen-player-header.tsx index a5d025338..146cf1098 100644 --- a/src/renderer/features/player/components/mobile-fullscreen-player-header.tsx +++ b/src/renderer/features/player/components/mobile-fullscreen-player-header.tsx @@ -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 ( diff --git a/src/renderer/features/player/utils/open-visualizer-settings-modal.ts b/src/renderer/features/player/utils/open-visualizer-settings-modal.ts index 8c173fad2..cffb2ac7d 100644 --- a/src/renderer/features/player/utils/open-visualizer-settings-modal.ts +++ b/src/renderer/features/player/utils/open-visualizer-settings-modal.ts @@ -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', }, diff --git a/src/renderer/features/settings/components/general/lyric-settings.tsx b/src/renderer/features/settings/components/general/lyric-settings.tsx index aafdeeb74..73fbbccac 100644 --- a/src/renderer/features/settings/components/general/lyric-settings.tsx +++ b/src/renderer/features/settings/components/general/lyric-settings.tsx @@ -21,20 +21,22 @@ export const LyricSettings = () => { const settings = useLyricsSettings(); const { setSettings } = useSettingsStoreActions(); + const updateSetting = (updates: Partial) => { + setSettings({ + lyrics: { + ...settings, + ...updates, + }, + }); + }; + const lyricOptions: SettingOption[] = [ { control: ( { - 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 = () => { { - 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 = () => { { - 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 = () => {