diff --git a/src/components/settings/LyricsTab.tsx b/src/components/settings/LyricsTab.tsx index 7260ca30..28d13c78 100644 --- a/src/components/settings/LyricsTab.tsx +++ b/src/components/settings/LyricsTab.tsx @@ -3,14 +3,24 @@ import { AudioLines, Music2 } from 'lucide-react'; import { useAuthStore } from '../../store/authStore'; import SettingsSubSection from '../SettingsSubSection'; import { SettingsGroup } from './SettingsGroup'; -import { SettingsToggle } from './SettingsToggle'; import { LyricsSourcesCustomizer } from './LyricsSourcesCustomizer'; +import { SettingsSegmented, type SegmentedOption } from './SettingsSegmented'; +import { SettingsSubCard, SettingsField } from './SettingsSubCard'; export function LyricsTab() { const { t } = useTranslation(); const sidebarLyricsStyle = useAuthStore(s => s.sidebarLyricsStyle); const setSidebarLyricsStyle = useAuthStore(s => s.setSidebarLyricsStyle); + const lyricsStyleOptions: SegmentedOption<'classic' | 'apple'>[] = [ + { id: 'classic', label: t('settings.sidebarLyricsStyleClassic') }, + { id: 'apple', label: t('settings.sidebarLyricsStyleApple') }, + ]; + const lyricsStyleDescKey = + sidebarLyricsStyle === 'classic' + ? 'settings.sidebarLyricsStyleClassicDesc' + : 'settings.sidebarLyricsStyleAppleDesc'; + return ( <> } > - {(['classic', 'apple'] as const).map((style, i) => { - const key = style === 'classic' ? 'Classic' : 'Apple'; - const other = style === 'classic' ? 'apple' : 'classic'; - return ( -
- {i > 0 &&
} - setSidebarLyricsStyle(c ? style : other)} - /> -
- ); - })} + + + + diff --git a/src/components/settings/PersonalisationTab.tsx b/src/components/settings/PersonalisationTab.tsx index 76750f54..28971487 100644 --- a/src/components/settings/PersonalisationTab.tsx +++ b/src/components/settings/PersonalisationTab.tsx @@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next'; import { Disc3, LayoutGrid, ListOrdered, ListTodo, PanelLeft, RotateCcw, Users } from 'lucide-react'; import { useArtistLayoutStore } from '../../store/artistLayoutStore'; import { useAuthStore } from '../../store/authStore'; +import type { QueueDisplayMode } from '../../store/authStoreTypes'; import { useHomeStore } from '../../store/homeStore'; import { usePlayerBarLayoutStore } from '../../store/playerBarLayoutStore'; import { usePlaylistLayoutStore } from '../../store/playlistLayoutStore'; @@ -10,6 +11,8 @@ import { useSidebarStore } from '../../store/sidebarStore'; import SettingsSubSection from '../SettingsSubSection'; import { SettingsGroup } from './SettingsGroup'; import { SettingsToggle } from './SettingsToggle'; +import { SettingsSegmented, type SegmentedOption } from './SettingsSegmented'; +import { SettingsSubCard, SettingsField } from './SettingsSubCard'; import { ArtistLayoutCustomizer } from './ArtistLayoutCustomizer'; import { HomeCustomizer } from './HomeCustomizer'; import { PlayerBarLayoutCustomizer } from './PlayerBarLayoutCustomizer'; @@ -24,6 +27,19 @@ export function PersonalisationTab() { const preservePlayNextOrder = useAuthStore(s => s.preservePlayNextOrder); const setPreservePlayNextOrder = useAuthStore(s => s.setPreservePlayNextOrder); const advancedSettingsEnabled = useAuthStore(s => s.advancedSettingsEnabled); + + const queueModeOptions: SegmentedOption[] = [ + { id: 'queue', label: t('queue.title') }, + { id: 'playlist', label: t('queue.modePlaylist') }, + { id: 'timeline', label: t('queue.modeTimeline') }, + ]; + const queueModeDescKey = + queueDisplayMode === 'queue' + ? 'settings.queueModeQueueSub' + : queueDisplayMode === 'playlist' + ? 'settings.queueModePlaylistSub' + : 'settings.queueModeTimelineSub'; + return ( <> } > <> + {/* Three mutually exclusive modes — a segmented picker enforces that + exactly one is active, instead of toggles that read as independent. */} - {/* Three mutually exclusive modes — exactly one is always active, so - turning one on turns the others off; the active one cannot be - switched off directly (ignore the uncheck). */} - { if (c) setQueueDisplayMode('queue'); }} - /> -
- { if (c) setQueueDisplayMode('playlist'); }} - /> -
- { if (c) setQueueDisplayMode('timeline'); }} + + + + diff --git a/src/components/settings/SettingsSegmented.tsx b/src/components/settings/SettingsSegmented.tsx new file mode 100644 index 00000000..8b443a2e --- /dev/null +++ b/src/components/settings/SettingsSegmented.tsx @@ -0,0 +1,55 @@ +import type { CSSProperties } from 'react'; + +export interface SegmentedOption { + id: T; + label: string; + /** Disables this single option while leaving the rest selectable. */ + disabled?: boolean; +} + +interface Props { + options: SegmentedOption[]; + value: T; + onChange: (id: T) => void; + /** Disables the whole control (e.g. an Orbit guest mirroring the host). */ + disabled?: boolean; + /** Extra class appended to the `settings-segmented` wrapper. */ + className?: string; + /** Inline style on the wrapper (e.g. the dimmed host-controlled state). */ + style?: CSSProperties; +} + +/** + * Shared `settings-segmented` picker: a row of mutually-exclusive pill buttons + * where exactly one is active (`btn-primary`) and the rest are `btn-ghost`. The + * canonical replacement for stacks of mutually-exclusive toggles, which falsely + * read as "you can turn several on" — see the Track-transitions section for the + * reference look. + * + * Scope is the segmented control only; callers render any per-option detail + * (sliders, descriptions, sub-cards) below it themselves. + */ +export function SettingsSegmented({ + options, + value, + onChange, + disabled, + className, + style, +}: Props) { + return ( +
+ {options.map(opt => ( + + ))} +
+ ); +} diff --git a/src/components/settings/audio/TrackTransitionsBlock.tsx b/src/components/settings/audio/TrackTransitionsBlock.tsx index e30ec2b9..ba37da23 100644 --- a/src/components/settings/audio/TrackTransitionsBlock.tsx +++ b/src/components/settings/audio/TrackTransitionsBlock.tsx @@ -14,6 +14,7 @@ import { import { SettingsGroup } from '../SettingsGroup'; import { SettingsToggle } from '../SettingsToggle'; import { SettingsSubCard, SettingsField, SettingsRow, SettingsValue } from '../SettingsSubCard'; +import { SettingsSegmented, type SegmentedOption } from '../SettingsSegmented'; interface Props { t: TFunction; @@ -42,13 +43,18 @@ export function TrackTransitionsBlock({ t }: Props) { s => s.role === 'guest' && (s.phase === 'active' || s.phase === 'joining'), ); - const transitions: { id: TransitionMode; label: string }[] = [ + const transitions: SegmentedOption[] = [ { id: 'none', label: t('settings.transitionOff') }, { id: 'gapless', label: t('settings.gapless') }, { id: 'crossfade', label: t('settings.crossfade') }, { id: 'autodj', label: t('settings.autoDj') }, ]; + const overlapCapOptions: SegmentedOption<'auto' | 'limit'>[] = [ + { id: 'auto', label: t('settings.autodjOverlapCapAuto') }, + { id: 'limit', label: t('settings.autodjOverlapCapLimit') }, + ]; + return ( {hostControlled && ( @@ -56,19 +62,13 @@ export function TrackTransitionsBlock({ t }: Props) { {t('settings.transitionsHostControlled')}
)} -
- {transitions.map(item => ( - - ))} -
+ {mode === 'crossfade' && ( @@ -96,24 +96,12 @@ export function TrackTransitionsBlock({ t }: Props) { label={t('settings.autodjOverlapCapTitle')} desc={t('settings.autodjOverlapCapDesc')} > -
- - -
+ {auth.autodjOverlapCapMode === 'limit' && (