mirror of
https://github.com/imsyy/SPlayer.git
synced 2025-11-25 03:14:57 +08:00
🌈 style: 优化部分样式
This commit is contained in:
@@ -27,7 +27,7 @@ const initFileIpc = (): void => {
|
||||
const filePath = resolve(dirPath).replace(/\\/g, "/");
|
||||
console.info(`📂 Fetching music files from: ${filePath}`);
|
||||
// 查找指定目录下的所有音乐文件
|
||||
const musicFiles = await FastGlob("**/*.{mp3,wav,flac}", { cwd: filePath });
|
||||
const musicFiles = await FastGlob("**/*.{mp3,wav,flac,aac,webm}", { cwd: filePath });
|
||||
// 解析元信息
|
||||
const metadataPromises = musicFiles.map(async (file) => {
|
||||
const filePath = join(dirPath, file);
|
||||
|
||||
1
src/assets/icons/Close.svg
Normal file
1
src/assets/icons/Close.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="m12 13.4l-4.9 4.9q-.275.275-.7.275t-.7-.275t-.275-.7t.275-.7l4.9-4.9l-4.9-4.9q-.275-.275-.275-.7t.275-.7t.7-.275t.7.275l4.9 4.9l4.9-4.9q.275-.275.7-.275t.7.275t.275.7t-.275.7L13.4 12l4.9 4.9q.275.275.275.7t-.275.7t-.7.275t-.7-.275z"/></svg>
|
||||
|
After Width: | Height: | Size: 467 B |
4
src/assets/icons/Lock.svg
Normal file
4
src/assets/icons/Lock.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||
<path fill="currentColor"
|
||||
d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2M9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9zm9 14H6V10h12zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2s-2 .9-2 2s.9 2 2 2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 349 B |
1
src/assets/icons/LockOpen.svg
Normal file
1
src/assets/icons/LockOpen.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M6 20h12V10H6zm6-3q.825 0 1.413-.587T14 15t-.587-1.412T12 13t-1.412.588T10 15t.588 1.413T12 17m-6 3V10zm0 2q-.825 0-1.412-.587T4 20V10q0-.825.588-1.412T6 8h7V6q0-2.075 1.463-3.537T18 1q1.775 0 3.1 1.075t1.75 2.7q.125.425-.162.825T22 6q-.425 0-.7-.175t-.4-.575q-.275-.95-1.062-1.6T18 3q-1.25 0-2.125.875T15 6v2h3q.825 0 1.413.588T20 10v10q0 .825-.587 1.413T18 22z"/></svg>
|
||||
|
After Width: | Height: | Size: 598 B |
4
src/assets/icons/TextSizeAdd.svg
Normal file
4
src/assets/icons/TextSizeAdd.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||
<path fill="currentColor"
|
||||
d="M8.5 7h2L16 21h-2.4l-1.1-3H6.3l-1.1 3H3zm-1.4 9h4.8L9.5 9.7zM22 5v2h-3v3h-2V7h-3V5h3V2h2v3z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 227 B |
3
src/assets/icons/TextSizeReduce.svg
Normal file
3
src/assets/icons/TextSizeReduce.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M10.5 7h-2L3 21h2.2l1.1-3h6.2l1.1 3H16zm-3.4 9l2.4-6.3l2.4 6.3zM22 7h-8V5h8z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 204 B |
@@ -1,7 +1,11 @@
|
||||
<!-- 全局图标 -->
|
||||
<template>
|
||||
<n-icon v-if="name" :size="size" :color="color" :depth="depth">
|
||||
<div ref="svgContainer" class="svg-container" />
|
||||
<div
|
||||
ref="svgContainer"
|
||||
:style="{ transform: offset ? `translateY(${offset}px)` : undefined }"
|
||||
class="svg-container"
|
||||
/>
|
||||
</n-icon>
|
||||
</template>
|
||||
|
||||
@@ -10,6 +14,7 @@ const props = defineProps<{
|
||||
name: string;
|
||||
size?: string | number;
|
||||
color?: string;
|
||||
offset?: number;
|
||||
depth?: 1 | 2 | 3 | 4 | 5;
|
||||
}>();
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<s-image
|
||||
v-else-if="settingStore.playerBackgroundType === 'blur'"
|
||||
:src="musicStore.songCover"
|
||||
:observe-visibility="false"
|
||||
class="bg-img"
|
||||
alt="cover"
|
||||
/>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<s-image
|
||||
:key="musicStore.getSongCover()"
|
||||
:src="musicStore.getSongCover('l')"
|
||||
:observe-visibility="false"
|
||||
class="cover-img"
|
||||
/>
|
||||
<!-- 动态封面 -->
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
:key="imgSrc"
|
||||
:alt="alt || 'image'"
|
||||
:class="['cover', { loaded: isLoaded }]"
|
||||
:decoding="decodeAsync ? 'async' : 'auto'"
|
||||
:loading="nativeLazy ? 'lazy' : 'eager'"
|
||||
@load="imageLoaded"
|
||||
@error="imageError"
|
||||
/>
|
||||
@@ -27,9 +29,21 @@ const props = withDefaults(
|
||||
src: string | undefined;
|
||||
defaultSrc?: string;
|
||||
alt?: string;
|
||||
// 是否进行可视状态变化
|
||||
observeVisibility?: boolean;
|
||||
// 在不可视时是否释放图片以回收内存
|
||||
releaseOnHide?: boolean;
|
||||
// 是否使用浏览器异步解码
|
||||
decodeAsync?: boolean;
|
||||
// 是否使用原生懒加载
|
||||
nativeLazy?: boolean;
|
||||
}>(),
|
||||
{
|
||||
defaultSrc: "/images/song.jpg?assest",
|
||||
observeVisibility: true,
|
||||
releaseOnHide: false,
|
||||
decodeAsync: true,
|
||||
nativeLazy: true,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -49,31 +63,57 @@ const imgContainer = ref<HTMLImageElement>();
|
||||
|
||||
// 是否加载完成
|
||||
const isLoaded = ref<boolean>(false);
|
||||
// 可视状态上一次值,避免重复 emit
|
||||
const lastShowState = ref<boolean | null>(null);
|
||||
// 加载竞态 token,防止旧图片回调覆盖新状态
|
||||
const loadToken = ref<number>(0);
|
||||
const currentToken = ref<number>(0);
|
||||
|
||||
// 是否可视
|
||||
const isCanLook = useElementVisibility(imgContainer);
|
||||
|
||||
// 图片加载完成
|
||||
const imageLoaded = (e: Event) => {
|
||||
// 竞态保护:仅响应最新一次设置的图片
|
||||
if (currentToken.value !== loadToken.value) return;
|
||||
if (isLoaded.value) return;
|
||||
isLoaded.value = true;
|
||||
// 加载完成
|
||||
emit("load", e);
|
||||
};
|
||||
|
||||
// 图片加载失败
|
||||
const imageError = (e: Event) => {
|
||||
// 竞态保护
|
||||
if (currentToken.value !== loadToken.value) return;
|
||||
isLoaded.value = false;
|
||||
// 避免默认图也反复触发导致死循环
|
||||
if (imgSrc.value !== props.defaultSrc) {
|
||||
imgSrc.value = props.defaultSrc;
|
||||
// 加载失败
|
||||
}
|
||||
emit("error", e);
|
||||
};
|
||||
|
||||
// 可视状态变化
|
||||
// 可视状态变化(可控)
|
||||
watch(
|
||||
isCanLook,
|
||||
(show) => {
|
||||
if (!props.observeVisibility) return;
|
||||
// 去重:仅在状态变化时触发
|
||||
if (lastShowState.value !== show) {
|
||||
lastShowState.value = show;
|
||||
emit("update:show", show);
|
||||
if (show) imgSrc.value = props.src;
|
||||
}
|
||||
if (show) {
|
||||
// 进入可视区再加载,避免重复赋值
|
||||
if (imgSrc.value !== props.src) {
|
||||
loadToken.value += 1;
|
||||
currentToken.value = loadToken.value;
|
||||
imgSrc.value = props.src;
|
||||
}
|
||||
} else if (props.releaseOnHide) {
|
||||
// 释放图片以回收内存
|
||||
if (imgSrc.value !== undefined) imgSrc.value = undefined;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
@@ -83,13 +123,40 @@ watch(
|
||||
() => props.src,
|
||||
(val) => {
|
||||
isLoaded.value = false;
|
||||
// 不同值时才进行赋值,减少重绘
|
||||
if (props.observeVisibility) {
|
||||
if (isCanLook.value) {
|
||||
if (imgSrc.value !== val) {
|
||||
loadToken.value += 1;
|
||||
currentToken.value = loadToken.value;
|
||||
imgSrc.value = val;
|
||||
}
|
||||
} else {
|
||||
imgSrc.value = undefined;
|
||||
if (props.releaseOnHide) {
|
||||
if (imgSrc.value !== undefined) imgSrc.value = undefined;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (imgSrc.value !== val) {
|
||||
loadToken.value += 1;
|
||||
currentToken.value = loadToken.value;
|
||||
imgSrc.value = val;
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
try {
|
||||
if (imgRef.value) imgRef.value.src = "";
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
imgSrc.value = undefined;
|
||||
imgRef.value = undefined;
|
||||
imgContainer.value = undefined;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -393,3 +393,38 @@ export const shuffleArray = <T>(arr: T[]): T[] => {
|
||||
}
|
||||
return copy;
|
||||
};
|
||||
|
||||
/**
|
||||
* 在浏览器空闲时执行任务
|
||||
* @param task 要执行的任务
|
||||
*/
|
||||
export const runIdle = (task: () => void) => {
|
||||
try {
|
||||
const ric = window?.requestIdleCallback as ((cb: () => void) => number) | undefined;
|
||||
if (typeof ric === "function") {
|
||||
ric(() => {
|
||||
try {
|
||||
task();
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
task();
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
} catch {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
task();
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { cloneDeep } from "lodash-es";
|
||||
import { useMusicStore, useStatusStore, useDataStore, useSettingStore } from "@/stores";
|
||||
import { resetSongLyric, parseLocalLyric, calculateLyricIndex } from "./lyric";
|
||||
import { calculateProgress } from "./time";
|
||||
import { isElectron, isDev, shuffleArray } from "./helper";
|
||||
import { isElectron, isDev, shuffleArray, runIdle } from "./helper";
|
||||
import { heartRateList } from "@/api/playlist";
|
||||
import { formatSongsList } from "./format";
|
||||
import { isLogin } from "./auth";
|
||||
@@ -40,7 +40,6 @@ class Player {
|
||||
private analyser: AnalyserNode | null = null;
|
||||
private dataArray: Uint8Array<ArrayBuffer> | null = null;
|
||||
/** 其他数据 */
|
||||
private testNumber: number = 0;
|
||||
private message: MessageReactive | null = null;
|
||||
/** 预载下一首歌曲播放地址缓存(仅存 URL,不创建 Howl) */
|
||||
private nextPrefetch: { id: number; url: string | null; ublock: boolean } | null = null;
|
||||
@@ -48,6 +47,8 @@ class Player {
|
||||
private playSessionId: number = 0;
|
||||
/** 是否正在切换歌曲 */
|
||||
private switching: boolean = false;
|
||||
/** 当前曲目重试信息(按歌曲维度计数) */
|
||||
private retryInfo: { songId: number; count: number } = { songId: 0, count: 0 };
|
||||
constructor() {
|
||||
// 创建播放器实例
|
||||
this.player = new Howl({ src: [""], format: allowPlayFormat, autoplay: false });
|
||||
@@ -56,14 +57,51 @@ class Player {
|
||||
// 挂载全局
|
||||
window.$player = this;
|
||||
}
|
||||
/**
|
||||
* 新建会话并返回会话 id
|
||||
*/
|
||||
private newSession(): number {
|
||||
this.playSessionId += 1;
|
||||
return this.playSessionId;
|
||||
}
|
||||
/**
|
||||
* 检查传入会话是否过期
|
||||
*/
|
||||
private isStale(sessionId: number): boolean {
|
||||
return sessionId !== this.playSessionId;
|
||||
}
|
||||
/**
|
||||
* 保护执行:会话过期则早退
|
||||
*/
|
||||
private guard<T>(sessionId: number, fn: () => T): T | undefined {
|
||||
if (this.isStale(sessionId)) return;
|
||||
return fn();
|
||||
}
|
||||
/**
|
||||
* 重置底层播放器与定时器(幂等)
|
||||
*/
|
||||
private resetPlayerCore() {
|
||||
try {
|
||||
this.player?.off();
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
try {
|
||||
Howler.stop();
|
||||
Howler.unload();
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
this.cleanupAllTimers();
|
||||
}
|
||||
/**
|
||||
* 处理播放状态
|
||||
*/
|
||||
private handlePlayStatus(sessionId?: number) {
|
||||
private handlePlayStatus() {
|
||||
// const musicStore = useMusicStore();
|
||||
const statusStore = useStatusStore();
|
||||
const settingStore = useSettingStore();
|
||||
const currentSessionId = sessionId ?? this.playSessionId;
|
||||
const currentSessionId = this.playSessionId;
|
||||
// 清理定时器
|
||||
clearInterval(this.playerInterval);
|
||||
// 更新播放状态
|
||||
@@ -171,12 +209,7 @@ class Player {
|
||||
* @param autoPlay 是否自动播放
|
||||
* @param seek 播放位置
|
||||
*/
|
||||
private async createPlayer(
|
||||
src: string,
|
||||
autoPlay: boolean = true,
|
||||
seek: number = 0,
|
||||
sessionId?: number,
|
||||
) {
|
||||
private async createPlayer(src: string, autoPlay: boolean = true, seek: number = 0) {
|
||||
// 获取数据
|
||||
const dataStore = useDataStore();
|
||||
const musicStore = useMusicStore();
|
||||
@@ -184,23 +217,15 @@ class Player {
|
||||
const settingStore = useSettingStore();
|
||||
// 播放信息
|
||||
const { id, path, type } = musicStore.playSong;
|
||||
const currentSessionId = sessionId ?? this.playSessionId;
|
||||
const currentSessionId = this.playSessionId;
|
||||
// 检查会话是否过期
|
||||
if (currentSessionId !== this.playSessionId) {
|
||||
console.log("🚫 Session expired, skipping player creation");
|
||||
return;
|
||||
}
|
||||
// 清理播放器(移除事件,停止并卸载)
|
||||
try {
|
||||
this.player.off();
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
Howler.stop();
|
||||
Howler.unload();
|
||||
// 清理所有定时器
|
||||
this.cleanupAllTimers();
|
||||
// 再次检查会话是否过期(异步操作后)
|
||||
// 统一重置底层播放器
|
||||
this.resetPlayerCore();
|
||||
// 二次检查会话
|
||||
if (currentSessionId !== this.playSessionId) {
|
||||
console.log("🚫 Session expired after cleanup, aborting");
|
||||
return;
|
||||
@@ -217,27 +242,29 @@ class Player {
|
||||
rate: statusStore.playRate,
|
||||
});
|
||||
// 播放器事件(绑定当前会话)
|
||||
this.playerEvent({ seek, sessionId: currentSessionId });
|
||||
this.playerEvent({ seek });
|
||||
// 播放设备
|
||||
if (!settingStore.showSpectrums) this.toggleOutputDevice();
|
||||
// 自动播放(仅一次性触发)
|
||||
if (autoPlay) await this.play();
|
||||
// 获取歌曲附加信息 - 非电台和本地
|
||||
if (type !== "radio" && !path) {
|
||||
getLyricData(id);
|
||||
runIdle(() => getLyricData(id));
|
||||
} else resetSongLyric();
|
||||
// 定时获取状态
|
||||
if (!this.playerInterval) this.handlePlayStatus(currentSessionId);
|
||||
if (!this.playerInterval) this.handlePlayStatus();
|
||||
// 新增播放历史
|
||||
if (type !== "radio") dataStore.setHistory(musicStore.playSong);
|
||||
// 获取歌曲封面主色
|
||||
if (!path) getCoverColor(musicStore.songCover);
|
||||
if (!path) runIdle(() => getCoverColor(musicStore.songCover));
|
||||
// 更新 MediaSession
|
||||
if (!path) this.updateMediaSession();
|
||||
if (!path) runIdle(() => this.updateMediaSession());
|
||||
// 开发模式
|
||||
if (isDev) window.player = this.player;
|
||||
// 异步预载下一首播放地址(不阻塞当前播放)
|
||||
// 预载下一首播放地址
|
||||
runIdle(() => {
|
||||
void this.prefetchNextSongUrl();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 播放器事件
|
||||
@@ -246,8 +273,6 @@ class Player {
|
||||
options: {
|
||||
// 恢复进度
|
||||
seek?: number;
|
||||
// 当前会话 id,用于忽略过期事件
|
||||
sessionId?: number;
|
||||
} = { seek: 0 },
|
||||
) {
|
||||
// 获取数据
|
||||
@@ -257,7 +282,7 @@ class Player {
|
||||
const playSongData = getPlaySongData();
|
||||
// 获取配置
|
||||
const { seek } = options;
|
||||
const currentSessionId = options.sessionId ?? this.playSessionId;
|
||||
const currentSessionId = this.playSessionId;
|
||||
// 初次加载
|
||||
this.player.once("load", () => {
|
||||
if (currentSessionId !== this.playSessionId) return;
|
||||
@@ -284,6 +309,14 @@ class Player {
|
||||
}
|
||||
// 更新状态
|
||||
statusStore.playLoading = false;
|
||||
// 重置当前曲目重试计数
|
||||
try {
|
||||
const current = getPlaySongData();
|
||||
const sid = current?.type === "radio" ? current?.dj?.id : current?.id;
|
||||
this.retryInfo = { songId: Number(sid || 0), count: 0 };
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
// ipc
|
||||
if (isElectron) {
|
||||
window.electron.ipcRenderer.send("play-song-change", getPlayerInfo());
|
||||
@@ -297,6 +330,14 @@ class Player {
|
||||
this.player.on("play", () => {
|
||||
if (currentSessionId !== this.playSessionId) return;
|
||||
window.document.title = getPlayerInfo() || "SPlayer";
|
||||
// 重置重试计数
|
||||
try {
|
||||
const current = getPlaySongData();
|
||||
const sid = current?.type === "radio" ? current?.dj?.id : current?.id;
|
||||
this.retryInfo = { songId: Number(sid || 0), count: 0 };
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
// ipc
|
||||
if (isElectron) {
|
||||
window.electron.ipcRenderer.send("play-status-change", true);
|
||||
@@ -338,9 +379,15 @@ class Player {
|
||||
this.player.on("loaderror", (sourceid, err: unknown) => {
|
||||
if (currentSessionId !== this.playSessionId) return;
|
||||
const code = typeof err === "number" ? err : undefined;
|
||||
this.errorNext(code);
|
||||
this.handlePlaybackError(code);
|
||||
console.error("❌ song error:", sourceid, playSongData, err);
|
||||
});
|
||||
this.player.on("playerror", (sourceid, err: unknown) => {
|
||||
if (currentSessionId !== this.playSessionId) return;
|
||||
const code = typeof err === "number" ? err : undefined;
|
||||
this.handlePlaybackError(code);
|
||||
console.error("❌ song play error:", sourceid, playSongData, err);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 初始化 MediaSession
|
||||
@@ -440,27 +487,31 @@ class Player {
|
||||
updateSpectrumData();
|
||||
}
|
||||
/**
|
||||
* 播放错误
|
||||
* 在播放错误时,播放下一首
|
||||
* 集中处理播放错误与重试策略
|
||||
*/
|
||||
private async errorNext(errCode?: number) {
|
||||
private async handlePlaybackError(errCode?: number) {
|
||||
const dataStore = useDataStore();
|
||||
// 次数加一
|
||||
this.testNumber++;
|
||||
if (this.testNumber > 5) {
|
||||
this.testNumber = 0;
|
||||
this.resetStatus();
|
||||
window.$message.error("当前重试次数过多,请稍后再试");
|
||||
return;
|
||||
const playSongData = getPlaySongData();
|
||||
const currentSongId = playSongData?.type === "radio" ? playSongData.dj?.id : playSongData?.id;
|
||||
// 初始化/切换曲目时重置计数
|
||||
if (!this.retryInfo.songId || this.retryInfo.songId !== Number(currentSongId || 0)) {
|
||||
this.retryInfo = { songId: Number(currentSongId || 0), count: 0 };
|
||||
}
|
||||
// 错误 2 通常为网络地址过期
|
||||
if (errCode === 2) {
|
||||
// 重载播放器
|
||||
this.retryInfo.count += 1;
|
||||
// 错误码 2:资源过期或临时网络错误,允许较少次数的刷新
|
||||
if (errCode === 2 && this.retryInfo.count <= 2) {
|
||||
await this.initPlayer(true, this.getSeek());
|
||||
return;
|
||||
}
|
||||
// 播放下一曲
|
||||
// 其它错误:最多 3 次
|
||||
if (this.retryInfo.count <= 3) {
|
||||
await this.initPlayer(true, 0);
|
||||
return;
|
||||
}
|
||||
// 超过次数:切到下一首或清空
|
||||
this.retryInfo.count = 0;
|
||||
if (dataStore.playList.length > 1) {
|
||||
window.$message.error("当前歌曲播放失败,已跳至下一首");
|
||||
await this.nextOrPrev("next");
|
||||
} else {
|
||||
window.$message.error("当前列表暂无可播放歌曲");
|
||||
@@ -498,12 +549,12 @@ class Player {
|
||||
musicStore.playSong.cover = "/images/song.jpg?assest";
|
||||
}
|
||||
// 获取主色
|
||||
getCoverColor(musicStore.playSong.cover);
|
||||
runIdle(() => getCoverColor(musicStore.playSong.cover));
|
||||
// 获取歌词数据
|
||||
const { lyric, format } = await window.electron.ipcRenderer.invoke("get-music-lyric", path);
|
||||
parseLocalLyric(lyric, format);
|
||||
// 更新媒体会话
|
||||
this.updateMediaSession();
|
||||
runIdle(() => this.updateMediaSession());
|
||||
} catch (error) {
|
||||
window.$message.error("获取本地歌曲元信息失败");
|
||||
console.error("Failed to parse local music info:", error);
|
||||
@@ -540,7 +591,7 @@ class Player {
|
||||
const musicStore = useMusicStore();
|
||||
const statusStore = useStatusStore();
|
||||
const settingStore = useSettingStore();
|
||||
const sessionId = ++this.playSessionId;
|
||||
const sessionId = this.newSession();
|
||||
try {
|
||||
// 获取播放数据
|
||||
const playSongData = getPlaySongData();
|
||||
@@ -550,9 +601,12 @@ class Player {
|
||||
musicStore.playSong = playSongData;
|
||||
// 更改状态
|
||||
statusStore.playLoading = true;
|
||||
// 清理旧播放器与计时器
|
||||
this.resetPlayerCore();
|
||||
// 本地歌曲
|
||||
if (path) {
|
||||
await this.createPlayer(`file://${path}`, autoPlay, seek, sessionId);
|
||||
if (this.isStale(sessionId)) return;
|
||||
await this.createPlayer(`file://${path}`, autoPlay, seek);
|
||||
// 获取歌曲元信息
|
||||
await this.parseLocalMusicInfo(path);
|
||||
}
|
||||
@@ -564,7 +618,8 @@ class Player {
|
||||
const cached = this.nextPrefetch;
|
||||
if (cached && cached.id === songId && cached.url) {
|
||||
statusStore.playUblock = cached.ublock;
|
||||
await this.createPlayer(cached.url, autoPlay, seek, sessionId);
|
||||
if (this.isStale(sessionId)) return;
|
||||
await this.createPlayer(cached.url, autoPlay, seek);
|
||||
} else {
|
||||
// 官方地址失败或仅为试听时再尝试解锁(Electron 且非电台且开启解灰)
|
||||
const canUnlock = isElectron && type !== "radio" && settingStore.useSongUnlock;
|
||||
@@ -572,20 +627,23 @@ class Player {
|
||||
if (officialUrl && !isTrial) {
|
||||
// 官方可播放且非试听
|
||||
statusStore.playUblock = false;
|
||||
await this.createPlayer(officialUrl, autoPlay, seek, sessionId);
|
||||
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);
|
||||
await this.createPlayer(unlockUrl, autoPlay, seek, sessionId);
|
||||
if (this.isStale(sessionId)) return;
|
||||
await this.createPlayer(unlockUrl, autoPlay, seek);
|
||||
} else if (officialUrl) {
|
||||
// 解锁失败,若允许试听则播放试听
|
||||
if (isTrial && settingStore.playSongDemo) {
|
||||
window.$message.warning("当前歌曲仅可试听,请开通会员后重试");
|
||||
statusStore.playUblock = false;
|
||||
await this.createPlayer(officialUrl, autoPlay, seek, sessionId);
|
||||
if (this.isStale(sessionId)) return;
|
||||
await this.createPlayer(officialUrl, autoPlay, seek);
|
||||
} else {
|
||||
// 不允许试听
|
||||
statusStore.playUblock = false;
|
||||
@@ -663,6 +721,7 @@ class Player {
|
||||
async pause(changeStatus: boolean = true) {
|
||||
const statusStore = useStatusStore();
|
||||
const settingStore = useSettingStore();
|
||||
const localSession = this.playSessionId;
|
||||
|
||||
// 播放器未加载完成或不存在
|
||||
if (!this.player || this.player.state() !== "loaded") {
|
||||
@@ -675,7 +734,7 @@ class Player {
|
||||
await new Promise<void>((resolve) => {
|
||||
this.player.fade(statusStore.playVolume, 0, settingStore.getFadeTime);
|
||||
this.player.once("fade", () => {
|
||||
this.player.pause();
|
||||
this.guard(localSession, () => this.player.pause());
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
38
src/views/DesktopLyrics/index.d.ts
vendored
Normal file
38
src/views/DesktopLyrics/index.d.ts
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
import { LyricType } from "@/types/main";
|
||||
|
||||
/** 桌面歌词数据 */
|
||||
export interface LyricData {
|
||||
/** 播放歌曲名称 */
|
||||
playName: string;
|
||||
/** 播放状态 */
|
||||
playStatus: boolean;
|
||||
/** 播放进度 */
|
||||
progress: number;
|
||||
/** 歌词数据 */
|
||||
lrcData: LyricType[];
|
||||
yrcData: LyricType[];
|
||||
}
|
||||
|
||||
/** 桌面歌词配置 */
|
||||
export interface LyricConfig {
|
||||
/** 是否锁定歌词 */
|
||||
isLock: boolean;
|
||||
/** 已播放颜色 */
|
||||
playedColor: string;
|
||||
/** 未播放颜色 */
|
||||
unplayedColor: string;
|
||||
/** 描边 */
|
||||
stroke: string;
|
||||
/** 描边宽度 */
|
||||
strokeWidth: number;
|
||||
/** 字体 */
|
||||
fontFamily: string;
|
||||
/** 字体大小 */
|
||||
fontSize: number;
|
||||
/** 行高 */
|
||||
lineHeight: number;
|
||||
/** 是否双行 */
|
||||
isDoubleLine: boolean;
|
||||
/** 文本排版位置 */
|
||||
position: "left" | "center" | "right" | "both";
|
||||
}
|
||||
@@ -4,45 +4,71 @@
|
||||
{{ lyricData.playName }}
|
||||
{{ lyricConfig }}
|
||||
456456
|
||||
<n-flex class="menu" align="center" justify="space-between">
|
||||
<n-flex class="name">
|
||||
<div class="menu-btn">
|
||||
<SvgIcon name="Logo" />
|
||||
<div class="header" align="center" justify="space-between">
|
||||
<n-flex :wrap="false" align="center" justify="flex-start" size="small">
|
||||
<div class="menu-btn" title="返回应用">
|
||||
<SvgIcon name="Music" />
|
||||
</div>
|
||||
<div class="menu-btn" title="增加字体大小">
|
||||
<SvgIcon :offset="-1" name="TextSizeAdd" />
|
||||
</div>
|
||||
<div class="menu-btn" title="减少字体大小">
|
||||
<SvgIcon :offset="-1" name="TextSizeReduce" />
|
||||
</div>
|
||||
<span class="song-name">{{ lyricData.playName }}</span>
|
||||
</n-flex>
|
||||
<n-flex :wrap="false" align="center" justify="center" size="small">
|
||||
<div class="menu-btn" title="上一曲">
|
||||
<SvgIcon name="SkipPrev" />
|
||||
</div>
|
||||
<div class="menu-btn" :title="lyricData.playStatus ? '暂停' : '播放'">
|
||||
<SvgIcon :name="lyricData.playStatus ? 'Pause' : 'Play'" />
|
||||
</div>
|
||||
<div class="menu-btn" title="下一曲">
|
||||
<SvgIcon name="SkipNext" />
|
||||
</div>
|
||||
</n-flex>
|
||||
<n-flex :wrap="false" align="center" justify="flex-end" size="small">
|
||||
<div class="menu-btn" title="锁定">
|
||||
<SvgIcon name="Lock" />
|
||||
</div>
|
||||
<div class="menu-btn" title="解锁">
|
||||
<SvgIcon name="LockOpen" />
|
||||
</div>
|
||||
<div class="menu-btn" title="关闭">
|
||||
<SvgIcon name="Close" />
|
||||
</div>
|
||||
</n-flex>
|
||||
</div>
|
||||
</div>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { LyricType } from "@/types/main";
|
||||
import { Position } from "@vueuse/core";
|
||||
import { LyricConfig, LyricData } from ".";
|
||||
|
||||
// 桌面歌词数据
|
||||
const lyricData = reactive<{
|
||||
playName: string;
|
||||
playStatus: boolean;
|
||||
progress: number;
|
||||
lrcData: LyricType[];
|
||||
yrcData: LyricType[];
|
||||
lyricIndex: number;
|
||||
}>({
|
||||
const lyricData = reactive<LyricData>({
|
||||
playName: "未知歌曲",
|
||||
playStatus: false,
|
||||
progress: 0,
|
||||
lrcData: [],
|
||||
yrcData: [],
|
||||
lyricIndex: -1,
|
||||
});
|
||||
|
||||
// 桌面歌词配置
|
||||
const lyricConfig = reactive<{
|
||||
fontSize: number;
|
||||
lineHeight: number;
|
||||
}>({
|
||||
const lyricConfig = reactive<LyricConfig>({
|
||||
isLock: false,
|
||||
playedColor: "#fff",
|
||||
unplayedColor: "#ccc",
|
||||
stroke: "#000",
|
||||
strokeWidth: 2,
|
||||
fontFamily: "system-ui",
|
||||
fontSize: 24,
|
||||
lineHeight: 48,
|
||||
isDoubleLine: false,
|
||||
position: "center",
|
||||
});
|
||||
|
||||
// 桌面歌词元素
|
||||
@@ -57,6 +83,10 @@ const lyricDragMove = (position: Position) => {
|
||||
useDraggable(desktopLyricsRef, {
|
||||
onMove: lyricDragMove,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 接收歌词配置
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -68,16 +98,62 @@ useDraggable(desktopLyricsRef, {
|
||||
overflow: hidden;
|
||||
transition: background-color 0.3s;
|
||||
cursor: move;
|
||||
.header {
|
||||
opacity: 0;
|
||||
margin-bottom: 12px;
|
||||
transition: opacity 0.3s;
|
||||
// 子内容三等分grid
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-gap: 12px;
|
||||
> * {
|
||||
min-width: 0;
|
||||
}
|
||||
.song-name {
|
||||
font-size: 1em;
|
||||
text-align: left;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.menu-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
padding: 6px;
|
||||
border-radius: 8px;
|
||||
will-change: transform;
|
||||
transition:
|
||||
background-color 0.3s,
|
||||
transform 0.3s;
|
||||
cursor: pointer;
|
||||
.n-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
&:not(.lock) {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
.header {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- <style>
|
||||
<style>
|
||||
body {
|
||||
background-image: url("https://picsum.photos/1920/1080");
|
||||
}
|
||||
</style> -->
|
||||
</style>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"types": ["node", "electron", "electron-vite/node", "./auto-imports.d.ts"],
|
||||
"types": ["node", "electron", "electron-vite/node", "./auto-imports.d.ts", "./components.d.ts"],
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"target": "es2022"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user