2025-03-31 23:05:19 +08:00
|
|
|
|
import { cloneDeep } from 'lodash';
|
2025-02-19 01:01:43 +08:00
|
|
|
|
import { createDiscreteApi } from 'naive-ui';
|
2025-08-07 22:57:17 +08:00
|
|
|
|
import { computed, type ComputedRef, nextTick, onUnmounted, ref, watch } from 'vue';
|
2024-10-18 18:37:53 +08:00
|
|
|
|
|
2025-01-16 23:19:16 +08:00
|
|
|
|
import useIndexedDB from '@/hooks/IndexDBHook';
|
2024-12-12 22:18:52 +08:00
|
|
|
|
import { audioService } from '@/services/audioService';
|
2025-07-23 23:54:35 +08:00
|
|
|
|
import type { usePlayerStore } from '@/store';
|
2025-08-07 22:57:17 +08:00
|
|
|
|
import type { Artist, ILyricText, SongResult } from '@/types/music';
|
2025-01-01 02:25:18 +08:00
|
|
|
|
import { isElectron } from '@/utils';
|
2025-01-10 22:49:55 +08:00
|
|
|
|
import { getTextColors } from '@/utils/linearColor';
|
2025-10-11 20:23:54 +08:00
|
|
|
|
import { parseLyrics } from '@/utils/yrcParser';
|
2025-02-19 01:01:43 +08:00
|
|
|
|
|
2024-09-12 16:44:42 +08:00
|
|
|
|
const windowData = window as any;
|
|
|
|
|
|
|
2025-07-23 23:54:35 +08:00
|
|
|
|
// 全局 playerStore 引用,通过 initMusicHook 函数注入
|
|
|
|
|
|
let playerStore: ReturnType<typeof usePlayerStore> | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化函数,接受 store 实例
|
|
|
|
|
|
export const initMusicHook = (store: ReturnType<typeof usePlayerStore>) => {
|
|
|
|
|
|
playerStore = store;
|
|
|
|
|
|
|
|
|
|
|
|
// 创建 computed 属性
|
|
|
|
|
|
playMusic = computed(() => getPlayerStore().playMusic as SongResult);
|
|
|
|
|
|
artistList = computed(
|
|
|
|
|
|
() => (getPlayerStore().playMusic.ar || getPlayerStore().playMusic?.song?.artists) as Artist[]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 在 store 注入后初始化需要 store 的功能
|
|
|
|
|
|
setupKeyboardListeners();
|
|
|
|
|
|
setupMusicWatchers();
|
|
|
|
|
|
setupCorrectionTimeWatcher();
|
|
|
|
|
|
setupPlayStateWatcher();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取 playerStore 的辅助函数
|
|
|
|
|
|
const getPlayerStore = () => {
|
|
|
|
|
|
if (!playerStore) {
|
|
|
|
|
|
throw new Error('MusicHook not initialized. Call initMusicHook first.');
|
|
|
|
|
|
}
|
|
|
|
|
|
return playerStore;
|
|
|
|
|
|
};
|
2024-10-18 18:37:53 +08:00
|
|
|
|
export const lrcArray = ref<ILyricText[]>([]); // 歌词数组
|
|
|
|
|
|
export const lrcTimeArray = ref<number[]>([]); // 歌词时间数组
|
|
|
|
|
|
export const nowTime = ref(0); // 当前播放时间
|
|
|
|
|
|
export const allTime = ref(0); // 总播放时间
|
|
|
|
|
|
export const nowIndex = ref(0); // 当前播放歌词
|
|
|
|
|
|
export const currentLrcProgress = ref(0); // 来存储当前歌词的进度
|
2024-12-12 22:18:52 +08:00
|
|
|
|
export const sound = ref<Howl | null>(audioService.getCurrentSound());
|
2024-12-16 22:12:28 +08:00
|
|
|
|
export const isLyricWindowOpen = ref(false); // 新增状态
|
2025-01-10 22:49:55 +08:00
|
|
|
|
export const textColors = ref<any>(getTextColors());
|
2025-07-23 23:54:35 +08:00
|
|
|
|
|
|
|
|
|
|
// 这些 computed 属性需要在初始化后创建
|
|
|
|
|
|
export let playMusic: ComputedRef<SongResult>;
|
|
|
|
|
|
export let artistList: ComputedRef<Artist[]>;
|
2025-01-16 23:19:16 +08:00
|
|
|
|
|
2025-09-14 01:03:29 +08:00
|
|
|
|
export const musicDB = await useIndexedDB(
|
|
|
|
|
|
'musicDB',
|
|
|
|
|
|
[
|
|
|
|
|
|
{ name: 'music', keyPath: 'id' },
|
|
|
|
|
|
{ name: 'music_lyric', keyPath: 'id' },
|
|
|
|
|
|
{ name: 'api_cache', keyPath: 'id' },
|
|
|
|
|
|
{ name: 'music_url_cache', keyPath: 'id' },
|
|
|
|
|
|
{ name: 'music_failed_cache', keyPath: 'id' }
|
|
|
|
|
|
],
|
2025-10-11 20:23:54 +08:00
|
|
|
|
3
|
2025-09-14 01:03:29 +08:00
|
|
|
|
);
|
2024-12-12 22:18:52 +08:00
|
|
|
|
|
2025-07-23 23:54:35 +08:00
|
|
|
|
// 键盘事件处理器,在初始化后设置
|
|
|
|
|
|
const setupKeyboardListeners = () => {
|
|
|
|
|
|
document.onkeyup = (e) => {
|
|
|
|
|
|
// 检查事件目标是否是输入框元素
|
|
|
|
|
|
const target = e.target as HTMLElement;
|
|
|
|
|
|
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2024-12-14 13:00:06 +08:00
|
|
|
|
|
2025-07-23 23:54:35 +08:00
|
|
|
|
const store = getPlayerStore();
|
|
|
|
|
|
switch (e.code) {
|
|
|
|
|
|
case 'Space':
|
2026-03-04 19:53:50 +08:00
|
|
|
|
if (store.playMusic?.id) {
|
|
|
|
|
|
void store.setPlay({ ...store.playMusic });
|
2025-07-23 23:54:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2024-12-12 22:18:52 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-01-17 22:35:42 +08:00
|
|
|
|
const { message } = createDiscreteApi(['message']);
|
|
|
|
|
|
|
2026-03-04 19:53:50 +08:00
|
|
|
|
let audioListenersInitialized = false;
|
2025-03-08 18:31:46 +08:00
|
|
|
|
|
2025-10-11 20:23:54 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 解析歌词字符串并转换为ILyricText格式
|
|
|
|
|
|
* @param lyricsStr 歌词字符串
|
|
|
|
|
|
* @returns 解析后的歌词数据
|
|
|
|
|
|
*/
|
|
|
|
|
|
const parseLyricsString = async (
|
|
|
|
|
|
lyricsStr: string
|
|
|
|
|
|
): Promise<{ lrcArray: ILyricText[]; lrcTimeArray: number[]; hasWordByWord: boolean }> => {
|
|
|
|
|
|
if (!lyricsStr || typeof lyricsStr !== 'string') {
|
|
|
|
|
|
return { lrcArray: [], lrcTimeArray: [], hasWordByWord: false };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const parseResult = parseLyrics(lyricsStr);
|
2025-10-12 17:11:48 +08:00
|
|
|
|
console.log('parseResult', parseResult);
|
2025-10-11 20:23:54 +08:00
|
|
|
|
|
|
|
|
|
|
if (!parseResult.success) {
|
|
|
|
|
|
console.error('歌词解析失败:', parseResult.error.message);
|
|
|
|
|
|
return { lrcArray: [], lrcTimeArray: [], hasWordByWord: false };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const { lyrics } = parseResult.data;
|
|
|
|
|
|
const lrcArray: ILyricText[] = [];
|
|
|
|
|
|
const lrcTimeArray: number[] = [];
|
|
|
|
|
|
let hasWordByWord = false;
|
|
|
|
|
|
|
|
|
|
|
|
for (const line of lyrics) {
|
|
|
|
|
|
// 检查是否有逐字歌词
|
|
|
|
|
|
const hasWords = line.words && line.words.length > 0;
|
|
|
|
|
|
if (hasWords) {
|
|
|
|
|
|
hasWordByWord = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
lrcArray.push({
|
|
|
|
|
|
text: line.fullText,
|
|
|
|
|
|
trText: '', // 翻译文本稍后处理
|
|
|
|
|
|
words: hasWords
|
|
|
|
|
|
? line.words.map((word) => ({
|
2025-10-12 17:11:48 +08:00
|
|
|
|
...word
|
2025-10-11 20:23:54 +08:00
|
|
|
|
}))
|
|
|
|
|
|
: undefined,
|
|
|
|
|
|
hasWordByWord: hasWords,
|
|
|
|
|
|
startTime: line.startTime,
|
|
|
|
|
|
duration: line.duration
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
lrcTimeArray.push(line.startTime);
|
|
|
|
|
|
}
|
|
|
|
|
|
return { lrcArray, lrcTimeArray, hasWordByWord };
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('解析歌词时发生错误:', error);
|
|
|
|
|
|
return { lrcArray: [], lrcTimeArray: [], hasWordByWord: false };
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-07-23 23:54:35 +08:00
|
|
|
|
// 设置音乐相关的监听器
|
|
|
|
|
|
const setupMusicWatchers = () => {
|
|
|
|
|
|
const store = getPlayerStore();
|
2025-03-08 19:00:50 +08:00
|
|
|
|
|
2025-07-23 23:54:35 +08:00
|
|
|
|
// 监听 playerStore.playMusic 的变化以更新歌词数据
|
|
|
|
|
|
watch(
|
2025-10-11 20:23:54 +08:00
|
|
|
|
() => store.playMusic.id,
|
|
|
|
|
|
async (newId, oldId) => {
|
|
|
|
|
|
// 如果没有歌曲ID,清空歌词
|
|
|
|
|
|
if (!newId) {
|
|
|
|
|
|
lrcArray.value = [];
|
|
|
|
|
|
lrcTimeArray.value = [];
|
|
|
|
|
|
nowIndex.value = 0;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 避免相同ID的重复执行(但允许初始化时执行)
|
|
|
|
|
|
if (newId === oldId && lrcArray.value.length > 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 歌曲切换时重置歌词索引
|
|
|
|
|
|
if (newId !== oldId) {
|
|
|
|
|
|
nowIndex.value = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await nextTick(async () => {
|
2025-07-23 23:54:35 +08:00
|
|
|
|
console.log('歌曲切换,更新歌词数据');
|
|
|
|
|
|
|
2025-10-11 20:23:54 +08:00
|
|
|
|
// 检查是否有原始歌词字符串需要解析
|
|
|
|
|
|
const lyricData = playMusic.value.lyric;
|
|
|
|
|
|
if (lyricData && typeof lyricData === 'string') {
|
|
|
|
|
|
// 如果歌词是字符串格式,使用新的解析器
|
|
|
|
|
|
const {
|
|
|
|
|
|
lrcArray: parsedLrcArray,
|
|
|
|
|
|
lrcTimeArray: parsedTimeArray,
|
|
|
|
|
|
hasWordByWord
|
|
|
|
|
|
} = await parseLyricsString(lyricData);
|
|
|
|
|
|
lrcArray.value = parsedLrcArray;
|
|
|
|
|
|
lrcTimeArray.value = parsedTimeArray;
|
|
|
|
|
|
|
|
|
|
|
|
// 更新歌曲的歌词数据结构
|
|
|
|
|
|
if (playMusic.value.lyric && typeof playMusic.value.lyric === 'object') {
|
|
|
|
|
|
playMusic.value.lyric.hasWordByWord = hasWordByWord;
|
|
|
|
|
|
}
|
2026-03-27 23:00:39 +08:00
|
|
|
|
} else if (lyricData && typeof lyricData === 'object' && lyricData.lrcArray?.length > 0) {
|
2025-10-11 20:23:54 +08:00
|
|
|
|
// 使用现有的歌词数据结构
|
2026-03-27 23:00:39 +08:00
|
|
|
|
const rawLrc = lyricData.lrcArray || [];
|
|
|
|
|
|
lrcTimeArray.value = lyricData.lrcTimeArray || [];
|
2025-10-11 20:23:54 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { translateLyrics } = await import('@/services/lyricTranslation');
|
|
|
|
|
|
lrcArray.value = await translateLyrics(rawLrc as any);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('翻译歌词失败,使用原始歌词:', e);
|
|
|
|
|
|
lrcArray.value = rawLrc as any;
|
|
|
|
|
|
}
|
2026-03-27 23:00:39 +08:00
|
|
|
|
} else if (isElectron && playMusic.value.playMusicUrl?.startsWith('local:///')) {
|
|
|
|
|
|
// 从下载/本地文件的 ID3/FLAC 元数据中提取嵌入歌词
|
|
|
|
|
|
try {
|
|
|
|
|
|
let filePath = decodeURIComponent(
|
|
|
|
|
|
playMusic.value.playMusicUrl.replace('local:///', '')
|
|
|
|
|
|
);
|
|
|
|
|
|
// 处理 Windows 路径:/C:/... → C:/...
|
|
|
|
|
|
if (/^\/[a-zA-Z]:\//.test(filePath)) {
|
|
|
|
|
|
filePath = filePath.slice(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
const embeddedLyrics = await window.api.getEmbeddedLyrics(filePath);
|
|
|
|
|
|
if (embeddedLyrics) {
|
|
|
|
|
|
const {
|
|
|
|
|
|
lrcArray: parsedLrcArray,
|
|
|
|
|
|
lrcTimeArray: parsedTimeArray,
|
|
|
|
|
|
hasWordByWord
|
|
|
|
|
|
} = await parseLyricsString(embeddedLyrics);
|
|
|
|
|
|
lrcArray.value = parsedLrcArray;
|
|
|
|
|
|
lrcTimeArray.value = parsedTimeArray;
|
|
|
|
|
|
if (playMusic.value.lyric && typeof playMusic.value.lyric === 'object') {
|
|
|
|
|
|
(playMusic.value.lyric as any).hasWordByWord = hasWordByWord;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 无嵌入歌词 — 若有数字 ID,尝试 API 兜底
|
|
|
|
|
|
const songId = playMusic.value.id;
|
|
|
|
|
|
if (songId && typeof songId === 'number') {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { getMusicLrc } = await import('@/api/music');
|
|
|
|
|
|
const res = await getMusicLrc(songId);
|
|
|
|
|
|
if (res?.data?.lrc?.lyric) {
|
|
|
|
|
|
const { lrcArray: apiLrcArray, lrcTimeArray: apiTimeArray } =
|
|
|
|
|
|
await parseLyricsString(res.data.lrc.lyric);
|
|
|
|
|
|
lrcArray.value = apiLrcArray;
|
|
|
|
|
|
lrcTimeArray.value = apiTimeArray;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (apiErr) {
|
|
|
|
|
|
console.error('API lyrics fallback failed:', apiErr);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('Failed to extract embedded lyrics:', err);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 无歌词数据
|
|
|
|
|
|
lrcArray.value = [];
|
|
|
|
|
|
lrcTimeArray.value = [];
|
2025-10-11 20:23:54 +08:00
|
|
|
|
}
|
2025-07-23 23:54:35 +08:00
|
|
|
|
// 当歌词数据更新时,如果歌词窗口打开,则发送数据
|
|
|
|
|
|
if (isElectron && isLyricWindowOpen.value) {
|
|
|
|
|
|
console.log('歌词窗口已打开,同步最新歌词数据');
|
|
|
|
|
|
// 不管歌词数组是否为空,都发送最新数据
|
2025-03-08 19:00:50 +08:00
|
|
|
|
sendLyricToWin();
|
2025-07-23 23:54:35 +08:00
|
|
|
|
|
|
|
|
|
|
// 再次延迟发送,确保歌词窗口已完全加载
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
sendLyricToWin();
|
|
|
|
|
|
}, 500);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
2025-10-11 20:23:54 +08:00
|
|
|
|
{ immediate: true }
|
2025-07-23 23:54:35 +08:00
|
|
|
|
);
|
|
|
|
|
|
};
|
2024-12-12 22:18:52 +08:00
|
|
|
|
|
2025-01-12 19:14:25 +08:00
|
|
|
|
const setupAudioListeners = () => {
|
2026-03-04 19:53:50 +08:00
|
|
|
|
// 监听器只注册一次,避免重复绑定和误清理全局恢复监听器
|
|
|
|
|
|
if (audioListenersInitialized) {
|
|
|
|
|
|
return () => {};
|
|
|
|
|
|
}
|
|
|
|
|
|
audioListenersInitialized = true;
|
|
|
|
|
|
|
2026-03-12 18:09:20 +08:00
|
|
|
|
let interval: number | null = null;
|
2026-02-06 20:34:07 +08:00
|
|
|
|
// 播放状态恢复定时器:当 interval 因异常被清除时,自动恢复
|
2026-03-12 18:09:20 +08:00
|
|
|
|
let recoveryTimer: number | null = null;
|
2026-03-12 18:07:20 +08:00
|
|
|
|
let lyricThrottleCounter = 0;
|
2026-03-12 18:09:20 +08:00
|
|
|
|
let lastSavedProgress = 0;
|
2024-12-12 22:18:52 +08:00
|
|
|
|
|
2025-01-16 23:19:16 +08:00
|
|
|
|
const clearInterval = () => {
|
|
|
|
|
|
if (interval) {
|
|
|
|
|
|
window.clearInterval(interval);
|
|
|
|
|
|
interval = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-06 20:34:07 +08:00
|
|
|
|
const stopRecovery = () => {
|
|
|
|
|
|
if (recoveryTimer) {
|
|
|
|
|
|
window.clearInterval(recoveryTimer);
|
|
|
|
|
|
recoveryTimer = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 启动进度更新 interval
|
|
|
|
|
|
* 从 audioService 实时获取 sound 引用,避免闭包中 sound.value 过期
|
|
|
|
|
|
*/
|
|
|
|
|
|
const startProgressInterval = () => {
|
|
|
|
|
|
clearInterval();
|
|
|
|
|
|
interval = window.setInterval(() => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 每次从 audioService 获取最新的 sound 引用,而不是依赖闭包中的 sound.value
|
|
|
|
|
|
const currentSound = audioService.getCurrentSound();
|
|
|
|
|
|
if (!currentSound) {
|
|
|
|
|
|
// sound 暂时为空(可能在切歌/重建中),不清除 interval,等待恢复
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (typeof currentSound.seek !== 'function') {
|
|
|
|
|
|
// seek 方法不可用,跳过本次更新,不清除 interval
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const currentTime = currentSound.seek() as number;
|
|
|
|
|
|
if (typeof currentTime !== 'number' || Number.isNaN(currentTime)) {
|
|
|
|
|
|
// 无效时间,跳过本次更新
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 同步 sound.value 引用(确保外部也能拿到最新的)
|
|
|
|
|
|
if (sound.value !== currentSound) {
|
|
|
|
|
|
sound.value = currentSound;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
nowTime.value = currentTime;
|
|
|
|
|
|
allTime.value = currentSound.duration() as number;
|
2026-03-12 18:09:20 +08:00
|
|
|
|
|
|
|
|
|
|
// === 歌词索引更新 ===
|
2026-02-06 20:34:07 +08:00
|
|
|
|
const newIndex = getLrcIndex(nowTime.value);
|
|
|
|
|
|
if (newIndex !== nowIndex.value) {
|
|
|
|
|
|
nowIndex.value = newIndex;
|
2026-03-12 18:09:20 +08:00
|
|
|
|
currentLrcProgress.value = 0; // 换行时重置进度
|
2026-02-06 20:34:07 +08:00
|
|
|
|
if (isElectron && isLyricWindowOpen.value) {
|
|
|
|
|
|
sendLyricToWin();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-12 18:09:20 +08:00
|
|
|
|
|
|
|
|
|
|
// === 逐字歌词行内进度 ===
|
|
|
|
|
|
const { start, end } = currentLrcTiming.value;
|
|
|
|
|
|
if (typeof start === 'number' && typeof end === 'number' && start !== end) {
|
|
|
|
|
|
const elapsed = currentTime - start;
|
|
|
|
|
|
const duration = end - start;
|
|
|
|
|
|
const progress = (elapsed / duration) * 100;
|
|
|
|
|
|
currentLrcProgress.value = Math.min(Math.max(progress, 0), 100);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// === 节流发送轻量歌词进度更新(每 ~200ms / 约每 4 个 tick)===
|
2026-03-12 18:07:20 +08:00
|
|
|
|
lyricThrottleCounter++;
|
|
|
|
|
|
if (isElectron && isLyricWindowOpen.value && lyricThrottleCounter % 4 === 0) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
window.api.sendLyric(
|
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
|
type: 'update',
|
|
|
|
|
|
nowIndex: nowIndex.value,
|
|
|
|
|
|
nowTime: nowTime.value,
|
|
|
|
|
|
isPlay: getPlayerStore().play
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// 忽略发送失败
|
|
|
|
|
|
}
|
2026-02-06 20:34:07 +08:00
|
|
|
|
}
|
2026-03-12 18:09:20 +08:00
|
|
|
|
|
|
|
|
|
|
// === localStorage 进度保存(每 ~2 秒)===
|
|
|
|
|
|
if (
|
|
|
|
|
|
Math.floor(currentTime) % 2 === 0 &&
|
|
|
|
|
|
Math.floor(currentTime) !== Math.floor(lastSavedProgress)
|
|
|
|
|
|
) {
|
|
|
|
|
|
lastSavedProgress = currentTime;
|
|
|
|
|
|
if (getPlayerStore().playMusic?.id) {
|
|
|
|
|
|
localStorage.setItem(
|
|
|
|
|
|
'playProgress',
|
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
|
songId: getPlayerStore().playMusic.id,
|
|
|
|
|
|
progress: currentTime
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-06 20:34:07 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('进度更新 interval 出错:', error);
|
|
|
|
|
|
// 出错时不清除 interval,让下一次 tick 继续尝试
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 50);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 启动播放状态恢复监控
|
|
|
|
|
|
* 每 500ms 检查一次:如果 store 认为在播放但 interval 已丢失,则恢复
|
|
|
|
|
|
*/
|
|
|
|
|
|
const startRecoveryMonitor = () => {
|
|
|
|
|
|
stopRecovery();
|
|
|
|
|
|
recoveryTimer = window.setInterval(() => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const store = getPlayerStore();
|
|
|
|
|
|
if (store.play && !interval) {
|
|
|
|
|
|
const currentSound = audioService.getCurrentSound();
|
|
|
|
|
|
if (currentSound && currentSound.playing()) {
|
|
|
|
|
|
console.warn('[MusicHook] 检测到播放中但 interval 丢失,自动恢复');
|
|
|
|
|
|
startProgressInterval();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// 静默忽略
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 500);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 启动恢复监控
|
|
|
|
|
|
startRecoveryMonitor();
|
|
|
|
|
|
|
2025-03-31 22:57:00 +08:00
|
|
|
|
// 监听seek开始事件,立即更新UI
|
|
|
|
|
|
audioService.on('seek_start', (time) => {
|
|
|
|
|
|
// 直接更新显示位置,不检查拖动状态
|
|
|
|
|
|
nowTime.value = time;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 监听seek完成事件
|
|
|
|
|
|
audioService.on('seek', () => {
|
|
|
|
|
|
try {
|
2026-02-06 20:34:07 +08:00
|
|
|
|
const currentSound = audioService.getCurrentSound();
|
2025-03-31 22:57:00 +08:00
|
|
|
|
if (currentSound) {
|
|
|
|
|
|
// 立即更新显示时间,不进行任何检查
|
|
|
|
|
|
const currentTime = currentSound.seek() as number;
|
|
|
|
|
|
if (typeof currentTime === 'number' && !Number.isNaN(currentTime)) {
|
|
|
|
|
|
nowTime.value = currentTime;
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否需要更新歌词
|
|
|
|
|
|
const newIndex = getLrcIndex(nowTime.value);
|
|
|
|
|
|
if (newIndex !== nowIndex.value) {
|
|
|
|
|
|
nowIndex.value = newIndex;
|
|
|
|
|
|
if (isElectron && isLyricWindowOpen.value) {
|
|
|
|
|
|
sendLyricToWin();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('处理seek事件出错:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-03-08 18:31:46 +08:00
|
|
|
|
// 立即更新一次时间和进度(解决初始化时进度条不显示问题)
|
|
|
|
|
|
const updateCurrentTimeAndDuration = () => {
|
|
|
|
|
|
const currentSound = audioService.getCurrentSound();
|
|
|
|
|
|
if (currentSound) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 更新当前时间和总时长
|
|
|
|
|
|
const currentTime = currentSound.seek() as number;
|
|
|
|
|
|
if (typeof currentTime === 'number' && !Number.isNaN(currentTime)) {
|
|
|
|
|
|
nowTime.value = currentTime;
|
|
|
|
|
|
allTime.value = currentSound.duration() as number;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('初始化时间和进度失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 立即执行一次更新
|
|
|
|
|
|
updateCurrentTimeAndDuration();
|
|
|
|
|
|
|
2024-12-12 22:18:52 +08:00
|
|
|
|
// 监听播放
|
2025-01-12 19:14:25 +08:00
|
|
|
|
audioService.on('play', () => {
|
2025-07-23 23:54:35 +08:00
|
|
|
|
getPlayerStore().setPlayMusic(true);
|
2025-04-12 13:05:08 +08:00
|
|
|
|
if (isElectron) {
|
2025-07-23 23:54:35 +08:00
|
|
|
|
window.api.sendSong(cloneDeep(getPlayerStore().playMusic));
|
2025-04-12 13:05:08 +08:00
|
|
|
|
}
|
2026-02-06 20:34:07 +08:00
|
|
|
|
// 启动进度更新
|
|
|
|
|
|
startProgressInterval();
|
2024-12-12 22:18:52 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 监听暂停
|
2025-01-12 19:14:25 +08:00
|
|
|
|
audioService.on('pause', () => {
|
2025-03-08 18:31:46 +08:00
|
|
|
|
console.log('音频暂停事件触发');
|
2025-07-23 23:54:35 +08:00
|
|
|
|
getPlayerStore().setPlayMusic(false);
|
2025-01-16 23:19:16 +08:00
|
|
|
|
clearInterval();
|
2025-01-01 02:25:18 +08:00
|
|
|
|
if (isElectron && isLyricWindowOpen.value) {
|
2024-12-16 22:12:28 +08:00
|
|
|
|
sendLyricToWin();
|
|
|
|
|
|
}
|
2024-12-12 22:18:52 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-02-06 20:34:07 +08:00
|
|
|
|
const replayMusic = async (retryCount: number = 0) => {
|
|
|
|
|
|
const MAX_REPLAY_RETRIES = 3;
|
2025-01-17 22:34:49 +08:00
|
|
|
|
try {
|
|
|
|
|
|
// 如果当前有音频实例,先停止并销毁
|
2026-02-06 20:34:07 +08:00
|
|
|
|
const currentSound = audioService.getCurrentSound();
|
|
|
|
|
|
if (currentSound) {
|
|
|
|
|
|
currentSound.stop();
|
|
|
|
|
|
currentSound.unload();
|
2025-01-17 22:34:49 +08:00
|
|
|
|
}
|
2026-02-06 20:34:07 +08:00
|
|
|
|
sound.value = null;
|
2025-01-17 22:34:49 +08:00
|
|
|
|
|
|
|
|
|
|
// 重新播放当前歌曲
|
2025-07-23 23:54:35 +08:00
|
|
|
|
if (getPlayerStore().playMusicUrl && playMusic.value) {
|
|
|
|
|
|
const newSound = await audioService.play(getPlayerStore().playMusicUrl, playMusic.value);
|
2025-01-17 22:34:49 +08:00
|
|
|
|
sound.value = newSound as Howl;
|
|
|
|
|
|
setupAudioListeners();
|
|
|
|
|
|
} else {
|
2026-02-06 20:34:07 +08:00
|
|
|
|
console.error('单曲循环:无可用 URL 或歌曲数据');
|
2025-07-23 23:54:35 +08:00
|
|
|
|
getPlayerStore().nextPlay();
|
2025-01-17 22:34:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2026-02-06 20:34:07 +08:00
|
|
|
|
console.error('单曲循环重播失败:', error);
|
|
|
|
|
|
if (retryCount < MAX_REPLAY_RETRIES) {
|
|
|
|
|
|
console.log(`单曲循环重试 ${retryCount + 1}/${MAX_REPLAY_RETRIES}`);
|
|
|
|
|
|
setTimeout(() => replayMusic(retryCount + 1), 1000 * (retryCount + 1));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.error('单曲循环重试次数用尽,切换下一首');
|
|
|
|
|
|
getPlayerStore().nextPlay();
|
|
|
|
|
|
}
|
2025-01-17 22:34:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2024-12-12 22:18:52 +08:00
|
|
|
|
// 监听结束
|
2026-03-22 16:49:00 +08:00
|
|
|
|
audioService.on('end', async () => {
|
2025-03-08 18:31:46 +08:00
|
|
|
|
console.log('音频播放结束事件触发');
|
2025-01-16 23:19:16 +08:00
|
|
|
|
clearInterval();
|
2025-01-16 00:40:44 +08:00
|
|
|
|
|
2025-07-23 23:54:35 +08:00
|
|
|
|
if (getPlayerStore().playMode === 1) {
|
2024-12-15 01:40:13 +08:00
|
|
|
|
// 单曲循环模式
|
2026-02-06 20:34:07 +08:00
|
|
|
|
replayMusic();
|
2026-03-22 16:49:00 +08:00
|
|
|
|
} else if (getPlayerStore().isFmPlaying) {
|
|
|
|
|
|
// 私人FM模式:自动获取下一首
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { getPersonalFM } = await import('@/api/home');
|
|
|
|
|
|
const res = await getPersonalFM();
|
|
|
|
|
|
const songs = res.data?.data;
|
|
|
|
|
|
if (Array.isArray(songs) && songs.length > 0) {
|
|
|
|
|
|
const song = songs[0];
|
|
|
|
|
|
const fmSong = {
|
|
|
|
|
|
id: song.id,
|
|
|
|
|
|
name: song.name,
|
|
|
|
|
|
picUrl: song.al?.picUrl || song.album?.picUrl,
|
|
|
|
|
|
ar: song.artists || song.ar,
|
|
|
|
|
|
al: song.al || song.album,
|
|
|
|
|
|
source: 'netease' as const,
|
|
|
|
|
|
song,
|
|
|
|
|
|
...song,
|
|
|
|
|
|
playLoading: false
|
|
|
|
|
|
} as any;
|
|
|
|
|
|
const { usePlaylistStore } = await import('@/store/modules/playlist');
|
|
|
|
|
|
const playlistStore = usePlaylistStore();
|
|
|
|
|
|
playlistStore.setPlayList([fmSong], false, false);
|
|
|
|
|
|
getPlayerStore().isFmPlaying = true; // setPlayList 会清除,需重设
|
|
|
|
|
|
await getPlayerStore().handlePlayMusic(fmSong, true);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
getPlayerStore().setIsPlay(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('FM自动播放下一首失败:', error);
|
|
|
|
|
|
getPlayerStore().setIsPlay(false);
|
|
|
|
|
|
}
|
2024-12-15 01:40:13 +08:00
|
|
|
|
} else {
|
2025-08-12 19:51:29 +08:00
|
|
|
|
// 顺序播放、列表循环、随机播放模式都使用统一的nextPlay方法
|
2025-07-23 23:54:35 +08:00
|
|
|
|
getPlayerStore().nextPlay();
|
2024-12-15 01:40:13 +08:00
|
|
|
|
}
|
2024-12-12 22:18:52 +08:00
|
|
|
|
});
|
2025-01-12 19:14:25 +08:00
|
|
|
|
|
2025-03-22 10:37:57 +08:00
|
|
|
|
audioService.on('previoustrack', () => {
|
2025-07-23 23:54:35 +08:00
|
|
|
|
getPlayerStore().prevPlay();
|
2025-03-22 10:37:57 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
audioService.on('nexttrack', () => {
|
2025-07-23 23:54:35 +08:00
|
|
|
|
getPlayerStore().nextPlay();
|
2025-03-22 10:37:57 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-02-06 20:34:07 +08:00
|
|
|
|
return () => {
|
|
|
|
|
|
clearInterval();
|
|
|
|
|
|
stopRecovery();
|
|
|
|
|
|
};
|
2024-12-12 22:18:52 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export const play = () => {
|
2025-12-20 14:16:32 +08:00
|
|
|
|
const currentSound = audioService.getCurrentSound();
|
|
|
|
|
|
if (currentSound) {
|
|
|
|
|
|
currentSound.play();
|
|
|
|
|
|
// 在播放时也进行状态检测,防止URL已过期导致无声
|
|
|
|
|
|
getPlayerStore().checkPlaybackState(getPlayerStore().playMusic);
|
|
|
|
|
|
}
|
2024-12-12 22:18:52 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export const pause = () => {
|
2025-03-08 18:31:46 +08:00
|
|
|
|
const currentSound = audioService.getCurrentSound();
|
|
|
|
|
|
if (currentSound) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 保存当前播放进度
|
|
|
|
|
|
const currentTime = currentSound.seek() as number;
|
2025-07-23 23:54:35 +08:00
|
|
|
|
if (getPlayerStore().playMusic && getPlayerStore().playMusic.id) {
|
2025-03-08 18:31:46 +08:00
|
|
|
|
localStorage.setItem(
|
|
|
|
|
|
'playProgress',
|
|
|
|
|
|
JSON.stringify({
|
2025-07-23 23:54:35 +08:00
|
|
|
|
songId: getPlayerStore().playMusic.id,
|
2025-03-08 18:31:46 +08:00
|
|
|
|
progress: currentTime
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-01 23:22:26 +08:00
|
|
|
|
audioService.pause();
|
2025-03-08 18:31:46 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('暂停播放出错:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-12-12 22:18:52 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-05-26 22:58:42 +08:00
|
|
|
|
// 歌词矫正时间映射(每首歌独立)
|
|
|
|
|
|
const CORRECTION_KEY = 'lyric-correction-map';
|
|
|
|
|
|
const correctionTimeMap = ref<Record<string, number>>({});
|
2023-12-17 14:48:21 +08:00
|
|
|
|
|
2025-05-26 22:58:42 +08:00
|
|
|
|
// 初始化 correctionTimeMap
|
|
|
|
|
|
const loadCorrectionMap = () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const raw = localStorage.getItem(CORRECTION_KEY);
|
|
|
|
|
|
correctionTimeMap.value = raw ? JSON.parse(raw) : {};
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
correctionTimeMap.value = {};
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
const saveCorrectionMap = () => {
|
|
|
|
|
|
localStorage.setItem(CORRECTION_KEY, JSON.stringify(correctionTimeMap.value));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
loadCorrectionMap();
|
|
|
|
|
|
|
|
|
|
|
|
// 歌词矫正时间,当前歌曲
|
|
|
|
|
|
export const correctionTime = ref(0);
|
|
|
|
|
|
|
2025-07-23 23:54:35 +08:00
|
|
|
|
// 设置歌词矫正时间的监听器
|
|
|
|
|
|
const setupCorrectionTimeWatcher = () => {
|
|
|
|
|
|
// 切歌时自动读取矫正时间
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => playMusic.value?.id,
|
|
|
|
|
|
(id) => {
|
|
|
|
|
|
if (!id) return;
|
|
|
|
|
|
correctionTime.value = correctionTimeMap.value[id] ?? 0;
|
|
|
|
|
|
},
|
|
|
|
|
|
{ immediate: true }
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
2025-05-26 22:58:42 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 调整歌词矫正时间(每首歌独立)
|
|
|
|
|
|
* @param delta 增加/减少的秒数(正为加,负为减)
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const adjustCorrectionTime = (delta: number) => {
|
|
|
|
|
|
const id = playMusic.value?.id;
|
|
|
|
|
|
if (!id) return;
|
|
|
|
|
|
const newVal = Math.max(-10, Math.min(10, (correctionTime.value ?? 0) + delta));
|
|
|
|
|
|
correctionTime.value = newVal;
|
|
|
|
|
|
correctionTimeMap.value[id] = newVal;
|
|
|
|
|
|
saveCorrectionMap();
|
|
|
|
|
|
};
|
2023-12-17 14:48:21 +08:00
|
|
|
|
|
2024-10-18 18:37:53 +08:00
|
|
|
|
// 获取当前播放歌词
|
|
|
|
|
|
export const isCurrentLrc = (index: number, time: number): boolean => {
|
|
|
|
|
|
const currentTime = lrcTimeArray.value[index];
|
2025-10-11 20:23:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果是最后一句歌词,只需要判断时间是否大于等于当前句的开始时间
|
|
|
|
|
|
if (index === lrcTimeArray.value.length - 1) {
|
|
|
|
|
|
const correctedTime = time + correctionTime.value;
|
|
|
|
|
|
return correctedTime >= currentTime;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 非最后一句歌词,需要判断时间在当前句和下一句之间
|
2024-10-18 18:37:53 +08:00
|
|
|
|
const nextTime = lrcTimeArray.value[index + 1];
|
2025-05-26 22:58:42 +08:00
|
|
|
|
const correctedTime = time + correctionTime.value;
|
2025-10-11 20:23:54 +08:00
|
|
|
|
return correctedTime >= currentTime && correctedTime < nextTime;
|
2024-05-16 18:54:30 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2024-10-18 18:37:53 +08:00
|
|
|
|
// 获取当前播放歌词INDEX
|
|
|
|
|
|
export const getLrcIndex = (time: number): number => {
|
2025-05-26 22:58:42 +08:00
|
|
|
|
const correctedTime = time + correctionTime.value;
|
2025-10-11 20:23:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果歌词数组为空,返回当前索引
|
|
|
|
|
|
if (lrcTimeArray.value.length === 0) {
|
|
|
|
|
|
return nowIndex.value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理最后一句歌词的情况
|
|
|
|
|
|
const lastIndex = lrcTimeArray.value.length - 1;
|
|
|
|
|
|
if (correctedTime >= lrcTimeArray.value[lastIndex]) {
|
|
|
|
|
|
nowIndex.value = lastIndex;
|
|
|
|
|
|
return lastIndex;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 查找当前时间对应的歌词索引
|
|
|
|
|
|
for (let i = 0; i < lrcTimeArray.value.length - 1; i++) {
|
|
|
|
|
|
const currentTime = lrcTimeArray.value[i];
|
|
|
|
|
|
const nextTime = lrcTimeArray.value[i + 1];
|
|
|
|
|
|
|
|
|
|
|
|
if (correctedTime >= currentTime && correctedTime < nextTime) {
|
2024-10-18 18:37:53 +08:00
|
|
|
|
nowIndex.value = i;
|
2024-05-16 18:54:30 +08:00
|
|
|
|
return i;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-11 20:23:54 +08:00
|
|
|
|
|
2024-05-16 18:54:30 +08:00
|
|
|
|
return nowIndex.value;
|
|
|
|
|
|
};
|
2023-12-17 14:48:21 +08:00
|
|
|
|
|
2024-12-14 13:15:59 +08:00
|
|
|
|
// 获取当前播放歌词进度
|
2024-10-18 18:37:53 +08:00
|
|
|
|
const currentLrcTiming = computed(() => {
|
|
|
|
|
|
const start = lrcTimeArray.value[nowIndex.value] || 0;
|
|
|
|
|
|
const end = lrcTimeArray.value[nowIndex.value + 1] || start + 1;
|
|
|
|
|
|
return { start, end };
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 获取歌词样式
|
|
|
|
|
|
export const getLrcStyle = (index: number) => {
|
2025-05-26 22:58:42 +08:00
|
|
|
|
const currentTime = nowTime.value + correctionTime.value;
|
|
|
|
|
|
const start = lrcTimeArray.value[index];
|
2025-07-23 23:54:35 +08:00
|
|
|
|
const end = lrcTimeArray.value[index + 1] ?? start + 1;
|
2025-05-26 22:58:42 +08:00
|
|
|
|
|
|
|
|
|
|
if (currentTime >= start && currentTime < end) {
|
|
|
|
|
|
// 当前句,显示进度
|
|
|
|
|
|
const progress = ((currentTime - start) / (end - start)) * 100;
|
2024-10-18 18:37:53 +08:00
|
|
|
|
return {
|
2025-05-26 22:58:42 +08:00
|
|
|
|
backgroundImage: `linear-gradient(to right, #ffffff ${progress}%, #ffffff8a ${progress}%)`,
|
2024-10-18 18:37:53 +08:00
|
|
|
|
backgroundClip: 'text',
|
|
|
|
|
|
WebkitBackgroundClip: 'text',
|
|
|
|
|
|
color: 'transparent',
|
2025-01-01 02:25:18 +08:00
|
|
|
|
transition: 'background-image 0.1s linear'
|
2024-10-18 18:37:53 +08:00
|
|
|
|
};
|
|
|
|
|
|
}
|
2025-05-26 22:58:42 +08:00
|
|
|
|
// 其它句
|
2024-10-18 18:37:53 +08:00
|
|
|
|
return {};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 播放进度
|
|
|
|
|
|
export const useLyricProgress = () => {
|
2025-03-08 18:31:46 +08:00
|
|
|
|
// 如果已经在全局更新进度,立即返回
|
2024-10-18 18:37:53 +08:00
|
|
|
|
return {
|
2025-01-01 02:25:18 +08:00
|
|
|
|
getLrcStyle
|
2024-10-18 18:37:53 +08:00
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2024-12-29 00:43:39 +08:00
|
|
|
|
// 设置当前播放时间
|
2024-12-12 22:18:52 +08:00
|
|
|
|
export const setAudioTime = (index: number) => {
|
|
|
|
|
|
const currentSound = sound.value;
|
|
|
|
|
|
if (!currentSound) return;
|
|
|
|
|
|
|
|
|
|
|
|
currentSound.seek(lrcTimeArray.value[index]);
|
|
|
|
|
|
currentSound.play();
|
2024-05-16 18:54:30 +08:00
|
|
|
|
};
|
2023-12-17 14:48:21 +08:00
|
|
|
|
|
2024-05-16 18:54:30 +08:00
|
|
|
|
// 获取当前播放的歌词
|
|
|
|
|
|
export const getCurrentLrc = () => {
|
|
|
|
|
|
const index = getLrcIndex(nowTime.value);
|
2024-10-18 18:37:53 +08:00
|
|
|
|
return {
|
|
|
|
|
|
currentLrc: lrcArray.value[index],
|
2025-01-01 02:25:18 +08:00
|
|
|
|
nextLrc: lrcArray.value[index + 1]
|
2024-10-18 18:37:53 +08:00
|
|
|
|
};
|
2024-05-16 18:54:30 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2024-12-12 22:18:52 +08:00
|
|
|
|
// 获取一句歌词播放时间几秒到几秒
|
2024-10-18 18:37:53 +08:00
|
|
|
|
export const getLrcTimeRange = (index: number) => ({
|
|
|
|
|
|
currentTime: lrcTimeArray.value[index],
|
2025-01-01 02:25:18 +08:00
|
|
|
|
nextTime: lrcTimeArray.value[index + 1]
|
2024-10-18 18:37:53 +08:00
|
|
|
|
});
|
2024-05-16 18:54:30 +08:00
|
|
|
|
|
2024-12-06 23:50:44 +08:00
|
|
|
|
// 监听歌词数组变化,当切换歌曲时重新初始化歌词窗口
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => lrcArray.value,
|
|
|
|
|
|
(newLrcArray) => {
|
2025-01-01 02:25:18 +08:00
|
|
|
|
if (newLrcArray.length > 0 && isElectron && isLyricWindowOpen.value) {
|
2024-12-06 23:50:44 +08:00
|
|
|
|
sendLyricToWin();
|
|
|
|
|
|
}
|
2025-01-01 02:25:18 +08:00
|
|
|
|
}
|
2024-12-06 23:50:44 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 发送歌词更新数据
|
2024-12-16 22:12:28 +08:00
|
|
|
|
export const sendLyricToWin = () => {
|
2025-01-01 02:25:18 +08:00
|
|
|
|
if (!isElectron || !isLyricWindowOpen.value) {
|
2025-03-08 19:00:50 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否有播放的歌曲
|
|
|
|
|
|
if (!playMusic.value || !playMusic.value.id) {
|
2024-12-16 22:12:28 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2024-10-18 18:37:53 +08:00
|
|
|
|
|
2024-05-16 18:54:30 +08:00
|
|
|
|
try {
|
2025-03-08 19:00:50 +08:00
|
|
|
|
// 记录歌词发送状态
|
|
|
|
|
|
if (lrcArray.value && lrcArray.value.length > 0) {
|
2024-05-16 18:54:30 +08:00
|
|
|
|
const nowIndex = getLrcIndex(nowTime.value);
|
2025-03-08 19:00:50 +08:00
|
|
|
|
// 构建完整的歌词更新数据
|
2024-12-06 23:50:44 +08:00
|
|
|
|
const updateData = {
|
2024-12-16 22:12:28 +08:00
|
|
|
|
type: 'full',
|
2024-05-16 18:54:30 +08:00
|
|
|
|
nowIndex,
|
|
|
|
|
|
nowTime: nowTime.value,
|
2025-03-08 19:00:50 +08:00
|
|
|
|
startCurrentTime: lrcTimeArray.value[nowIndex] || 0,
|
|
|
|
|
|
nextTime: lrcTimeArray.value[nowIndex + 1] || 0,
|
2025-07-23 23:54:35 +08:00
|
|
|
|
isPlay: getPlayerStore().play,
|
2024-12-16 22:12:28 +08:00
|
|
|
|
lrcArray: lrcArray.value,
|
|
|
|
|
|
lrcTimeArray: lrcTimeArray.value,
|
|
|
|
|
|
allTime: allTime.value,
|
2025-01-01 02:25:18 +08:00
|
|
|
|
playMusic: playMusic.value
|
2024-05-16 18:54:30 +08:00
|
|
|
|
};
|
2025-03-08 19:00:50 +08:00
|
|
|
|
|
|
|
|
|
|
// 发送数据到歌词窗口
|
2025-01-01 02:25:18 +08:00
|
|
|
|
window.api.sendLyric(JSON.stringify(updateData));
|
2025-03-08 19:00:50 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
console.log('No lyric data available, sending empty lyric message');
|
|
|
|
|
|
|
|
|
|
|
|
// 发送没有歌词的提示
|
|
|
|
|
|
const emptyLyricData = {
|
|
|
|
|
|
type: 'empty',
|
|
|
|
|
|
nowIndex: 0,
|
|
|
|
|
|
nowTime: nowTime.value,
|
|
|
|
|
|
startCurrentTime: 0,
|
|
|
|
|
|
nextTime: 0,
|
2025-07-23 23:54:35 +08:00
|
|
|
|
isPlay: getPlayerStore().play,
|
2025-03-08 19:00:50 +08:00
|
|
|
|
lrcArray: [{ text: '当前歌曲暂无歌词', trText: '' }],
|
|
|
|
|
|
lrcTimeArray: [0],
|
|
|
|
|
|
allTime: allTime.value,
|
|
|
|
|
|
playMusic: playMusic.value
|
|
|
|
|
|
};
|
|
|
|
|
|
window.api.sendLyric(JSON.stringify(emptyLyricData));
|
2024-05-16 18:54:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2024-12-06 23:50:44 +08:00
|
|
|
|
console.error('Error sending lyric update:', error);
|
2024-05-16 18:54:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-03-08 19:00:50 +08:00
|
|
|
|
// 歌词同步定时器
|
|
|
|
|
|
let lyricSyncInterval: any = null;
|
|
|
|
|
|
|
|
|
|
|
|
// 开始歌词同步
|
|
|
|
|
|
const startLyricSync = () => {
|
|
|
|
|
|
// 清除已有的定时器
|
|
|
|
|
|
if (lyricSyncInterval) {
|
|
|
|
|
|
clearInterval(lyricSyncInterval);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 每秒同步一次歌词数据
|
|
|
|
|
|
lyricSyncInterval = setInterval(() => {
|
2025-07-23 23:54:35 +08:00
|
|
|
|
if (isElectron && isLyricWindowOpen.value && getPlayerStore().play && playMusic.value?.id) {
|
2025-03-08 19:00:50 +08:00
|
|
|
|
// 发送当前播放进度的更新
|
|
|
|
|
|
try {
|
|
|
|
|
|
const updateData = {
|
|
|
|
|
|
type: 'update',
|
|
|
|
|
|
nowIndex: getLrcIndex(nowTime.value),
|
|
|
|
|
|
nowTime: nowTime.value,
|
2025-07-23 23:54:35 +08:00
|
|
|
|
isPlay: getPlayerStore().play
|
2025-03-08 19:00:50 +08:00
|
|
|
|
};
|
|
|
|
|
|
window.api.sendLyric(JSON.stringify(updateData));
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('发送歌词进度更新失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 1000);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 停止歌词同步
|
|
|
|
|
|
const stopLyricSync = () => {
|
|
|
|
|
|
if (lyricSyncInterval) {
|
|
|
|
|
|
clearInterval(lyricSyncInterval);
|
|
|
|
|
|
lyricSyncInterval = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 修改openLyric函数,添加定时同步
|
2024-05-16 18:54:30 +08:00
|
|
|
|
export const openLyric = () => {
|
2025-01-01 02:25:18 +08:00
|
|
|
|
if (!isElectron) return;
|
2025-03-08 19:00:50 +08:00
|
|
|
|
|
|
|
|
|
|
// 检查是否有播放中的歌曲
|
|
|
|
|
|
if (!playMusic.value || !playMusic.value.id) {
|
|
|
|
|
|
console.log('没有正在播放的歌曲,无法打开歌词窗口');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-16 22:12:28 +08:00
|
|
|
|
console.log('Opening lyric window with current song:', playMusic.value?.name);
|
2024-12-16 22:25:38 +08:00
|
|
|
|
|
|
|
|
|
|
isLyricWindowOpen.value = !isLyricWindowOpen.value;
|
|
|
|
|
|
if (isLyricWindowOpen.value) {
|
2025-03-08 19:00:50 +08:00
|
|
|
|
// 立即打开窗口
|
|
|
|
|
|
window.api.openLyric();
|
|
|
|
|
|
|
|
|
|
|
|
// 确保有歌词数据,如果没有,则使用默认的"无歌词"提示
|
|
|
|
|
|
if (!lrcArray.value || lrcArray.value.length === 0) {
|
|
|
|
|
|
// 如果当前播放的歌曲有ID但没有歌词,则尝试加载歌词
|
|
|
|
|
|
console.log('尝试加载歌词数据...');
|
|
|
|
|
|
// 发送默认的"无歌词"数据
|
|
|
|
|
|
const emptyLyricData = {
|
|
|
|
|
|
type: 'empty',
|
|
|
|
|
|
nowIndex: 0,
|
|
|
|
|
|
nowTime: nowTime.value,
|
|
|
|
|
|
startCurrentTime: 0,
|
|
|
|
|
|
nextTime: 0,
|
2025-07-23 23:54:35 +08:00
|
|
|
|
isPlay: getPlayerStore().play,
|
2025-03-08 19:00:50 +08:00
|
|
|
|
lrcArray: [{ text: '加载歌词中...', trText: '' }],
|
|
|
|
|
|
lrcTimeArray: [0],
|
|
|
|
|
|
allTime: allTime.value,
|
|
|
|
|
|
playMusic: playMusic.value
|
|
|
|
|
|
};
|
|
|
|
|
|
window.api.sendLyric(JSON.stringify(emptyLyricData));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 发送完整歌词数据
|
|
|
|
|
|
sendLyricToWin();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 18:31:16 +08:00
|
|
|
|
// 延迟重发一次,以防窗口加载略慢
|
2024-12-16 22:25:38 +08:00
|
|
|
|
setTimeout(() => {
|
2026-03-12 18:31:16 +08:00
|
|
|
|
if (isLyricWindowOpen.value) {
|
|
|
|
|
|
sendLyricToWin();
|
|
|
|
|
|
}
|
2024-12-16 22:25:38 +08:00
|
|
|
|
}, 500);
|
2025-03-08 19:00:50 +08:00
|
|
|
|
|
|
|
|
|
|
// 启动歌词同步
|
|
|
|
|
|
startLyricSync();
|
2024-12-16 22:25:38 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
closeLyric();
|
2025-03-08 19:00:50 +08:00
|
|
|
|
// 停止歌词同步
|
|
|
|
|
|
stopLyricSync();
|
2024-12-16 22:25:38 +08:00
|
|
|
|
}
|
2024-05-16 18:54:30 +08:00
|
|
|
|
};
|
2024-12-16 22:12:28 +08:00
|
|
|
|
|
2025-03-08 19:00:50 +08:00
|
|
|
|
// 修改closeLyric函数,确保停止定时同步
|
2024-12-16 22:12:28 +08:00
|
|
|
|
export const closeLyric = () => {
|
2025-01-01 02:25:18 +08:00
|
|
|
|
if (!isElectron) return;
|
2025-03-08 19:00:50 +08:00
|
|
|
|
isLyricWindowOpen.value = false; // 确保状态更新
|
2024-12-16 22:12:28 +08:00
|
|
|
|
windowData.electron.ipcRenderer.send('close-lyric');
|
2025-03-08 19:00:50 +08:00
|
|
|
|
|
|
|
|
|
|
// 停止歌词同步
|
|
|
|
|
|
stopLyricSync();
|
2024-12-16 22:12:28 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-07-23 23:54:35 +08:00
|
|
|
|
// 设置播放状态监听器
|
|
|
|
|
|
const setupPlayStateWatcher = () => {
|
|
|
|
|
|
// 在组件挂载时设置对播放状态的监听
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => getPlayerStore().play,
|
|
|
|
|
|
(isPlaying) => {
|
|
|
|
|
|
// 如果歌词窗口打开,根据播放状态控制同步
|
|
|
|
|
|
if (isElectron && isLyricWindowOpen.value) {
|
|
|
|
|
|
if (isPlaying) {
|
|
|
|
|
|
startLyricSync();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 如果暂停播放,发送一次暂停状态的更新
|
|
|
|
|
|
const pauseData = {
|
|
|
|
|
|
type: 'update',
|
|
|
|
|
|
isPlay: false
|
|
|
|
|
|
};
|
|
|
|
|
|
window.api.sendLyric(JSON.stringify(pauseData));
|
|
|
|
|
|
}
|
2025-03-08 19:00:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-07-23 23:54:35 +08:00
|
|
|
|
);
|
|
|
|
|
|
};
|
2025-03-08 19:00:50 +08:00
|
|
|
|
|
|
|
|
|
|
// 在组件卸载时清理资源
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
stopLyricSync();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-11 20:23:54 +08:00
|
|
|
|
// 导出歌词解析函数供外部使用
|
|
|
|
|
|
export { parseLyricsString };
|
|
|
|
|
|
|
2024-12-16 22:12:28 +08:00
|
|
|
|
// 添加播放控制命令监听
|
2025-01-01 02:25:18 +08:00
|
|
|
|
if (isElectron) {
|
|
|
|
|
|
windowData.electron.ipcRenderer.on('lyric-control-back', (_, command: string) => {
|
2024-12-16 22:12:28 +08:00
|
|
|
|
switch (command) {
|
|
|
|
|
|
case 'playpause':
|
2026-03-04 19:53:50 +08:00
|
|
|
|
if (getPlayerStore().playMusic?.id) {
|
|
|
|
|
|
void getPlayerStore().setPlay({ ...getPlayerStore().playMusic });
|
2024-12-16 22:12:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'prev':
|
2025-07-23 23:54:35 +08:00
|
|
|
|
getPlayerStore().prevPlay();
|
2024-12-16 22:12:28 +08:00
|
|
|
|
break;
|
|
|
|
|
|
case 'next':
|
2025-07-23 23:54:35 +08:00
|
|
|
|
getPlayerStore().nextPlay();
|
2024-12-16 22:12:28 +08:00
|
|
|
|
break;
|
|
|
|
|
|
case 'close':
|
2025-03-08 19:00:50 +08:00
|
|
|
|
isLyricWindowOpen.value = false; // 确保状态更新
|
2024-12-16 22:12:28 +08:00
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
console.log('Unknown command:', command);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-01-12 19:14:25 +08:00
|
|
|
|
|
|
|
|
|
|
// 在组件挂载时设置监听器
|
2025-03-21 00:19:15 +08:00
|
|
|
|
export const initAudioListeners = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 确保有正在播放的音乐
|
2025-07-23 23:54:35 +08:00
|
|
|
|
if (!getPlayerStore().playMusic || !getPlayerStore().playMusic.id) {
|
2025-03-21 00:19:15 +08:00
|
|
|
|
console.log('没有正在播放的音乐,跳过音频监听器初始化');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 确保有音频实例
|
|
|
|
|
|
const initialSound = audioService.getCurrentSound();
|
|
|
|
|
|
if (!initialSound) {
|
|
|
|
|
|
console.log('没有音频实例,等待音频加载...');
|
|
|
|
|
|
// 等待音频加载完成
|
|
|
|
|
|
await new Promise<void>((resolve) => {
|
|
|
|
|
|
const checkInterval = setInterval(() => {
|
|
|
|
|
|
const sound = audioService.getCurrentSound();
|
|
|
|
|
|
if (sound) {
|
|
|
|
|
|
clearInterval(checkInterval);
|
|
|
|
|
|
resolve();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 100);
|
|
|
|
|
|
|
|
|
|
|
|
// 设置超时
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
clearInterval(checkInterval);
|
|
|
|
|
|
console.log('等待音频加载超时');
|
|
|
|
|
|
resolve();
|
|
|
|
|
|
}, 5000);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化音频监听器
|
|
|
|
|
|
setupAudioListeners();
|
2025-03-08 19:00:50 +08:00
|
|
|
|
|
2026-03-12 18:31:16 +08:00
|
|
|
|
// 监听歌词窗口事件
|
2025-03-21 00:19:15 +08:00
|
|
|
|
if (isElectron) {
|
|
|
|
|
|
window.api.onLyricWindowClosed(() => {
|
|
|
|
|
|
isLyricWindowOpen.value = false;
|
|
|
|
|
|
});
|
2026-03-12 18:31:16 +08:00
|
|
|
|
// 歌词窗口 Vue 加载完成后,发送完整歌词数据
|
|
|
|
|
|
window.api.onLyricWindowReady(() => {
|
|
|
|
|
|
if (isLyricWindowOpen.value) {
|
|
|
|
|
|
sendLyricToWin();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-03-21 00:19:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取最新的音频实例
|
|
|
|
|
|
const finalSound = audioService.getCurrentSound();
|
|
|
|
|
|
if (finalSound) {
|
|
|
|
|
|
// 更新全局 sound 引用
|
|
|
|
|
|
sound.value = finalSound;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.warn('无法获取音频实例,跳过进度更新初始化');
|
2025-03-08 18:31:46 +08:00
|
|
|
|
}
|
2025-03-21 00:19:15 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('初始化音频监听器失败:', error);
|
2025-03-08 18:31:46 +08:00
|
|
|
|
}
|
2025-03-21 00:19:15 +08:00
|
|
|
|
};
|
2025-03-30 12:40:39 +08:00
|
|
|
|
|
|
|
|
|
|
// 监听URL过期事件,自动重新获取URL并恢复播放
|
|
|
|
|
|
audioService.on('url_expired', async (expiredTrack) => {
|
|
|
|
|
|
if (!expiredTrack) return;
|
|
|
|
|
|
|
|
|
|
|
|
console.log('检测到URL过期事件,准备重新获取URL', expiredTrack.name);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2025-12-20 14:16:32 +08:00
|
|
|
|
// 使用 handlePlayMusic 重新播放,它会自动处理 URL 获取和状态跟踪
|
|
|
|
|
|
// 我们将 isFirstPlay 设为 true 以强制获取新 URL
|
|
|
|
|
|
const trackToPlay = {
|
|
|
|
|
|
...expiredTrack,
|
|
|
|
|
|
isFirstPlay: true,
|
|
|
|
|
|
playMusicUrl: undefined
|
|
|
|
|
|
};
|
2025-03-30 12:40:39 +08:00
|
|
|
|
|
2025-12-20 14:16:32 +08:00
|
|
|
|
await getPlayerStore().handlePlayMusic(trackToPlay, getPlayerStore().play);
|
2025-03-30 12:40:39 +08:00
|
|
|
|
|
2025-12-20 14:16:32 +08:00
|
|
|
|
message.success('已自动恢复播放');
|
2025-03-30 12:40:39 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('处理URL过期事件失败:', error);
|
|
|
|
|
|
message.error('恢复播放失败,请手动点击播放');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-05-02 19:25:12 +08:00
|
|
|
|
|
|
|
|
|
|
// 添加音频就绪事件监听器
|
|
|
|
|
|
window.addEventListener('audio-ready', ((event: CustomEvent) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { sound: newSound } = event.detail;
|
|
|
|
|
|
if (newSound) {
|
|
|
|
|
|
// 更新本地 sound 引用
|
|
|
|
|
|
sound.value = newSound as Howl;
|
2025-07-23 23:54:35 +08:00
|
|
|
|
|
2025-05-02 19:25:12 +08:00
|
|
|
|
// 设置音频监听器
|
|
|
|
|
|
setupAudioListeners();
|
2025-07-23 23:54:35 +08:00
|
|
|
|
|
2025-05-02 19:25:12 +08:00
|
|
|
|
// 获取当前播放位置并更新显示
|
|
|
|
|
|
const currentPosition = newSound.seek() as number;
|
|
|
|
|
|
if (typeof currentPosition === 'number' && !Number.isNaN(currentPosition)) {
|
|
|
|
|
|
nowTime.value = currentPosition;
|
|
|
|
|
|
}
|
2025-07-23 23:54:35 +08:00
|
|
|
|
|
2025-05-02 19:25:12 +08:00
|
|
|
|
console.log('音频就绪,已设置监听器并更新进度显示');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('处理音频就绪事件出错:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}) as EventListener);
|