diff --git a/src/components/settings/AnalyticsStrategySection.tsx b/src/components/settings/AnalyticsStrategySection.tsx index c621ba9d..1bcdb767 100644 --- a/src/components/settings/AnalyticsStrategySection.tsx +++ b/src/components/settings/AnalyticsStrategySection.tsx @@ -6,6 +6,7 @@ import { save as saveDialog } from '@tauri-apps/plugin-dialog'; import { writeFile } from '@tauri-apps/plugin-fs'; import SettingsSubSection from '../SettingsSubSection'; import { SettingsGroup } from './SettingsGroup'; +import { SettingsSubCard } from './SettingsSubCard'; import { useAnalysisStrategyStore } from '../../store/analysisStrategyStore'; import { useAuthStore } from '../../store/authStore'; import { @@ -255,6 +256,7 @@ export default function AnalyticsStrategySection() { >
+
@@ -434,6 +436,7 @@ export default function AnalyticsStrategySection() { {t('settings.analyticsStrategyAdvancedWarning')} + diff --git a/src/components/settings/AppearanceTab.tsx b/src/components/settings/AppearanceTab.tsx index ae4156fb..955ef5c1 100644 --- a/src/components/settings/AppearanceTab.tsx +++ b/src/components/settings/AppearanceTab.tsx @@ -14,6 +14,7 @@ import { IS_LINUX, IS_WINDOWS } from '../../utils/platform'; import SettingsSubSection from '../SettingsSubSection'; import { SettingsGroup } from './SettingsGroup'; import { SettingsToggle } from './SettingsToggle'; +import { SettingsSubCard, SettingsField, SettingsRow, SettingsValue } from './SettingsSubCard'; import { SeekbarPreview } from '../WaveformSeekPreview'; import WindowButtonPreview from '../WindowButtonPreview'; @@ -40,14 +41,15 @@ export function AppearanceTab() {
{t('settings.libraryGridMaxColumnsPerfHint')}
-
-
-

- {t('settings.libraryGridMaxColumnsDesc')} -

+ {auth.libraryGridMaxColumns} + + @@ -143,24 +139,24 @@ export function AppearanceTab() { /> {auth.useCustomTitlebar && ( <> -
-
-
{t('settings.windowButtonStyle')}
-
- {t('settings.windowButtonStyleDesc')} -
-
- {(['dots', 'dotsGlyph', 'flat', 'pill', 'outline', 'glyph'] as WindowButtonStyle[]).map(style => ( - auth.setWindowButtonStyle(style)} - /> - ))} -
-
+ + +
+ {(['dots', 'dotsGlyph', 'flat', 'pill', 'outline', 'glyph'] as WindowButtonStyle[]).map(style => ( + auth.setWindowButtonStyle(style)} + /> + ))} +
+
+
-
-
- {t('settings.uiScaleLabel')} - - {Math.round(fontStore.uiScale * 100)}% - -
- {(() => { - const presets = [80, 90, 100, 110, 125, 150]; - const currentPct = Math.round(fontStore.uiScale * 100); - let idx = presets.indexOf(currentPct); - if (idx < 0) { - // Snap legacy off-preset values to the closest preset. - idx = presets.reduce((best, p, i) => - Math.abs(p - currentPct) < Math.abs(presets[best] - currentPct) ? i : best, 0); - } - return ( - <> - fontStore.setUiScale(presets[parseInt(e.target.value, 10)] / 100)} - className="ui-scale-slider" - /> -
- {presets.map(p => { - const active = currentPct === p; - return ( - - ); - })} -
- - ); - })()} -
+ + + {(() => { + const presets = [80, 90, 100, 110, 125, 150]; + const currentPct = Math.round(fontStore.uiScale * 100); + let idx = presets.indexOf(currentPct); + if (idx < 0) { + // Snap legacy off-preset values to the closest preset. + idx = presets.reduce((best, p, i) => + Math.abs(p - currentPct) < Math.abs(presets[best] - currentPct) ? i : best, 0); + } + return ( + <> + + fontStore.setUiScale(presets[parseInt(e.target.value, 10)] / 100)} + className="ui-scale-slider" + /> + {currentPct}% + +
+ {presets.map(p => { + const active = currentPct === p; + return ( + + ); + })} +
+ + ); + })()} +
+
@@ -242,6 +237,7 @@ export function AppearanceTab() { >
+
{( [ @@ -283,6 +279,7 @@ export function AppearanceTab() { ))}
+
@@ -293,20 +290,21 @@ export function AppearanceTab() { >
-
- {t('settings.seekbarStyleDesc')} -
-
- {(['truewave', 'pseudowave', 'linedot', 'bar', 'thick', 'segmented', 'neon', 'pulsewave', 'particletrail', 'liquidfill', 'retrotape'] as SeekbarStyle[]).map(style => ( - auth.setSeekbarStyle(style)} - /> - ))} -
+ + +
+ {(['truewave', 'pseudowave', 'linedot', 'bar', 'thick', 'segmented', 'neon', 'pulsewave', 'particletrail', 'liquidfill', 'retrotape'] as SeekbarStyle[]).map(style => ( + auth.setSeekbarStyle(style)} + /> + ))} +
+
+
diff --git a/src/components/settings/BackupSection.tsx b/src/components/settings/BackupSection.tsx index b91b5ac0..8fd2e806 100644 --- a/src/components/settings/BackupSection.tsx +++ b/src/components/settings/BackupSection.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'; import { Clock3, Download, Upload } from 'lucide-react'; import { createPortal } from 'react-dom'; import { SettingsGroup } from './SettingsGroup'; +import { SettingsSubCard, SettingsField } from './SettingsSubCard'; import { exportBackupToPath, importAnyBackupFromPath, @@ -151,12 +152,12 @@ export function BackupSection() { return (
-
+
{(['full', 'library', 'config'] as BackupMode[]).map(candidate => (
-
-
-
{modeTitle}
-
{modeDesc}
-
-
- -
- - -
+ + +
+ + +
+
+
{busyOverlay}
diff --git a/src/components/settings/CoverCacheStrategySection.tsx b/src/components/settings/CoverCacheStrategySection.tsx index 74cca612..c5527716 100644 --- a/src/components/settings/CoverCacheStrategySection.tsx +++ b/src/components/settings/CoverCacheStrategySection.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'; import { listen } from '@tauri-apps/api/event'; import SettingsSubSection from '../SettingsSubSection'; import { SettingsGroup } from './SettingsGroup'; +import { SettingsSubCard } from './SettingsSubCard'; import { useCoverStrategyStore } from '../../store/coverStrategyStore'; import { useAuthStore } from '../../store/authStore'; import { @@ -234,6 +235,7 @@ export default function CoverCacheStrategySection() { }>
+
@@ -416,6 +418,7 @@ export default function CoverCacheStrategySection() { )} + diff --git a/src/components/settings/InputTab.tsx b/src/components/settings/InputTab.tsx index c15df108..113ed9e2 100644 --- a/src/components/settings/InputTab.tsx +++ b/src/components/settings/InputTab.tsx @@ -6,6 +6,7 @@ import { useGlobalShortcutsStore, type GlobalAction, buildGlobalShortcut, format import { useKeybindingsStore, type KeyAction, buildInAppBinding, formatBinding } from '../../store/keybindingsStore'; import SettingsSubSection from '../SettingsSubSection'; import { SettingsGroup } from './SettingsGroup'; +import { SettingsSubCard } from './SettingsSubCard'; export function InputTab() { const { t } = useTranslation(); @@ -34,6 +35,7 @@ export function InputTab() { >
+
{IN_APP_SHORTCUT_ACTIONS.map(({ id: action, getLabel }) => { const label = getLabel(t); @@ -97,6 +99,7 @@ export function InputTab() { ); })}
+
@@ -120,6 +123,7 @@ export function InputTab() { >
+
{GLOBAL_SHORTCUT_ACTIONS.map(({ id: action, getLabel }) => { const label = getLabel(t); @@ -181,6 +185,7 @@ export function InputTab() { ); })}
+
diff --git a/src/components/settings/IntegrationsTab.tsx b/src/components/settings/IntegrationsTab.tsx index 74714d8d..77835852 100644 --- a/src/components/settings/IntegrationsTab.tsx +++ b/src/components/settings/IntegrationsTab.tsx @@ -5,6 +5,7 @@ import { useThemeStore } from '../../store/themeStore'; import SettingsSubSection from '../SettingsSubSection'; import { SettingsGroup } from './SettingsGroup'; import { SettingsToggle } from './SettingsToggle'; +import { SettingsSubCard, SettingsField } from './SettingsSubCard'; import { MusicNetworkSection } from './musicNetwork/MusicNetworkSection'; import { purgeExternalArtworkAllServers } from '../../api/coverCache'; @@ -77,46 +78,44 @@ export function IntegrationsTab() { -
- - auth.setDiscordTemplateName(e.target.value)} - placeholder="{title}" - /> -
-
- - auth.setDiscordTemplateDetails(e.target.value)} - placeholder="{artist}" - /> -
-
- - auth.setDiscordTemplateState(e.target.value)} - placeholder="{title}" - /> -
-
- - auth.setDiscordTemplateLargeText(e.target.value)} - placeholder="{album}" - /> -
+ + + auth.setDiscordTemplateName(e.target.value)} + placeholder="{title}" + /> + + + auth.setDiscordTemplateDetails(e.target.value)} + placeholder="{artist}" + /> + + + auth.setDiscordTemplateState(e.target.value)} + placeholder="{title}" + /> + + + auth.setDiscordTemplateLargeText(e.target.value)} + placeholder="{album}" + /> + +
)} @@ -166,31 +165,33 @@ export function IntegrationsTab() { title={t('settings.externalArtworkByokTitle')} desc={t('settings.externalArtworkByokDesc')} > -
- theme.setExternalArtworkByok(e.target.value)} - placeholder="fanart.tv personal API key" - spellCheck={false} - autoComplete="off" - /> - {theme.externalArtworkByok.trim() && ( -
- {t('settings.externalArtworkByokSaved')} -
- )} -
+ + + theme.setExternalArtworkByok(e.target.value)} + placeholder="fanart.tv personal API key" + spellCheck={false} + autoComplete="off" + /> + {theme.externalArtworkByok.trim() && ( +
+ {t('settings.externalArtworkByokSaved')} +
+ )} +
+
)} diff --git a/src/components/settings/LibraryTab.tsx b/src/components/settings/LibraryTab.tsx index 4535f6f5..d28f6d2c 100644 --- a/src/components/settings/LibraryTab.tsx +++ b/src/components/settings/LibraryTab.tsx @@ -6,6 +6,7 @@ import { MIX_MIN_RATING_FILTER_MAX_STARS } from '../../store/authStoreDefaults'; import SettingsSubSection from '../SettingsSubSection'; import { SettingsGroup } from './SettingsGroup'; import { SettingsToggle } from './SettingsToggle'; +import { SettingsSubCard, SettingsField } from './SettingsSubCard'; import StarRating from '../StarRating'; import AnalyticsStrategySection from './AnalyticsStrategySection'; const AUDIOBOOK_GENRES_DISPLAY = ['Hörbuch', 'Hoerbuch', 'Hörspiel', 'Hoerspiel', 'Audiobook', 'Audio Book', 'Spoken Word', 'Spokenword', 'Podcast', 'Kapitel', 'Thriller', 'Krimi', 'Speech', 'Fantasy', 'Comedy', 'Literature']; @@ -26,78 +27,77 @@ export function LibraryTab() { >
-

- {t('settings.randomMixBlacklistDesc')} -

- -
{t('settings.randomMixBlacklistTitle')}
-
- {auth.customGenreBlacklist.length === 0 ? ( - {t('settings.randomMixBlacklistEmpty')} - ) : ( - auth.customGenreBlacklist.map(genre => ( - - {genre} - - - )) - )} -
- -
- setNewGenre(e.target.value)} - onKeyDown={e => { - if (e.key === 'Enter' && newGenre.trim()) { - const trimmed = newGenre.trim(); - if (!auth.customGenreBlacklist.includes(trimmed)) { - auth.setCustomGenreBlacklist([...auth.customGenreBlacklist, trimmed]); - } - setNewGenre(''); - } - }} - placeholder={t('settings.randomMixBlacklistPlaceholder')} - style={{ fontSize: 13 }} - /> - -
- -
{t('settings.randomMixHardcodedTitle')}
-
- {AUDIOBOOK_GENRES_DISPLAY.map(genre => ( - - {genre} - - ))} -
+ + + +
+ {auth.customGenreBlacklist.length === 0 ? ( + {t('settings.randomMixBlacklistEmpty')} + ) : ( + auth.customGenreBlacklist.map(genre => ( + + {genre} + + + )) + )} +
+
+ setNewGenre(e.target.value)} + onKeyDown={e => { + if (e.key === 'Enter' && newGenre.trim()) { + const trimmed = newGenre.trim(); + if (!auth.customGenreBlacklist.includes(trimmed)) { + auth.setCustomGenreBlacklist([...auth.customGenreBlacklist, trimmed]); + } + setNewGenre(''); + } + }} + placeholder={t('settings.randomMixBlacklistPlaceholder')} + style={{ fontSize: 13 }} + /> + +
+
+ +
+ {AUDIOBOOK_GENRES_DISPLAY.map(genre => ( + + {genre} + + ))} +
+
+
@@ -156,8 +156,7 @@ export function LibraryTab() { onChange={auth.setMixMinRatingFilterEnabled} /> {auth.mixMinRatingFilterEnabled && ( - <> -
+
))}
- +
)}
diff --git a/src/components/settings/SettingsSubCard.tsx b/src/components/settings/SettingsSubCard.tsx new file mode 100644 index 00000000..6ba7ee48 --- /dev/null +++ b/src/components/settings/SettingsSubCard.tsx @@ -0,0 +1,104 @@ +import React from 'react'; + +/** + * Settings sub-card primitives — the canonical way to render per-mode / detail + * controls as a set-apart group inside a settings section (e.g. the "Target + * LUFS" box under the Normalization engine picker). The Audio tab's first four + * sections are the reference for this look. + * + * These wrap the shared `settings-norm-*` styles so callers never hand-roll the + * box with inline padding or one-off classes. New settings sub-options should + * use these instead of bare `
`s — see `NormalizationBlock` for usage. + */ + +/** Bordered, accent-tinted panel that groups detail controls inside a section. */ +export function SettingsSubCard({ + children, + style, + className, +}: { + children: React.ReactNode; + style?: React.CSSProperties; + className?: string; +}) { + return ( +
+ {children} +
+ ); +} + +/** + * One labelled group inside a {@link SettingsSubCard}: an optional title and + * description, the controls, and an optional grey note line below them. + * + * `row` lays the label out beside the controls (for a slider/value pair, like + * the Normalization sliders); the default stacks label → description → + * controls (for a picker, like the Hi-Res blend-rate buttons). + */ +export function SettingsField({ + label, + desc, + note, + row = false, + children, +}: { + label?: React.ReactNode; + desc?: React.ReactNode; + note?: React.ReactNode; + row?: boolean; + children?: React.ReactNode; +}) { + return ( +
+ {row ? ( + <> +
+ {label != null && {label}} + {children} +
+ {desc != null &&
{desc}
} + + ) : ( + <> + {label != null && ( + + {label} + + )} + {desc != null &&
{desc}
} + {children} + + )} + {note != null && ( +
+ {note} +
+ )} +
+ ); +} + +/** + * A horizontal control row (label and/or control + value side by side), for a + * standalone slider inside a {@link SettingsSubCard} or a nested slider/value + * pair inside a stacked {@link SettingsField}. For a labelled field whose + * description sits below the row, prefer ``. + */ +export function SettingsRow({ children }: { children: React.ReactNode }) { + return
{children}
; +} + +/** Right-aligned, tabular value readout for a slider row. */ +export function SettingsValue({ children }: { children: React.ReactNode }) { + return {children}; +} + +/** + * Accent callout (border-left + tinted background) for an important caveat + * inside a {@link SettingsSubCard} — e.g. the Normalization first-play note. + * For a plain grey note line, use the `note` prop on {@link SettingsField}. + */ +export function SettingsCallout({ children }: { children: React.ReactNode }) { + return
{children}
; +} diff --git a/src/components/settings/StorageTab.tsx b/src/components/settings/StorageTab.tsx index 86a0da1d..924697b5 100644 --- a/src/components/settings/StorageTab.tsx +++ b/src/components/settings/StorageTab.tsx @@ -10,6 +10,7 @@ import { formatBytes, snapHotCacheMb } from '../../utils/format/formatBytes'; import SettingsSubSection from '../SettingsSubSection'; import { SettingsGroup } from './SettingsGroup'; import { SettingsToggle } from './SettingsToggle'; +import { SettingsSubCard, SettingsField, SettingsValue } from './SettingsSubCard'; import CoverCacheStrategySection from './CoverCacheStrategySection'; export function StorageTab() { @@ -77,33 +78,32 @@ export function StorageTab() { >
-
- - {auth.mediaDir && ( - - )} - -
- {auth.mediaDir && ( -
- {t('settings.mediaDirHint')} -
- )} + + +
+ + {auth.mediaDir && ( + + )} + +
+
+
@@ -135,42 +135,35 @@ export function StorageTab() { /> {auth.hotCacheEnabled && ( -
-
-
- {t('settings.cacheUsedHot')} - {hotCacheBytes !== null ? formatBytes(hotCacheBytes) : '…'} + + +
+
+ {t('settings.cacheUsedHot')} + {hotCacheBytes !== null ? formatBytes(hotCacheBytes) : '…'} +
+
+ {t('settings.hotCacheTrackCount')} + {hotCacheTrackCount} +
-
- {t('settings.hotCacheTrackCount')} - {hotCacheTrackCount} -
-
- -
-
{t('settings.hotCacheMaxMb')}
-
- auth.setHotCacheMaxMb(parseInt(e.target.value, 10))} style={{ flex: 1, minWidth: 80, maxWidth: 200 }} id="hot-cache-max-mb-slider" /> - {snapHotCacheMb(auth.hotCacheMaxMb)} MB -
-
-
-
{t('settings.hotCacheDebounce')}
-
- auth.setHotCacheDebounceSec(parseInt(e.target.value, 10))} style={{ flex: 1, minWidth: 80, maxWidth: 200 }} id="hot-cache-debounce-slider" /> - - {Math.min(600, Math.max(0, auth.hotCacheDebounceSec)) === 0 - ? t('settings.hotCacheDebounceImmediate') - : t('settings.hotCacheDebounceSeconds', { n: Math.min(600, Math.max(0, auth.hotCacheDebounceSec)) })} - -
-
- -
+ + + auth.setHotCacheMaxMb(parseInt(e.target.value, 10))} id="hot-cache-max-mb-slider" /> + {snapHotCacheMb(auth.hotCacheMaxMb)} MB + + + auth.setHotCacheDebounceSec(parseInt(e.target.value, 10))} id="hot-cache-debounce-slider" /> + + {Math.min(600, Math.max(0, auth.hotCacheDebounceSec)) === 0 + ? t('settings.hotCacheDebounceImmediate') + : t('settings.hotCacheDebounceSeconds', { n: Math.min(600, Math.max(0, auth.hotCacheDebounceSec)) })} + + -
+ )}
@@ -190,29 +183,33 @@ export function StorageTab() { >
-
- - {auth.downloadFolder && ( - - )} - -
+ + +
+ + {auth.downloadFolder && ( + + )} + +
+
+
diff --git a/src/components/settings/SystemTab.tsx b/src/components/settings/SystemTab.tsx index 083624a6..15e9e5b2 100644 --- a/src/components/settings/SystemTab.tsx +++ b/src/components/settings/SystemTab.tsx @@ -17,6 +17,7 @@ import LicensesPanel from '../LicensesPanel'; import SettingsSubSection from '../SettingsSubSection'; import { SettingsGroup } from './SettingsGroup'; import { SettingsToggle } from './SettingsToggle'; +import { SettingsSubCard, SettingsField } from './SettingsSubCard'; import { BackupSection } from './BackupSection'; import { CONTRIBUTORS, MAINTAINERS } from '../../config/settingsCredits'; @@ -58,25 +59,27 @@ export function SystemTab() { >
-
- i18n.changeLanguage(v)} - options={[ - { value: 'en', label: t('settings.languageEn') }, - { value: 'de', label: t('settings.languageDe') }, - { value: 'es', label: t('settings.languageEs') }, - { value: 'fr', label: t('settings.languageFr') }, - { value: 'nl', label: t('settings.languageNl') }, - { value: 'nb', label: t('settings.languageNb') }, - { value: 'ru', label: t('settings.languageRu') }, - { value: 'zh', label: t('settings.languageZh') }, - { value: 'ro', label: t('settings.languageRo') }, - { value: 'ja', label: t('settings.languageJa') }, - { value: 'hu', label: t('settings.languageHu') }, - ]} - /> -
+ + + i18n.changeLanguage(v)} + options={[ + { value: 'en', label: t('settings.languageEn') }, + { value: 'de', label: t('settings.languageDe') }, + { value: 'es', label: t('settings.languageEs') }, + { value: 'fr', label: t('settings.languageFr') }, + { value: 'nl', label: t('settings.languageNl') }, + { value: 'nb', label: t('settings.languageNb') }, + { value: 'ru', label: t('settings.languageRu') }, + { value: 'zh', label: t('settings.languageZh') }, + { value: 'ro', label: t('settings.languageRo') }, + { value: 'ja', label: t('settings.languageJa') }, + { value: 'hu', label: t('settings.languageHu') }, + ]} + /> + +
@@ -119,13 +122,11 @@ export function SystemTab() { onChange={auth.setLinuxWebkitInputForceRepaint} /> {waylandTextRenderAvailable && ( - <> -
-
-
{t('settings.linuxWaylandTextRender')}
-
- {t('settings.linuxWaylandTextRenderDesc')} -
+ + auth.setLinuxWaylandTextRenderProfile(v as LinuxWaylandTextRenderProfile)} @@ -136,30 +137,28 @@ export function SystemTab() { { value: 'minimal', label: t('settings.linuxWaylandTextRenderMinimal') }, ]} /> -
- + + )} )} -
-
-
{t('settings.clockFormat')}
-
{t('settings.clockFormatDesc')}
-
-
- auth.setClockFormat(v as ClockFormat)} - options={[ - { value: 'auto', label: t('settings.clockFormatAuto') }, - { value: '24h', label: t('settings.clockFormatTwentyFour') }, - { value: '12h', label: t('settings.clockFormatTwelve') }, - ]} - /> -
-
+ + +
+ auth.setClockFormat(v as ClockFormat)} + options={[ + { value: 'auto', label: t('settings.clockFormatAuto') }, + { value: '24h', label: t('settings.clockFormatTwentyFour') }, + { value: '12h', label: t('settings.clockFormatTwelve') }, + ]} + /> +
+
+
@@ -177,26 +176,27 @@ export function SystemTab() { >
-
- {t('settings.loggingModeDesc')} -
- auth.setLoggingMode(v as LoggingMode)} - options={[ - { value: 'off', label: t('settings.loggingModeOff') }, - { value: 'normal', label: t('settings.loggingModeNormal') }, - { value: 'debug', label: t('settings.loggingModeDebug') }, - ]} - /> - {auth.loggingMode === 'debug' && ( -
- -
- )} + + + auth.setLoggingMode(v as LoggingMode)} + options={[ + { value: 'off', label: t('settings.loggingModeOff') }, + { value: 'normal', label: t('settings.loggingModeNormal') }, + { value: 'debug', label: t('settings.loggingModeDebug') }, + ]} + /> + {auth.loggingMode === 'debug' && ( +
+ +
+ )} +
+
diff --git a/src/components/settings/ThemesTab.tsx b/src/components/settings/ThemesTab.tsx index f3303074..4bea92bb 100644 --- a/src/components/settings/ThemesTab.tsx +++ b/src/components/settings/ThemesTab.tsx @@ -10,6 +10,7 @@ import { InstalledThemes } from './InstalledThemes'; import { ThemeImportSection } from './ThemeImportSection'; import { ThemeStoreSection } from './ThemeStoreSection'; import { SettingsGroup } from './SettingsGroup'; +import { SettingsSubCard, SettingsField } from './SettingsSubCard'; /** * A flat, always-visible section. The Themes tab has a single purpose, so its @@ -94,9 +95,8 @@ export function ThemesTab() { const nightM = theme.timeNightStart.split(':')[1]; const isSystem = theme.schedulerMode === 'system'; return ( -
-
- + +
-
+ {isSystem && ( -
+
{t('settings.themeSchedulerSystemRestartHint')}
)} @@ -153,7 +153,7 @@ export function ThemesTab() {
)}
-
+ ); })()}
diff --git a/src/components/settings/audio/HiResCrossfadeResampleBlock.tsx b/src/components/settings/audio/HiResCrossfadeResampleBlock.tsx index 8e3d42e6..7e508d5c 100644 --- a/src/components/settings/audio/HiResCrossfadeResampleBlock.tsx +++ b/src/components/settings/audio/HiResCrossfadeResampleBlock.tsx @@ -4,6 +4,7 @@ import { sanitizeHiResCrossfadeResampleHz, } from '../../../utils/audio/hiResCrossfadeResample'; import type { TFunction } from 'i18next'; +import { SettingsSubCard, SettingsField } from '../SettingsSubCard'; interface Props { enabled: boolean; @@ -28,12 +29,12 @@ export function HiResCrossfadeResampleBlock({ if (!enabled) return null; return ( -
-
- - {t('settings.hiResCrossfadeResampleTitle')} - -
{t('settings.hiResCrossfadeResampleDesc')}
+ +
{HI_RES_CROSSFADE_RESAMPLE_OPTIONS.map((hz) => (
-
- {t('settings.hiResCrossfadeResampleWarning')} -
-
-
+ + ); } diff --git a/src/components/settings/audio/NormalizationBlock.tsx b/src/components/settings/audio/NormalizationBlock.tsx index d0d3e9e5..744912a2 100644 --- a/src/components/settings/audio/NormalizationBlock.tsx +++ b/src/components/settings/audio/NormalizationBlock.tsx @@ -5,6 +5,7 @@ import { useAuthStore } from '../../../store/authStore'; import { DEFAULT_LOUDNESS_PRE_ANALYSIS_ATTENUATION_DB } from '../../../store/authStoreDefaults'; import { LoudnessLufsButtonGroup } from '../LoudnessLufsButtonGroup'; import { SettingsGroup } from '../SettingsGroup'; +import { SettingsSubCard, SettingsField, SettingsValue, SettingsCallout } from '../SettingsSubCard'; interface Props { preAnalysisEffectiveDb: number; @@ -68,116 +69,102 @@ export function NormalizationBlock({ preAnalysisEffectiveDb, t }: Props) { {auth.normalizationEngine === 'replaygain' && ( -
-
-
- {t('settings.replayGainMode')} -
- - - -
-
- {auth.replayGainMode === 'auto' && ( -
{t('settings.replayGainAutoDesc')}
- )} -
-
-
- {t('settings.replayGainPreGain')} - auth.setReplayGainPreGainDb(Number(e.target.value))} - /> - - {auth.replayGainPreGainDb > 0 ? `+${auth.replayGainPreGainDb}` : auth.replayGainPreGainDb} dB - -
-
{t('settings.replayGainPreGainDesc')}
-
-
-
- {t('settings.replayGainFallback')} - auth.setReplayGainFallbackDb(Number(e.target.value))} - /> - - {auth.replayGainFallbackDb > 0 ? `+${auth.replayGainFallbackDb}` : auth.replayGainFallbackDb} dB - -
-
{t('settings.replayGainFallbackDesc')}
-
-
- )} - {auth.normalizationEngine === 'loudness' && ( -
-
-
- {t('settings.loudnessTargetLufs')} - -
-
{t('settings.loudnessTargetLufsDesc')}
-
-
-
- {t('settings.loudnessPreAnalysisAttenuation')} - auth.setLoudnessPreAnalysisAttenuationDb(Number(e.target.value))} - /> - - {preAnalysisEffectiveDb} dB - + + +
+ +
-
- {t('settings.loudnessPreAnalysisAttenuationDesc')}{' '} - {t('settings.loudnessPreAnalysisAttenuationRef', { - ref: auth.loudnessPreAnalysisAttenuationDb, - eff: preAnalysisEffectiveDb, - tgt: auth.loudnessTargetLufs, - })} -
-
-
{t('settings.loudnessFirstPlayNote')}
-
+ + + auth.setReplayGainPreGainDb(Number(e.target.value))} + /> + + {auth.replayGainPreGainDb > 0 ? `+${auth.replayGainPreGainDb}` : auth.replayGainPreGainDb} dB + + + + auth.setReplayGainFallbackDb(Number(e.target.value))} + /> + + {auth.replayGainFallbackDb > 0 ? `+${auth.replayGainFallbackDb}` : auth.replayGainFallbackDb} dB + + + + )} + {auth.normalizationEngine === 'loudness' && ( + + + + + + {t('settings.loudnessPreAnalysisAttenuationDesc')}{' '} + {t('settings.loudnessPreAnalysisAttenuationRef', { + ref: auth.loudnessPreAnalysisAttenuationDb, + eff: preAnalysisEffectiveDb, + tgt: auth.loudnessTargetLufs, + })} + + } + row + > + auth.setLoudnessPreAnalysisAttenuationDb(Number(e.target.value))} + /> + {preAnalysisEffectiveDb} dB + + + {t('settings.loudnessFirstPlayNote')} + )} ); diff --git a/src/components/settings/audio/PlaybackRateBlock.tsx b/src/components/settings/audio/PlaybackRateBlock.tsx index 20b607e9..242be702 100644 --- a/src/components/settings/audio/PlaybackRateBlock.tsx +++ b/src/components/settings/audio/PlaybackRateBlock.tsx @@ -23,6 +23,7 @@ import { useOrbitStore } from '../../../store/orbitStore'; import { useAuthStore } from '../../../store/authStore'; import { isOrbitPlaybackSyncActive } from '../../../utils/orbit'; import { SettingsToggle } from '../SettingsToggle'; +import { SettingsSubCard } from '../SettingsSubCard'; interface Props { t: TFunction; @@ -118,10 +119,9 @@ export function PlaybackRateControls({ t, showEnable = true }: Props) { /> )} - {(!showEnable || enabled) && ( - <> - {showEnable &&
} - + {(!showEnable || enabled) && (() => { + const body = ( + <>
{!compact && ( {t('settings.playbackRateStrategy')} @@ -281,8 +281,12 @@ export function PlaybackRateControls({ t, showEnable = true }: Props) { {t('settings.playbackRateNeutral')}

)} - - )} + + ); + return compact + ? body + : {body}; + })()}
); } diff --git a/src/components/settings/audio/TrackPreviewsSection.tsx b/src/components/settings/audio/TrackPreviewsSection.tsx index bf836a4c..19f42303 100644 --- a/src/components/settings/audio/TrackPreviewsSection.tsx +++ b/src/components/settings/audio/TrackPreviewsSection.tsx @@ -7,6 +7,7 @@ import type { TrackPreviewLocation } from '../../../store/authStoreTypes'; import SettingsSubSection from '../../SettingsSubSection'; import { SettingsGroup } from '../SettingsGroup'; import { SettingsToggle } from '../SettingsToggle'; +import { SettingsSubCard, SettingsField, SettingsValue } from '../SettingsSubCard'; interface Props { t: TFunction; @@ -37,20 +38,12 @@ export function TrackPreviewsSection({ t }: Props) { /> {auth.trackPreviewsEnabled && ( - <> -
-
-
- {t('settings.trackPreviewLocationsTitle')} -
-
- {t('settings.trackPreviewLocationsDesc')} -
-
+ + +
{TRACK_PREVIEW_LOCATIONS.map((loc: TrackPreviewLocation) => (
@@ -64,58 +57,44 @@ export function TrackPreviewsSection({ t }: Props) {
))}
-
+
-
-
-
- {t('settings.trackPreviewStart')} -
-
- {t('settings.trackPreviewStartDesc')} -
-
- auth.setTrackPreviewStartRatio(parseFloat(e.target.value))} - style={{ flex: 1, minWidth: 80, maxWidth: 240 }} - aria-label={t('settings.trackPreviewStart')} - /> - - {Math.round(auth.trackPreviewStartRatio * 100)}% - -
-
+ + auth.setTrackPreviewStartRatio(parseFloat(e.target.value))} + aria-label={t('settings.trackPreviewStart')} + /> + {Math.round(auth.trackPreviewStartRatio * 100)}% + -
-
-
- {t('settings.trackPreviewDuration')} -
-
- {t('settings.trackPreviewDurationDesc')} -
-
- auth.setTrackPreviewDurationSec(parseInt(e.target.value, 10))} - style={{ flex: 1, minWidth: 80, maxWidth: 240 }} - aria-label={t('settings.trackPreviewDuration')} - /> - - {t('settings.trackPreviewDurationSecs', { n: auth.trackPreviewDurationSec })} - -
-
- + + auth.setTrackPreviewDurationSec(parseInt(e.target.value, 10))} + aria-label={t('settings.trackPreviewDuration')} + /> + + {t('settings.trackPreviewDurationSecs', { n: auth.trackPreviewDurationSec })} + + + )}
diff --git a/src/components/settings/audio/TrackTransitionsBlock.tsx b/src/components/settings/audio/TrackTransitionsBlock.tsx index 1e3425c5..e30ec2b9 100644 --- a/src/components/settings/audio/TrackTransitionsBlock.tsx +++ b/src/components/settings/audio/TrackTransitionsBlock.tsx @@ -13,6 +13,7 @@ import { } from '../../../utils/playback/playbackTransition'; import { SettingsGroup } from '../SettingsGroup'; import { SettingsToggle } from '../SettingsToggle'; +import { SettingsSubCard, SettingsField, SettingsRow, SettingsValue } from '../SettingsSubCard'; interface Props { t: TFunction; @@ -70,8 +71,8 @@ export function TrackTransitionsBlock({ t }: Props) {
{mode === 'crossfade' && ( -
-
+ + auth.setCrossfadeSecs(parseFloat(e.target.value))} id="crossfade-secs-slider" /> - + {t('settings.crossfadeSecs', { n: auth.crossfadeSecs.toFixed(1) })} - -
-
+ + +
)} {mode === 'autodj' && ( -
-
{t('settings.autoDjDesc')}
-
- - {t('settings.autodjOverlapCapTitle')} - -
{t('settings.autodjOverlapCapDesc')}
+ + +
{auth.autodjOverlapCapMode === 'limit' && ( -
+ auth.setAutodjOverlapCapSec(parseInt(e.target.value, 10))} id="autodj-overlap-cap-slider" /> - + {t('settings.autodjOverlapCapSecs', { n: auth.autodjOverlapCapSec })} - -
+ + )} -
+ -
+ )} );