feat(player): Now Playing overlay + synced lyrics panel (A1/A11, §6.7)

Full-screen overlay expanded from the persistent player: big artwork or a
Lyrics tab. Synced (LRC) lyrics highlight the active line by playback position,
auto-scroll, and seek on tap; plain text is the fallback; missing lyrics offers
a re-fetch. Adds lyrics endpoint + LRC parser + isNowPlayingOpen player state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Цвылев Александр Вадимович
2026-07-28 21:04:39 +03:00
parent 6efbf4b68b
commit 9d88d70be0
11 changed files with 509 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
import { api } from '../index';
import type { Lyrics } from '../types';
// `GET /tracks/{id}/lyrics` — cached lyrics for the Now Playing panel (§6.7).
// A miss is a normal 200 with `status: 'not_found'`, not an error.
interface RawLyrics {
track_id: string;
status: string;
source: string | null;
synced: string | null;
plain: string | null;
synced_available: boolean;
}
const toLyrics = (r: RawLyrics): Lyrics => ({
trackId: r.track_id,
status: r.status as Lyrics['status'],
source: r.source ?? undefined,
synced: r.synced ?? undefined,
plain: r.plain ?? undefined,
syncedAvailable: r.synced_available,
});
export const lyricsApi = api.injectEndpoints({
endpoints: (build) => ({
getLyrics: build.query<Lyrics, string>({
query: (trackId) => `/tracks/${trackId}/lyrics`,
transformResponse: (raw: RawLyrics) => toLyrics(raw),
}),
refetchLyrics: build.mutation<Lyrics, string>({
query: (trackId) => ({
url: `/tracks/${trackId}/lyrics/refetch`,
method: 'POST',
}),
transformResponse: (raw: RawLyrics) => toLyrics(raw),
// Push the fresh result straight into the getLyrics cache for this track.
async onQueryStarted(trackId, { dispatch, queryFulfilled }) {
const { data } = await queryFulfilled;
dispatch(
lyricsApi.util.upsertQueryData('getLyrics', trackId, data),
);
},
}),
}),
overrideExisting: false,
});
export const { useGetLyricsQuery, useRefetchLyricsMutation } = lyricsApi;
+11
View File
@@ -192,6 +192,17 @@ export interface DuplicateGroup {
tracks: Track[];
}
/** Cached lyrics for a track (§6.7). `not_found` is a normal state, not an
* error. `synced` is raw LRC (client parses timestamps); `plain` is fallback. */
export interface Lyrics {
trackId: string;
status: 'found' | 'not_found' | 'pending';
source?: string;
synced?: string;
plain?: string;
syncedAvailable: boolean;
}
export interface User {
id: string;
username: string;