feat: Settings is one page per section down a left rail, Account first, every setting a row; the profile form no longer saves blanks over your name and email (M503 #5595 #5596 #5597)
release / govulncheck (push) Successful in 18s
release / web (push) Successful in 1m45s
release / go (push) Successful in 1m58s
release / integration (push) Successful in 5m39s
release / android (push) Successful in 5m43s
release / Build signed APK (releases and dev) (push) Successful in 5m58s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 26s
release / Verify release artifacts (tag releases only) (push) Skipped

- /settings lands on Account: the Android app, profile, password, signed-in
  devices. Then Playback, Appearance, Notifications, Connections, About.
- Each setting is a row (name, one short line, control at the end) in a
  capped-width column; one-of-N choices are a shared Segmented control.
- Collection and Settings share one SectionRail.
- Connections leads ListenBrainz with a status line: not connected,
  connected and not sending, or sending with the last scrobble's age.
- Notifications prints its column labels once; the digest email's
  settings link points at /settings/notifications.
- #5596: the profile form now loads what is stored and sends only the
  fields that changed. It used to start blank, and the server reads a
  blank as "clear", so one Save wiped the display name and email.
- Recommendation metrics moves to Library › Your listening, and Hidden
  tracks gets a Library tab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 23:34:51 -04:00
co-authored by Claude Opus 5.5
parent c0c0f8a71f
commit c94c09aeae
32 changed files with 1552 additions and 1408 deletions
+23 -26
View File
@@ -1,19 +1,20 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Smartphone } from 'lucide-svelte';
import { Download, Smartphone } from 'lucide-svelte';
import { api } from '#lib/api/client.js';
import SettingsGroup from '#lib/components/settings/SettingsGroup.svelte';
import SettingRow from '#lib/components/settings/SettingRow.svelte';
// Polls /api/client/version once on mount. Renders nothing on 404
// (no bundled APK — graceful degradation in dev / pre-CI images)
// or 401 (defensive — caller must be logged in).
// The Android app this server bundles, at the top of Settings › Account
// (#5595). Polls /api/client/version once on mount. Renders nothing on
// 404 (no bundled APK: dev and pre-CI images) or 401.
//
// The /api/client/* endpoints are auth-gated to prevent anonymous
// bandwidth abuse on the APK stream, so this component only shows
// for logged-in users. Mount in Settings, never in pre-auth surfaces.
// bandwidth abuse on the APK stream, so this only shows to a signed-in
// user. Mount in Settings, never on a pre-auth page.
// Server returns snake_case (clientVersionResponse in
// internal/api/client_assets.go). Map to a camelCase struct for
// the rest of the component.
// internal/api/client_assets.go).
type WireInfo = { version: string; apk_url: string; size_bytes: number };
type Info = { version: string; apkUrl: string; sizeBytes: number };
@@ -40,23 +41,19 @@
</script>
{#if info}
<section class="space-y-3 rounded border border-border bg-surface p-4">
<h2 class="text-lg font-semibold">Mobile app</h2>
<p class="text-sm text-text-secondary">
Install the Android app paired to this server. Sign in with the same account.
</p>
<a
href={info.apkUrl}
download="minstrel.apk"
class="mobile-app-download flex items-center gap-3 rounded-md border border-border bg-surface px-3 py-2 text-sm text-text-primary no-underline transition-colors hover:bg-surface-hover hover:text-accent-fg focus-visible:ring-2 focus-visible:ring-accent"
<SettingsGroup title="Android app">
<SettingRow
label="Minstrel for Android"
hint="{info.version}{info.sizeBytes > 0 ? ` · ${formatSize(info.sizeBytes)}` : ''} · sign in with this account"
>
<Smartphone size={18} strokeWidth={1.5} class="shrink-0 text-accent-fg" />
<span class="flex flex-col leading-tight">
<span class="font-medium">Get the Android app</span>
<span class="text-xs text-text-secondary">
{info.version}{info.sizeBytes > 0 ? ` · ${formatSize(info.sizeBytes)}` : ''}
</span>
</span>
</a>
</section>
<Smartphone size={18} strokeWidth={1.5} class="text-text-secondary" aria-hidden="true" />
<a
href={info.apkUrl}
download="minstrel.apk"
class="inline-flex items-center gap-1.5 rounded-md bg-action-secondary px-3 py-1.5 text-sm text-action-fg hover:opacity-90 focus-visible:ring-2 focus-visible:ring-accent"
>
<Download size={16} strokeWidth={1.75} aria-hidden="true" /> Download
</a>
</SettingRow>
</SettingsGroup>
{/if}