From 81f7b424094d7435337bf661bf1333506156fb7d Mon Sep 17 00:00:00 2001 From: jeffvli Date: Wed, 29 Jul 2026 22:04:47 -0700 Subject: [PATCH] redesign lyrics settings form --- src/i18n/locales/en.json | 25 +- .../components/lyrics-settings-form.tsx | 433 ++++++++++-------- .../player/components/right-controls.tsx | 49 +- .../components/list-config-menu.module.css | 5 +- .../shared/components/list-config-menu.tsx | 17 +- 5 files changed, 296 insertions(+), 233 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index f232c1c7a..b2e0743cc 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -561,19 +561,33 @@ "dynamicImageBlur": "Image blur size", "dynamicIsImage": "Enable background image", "followCurrentLyric": "Follow current lyric", + "followCurrentLyric_description": "Automatically scroll to keep the active lyric line in view", "lyricFollowScrollAlignment": "Lyrics follow alignment", + "lyricFollowScrollAlignment_description": "Vertical position of the active line while following", "lyricAlignment": "Lyric alignment", + "lyricAlignment_description": "Horizontal alignment of lyric text", "lyricOffset": "Lyrics offset (ms)", "lyricGap": "Lyric gap", + "lyricGapSynchronized_description": "Vertical spacing between synchronized lyric lines", + "lyricGapUnsynchronized_description": "Vertical spacing between unsynchronized lyric lines", "lyricLineLeadTime": "Line lead time (ms)", + "lyricLineLeadTime_description": "Advance the active line this many milliseconds before its timestamp", "lyricPaddingLeft": "Lyrics left padding (%)", + "lyricPaddingLeft_description": "Left padding as a percentage of the lyrics area", "lyricPaddingRight": "Lyrics right padding (%)", + "lyricPaddingRight_description": "Right padding as a percentage of the lyrics area", "lyricSize": "Lyric size", + "lyricSizeSynchronized_description": "Font size for synchronized (timed) lyrics", + "lyricSizeUnsynchronized_description": "Font size for unsynchronized (plain) lyrics", "lyricOpacityNonActive": "Non-active lyric opacity", + "lyricOpacityNonActive_description": "Opacity of lyric lines that are not currently active", "lyricScaleNonActive": "Non-active lyric scale", + "lyricScaleNonActive_description": "Scale of lyric lines that are not currently active", "opacity": "Opacity", "showLyricMatch": "Show lyric match", + "showLyricMatch_description": "Show the song name and artist of the matched lyrics", "showLyricProvider": "Show lyric provider", + "showLyricProvider_description": "Show which provider supplied the current lyrics", "synchronized": "Synchronized", "unsynchronized": "Unsynchronized", "useImageAspectRatio": "Use image aspect ratio" @@ -668,6 +682,7 @@ "audio": "Audio", "lyrics": "Lyrics", "lyricsDisplay": "Lyrics Display", + "lyricsTranslation": "Lyrics Translation", "transcoding": "Transcoding", "discord": "Discord", "logger": "Logger", @@ -891,9 +906,9 @@ "discordUpdateInterval_description": "The time in seconds between each update (minimum 15 seconds)", "enableAutoTranslation_description": "Enable translation automatically when lyrics are loaded", "enableAutoTranslation": "Enable auto translation", - "enableFurigana_description": "Display pronunciation guides (furigana) over Japanese kanji lyrics.", + "enableFurigana_description": "Display pronunciation guides (furigana) over Japanese kanji lyrics", "enableFurigana": "Enable furigana generation", - "enableRomaji_description": "Display a romaji pronunciation line under Japanese lyrics.", + "enableRomaji_description": "Display a romaji pronunciation line under Japanese lyrics", "enableRomaji": "Enable romaji generation", "equalizer_descriptionMpv": "Parametric equalizer via FFmpeg lavfi (MPV)", "equalizer_descriptionWebAudio": "Parametric equalizer via Web Audio API", @@ -1003,7 +1018,7 @@ "listenbrainz": "Show ListenBrainz links", "lastfmApiKey_description": "The API key for {{lastfm}}. Required for cover art", "lastfmApiKey": "{{lastfm}} API key", - "lyricFetch_description": "Fetch lyrics from various internet sources", + "lyricFetch_description": "Fetch lyrics from the selected providers", "lyricFetch": "Fetch lyrics from the internet", "lyricFetchProvider_description": "Select the providers to fetch lyrics from", "lyricFetchProvider": "Providers to fetch lyrics from", @@ -1212,9 +1227,9 @@ "transcodeFormat": "Format to transcode", "translationApiKey_description": "API key for translation (global service endpoint only)", "translationApiKey": "Translation API key", - "translationApiProvider_description": "API provider for translation", + "translationApiProvider_description": "Selects the API provider used for translation", "translationApiProvider": "Translation API provider", - "translationTargetLanguage_description": "Target language for translation", + "translationTargetLanguage_description": "Sets the target language used for automatic translation", "translationTargetLanguage": "Translation target language", "trayEnabled_description": "Show/hide tray icon/menu. If disabled, also disables minimize/exit to tray", "trayEnabled": "Show tray", diff --git a/src/renderer/features/lyrics/components/lyrics-settings-form.tsx b/src/renderer/features/lyrics/components/lyrics-settings-form.tsx index a8034623e..4385fff88 100644 --- a/src/renderer/features/lyrics/components/lyrics-settings-form.tsx +++ b/src/renderer/features/lyrics/components/lyrics-settings-form.tsx @@ -3,9 +3,9 @@ import { useTranslation } from 'react-i18next'; import { languages } from '/@/i18n/i18n'; import { - SettingOption, - SettingsSection, -} from '/@/renderer/features/settings/components/settings-section'; + ListConfigBooleanControl, + ListConfigTable, +} from '/@/renderer/features/shared/components/list-config-menu'; import { useLyricsDisplaySettings, useLyricsSettings, @@ -19,9 +19,7 @@ import { SegmentedControl } from '/@/shared/components/segmented-control/segment import { Select } from '/@/shared/components/select/select'; import { Slider } from '/@/shared/components/slider/slider'; 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; @@ -58,131 +56,169 @@ export const LyricsSettingsForm = ({ settingsKey }: LyricsSettingsFormProps) => }); }; - const displayOptions: SettingOption[] = [ + const displayOptions = [ { - control: ( - { - const value = Number(e.currentTarget.value); + component: ( + `${value}px`} + marks={[ + { label: '8', value: 8 }, + { label: '24', value: 24 }, + { label: '48', value: 48 }, + { label: '72', value: 72 }, + ]} + max={72} + min={8} + onChangeEnd={(value) => { updateDisplaySetting({ fontSize: value }); }} - rightSection={ - - px - - } step={1} - value={displaySettings.fontSize} - width={100} + w="75%" /> ), - description: '', - title: t( + description: t('page.fullscreenPlayer.config.lyricSizeSynchronized', { + context: 'description', + }), + id: 'fontSize', + label: t( `${t('page.fullscreenPlayer.config.lyricSize')} (${t('page.fullscreenPlayer.config.synchronized')})`, ), }, { - control: ( - { - const value = Number(e.currentTarget.value); + component: ( + `${value}px`} + marks={[ + { label: '8', value: 8 }, + { label: '24', value: 24 }, + { label: '48', value: 48 }, + { label: '72', value: 72 }, + ]} + max={72} + min={8} + onChangeEnd={(value) => { updateDisplaySetting({ fontSizeUnsync: value }); }} - rightSection={ - - px - - } step={1} - value={displaySettings.fontSizeUnsync} - width={100} + w="75%" /> ), - description: '', - title: t( + description: t('page.fullscreenPlayer.config.lyricSizeUnsynchronized', { + context: 'description', + }), + id: 'fontSizeUnsync', + label: t( `${t('page.fullscreenPlayer.config.lyricSize')} (${t('page.fullscreenPlayer.config.unsynchronized')})`, ), }, { - control: ( - { - const value = Number(e.currentTarget.value); + component: ( + `${value}px`} + marks={[ + { label: '0', value: 0 }, + { label: '25', value: 25 }, + { label: '50', value: 50 }, + ]} + max={50} + min={0} + onChangeEnd={(value) => { updateDisplaySetting({ gap: value }); }} - rightSection={ - - px - - } step={1} - value={displaySettings.gap} - width={100} + w="75%" /> ), - description: '', - title: t( + description: t('page.fullscreenPlayer.config.lyricGapSynchronized', { + context: 'description', + }), + id: 'gap', + label: t( `${t('page.fullscreenPlayer.config.lyricGap')} (${t('page.fullscreenPlayer.config.synchronized')})`, ), }, { - control: ( - { - const value = Number(e.currentTarget.value); + component: ( + `${value}px`} + marks={[ + { label: '0', value: 0 }, + { label: '25', value: 25 }, + { label: '50', value: 50 }, + ]} + max={50} + min={0} + onChangeEnd={(value) => { updateDisplaySetting({ gapUnsync: value }); }} - rightSection={ - - px - - } step={1} - value={displaySettings.gapUnsync} - width={100} + w="75%" /> ), - description: '', - title: t( + description: t('page.fullscreenPlayer.config.lyricGapUnsynchronized', { + context: 'description', + }), + id: 'gapUnsync', + label: t( `${t('page.fullscreenPlayer.config.lyricGap')} (${t('page.fullscreenPlayer.config.unsynchronized')})`, ), }, { - control: ( + component: ( `${value}%`} + marks={[ + { label: '0', value: 0 }, + { label: '25', value: 25 }, + { label: '50', value: 50 }, + ]} max={50} min={0} onChangeEnd={(value) => { updateDisplaySetting({ paddingLeft: value }); }} step={1} - w={100} + w="100%" /> ), - description: '', - title: t('page.fullscreenPlayer.config.lyricPaddingLeft'), + description: t('page.fullscreenPlayer.config.lyricPaddingLeft', { + context: 'description', + }), + id: 'paddingLeft', + label: t('page.fullscreenPlayer.config.lyricPaddingLeft'), }, { - control: ( + component: ( `${value}%`} + marks={[ + { label: '0', value: 0 }, + { label: '25', value: 25 }, + { label: '50', value: 50 }, + ]} max={50} min={0} onChangeEnd={(value) => { updateDisplaySetting({ paddingRight: value }); }} step={1} - w={100} + w="100%" /> ), - description: '', - title: t('page.fullscreenPlayer.config.lyricPaddingRight'), + description: t('page.fullscreenPlayer.config.lyricPaddingRight', { + context: 'description', + }), + id: 'paddingRight', + label: t('page.fullscreenPlayer.config.lyricPaddingRight'), }, { - control: ( + component: ( updateLyricsSetting({ alignment: value as 'center' | 'left' | 'right' }) } value={lyricsSettings.alignment} + w="100%" /> ), - description: '', - title: t('page.fullscreenPlayer.config.lyricAlignment'), + description: t('page.fullscreenPlayer.config.lyricAlignment', { + context: 'description', + }), + id: 'alignment', + label: t('page.fullscreenPlayer.config.lyricAlignment'), }, { - control: ( - updateLyricsSetting({ follow: e.currentTarget.checked })} + component: ( + updateLyricsSetting({ follow: value })} + value={lyricsSettings.follow} /> ), - description: '', - title: t('page.fullscreenPlayer.config.followCurrentLyric'), + description: t('page.fullscreenPlayer.config.followCurrentLyric', { + context: 'description', + }), + id: 'follow', + label: t('page.fullscreenPlayer.config.followCurrentLyric'), }, { - control: ( + component: ( value.toString()} + marks={[ + { label: '-50', value: -50 }, + { label: '-25', value: -25 }, + { label: '0', value: 0 }, + { label: '25', value: 25 }, + { label: '50', value: 50 }, + ]} max={50} min={-50} onChangeEnd={(value) => { updateLyricsSetting({ followScrollAlignment: value }); }} step={1} - w={100} + w="100%" /> ), - description: '', - title: t('page.fullscreenPlayer.config.lyricFollowScrollAlignment'), + description: t('page.fullscreenPlayer.config.lyricFollowScrollAlignment', { + context: 'description', + }), + id: 'followScrollAlignment', + label: t('page.fullscreenPlayer.config.lyricFollowScrollAlignment'), }, { - control: ( + component: ( width={100} /> ), - description: '', - title: t('page.fullscreenPlayer.config.lyricLineLeadTime'), + description: t('page.fullscreenPlayer.config.lyricLineLeadTime', { + context: 'description', + }), + id: 'lineLeadTimeMs', + label: t('page.fullscreenPlayer.config.lyricLineLeadTime'), }, { - control: ( + component: ( (e * 100).toFixed(0) + '%'} + marks={[ + { label: '0%', value: 0 }, + { label: '25%', value: 0.25 }, + { label: '50%', value: 0.5 }, + { label: '75%', value: 0.75 }, + { label: '100%', value: 1 }, + ]} max={1.0} min={0.0} onChangeEnd={(e) => { @@ -259,17 +321,25 @@ export const LyricsSettingsForm = ({ settingsKey }: LyricsSettingsFormProps) => }); }} step={0.01} - w={100} + w="100%" /> ), - description: '', - title: t(`${t('page.fullscreenPlayer.config.lyricOpacityNonActive')}`, {}), + description: t('page.fullscreenPlayer.config.lyricOpacityNonActive', { + context: 'description', + }), + id: 'opacityNonActive', + label: t('page.fullscreenPlayer.config.lyricOpacityNonActive'), }, { - control: ( + component: ( (e * 100).toFixed(0) + '%'} + marks={[ + { label: '50%', value: 0.5 }, + { label: '75%', value: 0.75 }, + { label: '100%', value: 1 }, + ]} max={1.0} min={0.5} onChangeEnd={(e) => { @@ -278,69 +348,74 @@ export const LyricsSettingsForm = ({ settingsKey }: LyricsSettingsFormProps) => }); }} step={0.01} - w={100} + w="100%" /> ), - description: '', - title: t(`${t('page.fullscreenPlayer.config.lyricScaleNonActive')}`, {}), + description: t('page.fullscreenPlayer.config.lyricScaleNonActive', { + context: 'description', + }), + id: 'scaleNonActive', + label: t('page.fullscreenPlayer.config.lyricScaleNonActive'), }, { - control: ( - updateLyricsSetting({ showMatch: e.currentTarget.checked })} + component: ( + updateLyricsSetting({ showMatch: value })} + value={lyricsSettings.showMatch} /> ), - description: '', - title: t('page.fullscreenPlayer.config.showLyricMatch'), + description: t('page.fullscreenPlayer.config.showLyricMatch', { + context: 'description', + }), + id: 'showMatch', + label: t('page.fullscreenPlayer.config.showLyricMatch'), }, { - control: ( - updateLyricsSetting({ showProvider: e.currentTarget.checked })} + component: ( + updateLyricsSetting({ showProvider: value })} + value={lyricsSettings.showProvider} /> ), - description: '', - title: t('page.fullscreenPlayer.config.showLyricProvider'), + description: t('page.fullscreenPlayer.config.showLyricProvider', { + context: 'description', + }), + id: 'showProvider', + label: t('page.fullscreenPlayer.config.showLyricProvider'), }, ]; - const lyricOptions: SettingOption[] = [ + const lyricOptions = [ { - control: ( - - updateLyricsSetting({ preferLocalLyrics: e.currentTarget.checked }) - } + component: ( + updateLyricsSetting({ preferLocalLyrics: value })} + value={lyricsSettings.preferLocalLyrics} /> ), description: t('setting.preferLocalLyrics', { context: 'description', }), + id: 'preferLocalLyrics', isHidden: !isElectron(), - title: t('setting.preferLocalLyrics'), + label: t('setting.preferLocalLyrics'), }, { - control: ( - updateLyricsSetting({ fetch: e.currentTarget.checked })} + component: ( + updateLyricsSetting({ fetch: value })} + value={lyricsSettings.fetch} /> ), description: t('setting.lyricFetch', { context: 'description', }), + id: 'fetch', isHidden: !isElectron(), - title: t('setting.lyricFetch'), + label: t('setting.lyricFetch'), }, { - control: ( + component: ( localSettings?.set('lyrics', e); updateLyricsSetting({ sources: e.map((source) => source as LyricSource) }); }} - width={300} + width="100%" /> ), description: t('setting.lyricFetchProvider', { context: 'description', }), + id: 'sources', isHidden: !isElectron(), - title: t('setting.lyricFetchProvider'), + label: t('setting.lyricFetchProvider'), }, { - control: ( - - updateLyricsSetting({ enableFurigana: e.currentTarget.checked }) - } + component: ( + updateLyricsSetting({ enableFurigana: value })} + value={lyricsSettings.enableFurigana ?? false} /> ), description: t('setting.enableFurigana', { context: 'description', }), - title: t('setting.enableFurigana'), + id: 'enableFurigana', + label: t('setting.enableFurigana'), }, { - control: ( - updateLyricsSetting({ enableRomaji: e.currentTarget.checked })} + component: ( + updateLyricsSetting({ enableRomaji: value })} + value={lyricsSettings.enableRomaji ?? false} /> ), description: t('setting.enableRomaji', { context: 'description', }), - title: t('setting.enableRomaji'), + id: 'enableRomaji', + label: t('setting.enableRomaji'), }, { - control: ( - { - const isChecked = e.currentTarget.checked; - updateLyricsSetting({ enableNeteaseTranslation: isChecked }); - localSettings?.set('enableNeteaseTranslation', isChecked); + component: ( + { + updateLyricsSetting({ enableNeteaseTranslation: value }); + localSettings?.set('enableNeteaseTranslation', value); }} + value={lyricsSettings.enableNeteaseTranslation} /> ), description: t('setting.neteaseTranslation', { context: 'description', }), + id: 'enableNeteaseTranslation', isHidden: !isElectron(), - title: t('setting.neteaseTranslation'), + label: t('setting.neteaseTranslation'), }, + ]; + + const translationSettings = [ { - control: ( - { - const value = Number(e.currentTarget.value); - updateLyricsSetting({ delayMs: value }); - }} - step={10} - width={100} + component: ( + updateLyricsSetting({ enableAutoTranslation: value })} + value={lyricsSettings.enableAutoTranslation} /> ), - description: t('setting.lyricOffset', { + description: t('setting.enableAutoTranslation', { context: 'description', }), + id: 'enableAutoTranslation', isHidden: !isElectron(), - title: t('setting.lyricOffset'), + label: t('setting.enableAutoTranslation'), }, { - control: ( + component: ( updateLyricsSetting({ translationApiProvider: value }); }} value={lyricsSettings.translationApiProvider} + width="100%" /> ), description: t('setting.translationApiProvider', { context: 'description', }), + id: 'translationApiProvider', isHidden: !isElectron(), - title: t('setting.translationApiProvider'), + label: t('setting.translationApiProvider'), }, { - control: ( + component: ( { updateLyricsSetting({ translationApiKey: e.currentTarget.value }); }} value={lyricsSettings.translationApiKey} + width="100%" /> ), description: t('setting.translationApiKey', { context: 'description', }), + id: 'translationApiKey', isHidden: !isElectron(), - title: t('setting.translationApiKey'), - }, - { - control: ( - - updateLyricsSetting({ enableAutoTranslation: e.currentTarget.checked }) - } - /> - ), - description: t('setting.enableAutoTranslation', { - context: 'description', - }), - isHidden: !isElectron(), - title: t('setting.enableAutoTranslation'), + label: t('setting.translationApiKey'), }, ]; return ( - -
- -
+
- + +
+
+ +
+
+
); diff --git a/src/renderer/features/player/components/right-controls.tsx b/src/renderer/features/player/components/right-controls.tsx index 0212c4690..d95714dfb 100644 --- a/src/renderer/features/player/components/right-controls.tsx +++ b/src/renderer/features/player/components/right-controls.tsx @@ -54,7 +54,6 @@ import { SegmentedControl } from '/@/shared/components/segmented-control/segment import { Select } from '/@/shared/components/select/select'; import { Slider } from '/@/shared/components/slider/slider'; import { Stack } from '/@/shared/components/stack/stack'; -import { Text } from '/@/shared/components/text/text'; import { useMediaQuery } from '/@/shared/hooks/use-media-query'; import { useThrottledCallback } from '/@/shared/hooks/use-throttled-callback'; import { useThrottledValue } from '/@/shared/hooks/use-throttled-value'; @@ -201,17 +200,9 @@ const AutoDJButton = () => { w="96px" /> ), + description: t('setting.autoDJ_itemCount_description'), id: 'itemCount', - label: ( - - - {t('setting.autoDJ_itemCount')} - - - {t('setting.autoDJ_itemCount_description')} - - - ), + label: t('setting.autoDJ_itemCount'), }, { component: ( @@ -239,17 +230,9 @@ const AutoDJButton = () => { w="144px" /> ), + description: t('setting.autoDJ_timing_description'), id: 'timing', - label: ( - - - {t('setting.autoDJ_timing')} - - - {t('setting.autoDJ_timing_description')} - - - ), + label: t('setting.autoDJ_timing'), }, ], [ @@ -279,17 +262,9 @@ const AutoDJButton = () => { value={settings.allowDuplicates} /> ), + description: t('setting.autoDJ_allowDuplicates_description'), id: 'allowDuplicates', - label: ( - - - {t('setting.autoDJ_allowDuplicates')} - - - {t('setting.autoDJ_allowDuplicates_description')} - - - ), + label: t('setting.autoDJ_allowDuplicates'), }, { component: ( @@ -304,17 +279,9 @@ const AutoDJButton = () => { value={settings.onlySimilar} /> ), + description: t('setting.autoDJ_onlySimilar_description'), id: 'onlySimilar', - label: ( - - - {t('setting.autoDJ_onlySimilar')} - - - {t('setting.autoDJ_onlySimilar_description')} - - - ), + label: t('setting.autoDJ_onlySimilar'), }, ], [setSettings, settings.allowDuplicates, settings.onlySimilar, t], diff --git a/src/renderer/features/shared/components/list-config-menu.module.css b/src/renderer/features/shared/components/list-config-menu.module.css index 5207fadce..ded2202c1 100644 --- a/src/renderer/features/shared/components/list-config-menu.module.css +++ b/src/renderer/features/shared/components/list-config-menu.module.css @@ -6,13 +6,13 @@ width: 50%; padding: var(--theme-spacing-md) var(--theme-spacing-md) var(--theme-spacing-md) 0; font-weight: 500; - vertical-align: middle; + vertical-align: top; background-color: initial; } .td { padding: 0; - vertical-align: middle; + vertical-align: top; } .control { @@ -20,6 +20,7 @@ align-items: center; justify-content: flex-end; width: 100%; + padding: var(--theme-spacing-md) 0; } .divider-cell { diff --git a/src/renderer/features/shared/components/list-config-menu.tsx b/src/renderer/features/shared/components/list-config-menu.tsx index 28691630a..8ac1a6dc5 100644 --- a/src/renderer/features/shared/components/list-config-menu.tsx +++ b/src/renderer/features/shared/components/list-config-menu.tsx @@ -17,6 +17,7 @@ import { SegmentedControl } from '/@/shared/components/segmented-control/segment import { Stack } from '/@/shared/components/stack/stack'; import { Switch } from '/@/shared/components/switch/switch'; import { Table } from '/@/shared/components/table/table'; +import { Text } from '/@/shared/components/text/text'; import { useDisclosure } from '/@/shared/hooks/use-disclosure'; import { ItemListKey, ListDisplayType } from '/@/shared/types/types'; @@ -232,6 +233,7 @@ export const ListConfigTable = ({ }: { options: { component: ReactNode; + description?: ReactNode | string; id: string; isDivider?: boolean; isHidden?: boolean; @@ -269,7 +271,20 @@ export const ListConfigTable = ({ return ( - {option.label} + + {option.description !== undefined ? ( + + + {option.label} + + + {option.description} + + + ) : ( + option.label + )} +
{option.component}