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",
+ },
});