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,50 @@
|
||||
/** One parsed LRC line: a timestamp (seconds) and its text. */
|
||||
export interface LrcLine {
|
||||
time: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const TAG = /\[(\d{1,2}):(\d{2})(?:[.:](\d{1,3}))?\]/g;
|
||||
|
||||
/**
|
||||
* Parse an LRC string into time-sorted lines. A single source line may carry
|
||||
* several timestamps (repeated lyric) — each becomes its own entry. Metadata
|
||||
* tags (`[ar:…]`, `[ti:…]`) and untimed lines are skipped; empty-text lines are
|
||||
* kept so the active-line highlight advances through instrumental gaps.
|
||||
*/
|
||||
export function parseLrc(lrc: string): LrcLine[] {
|
||||
const lines: LrcLine[] = [];
|
||||
for (const raw of lrc.split(/\r?\n/)) {
|
||||
const stamps = [...raw.matchAll(TAG)];
|
||||
if (stamps.length === 0) continue;
|
||||
const text = raw.replace(/\[[^\]]*\]/g, '').trim();
|
||||
for (const m of stamps) {
|
||||
const min = Number(m[1]);
|
||||
const sec = Number(m[2]);
|
||||
const frac = m[3] ? Number(m[3].padEnd(3, '0')) / 1000 : 0;
|
||||
lines.push({ time: min * 60 + sec + frac, text });
|
||||
}
|
||||
}
|
||||
return lines.sort((a, b) => a.time - b.time);
|
||||
}
|
||||
|
||||
/**
|
||||
* Index of the active line for a playback position (seconds) — the last line
|
||||
* whose timestamp has passed. Returns -1 before the first line. Assumes `lines`
|
||||
* is time-sorted (as `parseLrc` returns).
|
||||
*/
|
||||
export function activeLineIndex(lines: LrcLine[], position: number): number {
|
||||
let lo = 0;
|
||||
let hi = lines.length - 1;
|
||||
let result = -1;
|
||||
while (lo <= hi) {
|
||||
const mid = (lo + hi) >> 1;
|
||||
if (lines[mid].time <= position) {
|
||||
result = mid;
|
||||
lo = mid + 1;
|
||||
} else {
|
||||
hi = mid - 1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user