bold words
This commit is contained in:
@@ -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(<Text key={`br-${i}-${j}`}>{"\n"}</Text>);
|
||||
} else if (p.length > 0) {
|
||||
spans.push(
|
||||
<Text
|
||||
key={`w-${i}-${j}`}
|
||||
style={
|
||||
isHighlighted ? styles.karaokeWordActive : styles.karaokeWord
|
||||
}
|
||||
>
|
||||
{p}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Add space between words when no newline
|
||||
if (!/\n/.test(text) && i < alignedWords.length - 1) {
|
||||
spans.push(
|
||||
<Text key={`sp-${i}`} style={styles.karaokeWord}>
|
||||
{" "}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Text style={styles.karaokeContainer} selectable>
|
||||
{spans}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Page
|
||||
headerType="NAVIGATION"
|
||||
@@ -346,8 +417,10 @@ const MusicDetails = () => {
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
{description?.length > 0 && (
|
||||
<View style={{ marginTop: 30, gap: 20 }}>
|
||||
<View style={{ marginTop: 30, gap: 20 }}>
|
||||
{alignedWords?.length ? (
|
||||
renderKaraokeFullLyrics()
|
||||
) : description?.length > 0 ? (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
@@ -357,8 +430,8 @@ const MusicDetails = () => {
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
) : null}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</Page>
|
||||
);
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user