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