mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-05-16 17:47:28 +08:00
✨ feat: 优化桌面歌词功能 添加歌词进度 优化歌词页面样式
This commit is contained in:
@@ -3,6 +3,7 @@ const path = require('path');
|
|||||||
const Store = require('electron-store');
|
const Store = require('electron-store');
|
||||||
const setJson = require('./electron/set.json');
|
const setJson = require('./electron/set.json');
|
||||||
const { loadLyricWindow } = require('./electron/lyric');
|
const { loadLyricWindow } = require('./electron/lyric');
|
||||||
|
const config = require('./electron/config');
|
||||||
|
|
||||||
let mainWin = null;
|
let mainWin = null;
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
@@ -11,8 +12,8 @@ function createWindow() {
|
|||||||
height: 780,
|
height: 780,
|
||||||
frame: false,
|
frame: false,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true,
|
nodeIntegration: false,
|
||||||
// contextIsolation: false,
|
contextIsolation: true,
|
||||||
preload: path.join(__dirname, '/electron/preload.js'),
|
preload: path.join(__dirname, '/electron/preload.js'),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -20,7 +21,7 @@ function createWindow() {
|
|||||||
win.setMinimumSize(1200, 780);
|
win.setMinimumSize(1200, 780);
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
win.webContents.openDevTools({ mode: 'detach' });
|
win.webContents.openDevTools({ mode: 'detach' });
|
||||||
win.loadURL('http://localhost:4488/');
|
win.loadURL(`http://localhost:${config.development.mainPort}/`);
|
||||||
} else {
|
} else {
|
||||||
win.loadURL(`file://${__dirname}/dist/index.html`);
|
win.loadURL(`file://${__dirname}/dist/index.html`);
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
@@ -13,6 +13,7 @@ declare module 'vue' {
|
|||||||
MvPlayer: typeof import('./src/components/MvPlayer.vue')['default']
|
MvPlayer: typeof import('./src/components/MvPlayer.vue')['default']
|
||||||
NAvatar: typeof import('naive-ui')['NAvatar']
|
NAvatar: typeof import('naive-ui')['NAvatar']
|
||||||
NButton: typeof import('naive-ui')['NButton']
|
NButton: typeof import('naive-ui')['NButton']
|
||||||
|
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
|
||||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||||
NDrawer: typeof import('naive-ui')['NDrawer']
|
NDrawer: typeof import('naive-ui')['NDrawer']
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
module.exports = {
|
||||||
|
// 开发环境配置
|
||||||
|
development: {
|
||||||
|
mainPort: 4488,
|
||||||
|
lyricPort: 4488,
|
||||||
|
},
|
||||||
|
// 生产环境配置
|
||||||
|
production: {
|
||||||
|
distPath: '../dist',
|
||||||
|
},
|
||||||
|
};
|
||||||
+8
-4
@@ -1,5 +1,6 @@
|
|||||||
const { BrowserWindow } = require('electron');
|
const { BrowserWindow } = require('electron');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const config = require('./config');
|
||||||
|
|
||||||
let lyricWindow = null;
|
let lyricWindow = null;
|
||||||
|
|
||||||
@@ -10,13 +11,16 @@ const createWin = () => {
|
|||||||
frame: false,
|
frame: false,
|
||||||
show: false,
|
show: false,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
|
hasShadow: false,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true,
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
preload: `${__dirname}/preload.js`,
|
preload: `${__dirname}/preload.js`,
|
||||||
contextIsolation: false,
|
webSecurity: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadLyricWindow = (ipcMain) => {
|
const loadLyricWindow = (ipcMain) => {
|
||||||
ipcMain.on('open-lyric', () => {
|
ipcMain.on('open-lyric', () => {
|
||||||
if (lyricWindow) {
|
if (lyricWindow) {
|
||||||
@@ -28,9 +32,9 @@ const loadLyricWindow = (ipcMain) => {
|
|||||||
createWin();
|
createWin();
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
lyricWindow.webContents.openDevTools({ mode: 'detach' });
|
lyricWindow.webContents.openDevTools({ mode: 'detach' });
|
||||||
lyricWindow.loadURL('http://localhost:4678/#/lyric');
|
lyricWindow.loadURL(`http://localhost:${config.development.lyricPort}/#/lyric`);
|
||||||
} else {
|
} else {
|
||||||
const distPath = path.resolve(__dirname, '../dist');
|
const distPath = path.resolve(__dirname, config.production.distPath);
|
||||||
lyricWindow.loadURL(`file://${distPath}/index.html#/lyric`);
|
lyricWindow.loadURL(`file://${distPath}/index.html#/lyric`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+14
-12
@@ -1,5 +1,6 @@
|
|||||||
const { contextBridge, ipcRenderer, app } = require('electron');
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
|
||||||
|
// 主进程通信
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
minimize: () => ipcRenderer.send('minimize-window'),
|
minimize: () => ipcRenderer.send('minimize-window'),
|
||||||
maximize: () => ipcRenderer.send('maximize-window'),
|
maximize: () => ipcRenderer.send('maximize-window'),
|
||||||
@@ -11,18 +12,19 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
sendLyric: (data) => ipcRenderer.send('send-lyric', data),
|
sendLyric: (data) => ipcRenderer.send('send-lyric', data),
|
||||||
});
|
});
|
||||||
|
|
||||||
const electronHandler = {
|
// 存储相关
|
||||||
|
contextBridge.exposeInMainWorld('electron', {
|
||||||
ipcRenderer: {
|
ipcRenderer: {
|
||||||
setStoreValue: (key, value) => {
|
setStoreValue: (key, value) => ipcRenderer.send('setStore', key, value),
|
||||||
ipcRenderer.send('setStore', key, value);
|
getStoreValue: (key) => ipcRenderer.sendSync('getStore', key),
|
||||||
|
on: (channel, func) => {
|
||||||
|
ipcRenderer.on(channel, (event, ...args) => func(...args));
|
||||||
},
|
},
|
||||||
|
once: (channel, func) => {
|
||||||
getStoreValue(key) {
|
ipcRenderer.once(channel, (event, ...args) => func(...args));
|
||||||
const resp = ipcRenderer.sendSync('getStore', key);
|
},
|
||||||
return resp;
|
send: (channel, data) => {
|
||||||
|
ipcRenderer.send(channel, data);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
app,
|
});
|
||||||
};
|
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electron', electronHandler);
|
|
||||||
|
|||||||
@@ -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;
|
||||||
+1
-1
@@ -32,7 +32,7 @@
|
|||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"electron": "^32.0.1",
|
"electron": "^32.2.7",
|
||||||
"electron-builder": "^25.0.5",
|
"electron-builder": "^25.0.5",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-airbnb-base": "^15.0.0",
|
"eslint-config-airbnb-base": "^15.0.0",
|
||||||
|
|||||||
+87
-14
@@ -160,37 +160,110 @@ export const getLrcTimeRange = (index: number) => ({
|
|||||||
nextTime: lrcTimeArray.value[index + 1],
|
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) => {
|
export const sendLyricToWin = (isPlay: boolean = true) => {
|
||||||
if (!isElectron.value) return;
|
if (!isElectron.value) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (lrcArray.value.length > 0) {
|
if (lrcArray.value.length > 0) {
|
||||||
const nowIndex = getLrcIndex(nowTime.value);
|
const nowIndex = getLrcIndex(nowTime.value);
|
||||||
const { currentLrc, nextLrc } = getCurrentLrc();
|
const updateData = {
|
||||||
const { currentTime, nextTime } = getLrcTimeRange(nowIndex);
|
type: 'update',
|
||||||
// 设置lyricWinData 获取 当前播放的两句歌词 和歌词时间
|
|
||||||
const lyricWinData = {
|
|
||||||
currentLrc,
|
|
||||||
nextLrc,
|
|
||||||
currentTime,
|
|
||||||
nextTime,
|
|
||||||
nowIndex,
|
nowIndex,
|
||||||
lrcTimeArray: lrcTimeArray.value,
|
|
||||||
lrcArray: lrcArray.value,
|
|
||||||
nowTime: nowTime.value,
|
nowTime: nowTime.value,
|
||||||
allTime: allTime.value,
|
|
||||||
startCurrentTime: lrcTimeArray.value[nowIndex],
|
startCurrentTime: lrcTimeArray.value[nowIndex],
|
||||||
|
nextTime: lrcTimeArray.value[nowIndex + 1],
|
||||||
isPlay,
|
isPlay,
|
||||||
};
|
};
|
||||||
windowData.electronAPI.sendLyric(JSON.stringify(lyricWinData));
|
windowData.electronAPI.sendLyric(JSON.stringify(updateData));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error sending lyric to window:', error);
|
console.error('Error sending lyric update:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const openLyric = () => {
|
export const openLyric = () => {
|
||||||
if (!isElectron.value) return;
|
if (!isElectron.value) return;
|
||||||
|
console.log('Opening lyric window');
|
||||||
windowData.electronAPI.openLyric();
|
windowData.electronAPI.openLyric();
|
||||||
sendLyricToWin();
|
|
||||||
|
// 延迟一下初始化,确保窗口已经创建
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('Initializing lyric window after delay');
|
||||||
|
initLyricWindow();
|
||||||
|
sendLyricToWin();
|
||||||
|
}, 500);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ defineExpose({
|
|||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
width: 500px;
|
width: 500px;
|
||||||
height: 550px;
|
height: 550px;
|
||||||
|
mask-image: linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%);
|
||||||
&-text {
|
&-text {
|
||||||
@apply text-2xl cursor-pointer font-bold px-2 py-4;
|
@apply text-2xl cursor-pointer font-bold px-2 py-4;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
|||||||
+575
-183
@@ -1,76 +1,107 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="lyric-window" :class="[lyricSetting.theme, { lyric_lock: lyricSetting.isLock }]">
|
<div
|
||||||
<div class="drag-bar"></div>
|
class="lyric-window"
|
||||||
<div class="lyric-bar" :class="{ 'lyric-bar-hover': isDrag }">
|
:class="[lyricSetting.theme, { lyric_lock: lyricSetting.isLock }]"
|
||||||
<div class="buttons">
|
@mouseenter="handleMouseEnter"
|
||||||
<!-- <div class="music-buttons">
|
@mouseleave="handleMouseLeave"
|
||||||
<div @click="handlePrev">
|
>
|
||||||
<i class="iconfont icon-prev"></i>
|
<!-- 顶部控制栏 -->
|
||||||
</div>
|
<div class="control-bar" :class="{ 'control-bar-show': showControls }">
|
||||||
<div class="music-buttons-play" @click="playMusicEvent">
|
<div class="font-size-controls">
|
||||||
<i class="iconfont icon" :class="play ? 'icon-stop' : 'icon-play'"></i>
|
<n-button-group>
|
||||||
</div>
|
<n-button quaternary size="small" :disabled="fontSize <= 12" @click="decreaseFontSize">
|
||||||
<div @click="handleEnded">
|
<i class="ri-subtract-line"></i>
|
||||||
<i class="iconfont icon-next"></i>
|
</n-button>
|
||||||
</div>
|
<n-button quaternary size="small" :disabled="fontSize >= 48" @click="increaseFontSize">
|
||||||
</div> -->
|
<i class="ri-add-line"></i>
|
||||||
<div class="button check-theme" @click="checkTheme">
|
</n-button>
|
||||||
<i v-if="lyricSetting.theme === 'light'" class="icon ri-sun-line"></i>
|
</n-button-group>
|
||||||
<i v-else class="icon ri-moon-line"></i>
|
</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>
|
||||||
<div class="button">
|
<div class="control-button" @click="handleTop">
|
||||||
<i class="icon ri-share-2-line" :class="{ checked: lyricSetting.isTop }" @click="handleTop"></i>
|
<i class="ri-pushpin-line" :class="{ active: lyricSetting.isTop }"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="button button-lock" @click="handleLock">
|
<div class="control-button" @click="handleLock">
|
||||||
<i v-if="lyricSetting.isLock" class="icon ri-lock-line"></i>
|
<i v-if="lyricSetting.isLock" class="ri-lock-line"></i>
|
||||||
<i v-else class="icon ri-lock-unlock-line"></i>
|
<i v-else class="ri-lock-unlock-line"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="button">
|
<div class="control-button" @click="handleClose">
|
||||||
<i class="icon ri-close-circle-line" @click="handleClose"></i>
|
<i class="ri-close-line"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
<div ref="containerRef" class="lyric-container">
|
||||||
<p class="lyric-current">{{ lyricData.currentLrc.trText }}</p>
|
<div class="lyric-scroll">
|
||||||
<template v-if="lyricData.lrcArray[lyricData.nowIndex + 1]">
|
<div class="lyric-wrapper" :style="wrapperStyle">
|
||||||
<h2 class="lyric lyric-next">
|
<template v-if="staticData.lrcArray?.length > 0">
|
||||||
{{ lyricData.lrcArray[lyricData.nowIndex + 1].text }}
|
<div
|
||||||
</h2>
|
v-for="(line, index) in staticData.lrcArray"
|
||||||
<p class="lyric-next">{{ lyricData.nextLrc.trText }}</p>
|
:key="index"
|
||||||
</template>
|
class="lyric-line"
|
||||||
</template>
|
: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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useIpcRenderer } from '@vueuse/electron';
|
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'Lyric',
|
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: {
|
const staticData = ref<{
|
||||||
text: '',
|
lrcArray: Array<{ text: string; trText: string }>;
|
||||||
trText: '',
|
lrcTimeArray: number[];
|
||||||
},
|
allTime: number;
|
||||||
nextLrc: {
|
}>({
|
||||||
text: '',
|
lrcArray: [],
|
||||||
trText: '',
|
lrcTimeArray: [],
|
||||||
},
|
|
||||||
currentTime: 0,
|
|
||||||
nextTime: 0,
|
|
||||||
nowTime: 0,
|
|
||||||
allTime: 0,
|
allTime: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 动态数据
|
||||||
|
const dynamicData = ref({
|
||||||
|
nowTime: 0,
|
||||||
startCurrentTime: 0,
|
startCurrentTime: 0,
|
||||||
lrcArray: [] as any,
|
nextTime: 0,
|
||||||
lrcTimeArray: [] as any,
|
isPlay: true,
|
||||||
nowIndex: 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const lyricSetting = ref({
|
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(() => {
|
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 {
|
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) {
|
} catch (error) {
|
||||||
console.error('error', error);
|
console.error('Error parsing lyric data:', error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', updateContainerHeight);
|
||||||
|
});
|
||||||
|
|
||||||
const checkTheme = () => {
|
const checkTheme = () => {
|
||||||
if (lyricSetting.value.theme === 'light') {
|
if (lyricSetting.value.theme === 'light') {
|
||||||
lyricSetting.value.theme = 'dark';
|
lyricSetting.value.theme = 'dark';
|
||||||
@@ -103,7 +441,7 @@ const checkTheme = () => {
|
|||||||
|
|
||||||
const handleTop = () => {
|
const handleTop = () => {
|
||||||
lyricSetting.value.isTop = !lyricSetting.value.isTop;
|
lyricSetting.value.isTop = !lyricSetting.value.isTop;
|
||||||
ipcRenderer.send('top-lyric', lyricSetting.value.isTop);
|
windowData.electron.ipcRenderer.send('top-lyric', lyricSetting.value.isTop);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLock = () => {
|
const handleLock = () => {
|
||||||
@@ -111,26 +449,16 @@ const handleLock = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
ipcRenderer.send('close-lyric');
|
windowData.electron.ipcRenderer.send('close-lyric');
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => lyricSetting.value,
|
() => lyricSetting.value,
|
||||||
(newValue) => {
|
(newValue: any) => {
|
||||||
localStorage.setItem('lyricData', JSON.stringify(newValue));
|
localStorage.setItem('lyricData', JSON.stringify(newValue));
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ 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>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -143,132 +471,196 @@ body {
|
|||||||
.lyric-window {
|
.lyric-window {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
@apply overflow-hidden text-gray-600 rounded-xl box-border;
|
position: relative;
|
||||||
// border: 4px solid transparent;
|
overflow: hidden;
|
||||||
&:hover .lyric-bar {
|
background: transparent;
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
&:hover .drag-bar {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
box-shadow: inset 0 0 10px 0 rgba(255, 255, 255, 0.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyric_lock {
|
&.dark {
|
||||||
&:hover {
|
--bg-color: transparent;
|
||||||
box-shadow: none;
|
--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;
|
&.light {
|
||||||
.button {
|
--bg-color: transparent;
|
||||||
opacity: 0;
|
--text-color: #333333;
|
||||||
}
|
--text-secondary: rgba(51, 51, 51, 0.6);
|
||||||
.button-lock {
|
--highlight-color: #1db954;
|
||||||
opacity: 1;
|
--control-bg: rgba(255, 255, 255, 0.3);
|
||||||
color: #d6d6d6;
|
}
|
||||||
|
|
||||||
|
&.lyric_lock {
|
||||||
|
.control-bar {
|
||||||
|
background: var(--control-bg);
|
||||||
|
|
||||||
|
&-show {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:hover .drag-bar {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.control-bar {
|
||||||
@apply text-xl hover:text-white;
|
position: absolute;
|
||||||
}
|
top: 0;
|
||||||
|
left: 0;
|
||||||
.lyric-bar {
|
right: 0;
|
||||||
background-color: #b1b1b1;
|
|
||||||
@apply flex flex-col justify-center items-center;
|
|
||||||
width: 100vw;
|
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
background: var(--control-bg);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 20px;
|
||||||
opacity: 0;
|
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 {
|
&: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;
|
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;
|
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>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user