mirror of
https://github.com/imsyy/SPlayer.git
synced 2025-11-25 03:14:57 +08:00
✨ feat: 优化列表播放方式 & 添加一些桌面歌词配置 #536
This commit is contained in:
1
components.d.ts
vendored
1
components.d.ts
vendored
@@ -101,7 +101,6 @@ declare module 'vue' {
|
||||
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']
|
||||
|
||||
13
package.json
13
package.json
@@ -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.12",
|
||||
"@neteasecloudmusicapienhanced/api": "^4.29.15",
|
||||
"@pixi/app": "^7.4.3",
|
||||
"@pixi/core": "^7.4.3",
|
||||
"@pixi/display": "^7.4.3",
|
||||
@@ -49,7 +49,8 @@
|
||||
"@pixi/filter-color-matrix": "^7.4.3",
|
||||
"@pixi/sprite": "^7.4.3",
|
||||
"@vueuse/core": "^13.9.0",
|
||||
"axios": "^1.12.2",
|
||||
"axios": "^1.13.2",
|
||||
"axios-retry": "^4.5.0",
|
||||
"change-case": "^5.4.4",
|
||||
"dayjs": "^1.11.18",
|
||||
"electron-dl": "^4.0.0",
|
||||
@@ -91,7 +92,7 @@
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"ajv": "^8.17.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"electron": "^38.2.2",
|
||||
"electron": "38.2.2",
|
||||
"electron-builder": "^26.0.12",
|
||||
"electron-log": "^5.4.3",
|
||||
"electron-vite": "^4.0.1",
|
||||
@@ -107,13 +108,13 @@
|
||||
"typescript": "^5.9.3",
|
||||
"unplugin-auto-import": "^20.2.0",
|
||||
"unplugin-vue-components": "^29.1.0",
|
||||
"vite": "^7.1.9",
|
||||
"vite": "^7.2.2",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-vue-devtools": "^8.0.3",
|
||||
"vite-plugin-wasm": "^3.5.0",
|
||||
"vue": "^3.5.22",
|
||||
"vue": "^3.5.24",
|
||||
"vue-router": "^4.5.1",
|
||||
"vue-tsc": "^3.1.1"
|
||||
"vue-tsc": "^3.1.3"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
|
||||
449
pnpm-lock.yaml
generated
449
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,8 @@ const config: LyricConfig = {
|
||||
fontFamily: "system-ui",
|
||||
fontSize: 24,
|
||||
fontIsBold: false,
|
||||
showTran: true,
|
||||
showYrc: true,
|
||||
isDoubleLine: true,
|
||||
position: "both",
|
||||
limitBounds: false,
|
||||
|
||||
@@ -60,7 +60,11 @@
|
||||
:hiddenCover="hiddenCover"
|
||||
:hiddenAlbum="hiddenAlbum"
|
||||
:hiddenSize="hiddenSize"
|
||||
@dblclick.stop="player.updatePlayList(listData, itemData, playListId)"
|
||||
@dblclick.stop="
|
||||
doubleClickAction === 'add'
|
||||
? player.addNextSong(itemData, true)
|
||||
: player.updatePlayList(listData, itemData, playListId)
|
||||
"
|
||||
@contextmenu.stop="
|
||||
songListMenuRef?.openDropdown(
|
||||
$event,
|
||||
@@ -153,6 +157,8 @@ const props = withDefaults(
|
||||
playListId?: number;
|
||||
// 是否为每日推荐
|
||||
isDailyRecommend?: boolean;
|
||||
// 双击播放操作
|
||||
doubleClickAction?: "all" | "add";
|
||||
}>(),
|
||||
{
|
||||
type: "song",
|
||||
|
||||
@@ -416,6 +416,30 @@
|
||||
/>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="label">
|
||||
<n-text class="name">显示逐字歌词</n-text>
|
||||
<n-text class="tip" :depth="3">是否显示桌面歌词逐字效果</n-text>
|
||||
</div>
|
||||
<n-switch
|
||||
v-model:value="desktopLyricConfig.showYrc"
|
||||
:round="false"
|
||||
class="set"
|
||||
@update:value="saveDesktopLyricConfig"
|
||||
/>
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="label">
|
||||
<n-text class="name">显示翻译</n-text>
|
||||
<n-text class="tip" :depth="3">是否显示桌面歌词翻译</n-text>
|
||||
</div>
|
||||
<n-switch
|
||||
v-model:value="desktopLyricConfig.showTran"
|
||||
:round="false"
|
||||
class="set"
|
||||
@update:value="saveDesktopLyricConfig"
|
||||
/>
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="label">
|
||||
<n-text class="name">文字加粗</n-text>
|
||||
@@ -549,13 +573,21 @@ const saveDesktopLyricConfig = () => {
|
||||
const restoreDesktopLyricConfig = () => {
|
||||
try {
|
||||
if (!isElectron) return;
|
||||
window.electron.ipcRenderer.send(
|
||||
"update-desktop-lyric-option",
|
||||
defaultDesktopLyricConfig,
|
||||
true,
|
||||
);
|
||||
window.$message.success("桌面歌词配置已恢复默认");
|
||||
console.log(defaultDesktopLyricConfig, desktopLyricConfig);
|
||||
window.$dialog.warning({
|
||||
title: "警告",
|
||||
content: "此操作将恢复所有桌面歌词配置为默认值,是否继续?",
|
||||
positiveText: "确定",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: () => {
|
||||
window.electron.ipcRenderer.send(
|
||||
"update-desktop-lyric-option",
|
||||
defaultDesktopLyricConfig,
|
||||
true,
|
||||
);
|
||||
window.$message.success("桌面歌词配置已恢复默认");
|
||||
console.log(defaultDesktopLyricConfig, desktopLyricConfig);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to save options:", error);
|
||||
window.$message.error("桌面歌词配置恢复默认失败");
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
<n-card class="set-item">
|
||||
<div class="label">
|
||||
<n-text class="name">真实 IP 地址</n-text>
|
||||
<n-text class="tip" :depth="3">可在此处输入国内 IP</n-text>
|
||||
<n-text class="tip" :depth="3">可在此处输入国内 IP,不填写则为随机</n-text>
|
||||
</div>
|
||||
<n-input
|
||||
v-model:value="settingStore.realIP"
|
||||
:disabled="!settingStore.useRealIP"
|
||||
placeholder="请填写真实 IP 地址"
|
||||
placeholder="127.0.0.1"
|
||||
class="set"
|
||||
>
|
||||
<template #prefix>
|
||||
|
||||
@@ -246,7 +246,7 @@ export const useSettingStore = defineStore("setting", {
|
||||
proxyServe: "127.0.0.1",
|
||||
proxyPort: 80,
|
||||
useRealIP: false,
|
||||
realIP: "116.25.146.177",
|
||||
realIP: "",
|
||||
}),
|
||||
getters: {
|
||||
/**
|
||||
|
||||
4
src/types/desktop-lyric.d.ts
vendored
4
src/types/desktop-lyric.d.ts
vendored
@@ -37,6 +37,10 @@ export interface LyricConfig {
|
||||
fontIsBold: boolean;
|
||||
/** 是否双行 */
|
||||
isDoubleLine: boolean;
|
||||
/** 显示翻译 */
|
||||
showTran: boolean;
|
||||
/** 是否开启逐字歌词 */
|
||||
showYrc: boolean;
|
||||
/** 文本排版位置 */
|
||||
position: "left" | "center" | "right" | "both";
|
||||
/** 是否限制在屏幕边界内拖动 */
|
||||
|
||||
@@ -587,6 +587,7 @@ class Player {
|
||||
const statusStore = useStatusStore();
|
||||
const settingStore = useSettingStore();
|
||||
const sessionId = this.newSession();
|
||||
|
||||
try {
|
||||
// 获取播放数据
|
||||
const playSongData = getPlaySongData();
|
||||
@@ -595,100 +596,91 @@ class Player {
|
||||
return;
|
||||
}
|
||||
const { id, dj, path, type } = playSongData;
|
||||
|
||||
// 更改当前播放歌曲
|
||||
musicStore.playSong = playSongData;
|
||||
// 更改状态
|
||||
statusStore.playLoading = true;
|
||||
|
||||
// 清理旧播放器与计时器
|
||||
this.resetPlayerCore();
|
||||
|
||||
// 本地歌曲
|
||||
if (path) {
|
||||
if (this.isStale(sessionId)) return;
|
||||
await this.createPlayer(`file://${path}`, autoPlay, seek);
|
||||
// 获取歌曲元信息
|
||||
await this.parseLocalMusicInfo(path);
|
||||
try {
|
||||
await this.createPlayer(`file://${path}`, autoPlay, seek);
|
||||
await this.parseLocalMusicInfo(path);
|
||||
} catch (err) {
|
||||
console.error("播放器初始化错误(本地):", err);
|
||||
}
|
||||
}
|
||||
// 在线歌曲
|
||||
else if (id && dataStore.playList.length) {
|
||||
const songId = type === "radio" ? dj?.id : id;
|
||||
if (!songId) throw new Error("Get song id error");
|
||||
// 优先使用预载的下一首 URL(若命中缓存)
|
||||
const cached = this.nextPrefetch;
|
||||
if (cached && cached.id === songId && cached.url) {
|
||||
statusStore.playUblock = cached.ublock;
|
||||
if (this.isStale(sessionId)) return;
|
||||
await this.createPlayer(cached.url, autoPlay, seek);
|
||||
} else {
|
||||
// 官方地址失败或仅为试听时再尝试解锁(Electron 且非电台且开启解灰)
|
||||
const canUnlock = isElectron && type !== "radio" && settingStore.useSongUnlock;
|
||||
const { url: officialUrl, isTrial } = await getOnlineUrl(songId);
|
||||
if (officialUrl && !isTrial) {
|
||||
// 官方可播放且非试听
|
||||
statusStore.playUblock = false;
|
||||
if (this.isStale(sessionId)) return;
|
||||
await this.createPlayer(officialUrl, autoPlay, seek);
|
||||
} else if (canUnlock) {
|
||||
// 官方失败或为试听时尝试解锁
|
||||
const unlockUrl = await getUnlockSongUrl(playSongData);
|
||||
if (unlockUrl) {
|
||||
statusStore.playUblock = true;
|
||||
console.log("🎼 Song unlock successfully:", unlockUrl);
|
||||
if (this.isStale(sessionId)) return;
|
||||
await this.createPlayer(unlockUrl, autoPlay, seek);
|
||||
} else if (officialUrl) {
|
||||
// 解锁失败,若允许试听则播放试听
|
||||
if (isTrial && settingStore.playSongDemo) {
|
||||
window.$message.warning("当前歌曲仅可试听,请开通会员后重试");
|
||||
statusStore.playUblock = false;
|
||||
if (this.isStale(sessionId)) return;
|
||||
await this.createPlayer(officialUrl, autoPlay, seek);
|
||||
} else {
|
||||
// 不允许试听
|
||||
statusStore.playUblock = false;
|
||||
if (statusStore.playIndex === dataStore.playList.length - 1) {
|
||||
statusStore.$patch({ playStatus: false, playLoading: false });
|
||||
window.$message.warning("当前列表歌曲无法播放,请更换歌曲");
|
||||
} else {
|
||||
window.$message.error("该歌曲暂无音源,跳至下一首");
|
||||
// 防止切歌保护状态阻塞跳转
|
||||
this.switching = false;
|
||||
await this.nextOrPrev("next");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 无任何可用地址
|
||||
statusStore.playUblock = false;
|
||||
if (statusStore.playIndex === dataStore.playList.length - 1) {
|
||||
statusStore.$patch({ playStatus: false, playLoading: false });
|
||||
window.$message.warning("当前列表歌曲无法播放,请更换歌曲");
|
||||
} else {
|
||||
window.$message.error("该歌曲暂无音源,跳至下一首");
|
||||
// 防止切歌保护状态阻塞跳转
|
||||
this.switching = false;
|
||||
await this.nextOrPrev("next");
|
||||
return;
|
||||
}
|
||||
}
|
||||
let playerUrl: string | null = null;
|
||||
|
||||
// 获取歌曲 URL 单独 try-catch
|
||||
try {
|
||||
const songId = type === "radio" ? dj?.id : id;
|
||||
if (!songId) throw new Error("获取歌曲 ID 失败");
|
||||
|
||||
// 使用预载缓存
|
||||
const cached = this.nextPrefetch;
|
||||
if (cached && cached.id === songId && cached.url) {
|
||||
playerUrl = cached.url;
|
||||
statusStore.playUblock = cached.ublock;
|
||||
} else {
|
||||
if (dataStore.playList.length === 1) {
|
||||
this.resetStatus();
|
||||
window.$message.warning("当前播放列表已无可播放歌曲,请更换");
|
||||
return;
|
||||
const canUnlock = isElectron && type !== "radio" && settingStore.useSongUnlock;
|
||||
const { url: officialUrl, isTrial } = await getOnlineUrl(songId);
|
||||
|
||||
if (officialUrl && !isTrial) {
|
||||
playerUrl = officialUrl;
|
||||
statusStore.playUblock = false;
|
||||
} else if (canUnlock) {
|
||||
const unlockUrl = await getUnlockSongUrl(playSongData);
|
||||
if (unlockUrl) {
|
||||
playerUrl = unlockUrl;
|
||||
statusStore.playUblock = true;
|
||||
console.log("🎼 Song unlock successfully:", unlockUrl);
|
||||
} else if (officialUrl && isTrial && settingStore.playSongDemo) {
|
||||
window.$message.warning("当前歌曲仅可试听,请开通会员后重试");
|
||||
playerUrl = officialUrl;
|
||||
statusStore.playUblock = false;
|
||||
} else {
|
||||
playerUrl = null;
|
||||
}
|
||||
} else {
|
||||
window.$message.error("该歌曲无法播放,跳至下一首");
|
||||
// 防止切歌保护状态阻塞跳转
|
||||
this.switching = false;
|
||||
await this.nextOrPrev();
|
||||
return;
|
||||
playerUrl = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!playerUrl) {
|
||||
window.$message.error("该歌曲暂无音源,跳至下一首");
|
||||
this.switching = false;
|
||||
await this.nextOrPrev("next");
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("❌ 获取歌曲地址出错:", err);
|
||||
window.$message.error("获取歌曲地址失败,跳至下一首");
|
||||
this.switching = false;
|
||||
await this.nextOrPrev("next");
|
||||
return;
|
||||
}
|
||||
|
||||
// 有有效 URL 才创建播放器
|
||||
if (playerUrl && !this.isStale(sessionId)) {
|
||||
try {
|
||||
await this.createPlayer(playerUrl, autoPlay, seek);
|
||||
} catch (err) {
|
||||
console.error("播放器初始化错误(在线):", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 初始化音乐播放器出错:", error);
|
||||
window.$message.error("播放器遇到错误,请尝试软件热重载");
|
||||
// this.errorNext();
|
||||
} catch (err) {
|
||||
console.error("❌ 初始化音乐播放器出错:", err);
|
||||
window.$message.error("播放遇到错误,尝试下一首");
|
||||
this.switching = false;
|
||||
await this.nextOrPrev("next");
|
||||
} finally {
|
||||
this.switching = false;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { isDev, isElectron } from "./env";
|
||||
import { useSettingStore } from "@/stores";
|
||||
import { getCookie } from "./cookie";
|
||||
import { isLogin } from "./auth";
|
||||
import axiosRetry from "axios-retry";
|
||||
|
||||
// 全局地址
|
||||
const baseURL: string = String(isDev ? "/api/netease" : import.meta.env["VITE_API_URL"]);
|
||||
@@ -16,6 +17,12 @@ const server: AxiosInstance = axios.create({
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
// 请求重试
|
||||
axiosRetry(server, {
|
||||
// 重试次数
|
||||
retries: 3,
|
||||
});
|
||||
|
||||
// 请求拦截器
|
||||
server.interceptors.request.use(
|
||||
(request) => {
|
||||
@@ -33,7 +40,11 @@ server.interceptors.request.use(
|
||||
}
|
||||
// 自定义 realIP
|
||||
if (settingStore.useRealIP) {
|
||||
request.params.realIP = settingStore.realIP || "116.25.146.177";
|
||||
if (settingStore.realIP) {
|
||||
request.params.realIP = settingStore.realIP;
|
||||
} else {
|
||||
request.params.randomCNIP = true;
|
||||
}
|
||||
}
|
||||
// proxy
|
||||
if (settingStore.proxyProtocol !== "off") {
|
||||
@@ -90,8 +101,6 @@ server.interceptors.response.use(
|
||||
// meta: "若持续发生,可尝试软件热重载",
|
||||
// duration: 5000,
|
||||
// });
|
||||
// 控制台输出
|
||||
window.$message.warning("请求出错,若持续发生,可尝试软件热重载");
|
||||
// 返回错误
|
||||
return Promise.reject(error);
|
||||
},
|
||||
|
||||
@@ -69,7 +69,9 @@
|
||||
:ref="(el) => line.active && (currentLineRef = el as HTMLElement)"
|
||||
>
|
||||
<!-- 逐字歌词渲染 -->
|
||||
<template v-if="lyricData?.yrcData?.length && line.line?.contents?.length">
|
||||
<template
|
||||
v-if="lyricConfig.showYrc && lyricData?.yrcData?.length && line.line?.contents?.length"
|
||||
>
|
||||
<span
|
||||
class="scroll-content"
|
||||
:style="getScrollStyle(line)"
|
||||
@@ -210,7 +212,7 @@ const renderLyricLines = computed<RenderLine[]>(() => {
|
||||
if (!current) return [];
|
||||
const safeEnd = getSafeEndTime(lyrics, idx);
|
||||
// 有翻译:保留第二行显示翻译,第一行显示原文(逐字由 contents 驱动)
|
||||
if (current.tran && current.tran.trim().length > 0) {
|
||||
if (lyricConfig.showTran && current.tran && current.tran.trim().length > 0) {
|
||||
const lines: RenderLine[] = [
|
||||
{ line: { ...current, endTime: safeEnd }, index: idx, key: `${idx}:orig`, active: true },
|
||||
{
|
||||
@@ -279,7 +281,7 @@ const currentContentRef = ref<HTMLElement | null>(null);
|
||||
const scrollStartAtProgress = 0.5;
|
||||
|
||||
/**
|
||||
* 逐字歌词滚动样式计算(基于毫秒游标插值)
|
||||
* 歌词滚动样式计算
|
||||
* - 容器 `currentLineRef` 与内容 `currentContentRef` 分别记录当前激活行与其文本内容
|
||||
* - 当内容宽度超过容器宽度(overflow > 0)时,才会触发水平滚动
|
||||
* - 进度采用毫秒锚点插值(`playSeekMs`),并以当前行的 `time` 与有效 `endTime` 计算区间
|
||||
@@ -440,7 +442,11 @@ watchThrottled(
|
||||
const next = { fontSize: size };
|
||||
window.electron.ipcRenderer.send("update-desktop-lyric-option", next, true);
|
||||
},
|
||||
{ immediate: true, throttle: 100 },
|
||||
{
|
||||
leading: true,
|
||||
immediate: true,
|
||||
throttle: 100,
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -155,6 +155,7 @@
|
||||
:data="albumDataShow"
|
||||
:loading="loading"
|
||||
:height="songListHeight"
|
||||
:doubleClickAction="searchData?.length ? 'add' : 'all'"
|
||||
hidden-album
|
||||
@scroll="listScroll"
|
||||
/>
|
||||
|
||||
@@ -149,6 +149,7 @@
|
||||
:loading="loading"
|
||||
:height="songListHeight"
|
||||
:playListId="playlistId"
|
||||
:doubleClickAction="searchData?.length ? 'add' : 'all'"
|
||||
@scroll="listScroll"
|
||||
@removeSong="removeSong"
|
||||
/>
|
||||
@@ -418,7 +419,7 @@ onMounted(async () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 获取我喜欢的音乐歌单ID
|
||||
const likedPlaylistId = dataStore.userLikeData.playlists?.[0]?.id;
|
||||
if (likedPlaylistId) {
|
||||
|
||||
@@ -194,6 +194,7 @@
|
||||
:loading="loading"
|
||||
:height="songListHeight"
|
||||
:playListId="playlistId"
|
||||
:doubleClickAction="searchData?.length ? 'add' : 'all'"
|
||||
@scroll="listScroll"
|
||||
@removeSong="removeSong"
|
||||
/>
|
||||
|
||||
@@ -156,6 +156,7 @@
|
||||
:loading="loading"
|
||||
:height="songListHeight"
|
||||
:radioId="radioId"
|
||||
:doubleClickAction="searchData?.length ? 'add' : 'all'"
|
||||
type="radio"
|
||||
@scroll="listScroll"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user