mirror of
https://github.com/imsyy/SPlayer.git
synced 2025-11-25 03:14:57 +08:00
Compare commits
2 Commits
1b2985892b
...
cea9f7b025
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cea9f7b025 | ||
|
|
6b653bc5e8 |
8
components.d.ts
vendored
8
components.d.ts
vendored
@@ -61,8 +61,11 @@ declare module 'vue' {
|
||||
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
|
||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||
NDynamicTags: typeof import('naive-ui')['NDynamicTags']
|
||||
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||
NFlex: typeof import('naive-ui')['NFlex']
|
||||
NFloatButton: typeof import('naive-ui')['NFloatButton']
|
||||
NFloatButtonGroup: typeof import('naive-ui')['NFloatButtonGroup']
|
||||
NForm: typeof import('naive-ui')['NForm']
|
||||
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||
NFormItemGi: typeof import('naive-ui')['NFormItemGi']
|
||||
@@ -70,6 +73,7 @@ declare module 'vue' {
|
||||
NGlobalStyle: typeof import('naive-ui')['NGlobalStyle']
|
||||
NGrid: typeof import('naive-ui')['NGrid']
|
||||
NH1: typeof import('naive-ui')['NH1']
|
||||
NH2: typeof import('naive-ui')['NH2']
|
||||
NH3: typeof import('naive-ui')['NH3']
|
||||
NIcon: typeof import('naive-ui')['NIcon']
|
||||
NImage: typeof import('naive-ui')['NImage']
|
||||
@@ -93,14 +97,18 @@ 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']
|
||||
NSlider: typeof import('naive-ui')['NSlider']
|
||||
NSpin: typeof import('naive-ui')['NSpin']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTab: typeof import('naive-ui')['NTab']
|
||||
NTabPane: typeof import('naive-ui')['NTabPane']
|
||||
NTabs: typeof import('naive-ui')['NTabs']
|
||||
NTag: typeof import('naive-ui')['NTag']
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"@electron-toolkit/utils": "^4.0.0",
|
||||
"@imsyy/color-utils": "^1.0.2",
|
||||
"@material/material-color-utilities": "^0.3.0",
|
||||
"@neteasecloudmusicapienhanced/api": "^4.29.15",
|
||||
"@neteasecloudmusicapienhanced/api": "^4.29.16",
|
||||
"@pixi/app": "^7.4.3",
|
||||
"@pixi/core": "^7.4.3",
|
||||
"@pixi/display": "^7.4.3",
|
||||
|
||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -34,8 +34,8 @@ importers:
|
||||
specifier: ^0.3.0
|
||||
version: 0.3.0
|
||||
'@neteasecloudmusicapienhanced/api':
|
||||
specifier: ^4.29.15
|
||||
version: 4.29.15
|
||||
specifier: ^4.29.16
|
||||
version: 4.29.16
|
||||
'@pixi/app':
|
||||
specifier: ^7.4.3
|
||||
version: 7.4.3(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3))
|
||||
@@ -967,8 +967,8 @@ packages:
|
||||
'@material/material-color-utilities@0.3.0':
|
||||
resolution: {integrity: sha512-ztmtTd6xwnuh2/xu+Vb01btgV8SQWYCaK56CkRK8gEkWe5TuDyBcYJ0wgkMRn+2VcE9KUmhvkz+N9GHrqw/C0g==}
|
||||
|
||||
'@neteasecloudmusicapienhanced/api@4.29.15':
|
||||
resolution: {integrity: sha512-/dw0ON90NDXJnULEyKzGEvKjjkJyHYb9fJgKa1dxsN5AIN/tXNAu4AV2vMJcPGfA/NV7iRnLDEPS8ocJCbCU2w==}
|
||||
'@neteasecloudmusicapienhanced/api@4.29.16':
|
||||
resolution: {integrity: sha512-ml6cTErjJ/fn+E0wvlNGGoW74rDj3mfogO85xySuTCuX/Mdhuayv+pPtoSm5YDM6j26hHmS8+rV2ma4o16pUiw==}
|
||||
engines: {node: '>=12'}
|
||||
hasBin: true
|
||||
|
||||
@@ -5331,7 +5331,7 @@ snapshots:
|
||||
|
||||
'@material/material-color-utilities@0.3.0': {}
|
||||
|
||||
'@neteasecloudmusicapienhanced/api@4.29.15':
|
||||
'@neteasecloudmusicapienhanced/api@4.29.16':
|
||||
dependencies:
|
||||
'@unblockneteasemusic/server': 0.28.0
|
||||
axios: 1.13.2
|
||||
|
||||
@@ -58,7 +58,10 @@
|
||||
]"
|
||||
:style="{
|
||||
filter: settingStore.lyricsBlur
|
||||
? `blur(${Math.min(Math.abs(statusStore.lyricIndex - index) * 1.8, 10)}px)`
|
||||
? (playSeek >= item.time && playSeek < item.endTime) ||
|
||||
statusStore.lyricIndex === index
|
||||
? 'blur(0)'
|
||||
: `blur(${Math.min(Math.abs(statusStore.lyricIndex - index) * 1.8, 10)}px)`
|
||||
: 'blur(0)',
|
||||
}"
|
||||
@click="jumpSeek(item.time)"
|
||||
|
||||
@@ -287,7 +287,10 @@
|
||||
</n-collapse-transition>
|
||||
</div>
|
||||
<div class="set-list">
|
||||
<n-h3 prefix="bar"> Apple Music-like Lyrics </n-h3>
|
||||
<n-h3 prefix="bar">
|
||||
Apple Music-like Lyrics
|
||||
<n-tag type="warning" size="small" round>Beta</n-tag>
|
||||
</n-h3>
|
||||
<n-card class="set-item">
|
||||
<div class="label">
|
||||
<n-text class="name">使用 Apple Music-like Lyrics</n-text>
|
||||
@@ -318,7 +321,10 @@
|
||||
</n-card>
|
||||
</div>
|
||||
<div v-if="isElectron" class="set-list">
|
||||
<n-h3 prefix="bar"> 桌面歌词 </n-h3>
|
||||
<n-h3 prefix="bar">
|
||||
桌面歌词
|
||||
<n-tag type="warning" size="small" round>Beta</n-tag>
|
||||
</n-h3>
|
||||
<n-card class="set-item">
|
||||
<div class="label">
|
||||
<n-text class="name">开启桌面歌词</n-text>
|
||||
|
||||
@@ -195,6 +195,12 @@ const toGithub = () => {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.n-h {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.n-collapse-transition {
|
||||
margin-bottom: 12px;
|
||||
&:last-child {
|
||||
|
||||
@@ -26,61 +26,95 @@ export const getLyricData = async (id: number) => {
|
||||
try {
|
||||
// 检测本地歌词覆盖
|
||||
const getLyric = getLyricFun(settingStore.localLyricPath, id);
|
||||
// 先加载 LRC,不阻塞到 TTML 完成
|
||||
// 并发请求:如果 TTML 先到并且有效,则直接采用 TTML,不再等待或覆盖为 LRC
|
||||
const lrcPromise = getLyric("lrc", songLyric);
|
||||
const ttmlPromise = settingStore.enableTTMLLyric ? getLyric("ttml", songLyricTTML) : null;
|
||||
|
||||
const { lyric: lyricRes, isLocal: lyricLocal } = await lrcPromise;
|
||||
parsedLyricsData(lyricRes, lyricLocal && !settingStore.enableExcludeLocalLyrics);
|
||||
// LRC 到达后即可认为加载完成
|
||||
statusStore.lyricLoading = false;
|
||||
let settled = false; // 是否已采用某一种歌词并结束加载状态
|
||||
let ttmlAdopted = false; // 是否已采用 TTML
|
||||
|
||||
// TTML 并行加载,完成后增量更新,不阻塞整体流程
|
||||
if (ttmlPromise) {
|
||||
statusStore.usingTTMLLyric = false;
|
||||
void ttmlPromise
|
||||
.then(({ lyric: ttmlContent, isLocal: ttmlLocal }) => {
|
||||
if (!ttmlContent) {
|
||||
statusStore.usingTTMLLyric = false;
|
||||
return;
|
||||
}
|
||||
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);
|
||||
console.log("TTML lyrics:", ttmlLyric, ttmlYrcLyric);
|
||||
// 合并数据
|
||||
const updates: Partial<{ yrcAMData: LyricLine[]; yrcData: LyricType[] }> = {};
|
||||
if (ttmlLyric?.length) {
|
||||
updates.yrcAMData = ttmlLyric;
|
||||
console.log("✅ TTML AMLL lyrics success");
|
||||
}
|
||||
if (ttmlYrcLyric?.length) {
|
||||
updates.yrcData = ttmlYrcLyric;
|
||||
console.log("✅ TTML Yrc lyrics success");
|
||||
}
|
||||
if (Object.keys(updates).length) {
|
||||
musicStore.setSongLyric(updates);
|
||||
statusStore.usingTTMLLyric = true;
|
||||
} else {
|
||||
statusStore.usingTTMLLyric = false;
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("❌ Error loading TTML lyrics:", err);
|
||||
const adoptTTML = async () => {
|
||||
if (!ttmlPromise) {
|
||||
statusStore.usingTTMLLyric = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { lyric: ttmlContent, isLocal: ttmlLocal } = await ttmlPromise;
|
||||
if (!ttmlContent) {
|
||||
statusStore.usingTTMLLyric = false;
|
||||
});
|
||||
} else {
|
||||
statusStore.usingTTMLLyric = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
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);
|
||||
|
||||
console.log("Lyrics: ", musicStore.songLyric);
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
const adoptLRC = async () => {
|
||||
try {
|
||||
const { lyric: lyricRes, isLocal: lyricLocal } = await lrcPromise;
|
||||
// 如果 TTML 已采用,则忽略 LRC
|
||||
if (ttmlAdopted) return;
|
||||
parsedLyricsData(lyricRes, lyricLocal && !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;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { isDev, isElectron } from "./env";
|
||||
import { useSettingStore } from "@/stores";
|
||||
import { getCookie } from "./cookie";
|
||||
import { isLogin } from "./auth";
|
||||
import axiosRetry from "axios-retry";
|
||||
// import axiosRetry from "axios-retry";
|
||||
|
||||
// 全局地址
|
||||
const baseURL: string = String(isDev ? "/api/netease" : import.meta.env["VITE_API_URL"]);
|
||||
@@ -18,10 +18,10 @@ const server: AxiosInstance = axios.create({
|
||||
});
|
||||
|
||||
// 请求重试
|
||||
axiosRetry(server, {
|
||||
// 重试次数
|
||||
retries: 3,
|
||||
});
|
||||
// axiosRetry(server, {
|
||||
// // 重试次数
|
||||
// retries: 3,
|
||||
// });
|
||||
|
||||
// 请求拦截器
|
||||
server.interceptors.request.use(
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Position, useRafFn } from "@vueuse/core";
|
||||
import { useRafFn } from "@vueuse/core";
|
||||
import { LyricContentType, LyricType } from "@/types/main";
|
||||
import { LyricConfig, LyricData, RenderLine } from "@/types/desktop-lyric";
|
||||
import defaultDesktopLyricConfig from "@/assets/data/lyricConfig";
|
||||
@@ -332,40 +332,50 @@ const dragState = reactive({
|
||||
|
||||
/**
|
||||
* 桌面歌词拖动开始
|
||||
* @param _position 拖动位置
|
||||
* @param event 指针事件
|
||||
*/
|
||||
const lyricDragStart = async (_position: Position, event: PointerEvent) => {
|
||||
const onDocPointerDown = async (event: PointerEvent) => {
|
||||
if (lyricConfig.isLock) return;
|
||||
// 仅左键触发
|
||||
if (event.button !== 0) return;
|
||||
const target = event?.target as HTMLElement | null;
|
||||
if (!target) return;
|
||||
// 过滤 header 中的按钮:不触发拖拽
|
||||
if (target.closest(".menu-btn")) return;
|
||||
startDrag(event);
|
||||
};
|
||||
|
||||
/**
|
||||
* 桌面歌词拖动开始
|
||||
* @param event 指针事件
|
||||
*/
|
||||
const startDrag = async (event: PointerEvent) => {
|
||||
dragState.isDragging = true;
|
||||
const { x, y } = await window.electron.ipcRenderer.invoke("get-window-bounds");
|
||||
const { width, height } = await window.api.store.get("lyric");
|
||||
// 直接限制最大宽高
|
||||
window.electron.ipcRenderer.send("toggle-fixed-max-size", {
|
||||
width,
|
||||
height,
|
||||
fixed: true,
|
||||
});
|
||||
window.electron.ipcRenderer.send("toggle-fixed-max-size", { width, height, fixed: true });
|
||||
dragState.startX = event?.screenX ?? 0;
|
||||
dragState.startY = event?.screenY ?? 0;
|
||||
dragState.startWinX = x;
|
||||
dragState.startWinY = y;
|
||||
dragState.winWidth = width ?? 0;
|
||||
dragState.winHeight = height ?? 0;
|
||||
document.addEventListener("pointermove", onDocPointerMove, { capture: true });
|
||||
document.addEventListener("pointerup", onDocPointerUp, { capture: true });
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
/**
|
||||
* 桌面歌词拖动移动
|
||||
* @param _position 拖动位置
|
||||
* @param event 指针事件
|
||||
*/
|
||||
const lyricDragMove = async (_position: Position, event: PointerEvent) => {
|
||||
const onDocPointerMove = async (event: PointerEvent) => {
|
||||
if (!dragState.isDragging || lyricConfig.isLock) return;
|
||||
const screenX = event?.screenX ?? 0;
|
||||
const screenY = event?.screenY ?? 0;
|
||||
let newWinX = Math.round(dragState.startWinX + (screenX - dragState.startX));
|
||||
let newWinY = Math.round(dragState.startWinY + (screenY - dragState.startY));
|
||||
// 可选:限制在屏幕边界(支持多屏)
|
||||
// 是否限制在屏幕边界(支持多屏)
|
||||
if (lyricConfig.limitBounds) {
|
||||
const { minX, minY, maxX, maxY } = await window.electron.ipcRenderer.invoke(
|
||||
"get-virtual-screen-bounds",
|
||||
@@ -382,36 +392,30 @@ const lyricDragMove = async (_position: Position, event: PointerEvent) => {
|
||||
);
|
||||
};
|
||||
|
||||
// 监听桌面歌词拖动
|
||||
useDraggable(desktopLyricRef, {
|
||||
onStart: (position, event) => {
|
||||
lyricDragStart(position, event);
|
||||
},
|
||||
onMove: (position, event) => {
|
||||
lyricDragMove(position, event);
|
||||
},
|
||||
onEnd: () => {
|
||||
// 关闭拖拽状态
|
||||
dragState.isDragging = false;
|
||||
requestAnimationFrame(() => {
|
||||
// 恢复拖拽前宽高
|
||||
window.electron.ipcRenderer.send(
|
||||
"update-lyric-size",
|
||||
dragState.winWidth,
|
||||
dragState.winHeight,
|
||||
);
|
||||
// 根据字体大小恢复一次高度
|
||||
const height = fontSizeToHeight(lyricConfig.fontSize);
|
||||
if (height) pushWindowHeight(height);
|
||||
// 恢复最大宽高
|
||||
window.electron.ipcRenderer.send("toggle-fixed-max-size", {
|
||||
width: dragState.winWidth,
|
||||
height: dragState.winHeight,
|
||||
fixed: false,
|
||||
});
|
||||
/**
|
||||
* 桌面歌词拖动结束
|
||||
*/
|
||||
const onDocPointerUp = () => {
|
||||
if (!dragState.isDragging) return;
|
||||
// 关闭拖拽状态
|
||||
dragState.isDragging = false;
|
||||
// 移除全局监听
|
||||
document.removeEventListener("pointermove", onDocPointerMove, { capture: true });
|
||||
document.removeEventListener("pointerup", onDocPointerUp, { capture: true });
|
||||
requestAnimationFrame(() => {
|
||||
// 恢复拖拽前宽高
|
||||
window.electron.ipcRenderer.send("update-lyric-size", dragState.winWidth, dragState.winHeight);
|
||||
// 根据字体大小恢复一次高度
|
||||
const height = fontSizeToHeight(lyricConfig.fontSize);
|
||||
if (height) pushWindowHeight(height);
|
||||
// 恢复最大宽高
|
||||
window.electron.ipcRenderer.send("toggle-fixed-max-size", {
|
||||
width: dragState.winWidth,
|
||||
height: dragState.winHeight,
|
||||
fixed: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 监听窗口大小变化
|
||||
const { height: winHeight } = useWindowSize();
|
||||
@@ -547,11 +551,16 @@ onMounted(() => {
|
||||
} else {
|
||||
pauseSeek();
|
||||
}
|
||||
// 拖拽入口
|
||||
document.addEventListener("pointerdown", onDocPointerDown, { capture: true });
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 关闭 RAF
|
||||
pauseSeek();
|
||||
// 解绑事件
|
||||
document.removeEventListener("pointerdown", onDocPointerDown, { capture: true });
|
||||
if (dragState.isDragging) onDocPointerUp();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -570,9 +579,10 @@ onBeforeUnmount(() => {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transition: background-color 0.3s;
|
||||
cursor: move;
|
||||
cursor: default;
|
||||
.header {
|
||||
margin-bottom: 12px;
|
||||
cursor: default;
|
||||
// 子内容三等分grid
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
@@ -752,6 +762,9 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
.lyric-container {
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user