redesign lyrics settings form

This commit is contained in:
jeffvli
2026-07-29 22:04:47 -07:00
parent 75c43d6724
commit 81f7b42409
5 changed files with 296 additions and 233 deletions
+20 -5
View File
@@ -561,19 +561,33 @@
"dynamicImageBlur": "Image blur size", "dynamicImageBlur": "Image blur size",
"dynamicIsImage": "Enable background image", "dynamicIsImage": "Enable background image",
"followCurrentLyric": "Follow current lyric", "followCurrentLyric": "Follow current lyric",
"followCurrentLyric_description": "Automatically scroll to keep the active lyric line in view",
"lyricFollowScrollAlignment": "Lyrics follow alignment", "lyricFollowScrollAlignment": "Lyrics follow alignment",
"lyricFollowScrollAlignment_description": "Vertical position of the active line while following",
"lyricAlignment": "Lyric alignment", "lyricAlignment": "Lyric alignment",
"lyricAlignment_description": "Horizontal alignment of lyric text",
"lyricOffset": "Lyrics offset (ms)", "lyricOffset": "Lyrics offset (ms)",
"lyricGap": "Lyric gap", "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": "Line lead time (ms)",
"lyricLineLeadTime_description": "Advance the active line this many milliseconds before its timestamp",
"lyricPaddingLeft": "Lyrics left padding (%)", "lyricPaddingLeft": "Lyrics left padding (%)",
"lyricPaddingLeft_description": "Left padding as a percentage of the lyrics area",
"lyricPaddingRight": "Lyrics right padding (%)", "lyricPaddingRight": "Lyrics right padding (%)",
"lyricPaddingRight_description": "Right padding as a percentage of the lyrics area",
"lyricSize": "Lyric size", "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": "Non-active lyric opacity",
"lyricOpacityNonActive_description": "Opacity of lyric lines that are not currently active",
"lyricScaleNonActive": "Non-active lyric scale", "lyricScaleNonActive": "Non-active lyric scale",
"lyricScaleNonActive_description": "Scale of lyric lines that are not currently active",
"opacity": "Opacity", "opacity": "Opacity",
"showLyricMatch": "Show lyric match", "showLyricMatch": "Show lyric match",
"showLyricMatch_description": "Show the song name and artist of the matched lyrics",
"showLyricProvider": "Show lyric provider", "showLyricProvider": "Show lyric provider",
"showLyricProvider_description": "Show which provider supplied the current lyrics",
"synchronized": "Synchronized", "synchronized": "Synchronized",
"unsynchronized": "Unsynchronized", "unsynchronized": "Unsynchronized",
"useImageAspectRatio": "Use image aspect ratio" "useImageAspectRatio": "Use image aspect ratio"
@@ -668,6 +682,7 @@
"audio": "Audio", "audio": "Audio",
"lyrics": "Lyrics", "lyrics": "Lyrics",
"lyricsDisplay": "Lyrics Display", "lyricsDisplay": "Lyrics Display",
"lyricsTranslation": "Lyrics Translation",
"transcoding": "Transcoding", "transcoding": "Transcoding",
"discord": "Discord", "discord": "Discord",
"logger": "Logger", "logger": "Logger",
@@ -891,9 +906,9 @@
"discordUpdateInterval_description": "The time in seconds between each update (minimum 15 seconds)", "discordUpdateInterval_description": "The time in seconds between each update (minimum 15 seconds)",
"enableAutoTranslation_description": "Enable translation automatically when lyrics are loaded", "enableAutoTranslation_description": "Enable translation automatically when lyrics are loaded",
"enableAutoTranslation": "Enable auto translation", "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", "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", "enableRomaji": "Enable romaji generation",
"equalizer_descriptionMpv": "Parametric equalizer via FFmpeg lavfi (MPV)", "equalizer_descriptionMpv": "Parametric equalizer via FFmpeg lavfi (MPV)",
"equalizer_descriptionWebAudio": "Parametric equalizer via Web Audio API", "equalizer_descriptionWebAudio": "Parametric equalizer via Web Audio API",
@@ -1003,7 +1018,7 @@
"listenbrainz": "Show ListenBrainz links", "listenbrainz": "Show ListenBrainz links",
"lastfmApiKey_description": "The API key for {{lastfm}}. Required for cover art", "lastfmApiKey_description": "The API key for {{lastfm}}. Required for cover art",
"lastfmApiKey": "{{lastfm}} API key", "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", "lyricFetch": "Fetch lyrics from the internet",
"lyricFetchProvider_description": "Select the providers to fetch lyrics from", "lyricFetchProvider_description": "Select the providers to fetch lyrics from",
"lyricFetchProvider": "Providers to fetch lyrics from", "lyricFetchProvider": "Providers to fetch lyrics from",
@@ -1212,9 +1227,9 @@
"transcodeFormat": "Format to transcode", "transcodeFormat": "Format to transcode",
"translationApiKey_description": "API key for translation (global service endpoint only)", "translationApiKey_description": "API key for translation (global service endpoint only)",
"translationApiKey": "Translation API key", "translationApiKey": "Translation API key",
"translationApiProvider_description": "API provider for translation", "translationApiProvider_description": "Selects the API provider used for translation",
"translationApiProvider": "Translation API provider", "translationApiProvider": "Translation API provider",
"translationTargetLanguage_description": "Target language for translation", "translationTargetLanguage_description": "Sets the target language used for automatic translation",
"translationTargetLanguage": "Translation target language", "translationTargetLanguage": "Translation target language",
"trayEnabled_description": "Show/hide tray icon/menu. If disabled, also disables minimize/exit to tray", "trayEnabled_description": "Show/hide tray icon/menu. If disabled, also disables minimize/exit to tray",
"trayEnabled": "Show tray", "trayEnabled": "Show tray",
@@ -3,9 +3,9 @@ import { useTranslation } from 'react-i18next';
import { languages } from '/@/i18n/i18n'; import { languages } from '/@/i18n/i18n';
import { import {
SettingOption, ListConfigBooleanControl,
SettingsSection, ListConfigTable,
} from '/@/renderer/features/settings/components/settings-section'; } from '/@/renderer/features/shared/components/list-config-menu';
import { import {
useLyricsDisplaySettings, useLyricsDisplaySettings,
useLyricsSettings, useLyricsSettings,
@@ -19,9 +19,7 @@ import { SegmentedControl } from '/@/shared/components/segmented-control/segment
import { Select } from '/@/shared/components/select/select'; import { Select } from '/@/shared/components/select/select';
import { Slider } from '/@/shared/components/slider/slider'; import { Slider } from '/@/shared/components/slider/slider';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { Switch } from '/@/shared/components/switch/switch';
import { TextInput } from '/@/shared/components/text-input/text-input'; import { TextInput } from '/@/shared/components/text-input/text-input';
import { Text } from '/@/shared/components/text/text';
import { LyricSource } from '/@/shared/types/domain-types'; import { LyricSource } from '/@/shared/types/domain-types';
const localSettings = isElectron() ? window.api.localSettings : null; const localSettings = isElectron() ? window.api.localSettings : null;
@@ -58,131 +56,169 @@ export const LyricsSettingsForm = ({ settingsKey }: LyricsSettingsFormProps) =>
}); });
}; };
const displayOptions: SettingOption[] = [ const displayOptions = [
{ {
control: ( component: (
<NumberInput <Slider
onBlur={(e) => { defaultValue={displaySettings.fontSize}
const value = Number(e.currentTarget.value); label={(value) => `${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 }); updateDisplaySetting({ fontSize: value });
}} }}
rightSection={
<Text pr="md" size="sm">
px
</Text>
}
step={1} step={1}
value={displaySettings.fontSize} w="75%"
width={100}
/> />
), ),
description: '', description: t('page.fullscreenPlayer.config.lyricSizeSynchronized', {
title: t( context: 'description',
}),
id: 'fontSize',
label: t(
`${t('page.fullscreenPlayer.config.lyricSize')} (${t('page.fullscreenPlayer.config.synchronized')})`, `${t('page.fullscreenPlayer.config.lyricSize')} (${t('page.fullscreenPlayer.config.synchronized')})`,
), ),
}, },
{ {
control: ( component: (
<NumberInput <Slider
onBlur={(e) => { defaultValue={displaySettings.fontSizeUnsync}
const value = Number(e.currentTarget.value); label={(value) => `${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 }); updateDisplaySetting({ fontSizeUnsync: value });
}} }}
rightSection={
<Text pr="md" size="sm">
px
</Text>
}
step={1} step={1}
value={displaySettings.fontSizeUnsync} w="75%"
width={100}
/> />
), ),
description: '', description: t('page.fullscreenPlayer.config.lyricSizeUnsynchronized', {
title: t( context: 'description',
}),
id: 'fontSizeUnsync',
label: t(
`${t('page.fullscreenPlayer.config.lyricSize')} (${t('page.fullscreenPlayer.config.unsynchronized')})`, `${t('page.fullscreenPlayer.config.lyricSize')} (${t('page.fullscreenPlayer.config.unsynchronized')})`,
), ),
}, },
{ {
control: ( component: (
<NumberInput <Slider
onBlur={(e) => { defaultValue={displaySettings.gap}
const value = Number(e.currentTarget.value); label={(value) => `${value}px`}
marks={[
{ label: '0', value: 0 },
{ label: '25', value: 25 },
{ label: '50', value: 50 },
]}
max={50}
min={0}
onChangeEnd={(value) => {
updateDisplaySetting({ gap: value }); updateDisplaySetting({ gap: value });
}} }}
rightSection={
<Text pr="md" size="sm">
px
</Text>
}
step={1} step={1}
value={displaySettings.gap} w="75%"
width={100}
/> />
), ),
description: '', description: t('page.fullscreenPlayer.config.lyricGapSynchronized', {
title: t( context: 'description',
}),
id: 'gap',
label: t(
`${t('page.fullscreenPlayer.config.lyricGap')} (${t('page.fullscreenPlayer.config.synchronized')})`, `${t('page.fullscreenPlayer.config.lyricGap')} (${t('page.fullscreenPlayer.config.synchronized')})`,
), ),
}, },
{ {
control: ( component: (
<NumberInput <Slider
onBlur={(e) => { defaultValue={displaySettings.gapUnsync}
const value = Number(e.currentTarget.value); label={(value) => `${value}px`}
marks={[
{ label: '0', value: 0 },
{ label: '25', value: 25 },
{ label: '50', value: 50 },
]}
max={50}
min={0}
onChangeEnd={(value) => {
updateDisplaySetting({ gapUnsync: value }); updateDisplaySetting({ gapUnsync: value });
}} }}
rightSection={
<Text pr="md" size="sm">
px
</Text>
}
step={1} step={1}
value={displaySettings.gapUnsync} w="75%"
width={100}
/> />
), ),
description: '', description: t('page.fullscreenPlayer.config.lyricGapUnsynchronized', {
title: t( context: 'description',
}),
id: 'gapUnsync',
label: t(
`${t('page.fullscreenPlayer.config.lyricGap')} (${t('page.fullscreenPlayer.config.unsynchronized')})`, `${t('page.fullscreenPlayer.config.lyricGap')} (${t('page.fullscreenPlayer.config.unsynchronized')})`,
), ),
}, },
{ {
control: ( component: (
<Slider <Slider
defaultValue={displaySettings.paddingLeft ?? 0} defaultValue={displaySettings.paddingLeft ?? 0}
label={(value) => `${value}%`} label={(value) => `${value}%`}
marks={[
{ label: '0', value: 0 },
{ label: '25', value: 25 },
{ label: '50', value: 50 },
]}
max={50} max={50}
min={0} min={0}
onChangeEnd={(value) => { onChangeEnd={(value) => {
updateDisplaySetting({ paddingLeft: value }); updateDisplaySetting({ paddingLeft: value });
}} }}
step={1} step={1}
w={100} w="100%"
/> />
), ),
description: '', description: t('page.fullscreenPlayer.config.lyricPaddingLeft', {
title: t('page.fullscreenPlayer.config.lyricPaddingLeft'), context: 'description',
}),
id: 'paddingLeft',
label: t('page.fullscreenPlayer.config.lyricPaddingLeft'),
}, },
{ {
control: ( component: (
<Slider <Slider
defaultValue={displaySettings.paddingRight ?? 0} defaultValue={displaySettings.paddingRight ?? 0}
label={(value) => `${value}%`} label={(value) => `${value}%`}
marks={[
{ label: '0', value: 0 },
{ label: '25', value: 25 },
{ label: '50', value: 50 },
]}
max={50} max={50}
min={0} min={0}
onChangeEnd={(value) => { onChangeEnd={(value) => {
updateDisplaySetting({ paddingRight: value }); updateDisplaySetting({ paddingRight: value });
}} }}
step={1} step={1}
w={100} w="100%"
/> />
), ),
description: '', description: t('page.fullscreenPlayer.config.lyricPaddingRight', {
title: t('page.fullscreenPlayer.config.lyricPaddingRight'), context: 'description',
}),
id: 'paddingRight',
label: t('page.fullscreenPlayer.config.lyricPaddingRight'),
}, },
{ {
control: ( component: (
<SegmentedControl <SegmentedControl
data={[ data={[
{ label: t('common.left'), value: 'left' }, { label: t('common.left'), value: 'left' },
@@ -196,41 +232,57 @@ export const LyricsSettingsForm = ({ settingsKey }: LyricsSettingsFormProps) =>
updateLyricsSetting({ alignment: value as 'center' | 'left' | 'right' }) updateLyricsSetting({ alignment: value as 'center' | 'left' | 'right' })
} }
value={lyricsSettings.alignment} value={lyricsSettings.alignment}
w="100%"
/> />
), ),
description: '', description: t('page.fullscreenPlayer.config.lyricAlignment', {
title: t('page.fullscreenPlayer.config.lyricAlignment'), context: 'description',
}),
id: 'alignment',
label: t('page.fullscreenPlayer.config.lyricAlignment'),
}, },
{ {
control: ( component: (
<Switch <ListConfigBooleanControl
aria-label="Follow lyrics" onChange={(value) => updateLyricsSetting({ follow: value })}
defaultChecked={lyricsSettings.follow} value={lyricsSettings.follow}
onChange={(e) => updateLyricsSetting({ follow: e.currentTarget.checked })}
/> />
), ),
description: '', description: t('page.fullscreenPlayer.config.followCurrentLyric', {
title: t('page.fullscreenPlayer.config.followCurrentLyric'), context: 'description',
}),
id: 'follow',
label: t('page.fullscreenPlayer.config.followCurrentLyric'),
}, },
{ {
control: ( component: (
<Slider <Slider
defaultValue={lyricsSettings.followScrollAlignment ?? 0} defaultValue={lyricsSettings.followScrollAlignment ?? 0}
label={(value) => value.toString()} label={(value) => 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} max={50}
min={-50} min={-50}
onChangeEnd={(value) => { onChangeEnd={(value) => {
updateLyricsSetting({ followScrollAlignment: value }); updateLyricsSetting({ followScrollAlignment: value });
}} }}
step={1} step={1}
w={100} w="100%"
/> />
), ),
description: '', description: t('page.fullscreenPlayer.config.lyricFollowScrollAlignment', {
title: t('page.fullscreenPlayer.config.lyricFollowScrollAlignment'), context: 'description',
}),
id: 'followScrollAlignment',
label: t('page.fullscreenPlayer.config.lyricFollowScrollAlignment'),
}, },
{ {
control: ( component: (
<NumberInput <NumberInput
defaultValue={lyricsSettings.lineLeadTimeMs} defaultValue={lyricsSettings.lineLeadTimeMs}
max={3000} max={3000}
@@ -243,14 +295,24 @@ export const LyricsSettingsForm = ({ settingsKey }: LyricsSettingsFormProps) =>
width={100} width={100}
/> />
), ),
description: '', description: t('page.fullscreenPlayer.config.lyricLineLeadTime', {
title: t('page.fullscreenPlayer.config.lyricLineLeadTime'), context: 'description',
}),
id: 'lineLeadTimeMs',
label: t('page.fullscreenPlayer.config.lyricLineLeadTime'),
}, },
{ {
control: ( component: (
<Slider <Slider
defaultValue={displaySettings.opacityNonActive} defaultValue={displaySettings.opacityNonActive}
label={(e) => (e * 100).toFixed(0) + '%'} label={(e) => (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} max={1.0}
min={0.0} min={0.0}
onChangeEnd={(e) => { onChangeEnd={(e) => {
@@ -259,17 +321,25 @@ export const LyricsSettingsForm = ({ settingsKey }: LyricsSettingsFormProps) =>
}); });
}} }}
step={0.01} step={0.01}
w={100} w="100%"
/> />
), ),
description: '', description: t('page.fullscreenPlayer.config.lyricOpacityNonActive', {
title: t(`${t('page.fullscreenPlayer.config.lyricOpacityNonActive')}`, {}), context: 'description',
}),
id: 'opacityNonActive',
label: t('page.fullscreenPlayer.config.lyricOpacityNonActive'),
}, },
{ {
control: ( component: (
<Slider <Slider
defaultValue={displaySettings.scaleNonActive} defaultValue={displaySettings.scaleNonActive}
label={(e) => (e * 100).toFixed(0) + '%'} label={(e) => (e * 100).toFixed(0) + '%'}
marks={[
{ label: '50%', value: 0.5 },
{ label: '75%', value: 0.75 },
{ label: '100%', value: 1 },
]}
max={1.0} max={1.0}
min={0.5} min={0.5}
onChangeEnd={(e) => { onChangeEnd={(e) => {
@@ -278,69 +348,74 @@ export const LyricsSettingsForm = ({ settingsKey }: LyricsSettingsFormProps) =>
}); });
}} }}
step={0.01} step={0.01}
w={100} w="100%"
/> />
), ),
description: '', description: t('page.fullscreenPlayer.config.lyricScaleNonActive', {
title: t(`${t('page.fullscreenPlayer.config.lyricScaleNonActive')}`, {}), context: 'description',
}),
id: 'scaleNonActive',
label: t('page.fullscreenPlayer.config.lyricScaleNonActive'),
}, },
{ {
control: ( component: (
<Switch <ListConfigBooleanControl
aria-label="Show match" onChange={(value) => updateLyricsSetting({ showMatch: value })}
defaultChecked={lyricsSettings.showMatch} value={lyricsSettings.showMatch}
onChange={(e) => updateLyricsSetting({ showMatch: e.currentTarget.checked })}
/> />
), ),
description: '', description: t('page.fullscreenPlayer.config.showLyricMatch', {
title: t('page.fullscreenPlayer.config.showLyricMatch'), context: 'description',
}),
id: 'showMatch',
label: t('page.fullscreenPlayer.config.showLyricMatch'),
}, },
{ {
control: ( component: (
<Switch <ListConfigBooleanControl
aria-label="Show provider" onChange={(value) => updateLyricsSetting({ showProvider: value })}
defaultChecked={lyricsSettings.showProvider} value={lyricsSettings.showProvider}
onChange={(e) => updateLyricsSetting({ showProvider: e.currentTarget.checked })}
/> />
), ),
description: '', description: t('page.fullscreenPlayer.config.showLyricProvider', {
title: t('page.fullscreenPlayer.config.showLyricProvider'), context: 'description',
}),
id: 'showProvider',
label: t('page.fullscreenPlayer.config.showLyricProvider'),
}, },
]; ];
const lyricOptions: SettingOption[] = [ const lyricOptions = [
{ {
control: ( component: (
<Switch <ListConfigBooleanControl
aria-label="Prefer local lyrics" onChange={(value) => updateLyricsSetting({ preferLocalLyrics: value })}
defaultChecked={lyricsSettings.preferLocalLyrics} value={lyricsSettings.preferLocalLyrics}
onChange={(e) =>
updateLyricsSetting({ preferLocalLyrics: e.currentTarget.checked })
}
/> />
), ),
description: t('setting.preferLocalLyrics', { description: t('setting.preferLocalLyrics', {
context: 'description', context: 'description',
}), }),
id: 'preferLocalLyrics',
isHidden: !isElectron(), isHidden: !isElectron(),
title: t('setting.preferLocalLyrics'), label: t('setting.preferLocalLyrics'),
}, },
{ {
control: ( component: (
<Switch <ListConfigBooleanControl
aria-label="Enable fetching lyrics" onChange={(value) => updateLyricsSetting({ fetch: value })}
defaultChecked={lyricsSettings.fetch} value={lyricsSettings.fetch}
onChange={(e) => updateLyricsSetting({ fetch: e.currentTarget.checked })}
/> />
), ),
description: t('setting.lyricFetch', { description: t('setting.lyricFetch', {
context: 'description', context: 'description',
}), }),
id: 'fetch',
isHidden: !isElectron(), isHidden: !isElectron(),
title: t('setting.lyricFetch'), label: t('setting.lyricFetch'),
}, },
{ {
control: ( component: (
<MultiSelect <MultiSelect
aria-label="Lyric providers" aria-label="Lyric providers"
clearable clearable
@@ -350,97 +425,96 @@ export const LyricsSettingsForm = ({ settingsKey }: LyricsSettingsFormProps) =>
localSettings?.set('lyrics', e); localSettings?.set('lyrics', e);
updateLyricsSetting({ sources: e.map((source) => source as LyricSource) }); updateLyricsSetting({ sources: e.map((source) => source as LyricSource) });
}} }}
width={300} width="100%"
/> />
), ),
description: t('setting.lyricFetchProvider', { description: t('setting.lyricFetchProvider', {
context: 'description', context: 'description',
}), }),
id: 'sources',
isHidden: !isElectron(), isHidden: !isElectron(),
title: t('setting.lyricFetchProvider'), label: t('setting.lyricFetchProvider'),
}, },
{ {
control: ( component: (
<Switch <ListConfigBooleanControl
aria-label="Enable furigana" onChange={(value) => updateLyricsSetting({ enableFurigana: value })}
defaultChecked={lyricsSettings.enableFurigana} value={lyricsSettings.enableFurigana ?? false}
onChange={(e) =>
updateLyricsSetting({ enableFurigana: e.currentTarget.checked })
}
/> />
), ),
description: t('setting.enableFurigana', { description: t('setting.enableFurigana', {
context: 'description', context: 'description',
}), }),
title: t('setting.enableFurigana'), id: 'enableFurigana',
label: t('setting.enableFurigana'),
}, },
{ {
control: ( component: (
<Switch <ListConfigBooleanControl
aria-label="Enable romaji" onChange={(value) => updateLyricsSetting({ enableRomaji: value })}
defaultChecked={lyricsSettings.enableRomaji} value={lyricsSettings.enableRomaji ?? false}
onChange={(e) => updateLyricsSetting({ enableRomaji: e.currentTarget.checked })}
/> />
), ),
description: t('setting.enableRomaji', { description: t('setting.enableRomaji', {
context: 'description', context: 'description',
}), }),
title: t('setting.enableRomaji'), id: 'enableRomaji',
label: t('setting.enableRomaji'),
}, },
{ {
control: ( component: (
<Switch <ListConfigBooleanControl
aria-label="Enable NetEase translations" onChange={(value) => {
defaultChecked={lyricsSettings.enableNeteaseTranslation} updateLyricsSetting({ enableNeteaseTranslation: value });
onChange={(e) => { localSettings?.set('enableNeteaseTranslation', value);
const isChecked = e.currentTarget.checked;
updateLyricsSetting({ enableNeteaseTranslation: isChecked });
localSettings?.set('enableNeteaseTranslation', isChecked);
}} }}
value={lyricsSettings.enableNeteaseTranslation}
/> />
), ),
description: t('setting.neteaseTranslation', { description: t('setting.neteaseTranslation', {
context: 'description', context: 'description',
}), }),
id: 'enableNeteaseTranslation',
isHidden: !isElectron(), isHidden: !isElectron(),
title: t('setting.neteaseTranslation'), label: t('setting.neteaseTranslation'),
}, },
];
const translationSettings = [
{ {
control: ( component: (
<NumberInput <ListConfigBooleanControl
defaultValue={lyricsSettings.delayMs} onChange={(value) => updateLyricsSetting({ enableAutoTranslation: value })}
onBlur={(e) => { value={lyricsSettings.enableAutoTranslation}
const value = Number(e.currentTarget.value);
updateLyricsSetting({ delayMs: value });
}}
step={10}
width={100}
/> />
), ),
description: t('setting.lyricOffset', { description: t('setting.enableAutoTranslation', {
context: 'description', context: 'description',
}), }),
id: 'enableAutoTranslation',
isHidden: !isElectron(), isHidden: !isElectron(),
title: t('setting.lyricOffset'), label: t('setting.enableAutoTranslation'),
}, },
{ {
control: ( component: (
<Select <Select
data={languages} data={languages}
onChange={(value) => { onChange={(value) => {
updateLyricsSetting({ translationTargetLanguage: value }); updateLyricsSetting({ translationTargetLanguage: value });
}} }}
value={lyricsSettings.translationTargetLanguage} value={lyricsSettings.translationTargetLanguage}
width="100%"
/> />
), ),
description: t('setting.translationTargetLanguage', { description: t('setting.translationTargetLanguage', {
context: 'description', context: 'description',
}), }),
id: 'translationTargetLanguage',
isHidden: !isElectron(), isHidden: !isElectron(),
title: t('setting.translationTargetLanguage'), label: t('setting.translationTargetLanguage'),
}, },
{ {
control: ( component: (
<Select <Select
clearable clearable
data={['Microsoft Azure', 'Google Cloud']} data={['Microsoft Azure', 'Google Cloud']}
@@ -448,54 +522,45 @@ export const LyricsSettingsForm = ({ settingsKey }: LyricsSettingsFormProps) =>
updateLyricsSetting({ translationApiProvider: value }); updateLyricsSetting({ translationApiProvider: value });
}} }}
value={lyricsSettings.translationApiProvider} value={lyricsSettings.translationApiProvider}
width="100%"
/> />
), ),
description: t('setting.translationApiProvider', { description: t('setting.translationApiProvider', {
context: 'description', context: 'description',
}), }),
id: 'translationApiProvider',
isHidden: !isElectron(), isHidden: !isElectron(),
title: t('setting.translationApiProvider'), label: t('setting.translationApiProvider'),
}, },
{ {
control: ( component: (
<TextInput <TextInput
onChange={(e) => { onChange={(e) => {
updateLyricsSetting({ translationApiKey: e.currentTarget.value }); updateLyricsSetting({ translationApiKey: e.currentTarget.value });
}} }}
value={lyricsSettings.translationApiKey} value={lyricsSettings.translationApiKey}
width="100%"
/> />
), ),
description: t('setting.translationApiKey', { description: t('setting.translationApiKey', {
context: 'description', context: 'description',
}), }),
id: 'translationApiKey',
isHidden: !isElectron(), isHidden: !isElectron(),
title: t('setting.translationApiKey'), label: t('setting.translationApiKey'),
},
{
control: (
<Switch
aria-label="Enable auto translation"
defaultChecked={lyricsSettings.enableAutoTranslation}
onChange={(e) =>
updateLyricsSetting({ enableAutoTranslation: e.currentTarget.checked })
}
/>
),
description: t('setting.enableAutoTranslation', {
context: 'description',
}),
isHidden: !isElectron(),
title: t('setting.enableAutoTranslation'),
}, },
]; ];
return ( return (
<Stack gap="md" p="md"> <Stack gap="sm">
<Fieldset legend={t('page.setting.lyricsDisplay')}>
<SettingsSection options={displayOptions} />
</Fieldset>
<Fieldset legend={t('page.setting.lyrics')}> <Fieldset legend={t('page.setting.lyrics')}>
<SettingsSection options={lyricOptions} /> <ListConfigTable options={lyricOptions} />
</Fieldset>
<Fieldset legend={t('page.setting.lyricsDisplay')}>
<ListConfigTable options={displayOptions} />
</Fieldset>
<Fieldset legend={t('page.setting.lyricsTranslation')}>
<ListConfigTable options={translationSettings} />
</Fieldset> </Fieldset>
</Stack> </Stack>
); );
@@ -54,7 +54,6 @@ import { SegmentedControl } from '/@/shared/components/segmented-control/segment
import { Select } from '/@/shared/components/select/select'; import { Select } from '/@/shared/components/select/select';
import { Slider } from '/@/shared/components/slider/slider'; import { Slider } from '/@/shared/components/slider/slider';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { Text } from '/@/shared/components/text/text';
import { useMediaQuery } from '/@/shared/hooks/use-media-query'; import { useMediaQuery } from '/@/shared/hooks/use-media-query';
import { useThrottledCallback } from '/@/shared/hooks/use-throttled-callback'; import { useThrottledCallback } from '/@/shared/hooks/use-throttled-callback';
import { useThrottledValue } from '/@/shared/hooks/use-throttled-value'; import { useThrottledValue } from '/@/shared/hooks/use-throttled-value';
@@ -201,17 +200,9 @@ const AutoDJButton = () => {
w="96px" w="96px"
/> />
), ),
description: t('setting.autoDJ_itemCount_description'),
id: 'itemCount', id: 'itemCount',
label: ( label: t('setting.autoDJ_itemCount'),
<Stack gap="xs">
<Text isNoSelect size="sm">
{t('setting.autoDJ_itemCount')}
</Text>
<Text isMuted isNoSelect size="xs">
{t('setting.autoDJ_itemCount_description')}
</Text>
</Stack>
),
}, },
{ {
component: ( component: (
@@ -239,17 +230,9 @@ const AutoDJButton = () => {
w="144px" w="144px"
/> />
), ),
description: t('setting.autoDJ_timing_description'),
id: 'timing', id: 'timing',
label: ( label: t('setting.autoDJ_timing'),
<Stack gap="xs">
<Text isNoSelect size="sm">
{t('setting.autoDJ_timing')}
</Text>
<Text isMuted isNoSelect size="xs">
{t('setting.autoDJ_timing_description')}
</Text>
</Stack>
),
}, },
], ],
[ [
@@ -279,17 +262,9 @@ const AutoDJButton = () => {
value={settings.allowDuplicates} value={settings.allowDuplicates}
/> />
), ),
description: t('setting.autoDJ_allowDuplicates_description'),
id: 'allowDuplicates', id: 'allowDuplicates',
label: ( label: t('setting.autoDJ_allowDuplicates'),
<Stack gap="xs">
<Text isNoSelect size="sm">
{t('setting.autoDJ_allowDuplicates')}
</Text>
<Text isMuted isNoSelect size="xs">
{t('setting.autoDJ_allowDuplicates_description')}
</Text>
</Stack>
),
}, },
{ {
component: ( component: (
@@ -304,17 +279,9 @@ const AutoDJButton = () => {
value={settings.onlySimilar} value={settings.onlySimilar}
/> />
), ),
description: t('setting.autoDJ_onlySimilar_description'),
id: 'onlySimilar', id: 'onlySimilar',
label: ( label: t('setting.autoDJ_onlySimilar'),
<Stack gap="xs">
<Text isNoSelect size="sm">
{t('setting.autoDJ_onlySimilar')}
</Text>
<Text isMuted isNoSelect size="xs">
{t('setting.autoDJ_onlySimilar_description')}
</Text>
</Stack>
),
}, },
], ],
[setSettings, settings.allowDuplicates, settings.onlySimilar, t], [setSettings, settings.allowDuplicates, settings.onlySimilar, t],
@@ -6,13 +6,13 @@
width: 50%; width: 50%;
padding: var(--theme-spacing-md) var(--theme-spacing-md) var(--theme-spacing-md) 0; padding: var(--theme-spacing-md) var(--theme-spacing-md) var(--theme-spacing-md) 0;
font-weight: 500; font-weight: 500;
vertical-align: middle; vertical-align: top;
background-color: initial; background-color: initial;
} }
.td { .td {
padding: 0; padding: 0;
vertical-align: middle; vertical-align: top;
} }
.control { .control {
@@ -20,6 +20,7 @@
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
width: 100%; width: 100%;
padding: var(--theme-spacing-md) 0;
} }
.divider-cell { .divider-cell {
@@ -17,6 +17,7 @@ import { SegmentedControl } from '/@/shared/components/segmented-control/segment
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { Switch } from '/@/shared/components/switch/switch'; import { Switch } from '/@/shared/components/switch/switch';
import { Table } from '/@/shared/components/table/table'; import { Table } from '/@/shared/components/table/table';
import { Text } from '/@/shared/components/text/text';
import { useDisclosure } from '/@/shared/hooks/use-disclosure'; import { useDisclosure } from '/@/shared/hooks/use-disclosure';
import { ItemListKey, ListDisplayType } from '/@/shared/types/types'; import { ItemListKey, ListDisplayType } from '/@/shared/types/types';
@@ -232,6 +233,7 @@ export const ListConfigTable = ({
}: { }: {
options: { options: {
component: ReactNode; component: ReactNode;
description?: ReactNode | string;
id: string; id: string;
isDivider?: boolean; isDivider?: boolean;
isHidden?: boolean; isHidden?: boolean;
@@ -269,7 +271,20 @@ export const ListConfigTable = ({
return ( return (
<Table.Tr key={option.id}> <Table.Tr key={option.id}>
<Table.Th>{option.label}</Table.Th> <Table.Th>
{option.description !== undefined ? (
<Stack gap="xs">
<Text isNoSelect size="sm">
{option.label}
</Text>
<Text isMuted isNoSelect size="xs">
{option.description}
</Text>
</Stack>
) : (
option.label
)}
</Table.Th>
<Table.Td> <Table.Td>
<div className={styles.control}>{option.component}</div> <div className={styles.control}>{option.component}</div>
</Table.Td> </Table.Td>