diff --git a/src/screens/Library/MusicDetails.js b/src/screens/Library/MusicDetails.js
index 430f4cf..9123dfb 100644
--- a/src/screens/Library/MusicDetails.js
+++ b/src/screens/Library/MusicDetails.js
@@ -1,7 +1,7 @@
import { useRoute } from "@react-navigation/core";
import { useAudioPlayer } from "expo-audio";
import { Image as ExpoImage } from "expo-image";
-import React, { useEffect, useMemo, useState } from "react";
+import React, { useEffect, useMemo, useRef, useState } from "react";
import {
Pressable,
Image as RNImage,
@@ -217,53 +217,105 @@ const MusicDetails = () => {
() => Math.max(0, (progressInfo.pos || 0) / 1000),
[progressInfo.pos]
);
+ // Group words into timed lines (similar to KaraokeLyrics)
+ const groupAlignedWordsToLines = (words = [], { removeTags = true } = {}) => {
+ const out = [];
+ let buf = [];
+ let start = null;
+ const clean = (txt) =>
+ String(txt || "")
+ .replace(/\s+/g, " ")
+ .trim();
+ const isSentenceEnd = (txt) => /[.!?]$/.test((txt || "").trim());
+ const isSectionTag = (txt) =>
+ /^\s*\[[^\]]+\]\s*$/i.test((txt || "").trim());
- // 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);
+ for (let i = 0; i < words.length; i++) {
+ const w = words[i] || {};
+ const original = String(w.word || "");
+ const textNoNewline = original.replace(/\n/g, " ");
+ const textNoTag = removeTags
+ ? textNoNewline.replace(/^\s*\[[^\]]+\]\s*/i, "")
+ : textNoNewline;
+ const next = words[i + 1] || null;
+ const gapToNext = next
+ ? Math.max(0, Number(next.startS || 0) - Number(w.endS || 0))
+ : 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}
-
- );
+ if (buf.length === 0) start = Number(w.startS || 0);
+
+ if (isSectionTag(textNoNewline)) {
+ if (!removeTags) {
+ const joined = clean(textNoNewline);
+ if (joined)
+ out.push({
+ text: joined,
+ startS: start ?? Number(w.startS || 0),
+ endS: Number(w.endS || 0),
+ });
}
+ buf = [];
+ start = null;
+ continue;
+ }
+
+ if (!clean(textNoTag)) continue;
+
+ buf.push({
+ text: textNoTag,
+ startS: Number(w.startS || 0),
+ endS: Number(w.endS || 0),
});
- // Add space between words when no newline
- if (!/\n/.test(text) && i < alignedWords.length - 1) {
- spans.push(
-
- {" "}
-
- );
+ const eolByNewline = /\n/.test(original);
+ const eolByPause = gapToNext >= 0.6; // logical break
+ const eolByPunct = isSentenceEnd(textNoTag);
+ const isLast = i === words.length - 1;
+
+ if (eolByNewline || eolByPause || eolByPunct || isLast) {
+ const joined = clean(buf.map((b) => b.text).join(" "));
+ if (joined)
+ out.push({
+ text: joined,
+ startS: start ?? Number(w.startS || 0),
+ endS: Number(w.endS || 0),
+ words: buf,
+ });
+ buf = [];
+ start = null;
}
}
-
- return (
-
- {spans}
-
- );
+ return out;
};
+ const lines = useMemo(
+ () => groupAlignedWordsToLines(alignedWords, { removeTags: true }),
+ [alignedWords]
+ );
+ const currentLineIdx = useMemo(() => {
+ if (!lines || lines.length === 0) return -1;
+ for (let i = 0; i < lines.length; i++) {
+ const L = lines[i];
+ if (currentTimeS >= (L.startS || 0) && currentTimeS <= (L.endS || 0))
+ return i;
+ }
+ if (currentTimeS > (lines[lines.length - 1]?.endS || 0))
+ return lines.length - 1;
+ return -1;
+ }, [lines, currentTimeS]);
+
+ // Auto-scroll lyrics to keep the current line visible
+ const lyricsRef = useRef(null);
+ const lineYRef = useRef({});
+ useEffect(() => {
+ const y = lineYRef.current?.[currentLineIdx];
+ if (lyricsRef.current && typeof y === "number") {
+ try {
+ lyricsRef.current.scrollTo({ y: Math.max(0, y - 80), animated: true });
+ } catch (e) {}
+ }
+ }, [currentLineIdx]);
+
return (
{
),
})}
>
-
+
{coverUrl && (
{
)}
-
- {alignedWords?.length ? (
- renderKaraokeFullLyrics()
- ) : description?.length > 0 ? (
-
+ {lines?.length ? (
+
- {description}
-
+ {lines.map((L, i) => (
+ {
+ lineYRef.current[i] = e.nativeEvent.layout.y;
+ }}
+ style={{
+ marginBottom: 8,
+ flexDirection: "row",
+ flexWrap: "wrap",
+ // justifyContent: "",
+ }}
+ >
+ {(L.words || []).map((w, j) => (
+ = (w.startS || 0)
+ ? styles.karaokeWordActive
+ : styles.karaokeWord
+ }
+ >
+ {w.text}
+ {j < (L.words?.length || 1) - 1 ? " " : ""}
+
+ ))}
+
+ ))}
+
+ ) : description?.length > 0 ? (
+
+
+ {description}
+
+
) : null}
-
+
);
};
@@ -469,9 +553,7 @@ const styles = StyleSheet.create({
fontFamily: FONT_FAMILY.InterRegular,
},
karaokeWordActive: {
- color: Palette.white,
+ color: Palette.primary,
fontFamily: FONT_FAMILY.InterBold,
- fontStyle: "italic",
- fontWeight: "bold",
},
});