From 76eab9e70326fa075947fd8cc8a0724a3a916f5d Mon Sep 17 00:00:00 2001 From: Jonathan Date: Sun, 12 Jul 2026 15:33:20 +0200 Subject: [PATCH] feat(web): Settings tabs per service (Qobuz/Soulseek/Library/Discovery) Split the long Settings page into tabs so each service stands alone, instead of one long mixed scroll. Same handlers/aria-labels; credential save still PUTs the full config. Co-Authored-By: Claude Opus 4.8 (1M context) --- web/src/app/settings/settings-form.tsx | 233 ++++++++++++------------- 1 file changed, 114 insertions(+), 119 deletions(-) diff --git a/web/src/app/settings/settings-form.tsx b/web/src/app/settings/settings-form.tsx index 56c4521..422ece2 100644 --- a/web/src/app/settings/settings-form.tsx +++ b/web/src/app/settings/settings-form.tsx @@ -2,7 +2,6 @@ import { useEffect, useState } from "react"; import { PageHead } from "../_ui/page-head"; -import { SectionHeader } from "../_ui/section-header"; type Loaded = { qobuzEmail: string; @@ -13,6 +12,9 @@ type Loaded = { slskdApiKeySet: boolean; }; +type Tab = "Qobuz" | "Soulseek" | "Library" | "Discovery"; +const TABS: Tab[] = ["Qobuz", "Soulseek", "Library", "Discovery"]; + const DISCOVERY_FIELDS: [string, string][] = [ ["discover.intervalHours", "Interval (hours)"], ["discover.maxSeeds", "Max seeds per sweep"], @@ -22,6 +24,7 @@ const DISCOVERY_FIELDS: [string, string][] = [ ]; export function SettingsForm() { + const [tab, setTab] = useState("Qobuz"); const [qobuzEmail, setQobuzEmail] = useState(""); const [qobuzPassword, setQobuzPassword] = useState(""); const [qobuzUserId, setQobuzUserId] = useState(""); @@ -56,7 +59,7 @@ export function SettingsForm() { const id = setInterval(async () => { const s = await (await fetch("/api/scan")).json(); setScanResult(s.result); - if (!s.requested) setScanRequested(false); // worker finished the scan + if (!s.requested) setScanRequested(false); }, 2000); return () => clearInterval(id); }, [scanRequested]); @@ -80,7 +83,7 @@ export function SettingsForm() { .then((c: Record) => setDiscoverCfg(c)); }, []); - async function submit(e: React.FormEvent) { + async function saveCredentials(e: React.FormEvent) { e.preventDefault(); await fetch("/api/config", { method: "PUT", @@ -91,6 +94,7 @@ export function SettingsForm() { setQobuzToken(""); setSlskdApiKey(""); setSaved(true); + setTimeout(() => setSaved(false), 1500); setPwSet(pwSet || qobuzPassword !== ""); setTokenSet(tokenSet || qobuzToken !== ""); setKeySet(keySet || slskdApiKey !== ""); @@ -117,125 +121,116 @@ export function SettingsForm() {
- {/* ── Credentials ─────────────────────────── */} -
- - -
Qobuz — email / password
-
- - -
- -
Qobuz — auth token (more reliable; overrides email/password)
-
- - -
- -
Soulseek (slskd)
-
- - -
- -
- - {saved ? Saved : null} -
- + ))} +
- {/* ── Library ─────────────────────────────── */} -
- -
-
- Music path   /music -
-
- Mounted read/write into the worker. To point Lyra at a different folder, set{" "} - MUSIC_DIR in docker-compose.yml and rebuild — it can’t be remounted from here. -
-
-

Scan the library to record what’s already on disk as owned + monitored.

-
- - {scanResult ? ( - - Last scan: {scanResult} - - ) : null} -
-
- - {/* ── Discovery ───────────────────────────── */} -
- -
- -
-
- {DISCOVERY_FIELDS.map(([key, label]) => ( -
+
Auth token (more reliable; overrides email/password)
+
+ + +
+
+ + {saved ? Saved : null} +
+ + ) : null} + + {tab === "Soulseek" ? ( +
+
Soulseek (slskd)
+
+ + +
+
+ + {saved ? Saved : null} +
+
+ ) : null} + + {tab === "Library" ? ( +
+
+
+ Music path   /music +
+
+ Mounted read/write into the worker. To point Lyra at a different folder, set MUSIC_DIR in{" "} + docker-compose.yml and rebuild — it can’t be remounted from here. +
+
+

Scan the library to record what’s already on disk as owned + monitored.

+
+ + {scanResult ? ( + + Last scan: {scanResult} + + ) : null} +
+
+ ) : null} + + {tab === "Discovery" ? ( +
+
+ +
+
+ {DISCOVERY_FIELDS.map(([key, label]) => ( + + ))} +
+
+ + {discoverSaved ? Saved : null} +
+
+ ) : null} ); }