mirror of
https://github.com/imsyy/SPlayer.git
synced 2025-11-25 19:37:35 +08:00
🦄 refactor: 基础适配新格式
This commit is contained in:
2
components.d.ts
vendored
2
components.d.ts
vendored
@@ -100,9 +100,11 @@ declare module 'vue' {
|
||||
NP: typeof import('naive-ui')['NP']
|
||||
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
|
||||
NPopover: typeof import('naive-ui')['NPopover']
|
||||
NProgress: typeof import('naive-ui')['NProgress']
|
||||
NQrCode: typeof import('naive-ui')['NQrCode']
|
||||
NRadio: typeof import('naive-ui')['NRadio']
|
||||
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||
NResult: typeof import('naive-ui')['NResult']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSkeleton: typeof import('naive-ui')['NSkeleton']
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
'player-content',
|
||||
{
|
||||
pure: statusStore.pureLyricMode && musicStore.isHasLrc,
|
||||
'no-lrc': !musicStore.isHasLrc,
|
||||
'no-lrc': !musicStore.isHasLrc || (!musicStore.isHasLrc && !statusStore.lyricLoading),
|
||||
},
|
||||
]"
|
||||
@mousemove="playerMove"
|
||||
@@ -114,7 +114,8 @@ const instantLyrics = computed(() => {
|
||||
const content = isYrc
|
||||
? musicStore.songLyric.yrcData[statusStore.lyricIndex]
|
||||
: musicStore.songLyric.lrcData[statusStore.lyricIndex];
|
||||
return { content: content?.content, tran: settingStore.showTran && content?.tran };
|
||||
const contentStr = content?.words?.map((v) => v.word).join("") || "";
|
||||
return { content: contentStr, tran: settingStore.showTran && content?.translatedLyric };
|
||||
});
|
||||
|
||||
// 隐藏播放元素
|
||||
|
||||
@@ -72,8 +72,8 @@ const amLyricsData = computed<LyricLine[]>(() => {
|
||||
if (!songLyric) return [];
|
||||
|
||||
// 优先使用逐字歌词(YRC/TTML)
|
||||
const useYrc = songLyric.yrcAMData?.length && settingStore.showYrc;
|
||||
const lyrics = useYrc ? songLyric.yrcAMData : songLyric.lrcAMData;
|
||||
const useYrc = songLyric.yrcData?.length && settingStore.showYrc;
|
||||
const lyrics = useYrc ? songLyric.yrcData : songLyric.lrcData;
|
||||
|
||||
// 简单检查歌词有效性
|
||||
if (!Array.isArray(lyrics) || lyrics.length === 0) return [];
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div
|
||||
:key="musicStore.songLyric.lrcData?.[0]?.content"
|
||||
:key="musicStore.songLyric.lrcData?.[0]?.words?.[0]?.word"
|
||||
class="lyric-content"
|
||||
@after-enter="lyricsScroll(statusStore.lyricIndex)"
|
||||
@after-leave="lyricsScroll(statusStore.lyricIndex)"
|
||||
@@ -34,7 +34,7 @@
|
||||
<!-- 倒计时 -->
|
||||
<CountDown
|
||||
:start="0"
|
||||
:duration="musicStore.songLyric.yrcData[0].time || 0"
|
||||
:duration="(musicStore.songLyric.yrcData[0].startTime || 0) / 1000"
|
||||
:seek="playSeek"
|
||||
:playing="statusStore.playStatus"
|
||||
/>
|
||||
@@ -50,7 +50,7 @@
|
||||
// on: statusStore.lyricIndex === index,
|
||||
// 当播放时间大于等于当前歌词的开始时间
|
||||
on:
|
||||
(playSeek >= item.time && playSeek < item.endTime) ||
|
||||
(playSeek >= item.startTime / 1000 && playSeek < item.endTime / 1000) ||
|
||||
statusStore.lyricIndex === index,
|
||||
'is-bg': item.isBG,
|
||||
'is-duet': item.isDuet,
|
||||
@@ -58,60 +58,65 @@
|
||||
]"
|
||||
:style="{
|
||||
filter: settingStore.lyricsBlur
|
||||
? (playSeek >= item.time && playSeek < item.endTime) ||
|
||||
? (playSeek >= item.startTime / 1000 && playSeek < item.endTime / 1000) ||
|
||||
statusStore.lyricIndex === index
|
||||
? 'blur(0)'
|
||||
: `blur(${Math.min(Math.abs(statusStore.lyricIndex - index) * 1.8, 10)}px)`
|
||||
: 'blur(0)',
|
||||
}"
|
||||
@click="jumpSeek(item.time)"
|
||||
@click="jumpSeek(item.startTime)"
|
||||
>
|
||||
<!-- 歌词 -->
|
||||
<div class="content">
|
||||
<div
|
||||
v-for="(text, textIndex) in item.contents"
|
||||
v-for="(text, textIndex) in item.words"
|
||||
:key="textIndex"
|
||||
:class="{
|
||||
'content-text': true,
|
||||
'content-long':
|
||||
settingStore.showYrcLongEffect &&
|
||||
text.duration >= 1.5 &&
|
||||
playSeek <= text.endTime,
|
||||
'end-with-space': text.endsWithSpace,
|
||||
(text.endTime - item.startTime) / 1000 >= 1.5 &&
|
||||
playSeek <= text.endTime / 1000,
|
||||
'end-with-space': text.word.endsWith(' ') || text.startTime === 0,
|
||||
}"
|
||||
>
|
||||
<span class="word" :lang="getLyricLanguage(text.content)">
|
||||
{{ text.content }}
|
||||
<span class="word" :lang="getLyricLanguage(text.word)">
|
||||
{{ text.word }}
|
||||
</span>
|
||||
<span
|
||||
class="filler"
|
||||
:style="getYrcStyle(text, index)"
|
||||
:lang="getLyricLanguage(text.content)"
|
||||
:lang="getLyricLanguage(text.word)"
|
||||
>
|
||||
{{ text.content }}
|
||||
{{ text.word }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 翻译 -->
|
||||
<span v-if="item.tran && settingStore.showTran" class="tran" lang="en">
|
||||
{{ item.tran }}
|
||||
<span v-if="item.translatedLyric && settingStore.showTran" class="tran" lang="en">
|
||||
{{ item.translatedLyric }}
|
||||
</span>
|
||||
<!-- 音译 -->
|
||||
<span v-if="item.roma && settingStore.showRoma" class="roma" lang="en">
|
||||
{{ item.roma }}
|
||||
<span v-if="item.romanLyric && settingStore.showRoma" class="roma" lang="en">
|
||||
{{ item.romanLyric }}
|
||||
</span>
|
||||
<!-- 间奏倒计时 -->
|
||||
<div
|
||||
v-if="
|
||||
settingStore.countDownShow &&
|
||||
item.time > 0 &&
|
||||
musicStore.songLyric.yrcData[index + 1]?.time - item.endTime >= 10
|
||||
item.startTime > 0 &&
|
||||
((musicStore.songLyric.yrcData[index + 1]?.startTime || 0) - item.endTime) /
|
||||
1000 >=
|
||||
10
|
||||
"
|
||||
class="count-down-content"
|
||||
>
|
||||
<CountDown
|
||||
:start="item.endTime"
|
||||
:duration="musicStore.songLyric.yrcData[index + 1]?.time - item.endTime || 0"
|
||||
:start="item.endTime / 1000"
|
||||
:duration="
|
||||
((musicStore.songLyric.yrcData[index + 1]?.startTime || 0) - item.endTime) /
|
||||
1000
|
||||
"
|
||||
:seek="playSeek"
|
||||
:playing="statusStore.playStatus"
|
||||
/>
|
||||
@@ -125,7 +130,7 @@
|
||||
<!-- 倒计时 -->
|
||||
<CountDown
|
||||
:start="0"
|
||||
:duration="musicStore.songLyric.lrcData[0].time || 0"
|
||||
:duration="(musicStore.songLyric.lrcData[0].startTime || 0) / 1000"
|
||||
:seek="playSeek"
|
||||
:playing="statusStore.playStatus"
|
||||
/>
|
||||
@@ -140,17 +145,19 @@
|
||||
? `blur(${Math.min(Math.abs(statusStore.lyricIndex - index) * 1.8, 10)}px)`
|
||||
: 'blur(0)',
|
||||
}"
|
||||
@click="jumpSeek(item.time)"
|
||||
@click="jumpSeek(item.startTime)"
|
||||
>
|
||||
<!-- 歌词 -->
|
||||
<span class="content" :lang="getLyricLanguage(item.content)">{{ item.content }}</span>
|
||||
<span class="content" :lang="getLyricLanguage(item.words?.[0]?.word)">
|
||||
{{ item.words?.[0]?.word }}
|
||||
</span>
|
||||
<!-- 翻译 -->
|
||||
<span v-if="item.tran && settingStore.showTran" class="tran" lang="en">
|
||||
{{ item.tran }}
|
||||
<span v-if="item.translatedLyric && settingStore.showTran" class="tran" lang="en">
|
||||
{{ item.translatedLyric }}
|
||||
</span>
|
||||
<!-- 音译 -->
|
||||
<span v-if="item.roma && settingStore.showRoma" class="roma" lang="en">
|
||||
{{ item.roma }}
|
||||
<span v-if="item.romanLyric && settingStore.showRoma" class="roma" lang="en">
|
||||
{{ item.romanLyric }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="placeholder" />
|
||||
@@ -164,7 +171,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { LyricContentType } from "@/types/main";
|
||||
// import type { LyricContentType } from "@/types/main";
|
||||
import { LyricWord } from "@applemusic-like-lyrics/lyric";
|
||||
import { NScrollbar } from "naive-ui";
|
||||
import { useMusicStore, useSettingStore, useStatusStore } from "@/stores";
|
||||
import player from "@/utils/player";
|
||||
@@ -222,7 +230,7 @@ const INACTIVE_NO_ANIMATION_STYLE = { opacity: 0 } as const;
|
||||
* @param lyricIndex 歌词索引
|
||||
* @returns 逐字歌词动画样式
|
||||
*/
|
||||
const getYrcStyle = (wordData: LyricContentType, lyricIndex: number) => {
|
||||
const getYrcStyle = (wordData: LyricWord, lyricIndex: number) => {
|
||||
// 获取当前歌词行数据
|
||||
const currentLine = musicStore.songLyric.yrcData[lyricIndex];
|
||||
// 缓存 playSeek 值,避免多次访问响应式变量
|
||||
@@ -230,14 +238,14 @@ const getYrcStyle = (wordData: LyricContentType, lyricIndex: number) => {
|
||||
|
||||
// 判断当前行是否处于激活状态
|
||||
const isLineActive =
|
||||
(currentSeek >= currentLine.time && currentSeek < currentLine.endTime) ||
|
||||
(currentSeek >= currentLine.startTime / 1000 && currentSeek < currentLine.endTime / 1000) ||
|
||||
statusStore.lyricIndex === lyricIndex;
|
||||
|
||||
// 如果当前歌词行不是激活状态,返回固定样式,避免不必要的计算
|
||||
if (!isLineActive) {
|
||||
if (settingStore.showYrcAnimation) {
|
||||
// 判断单词是否已经唱过:已唱过保持填充状态(0%),未唱到保持未填充状态(100%)
|
||||
const hasPlayed = currentSeek >= wordData.time + wordData.duration;
|
||||
const hasPlayed = currentSeek >= wordData.endTime / 1000;
|
||||
return {
|
||||
WebkitMaskPositionX: hasPlayed ? "0%" : "100%",
|
||||
};
|
||||
@@ -249,24 +257,30 @@ const getYrcStyle = (wordData: LyricContentType, lyricIndex: number) => {
|
||||
// 激活状态的样式计算
|
||||
if (settingStore.showYrcAnimation) {
|
||||
// 如果播放状态不是加载中,且当前单词的时间加上持续时间减去播放进度大于 0
|
||||
if (statusStore.playLoading === false && wordData.time + wordData.duration - currentSeek > 0) {
|
||||
if (statusStore.playLoading === false && wordData.endTime / 1000 - currentSeek > 0) {
|
||||
return {
|
||||
transitionDuration: `0s, 0s, 0.35s`,
|
||||
transitionDelay: `0ms`,
|
||||
WebkitMaskPositionX: `${
|
||||
100 - Math.max(((currentSeek - wordData.time) / wordData.duration) * 100, 0)
|
||||
100 -
|
||||
Math.max(
|
||||
((currentSeek - wordData.startTime / 1000) /
|
||||
((wordData.endTime - wordData.startTime) / 1000)) *
|
||||
100,
|
||||
0,
|
||||
)
|
||||
}%`,
|
||||
};
|
||||
}
|
||||
// 预计算时间差,避免重复计算
|
||||
const timeDiff = wordData.time - currentSeek;
|
||||
const timeDiff = wordData.startTime - currentSeek * 1000;
|
||||
return {
|
||||
transitionDuration: `${wordData.duration}ms, ${wordData.duration * 0.8}ms, 0.35s`,
|
||||
transitionDelay: `${timeDiff}ms, ${timeDiff + wordData.duration * 0.5}ms, 0ms`,
|
||||
transitionDuration: `${wordData.endTime - wordData.startTime}ms, ${(wordData.endTime - wordData.startTime) * 0.8}ms, 0.35s`,
|
||||
transitionDelay: `${timeDiff}ms, ${timeDiff + (wordData.endTime - wordData.startTime) * 0.5}ms, 0ms`,
|
||||
};
|
||||
} else {
|
||||
// 无动画模式:根据单词时间判断透明度
|
||||
return statusStore.playLoading === false && wordData.time >= currentSeek
|
||||
return statusStore.playLoading === false && wordData.startTime / 1000 >= currentSeek
|
||||
? { opacity: 0 }
|
||||
: { opacity: 1 };
|
||||
}
|
||||
@@ -277,7 +291,7 @@ const jumpSeek = (time: number) => {
|
||||
if (!time) return;
|
||||
lrcMouseStatus.value = false;
|
||||
const offsetSeconds = statusStore.getSongOffset(musicStore.playSong?.id);
|
||||
player.setSeek(time - offsetSeconds);
|
||||
player.setSeek(time / 1000 - offsetSeconds);
|
||||
player.play();
|
||||
};
|
||||
|
||||
@@ -411,7 +425,7 @@ onBeforeUnmount(() => {
|
||||
opacity 0.3s,
|
||||
filter 0.3s,
|
||||
margin 0.3s,
|
||||
padding 0.3s !important;
|
||||
padding 0.3s;
|
||||
}
|
||||
&.end-with-space {
|
||||
margin-right: 12px;
|
||||
|
||||
@@ -275,9 +275,10 @@ const instantLyrics = computed(() => {
|
||||
const content = isYrc
|
||||
? musicStore.songLyric.yrcData[statusStore.lyricIndex]
|
||||
: musicStore.songLyric.lrcData[statusStore.lyricIndex];
|
||||
return content?.tran && settingStore.showTran
|
||||
? `${content?.content}( ${content?.tran} )`
|
||||
: content?.content;
|
||||
const contentStr = content?.words?.map((v) => v.word).join("") || "";
|
||||
return content?.translatedLyric && settingStore.showTran
|
||||
? `${contentStr}( ${content?.translatedLyric} )`
|
||||
: contentStr || "";
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import { defineStore } from "pinia";
|
||||
import type { LyricLine } from "@applemusic-like-lyrics/core";
|
||||
import type { SongType, LyricType } from "@/types/main";
|
||||
import type { SongType } from "@/types/main";
|
||||
import { isElectron } from "@/utils/env";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { SongLyric } from "@/types/lyric";
|
||||
|
||||
interface MusicState {
|
||||
playSong: SongType;
|
||||
playPlaylistId: number;
|
||||
songLyric: {
|
||||
lrcData: LyricType[];
|
||||
yrcData: LyricType[];
|
||||
lrcAMData: LyricLine[];
|
||||
yrcAMData: LyricLine[];
|
||||
};
|
||||
songLyric: SongLyric;
|
||||
personalFM: {
|
||||
playIndex: number;
|
||||
list: SongType[];
|
||||
@@ -46,8 +41,6 @@ export const useMusicStore = defineStore("music", {
|
||||
songLyric: {
|
||||
lrcData: [], // 普通歌词
|
||||
yrcData: [], // 逐字歌词
|
||||
lrcAMData: [], // 普通歌词-AM
|
||||
yrcAMData: [], // 逐字歌词-AM
|
||||
},
|
||||
// 私人FM数据
|
||||
personalFM: {
|
||||
@@ -88,32 +81,23 @@ export const useMusicStore = defineStore("music", {
|
||||
// 恢复默认音乐数据
|
||||
resetMusicData() {
|
||||
this.playSong = { ...defaultMusicData };
|
||||
this.songLyric = {
|
||||
lrcData: [],
|
||||
yrcData: [],
|
||||
lrcAMData: [],
|
||||
yrcAMData: [],
|
||||
};
|
||||
this.songLyric = { lrcData: [], yrcData: [] };
|
||||
},
|
||||
/**
|
||||
* 设置/更新歌曲歌词数据
|
||||
* @param updates 部分或完整歌词数据
|
||||
* @param replace 是否覆盖(true:用提供的数据覆盖并为缺省字段置空;false:合并更新)
|
||||
*/
|
||||
setSongLyric(updates: Partial<MusicState["songLyric"]>, replace: boolean = false) {
|
||||
setSongLyric(updates: Partial<SongLyric>, replace: boolean = false) {
|
||||
if (replace) {
|
||||
this.songLyric = {
|
||||
lrcData: updates.lrcData ?? [],
|
||||
yrcData: updates.yrcData ?? [],
|
||||
lrcAMData: updates.lrcAMData ?? [],
|
||||
yrcAMData: updates.yrcAMData ?? [],
|
||||
};
|
||||
} else {
|
||||
this.songLyric = {
|
||||
lrcData: updates.lrcData ?? this.songLyric.lrcData,
|
||||
yrcData: updates.yrcData ?? this.songLyric.yrcData,
|
||||
lrcAMData: updates.lrcAMData ?? this.songLyric.lrcAMData,
|
||||
yrcAMData: updates.yrcAMData ?? this.songLyric.yrcAMData,
|
||||
};
|
||||
}
|
||||
// 更新歌词窗口数据
|
||||
|
||||
8
src/types/desktop-lyric.d.ts
vendored
8
src/types/desktop-lyric.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
import { LyricType } from "@/types/main";
|
||||
import { type LyricLine } from "@applemusic-like-lyrics/lyric";
|
||||
|
||||
/** 桌面歌词数据 */
|
||||
export interface LyricData {
|
||||
@@ -13,8 +13,8 @@ export interface LyricData {
|
||||
/** 当前歌曲的时间偏移(秒,正负均可) */
|
||||
songOffset?: number;
|
||||
/** 歌词数据 */
|
||||
lrcData?: LyricType[];
|
||||
yrcData?: LyricType[];
|
||||
lrcData?: LyricLine[];
|
||||
yrcData?: LyricLine[];
|
||||
/** 歌词播放索引 */
|
||||
lyricIndex?: number;
|
||||
}
|
||||
@@ -52,7 +52,7 @@ export interface LyricConfig {
|
||||
*/
|
||||
export interface RenderLine {
|
||||
/** 当前整行歌词数据(用于逐字渲染) */
|
||||
line: LyricType;
|
||||
line: LyricLine;
|
||||
/** 当前行在歌词数组中的索引 */
|
||||
index: number;
|
||||
/** 唯一键 */
|
||||
|
||||
1015
src/utils/lyric.ts
1015
src/utils/lyric.ts
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,13 @@
|
||||
import { useStatusStore, useMusicStore, useSettingStore } from "@/stores";
|
||||
import { songLyric, songLyricTTML } from "@/api/song";
|
||||
import { type SongLyric } from "@/types/lyric";
|
||||
import { type LyricLine, parseLrc, parseTTML, parseYrc } from "@applemusic-like-lyrics/lyric";
|
||||
import {
|
||||
type LyricLine,
|
||||
LyricWord,
|
||||
parseLrc,
|
||||
parseTTML,
|
||||
parseYrc,
|
||||
} from "@applemusic-like-lyrics/lyric";
|
||||
import { isElectron } from "./env";
|
||||
import { isEmpty } from "lodash-es";
|
||||
|
||||
@@ -104,8 +110,7 @@ class LyricManager {
|
||||
const settingStore = useSettingStore();
|
||||
// 请求序列
|
||||
const req = this.activeLyricReq;
|
||||
// 请求是否成功
|
||||
let adopted = false;
|
||||
// 最终结果
|
||||
let result: SongLyric = { lrcData: [], yrcData: [] };
|
||||
// 过期判断
|
||||
const isStale = () => this.activeLyricReq !== req || musicStore.playSong?.id !== id;
|
||||
@@ -119,8 +124,7 @@ class LyricManager {
|
||||
const parsed = parseTTML(ttmlContent);
|
||||
const lines = parsed?.lines || [];
|
||||
if (!lines.length) return;
|
||||
result = { lrcData: [], yrcData: lines };
|
||||
adopted = true;
|
||||
result.yrcData = lines;
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
@@ -153,9 +157,11 @@ class LyricManager {
|
||||
if (data?.yromalrc?.lyric)
|
||||
yrcLines = this.alignLyrics(yrcLines, parseLrc(data.yromalrc.lyric), "romanLyric");
|
||||
}
|
||||
if (adopted) return;
|
||||
result = { lrcData: lrcLines, yrcData: yrcLines };
|
||||
adopted = true;
|
||||
if (lrcLines.length) result.lrcData = lrcLines;
|
||||
// 如果没有 TTML,则采用 网易云 YRC
|
||||
if (!result.yrcData.length && yrcLines.length) {
|
||||
result.yrcData = yrcLines;
|
||||
}
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
@@ -183,7 +189,19 @@ class LyricManager {
|
||||
const ttml = parseTTML(lyric);
|
||||
const lines = ttml?.lines || [];
|
||||
statusStore.usingTTMLLyric = true;
|
||||
return { lrcData: [], yrcData: lines };
|
||||
// 构成普通歌词
|
||||
const lrcLines: LyricLine[] = lines.map((line) => ({
|
||||
...line,
|
||||
words: [
|
||||
{
|
||||
word: line.words?.map((w) => w.word)?.join("") || "",
|
||||
startTime: line.startTime || 0,
|
||||
endTime: line.endTime || 0,
|
||||
romanWord: line.words?.map((w) => w.romanWord)?.join("") || "",
|
||||
},
|
||||
] as LyricWord[],
|
||||
}));
|
||||
return { lrcData: lrcLines, yrcData: lines };
|
||||
}
|
||||
// 解析本地歌词并对其
|
||||
const lrcLines = parseLrc(lyric);
|
||||
@@ -200,7 +218,6 @@ class LyricManager {
|
||||
* @returns 歌词数据
|
||||
*/
|
||||
private async checkLocalLyricOverride(id: number): Promise<SongLyric> {
|
||||
console.log("检测本地歌词覆盖", id);
|
||||
const statusStore = useStatusStore();
|
||||
const settingStore = useSettingStore();
|
||||
const { localLyricPath } = settingStore;
|
||||
@@ -222,6 +239,7 @@ class LyricManager {
|
||||
const lrcContent = typeof lrc === "string" ? lrc : "";
|
||||
if (lrcContent) {
|
||||
lrcLines = parseLrc(lrcContent);
|
||||
console.log("检测到本地歌词覆盖", lrcLines);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("parseLrc 本地解析失败:", err);
|
||||
@@ -232,6 +250,7 @@ class LyricManager {
|
||||
const ttmlContent = typeof ttml === "string" ? ttml : "";
|
||||
if (ttmlContent) {
|
||||
ttmlLines = parseTTML(ttmlContent).lines || [];
|
||||
console.log("检测到本地TTML歌词覆盖", ttmlLines);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("parseTTML 本地解析失败:", err);
|
||||
@@ -295,14 +314,10 @@ class LyricManager {
|
||||
try {
|
||||
// 歌词加载状态
|
||||
statusStore.lyricLoading = true;
|
||||
// 重置歌词
|
||||
this.resetSongLyric();
|
||||
// 标记当前歌词请求(避免旧请求覆盖新请求)
|
||||
this.activeLyricReq = ++this.lyricReqSeq;
|
||||
// 检查歌词覆盖
|
||||
let lyricData = await this.checkLocalLyricOverride(id);
|
||||
console.log("本地歌词覆盖", lyricData);
|
||||
|
||||
// 开始获取歌词
|
||||
if (!isEmpty(lyricData.lrcData) || !isEmpty(lyricData.yrcData)) {
|
||||
// 进行本地歌词对齐
|
||||
@@ -314,8 +329,13 @@ class LyricManager {
|
||||
}
|
||||
// 排除内容
|
||||
lyricData = this.handleLyricExclude(lyricData);
|
||||
// 设置歌词
|
||||
musicStore.setSongLyric(lyricData, true);
|
||||
console.log("最终歌词数据", lyricData);
|
||||
} catch (error) {
|
||||
console.error("❌ 处理歌词失败:", error);
|
||||
// 重置歌词
|
||||
this.resetSongLyric();
|
||||
} finally {
|
||||
// 歌词加载状态
|
||||
if (musicStore.playSong?.id === undefined || this.activeLyricReq === this.lyricReqSeq) {
|
||||
|
||||
@@ -1,173 +1,173 @@
|
||||
import { useMusicStore, useSettingStore, useStatusStore } from "@/stores";
|
||||
import {
|
||||
parsedLyricsData,
|
||||
parseLocalLyric,
|
||||
parseTTMLToAMLL,
|
||||
parseTTMLToYrc,
|
||||
resetSongLyric,
|
||||
} from "../lyric";
|
||||
import { songLyric, songLyricTTML } from "@/api/song";
|
||||
import { parseTTML } from "@applemusic-like-lyrics/lyric";
|
||||
import { LyricLine } from "@applemusic-like-lyrics/core";
|
||||
import { LyricType } from "@/types/main";
|
||||
// import { useMusicStore, useSettingStore, useStatusStore } from "@/stores";
|
||||
// import {
|
||||
// parsedLyricsData,
|
||||
// parseLocalLyric,
|
||||
// parseTTMLToAMLL,
|
||||
// parseTTMLToYrc,
|
||||
// resetSongLyric,
|
||||
// } from "../lyric";
|
||||
// import { songLyric, songLyricTTML } from "@/api/song";
|
||||
// import { parseTTML } from "@applemusic-like-lyrics/lyric";
|
||||
// import { LyricLine } from "@applemusic-like-lyrics/core";
|
||||
// import { LyricType } from "@/types/main";
|
||||
|
||||
/**
|
||||
* 获取歌词
|
||||
* @param id 歌曲id
|
||||
*/
|
||||
export const getLyricData = async (id: number) => {
|
||||
const musicStore = useMusicStore();
|
||||
const settingStore = useSettingStore();
|
||||
const statusStore = useStatusStore();
|
||||
// 切歌或重新获取时,先标记为加载中
|
||||
statusStore.lyricLoading = true;
|
||||
// /**
|
||||
// * 获取歌词
|
||||
// * @param id 歌曲id
|
||||
// */
|
||||
// export const getLyricData = async (id: number) => {
|
||||
// const musicStore = useMusicStore();
|
||||
// const settingStore = useSettingStore();
|
||||
// const statusStore = useStatusStore();
|
||||
// // 切歌或重新获取时,先标记为加载中
|
||||
// statusStore.lyricLoading = true;
|
||||
|
||||
if (!id) {
|
||||
statusStore.usingTTMLLyric = false;
|
||||
resetSongLyric();
|
||||
statusStore.lyricLoading = false;
|
||||
return;
|
||||
}
|
||||
// if (!id) {
|
||||
// statusStore.usingTTMLLyric = false;
|
||||
// resetSongLyric();
|
||||
// statusStore.lyricLoading = false;
|
||||
// return;
|
||||
// }
|
||||
|
||||
try {
|
||||
// 检测本地歌词覆盖
|
||||
const getLyric = getLyricFun(settingStore.localLyricPath, id);
|
||||
// try {
|
||||
// // 检测本地歌词覆盖
|
||||
// const getLyric = getLyricFun(settingStore.localLyricPath, id);
|
||||
|
||||
// 并发请求:如果 TTML 先到并且有效,则直接采用 TTML,不再等待或覆盖为 LRC
|
||||
const lrcPromise = getLyric("lrc", songLyric);
|
||||
// 这里的第二个 getLyric 方法不传入第二个参数(在线获取函数)表明不进行在线获取,仅获取本地
|
||||
const ttmlPromise = settingStore.enableTTMLLyric
|
||||
? getLyric("ttml", songLyricTTML)
|
||||
: getLyric("ttml");
|
||||
// // 并发请求:如果 TTML 先到并且有效,则直接采用 TTML,不再等待或覆盖为 LRC
|
||||
// const lrcPromise = getLyric("lrc", songLyric);
|
||||
// // 这里的第二个 getLyric 方法不传入第二个参数(在线获取函数)表明不进行在线获取,仅获取本地
|
||||
// const ttmlPromise = settingStore.enableTTMLLyric
|
||||
// ? getLyric("ttml", songLyricTTML)
|
||||
// : getLyric("ttml");
|
||||
|
||||
let settled = false; // 是否已采用某一种歌词并结束加载状态
|
||||
let ttmlAdopted = false; // 是否已采用 TTML
|
||||
// let settled = false; // 是否已采用某一种歌词并结束加载状态
|
||||
// let ttmlAdopted = false; // 是否已采用 TTML
|
||||
|
||||
const adoptTTML = async () => {
|
||||
if (!ttmlPromise) {
|
||||
statusStore.usingTTMLLyric = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { lyric: ttmlContent, isLocal: ttmlLocal } = await ttmlPromise;
|
||||
if (!ttmlContent) {
|
||||
statusStore.usingTTMLLyric = false;
|
||||
return;
|
||||
}
|
||||
// 本地 TTML 使用 parseLocalLyric,在线 TTML 使用原有解析方式
|
||||
if (ttmlLocal) {
|
||||
parseLocalLyric(ttmlContent, "ttml");
|
||||
statusStore.usingTTMLLyric = true;
|
||||
ttmlAdopted = true;
|
||||
if (!settled) {
|
||||
statusStore.lyricLoading = false;
|
||||
settled = true;
|
||||
}
|
||||
console.log("✅ TTML lyrics adopted (prefer TTML)");
|
||||
return;
|
||||
}
|
||||
// 在线 TTML 解析
|
||||
const parsedResult = parseTTML(ttmlContent);
|
||||
if (!parsedResult?.lines?.length) {
|
||||
statusStore.usingTTMLLyric = false;
|
||||
return;
|
||||
}
|
||||
const skipExcludeLocal = ttmlLocal && !settingStore.enableExcludeLocalLyrics;
|
||||
const skipExcludeTTML = !settingStore.enableExcludeTTML;
|
||||
const skipExclude = skipExcludeLocal || skipExcludeTTML;
|
||||
const ttmlLyric = parseTTMLToAMLL(parsedResult, skipExclude);
|
||||
const ttmlYrcLyric = parseTTMLToYrc(parsedResult, skipExclude);
|
||||
// const adoptTTML = async () => {
|
||||
// if (!ttmlPromise) {
|
||||
// statusStore.usingTTMLLyric = false;
|
||||
// return;
|
||||
// }
|
||||
// try {
|
||||
// const { lyric: ttmlContent, isLocal: ttmlLocal } = await ttmlPromise;
|
||||
// if (!ttmlContent) {
|
||||
// statusStore.usingTTMLLyric = false;
|
||||
// return;
|
||||
// }
|
||||
// // 本地 TTML 使用 parseLocalLyric,在线 TTML 使用原有解析方式
|
||||
// if (ttmlLocal) {
|
||||
// parseLocalLyric(ttmlContent, "ttml");
|
||||
// statusStore.usingTTMLLyric = true;
|
||||
// ttmlAdopted = true;
|
||||
// if (!settled) {
|
||||
// statusStore.lyricLoading = false;
|
||||
// settled = true;
|
||||
// }
|
||||
// console.log("✅ TTML lyrics adopted (prefer TTML)");
|
||||
// return;
|
||||
// }
|
||||
// // 在线 TTML 解析
|
||||
// const parsedResult = parseTTML(ttmlContent);
|
||||
// if (!parsedResult?.lines?.length) {
|
||||
// statusStore.usingTTMLLyric = false;
|
||||
// return;
|
||||
// }
|
||||
// const skipExcludeLocal = ttmlLocal && !settingStore.enableExcludeLocalLyrics;
|
||||
// const skipExcludeTTML = !settingStore.enableExcludeTTML;
|
||||
// const skipExclude = skipExcludeLocal || skipExcludeTTML;
|
||||
// const ttmlLyric = parseTTMLToAMLL(parsedResult, skipExclude);
|
||||
// const ttmlYrcLyric = parseTTMLToYrc(parsedResult, skipExclude);
|
||||
|
||||
const updates: Partial<{
|
||||
yrcAMData: LyricLine[];
|
||||
yrcData: LyricType[];
|
||||
lrcData: LyricType[];
|
||||
lrcAMData: LyricLine[];
|
||||
}> = {};
|
||||
if (ttmlLyric?.length) {
|
||||
updates.yrcAMData = ttmlLyric;
|
||||
// 若当前无 LRC-AM 数据,使用 TTML-AM 作为回退
|
||||
if (!musicStore.songLyric.lrcAMData?.length) {
|
||||
updates.lrcAMData = ttmlLyric;
|
||||
}
|
||||
}
|
||||
if (ttmlYrcLyric?.length) {
|
||||
updates.yrcData = ttmlYrcLyric;
|
||||
// 若当前无 LRC 数据,使用 TTML 行级数据作为回退
|
||||
if (!musicStore.songLyric.lrcData?.length) {
|
||||
updates.lrcData = ttmlYrcLyric;
|
||||
}
|
||||
}
|
||||
// const updates: Partial<{
|
||||
// yrcAMData: LyricLine[];
|
||||
// yrcData: LyricType[];
|
||||
// lrcData: LyricType[];
|
||||
// lrcAMData: LyricLine[];
|
||||
// }> = {};
|
||||
// if (ttmlLyric?.length) {
|
||||
// updates.yrcAMData = ttmlLyric;
|
||||
// // 若当前无 LRC-AM 数据,使用 TTML-AM 作为回退
|
||||
// if (!musicStore.songLyric.lrcAMData?.length) {
|
||||
// updates.lrcAMData = ttmlLyric;
|
||||
// }
|
||||
// }
|
||||
// if (ttmlYrcLyric?.length) {
|
||||
// updates.yrcData = ttmlYrcLyric;
|
||||
// // 若当前无 LRC 数据,使用 TTML 行级数据作为回退
|
||||
// if (!musicStore.songLyric.lrcData?.length) {
|
||||
// updates.lrcData = ttmlYrcLyric;
|
||||
// }
|
||||
// }
|
||||
|
||||
if (Object.keys(updates).length) {
|
||||
musicStore.setSongLyric(updates);
|
||||
statusStore.usingTTMLLyric = true;
|
||||
ttmlAdopted = true;
|
||||
if (!settled) {
|
||||
statusStore.lyricLoading = false;
|
||||
settled = true;
|
||||
}
|
||||
console.log("✅ TTML lyrics adopted (prefer TTML)");
|
||||
} else {
|
||||
statusStore.usingTTMLLyric = false;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("❌ Error loading TTML lyrics:", err);
|
||||
statusStore.usingTTMLLyric = false;
|
||||
}
|
||||
};
|
||||
// if (Object.keys(updates).length) {
|
||||
// musicStore.setSongLyric(updates);
|
||||
// statusStore.usingTTMLLyric = true;
|
||||
// ttmlAdopted = true;
|
||||
// if (!settled) {
|
||||
// statusStore.lyricLoading = false;
|
||||
// settled = true;
|
||||
// }
|
||||
// console.log("✅ TTML lyrics adopted (prefer TTML)");
|
||||
// } else {
|
||||
// statusStore.usingTTMLLyric = false;
|
||||
// }
|
||||
// } catch (err) {
|
||||
// console.error("❌ Error loading TTML lyrics:", err);
|
||||
// statusStore.usingTTMLLyric = false;
|
||||
// }
|
||||
// };
|
||||
|
||||
const adoptLRC = async () => {
|
||||
try {
|
||||
const { lyric: lyricRes, isLocal: lyricLocal } = await lrcPromise;
|
||||
// 如果 TTML 已采用,则忽略 LRC
|
||||
if (ttmlAdopted) return;
|
||||
// 如果没有歌词内容,直接返回
|
||||
if (!lyricRes) return;
|
||||
// 本地歌词使用 parseLocalLyric,在线歌词使用 parsedLyricsData
|
||||
if (lyricLocal) {
|
||||
parseLocalLyric(lyricRes, "lrc");
|
||||
} else {
|
||||
parsedLyricsData(lyricRes, !settingStore.enableExcludeLocalLyrics);
|
||||
}
|
||||
statusStore.usingTTMLLyric = false;
|
||||
if (!settled) {
|
||||
statusStore.lyricLoading = false;
|
||||
settled = true;
|
||||
}
|
||||
console.log("✅ LRC lyrics adopted");
|
||||
} catch (err) {
|
||||
console.error("❌ Error loading LRC lyrics:", err);
|
||||
if (!settled) statusStore.lyricLoading = false;
|
||||
}
|
||||
};
|
||||
// const adoptLRC = async () => {
|
||||
// try {
|
||||
// const { lyric: lyricRes, isLocal: lyricLocal } = await lrcPromise;
|
||||
// // 如果 TTML 已采用,则忽略 LRC
|
||||
// if (ttmlAdopted) return;
|
||||
// // 如果没有歌词内容,直接返回
|
||||
// if (!lyricRes) return;
|
||||
// // 本地歌词使用 parseLocalLyric,在线歌词使用 parsedLyricsData
|
||||
// if (lyricLocal) {
|
||||
// parseLocalLyric(lyricRes, "lrc");
|
||||
// } else {
|
||||
// parsedLyricsData(lyricRes, !settingStore.enableExcludeLocalLyrics);
|
||||
// }
|
||||
// statusStore.usingTTMLLyric = false;
|
||||
// if (!settled) {
|
||||
// statusStore.lyricLoading = false;
|
||||
// settled = true;
|
||||
// }
|
||||
// console.log("✅ LRC lyrics adopted");
|
||||
// } catch (err) {
|
||||
// console.error("❌ Error loading LRC lyrics:", err);
|
||||
// if (!settled) statusStore.lyricLoading = false;
|
||||
// }
|
||||
// };
|
||||
|
||||
// 启动并发任务:TTML 与 LRC 同时进行,哪个先成功就先用
|
||||
void adoptLRC();
|
||||
void adoptTTML();
|
||||
} catch (error) {
|
||||
console.error("❌ Error loading lyrics:", error);
|
||||
statusStore.usingTTMLLyric = false;
|
||||
resetSongLyric();
|
||||
statusStore.lyricLoading = false;
|
||||
}
|
||||
};
|
||||
// // 启动并发任务:TTML 与 LRC 同时进行,哪个先成功就先用
|
||||
// void adoptLRC();
|
||||
// void adoptTTML();
|
||||
// } catch (error) {
|
||||
// console.error("❌ Error loading lyrics:", error);
|
||||
// statusStore.usingTTMLLyric = false;
|
||||
// resetSongLyric();
|
||||
// statusStore.lyricLoading = false;
|
||||
// }
|
||||
// };
|
||||
|
||||
/**
|
||||
* 获取歌词函数生成器
|
||||
* @param paths 本地歌词路径数组
|
||||
* @param id 歌曲ID
|
||||
* @returns 返回一个函数,该函数接受扩展名和在线获取函数作为参数
|
||||
*/
|
||||
const getLyricFun =
|
||||
(paths: string[], id: number) =>
|
||||
async (
|
||||
ext: string,
|
||||
getOnline?: (id: number) => Promise<string | null>,
|
||||
): Promise<{ lyric: string | null; isLocal: boolean }> => {
|
||||
for (const path of paths) {
|
||||
const lyric = await window.electron.ipcRenderer.invoke("read-local-lyric", path, id, ext);
|
||||
if (lyric) return { lyric, isLocal: true };
|
||||
}
|
||||
return { lyric: getOnline ? await getOnline(id) : null, isLocal: false };
|
||||
};
|
||||
// /**
|
||||
// * 获取歌词函数生成器
|
||||
// * @param paths 本地歌词路径数组
|
||||
// * @param id 歌曲ID
|
||||
// * @returns 返回一个函数,该函数接受扩展名和在线获取函数作为参数
|
||||
// */
|
||||
// const getLyricFun =
|
||||
// (paths: string[], id: number) =>
|
||||
// async (
|
||||
// ext: string,
|
||||
// getOnline?: (id: number) => Promise<string | null>,
|
||||
// ): Promise<{ lyric: string | null; isLocal: boolean }> => {
|
||||
// for (const path of paths) {
|
||||
// const lyric = await window.electron.ipcRenderer.invoke("read-local-lyric", path, id, ext);
|
||||
// if (lyric) return { lyric, isLocal: true };
|
||||
// }
|
||||
// return { lyric: getOnline ? await getOnline(id) : null, isLocal: false };
|
||||
// };
|
||||
|
||||
@@ -3,7 +3,8 @@ import type { MessageReactive } from "naive-ui";
|
||||
import { Howl, Howler } from "howler";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { useMusicStore, useStatusStore, useDataStore, useSettingStore } from "@/stores";
|
||||
import { resetSongLyric, parseLocalLyric, calculateLyricIndex } from "./lyric";
|
||||
// import { resetSongLyric, parseLocalLyric, calculateLyricIndex } from "./lyric";
|
||||
import { calculateLyricIndex } from "./lyric";
|
||||
import { calculateProgress } from "./time";
|
||||
import { shuffleArray, runIdle } from "./helper";
|
||||
import { heartRateList } from "@/api/playlist";
|
||||
@@ -19,7 +20,7 @@ import {
|
||||
getUnlockSongUrl,
|
||||
} from "./player-utils/song";
|
||||
import { isDev, isElectron } from "./env";
|
||||
import { getLyricData } from "./player-utils/lyric";
|
||||
// import { getLyricData } from "./player-utils/lyric";
|
||||
import audioContextManager from "@/utils/player-utils/context";
|
||||
import lyricManager from "./lyricManager";
|
||||
import blob from "./blob";
|
||||
@@ -246,9 +247,9 @@ class Player {
|
||||
if (!settingStore.showSpectrums) this.toggleOutputDevice();
|
||||
// 自动播放
|
||||
if (autoPlay) await this.play();
|
||||
// 获取歌曲附加信息 - 非电台和本地
|
||||
if (type !== "radio" && !path) getLyricData(id);
|
||||
else resetSongLyric();
|
||||
// // 获取歌曲附加信息 - 非电台和本地
|
||||
// if (type !== "radio" && !path) getLyricData(id);
|
||||
// else resetSongLyric();
|
||||
// 获取歌词数据
|
||||
lyricManager.handleLyric(id, path);
|
||||
// 定时获取状态
|
||||
@@ -549,8 +550,8 @@ class Player {
|
||||
// 获取主色
|
||||
runIdle(() => getCoverColor(musicStore.playSong.cover));
|
||||
// 获取歌词数据
|
||||
const { lyric, format } = await window.electron.ipcRenderer.invoke("get-music-lyric", path);
|
||||
parseLocalLyric(lyric, format);
|
||||
// const { lyric, format } = await window.electron.ipcRenderer.invoke("get-music-lyric", path);
|
||||
// parseLocalLyric(lyric, format);
|
||||
// 更新媒体会话
|
||||
this.updateMediaSession();
|
||||
} catch (error) {
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
'lyric-line',
|
||||
{
|
||||
active: line.active,
|
||||
'is-yrc': Boolean(lyricData?.yrcData?.length && line.line?.contents?.length),
|
||||
'is-yrc': Boolean(lyricData?.yrcData?.length && line.line?.words?.length > 1),
|
||||
},
|
||||
]"
|
||||
:style="{
|
||||
@@ -73,7 +73,7 @@
|
||||
>
|
||||
<!-- 逐字歌词渲染 -->
|
||||
<template
|
||||
v-if="lyricConfig.showYrc && lyricData?.yrcData?.length && line.line?.contents?.length"
|
||||
v-if="lyricConfig.showYrc && lyricData?.yrcData?.length && line.line?.words?.length > 1"
|
||||
>
|
||||
<span
|
||||
class="scroll-content"
|
||||
@@ -82,21 +82,21 @@
|
||||
>
|
||||
<span class="content">
|
||||
<span
|
||||
v-for="(text, textIndex) in line.line.contents"
|
||||
v-for="(text, textIndex) in line.line.words"
|
||||
:key="textIndex"
|
||||
:class="{
|
||||
'content-text': true,
|
||||
'end-with-space': text.endsWithSpace,
|
||||
'end-with-space': text.word.endsWith(' ') || text.startTime === 0,
|
||||
}"
|
||||
>
|
||||
<span class="word" :style="{ color: lyricConfig.unplayedColor }">
|
||||
{{ text.content }}
|
||||
{{ text.word }}
|
||||
</span>
|
||||
<span
|
||||
class="filler"
|
||||
:style="[{ color: lyricConfig.playedColor }, getYrcStyle(text, line.index)]"
|
||||
>
|
||||
{{ text.content }}
|
||||
{{ text.word }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
@@ -109,7 +109,7 @@
|
||||
:style="getScrollStyle(line)"
|
||||
:ref="(el) => line.active && (currentContentRef = el as HTMLElement)"
|
||||
>
|
||||
{{ line.line?.content }}
|
||||
{{ line.line?.words?.[0]?.word || "" }}
|
||||
</span>
|
||||
</template>
|
||||
</span>
|
||||
@@ -122,7 +122,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRafFn } from "@vueuse/core";
|
||||
import { LyricContentType, LyricType } from "@/types/main";
|
||||
import { LyricLine, LyricWord } from "@applemusic-like-lyrics/lyric";
|
||||
import { LyricConfig, LyricData, RenderLine } from "@/types/desktop-lyric";
|
||||
import defaultDesktopLyricConfig from "@/assets/data/lyricConfig";
|
||||
|
||||
@@ -192,13 +192,13 @@ const handleMouseMove = () => {
|
||||
* @param idx 当前行索引
|
||||
* @returns 安全的结束时间(秒)
|
||||
*/
|
||||
const getSafeEndTime = (lyrics: LyricType[], idx: number) => {
|
||||
const getSafeEndTime = (lyrics: LyricLine[], idx: number) => {
|
||||
const cur = lyrics?.[idx];
|
||||
const next = lyrics?.[idx + 1];
|
||||
const curEnd = Number(cur?.endTime);
|
||||
const curStart = Number(cur?.time);
|
||||
const curStart = Number(cur?.startTime);
|
||||
if (Number.isFinite(curEnd) && curEnd > curStart) return curEnd;
|
||||
const nextStart = Number(next?.time);
|
||||
const nextStart = Number(next?.startTime);
|
||||
if (Number.isFinite(nextStart) && nextStart > curStart) return nextStart;
|
||||
// 无有效结束参照:返回 0(表示无时长,不滚动)
|
||||
return 0;
|
||||
@@ -213,19 +213,35 @@ const renderLyricLines = computed<RenderLine[]>(() => {
|
||||
if (!lyrics?.length) {
|
||||
return [
|
||||
{
|
||||
line: { time: 0, endTime: 0, content: "纯音乐,请欣赏", contents: [] },
|
||||
line: {
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
words: [{ word: "纯音乐,请欣赏", startTime: 0, endTime: 0, romanWord: "" }],
|
||||
translatedLyric: "",
|
||||
romanLyric: "",
|
||||
isBG: false,
|
||||
isDuet: false,
|
||||
},
|
||||
index: -1,
|
||||
key: "placeholder",
|
||||
active: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
let idx = lyricData?.lyricIndex ?? -1;
|
||||
// 显示歌名
|
||||
const idx = lyricData?.lyricIndex ?? -1;
|
||||
if (idx < 0) {
|
||||
const text = lyricData.playName ?? "未知歌曲";
|
||||
return [
|
||||
{
|
||||
line: { time: 0, endTime: 0, content: lyricData.playName ?? "未知歌曲", contents: [] },
|
||||
line: {
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
words: [{ word: text, startTime: 0, endTime: 0, romanWord: "" }],
|
||||
translatedLyric: "",
|
||||
romanLyric: "",
|
||||
isBG: false,
|
||||
isDuet: false,
|
||||
},
|
||||
index: -1,
|
||||
key: "placeholder",
|
||||
active: true,
|
||||
@@ -236,39 +252,79 @@ const renderLyricLines = computed<RenderLine[]>(() => {
|
||||
const next = lyrics[idx + 1];
|
||||
if (!current) return [];
|
||||
const safeEnd = getSafeEndTime(lyrics, idx);
|
||||
// 有翻译:保留第二行显示翻译,第一行显示原文(逐字由 contents 驱动)
|
||||
if (lyricConfig.showTran && current.tran && current.tran.trim().length > 0) {
|
||||
if (
|
||||
lyricConfig.showTran &&
|
||||
current.translatedLyric &&
|
||||
current.translatedLyric.trim().length > 0
|
||||
) {
|
||||
const lines: RenderLine[] = [
|
||||
{ line: { ...current, endTime: safeEnd }, index: idx, key: `${idx}:orig`, active: true },
|
||||
{
|
||||
line: { time: current.time, endTime: safeEnd, content: current.tran, contents: [] },
|
||||
line: {
|
||||
startTime: current.startTime,
|
||||
endTime: safeEnd,
|
||||
words: [
|
||||
{
|
||||
word: current.translatedLyric,
|
||||
startTime: current.startTime,
|
||||
endTime: safeEnd,
|
||||
romanWord: "",
|
||||
},
|
||||
],
|
||||
translatedLyric: "",
|
||||
romanLyric: "",
|
||||
isBG: false,
|
||||
isDuet: false,
|
||||
},
|
||||
index: idx,
|
||||
key: `${idx}:tran`,
|
||||
active: false,
|
||||
},
|
||||
];
|
||||
return lines.filter((l) => l.line?.content && l.line.content.trim().length > 0);
|
||||
return lines.filter((l) => {
|
||||
const s = (l.line?.words || [])
|
||||
.map((w) => w.word)
|
||||
.join("")
|
||||
.trim();
|
||||
return s.length > 0;
|
||||
});
|
||||
}
|
||||
// 单行:仅当前句原文,高亮
|
||||
if (!lyricConfig.isDoubleLine) {
|
||||
return [
|
||||
{ line: { ...current, endTime: safeEnd }, index: idx, key: `${idx}:orig`, active: true },
|
||||
].filter((l) => l.line?.content && l.line.content.trim().length > 0);
|
||||
].filter((l) => {
|
||||
const s = (l.line?.words || [])
|
||||
.map((w) => w.word)
|
||||
.join("")
|
||||
.trim();
|
||||
return s.length > 0;
|
||||
});
|
||||
}
|
||||
// 双行交替:只高亮当前句所在行
|
||||
const isEven = idx % 2 === 0;
|
||||
if (isEven) {
|
||||
const lines: RenderLine[] = [
|
||||
{ line: { ...current, endTime: safeEnd }, index: idx, key: `${idx}:orig`, active: true },
|
||||
...(next ? [{ line: next, index: idx + 1, key: `${idx + 1}:next`, active: false }] : []),
|
||||
];
|
||||
return lines.filter((l) => l.line?.content && l.line.content.trim().length > 0);
|
||||
return lines.filter((l) => {
|
||||
const s = (l.line?.words || [])
|
||||
.map((w) => w.word)
|
||||
.join("")
|
||||
.trim();
|
||||
return s.length > 0;
|
||||
});
|
||||
}
|
||||
const lines: RenderLine[] = [
|
||||
...(next ? [{ line: next, index: idx + 1, key: `${idx + 1}:next`, active: false }] : []),
|
||||
{ line: { ...current, endTime: safeEnd }, index: idx, key: `${idx}:orig`, active: true },
|
||||
];
|
||||
return lines.filter((l) => l.line?.content && l.line.content.trim().length > 0);
|
||||
return lines.filter((l) => {
|
||||
const s = (l.line?.words || [])
|
||||
.map((w) => w.word)
|
||||
.join("")
|
||||
.trim();
|
||||
return s.length > 0;
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -276,21 +332,24 @@ const renderLyricLines = computed<RenderLine[]>(() => {
|
||||
* @param wordData 逐字歌词数据
|
||||
* @param lyricIndex 歌词索引
|
||||
*/
|
||||
const getYrcStyle = (wordData: LyricContentType, lyricIndex: number) => {
|
||||
const getYrcStyle = (wordData: LyricWord, lyricIndex: number) => {
|
||||
const currentLine = lyricData.yrcData?.[lyricIndex];
|
||||
if (!currentLine) return { WebkitMaskPositionX: "100%" };
|
||||
const seek = playSeekMs.value / 1000; // 转为秒
|
||||
const seekSec = playSeekMs.value / 1000;
|
||||
const startSec = (currentLine.startTime || 0) / 1000;
|
||||
const endSec = (currentLine.endTime || 0) / 1000;
|
||||
const isLineActive =
|
||||
(seek >= currentLine.time && seek < currentLine.endTime) || lyricData.lyricIndex === lyricIndex;
|
||||
(seekSec >= startSec && seekSec < endSec) || lyricData.lyricIndex === lyricIndex;
|
||||
|
||||
if (!isLineActive) {
|
||||
// 已唱过保持填充状态(0%),未唱到保持未填充状态(100%)
|
||||
const hasPlayed = seek >= wordData.time + wordData.duration;
|
||||
const hasPlayed = seekSec >= (wordData.endTime || 0) / 1000;
|
||||
return { WebkitMaskPositionX: hasPlayed ? "0%" : "100%" };
|
||||
}
|
||||
// 激活状态:根据进度实时填充
|
||||
const duration = wordData.duration || 0.001; // 避免除零
|
||||
const progress = Math.max(Math.min((seek - wordData.time) / duration, 1), 0);
|
||||
const durationSec = Math.max(((wordData.endTime || 0) - (wordData.startTime || 0)) / 1000, 0.001);
|
||||
const progress = Math.max(
|
||||
Math.min((seekSec - (wordData.startTime || 0) / 1000) / durationSec, 1),
|
||||
0,
|
||||
);
|
||||
return {
|
||||
transitionDuration: `0s, 0s, 0.35s`,
|
||||
transitionDelay: `0ms`,
|
||||
@@ -324,10 +383,10 @@ const getScrollStyle = (line: RenderLine) => {
|
||||
if (overflow <= 0) return { transform: "translateX(0px)" };
|
||||
// 计算进度:毫秒锚点插值(`playSeekMs`),并以当前行的 `time` 与有效 `endTime` 计算区间
|
||||
const seekSec = playSeekMs.value / 1000;
|
||||
const start = Number(line.line.time ?? 0);
|
||||
const start = Number(line.line.startTime ?? 0) / 1000;
|
||||
// 仅在滚动计算中提前 1 秒
|
||||
const END_MARGIN_SEC = 1;
|
||||
const endRaw = Number(line.line.endTime);
|
||||
const endRaw = Number(line.line.endTime) / 1000;
|
||||
// 若 endTime 仍为 0 或不大于 start,视为无时长:不滚动
|
||||
const hasSafeEnd = Number.isFinite(endRaw) && endRaw > 0 && endRaw > start;
|
||||
if (!hasSafeEnd) return { transform: "translateX(0px)" };
|
||||
|
||||
Reference in New Issue
Block a user