feat(web): add /settings page with ListenBrainz section
Adds api.put helper, ListenBrainz API module with query/mutation helpers, a /settings route with token + enable/disable UI, and 5 tests. Adds Settings to the Shell nav. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
<script lang="ts">
|
||||
import { useQueryClient } from '@tanstack/svelte-query';
|
||||
import type { CreateQueryResult, CreateMutationResult } from '@tanstack/svelte-query';
|
||||
import {
|
||||
createLBStatusQuery,
|
||||
createTokenMutation,
|
||||
createEnabledMutation,
|
||||
type LBStatus
|
||||
} from '$lib/api/listenbrainz';
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const status = createLBStatusQuery() as CreateQueryResult<LBStatus>;
|
||||
const tokenMutation = createTokenMutation(queryClient);
|
||||
const enabledMutation = createEnabledMutation(queryClient);
|
||||
|
||||
let tokenInput = $state('');
|
||||
|
||||
function saveToken() {
|
||||
const t = tokenInput.trim();
|
||||
if (!t) return;
|
||||
$tokenMutation.mutate(t, { onSuccess: () => { tokenInput = ''; } });
|
||||
}
|
||||
|
||||
function clearToken() {
|
||||
$tokenMutation.mutate('');
|
||||
}
|
||||
|
||||
function toggleEnabled() {
|
||||
if (!$status.data) return;
|
||||
$enabledMutation.mutate(!$status.data.enabled);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<h1 class="text-2xl font-semibold">Settings</h1>
|
||||
|
||||
<section class="space-y-4 rounded border border-border bg-surface p-4">
|
||||
<h2 class="text-lg font-semibold">ListenBrainz</h2>
|
||||
|
||||
{#if $status.isPending}
|
||||
<p class="text-text-secondary">Loading…</p>
|
||||
{:else if $status.isError}
|
||||
<p class="text-text-secondary">Couldn't load status.</p>
|
||||
{:else if $status.data}
|
||||
<div class="space-y-3">
|
||||
<label class="block text-sm">
|
||||
<span class="block text-text-secondary mb-1">User token</span>
|
||||
{#if $status.data.token_set}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-text-primary">••••••••••• (set)</span>
|
||||
<button
|
||||
type="button"
|
||||
onclick={clearToken}
|
||||
class="text-sm text-accent hover:underline"
|
||||
disabled={$tokenMutation.isPending}
|
||||
>clear</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="password"
|
||||
bind:value={tokenInput}
|
||||
placeholder="paste your LB token"
|
||||
class="flex-1 rounded border border-border bg-background px-2 py-1 text-sm"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick={saveToken}
|
||||
disabled={!tokenInput.trim() || $tokenMutation.isPending}
|
||||
class="rounded bg-accent px-3 py-1 text-sm text-background disabled:opacity-50"
|
||||
>Save</button>
|
||||
</div>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<label class="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={$status.data.enabled}
|
||||
disabled={!$status.data.token_set || $enabledMutation.isPending}
|
||||
onchange={toggleEnabled}
|
||||
/>
|
||||
<span>Send my plays to ListenBrainz</span>
|
||||
</label>
|
||||
|
||||
{#if $status.data.last_scrobbled_at}
|
||||
<p class="text-sm text-text-secondary">
|
||||
Last scrobbled: {new Date($status.data.last_scrobbled_at).toLocaleString()}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<p class="text-xs text-text-secondary">
|
||||
Get a token at <a href="https://listenbrainz.org/profile/" target="_blank" rel="noopener" class="text-accent hover:underline">listenbrainz.org/profile</a>.
|
||||
Tokens are stored unencrypted in this server's database — treat as sensitive.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
Reference in New Issue
Block a user