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",
"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",
@@ -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: (
<NumberInput
onBlur={(e) => {
const value = Number(e.currentTarget.value);
component: (
<Slider
defaultValue={displaySettings.fontSize}
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 });
}}
rightSection={
<Text pr="md" size="sm">
px
</Text>
}
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: (
<NumberInput
onBlur={(e) => {
const value = Number(e.currentTarget.value);
component: (
<Slider
defaultValue={displaySettings.fontSizeUnsync}
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 });
}}
rightSection={
<Text pr="md" size="sm">
px
</Text>
}
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: (
<NumberInput
onBlur={(e) => {
const value = Number(e.currentTarget.value);
component: (
<Slider
defaultValue={displaySettings.gap}
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 });
}}
rightSection={
<Text pr="md" size="sm">
px
</Text>
}
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: (
<NumberInput
onBlur={(e) => {
const value = Number(e.currentTarget.value);
component: (
<Slider
defaultValue={displaySettings.gapUnsync}
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 });
}}
rightSection={
<Text pr="md" size="sm">
px
</Text>
}
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: (
<Slider
defaultValue={displaySettings.paddingLeft ?? 0}
label={(value) => `${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: (
<Slider
defaultValue={displaySettings.paddingRight ?? 0}
label={(value) => `${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: (
<SegmentedControl
data={[
{ label: t('common.left'), value: 'left' },
@@ -196,41 +232,57 @@ export const LyricsSettingsForm = ({ settingsKey }: LyricsSettingsFormProps) =>
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: (
<Switch
aria-label="Follow lyrics"
defaultChecked={lyricsSettings.follow}
onChange={(e) => updateLyricsSetting({ follow: e.currentTarget.checked })}
component: (
<ListConfigBooleanControl
onChange={(value) => 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: (
<Slider
defaultValue={lyricsSettings.followScrollAlignment ?? 0}
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}
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: (
<NumberInput
defaultValue={lyricsSettings.lineLeadTimeMs}
max={3000}
@@ -243,14 +295,24 @@ export const LyricsSettingsForm = ({ settingsKey }: LyricsSettingsFormProps) =>
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: (
<Slider
defaultValue={displaySettings.opacityNonActive}
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}
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: (
<Slider
defaultValue={displaySettings.scaleNonActive}
label={(e) => (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: (
<Switch
aria-label="Show match"
defaultChecked={lyricsSettings.showMatch}
onChange={(e) => updateLyricsSetting({ showMatch: e.currentTarget.checked })}
component: (
<ListConfigBooleanControl
onChange={(value) => 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: (
<Switch
aria-label="Show provider"
defaultChecked={lyricsSettings.showProvider}
onChange={(e) => updateLyricsSetting({ showProvider: e.currentTarget.checked })}
component: (
<ListConfigBooleanControl
onChange={(value) => 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: (
<Switch
aria-label="Prefer local lyrics"
defaultChecked={lyricsSettings.preferLocalLyrics}
onChange={(e) =>
updateLyricsSetting({ preferLocalLyrics: e.currentTarget.checked })
}
component: (
<ListConfigBooleanControl
onChange={(value) => 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: (
<Switch
aria-label="Enable fetching lyrics"
defaultChecked={lyricsSettings.fetch}
onChange={(e) => updateLyricsSetting({ fetch: e.currentTarget.checked })}
component: (
<ListConfigBooleanControl
onChange={(value) => 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: (
<MultiSelect
aria-label="Lyric providers"
clearable
@@ -350,97 +425,96 @@ export const LyricsSettingsForm = ({ settingsKey }: LyricsSettingsFormProps) =>
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: (
<Switch
aria-label="Enable furigana"
defaultChecked={lyricsSettings.enableFurigana}
onChange={(e) =>
updateLyricsSetting({ enableFurigana: e.currentTarget.checked })
}
component: (
<ListConfigBooleanControl
onChange={(value) => updateLyricsSetting({ enableFurigana: value })}
value={lyricsSettings.enableFurigana ?? false}
/>
),
description: t('setting.enableFurigana', {
context: 'description',
}),
title: t('setting.enableFurigana'),
id: 'enableFurigana',
label: t('setting.enableFurigana'),
},
{
control: (
<Switch
aria-label="Enable romaji"
defaultChecked={lyricsSettings.enableRomaji}
onChange={(e) => updateLyricsSetting({ enableRomaji: e.currentTarget.checked })}
component: (
<ListConfigBooleanControl
onChange={(value) => updateLyricsSetting({ enableRomaji: value })}
value={lyricsSettings.enableRomaji ?? false}
/>
),
description: t('setting.enableRomaji', {
context: 'description',
}),
title: t('setting.enableRomaji'),
id: 'enableRomaji',
label: t('setting.enableRomaji'),
},
{
control: (
<Switch
aria-label="Enable NetEase translations"
defaultChecked={lyricsSettings.enableNeteaseTranslation}
onChange={(e) => {
const isChecked = e.currentTarget.checked;
updateLyricsSetting({ enableNeteaseTranslation: isChecked });
localSettings?.set('enableNeteaseTranslation', isChecked);
component: (
<ListConfigBooleanControl
onChange={(value) => {
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: (
<NumberInput
defaultValue={lyricsSettings.delayMs}
onBlur={(e) => {
const value = Number(e.currentTarget.value);
updateLyricsSetting({ delayMs: value });
}}
step={10}
width={100}
component: (
<ListConfigBooleanControl
onChange={(value) => 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: (
<Select
data={languages}
onChange={(value) => {
updateLyricsSetting({ translationTargetLanguage: value });
}}
value={lyricsSettings.translationTargetLanguage}
width="100%"
/>
),
description: t('setting.translationTargetLanguage', {
context: 'description',
}),
id: 'translationTargetLanguage',
isHidden: !isElectron(),
title: t('setting.translationTargetLanguage'),
label: t('setting.translationTargetLanguage'),
},
{
control: (
component: (
<Select
clearable
data={['Microsoft Azure', 'Google Cloud']}
@@ -448,54 +522,45 @@ export const LyricsSettingsForm = ({ settingsKey }: LyricsSettingsFormProps) =>
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: (
<TextInput
onChange={(e) => {
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: (
<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'),
label: t('setting.translationApiKey'),
},
];
return (
<Stack gap="md" p="md">
<Fieldset legend={t('page.setting.lyricsDisplay')}>
<SettingsSection options={displayOptions} />
</Fieldset>
<Stack gap="sm">
<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>
</Stack>
);
@@ -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: (
<Stack gap="xs">
<Text isNoSelect size="sm">
{t('setting.autoDJ_itemCount')}
</Text>
<Text isMuted isNoSelect size="xs">
{t('setting.autoDJ_itemCount_description')}
</Text>
</Stack>
),
label: t('setting.autoDJ_itemCount'),
},
{
component: (
@@ -239,17 +230,9 @@ const AutoDJButton = () => {
w="144px"
/>
),
description: t('setting.autoDJ_timing_description'),
id: 'timing',
label: (
<Stack gap="xs">
<Text isNoSelect size="sm">
{t('setting.autoDJ_timing')}
</Text>
<Text isMuted isNoSelect size="xs">
{t('setting.autoDJ_timing_description')}
</Text>
</Stack>
),
label: t('setting.autoDJ_timing'),
},
],
[
@@ -279,17 +262,9 @@ const AutoDJButton = () => {
value={settings.allowDuplicates}
/>
),
description: t('setting.autoDJ_allowDuplicates_description'),
id: 'allowDuplicates',
label: (
<Stack gap="xs">
<Text isNoSelect size="sm">
{t('setting.autoDJ_allowDuplicates')}
</Text>
<Text isMuted isNoSelect size="xs">
{t('setting.autoDJ_allowDuplicates_description')}
</Text>
</Stack>
),
label: t('setting.autoDJ_allowDuplicates'),
},
{
component: (
@@ -304,17 +279,9 @@ const AutoDJButton = () => {
value={settings.onlySimilar}
/>
),
description: t('setting.autoDJ_onlySimilar_description'),
id: 'onlySimilar',
label: (
<Stack gap="xs">
<Text isNoSelect size="sm">
{t('setting.autoDJ_onlySimilar')}
</Text>
<Text isMuted isNoSelect size="xs">
{t('setting.autoDJ_onlySimilar_description')}
</Text>
</Stack>
),
label: t('setting.autoDJ_onlySimilar'),
},
],
[setSettings, settings.allowDuplicates, settings.onlySimilar, t],
@@ -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 {
@@ -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 (
<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>
<div className={styles.control}>{option.component}</div>
</Table.Td>