diff --git a/src/renderer/src/components/SettingsView.tsx b/src/renderer/src/components/SettingsView.tsx index 630a534..69d93b4 100644 --- a/src/renderer/src/components/SettingsView.tsx +++ b/src/renderer/src/components/SettingsView.tsx @@ -1,7 +1,9 @@ import { useEffect, useRef, useState } from 'react' import { Input, + Button, Caption1, + Subtitle2, Skeleton, SkeletonItem, makeStyles, @@ -29,26 +31,56 @@ const useStyles = makeStyles({ display: 'flex', flexDirection: 'column', gap: SPACE.section + }, + // The section jump nav (UX7): a row of anchor buttons under the search box. + sectionNav: { + display: 'flex', + gap: SPACE.xtight, + flexWrap: 'wrap' + }, + sectionHeading: { + color: tokens.colorNeutralForeground3, + paddingTop: SPACE.tight + }, + section: { + display: 'flex', + flexDirection: 'column', + gap: SPACE.section, + // Anchor scrolling lands the heading just below the screen padding. + scrollMarginTop: SPACE.section } }) -// The cards, in display order. Rendered inside React-owned wrappers so the search -// filter controls each card's visibility via state instead of mutating the card's -// own DOM `style` (M14) — which broke if a card ever set its own inline style. -const CARDS = [ - DownloadsCard, - AppearanceCard, - PostProcessingCard, - NetworkCard, - CookiesCard, - CustomCommandsCard, - FilenamesCard, - BackupCard, - DiagnosticsCard, - SoftwareUpdateCard, - AboutCard +type CardComponent = () => React.JSX.Element + +/** + * The cards, grouped into stable sections (UX7) with a jump nav — Settings used + * to be one unsegmented ~11-card scroll. Related cards are co-located: the + * Downloads section holds the format defaults AND the post-processing card so + * the two halves of "format" sit together (UX8). Cards render inside + * React-owned wrappers so the search filter controls each card's visibility via + * state instead of mutating the card's own DOM `style` (M14). + */ +const SECTIONS: { id: string; title: string; cards: CardComponent[] }[] = [ + { id: 'downloads', title: 'Downloads', cards: [DownloadsCard, PostProcessingCard, FilenamesCard] }, + { id: 'appearance', title: 'Appearance', cards: [AppearanceCard] }, + { id: 'network', title: 'Network & accounts', cards: [NetworkCard, CookiesCard] }, + { id: 'advanced', title: 'Advanced', cards: [CustomCommandsCard] }, + { + id: 'app', + title: 'App & maintenance', + cards: [BackupCard, DiagnosticsCard, SoftwareUpdateCard, AboutCard] + } ] +// Flat list in render order — the search filter indexes cards globally. +const CARDS: CardComponent[] = SECTIONS.flatMap((s) => s.cards) +// Each section's [start, end) range into CARDS. +const SECTION_RANGES = SECTIONS.map((s, si) => { + const start = SECTIONS.slice(0, si).reduce((n, x) => n + x.cards.length, 0) + return { start, end: start + s.cards.length } +}) + export function SettingsView(): React.JSX.Element { const styles = useStyles() const screen = useScreenStyles() @@ -62,6 +94,7 @@ export function SettingsView(): React.JSX.Element { // visibility, rather than us writing `display:none` onto React's own card nodes (M14). const [search, setSearch] = useState('') const cardRefs = useRef<(HTMLDivElement | null)[]>([]) + const sectionRefs = useRef<(HTMLDivElement | null)[]>([]) const [hidden, setHidden] = useState(() => CARDS.map(() => false)) useEffect(() => { @@ -76,6 +109,11 @@ export function SettingsView(): React.JSX.Element { }, [search]) const noResults = search.trim() !== '' && hidden.length > 0 && hidden.every(Boolean) + const searching = search.trim() !== '' + + function jumpTo(si: number): void { + sectionRefs.current[si]?.scrollIntoView({ behavior: 'smooth', block: 'start' }) + } return (
@@ -100,6 +138,17 @@ export function SettingsView(): React.JSX.Element { )}
+ {/* Section jump nav (UX7). Hidden while searching — the filter is the finder then. */} + {loaded && !searching && ( + + )} + {!loaded ? ( // Boot skeleton (UX26): three placeholder cards while persisted settings // load, instead of a flash of fallback values. @@ -109,18 +158,41 @@ export function SettingsView(): React.JSX.Element { ))} ) : ( - CARDS.map((Card, i) => ( -
{ - cardRefs.current[i] = el - }} - style={hidden[i] ? { display: 'none' } : undefined} - > - -
- )) + SECTIONS.map((section, si) => { + const range = SECTION_RANGES[si]! + // A section disappears with all of its cards during a search. + const sectionHidden = section.cards.every((_, ci) => hidden[range.start + ci]) + return ( +
{ + sectionRefs.current[si] = el + }} + className={styles.section} + style={sectionHidden ? { display: 'none' } : undefined} + > + + {section.title} + + {section.cards.map((Card, ci) => { + const i = range.start + ci + return ( +
{ + cardRefs.current[i] = el + }} + style={hidden[i] ? { display: 'none' } : undefined} + > + +
+ ) + })} +
+ ) + }) )} diff --git a/src/renderer/src/components/settings/AboutCard.tsx b/src/renderer/src/components/settings/AboutCard.tsx index 553cba1..b795d67 100644 --- a/src/renderer/src/components/settings/AboutCard.tsx +++ b/src/renderer/src/components/settings/AboutCard.tsx @@ -47,7 +47,7 @@ export function AboutCard(): React.JSX.Element {
- About + About
AeroFetch is a generic frontend for yt-dlp. It bundles ffmpeg and manages its own copy of diff --git a/src/renderer/src/components/settings/AppearanceCard.tsx b/src/renderer/src/components/settings/AppearanceCard.tsx index 783728e..e220d06 100644 --- a/src/renderer/src/components/settings/AppearanceCard.tsx +++ b/src/renderer/src/components/settings/AppearanceCard.tsx @@ -48,7 +48,7 @@ export function AppearanceCard(): React.JSX.Element {
- Appearance + Appearance
diff --git a/src/renderer/src/components/settings/BackupCard.tsx b/src/renderer/src/components/settings/BackupCard.tsx index 9f45e41..e309a79 100644 --- a/src/renderer/src/components/settings/BackupCard.tsx +++ b/src/renderer/src/components/settings/BackupCard.tsx @@ -16,7 +16,7 @@ export function BackupCard(): React.JSX.Element {
- Backup & restore + Backup & restore
Save your settings and custom-command templates to a JSON file, or restore them on another diff --git a/src/renderer/src/components/settings/CookiesCard.tsx b/src/renderer/src/components/settings/CookiesCard.tsx index fa4daec..54e2590 100644 --- a/src/renderer/src/components/settings/CookiesCard.tsx +++ b/src/renderer/src/components/settings/CookiesCard.tsx @@ -33,7 +33,7 @@ export function CookiesCard(): React.JSX.Element {
- Cookies + Cookies
Some sites only serve full quality, age-restricted, or members-only video to a logged-in diff --git a/src/renderer/src/components/settings/CustomCommandsCard.tsx b/src/renderer/src/components/settings/CustomCommandsCard.tsx index e66b89d..7989036 100644 --- a/src/renderer/src/components/settings/CustomCommandsCard.tsx +++ b/src/renderer/src/components/settings/CustomCommandsCard.tsx @@ -17,7 +17,7 @@ export function CustomCommandsCard(): React.JSX.Element {
- Custom commands + Custom commands
Named templates of extra yt-dlp flags -- your own power-user recipes (e.g. diff --git a/src/renderer/src/components/settings/DiagnosticsCard.tsx b/src/renderer/src/components/settings/DiagnosticsCard.tsx index 20f51ae..32eef82 100644 --- a/src/renderer/src/components/settings/DiagnosticsCard.tsx +++ b/src/renderer/src/components/settings/DiagnosticsCard.tsx @@ -29,7 +29,7 @@ export function DiagnosticsCard(): React.JSX.Element {
- Diagnostics + Diagnostics
Failed downloads are logged here even after you clear the queue, so you can copy the details diff --git a/src/renderer/src/components/settings/DownloadsCard.tsx b/src/renderer/src/components/settings/DownloadsCard.tsx index 910dfda..b248dec 100644 --- a/src/renderer/src/components/settings/DownloadsCard.tsx +++ b/src/renderer/src/components/settings/DownloadsCard.tsx @@ -48,7 +48,7 @@ export function DownloadsCard(): React.JSX.Element {
- Downloads + Downloads
- Filenames + Filenames
- Network + Network
- Format & post-processing + Post-processing
+ {/* UX8: this card is the advanced half — the default format itself (video/ + audio + quality) lives in the Downloads card, co-located just above. */} - Defaults applied to every new download (yt-dlp and ffmpeg do the work). + Advanced conversion applied to every new download — container, codec, subtitles, + SponsorBlock (yt-dlp and ffmpeg do the work). The default format itself is set in the + Downloads card above.
- Software update + Software update
{appVersion