mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-07-16 15:47:29 +08:00
Compare commits
5 Commits
b0b3eb3326
...
97220761cf
| Author | SHA1 | Date | |
|---|---|---|---|
| 97220761cf | |||
| 7282e876f4 | |||
| 6b22713854 | |||
| 0d960aa8d5 | |||
| e066efb373 |
@@ -10,6 +10,65 @@ let isDragging = false;
|
||||
|
||||
// 添加窗口大小变化防护
|
||||
let originalSize = { width: 0, height: 0 };
|
||||
// 鼠标位置轮询仅在"锁定 + 可见"时启用,解锁态下 DOM 事件已足够
|
||||
let mousePresenceTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let lastMouseInside: boolean | null = null;
|
||||
let isLyricLocked = false;
|
||||
let isLyricWindowVisible = false;
|
||||
|
||||
const isPointInsideWindow = (
|
||||
point: { x: number; y: number },
|
||||
bounds: { x: number; y: number; width: number; height: number }
|
||||
) => {
|
||||
return (
|
||||
point.x >= bounds.x &&
|
||||
point.x < bounds.x + bounds.width &&
|
||||
point.y >= bounds.y &&
|
||||
point.y < bounds.y + bounds.height
|
||||
);
|
||||
};
|
||||
|
||||
const stopMousePresenceTracking = () => {
|
||||
if (mousePresenceTimer) {
|
||||
clearInterval(mousePresenceTimer);
|
||||
mousePresenceTimer = null;
|
||||
}
|
||||
lastMouseInside = null;
|
||||
};
|
||||
|
||||
const emitMousePresence = () => {
|
||||
if (!lyricWindow || lyricWindow.isDestroyed()) return;
|
||||
|
||||
const mousePoint = screen.getCursorScreenPoint();
|
||||
const bounds = lyricWindow.getBounds();
|
||||
const isInside = isPointInsideWindow(mousePoint, bounds);
|
||||
|
||||
if (isInside === lastMouseInside) return;
|
||||
|
||||
lastMouseInside = isInside;
|
||||
lyricWindow.webContents.send('lyric-mouse-presence', isInside);
|
||||
};
|
||||
|
||||
const startMousePresenceTracking = () => {
|
||||
if (mousePresenceTimer) return;
|
||||
|
||||
emitMousePresence();
|
||||
mousePresenceTimer = setInterval(() => {
|
||||
if (!lyricWindow || lyricWindow.isDestroyed()) {
|
||||
stopMousePresenceTracking();
|
||||
return;
|
||||
}
|
||||
emitMousePresence();
|
||||
}, 50);
|
||||
};
|
||||
|
||||
const syncMousePresenceTracking = () => {
|
||||
if (isLyricLocked && isLyricWindowVisible && lyricWindow && !lyricWindow.isDestroyed()) {
|
||||
startMousePresenceTracking();
|
||||
} else {
|
||||
stopMousePresenceTracking();
|
||||
}
|
||||
};
|
||||
|
||||
const createWin = () => {
|
||||
console.log('Creating lyric window');
|
||||
@@ -102,12 +161,32 @@ const createWin = () => {
|
||||
|
||||
// 监听窗口关闭事件
|
||||
lyricWindow.on('closed', () => {
|
||||
stopMousePresenceTracking();
|
||||
isLyricLocked = false;
|
||||
isLyricWindowVisible = false;
|
||||
if (lyricWindow) {
|
||||
lyricWindow.destroy();
|
||||
lyricWindow = null;
|
||||
}
|
||||
});
|
||||
|
||||
lyricWindow.on('show', () => {
|
||||
isLyricWindowVisible = true;
|
||||
syncMousePresenceTracking();
|
||||
});
|
||||
lyricWindow.on('hide', () => {
|
||||
isLyricWindowVisible = false;
|
||||
stopMousePresenceTracking();
|
||||
});
|
||||
lyricWindow.on('minimize', () => {
|
||||
isLyricWindowVisible = false;
|
||||
stopMousePresenceTracking();
|
||||
});
|
||||
lyricWindow.on('restore', () => {
|
||||
isLyricWindowVisible = true;
|
||||
syncMousePresenceTracking();
|
||||
});
|
||||
|
||||
// 监听窗口大小变化事件,保存新的尺寸
|
||||
lyricWindow.on('resize', () => {
|
||||
// 如果正在拖动,忽略大小调整事件
|
||||
@@ -205,6 +284,17 @@ export const loadLyricWindow = (ipcMain: IpcMain, mainWin: BrowserWindow): void
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('set-lyric-lock-state', (_, isLocked: boolean) => {
|
||||
isLyricLocked = isLocked;
|
||||
if (lyricWindow && !lyricWindow.isDestroyed()) {
|
||||
// 锁定时禁用 resize,避免鼠标移到边缘仍显示调整光标
|
||||
lyricWindow.setResizable(!isLocked);
|
||||
// 设置初始穿透状态,后续 polling 会按实际位置纠正
|
||||
lyricWindow.setIgnoreMouseEvents(isLocked, { forward: true });
|
||||
}
|
||||
syncMousePresenceTracking();
|
||||
});
|
||||
|
||||
// 处理鼠标事件
|
||||
ipcMain.on('mouseenter-lyric', () => {
|
||||
if (lyricWindow && !lyricWindow.isDestroyed()) {
|
||||
|
||||
+127
-126
@@ -149,124 +149,125 @@ const parseLyricsString = async (
|
||||
}
|
||||
};
|
||||
|
||||
// 设置音乐相关的监听器
|
||||
// 解析当前 playMusic.lyric 写入 lrcArray, 供 watcher / openLyric / onLyricWindowReady 共用
|
||||
const ensureLyricsLoaded = async (force = false) => {
|
||||
const songId = playMusic.value?.id;
|
||||
if (!songId) {
|
||||
lrcArray.value = [];
|
||||
lrcTimeArray.value = [];
|
||||
nowIndex.value = 0;
|
||||
return;
|
||||
}
|
||||
if (!force && lrcArray.value.length > 0) return;
|
||||
|
||||
await nextTick();
|
||||
|
||||
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;
|
||||
}
|
||||
} else if (lyricData && typeof lyricData === 'object' && lyricData.lrcArray?.length > 0) {
|
||||
const rawLrc = lyricData.lrcArray || [];
|
||||
lrcTimeArray.value = lyricData.lrcTimeArray || [];
|
||||
|
||||
try {
|
||||
const { translateLyrics } = await import('@/services/lyricTranslation');
|
||||
lrcArray.value = await translateLyrics(rawLrc as any);
|
||||
} catch (e) {
|
||||
console.error('翻译歌词失败,使用原始歌词:', e);
|
||||
lrcArray.value = rawLrc as any;
|
||||
}
|
||||
} else if (isElectron && playMusic.value.playMusicUrl?.startsWith('local:///')) {
|
||||
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 if (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 if (typeof songId === 'number') {
|
||||
// 在线歌曲但 lyric 字段尚未加载, 主动调 API 兜底
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if (isElectron && isLyricWindowOpen.value) {
|
||||
sendLyricToWin();
|
||||
setTimeout(() => sendLyricToWin(), 500);
|
||||
}
|
||||
};
|
||||
|
||||
const setupMusicWatchers = () => {
|
||||
const store = getPlayerStore();
|
||||
|
||||
// 监听 playerStore.playMusic 的变化以更新歌词数据
|
||||
// 切歌时 id 变化, 强制重新解析
|
||||
watch(
|
||||
() => 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 () => {
|
||||
console.log('歌曲切换,更新歌词数据');
|
||||
|
||||
// 检查是否有原始歌词字符串需要解析
|
||||
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;
|
||||
}
|
||||
} else if (lyricData && typeof lyricData === 'object' && lyricData.lrcArray?.length > 0) {
|
||||
// 使用现有的歌词数据结构
|
||||
const rawLrc = lyricData.lrcArray || [];
|
||||
lrcTimeArray.value = lyricData.lrcTimeArray || [];
|
||||
|
||||
try {
|
||||
const { translateLyrics } = await import('@/services/lyricTranslation');
|
||||
lrcArray.value = await translateLyrics(rawLrc as any);
|
||||
} catch (e) {
|
||||
console.error('翻译歌词失败,使用原始歌词:', e);
|
||||
lrcArray.value = rawLrc as any;
|
||||
}
|
||||
} 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 = [];
|
||||
}
|
||||
// 当歌词数据更新时,如果歌词窗口打开,则发送数据
|
||||
if (isElectron && isLyricWindowOpen.value) {
|
||||
console.log('歌词窗口已打开,同步最新歌词数据');
|
||||
// 不管歌词数组是否为空,都发送最新数据
|
||||
sendLyricToWin();
|
||||
|
||||
// 再次延迟发送,确保歌词窗口已完全加载
|
||||
setTimeout(() => {
|
||||
sendLyricToWin();
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
if (newId !== oldId) nowIndex.value = 0;
|
||||
await ensureLyricsLoaded(true);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 同一首歌但 lyric 字段后到 (重启 + autoPlay 关闭场景)
|
||||
watch(
|
||||
() => playMusic.value?.lyric,
|
||||
() => {
|
||||
if (lrcArray.value.length === 0 && playMusic.value?.id) {
|
||||
ensureLyricsLoaded();
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const setupAudioListeners = () => {
|
||||
@@ -486,7 +487,10 @@ const setupAudioListeners = () => {
|
||||
if (isElectron) {
|
||||
window.api.sendSong(cloneDeep(getPlayerStore().playMusic));
|
||||
}
|
||||
// 启动进度更新
|
||||
// 兜底: 重启后首次点播放时 lrcArray 仍为空则主动加载
|
||||
if (lrcArray.value.length === 0 && playMusic.value?.id) {
|
||||
ensureLyricsLoaded();
|
||||
}
|
||||
startProgressInterval();
|
||||
});
|
||||
|
||||
@@ -893,28 +897,20 @@ const stopLyricSync = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 修改openLyric函数,添加定时同步
|
||||
export const openLyric = () => {
|
||||
export const openLyric = async () => {
|
||||
if (!isElectron) return;
|
||||
|
||||
// 检查是否有播放中的歌曲
|
||||
if (!playMusic.value || !playMusic.value.id) {
|
||||
console.log('没有正在播放的歌曲,无法打开歌词窗口');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Opening lyric window with current song:', playMusic.value?.name);
|
||||
|
||||
isLyricWindowOpen.value = !isLyricWindowOpen.value;
|
||||
if (isLyricWindowOpen.value) {
|
||||
// 立即打开窗口
|
||||
window.api.openLyric();
|
||||
|
||||
// 确保有歌词数据,如果没有,则使用默认的"无歌词"提示
|
||||
// 先发"加载中"占位, 防止窗口启动期间显示"无歌词"
|
||||
if (!lrcArray.value || lrcArray.value.length === 0) {
|
||||
// 如果当前播放的歌曲有ID但没有歌词,则尝试加载歌词
|
||||
console.log('尝试加载歌词数据...');
|
||||
// 发送默认的"无歌词"数据
|
||||
const emptyLyricData = {
|
||||
type: 'empty',
|
||||
nowIndex: 0,
|
||||
@@ -928,12 +924,15 @@ export const openLyric = () => {
|
||||
playMusic: playMusic.value
|
||||
};
|
||||
window.api.sendLyric(JSON.stringify(emptyLyricData));
|
||||
|
||||
// 关键: 主动加载歌词, 不依赖 watcher
|
||||
// (重启场景下 playerCore.playMusic 整体替换可能未触发 lyric watcher)
|
||||
await ensureLyricsLoaded(true);
|
||||
} else {
|
||||
// 发送完整歌词数据
|
||||
sendLyricToWin();
|
||||
}
|
||||
|
||||
// 延迟重发一次,以防窗口加载略慢
|
||||
// 延迟重发, 防窗口加载慢丢消息
|
||||
setTimeout(() => {
|
||||
if (isLyricWindowOpen.value) {
|
||||
sendLyricToWin();
|
||||
@@ -1055,11 +1054,13 @@ export const initAudioListeners = async () => {
|
||||
window.api.onLyricWindowClosed(() => {
|
||||
isLyricWindowOpen.value = false;
|
||||
});
|
||||
// 歌词窗口 Vue 加载完成后,发送完整歌词数据
|
||||
window.api.onLyricWindowReady(() => {
|
||||
if (isLyricWindowOpen.value) {
|
||||
sendLyricToWin();
|
||||
window.api.onLyricWindowReady(async () => {
|
||||
if (!isLyricWindowOpen.value) return;
|
||||
// 窗口加载完成时再兜底加载一次, 防止 openLyric 阶段 lyric 字段尚未到位
|
||||
if (lrcArray.value.length === 0 && playMusic.value?.id) {
|
||||
await ensureLyricsLoaded(true);
|
||||
}
|
||||
sendLyricToWin();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -341,6 +341,7 @@ const displayMode = computed(() => lyricSetting.value.displayMode);
|
||||
const showTranslation = computed(() => lyricSetting.value.showTranslation);
|
||||
|
||||
let hideControlsTimer: number | null = null;
|
||||
let removeMousePresenceListener: (() => void) | null = null;
|
||||
|
||||
const isHovering = ref(false);
|
||||
|
||||
@@ -400,6 +401,7 @@ watch(
|
||||
// 锁定时自动关闭主题色面板
|
||||
showThemeColorPanel.value = false;
|
||||
}
|
||||
windowData.electron.ipcRenderer.send('set-lyric-lock-state', newLock);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -782,10 +784,27 @@ onMounted(() => {
|
||||
|
||||
// 通知主窗口歌词窗口已就绪,请求发送完整歌词数据
|
||||
windowData.electron.ipcRenderer.send('lyric-ready');
|
||||
|
||||
removeMousePresenceListener = window.ipcRenderer.on(
|
||||
'lyric-mouse-presence',
|
||||
(isInside: boolean) => {
|
||||
isHovering.value = isInside;
|
||||
|
||||
if (lyricSetting.value.isLock) {
|
||||
windowData.electron.ipcRenderer.send('set-ignore-mouse', !isInside);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
windowData.electron.ipcRenderer.send('set-lyric-lock-state', lyricSetting.value.isLock);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', updateContainerHeight);
|
||||
if (removeMousePresenceListener) {
|
||||
removeMousePresenceListener();
|
||||
removeMousePresenceListener = null;
|
||||
}
|
||||
});
|
||||
|
||||
const checkTheme = () => {
|
||||
|
||||
Reference in New Issue
Block a user