feat: 优化桌面歌词添加歌曲控制 上一首下一首 播放暂停

This commit is contained in:
alger
2024-12-16 22:12:28 +08:00
parent e1557a51a3
commit 85bd0ad015
5 changed files with 229 additions and 147 deletions

2
app.js
View File

@@ -66,7 +66,7 @@ function createWindow() {
store.set('set', setJson);
}
loadLyricWindow(ipcMain);
loadLyricWindow(ipcMain, mainWin);
}
// 限制只能启动一个应用

View File

@@ -1,11 +1,11 @@
const { BrowserWindow, screen } = require('electron');
const { BrowserWindow, screen, ipcRenderer } = require('electron');
const path = require('path');
const config = require('./config');
let lyricWindow = null;
let isDragging = false;
const createWin = () => {
console.log('Creating lyric window');
lyricWindow = new BrowserWindow({
width: 800,
height: 200,
@@ -21,16 +21,26 @@ const createWin = () => {
webSecurity: false,
},
});
// 监听窗口关闭事件
lyricWindow.on('closed', () => {
console.log('Lyric window closed');
lyricWindow = null;
});
};
const loadLyricWindow = (ipcMain) => {
const loadLyricWindow = (ipcMain, mainWin) => {
ipcMain.on('open-lyric', () => {
console.log('Received open-lyric request');
if (lyricWindow) {
console.log('Lyric window exists, focusing');
if (lyricWindow.isMinimized()) lyricWindow.restore();
lyricWindow.focus();
lyricWindow.show();
return;
}
console.log('Creating new lyric window');
createWin();
if (process.env.NODE_ENV === 'development') {
lyricWindow.webContents.openDevTools({ mode: 'detach' });
@@ -41,26 +51,39 @@ const loadLyricWindow = (ipcMain) => {
}
lyricWindow.setMinimumSize(600, 200);
// 隐藏任务栏
lyricWindow.setSkipTaskbar(true);
lyricWindow.show();
lyricWindow.once('ready-to-show', () => {
console.log('Lyric window ready to show');
lyricWindow.show();
});
});
ipcMain.on('send-lyric', (e, data) => {
if (lyricWindow) {
lyricWindow.webContents.send('receive-lyric', data);
if (lyricWindow && !lyricWindow.isDestroyed()) {
try {
lyricWindow.webContents.send('receive-lyric', data);
} catch (error) {
console.error('Error processing lyric data:', error);
}
} else {
console.log('Cannot send lyric: window not available or destroyed');
}
});
ipcMain.on('top-lyric', (e, data) => {
lyricWindow.setAlwaysOnTop(data);
if (lyricWindow && !lyricWindow.isDestroyed()) {
lyricWindow.setAlwaysOnTop(data);
}
});
ipcMain.on('close-lyric', () => {
lyricWindow.close();
lyricWindow = null;
if (lyricWindow && !lyricWindow.isDestroyed()) {
lyricWindow.webContents.send('lyric-window-close');
mainWin.webContents.send('lyric-control-back', 'close');
lyricWindow.close();
lyricWindow = null;
}
});
ipcMain.on('mouseenter-lyric', () => {
@@ -71,11 +94,6 @@ const loadLyricWindow = (ipcMain) => {
lyricWindow.setIgnoreMouseEvents(false);
});
// 开始拖动
ipcMain.on('lyric-drag-start', () => {
isDragging = true;
});
// 处理拖动移动
ipcMain.on('lyric-drag-move', (e, { deltaX, deltaY }) => {
if (!lyricWindow) return;
@@ -91,11 +109,6 @@ const loadLyricWindow = (ipcMain) => {
lyricWindow.setPosition(newX, newY);
});
// 结束拖动
ipcMain.on('lyric-drag-end', () => {
isDragging = false;
});
// 添加鼠标穿透事件处理
ipcMain.on('set-ignore-mouse', (e, shouldIgnore) => {
if (!lyricWindow) return;
@@ -108,6 +121,12 @@ const loadLyricWindow = (ipcMain) => {
lyricWindow.setIgnoreMouseEvents(false);
}
});
// 添加播放控制处理
ipcMain.on('control-back', (e, command) => {
console.log('Received control-back request:', command);
mainWin.webContents.send('lyric-control-back', command);
});
};
module.exports = {

View File

@@ -17,6 +17,7 @@ export const correctionTime = ref(0.4); // 歌词矫正时间Correction time
export const currentLrcProgress = ref(0); // 来存储当前歌词的进度
export const playMusic = computed(() => store.state.playMusic as SongResult); // 当前播放歌曲
export const sound = ref<Howl | null>(audioService.getCurrentSound());
export const isLyricWindowOpen = ref(false); // 新增状态
document.onkeyup = (e) => {
// 检查事件目标是否是输入框元素
@@ -53,13 +54,18 @@ watch(
watch(
() => store.state.playMusic,
() => {
nextTick(() => {
nextTick(async () => {
lrcArray.value = playMusic.value.lyric?.lrcArray || [];
lrcTimeArray.value = playMusic.value.lyric?.lrcTimeArray || [];
// 当歌词数据更新时,如果歌词窗口打开,则发送数据
if (isElectron.value && isLyricWindowOpen.value && lrcArray.value.length > 0) {
sendLyricToWin();
}
});
},
{
deep: true,
immediate: true,
},
);
@@ -76,8 +82,13 @@ export const audioServiceOn = (audio: typeof audioService) => {
if (newIndex !== nowIndex.value) {
nowIndex.value = newIndex;
currentLrcProgress.value = 0;
// 当歌词索引更新时,发送歌词数据
if (isElectron.value && isLyricWindowOpen.value) {
sendLyricToWin();
}
}
if (isElectron.value) {
// 定期发送歌词数据更新
if (isElectron.value && isLyricWindowOpen.value) {
sendLyricToWin();
}
}, 50);
@@ -87,11 +98,14 @@ export const audioServiceOn = (audio: typeof audioService) => {
audio.onPause(() => {
store.commit('setPlayMusic', false);
clearInterval(interval);
// 暂停时也发送一次状态更新
if (isElectron.value && isLyricWindowOpen.value) {
sendLyricToWin();
}
});
// 监听结束
audio.onEnd(() => {
handleEnded();
if (store.state.playMode === 1) {
// 单曲循环模式
audio.getCurrentSound()?.play();
@@ -213,7 +227,7 @@ export const useLyricProgress = () => {
};
};
// 设置前播放时间
// 设置<EFBFBD><EFBFBD><EFBFBD>前播放时间
export const setAudioTime = (index: number) => {
const currentSound = sound.value;
if (!currentSound) return;
@@ -241,72 +255,33 @@ export const getLrcTimeRange = (index: number) => ({
watch(
() => lrcArray.value,
(newLrcArray) => {
if (newLrcArray.length > 0 && isElectron.value) {
// 重新初始化歌词数据
initLyricWindow();
// 发送当前状态
if (newLrcArray.length > 0 && isElectron.value && isLyricWindowOpen.value) {
sendLyricToWin();
}
},
);
// 监听播放状态变化
watch(isPlaying, (newIsPlaying) => {
if (isElectron.value) {
sendLyricToWin(newIsPlaying);
}
});
// 处理歌曲结束
export const handleEnded = () => {
if (isElectron.value) {
setTimeout(() => {
initLyricWindow();
sendLyricToWin();
}, 100);
}
};
// 初始化歌词数据
export const initLyricWindow = () => {
if (!isElectron.value) return;
try {
if (lrcArray.value.length > 0) {
console.log('Initializing lyric window with data:', {
lrcArray: lrcArray.value,
lrcTimeArray: lrcTimeArray.value,
allTime: allTime.value,
});
const staticData = {
type: 'init',
lrcArray: lrcArray.value,
lrcTimeArray: lrcTimeArray.value,
allTime: allTime.value,
};
windowData.electronAPI.sendLyric(JSON.stringify(staticData));
} else {
console.log('No lyrics available for initialization');
}
} catch (error) {
console.error('Error initializing lyric window:', error);
}
};
// 发送歌词更新数据
export const sendLyricToWin = (isPlay: boolean = true) => {
if (!isElectron.value) return;
export const sendLyricToWin = () => {
if (!isElectron.value || !isLyricWindowOpen.value) {
console.log('Cannot send lyric: electron or lyric window not available');
return;
}
try {
if (lrcArray.value.length > 0) {
const nowIndex = getLrcIndex(nowTime.value);
const updateData = {
type: 'update',
type: 'full',
nowIndex,
nowTime: nowTime.value,
startCurrentTime: lrcTimeArray.value[nowIndex],
nextTime: lrcTimeArray.value[nowIndex + 1],
isPlay,
isPlay: isPlaying.value,
lrcArray: lrcArray.value,
lrcTimeArray: lrcTimeArray.value,
allTime: allTime.value,
playMusic: playMusic.value,
};
windowData.electronAPI.sendLyric(JSON.stringify(updateData));
}
@@ -317,13 +292,55 @@ export const sendLyricToWin = (isPlay: boolean = true) => {
export const openLyric = () => {
if (!isElectron.value) return;
console.log('Opening lyric window');
console.log('Opening lyric window with current song:', playMusic.value?.name);
windowData.electronAPI.openLyric();
isLyricWindowOpen.value = true;
// 延迟一下初始化,确保窗口已经创建
setTimeout(() => {
console.log('Initializing lyric window after delay');
initLyricWindow();
sendLyricToWin();
if (isLyricWindowOpen.value) {
console.log('Initializing lyric window with data:', {
hasLyrics: lrcArray.value.length > 0,
songName: playMusic.value?.name,
});
sendLyricToWin();
}
}, 500);
};
// 添加关闭歌词窗口的方法
export const closeLyric = () => {
if (!isElectron.value) return;
isLyricWindowOpen.value = false;
windowData.electron.ipcRenderer.send('close-lyric');
};
// 添加播放控制命令监听
if (isElectron.value) {
windowData.electron.ipcRenderer.on('lyric-control-back', (command: string) => {
console.log('Received playback control command:', command);
switch (command) {
case 'playpause':
if (store.state.play) {
store.commit('setPlayMusic', false);
audioService.getCurrentSound()?.pause();
} else {
store.commit('setPlayMusic', true);
audioService.getCurrentSound()?.play();
}
break;
case 'prev':
store.commit('prevPlay');
break;
case 'next':
store.commit('nextPlay');
break;
case 'close':
closeLyric();
break;
default:
console.log('Unknown command:', command);
break;
}
});
}

View File

@@ -69,7 +69,11 @@
</n-tooltip>
<n-tooltip v-if="isElectron" class="music-lyric" trigger="hover" :z-index="9999999">
<template #trigger>
<i class="iconfont ri-netease-cloud-music-line" @click="openLyric"></i>
<i
class="iconfont ri-netease-cloud-music-line"
:class="{ 'text-green-500': isLyricWindowOpen }"
@click="openLyricWindow"
></i>
</template>
歌词
</n-tooltip>
@@ -113,7 +117,7 @@ import { useTemplateRef } from 'vue';
import { useStore } from 'vuex';
import SongItem from '@/components/common/SongItem.vue';
import { allTime, isElectron, nowTime, openLyric, sound } from '@/hooks/MusicHook';
import { allTime, isElectron, isLyricWindowOpen, nowTime, openLyric, sound } from '@/hooks/MusicHook';
import type { SongResult } from '@/type/music';
import { getImgUrl, secondToMinute, setAnimationClass } from '@/utils';
@@ -253,6 +257,10 @@ const toggleFavorite = async (e: Event) => {
store.commit('addToFavorite', playMusic.value.id);
}
};
const openLyricWindow = () => {
openLyric();
};
</script>
<style lang="scss" scoped>

View File

@@ -18,15 +18,28 @@
<i class="ri-add-line"></i>
</n-button>
</n-button-group>
<div>{{ staticData.playMusic.name }}</div>
</div>
<!-- 添加播放控制按钮 -->
<div class="play-controls">
<div class="control-button" @click="handlePrev">
<i class="ri-skip-back-fill"></i>
</div>
<div class="control-button play-button" @click="handlePlayPause">
<i :class="dynamicData.isPlay ? 'ri-pause-fill' : 'ri-play-fill'"></i>
</div>
<div class="control-button" @click="handleNext">
<i class="ri-skip-forward-fill"></i>
</div>
</div>
<div class="control-buttons">
<div class="control-button" @click="checkTheme">
<i v-if="lyricSetting.theme === 'light'" class="ri-sun-line"></i>
<i v-else class="ri-moon-line"></i>
</div>
<div class="control-button" @click="handleTop">
<!-- <div class="control-button" @click="handleTop">
<i class="ri-pushpin-line" :class="{ active: lyricSetting.isTop }"></i>
</div>
</div> -->
<div id="lyric-lock" class="control-button" @click="handleLock">
<i v-if="lyricSetting.isLock" class="ri-lock-line"></i>
<i v-else class="ri-lock-unlock-line"></i>
@@ -46,7 +59,10 @@
v-for="(line, index) in staticData.lrcArray"
:key="index"
class="lyric-line"
:style="lyricLineStyle"
:style="{
...lyricLineStyle,
display: line.text ? 'flex' : 'none',
}"
:class="{
'lyric-line-current': index === currentIndex,
'lyric-line-passed': index < currentIndex,
@@ -71,18 +87,18 @@
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import { SongResult } from '@/type/music';
defineOptions({
name: 'Lyric',
});
const windowData = window as any;
const containerRef = ref<HTMLElement | null>(null);
const containerHeight = ref(0);
const lineHeight = ref(60);
const currentIndex = ref(0);
const isInitialized = ref(false);
// 字体大小控制
const fontSize = ref(24); // 默认字体大小
const fontSizeStep = 2; // 每次整的步长
@@ -94,10 +110,12 @@ const staticData = ref<{
lrcArray: Array<{ text: string; trText: string }>;
lrcTimeArray: number[];
allTime: number;
playMusic: SongResult;
}>({
lrcArray: [],
lrcTimeArray: [],
allTime: 0,
playMusic: {} as SongResult,
});
// 动态数据
@@ -140,7 +158,6 @@ const clearHideTimer = () => {
// 处理鼠标进入窗口
const handleMouseEnter = () => {
console.log('handleMouseEnter');
if (lyricSetting.value.isLock) {
isHovering.value = true;
windowData.electron.ipcRenderer.send('set-ignore-mouse', true);
@@ -151,7 +168,6 @@ const handleMouseEnter = () => {
// 处理鼠标离开窗口
const handleMouseLeave = () => {
console.log('handleMouseLeave');
if (!lyricSetting.value.isLock) return;
isHovering.value = false;
windowData.electron.ipcRenderer.send('set-ignore-mouse', false);
@@ -180,7 +196,7 @@ onUnmounted(() => {
// 计算歌词滚动位置
const wrapperStyle = computed(() => {
if (!isInitialized.value || !containerHeight.value) {
if (!containerHeight.value) {
return {
transform: 'translateY(0)',
transition: 'none',
@@ -208,7 +224,7 @@ const wrapperStyle = computed(() => {
return {
transform: `translateY(${finalOffset}px)`,
transition: isInitialized.value ? 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)' : 'none',
transition: 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
};
});
@@ -281,8 +297,8 @@ const actualTime = ref(0);
// 计算当前行的进度
const currentProgress = computed(() => {
const { startCurrentTime, nextTime, isPlay } = dynamicData.value;
if (!startCurrentTime || !nextTime || !isPlay) return 0;
const { startCurrentTime, nextTime } = dynamicData.value;
if (!startCurrentTime || !nextTime) return 0;
const duration = nextTime - startCurrentTime;
const elapsed = actualTime.value - startCurrentTime;
@@ -364,22 +380,34 @@ const handleDataUpdate = (parsedData: {
nextTime: number;
isPlay: boolean;
nowIndex: number;
lrcArray: Array<{ text: string; trText: string }>;
lrcTimeArray: number[];
allTime: number;
playMusic: SongResult;
}) => {
// 确保数据存在且格式正确
if (!parsedData || typeof parsedData.nowTime !== 'number') {
if (!parsedData) {
console.error('Invalid update data received:', parsedData);
return;
}
// 更新静态数据
staticData.value = {
lrcArray: parsedData.lrcArray || [],
lrcTimeArray: parsedData.lrcTimeArray || [],
allTime: parsedData.allTime || 0,
playMusic: parsedData.playMusic || {},
};
// 更新动态数据
dynamicData.value = {
nowTime: parsedData.nowTime,
startCurrentTime: parsedData.startCurrentTime,
nextTime: parsedData.nextTime,
nowTime: parsedData.nowTime || 0,
startCurrentTime: parsedData.startCurrentTime || 0,
nextTime: parsedData.nextTime || 0,
isPlay: parsedData.isPlay,
};
// 更新索引
if (typeof parsedData.nowIndex === 'number' && parsedData.nowIndex !== currentIndex.value) {
if (typeof parsedData.nowIndex === 'number') {
currentIndex.value = parsedData.nowIndex;
}
};
@@ -400,33 +428,7 @@ onMounted(() => {
windowData.electron.ipcRenderer.on('receive-lyric', (data: string) => {
try {
const parsedData = JSON.parse(data);
if (parsedData.type === 'init') {
// 初始化重置状态
currentIndex.value = 0;
isInitialized.value = false;
// 清理可能存在的动画
if (animationFrameId.value) {
cancelAnimationFrame(animationFrameId.value);
animationFrameId.value = null;
}
// 确保数据格式正确
if (Array.isArray(parsedData.lrcArray)) {
staticData.value = {
lrcArray: parsedData.lrcArray,
lrcTimeArray: parsedData.lrcTimeArray || [],
allTime: parsedData.allTime || 0,
};
} else {
console.error('Invalid lyric array format:', parsedData);
}
nextTick(() => {
isInitialized.value = true;
});
} else if (parsedData.type === 'update') {
handleDataUpdate(parsedData);
}
handleDataUpdate(parsedData);
} catch (error) {
console.error('Error parsing lyric data:', error);
}
@@ -467,7 +469,7 @@ watch(
{ deep: true },
);
// 添拖动相关变量
// 添<EFBFBD><EFBFBD>拖动相关变量
const isDragging = ref(false);
const startPosition = ref({ x: 0, y: 0 });
@@ -534,6 +536,19 @@ onMounted(() => {
};
}
});
// 添加播放控制相关的函数
const handlePlayPause = () => {
windowData.electron.ipcRenderer.send('control-back', 'playpause');
};
const handlePrev = () => {
windowData.electron.ipcRenderer.send('control-back', 'prev');
};
const handleNext = () => {
windowData.electron.ipcRenderer.send('control-back', 'next');
};
</script>
<style>
@@ -554,7 +569,7 @@ body {
cursor: default;
&:hover {
background: rgba(0, 0, 0, 0.3);
background: rgba(0, 0, 0, 0.5);
.control-bar {
&-show {
opacity: 1;
@@ -571,7 +586,7 @@ body {
--text-color: #ffffff;
--text-secondary: rgba(255, 255, 255, 0.6);
--highlight-color: #1db954;
--control-bg: rgba(0, 0, 0, 0.3);
--control-bg: rgba(124, 124, 124, 0.3);
}
&.light {
@@ -584,13 +599,13 @@ body {
.control-bar {
position: absolute;
top: 0;
top: 10px;
left: 0;
right: 0;
height: 40px;
height: 80px;
display: flex;
justify-content: flex-end;
align-items: center;
justify-content: space-between;
align-items: start;
padding: 0 20px;
opacity: 0;
visibility: hidden;
@@ -600,13 +615,28 @@ body {
z-index: 100;
.font-size-controls {
margin-right: auto; // 将字体控制放在侧
padding-right: 20px;
-webkit-app-region: no-drag;
color: var(--text-color);
display: flex;
align-items: center;
gap: 16px;
}
.play-controls {
position: absolute;
top: 0px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 16px;
-webkit-app-region: no-drag;
.n-button {
.play-button {
width: 36px;
height: 36px;
i {
font-size: 16px;
font-size: 24px;
}
}
}
@@ -623,23 +653,21 @@ body {
}
.control-button {
width: 32px;
height: 32px;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 50%;
border-radius: 8px;
color: var(--text-color);
transition: all 0.2s ease;
backdrop-filter: blur(4px);
&:hover {
background: var(--control-bg);
}
i {
font-size: 18px;
font-size: 20px;
text-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
&.active {
@@ -650,7 +678,7 @@ body {
.lyric-container {
position: absolute;
top: 40px;
top: 80px;
left: 0;
right: 0;
bottom: 0;
@@ -689,8 +717,7 @@ body {
opacity: 1;
}
&.lyric-line-passed,
&.lyric-line-next {
&.lyric-line-passed {
opacity: 0.6;
}
}
@@ -751,6 +778,10 @@ body {
.lyric_lock & .font-size-controls {
display: none;
}
.lyric_lock & .play-controls {
display: none;
}
}
.lyric_lock {
@@ -758,5 +789,12 @@ body {
&:hover {
background: transparent;
}
#lyric-lock {
position: absolute;
top: 0;
right: 72px;
background: var(--control-bg);
}
}
</style>