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:
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user