feat(settings): wire Playback + Scrobbling panels to the backend

Adds src/api/endpoints/settings.ts (getSettings/updateSettings,
getScrobbling/updateScrobbling) plus AppSettings/ScrobblingConfig types
and a Settings cache tag. Replaces the "coming soon" scaffolds:

- Playback: stream-quality preference (SegmentedControl), server-synced.
- Scrobbling: enable + provider (Last.fm/ListenBrainz) + username +
  write-only session key, with a linked/not-linked badge and save states.

Built with modern-sk (Window/SegmentedControl/TextField/Button/Callout/
Badge) + the shared Loading/Error states; en + ru strings added. Server
state flows through RTK Query hooks only (no fetch in components).

Also drops a pre-existing unused TextField import in LibraryPage that was
failing `tsc --noEmit`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Цвылев Александр Вадимович
2026-07-28 15:16:19 +03:00
parent 205b51ec73
commit 57044dabba
8 changed files with 419 additions and 11 deletions
+90
View File
@@ -0,0 +1,90 @@
import { api } from '../index';
import type {
AppSettings,
ScrobbleProvider,
ScrobblingConfig,
StreamQuality,
ThemePref,
} from '../types';
// Wire shapes (snake_case) from the backend, mapped to the app's camelCase types.
interface RawSettings {
theme: ThemePref;
stream_quality: StreamQuality;
}
interface RawScrobbling {
enabled: boolean;
provider: ScrobbleProvider | null;
username: string | null;
configured: boolean;
}
const toSettings = (r: RawSettings): AppSettings => ({
theme: r.theme,
streamQuality: r.stream_quality,
});
const toScrobbling = (r: RawScrobbling): ScrobblingConfig => ({
enabled: r.enabled,
provider: r.provider,
username: r.username,
configured: r.configured,
});
export const settingsApi = api.injectEndpoints({
endpoints: (build) => ({
getSettings: build.query<AppSettings, void>({
query: () => '/settings',
transformResponse: (raw: RawSettings) => toSettings(raw),
providesTags: ['Settings'],
}),
updateSettings: build.mutation<
AppSettings,
{ theme?: ThemePref; streamQuality?: StreamQuality }
>({
query: ({ theme, streamQuality }) => ({
url: '/settings',
method: 'PATCH',
body: { theme, stream_quality: streamQuality },
}),
transformResponse: (raw: RawSettings) => toSettings(raw),
invalidatesTags: ['Settings'],
}),
getScrobbling: build.query<ScrobblingConfig, void>({
query: () => '/settings/scrobbling',
transformResponse: (raw: RawScrobbling) => toScrobbling(raw),
providesTags: ['Settings'],
}),
updateScrobbling: build.mutation<
ScrobblingConfig,
{
enabled: boolean;
provider: ScrobbleProvider | null;
username: string | null;
// Write-only: send only when the user typed a new key; omit to keep the
// stored one (the server never returns it).
sessionKey?: string;
}
>({
query: ({ enabled, provider, username, sessionKey }) => ({
url: '/settings/scrobbling',
method: 'PUT',
body: {
enabled,
provider,
username,
session_key: sessionKey,
},
}),
transformResponse: (raw: RawScrobbling) => toScrobbling(raw),
invalidatesTags: ['Settings'],
}),
}),
overrideExisting: false,
});
export const {
useGetSettingsQuery,
useUpdateSettingsMutation,
useGetScrobblingQuery,
useUpdateScrobblingMutation,
} = settingsApi;
+1
View File
@@ -23,6 +23,7 @@ export const api = createApi({
'Like',
'User',
'Storage',
'Settings',
],
// Tier 2 offline: seed the cache from the persisted snapshot dispatched at
// startup (see `store/rtkqPersist.ts`). Returning the saved queries/mutations
+21
View File
@@ -258,3 +258,24 @@ export interface MetadataEdit {
genre?: string;
trackNumber?: number;
}
// -- user settings (`/settings`) ---------------------------------------------
export type ThemePref = 'system' | 'light' | 'dark';
export type StreamQuality = 'original' | 'high' | 'medium' | 'low';
/** General per-user preferences from `GET /settings`. */
export interface AppSettings {
theme: ThemePref;
streamQuality: StreamQuality;
}
export type ScrobbleProvider = 'lastfm' | 'listenbrainz';
/** Scrobbling config from `GET /settings/scrobbling`. The session key is
* write-only server-side, so it never appears here — only `configured`. */
export interface ScrobblingConfig {
enabled: boolean;
provider: ScrobbleProvider | null;
username: string | null;
configured: boolean;
}