diff --git a/src/screens/Library/MusicDetails.js b/src/screens/Library/MusicDetails.js index 26bf97a..430f4cf 100644 --- a/src/screens/Library/MusicDetails.js +++ b/src/screens/Library/MusicDetails.js @@ -77,6 +77,22 @@ const MusicDetails = () => { const player = useAudioPlayer(songUrl ? { uri: songUrl } : undefined); + // Karaoke aligned words (from timestamps) + const alignedWords = useMemo(() => { + try { + const idx = Number(project?.songIndex) || 0; + const ts = project?.musicTimestamps?.[idx]; + const arr = Array.isArray(ts?.alignedWords) ? ts.alignedWords : []; + return arr.map((w) => ({ + word: String(w?.word ?? ""), + startS: Number(w?.startS ?? 0), + endS: Number(w?.endS ?? 0), + })); + } catch (e) { + return []; + } + }, [project?.musicTimestamps, project?.songIndex]); + // Reset counters when the track changes useEffect(() => { listenedMsRef.current = 0; @@ -189,10 +205,65 @@ const MusicDetails = () => { } const c = project?.lyrics?.couplet; const r = project?.lyrics?.refrain; - const parts = [c ? `[Couplet]\n${c}` : null, r ? `[Refrain]\n${r}` : null].filter(Boolean); + const parts = [ + c ? `[Couplet]\n${c}` : null, + r ? `[Refrain]\n${r}` : null, + ].filter(Boolean); return parts.length ? parts.join("\n\n") : ""; }, [project]); + // Current time in seconds for highlighting + const currentTimeS = useMemo( + () => Math.max(0, (progressInfo.pos || 0) / 1000), + [progressInfo.pos] + ); + + // Render full lyrics with the currently sung word highlighted + const renderKaraokeFullLyrics = () => { + if (!alignedWords?.length) return null; + const spans = []; + for (let i = 0; i < alignedWords.length; i++) { + const w = alignedWords[i]; + const text = String(w.word || ""); + // Highlight all words that already started (and keep them lit) + const isHighlighted = currentTimeS >= (w.startS || 0); + + // split by newline to preserve line breaks + const parts = text.split(/(\n+)/); + parts.forEach((p, j) => { + if (/^\n+$/.test(p)) { + spans.push({"\n"}); + } else if (p.length > 0) { + spans.push( + + {p} + + ); + } + }); + + // Add space between words when no newline + if (!/\n/.test(text) && i < alignedWords.length - 1) { + spans.push( + + {" "} + + ); + } + } + + return ( + + {spans} + + ); + }; + return ( { )} - {description?.length > 0 && ( - + + {alignedWords?.length ? ( + renderKaraokeFullLyrics() + ) : description?.length > 0 ? ( { > {description} - - )} + ) : null} + ); @@ -385,4 +458,20 @@ const styles = StyleSheet.create({ color: Palette.white, fontFamily: FONT_FAMILY.HelveticaNeueRegular, }, + karaokeContainer: { + fontSize: 16, + color: Palette.white, + fontFamily: FONT_FAMILY.InterRegular, + lineHeight: 24, + }, + karaokeWord: { + color: Palette.white, + fontFamily: FONT_FAMILY.InterRegular, + }, + karaokeWordActive: { + color: Palette.white, + fontFamily: FONT_FAMILY.InterBold, + fontStyle: "italic", + fontWeight: "bold", + }, });