import { usePlayerStore } from '@/stores/playerStore' import { useSettingsStore } from '@/stores/settingsStore' import { getLyricOffsetKey } from '@/lib/lyricOffset' import type { LyricLine as AMLLLyricLine } from '@applemusic-like-lyrics/core' import '@applemusic-like-lyrics/core/style.css' import { LyricPlayer } from '@applemusic-like-lyrics/react' import { useMemo } from 'react' const FULL_SIZE_STYLE = { width: '100%', height: '100%' } as const interface LyricLine { time: number text: string translation?: string } function parseLRC(lrc: string): { time: number; text: string }[] { const lines: { time: number; text: string }[] = [] // Supports [mm:ss], [mm:ss.x], [mm:ss.xx], [mm:ss.xxx] const regex = /\[(\d{2}):(\d{2})(?:\.(\d{1,3}))?\](.*)/g let match while ((match = regex.exec(lrc)) !== null) { const minutes = parseInt(match[1], 10) const seconds = parseInt(match[2], 10) const ms = match[3] ? parseInt(match[3].padEnd(3, '0'), 10) : 0 const time = minutes * 60 + seconds + ms / 1000 const text = match[4].trim() if (text) { lines.push({ time, text }) } } return lines.sort((a, b) => a.time - b.time) } function mergeLyrics(original: string, translated: string): LyricLine[] { const origLines = parseLRC(original) if (origLines.length === 0) return [] const result: LyricLine[] = origLines.map((l) => ({ ...l })) if (!translated) return result const transLines = parseLRC(translated) if (transLines.length === 0) return result const transMap = new Map() for (const tl of transLines) { transMap.set(Math.round(tl.time * 10) / 10, tl.text) } for (const line of result) { const key = Math.round(line.time * 10) / 10 const exact = transMap.get(key) if (exact) { line.translation = exact continue } for (let offset = 1; offset <= 5; offset++) { const near = transMap.get(Math.round((line.time + offset * 0.1) * 10) / 10) ?? transMap.get(Math.round((line.time - offset * 0.1) * 10) / 10) if (near) { line.translation = near break } } } return result } /** 将自有 LRC 解析结果转为 AMLL LyricLine 格式 */ function toAMLLLines(lines: LyricLine[]): AMLLLyricLine[] { return lines.map((line, i, arr) => { const startMs = Math.round(line.time * 1000) const endMs = Math.round((arr[i + 1]?.time ?? line.time + 5) * 1000) return { words: [ { word: line.text, startTime: startMs, endTime: endMs, romanWord: '', obscene: false, }, ], translatedLyric: line.translation ?? '', romanLyric: '', startTime: startMs, endTime: endMs, isBG: false, isDuet: false, } }) } export function LyricDisplay() { const lyric = usePlayerStore((s) => s.lyric) const tlyric = usePlayerStore((s) => s.tlyric) const lyricLoading = usePlayerStore((s) => s.lyricLoading) const ttmlLines = usePlayerStore((s) => s.ttmlLines) const currentTime = usePlayerStore((s) => s.currentTime) const isPlaying = usePlayerStore((s) => s.isPlaying) const currentTrack = usePlayerStore((s) => s.currentTrack) const lyricOffsetPreview = usePlayerStore((s) => s.lyricOffsetPreview) const alignAnchor = useSettingsStore((s) => s.lyricAlignAnchor) const alignPosition = useSettingsStore((s) => s.lyricAlignPosition) const enableSpring = useSettingsStore((s) => s.lyricEnableSpring) const enableBlur = useSettingsStore((s) => s.lyricEnableBlur) const enableScale = useSettingsStore((s) => s.lyricEnableScale) const fontWeight = useSettingsStore((s) => s.lyricFontWeight) const fontSize = useSettingsStore((s) => s.lyricFontSize) const translationFontSize = useSettingsStore((s) => s.lyricTranslationFontSize) const romanFontSize = useSettingsStore((s) => s.lyricRomanFontSize) const lyricOffsets = useSettingsStore((s) => s.lyricOffsets) const lyricOffsetKey = getLyricOffsetKey(currentTrack) const savedLyricOffsetMs = lyricOffsets[lyricOffsetKey ?? ''] ?? 0 const isCalibrating = lyricOffsetPreview?.key === lyricOffsetKey const lyricOffsetMs = isCalibrating ? lyricOffsetPreview.offsetMs : savedLyricOffsetMs // LRC 解析(仅在没有 TTML 时使用) const lrcLines = useMemo(() => mergeLyrics(lyric, tlyric), [lyric, tlyric]) const lrcAmllLines = useMemo(() => toAMLLLines(lrcLines), [lrcLines]) // TTML 优先,LRC 回退 const amllLines = ttmlLines ?? lrcAmllLines const hasLyrics = ttmlLines ? ttmlLines.length > 0 : lrcLines.length > 0 if (!hasLyrics) { return (

{lyricLoading ? '歌词加载中...' : '暂无歌词'}

) } return (
) }