mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 14:55:43 +00:00
refactor(settings): collapse all sections, drop font dropdown, surface OpenDyslexic (#508)
* refactor(settings): collapse all sections, drop font dropdown, surface OpenDyslexic Settings opened on a tab where four or five sub-sections were expanded on first render — audio device, theme list, lyrics sources, sidebar customizer, random-mix copy, offline dir, language picker, keybindings table. The page felt like a wall of controls before the user had even looked for something specific. Removed every `defaultOpen` flag from the SettingsSubSection call sites so each tab now boots with only the section headers visible. Component default was already `false`. ThemePicker auto-expanded the group containing the active theme on mount. Same noise on a screen that already has the longest accordion list in the app, and the blue dot in the group header already tells the user which group holds the active theme. Initial open-group is now `null` — all groups collapsed until the user clicks one. Font picker had a dropdown-style button that toggled a list inside the sub-section, which meant two clicks (open the section, then open the dropdown) for what should be a one-click choice. Removed the button + the `fontPickerOpen` state — opening the Font sub-section now reveals the full list directly and a click sets the font without collapsing anything. OpenDyslexic moved to the top of the list so users with dyslexia don't scroll past 14 sans-serifs to find their option; the rest stays in the original order. * docs: changelog entry for PR #508 Logs the Settings collapse-by-default + font picker cleanup + OpenDyslexic ordering in v1.46.0 "## Changed".
This commit is contained in:
committed by
GitHub
parent
f520f7951a
commit
57fe847d71
@@ -105,6 +105,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## Changed
|
||||
|
||||
### Settings — collapse-by-default cleanup, font picker without dropdown, OpenDyslexic at top
|
||||
|
||||
**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#508](https://github.com/Psychotoxical/psysonic/pull/508)**
|
||||
|
||||
* Every Settings sub-section now boots **collapsed**. Audio Device, Lyrics Sources, Last.fm, Sidebar, Random Mix, Offline Dir, Theme, Keybindings and Language used to auto-expand on first render — each tab felt like a wall of controls before the user had even looked for something specific. Click the section header to open what you actually need.
|
||||
* **ThemePicker** no longer auto-expands the group containing the active theme. The blue dot in the group header already surfaces which group holds it.
|
||||
* **Font picker** lost its inner dropdown button. Opening the Font sub-section now reveals the full font list directly; one click sets the font.
|
||||
* **OpenDyslexic** moves to the top of the font list so dyslexic readers don't scroll past 14 sans-serifs to find their option.
|
||||
|
||||
### Dependencies — npm / Cargo refresh and rodio 0.22
|
||||
|
||||
**By [@cucadmuh](https://github.com/cucadmuh), PR [#463](https://github.com/Psychotoxical/psysonic/pull/463)**
|
||||
|
||||
@@ -180,8 +180,10 @@ interface Props {
|
||||
}
|
||||
|
||||
export default function ThemePicker({ value, onChange }: Props) {
|
||||
const initialOpen = THEME_GROUPS.find(g => g.themes.some(t => t.id === value))?.group ?? THEME_GROUPS[0].group;
|
||||
const [openGroup, setOpenGroup] = useState<string | null>(initialOpen);
|
||||
// All groups collapsed on first render. The blue dot in the header surfaces
|
||||
// which group holds the active theme, so auto-expanding it on mount just
|
||||
// adds visual noise to a screen that already has a long sub-section list.
|
||||
const [openGroup, setOpenGroup] = useState<string | null>(null);
|
||||
|
||||
const toggle = (group: string) => setOpenGroup(prev => prev === group ? null : group);
|
||||
|
||||
|
||||
+41
-77
@@ -1677,7 +1677,6 @@ export default function Settings() {
|
||||
const [devicesLoading, setDevicesLoading] = useState(false);
|
||||
const [showClearConfirm, setShowClearConfirm] = useState(false);
|
||||
const [clearing, setClearing] = useState(false);
|
||||
const [fontPickerOpen, setFontPickerOpen] = useState(false);
|
||||
const [ndAdminAuth, setNdAdminAuth] = useState<{ token: string; serverUrl: string; username: string } | null>(null);
|
||||
const [ndAuthChecked, setNdAuthChecked] = useState(false);
|
||||
const addServerInviteAnchorRef = useRef<HTMLDivElement>(null);
|
||||
@@ -2269,7 +2268,6 @@ export default function Settings() {
|
||||
<SettingsSubSection
|
||||
title={t('settings.audioOutputDevice')}
|
||||
icon={<AudioLines size={16} />}
|
||||
defaultOpen
|
||||
>
|
||||
<div className="settings-card">
|
||||
{IS_MACOS ? (
|
||||
@@ -2698,7 +2696,6 @@ export default function Settings() {
|
||||
<SettingsSubSection
|
||||
title={t('settings.lyricsSourcesTitle')}
|
||||
icon={<Music2 size={16} />}
|
||||
defaultOpen
|
||||
>
|
||||
<LyricsSourcesCustomizer />
|
||||
</SettingsSubSection>
|
||||
@@ -2759,7 +2756,6 @@ export default function Settings() {
|
||||
<SettingsSubSection
|
||||
title={t('settings.lfmTitle')}
|
||||
icon={<LastfmIcon size={16} />}
|
||||
defaultOpen
|
||||
>
|
||||
<div className="settings-card">
|
||||
{auth.lastfmSessionKey ? (
|
||||
@@ -2958,7 +2954,6 @@ export default function Settings() {
|
||||
<SettingsSubSection
|
||||
title={t('settings.sidebarTitle')}
|
||||
icon={<PanelLeft size={16} />}
|
||||
defaultOpen
|
||||
action={
|
||||
<button
|
||||
type="button"
|
||||
@@ -3022,7 +3017,6 @@ export default function Settings() {
|
||||
<SettingsSubSection
|
||||
title={t('settings.randomMixTitle')}
|
||||
icon={<Shuffle size={16} />}
|
||||
defaultOpen
|
||||
>
|
||||
<div className="settings-card">
|
||||
<p style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: '1rem', lineHeight: 1.5 }}>
|
||||
@@ -3235,7 +3229,6 @@ export default function Settings() {
|
||||
<SettingsSubSection
|
||||
title={t('settings.offlineDirTitle')}
|
||||
icon={<Download size={16} />}
|
||||
defaultOpen
|
||||
>
|
||||
<div className="settings-card">
|
||||
<div style={{ fontSize: 12, color: 'var(--text-muted)', marginBottom: 14, lineHeight: 1.5 }}>
|
||||
@@ -3561,7 +3554,6 @@ export default function Settings() {
|
||||
<SettingsSubSection
|
||||
title={t('settings.theme')}
|
||||
icon={<Palette size={16} />}
|
||||
defaultOpen
|
||||
>
|
||||
<div className="settings-card">
|
||||
{theme.enableThemeScheduler && (
|
||||
@@ -3812,73 +3804,47 @@ export default function Settings() {
|
||||
icon={<Type size={16} />}
|
||||
>
|
||||
<div className="settings-card">
|
||||
<button
|
||||
className="btn btn-ghost"
|
||||
style={{ justifyContent: 'space-between', width: '100%', fontFamily: 'var(--font-sans)' }}
|
||||
onClick={() => setFontPickerOpen(o => !o)}
|
||||
>
|
||||
<span>{
|
||||
([
|
||||
{ id: 'inter', label: 'Inter' },
|
||||
{ id: 'outfit', label: 'Outfit' },
|
||||
{ id: 'dm-sans', label: 'DM Sans' },
|
||||
{ id: 'nunito', label: 'Nunito' },
|
||||
{ id: 'rubik', label: 'Rubik' },
|
||||
{ id: 'space-grotesk', label: 'Space Grotesk' },
|
||||
{ id: 'figtree', label: 'Figtree' },
|
||||
{ id: 'manrope', label: 'Manrope' },
|
||||
{ id: 'plus-jakarta-sans', label: 'Plus Jakarta Sans' },
|
||||
{ id: 'lexend', label: 'Lexend' },
|
||||
{ id: 'geist', label: 'Geist' },
|
||||
{ id: 'jetbrains-mono', label: 'JetBrains Mono' },
|
||||
{ id: 'golos-text', label: 'Golos Text' },
|
||||
{ id: 'unbounded', label: 'Unbounded' },
|
||||
{ id: 'opendyslexic', label: 'OpenDyslexic' },
|
||||
] as { id: FontId; label: string }[]).find(f => f.id === fontStore.font)?.label ?? fontStore.font
|
||||
}</span>
|
||||
<ChevronDown size={14} style={{ color: 'var(--text-muted)', transform: fontPickerOpen ? 'rotate(180deg)' : 'none', transition: 'transform 0.2s' }} />
|
||||
</button>
|
||||
{fontPickerOpen && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginTop: '8px' }}>
|
||||
{(
|
||||
[
|
||||
{ id: 'inter', label: 'Inter', stack: "'Inter Variable', sans-serif" },
|
||||
{ id: 'outfit', label: 'Outfit', stack: "'Outfit Variable', sans-serif" },
|
||||
{ id: 'dm-sans', label: 'DM Sans', stack: "'DM Sans Variable', sans-serif" },
|
||||
{ id: 'nunito', label: 'Nunito', stack: "'Nunito Variable', sans-serif" },
|
||||
{ id: 'rubik', label: 'Rubik', stack: "'Rubik Variable', sans-serif" },
|
||||
{ id: 'space-grotesk', label: 'Space Grotesk', stack: "'Space Grotesk Variable', sans-serif" },
|
||||
{ id: 'figtree', label: 'Figtree', stack: "'Figtree Variable', sans-serif" },
|
||||
{ id: 'manrope', label: 'Manrope', stack: "'Manrope Variable', sans-serif" },
|
||||
{ id: 'plus-jakarta-sans', label: 'Plus Jakarta Sans', stack: "'Plus Jakarta Sans Variable', sans-serif" },
|
||||
{ id: 'lexend', label: 'Lexend', stack: "'Lexend Variable', sans-serif" },
|
||||
{ id: 'geist', label: 'Geist', stack: "'Geist Variable', sans-serif" },
|
||||
{ id: 'jetbrains-mono', label: 'JetBrains Mono', stack: "'JetBrains Mono Variable', monospace" },
|
||||
{ id: 'golos-text', label: 'Golos Text', stack: "'Golos Text Variable', sans-serif" },
|
||||
{ id: 'unbounded', label: 'Unbounded', stack: "'Unbounded Variable', sans-serif" },
|
||||
{ id: 'opendyslexic', label: 'OpenDyslexic', stack: "'OpenDyslexic', sans-serif", hint: t('settings.fontHintOpenDyslexic') },
|
||||
] as { id: FontId; label: string; stack: string; hint?: string }[]
|
||||
).map(f => (
|
||||
<button
|
||||
key={f.id}
|
||||
className={`btn ${fontStore.font === f.id ? 'btn-primary' : 'btn-ghost'}`}
|
||||
style={{
|
||||
justifyContent: 'flex-start',
|
||||
fontFamily: f.stack,
|
||||
...(f.hint ? { flexDirection: 'column', alignItems: 'flex-start', gap: '2px', paddingTop: '8px', paddingBottom: '8px' } : null),
|
||||
}}
|
||||
onClick={() => { fontStore.setFont(f.id); setFontPickerOpen(false); }}
|
||||
>
|
||||
<span>{f.label}</span>
|
||||
{f.hint && (
|
||||
<span style={{ fontSize: 11, color: 'var(--text-muted)', fontFamily: 'var(--font-sans)' }}>
|
||||
{f.hint}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{(
|
||||
[
|
||||
// Accessibility-first: OpenDyslexic at the top so dyslexic
|
||||
// readers don't have to scroll past 14 sans-serifs to find it.
|
||||
{ id: 'opendyslexic', label: 'OpenDyslexic', stack: "'OpenDyslexic', sans-serif", hint: t('settings.fontHintOpenDyslexic') },
|
||||
{ id: 'inter', label: 'Inter', stack: "'Inter Variable', sans-serif" },
|
||||
{ id: 'outfit', label: 'Outfit', stack: "'Outfit Variable', sans-serif" },
|
||||
{ id: 'dm-sans', label: 'DM Sans', stack: "'DM Sans Variable', sans-serif" },
|
||||
{ id: 'nunito', label: 'Nunito', stack: "'Nunito Variable', sans-serif" },
|
||||
{ id: 'rubik', label: 'Rubik', stack: "'Rubik Variable', sans-serif" },
|
||||
{ id: 'space-grotesk', label: 'Space Grotesk', stack: "'Space Grotesk Variable', sans-serif" },
|
||||
{ id: 'figtree', label: 'Figtree', stack: "'Figtree Variable', sans-serif" },
|
||||
{ id: 'manrope', label: 'Manrope', stack: "'Manrope Variable', sans-serif" },
|
||||
{ id: 'plus-jakarta-sans', label: 'Plus Jakarta Sans', stack: "'Plus Jakarta Sans Variable', sans-serif" },
|
||||
{ id: 'lexend', label: 'Lexend', stack: "'Lexend Variable', sans-serif" },
|
||||
{ id: 'geist', label: 'Geist', stack: "'Geist Variable', sans-serif" },
|
||||
{ id: 'jetbrains-mono', label: 'JetBrains Mono', stack: "'JetBrains Mono Variable', monospace" },
|
||||
{ id: 'golos-text', label: 'Golos Text', stack: "'Golos Text Variable', sans-serif" },
|
||||
{ id: 'unbounded', label: 'Unbounded', stack: "'Unbounded Variable', sans-serif" },
|
||||
] as { id: FontId; label: string; stack: string; hint?: string }[]
|
||||
).map(f => (
|
||||
<button
|
||||
key={f.id}
|
||||
className={`btn ${fontStore.font === f.id ? 'btn-primary' : 'btn-ghost'}`}
|
||||
style={{
|
||||
justifyContent: 'flex-start',
|
||||
fontFamily: f.stack,
|
||||
...(f.hint ? { flexDirection: 'column', alignItems: 'flex-start', gap: '2px', paddingTop: '8px', paddingBottom: '8px' } : null),
|
||||
}}
|
||||
onClick={() => fontStore.setFont(f.id)}
|
||||
>
|
||||
<span>{f.label}</span>
|
||||
{f.hint && (
|
||||
<span style={{ fontSize: 11, color: 'var(--text-muted)', fontFamily: 'var(--font-sans)' }}>
|
||||
{f.hint}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsSubSection>
|
||||
|
||||
@@ -3948,7 +3914,6 @@ export default function Settings() {
|
||||
<SettingsSubSection
|
||||
title={t('settings.inputKeybindingsTitle')}
|
||||
icon={<Keyboard size={16} />}
|
||||
defaultOpen
|
||||
action={
|
||||
<button
|
||||
type="button"
|
||||
@@ -4320,7 +4285,6 @@ export default function Settings() {
|
||||
<SettingsSubSection
|
||||
title={t('settings.language')}
|
||||
icon={<Globe size={16} />}
|
||||
defaultOpen
|
||||
>
|
||||
<div className="settings-card">
|
||||
<div className="form-group" style={{ maxWidth: '300px' }}>
|
||||
|
||||
Reference in New Issue
Block a user