feat: 优化桌面歌词功能 添加歌词进度 优化歌词页面样式

This commit is contained in:
alger
2024-12-06 23:50:44 +08:00
parent 8870390770
commit edf5c77ea0
10 changed files with 771 additions and 217 deletions

7
app.js
View File

@@ -3,6 +3,7 @@ const path = require('path');
const Store = require('electron-store');
const setJson = require('./electron/set.json');
const { loadLyricWindow } = require('./electron/lyric');
const config = require('./electron/config');
let mainWin = null;
function createWindow() {
@@ -11,8 +12,8 @@ function createWindow() {
height: 780,
frame: false,
webPreferences: {
nodeIntegration: true,
// contextIsolation: false,
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, '/electron/preload.js'),
},
});
@@ -20,7 +21,7 @@ function createWindow() {
win.setMinimumSize(1200, 780);
if (process.env.NODE_ENV === 'development') {
win.webContents.openDevTools({ mode: 'detach' });
win.loadURL('http://localhost:4488/');
win.loadURL(`http://localhost:${config.development.mainPort}/`);
} else {
win.loadURL(`file://${__dirname}/dist/index.html`);
}

1
components.d.ts vendored
View File

@@ -13,6 +13,7 @@ declare module 'vue' {
MvPlayer: typeof import('./src/components/MvPlayer.vue')['default']
NAvatar: typeof import('naive-ui')['NAvatar']
NButton: typeof import('naive-ui')['NButton']
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
NDrawer: typeof import('naive-ui')['NDrawer']

11
electron/config.js Normal file
View File

@@ -0,0 +1,11 @@
module.exports = {
// 开发环境配置
development: {
mainPort: 4488,
lyricPort: 4488,
},
// 生产环境配置
production: {
distPath: '../dist',
},
};

View File

@@ -1,5 +1,6 @@
const { BrowserWindow } = require('electron');
const path = require('path');
const config = require('./config');
let lyricWindow = null;
@@ -10,13 +11,16 @@ const createWin = () => {
frame: false,
show: false,
transparent: true,
hasShadow: false,
webPreferences: {
nodeIntegration: true,
nodeIntegration: false,
contextIsolation: true,
preload: `${__dirname}/preload.js`,
contextIsolation: false,
webSecurity: false,
},
});
};
const loadLyricWindow = (ipcMain) => {
ipcMain.on('open-lyric', () => {
if (lyricWindow) {
@@ -28,9 +32,9 @@ const loadLyricWindow = (ipcMain) => {
createWin();
if (process.env.NODE_ENV === 'development') {
lyricWindow.webContents.openDevTools({ mode: 'detach' });
lyricWindow.loadURL('http://localhost:4678/#/lyric');
lyricWindow.loadURL(`http://localhost:${config.development.lyricPort}/#/lyric`);
} else {
const distPath = path.resolve(__dirname, '../dist');
const distPath = path.resolve(__dirname, config.production.distPath);
lyricWindow.loadURL(`file://${distPath}/index.html#/lyric`);
}

View File

@@ -1,5 +1,6 @@
const { contextBridge, ipcRenderer, app } = require('electron');
const { contextBridge, ipcRenderer } = require('electron');
// 主进程通信
contextBridge.exposeInMainWorld('electronAPI', {
minimize: () => ipcRenderer.send('minimize-window'),
maximize: () => ipcRenderer.send('maximize-window'),
@@ -11,18 +12,19 @@ contextBridge.exposeInMainWorld('electronAPI', {
sendLyric: (data) => ipcRenderer.send('send-lyric', data),
});
const electronHandler = {
// 存储相关
contextBridge.exposeInMainWorld('electron', {
ipcRenderer: {
setStoreValue: (key, value) => {
ipcRenderer.send('setStore', key, value);
setStoreValue: (key, value) => ipcRenderer.send('setStore', key, value),
getStoreValue: (key) => ipcRenderer.sendSync('getStore', key),
on: (channel, func) => {
ipcRenderer.on(channel, (event, ...args) => func(...args));
},
getStoreValue(key) {
const resp = ipcRenderer.sendSync('getStore', key);
return resp;
once: (channel, func) => {
ipcRenderer.once(channel, (event, ...args) => func(...args));
},
send: (channel, data) => {
ipcRenderer.send(channel, data);
},
},
app,
};
contextBridge.exposeInMainWorld('electron', electronHandler);
});

69
electron/update.js Normal file
View File

@@ -0,0 +1,69 @@
const { app, BrowserWindow } = require('electron');
const axios = require('axios');
const fs = require('fs');
const path = require('path');
const AdmZip = require('adm-zip');
class Updater {
constructor(mainWindow) {
this.mainWindow = mainWindow;
this.updateUrl = 'http://your-server.com/update'; // 更新服务器地址
this.version = app.getVersion();
}
// 检查更新
async checkForUpdates() {
try {
const response = await axios.get(`${this.updateUrl}/check`, {
params: {
version: this.version,
},
});
if (response.data.hasUpdate) {
await this.downloadUpdate(response.data.downloadUrl);
}
} catch (error) {
console.error('检查更新失败:', error);
}
}
// 下载更新
async downloadUpdate(downloadUrl) {
try {
const response = await axios({
url: downloadUrl,
method: 'GET',
responseType: 'arraybuffer',
});
const tempPath = path.join(app.getPath('temp'), 'update.zip');
fs.writeFileSync(tempPath, response.data);
await this.extractUpdate(tempPath);
} catch (error) {
console.error('下载更新失败:', error);
}
}
// 解压更新
async extractUpdate(zipPath) {
try {
const zip = new AdmZip(zipPath);
const targetPath = path.join(__dirname, '../dist'); // 前端文件目录
// 解压文件
zip.extractAllTo(targetPath, true);
// 删除临时文件
fs.unlinkSync(zipPath);
// 刷新页面
this.mainWindow.webContents.reload();
} catch (error) {
console.error('解压更新失败:', error);
}
}
}
module.exports = Updater;

View File

@@ -32,7 +32,7 @@
"autoprefixer": "^10.4.20",
"axios": "^1.7.7",
"cross-env": "^7.0.3",
"electron": "^32.0.1",
"electron": "^32.2.7",
"electron-builder": "^25.0.5",
"eslint": "^8.56.0",
"eslint-config-airbnb-base": "^15.0.0",

View File

@@ -160,37 +160,110 @@ export const getLrcTimeRange = (index: number) => ({
nextTime: lrcTimeArray.value[index + 1],
});
// 监听歌词数组变化,当切换歌曲时重新初始化歌词窗口
watch(
() => lrcArray.value,
(newLrcArray) => {
if (newLrcArray.length > 0 && isElectron.value) {
// 重新初始化歌词数据
initLyricWindow();
// 发送当前状态
sendLyricToWin();
}
},
);
// 监听播放状态变化
watch(isPlaying, (newIsPlaying) => {
if (isElectron.value) {
sendLyricToWin(newIsPlaying);
}
});
// 监听时间变化
watch(nowTime, (newTime) => {
const newIndex = getLrcIndex(newTime);
if (newIndex !== nowIndex.value) {
nowIndex.value = newIndex;
currentLrcProgress.value = 0; // 重置进度
// 当索引变化时发送更新
if (isElectron.value) {
sendLyricToWin();
}
}
});
// 处理歌曲结束
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;
try {
if (lrcArray.value.length > 0) {
const nowIndex = getLrcIndex(nowTime.value);
const { currentLrc, nextLrc } = getCurrentLrc();
const { currentTime, nextTime } = getLrcTimeRange(nowIndex);
// 设置lyricWinData 获取 当前播放的两句歌词 和歌词时间
const lyricWinData = {
currentLrc,
nextLrc,
currentTime,
nextTime,
const updateData = {
type: 'update',
nowIndex,
lrcTimeArray: lrcTimeArray.value,
lrcArray: lrcArray.value,
nowTime: nowTime.value,
allTime: allTime.value,
startCurrentTime: lrcTimeArray.value[nowIndex],
nextTime: lrcTimeArray.value[nowIndex + 1],
isPlay,
};
windowData.electronAPI.sendLyric(JSON.stringify(lyricWinData));
windowData.electronAPI.sendLyric(JSON.stringify(updateData));
}
} catch (error) {
console.error('Error sending lyric to window:', error);
console.error('Error sending lyric update:', error);
}
};
export const openLyric = () => {
if (!isElectron.value) return;
console.log('Opening lyric window');
windowData.electronAPI.openLyric();
sendLyricToWin();
// 延迟一下初始化,确保窗口已经创建
setTimeout(() => {
console.log('Initializing lyric window after delay');
initLyricWindow();
sendLyricToWin();
}, 500);
};

View File

@@ -252,6 +252,7 @@ defineExpose({
background-color: inherit;
width: 500px;
height: 550px;
mask-image: linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%);
&-text {
@apply text-2xl cursor-pointer font-bold px-2 py-4;
transition: all 0.3s ease;

View File

@@ -1,76 +1,107 @@
<template>
<div class="lyric-window" :class="[lyricSetting.theme, { lyric_lock: lyricSetting.isLock }]">
<div class="drag-bar"></div>
<div class="lyric-bar" :class="{ 'lyric-bar-hover': isDrag }">
<div class="buttons">
<!-- <div class="music-buttons">
<div @click="handlePrev">
<i class="iconfont icon-prev"></i>
</div>
<div class="music-buttons-play" @click="playMusicEvent">
<i class="iconfont icon" :class="play ? 'icon-stop' : 'icon-play'"></i>
</div>
<div @click="handleEnded">
<i class="iconfont icon-next"></i>
</div>
</div> -->
<div class="button check-theme" @click="checkTheme">
<i v-if="lyricSetting.theme === 'light'" class="icon ri-sun-line"></i>
<i v-else class="icon ri-moon-line"></i>
<div
class="lyric-window"
:class="[lyricSetting.theme, { lyric_lock: lyricSetting.isLock }]"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
<!-- 顶部控制栏 -->
<div class="control-bar" :class="{ 'control-bar-show': showControls }">
<div class="font-size-controls">
<n-button-group>
<n-button quaternary size="small" :disabled="fontSize <= 12" @click="decreaseFontSize">
<i class="ri-subtract-line"></i>
</n-button>
<n-button quaternary size="small" :disabled="fontSize >= 48" @click="increaseFontSize">
<i class="ri-add-line"></i>
</n-button>
</n-button-group>
</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="button">
<i class="icon ri-share-2-line" :class="{ checked: lyricSetting.isTop }" @click="handleTop"></i>
<div class="control-button" @click="handleTop">
<i class="ri-pushpin-line" :class="{ active: lyricSetting.isTop }"></i>
</div>
<div class="button button-lock" @click="handleLock">
<i v-if="lyricSetting.isLock" class="icon ri-lock-line"></i>
<i v-else class="icon ri-lock-unlock-line"></i>
<div class="control-button" @click="handleLock">
<i v-if="lyricSetting.isLock" class="ri-lock-line"></i>
<i v-else class="ri-lock-unlock-line"></i>
</div>
<div class="button">
<i class="icon ri-close-circle-line" @click="handleClose"></i>
<div class="control-button" @click="handleClose">
<i class="ri-close-line"></i>
</div>
</div>
</div>
<div id="clickThroughElement" class="lyric-box">
<template v-if="lyricData.lrcArray[lyricData.nowIndex]">
<h2 class="lyric lyric-current">{{ lyricData.lrcArray[lyricData.nowIndex].text }}</h2>
<p class="lyric-current">{{ lyricData.currentLrc.trText }}</p>
<template v-if="lyricData.lrcArray[lyricData.nowIndex + 1]">
<h2 class="lyric lyric-next">
{{ lyricData.lrcArray[lyricData.nowIndex + 1].text }}
</h2>
<p class="lyric-next">{{ lyricData.nextLrc.trText }}</p>
</template>
</template>
<!-- 歌词显示区域 -->
<div ref="containerRef" class="lyric-container">
<div class="lyric-scroll">
<div class="lyric-wrapper" :style="wrapperStyle">
<template v-if="staticData.lrcArray?.length > 0">
<div
v-for="(line, index) in staticData.lrcArray"
:key="index"
class="lyric-line"
:style="lyricLineStyle"
:class="{
'lyric-line-current': index === currentIndex,
'lyric-line-passed': index < currentIndex,
'lyric-line-next': index === currentIndex + 1,
}"
>
<div class="lyric-text" :style="{ fontSize: `${fontSize}px` }">
<span class="lyric-text-inner" :style="getLyricStyle(index)">
{{ line.text || '' }}
</span>
</div>
<div v-if="line.trText" class="lyric-translation" :style="{ fontSize: `${fontSize * 0.6}px` }">
{{ line.trText }}
</div>
</div>
</template>
<div v-else class="lyric-empty">暂无歌词</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useIpcRenderer } from '@vueuse/electron';
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
defineOptions({
name: 'Lyric',
});
const ipcRenderer = useIpcRenderer();
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; // 每次整的步长
const lyricData = ref({
currentLrc: {
text: '',
trText: '',
},
nextLrc: {
text: '',
trText: '',
},
currentTime: 0,
nextTime: 0,
nowTime: 0,
// 静态数据
const staticData = ref<{
lrcArray: Array<{ text: string; trText: string }>;
lrcTimeArray: number[];
allTime: number;
}>({
lrcArray: [],
lrcTimeArray: [],
allTime: 0,
});
// 动态数据
const dynamicData = ref({
nowTime: 0,
startCurrentTime: 0,
lrcArray: [] as any,
lrcTimeArray: [] as any,
nowIndex: 0,
nextTime: 0,
isPlay: true,
});
const lyricSetting = ref({
@@ -83,16 +114,323 @@ const lyricSetting = ref({
}),
});
let hideControlsTimer: number | null = null;
const isHovering = ref(false);
// 计算是否栏
const showControls = computed(() => {
if (lyricSetting.value.isLock) {
return isHovering.value;
}
return true;
});
// 清除隐藏定时器
const clearHideTimer = () => {
if (hideControlsTimer) {
clearTimeout(hideControlsTimer);
hideControlsTimer = null;
}
};
// 处理鼠标进入窗口
const handleMouseEnter = () => {
if (!lyricSetting.value.isLock) return;
isHovering.value = true;
};
// 处理鼠标离开窗口
const handleMouseLeave = () => {
if (!lyricSetting.value.isLock) return;
isHovering.value = false;
};
// 监听锁定状态变化
watch(
() => lyricSetting.value.isLock,
(newLock: boolean) => {
if (newLock) {
isHovering.value = false;
}
},
);
onMounted(() => {
ipcRenderer.on('receive-lyric', (event, data) => {
// 初始化时,如果是锁定状态,确保控制栏隐藏
if (lyricSetting.value.isLock) {
isHovering.value = false;
}
});
onUnmounted(() => {
clearHideTimer();
});
// 计算歌词滚动位置
const wrapperStyle = computed(() => {
if (!isInitialized.value || !containerHeight.value) {
return {
transform: 'translateY(0)',
transition: 'none',
};
}
// 计算容器中心点
const containerCenter = containerHeight.value / 2;
// 计算当前行到顶部的距离包含padding
const currentLineTop = currentIndex.value * lineHeight.value + containerHeight.value * 0.2; // 加上顶部padding
// 计算偏移量,使当前行居中
const targetOffset = containerCenter - currentLineTop;
// 计算内容总高度包含padding
const contentHeight = staticData.value.lrcArray.length * lineHeight.value + containerHeight.value * 0.4; // 上下padding各20vh
// 计算最小和最大偏移量
const minOffset = -(contentHeight - containerHeight.value);
const maxOffset = 0;
// 限制偏移量在合理范围内
const finalOffset = Math.min(maxOffset, Math.max(minOffset, targetOffset));
return {
transform: `translateY(${finalOffset}px)`,
transition: isInitialized.value ? 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)' : 'none',
};
});
const lyricLineStyle = computed(() => ({
height: `${lineHeight.value}px`,
}));
// 更新容器高度和行高
const updateContainerHeight = () => {
if (!containerRef.value) return;
// 更新容器高度
containerHeight.value = containerRef.value.clientHeight;
// 计算基础行高(字体大小的2.5倍)
const baseLineHeight = fontSize.value * 2.5;
// 计算最大允许行高(容器高度的1/4)
const maxAllowedHeight = containerHeight.value / 3;
// 设置行高(不小于40px,不大于最大允许高度)
lineHeight.value = Math.min(maxAllowedHeight, Math.max(40, baseLineHeight));
};
// 处理字体大小变化
const handleFontSizeChange = async () => {
// 先保存字体大小
saveFontSize();
// 更新容器高度和行高
updateContainerHeight();
};
// 增加字体大小
const increaseFontSize = async () => {
if (fontSize.value < 48) {
fontSize.value += fontSizeStep;
await handleFontSizeChange();
}
};
// 减小字体大小
const decreaseFontSize = async () => {
if (fontSize.value > 12) {
fontSize.value -= fontSizeStep;
await handleFontSizeChange();
}
};
// 保存字体大小到本地存储
const saveFontSize = () => {
localStorage.setItem('lyricFontSize', fontSize.value.toString());
};
// 监听容器大小变化
onMounted(() => {
const resizeObserver = new ResizeObserver(() => {
updateContainerHeight();
});
if (containerRef.value) {
resizeObserver.observe(containerRef.value);
}
onUnmounted(() => {
resizeObserver.disconnect();
});
});
// 动画帧ID
const animationFrameId = ref<number | null>(null);
// 实际播放时间
const actualTime = ref(0);
// 计算当前行的进度
const currentProgress = computed(() => {
const { startCurrentTime, nextTime, isPlay } = dynamicData.value;
if (!startCurrentTime || !nextTime || !isPlay) return 0;
const duration = nextTime - startCurrentTime;
const elapsed = actualTime.value - startCurrentTime;
return Math.min(Math.max(elapsed / duration, 0), 1);
});
// 获取歌词样式
const getLyricStyle = (index: number) => {
if (index !== currentIndex.value) return {};
const progress = currentProgress.value * 100;
return {
background: `linear-gradient(to right, var(--highlight-color) ${progress}%, var(--text-color) ${progress}%)`,
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
transition: 'all 0.1s linear',
};
};
// 时间偏移量(毫秒)
const TIME_OFFSET = 400;
// 更新动画
const updateProgress = () => {
if (!dynamicData.value.isPlay) {
if (animationFrameId.value) {
cancelAnimationFrame(animationFrameId.value);
animationFrameId.value = null;
}
return;
}
// 计算实际时间,添加偏移量
const timeDiff = (performance.now() - lastUpdateTime.value) / 1000;
actualTime.value = dynamicData.value.nowTime + timeDiff + TIME_OFFSET / 1000;
// 继续动画
animationFrameId.value = requestAnimationFrame(updateProgress);
};
// 记录上次更新时间
const lastUpdateTime = ref(performance.now());
// 监听数据更新
watch(
() => dynamicData.value,
(newData: any) => {
// 更新最后更新时间
lastUpdateTime.value = performance.now();
// 更新实际时间,包含偏移量
actualTime.value = newData.nowTime + TIME_OFFSET / 1000;
// 如果正在播放且没有动画,启动动画
if (newData.isPlay && !animationFrameId.value) {
updateProgress();
}
},
{ deep: true },
);
// 监听播放状态变化
watch(
() => dynamicData.value.isPlay,
(isPlaying: boolean) => {
if (isPlaying) {
lastUpdateTime.value = performance.now();
updateProgress();
} else if (animationFrameId.value) {
cancelAnimationFrame(animationFrameId.value);
animationFrameId.value = null;
}
},
);
// 修改数据更新处理
const handleDataUpdate = (parsedData: {
nowTime: number;
startCurrentTime: number;
nextTime: number;
isPlay: boolean;
nowIndex: number;
}) => {
// 确保数据存在且格式正确
if (!parsedData || typeof parsedData.nowTime !== 'number') {
console.error('Invalid update data received:', parsedData);
return;
}
dynamicData.value = {
nowTime: parsedData.nowTime,
startCurrentTime: parsedData.startCurrentTime,
nextTime: parsedData.nextTime,
isPlay: parsedData.isPlay,
};
// 更新索引
if (typeof parsedData.nowIndex === 'number' && parsedData.nowIndex !== currentIndex.value) {
currentIndex.value = parsedData.nowIndex;
}
};
onMounted(() => {
// 加载保存的字体大小
const savedFontSize = localStorage.getItem('lyricFontSize');
if (savedFontSize) {
fontSize.value = Number(savedFontSize);
lineHeight.value = fontSize.value * 2.5;
}
// 初始化容器高度
updateContainerHeight();
window.addEventListener('resize', updateContainerHeight);
// 监听歌词数据
windowData.electron.ipcRenderer.on('receive-lyric', (data: string) => {
try {
lyricData.value = JSON.parse(data);
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);
}
} catch (error) {
console.error('error', error);
console.error('Error parsing lyric data:', error);
}
});
});
onUnmounted(() => {
window.removeEventListener('resize', updateContainerHeight);
});
const checkTheme = () => {
if (lyricSetting.value.theme === 'light') {
lyricSetting.value.theme = 'dark';
@@ -103,7 +441,7 @@ const checkTheme = () => {
const handleTop = () => {
lyricSetting.value.isTop = !lyricSetting.value.isTop;
ipcRenderer.send('top-lyric', lyricSetting.value.isTop);
windowData.electron.ipcRenderer.send('top-lyric', lyricSetting.value.isTop);
};
const handleLock = () => {
@@ -111,26 +449,16 @@ const handleLock = () => {
};
const handleClose = () => {
ipcRenderer.send('close-lyric');
windowData.electron.ipcRenderer.send('close-lyric');
};
watch(
() => lyricSetting.value,
(newValue) => {
(newValue: any) => {
localStorage.setItem('lyricData', JSON.stringify(newValue));
},
{ deep: true },
);
// onMounted(() => {
// const el = document.getElementById('clickThroughElement') as HTMLElement;
// el.addEventListener('mouseenter', () => {
// if (lyricSetting.value.isLock) ipcRenderer.send('mouseenter-lyric');
// });
// el.addEventListener('mouseleave', () => {
// if (lyricSetting.value.isLock) ipcRenderer.send('mouseleave-lyric');
// });
// });
</script>
<style>
@@ -143,132 +471,196 @@ body {
.lyric-window {
width: 100vw;
height: 100vh;
@apply overflow-hidden text-gray-600 rounded-xl box-border;
// border: 4px solid transparent;
&:hover .lyric-bar {
opacity: 1;
}
&:hover .drag-bar {
opacity: 1;
}
&:hover {
box-shadow: inset 0 0 10px 0 rgba(255, 255, 255, 0.5);
}
}
position: relative;
overflow: hidden;
background: transparent;
.lyric_lock {
&:hover {
box-shadow: none;
&.dark {
--bg-color: transparent;
--text-color: #ffffff;
--text-secondary: rgba(255, 255, 255, 0.6);
--highlight-color: #1db954;
--control-bg: rgba(0, 0, 0, 0.3);
}
&:hover .lyric-bar {
background-color: transparent;
.button {
opacity: 0;
}
.button-lock {
opacity: 1;
color: #d6d6d6;
&.light {
--bg-color: transparent;
--text-color: #333333;
--text-secondary: rgba(51, 51, 51, 0.6);
--highlight-color: #1db954;
--control-bg: rgba(255, 255, 255, 0.3);
}
&.lyric_lock {
.control-bar {
background: var(--control-bg);
&-show {
opacity: 1;
}
}
}
&:hover .drag-bar {
opacity: 0;
}
}
.icon {
@apply text-xl hover:text-white;
}
.lyric-bar {
background-color: #b1b1b1;
@apply flex flex-col justify-center items-center;
width: 100vw;
.control-bar {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 40px;
background: var(--control-bg);
backdrop-filter: blur(8px);
display: flex;
justify-content: flex-end;
align-items: center;
padding: 0 20px;
opacity: 0;
visibility: hidden;
transition:
opacity 0.2s ease,
visibility 0.2s ease;
-webkit-app-region: drag;
z-index: 100;
&-show {
opacity: 1;
visibility: visible;
}
.font-size-controls {
margin-right: auto; // 将字体控制放在侧
padding-right: 20px;
-webkit-app-region: no-drag;
.n-button {
i {
font-size: 16px;
}
}
}
.control-buttons {
-webkit-app-region: no-drag;
}
}
.control-buttons {
display: flex;
gap: 16px;
-webkit-app-region: no-drag;
}
.control-button {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 50%;
color: var(--text-color);
transition: all 0.2s ease;
backdrop-filter: blur(4px);
&:hover {
background: var(--control-bg);
}
i {
font-size: 18px;
text-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
&.active {
color: var(--highlight-color);
}
}
}
.lyric-container {
position: absolute;
top: 40px;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
}
.lyric-scroll {
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
mask-image: linear-gradient(to bottom, transparent 0%, black 20%, black 80%, transparent 100%);
}
.lyric-wrapper {
will-change: transform;
padding: 20vh 0;
transform-origin: center center;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.lyric-line {
padding: 4px 20px;
text-align: center;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
&.lyric-line-current {
transform: scale(1.05);
opacity: 1;
}
&.lyric-line-passed,
&.lyric-line-next {
opacity: 0.6;
}
}
.lyric-bar-hover {
.lyric-text {
font-weight: 600;
margin-bottom: 2px;
color: var(--text-color);
white-space: pre-wrap;
word-break: break-all;
text-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
transition: all 0.2s ease;
line-height: 1.4;
}
.lyric-translation {
color: var(--text-secondary);
white-space: pre-wrap;
word-break: break-all;
text-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
transition: font-size 0.2s ease;
line-height: 1.4; // 添加行高比例
}
.lyric-empty {
text-align: center;
color: var(--text-secondary);
font-size: 16px;
padding: 20px;
}
body {
background-color: transparent !important;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
'Helvetica Neue', sans-serif;
}
.lyric-content {
transition: font-size 0.2s ease;
}
.lyric-line-current {
opacity: 1;
}
.drag-bar {
-webkit-app-region: drag;
height: 20px;
cursor: move;
background-color: #383838;
opacity: 0;
}
.buttons {
width: 100vw;
height: 100px;
@apply flex justify-center items-center gap-4;
}
.button {
@apply cursor-pointer text-center;
}
.checked {
color: #fff !important;
}
.button-move {
-webkit-app-region: drag;
cursor: move;
}
.music-buttons {
@apply mx-6;
-webkit-app-region: no-drag;
.iconfont {
@apply text-2xl hover:text-green-500 transition;
}
@apply flex items-center;
> div {
@apply cursor-pointer;
}
&-play {
@apply flex justify-center items-center w-12 h-12 rounded-full mx-4 hover:bg-green-500 transition;
background: #383838;
}
}
.check-theme {
font-size: 26px;
cursor: pointer;
opacity: 1;
}
.lyric {
text-shadow: 0 0 1vw #2c2c2c;
font-size: 4vw;
@apply font-bold m-0 p-0 select-none pointer-events-none;
}
.lyric-current {
color: #333;
}
.lyric-next {
color: #999;
margin: 10px;
}
.lyric-window.dark {
.lyric {
text-shadow: none;
text-shadow: 0 0 1vw #000000;
}
.lyric-current {
color: #fff;
}
.lyric-next {
color: #cecece;
}
}
.lyric-box {
// writing-mode: vertical-rl;
padding: 10px;
}
</style>