diff --git a/web/src/app/_ui/help-button.tsx b/web/src/app/_ui/help-button.tsx new file mode 100644 index 0000000..8b585a0 --- /dev/null +++ b/web/src/app/_ui/help-button.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { useState, type ReactNode } from "react"; +import { Modal } from "./modal"; + +/** A small "?" affordance that opens its help content in a popup, keeping settings pages + * uncluttered. */ +export function HelpButton({ title, children }: { title: string; children: ReactNode }) { + const [open, setOpen] = useState(false); + return ( + <> + + setOpen(false)} title={title}> +
{children}
+
+ + ); +} diff --git a/web/src/app/design.css b/web/src/app/design.css index b53638c..a35fbf8 100644 --- a/web/src/app/design.css +++ b/web/src/app/design.css @@ -259,7 +259,7 @@ nav.contents .sep { flex: 1; } .field.wide { grid-column: 1 / -1; } .subhead { font-family: var(--mono); font-size: 0.66rem; letter-spacing: 0.16em; text-transform: uppercase; - color: var(--graphite); margin: 26px 0 2px; + color: var(--graphite); margin: 26px 0 2px; display: flex; align-items: center; gap: 10px; } .field .set { color: var(--accent); } .info-panel { @@ -276,6 +276,16 @@ nav.contents .sep { flex: 1; } .help { font-size: 0.8rem; color: var(--graphite); line-height: 1.5; margin: 4px 0 14px; max-width: 62ch; } .help b { color: var(--ink); font-weight: 600; } .help code { font-family: var(--mono); font-size: 0.85em; } +/* compact "?" trigger that opens help in a popup */ +.help-btn { + font-family: var(--mono); font-size: 0.72rem; line-height: 1; width: 20px; height: 20px; + border: 1.5px solid var(--rule-2); border-radius: 50%; background: transparent; color: var(--graphite); + cursor: pointer; display: inline-flex; align-items: center; justify-content: center; padding: 0; +} +.help-btn:hover { border-color: var(--accent); color: var(--accent); } +.help-body { font-size: 0.9rem; color: var(--ink); line-height: 1.6; } +.help-body b { font-weight: 600; } +.help-body code { font-family: var(--mono); font-size: 0.85em; } .info-panel + .info-panel { margin-top: 14px; } /* ── Modal ────────────────────────────────────────────── */ diff --git a/web/src/app/settings/settings-form.tsx b/web/src/app/settings/settings-form.tsx index d2e56aa..c86b1da 100644 --- a/web/src/app/settings/settings-form.tsx +++ b/web/src/app/settings/settings-form.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { PageHead } from "../_ui/page-head"; +import { HelpButton } from "../_ui/help-button"; type Loaded = { qobuzEmail: string; @@ -192,12 +193,14 @@ export function SettingsForm() { {tab === "Qobuz" ? (
-

- Lyra signs in to Qobuz with a user ID + auth token — the download engine - treats these as more reliable than email/password, which Qobuz often rejects. Find - them in your logged-in Qobuz web session (bundle/app secret + user auth token). -

-
Auth token
+
+ Auth token + + Lyra signs in to Qobuz with a user ID + auth token — the download engine + treats these as more reliable than email/password, which Qobuz often rejects. Find + them in your logged-in Qobuz web session (bundle/app secret + user auth token). + +
+ + When on, the worker periodically checks your followed artists: it grabs new + releases as they appear and upgrades owned albums that fall below the + quality cutoff. +
+
+ Quality cutoff — the minimum acceptable quality class: 1 = lossy,{" "} + 2 = CD-lossless (FLAC 16/44.1), 3 = hi-res. An owned album is + kept as-is when it’s ≥ the cutoff; otherwise a better copy is chased. Range 1–3. +
+
+ Poll interval — hours between new-release checks. Retry interval — hours + before re-attempting a release that couldn’t be found yet. Upgrade window — how + many days after first acquiring an album to keep looking for a better copy. +
-

- When on, the worker periodically checks your followed artists: it grabs new - releases as they appear and upgrades owned albums that fall below the - quality cutoff. -
- Quality cutoff — the minimum acceptable quality class: 1 = lossy,{" "} - 2 = CD-lossless (FLAC 16/44.1), 3 = hi-res. An owned album is - kept as-is when it’s ≥ the cutoff; otherwise a better copy is chased. Range 1–3. -
- Poll interval — hours between new-release checks. Retry interval — hours - before re-attempting a release that couldn’t be found yet. Upgrade window — how - many days after first acquiring an album to keep looking for a better copy. -

{MONITOR_FIELDS.map(([key, label]) => ( + + When on, the worker periodically sweeps your followed artists for similar artists + + their new releases (via ListenBrainz and, if configured, Last.fm) and lists them on{" "} + Discover. Nothing is downloaded automatically — you Follow/Want from there. +
+
+ Interval — hours between sweeps. Seeds per chunk — how many followed + artists are processed per worker cycle (keeps job-claiming responsive on big rosters). + Similar per seed — candidates pulled per artist. Albums per artist — + releases surfaced per suggested artist. Min score — drops weak suggestions.{" "} + ListenBrainz base URL — leave blank for the default endpoint. +
-

- When on, the worker periodically sweeps your followed artists for similar artists + - their new releases (via ListenBrainz and, if configured, Last.fm) and lists them on{" "} - Discover. Nothing is downloaded automatically — you Follow/Want from there. -
- Interval — hours between sweeps. Seeds per chunk — how many followed - artists are processed per worker cycle (keeps job-claiming responsive on big rosters). - Similar per seed — candidates pulled per artist. Albums per artist — - releases surfaced per suggested artist. Min score — drops weak suggestions.{" "} - ListenBrainz base URL — leave blank for the default endpoint. -

{DISCOVERY_FIELDS.map(([key, label]) => (