mirror of
https://github.com/timeshiftsauce/CeruMusic.git
synced 2025-11-25 03:15:07 +08:00
�feat: 添加桌面歌词功能,列表平台显示 fix:大幅优化打包体积
This commit is contained in:
@@ -6,8 +6,12 @@ asar: true
|
|||||||
files:
|
files:
|
||||||
- '!**/.vscode/*'
|
- '!**/.vscode/*'
|
||||||
- '!src/*'
|
- '!src/*'
|
||||||
|
- '!website/*'
|
||||||
|
- '!scripts/*'
|
||||||
|
- '!assets/*'
|
||||||
|
- '!docs/*'
|
||||||
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
||||||
- '!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
- '!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md,.idea,.kiro,.codebuddy}'
|
||||||
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||||
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
||||||
asarUnpack:
|
asarUnpack:
|
||||||
@@ -81,4 +85,4 @@ publish:
|
|||||||
provider: generic
|
provider: generic
|
||||||
url: https://update.ceru.shiqianjiang.cn
|
url: https://update.ceru.shiqianjiang.cn
|
||||||
electronDownload:
|
electronDownload:
|
||||||
mirror: https://npmmirror.com/mirrors/electron/
|
mirror: https://npmmirror.com/mirrors/electron/
|
||||||
@@ -8,7 +8,6 @@ import { TDesignResolver } from '@tdesign-vue-next/auto-import-resolver'
|
|||||||
import wasm from 'vite-plugin-wasm'
|
import wasm from 'vite-plugin-wasm'
|
||||||
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
|
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
|
||||||
import topLevelAwait from 'vite-plugin-top-level-await'
|
import topLevelAwait from 'vite-plugin-top-level-await'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
main: {
|
main: {
|
||||||
plugins: [externalizeDepsPlugin()],
|
plugins: [externalizeDepsPlugin()],
|
||||||
@@ -16,6 +15,14 @@ export default defineConfig({
|
|||||||
alias: {
|
alias: {
|
||||||
'@common': resolve('src/common')
|
'@common': resolve('src/common')
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
index: resolve(__dirname, 'src/main/index.ts'),
|
||||||
|
lyric: resolve(__dirname, 'src/web/lyric.html')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
preload: {
|
preload: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ceru-music",
|
"name": "ceru-music",
|
||||||
"version": "1.4.6",
|
"version": "1.4.7",
|
||||||
"description": "一款简洁优雅的音乐播放器",
|
"description": "一款简洁优雅的音乐播放器",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "sqj,wldss,star",
|
"author": "sqj,wldss,star",
|
||||||
|
|||||||
10
src/common/types/config.ts
Normal file
10
src/common/types/config.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export interface lyricConfig {
|
||||||
|
fontSize: number
|
||||||
|
mainColor: string
|
||||||
|
shadowColor: string
|
||||||
|
// 窗口位置
|
||||||
|
x?: number
|
||||||
|
y?: number
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import InitPluginService from './plugins'
|
import InitPluginService from './plugins'
|
||||||
import '../services/musicSdk/index'
|
import '../services/musicSdk/index'
|
||||||
import aiEvents from '../events/ai'
|
import aiEvents from '../events/ai'
|
||||||
import { app, powerSaveBlocker, Menu } from 'electron'
|
import { app, powerSaveBlocker } from 'electron'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import { type BrowserWindow, Tray, ipcMain } from 'electron'
|
import { type BrowserWindow, ipcMain } from 'electron'
|
||||||
export default function InitEventServices(mainWindow: BrowserWindow) {
|
export default function InitEventServices(mainWindow: BrowserWindow) {
|
||||||
InitPluginService()
|
InitPluginService()
|
||||||
aiEvents(mainWindow)
|
aiEvents(mainWindow)
|
||||||
@@ -12,58 +12,12 @@ export default function InitEventServices(mainWindow: BrowserWindow) {
|
|||||||
|
|
||||||
function basisEvent(mainWindow: BrowserWindow) {
|
function basisEvent(mainWindow: BrowserWindow) {
|
||||||
let psbId: number | null = null
|
let psbId: number | null = null
|
||||||
let tray: Tray | null = null
|
// 复用主进程创建的托盘
|
||||||
|
let tray: any = (global as any).__ceru_tray__ || null
|
||||||
let isQuitting = false
|
let isQuitting = false
|
||||||
function createTray(): void {
|
// 托盘菜单与图标由主进程统一创建,这里不再重复创建
|
||||||
// 创建系统托盘
|
// 播放/暂停由主进程托盘菜单触发 'music-control' 事件
|
||||||
const trayIconPath = path.join(__dirname, '../../resources/logo.png')
|
|
||||||
tray = new Tray(trayIconPath)
|
|
||||||
|
|
||||||
// 创建托盘菜单
|
|
||||||
const contextMenu = Menu.buildFromTemplate([
|
|
||||||
{
|
|
||||||
label: '显示窗口',
|
|
||||||
click: () => {
|
|
||||||
if (mainWindow) {
|
|
||||||
mainWindow.show()
|
|
||||||
mainWindow.focus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '播放/暂停',
|
|
||||||
click: () => {
|
|
||||||
// 这里可以添加播放控制逻辑
|
|
||||||
console.log('music-control')
|
|
||||||
mainWindow?.webContents.send('music-control')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
|
||||||
label: '退出',
|
|
||||||
click: () => {
|
|
||||||
isQuitting = true
|
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
tray.setContextMenu(contextMenu)
|
|
||||||
tray.setToolTip('Ceru Music')
|
|
||||||
|
|
||||||
// 单击托盘图标显示窗口
|
|
||||||
tray.on('click', () => {
|
|
||||||
if (mainWindow) {
|
|
||||||
if (mainWindow.isVisible()) {
|
|
||||||
mainWindow.hide()
|
|
||||||
} else {
|
|
||||||
mainWindow.show()
|
|
||||||
mainWindow.focus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
createTray()
|
|
||||||
// 应用退出前的清理
|
// 应用退出前的清理
|
||||||
app.on('before-quit', () => {
|
app.on('before-quit', () => {
|
||||||
isQuitting = true
|
isQuitting = true
|
||||||
@@ -93,7 +47,8 @@ function basisEvent(mainWindow: BrowserWindow) {
|
|||||||
// 进入 Mini 模式:隐藏窗口到系统托盘
|
// 进入 Mini 模式:隐藏窗口到系统托盘
|
||||||
mainWindow.hide()
|
mainWindow.hide()
|
||||||
// 显示托盘通知(可选)
|
// 显示托盘通知(可选)
|
||||||
if (tray) {
|
tray = (global as any).__ceru_tray__ || tray
|
||||||
|
if (tray && tray.displayBalloon) {
|
||||||
tray.displayBalloon({
|
tray.displayBalloon({
|
||||||
title: '澜音 Music',
|
title: '澜音 Music',
|
||||||
content: '已最小化到系统托盘啦,点击托盘图标可重新打开~'
|
content: '已最小化到系统托盘啦,点击托盘图标可重新打开~'
|
||||||
@@ -119,7 +74,8 @@ function basisEvent(mainWindow: BrowserWindow) {
|
|||||||
mainWindow?.hide()
|
mainWindow?.hide()
|
||||||
|
|
||||||
// 显示托盘通知
|
// 显示托盘通知
|
||||||
if (tray) {
|
tray = (global as any).__ceru_tray__ || tray
|
||||||
|
if (tray && tray.displayBalloon) {
|
||||||
tray.displayBalloon({
|
tray.displayBalloon({
|
||||||
title: 'Ceru Music',
|
title: 'Ceru Music',
|
||||||
content: '已最小化到系统托盘啦,点击托盘图标可重新打开~'
|
content: '已最小化到系统托盘啦,点击托盘图标可重新打开~'
|
||||||
|
|||||||
154
src/main/events/lyric.ts
Normal file
154
src/main/events/lyric.ts
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import { BrowserWindow, ipcMain, screen } from 'electron'
|
||||||
|
import { isAbsolute, relative, resolve } from 'path'
|
||||||
|
import { lyricConfig } from '@common/types/config'
|
||||||
|
import { configManager } from '../services/ConfigManager'
|
||||||
|
|
||||||
|
import lyricWindow from '../windows/lyric-window'
|
||||||
|
|
||||||
|
const lyricStore = {
|
||||||
|
get: () =>
|
||||||
|
configManager.get<lyricConfig>('lyric', {
|
||||||
|
fontSize: 30,
|
||||||
|
mainColor: '#73BCFC',
|
||||||
|
shadowColor: 'rgba(255, 255, 255, 0.5)',
|
||||||
|
x: screen.getPrimaryDisplay().workAreaSize.width / 2 - 400,
|
||||||
|
y: screen.getPrimaryDisplay().workAreaSize.height - 90,
|
||||||
|
width: 800,
|
||||||
|
height: 180
|
||||||
|
}),
|
||||||
|
set: (value: lyricConfig) => configManager.set<lyricConfig>('lyric', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 歌词相关 IPC
|
||||||
|
*/
|
||||||
|
const initLyricIpc = (mainWin?: BrowserWindow | null): void => {
|
||||||
|
// const mainWin = mainWindow.getWin()
|
||||||
|
const lyricWin = lyricWindow.getWin()
|
||||||
|
|
||||||
|
// 切换桌面歌词
|
||||||
|
ipcMain.on('change-desktop-lyric', (_event, val: boolean) => {
|
||||||
|
if (val) {
|
||||||
|
lyricWin?.show()
|
||||||
|
lyricWin?.setAlwaysOnTop(true, 'screen-saver')
|
||||||
|
} else lyricWin?.hide()
|
||||||
|
})
|
||||||
|
ipcMain.on('win-show', () => {
|
||||||
|
mainWin?.show()
|
||||||
|
})
|
||||||
|
// 音乐名称更改
|
||||||
|
ipcMain.on('play-song-change', (_, title) => {
|
||||||
|
if (!title) return
|
||||||
|
lyricWin?.webContents.send('play-song-change', title)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 音乐歌词更改
|
||||||
|
ipcMain.on('play-lyric-change', (_, lyricData) => {
|
||||||
|
if (!lyricData) return
|
||||||
|
lyricWin?.webContents.send('play-lyric-change', lyricData)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 播放状态更改(播放/暂停)
|
||||||
|
ipcMain.on('play-status-change', (_, status: boolean) => {
|
||||||
|
lyricWin?.webContents.send('play-status-change', status)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取窗口位置
|
||||||
|
ipcMain.handle('get-window-bounds', () => {
|
||||||
|
return lyricWin?.getBounds()
|
||||||
|
})
|
||||||
|
// 同步获取窗口位置(回退)
|
||||||
|
ipcMain.on('get-window-bounds-sync', (event) => {
|
||||||
|
event.returnValue = lyricWin?.getBounds()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取屏幕尺寸
|
||||||
|
ipcMain.handle('get-screen-size', () => {
|
||||||
|
const { width, height } = screen.getPrimaryDisplay().workAreaSize
|
||||||
|
return { width, height }
|
||||||
|
})
|
||||||
|
// 同步获取屏幕尺寸(回退)
|
||||||
|
ipcMain.on('get-screen-size-sync', (event) => {
|
||||||
|
const { width, height } = screen.getPrimaryDisplay().workAreaSize
|
||||||
|
event.returnValue = { width, height }
|
||||||
|
})
|
||||||
|
|
||||||
|
// 移动窗口
|
||||||
|
ipcMain.on('move-window', (_, x, y, width, height) => {
|
||||||
|
lyricWin?.setBounds({ x, y, width, height })
|
||||||
|
// 保存配置
|
||||||
|
lyricStore.set({ ...lyricStore.get(), x, y, width, height })
|
||||||
|
// 保持置顶
|
||||||
|
lyricWin?.setAlwaysOnTop(true, 'screen-saver')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 更新高度
|
||||||
|
ipcMain.on('update-window-height', (_, height) => {
|
||||||
|
if (!lyricWin) return
|
||||||
|
const { width } = lyricWin.getBounds()
|
||||||
|
|
||||||
|
// 更新窗口高度
|
||||||
|
lyricWin.setBounds({ width, height })
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取配置
|
||||||
|
ipcMain.handle('get-desktop-lyric-option', () => {
|
||||||
|
return lyricStore.get()
|
||||||
|
})
|
||||||
|
// 同步获取配置(用于 invoke 不可用的回退)
|
||||||
|
ipcMain.on('get-desktop-lyric-option-sync', (event) => {
|
||||||
|
event.returnValue = lyricStore.get()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 保存配置
|
||||||
|
ipcMain.on('set-desktop-lyric-option', (_, option, callback: boolean = false) => {
|
||||||
|
lyricStore.set(option)
|
||||||
|
// 触发窗口更新
|
||||||
|
if (callback && lyricWin) {
|
||||||
|
lyricWin.webContents.send('desktop-lyric-option-change', option)
|
||||||
|
}
|
||||||
|
mainWin?.webContents.send('desktop-lyric-option-change', option)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 发送主程序事件
|
||||||
|
ipcMain.on('send-main-event', (_, name, val) => {
|
||||||
|
mainWin?.webContents.send(name, val)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 关闭桌面歌词
|
||||||
|
ipcMain.on('closeDesktopLyric', () => {
|
||||||
|
lyricWin?.hide()
|
||||||
|
mainWin?.webContents.send('closeDesktopLyric')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 锁定/解锁桌面歌词
|
||||||
|
let lyricLockState = false
|
||||||
|
ipcMain.on('toogleDesktopLyricLock', (_, isLock: boolean) => {
|
||||||
|
if (!lyricWin) return
|
||||||
|
lyricLockState = !!isLock
|
||||||
|
// 是否穿透
|
||||||
|
if (lyricLockState) {
|
||||||
|
lyricWin.setIgnoreMouseEvents(true, { forward: true })
|
||||||
|
} else {
|
||||||
|
lyricWin.setIgnoreMouseEvents(false)
|
||||||
|
}
|
||||||
|
// 广播到桌面歌词窗口与主窗口,保持两端状态一致
|
||||||
|
lyricWin.webContents.send('toogleDesktopLyricLock', lyricLockState)
|
||||||
|
mainWin?.webContents.send('toogleDesktopLyricLock', lyricLockState)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 查询当前桌面歌词锁定状态
|
||||||
|
ipcMain.handle('get-lyric-lock-state', () => lyricLockState)
|
||||||
|
|
||||||
|
// 检查是否是子文件夹
|
||||||
|
ipcMain.handle('check-if-subfolder', (_, localFilesPath: string[], selectedDir: string) => {
|
||||||
|
const resolvedSelectedDir = resolve(selectedDir)
|
||||||
|
const allPaths = localFilesPath.map((p) => resolve(p))
|
||||||
|
return allPaths.some((existingPath) => {
|
||||||
|
const relativePath = relative(existingPath, resolvedSelectedDir)
|
||||||
|
return relativePath && !relativePath.startsWith('..') && !isAbsolute(relativePath)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default initLyricIpc
|
||||||
@@ -97,7 +97,7 @@ function getDefaultMessage(type: string, data: any, pluginName: string): string
|
|||||||
*/
|
*/
|
||||||
export function sendPluginNotice(noticeData: PluginNoticeData, pluginName?: string): void {
|
export function sendPluginNotice(noticeData: PluginNoticeData, pluginName?: string): void {
|
||||||
try {
|
try {
|
||||||
console.log(`[CeruMusic] 插件通知: ${noticeData.type}`, noticeData.data)
|
// console.log(`[CeruMusic] 插件通知: ${noticeData.type}`, noticeData.data)
|
||||||
|
|
||||||
// 获取主窗口实例
|
// 获取主窗口实例
|
||||||
const mainWindow = BrowserWindow.getAllWindows().find((win) => !win.isDestroyed())
|
const mainWindow = BrowserWindow.getAllWindows().find((win) => !win.isDestroyed())
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
import { app, shell, BrowserWindow, ipcMain, screen, Rectangle, Display } from 'electron'
|
import {
|
||||||
|
app,
|
||||||
|
shell,
|
||||||
|
BrowserWindow,
|
||||||
|
ipcMain,
|
||||||
|
screen,
|
||||||
|
Rectangle,
|
||||||
|
Display,
|
||||||
|
Tray,
|
||||||
|
Menu
|
||||||
|
} from 'electron'
|
||||||
import { configManager } from './services/ConfigManager'
|
import { configManager } from './services/ConfigManager'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
||||||
@@ -6,10 +16,13 @@ import icon from '../../resources/logo.png?asset'
|
|||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import InitEventServices from './events'
|
import InitEventServices from './events'
|
||||||
|
|
||||||
|
import lyricWindow from './windows/lyric-window'
|
||||||
|
|
||||||
import './events/musicCache'
|
import './events/musicCache'
|
||||||
import './events/songList'
|
import './events/songList'
|
||||||
import './events/directorySettings'
|
import './events/directorySettings'
|
||||||
import './events/pluginNotice'
|
import './events/pluginNotice'
|
||||||
|
import initLyricIpc from './events/lyric'
|
||||||
|
|
||||||
// 获取单实例锁
|
// 获取单实例锁
|
||||||
const gotTheLock = app.requestSingleInstanceLock()
|
const gotTheLock = app.requestSingleInstanceLock()
|
||||||
@@ -30,6 +43,112 @@ if (!gotTheLock) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null
|
let mainWindow: BrowserWindow | null = null
|
||||||
|
let tray: Tray | null = null
|
||||||
|
let trayLyricLocked = false
|
||||||
|
|
||||||
|
function updateTrayMenu() {
|
||||||
|
const lyricWin = lyricWindow.getWin()
|
||||||
|
const isVisible = !!lyricWin && lyricWin.isVisible()
|
||||||
|
const toggleLyricLabel = isVisible ? '隐藏桌面歌词' : '显示桌面歌词'
|
||||||
|
const toggleLockLabel = trayLyricLocked ? '解锁桌面歌词' : '锁定桌面歌词'
|
||||||
|
|
||||||
|
const contextMenu = Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
label: toggleLyricLabel,
|
||||||
|
click: () => {
|
||||||
|
const target = !isVisible
|
||||||
|
ipcMain.emit('change-desktop-lyric', null, target)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: toggleLockLabel,
|
||||||
|
click: () => {
|
||||||
|
const next = !trayLyricLocked
|
||||||
|
ipcMain.emit('toogleDesktopLyricLock', null, next)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: '显示主窗口',
|
||||||
|
click: () => {
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.show()
|
||||||
|
mainWindow.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '播放/暂停',
|
||||||
|
click: () => {
|
||||||
|
mainWindow?.webContents.send('music-control')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: '退出',
|
||||||
|
click: () => {
|
||||||
|
app.quit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
tray?.setContextMenu(contextMenu)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupTray() {
|
||||||
|
// 全局单例防重复(热重载/多次执行保护)
|
||||||
|
const g: any = global as any
|
||||||
|
if (g.__ceru_tray__) {
|
||||||
|
try {
|
||||||
|
g.__ceru_tray__.destroy()
|
||||||
|
} catch {}
|
||||||
|
g.__ceru_tray__ = null
|
||||||
|
}
|
||||||
|
if (tray) {
|
||||||
|
try {
|
||||||
|
tray.destroy()
|
||||||
|
} catch {}
|
||||||
|
tray = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconPath = path.join(__dirname, '../../resources/logo.ico')
|
||||||
|
tray = new Tray(iconPath)
|
||||||
|
tray.setToolTip('Ceru Music')
|
||||||
|
updateTrayMenu()
|
||||||
|
|
||||||
|
// 左键单击切换主窗口显示
|
||||||
|
tray.on('click', () => {
|
||||||
|
if (!mainWindow) return
|
||||||
|
if (mainWindow.isVisible()) {
|
||||||
|
mainWindow.hide()
|
||||||
|
} else {
|
||||||
|
mainWindow.show()
|
||||||
|
mainWindow.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 防重复注册 IPC 监听(仅注册一次)
|
||||||
|
if (!g.__ceru_tray_ipc_bound__) {
|
||||||
|
ipcMain.on('toogleDesktopLyricLock', (_e, isLock: boolean) => {
|
||||||
|
trayLyricLocked = !!isLock
|
||||||
|
updateTrayMenu()
|
||||||
|
})
|
||||||
|
ipcMain.on('change-desktop-lyric', () => {
|
||||||
|
updateTrayMenu()
|
||||||
|
})
|
||||||
|
g.__ceru_tray_ipc_bound__ = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录全局托盘句柄
|
||||||
|
g.__ceru_tray__ = tray
|
||||||
|
|
||||||
|
app.once('before-quit', () => {
|
||||||
|
try {
|
||||||
|
tray?.destroy()
|
||||||
|
} catch {}
|
||||||
|
tray = null
|
||||||
|
g.__ceru_tray__ = null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据窗口当前所在的显示器,动态更新窗口的最大尺寸限制。
|
* 根据窗口当前所在的显示器,动态更新窗口的最大尺寸限制。
|
||||||
@@ -64,9 +183,6 @@ function createWindow(): void {
|
|||||||
height: 750,
|
height: 750,
|
||||||
minWidth: 1100,
|
minWidth: 1100,
|
||||||
minHeight: 670,
|
minHeight: 670,
|
||||||
// ⚠️ 关键修改 1: 移除 maxWidth 和 maxHeight 的硬编码限制
|
|
||||||
// maxWidth: screenWidth,
|
|
||||||
// maxHeight: screenHeight,
|
|
||||||
show: false,
|
show: false,
|
||||||
center: !savedBounds, // 如果有保存的位置,则不居中
|
center: !savedBounds, // 如果有保存的位置,则不居中
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
@@ -146,6 +262,7 @@ function createWindow(): void {
|
|||||||
return { action: 'deny' }
|
return { action: 'deny' }
|
||||||
})
|
})
|
||||||
InitEventServices(mainWindow)
|
InitEventServices(mainWindow)
|
||||||
|
|
||||||
// HMR for renderer base on electron-vite cli.
|
// HMR for renderer base on electron-vite cli.
|
||||||
// Load the remote URL for development or the local html file for production.
|
// Load the remote URL for development or the local html file for production.
|
||||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||||
@@ -179,6 +296,10 @@ app.whenReady().then(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
createWindow()
|
createWindow()
|
||||||
|
lyricWindow.create()
|
||||||
|
initLyricIpc(mainWindow)
|
||||||
|
// 仅在主进程初始化一次托盘
|
||||||
|
setupTray()
|
||||||
|
|
||||||
// 注册自动更新事件
|
// 注册自动更新事件
|
||||||
registerAutoUpdateEvents()
|
registerAutoUpdateEvents()
|
||||||
@@ -204,8 +325,7 @@ app.whenReady().then(() => {
|
|||||||
|
|
||||||
// 当所有窗口关闭时不退出应用,因为我们有系统托盘
|
// 当所有窗口关闭时不退出应用,因为我们有系统托盘
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
// 在 macOS 上,应用通常会保持活跃状态
|
// 保持应用常驻,通过系统托盘管理
|
||||||
// 在其他平台上,我们也保持应用运行,因为有系统托盘
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// In this file you can include the rest of your app's specific main process
|
// In this file you can include the rest of your app's specific main process
|
||||||
|
|||||||
47
src/main/logger/index.ts
Normal file
47
src/main/logger/index.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// 日志输出
|
||||||
|
import { existsSync, mkdirSync } from 'fs'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { app } from 'electron'
|
||||||
|
import log from 'electron-log'
|
||||||
|
|
||||||
|
// 日志文件路径
|
||||||
|
const logDir = join(app.getPath('logs'))
|
||||||
|
// 是否存在日志目录
|
||||||
|
if (!existsSync(logDir)) mkdirSync(logDir)
|
||||||
|
|
||||||
|
// 获取日期 - YYYY-MM-DD
|
||||||
|
const dateString = new Date().toISOString().slice(0, 10)
|
||||||
|
const logFilePath = join(logDir, `${dateString}.log`)
|
||||||
|
console.log(logFilePath, '546444444444444444444444444444444444')
|
||||||
|
|
||||||
|
// 配置日志系统
|
||||||
|
log.transports.console.useStyles = true // 颜色输出
|
||||||
|
log.transports.file.level = 'info' // 仅记录 info 及以上级别
|
||||||
|
log.transports.file.resolvePathFn = (): string => logFilePath // 日志文件路径
|
||||||
|
log.transports.file.maxSize = 2 * 1024 * 1024 // 文件最大 2MB
|
||||||
|
|
||||||
|
// 日志格式化
|
||||||
|
// log.transports.file.format = "[{y}-{m}-{d} {h}:{i}:{s}] [{level}] [{scope}] {text}";
|
||||||
|
|
||||||
|
// 绑定默认事件
|
||||||
|
const defaultLog = log.scope('default')
|
||||||
|
console.log = defaultLog.log
|
||||||
|
console.info = defaultLog.info
|
||||||
|
console.warn = defaultLog.warn
|
||||||
|
console.error = defaultLog.error
|
||||||
|
|
||||||
|
// 分作用域导出
|
||||||
|
export { defaultLog }
|
||||||
|
export const ipcLog = log.scope('ipc')
|
||||||
|
export const trayLog = log.scope('tray')
|
||||||
|
export const thumbarLog = log.scope('thumbar')
|
||||||
|
export const storeLog = log.scope('store')
|
||||||
|
export const updateLog = log.scope('update')
|
||||||
|
export const systemLog = log.scope('system')
|
||||||
|
export const configLog = log.scope('config')
|
||||||
|
export const windowsLog = log.scope('windows')
|
||||||
|
export const processLog = log.scope('process')
|
||||||
|
export const preloadLog = log.scope('preload')
|
||||||
|
export const rendererLog = log.scope('renderer')
|
||||||
|
export const shortcutLog = log.scope('shortcut')
|
||||||
|
export const serverLog = log.scope('server')
|
||||||
@@ -60,6 +60,7 @@ export class ConfigManager {
|
|||||||
// 设置配置项
|
// 设置配置项
|
||||||
public set<T>(key: string, value: T): void {
|
public set<T>(key: string, value: T): void {
|
||||||
this.config[key] = value
|
this.config[key] = value
|
||||||
|
this.saveConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除配置项
|
// 删除配置项
|
||||||
|
|||||||
@@ -493,7 +493,7 @@ class CeruMusicPluginHost {
|
|||||||
}, timeout)
|
}, timeout)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`${CONSTANTS.LOG_PREFIX} 发起请求: ${options.method || 'GET'} ${url}`)
|
// console.log(`${CONSTANTS.LOG_PREFIX} 发起请求: ${options.method || 'GET'} ${url}`)
|
||||||
|
|
||||||
const fetchOptions = {
|
const fetchOptions = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -504,7 +504,7 @@ class CeruMusicPluginHost {
|
|||||||
const response = await fetch(url, fetchOptions)
|
const response = await fetch(url, fetchOptions)
|
||||||
clearTimeout(timeoutId)
|
clearTimeout(timeoutId)
|
||||||
|
|
||||||
console.log(`${CONSTANTS.LOG_PREFIX} 请求响应: ${response.status} ${response.statusText}`)
|
// console.log(`${CONSTANTS.LOG_PREFIX} 请求响应: ${response.status} ${response.statusText}`)
|
||||||
|
|
||||||
const body = await this._parseResponseBody(response)
|
const body = await this._parseResponseBody(response)
|
||||||
const headers = this._extractHeaders(response)
|
const headers = this._extractHeaders(response)
|
||||||
@@ -515,11 +515,11 @@ class CeruMusicPluginHost {
|
|||||||
headers
|
headers
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`${CONSTANTS.LOG_PREFIX} 请求完成:`, {
|
// console.log(`${CONSTANTS.LOG_PREFIX} 请求完成:`, {
|
||||||
url,
|
// url,
|
||||||
status: response.status,
|
// status: response.status,
|
||||||
bodyType: typeof body
|
// bodyType: typeof body
|
||||||
})
|
// })
|
||||||
|
|
||||||
return result
|
return result
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
45
src/main/windows/index.ts
Normal file
45
src/main/windows/index.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { BrowserWindow, BrowserWindowConstructorOptions, app } from 'electron'
|
||||||
|
import { windowsLog } from '../logger'
|
||||||
|
import { join } from 'path'
|
||||||
|
import icon from '../../../resources/logo.png?asset'
|
||||||
|
|
||||||
|
export const createWindow = (
|
||||||
|
options: BrowserWindowConstructorOptions = {}
|
||||||
|
): BrowserWindow | null => {
|
||||||
|
try {
|
||||||
|
const defaultOptions: BrowserWindowConstructorOptions = {
|
||||||
|
title: app.getName(),
|
||||||
|
width: 1280,
|
||||||
|
height: 720,
|
||||||
|
frame: false, // 创建后是否显示窗口
|
||||||
|
center: true, // 窗口居中
|
||||||
|
icon, // 窗口图标
|
||||||
|
autoHideMenuBar: true, // 隐藏菜单栏
|
||||||
|
webPreferences: {
|
||||||
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
|
// 禁用渲染器沙盒
|
||||||
|
sandbox: false,
|
||||||
|
// 禁用同源策略
|
||||||
|
webSecurity: false,
|
||||||
|
// 允许 HTTP
|
||||||
|
allowRunningInsecureContent: true,
|
||||||
|
// 禁用拼写检查
|
||||||
|
spellcheck: false,
|
||||||
|
// 启用 Node.js
|
||||||
|
nodeIntegration: true,
|
||||||
|
nodeIntegrationInWorker: true,
|
||||||
|
// 关闭上下文隔离,确保在窗口中注入 window.electron
|
||||||
|
contextIsolation: false,
|
||||||
|
backgroundThrottling: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 合并参数
|
||||||
|
options = Object.assign(defaultOptions, options)
|
||||||
|
// 创建窗口
|
||||||
|
const win = new BrowserWindow(options)
|
||||||
|
return win
|
||||||
|
} catch (error) {
|
||||||
|
windowsLog.error(error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
92
src/main/windows/lyric-window.ts
Normal file
92
src/main/windows/lyric-window.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { BrowserWindow, screen } from 'electron'
|
||||||
|
import { createWindow } from './index'
|
||||||
|
import { configManager } from '../services/ConfigManager'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { lyricConfig } from '@common/types/config'
|
||||||
|
|
||||||
|
const lyricStore = {
|
||||||
|
get: () =>
|
||||||
|
configManager.get<lyricConfig>('lyric', {
|
||||||
|
fontSize: 30,
|
||||||
|
mainColor: '#73BCFC',
|
||||||
|
shadowColor: 'rgba(255, 255, 255, 0.5)',
|
||||||
|
x: screen.getPrimaryDisplay().workAreaSize.width / 2 - 400,
|
||||||
|
y: screen.getPrimaryDisplay().workAreaSize.height - 90,
|
||||||
|
width: 800,
|
||||||
|
height: 180
|
||||||
|
}),
|
||||||
|
set: (value: lyricConfig) => configManager.set<lyricConfig>('lyric', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
class LyricWindow {
|
||||||
|
private win: BrowserWindow | null = null
|
||||||
|
constructor() {}
|
||||||
|
/**
|
||||||
|
* 主窗口事件
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
private event(): void {
|
||||||
|
if (!this.win) return
|
||||||
|
// 歌词窗口缩放
|
||||||
|
this.win?.on('resized', () => {
|
||||||
|
const bounds = this.win?.getBounds()
|
||||||
|
if (bounds) {
|
||||||
|
const { width, height } = bounds
|
||||||
|
console.log('歌词窗口缩放:', width, height);
|
||||||
|
|
||||||
|
lyricStore.set({
|
||||||
|
...lyricStore.get(),
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 创建主窗口
|
||||||
|
* @returns BrowserWindow | null
|
||||||
|
*/
|
||||||
|
create(): BrowserWindow | null {
|
||||||
|
const { width, height, x, y } = lyricStore.get()
|
||||||
|
this.win = createWindow({
|
||||||
|
width: width || 800,
|
||||||
|
height: height || 180,
|
||||||
|
minWidth: 440,
|
||||||
|
minHeight: 120,
|
||||||
|
maxWidth: 1600,
|
||||||
|
maxHeight: 300,
|
||||||
|
show: false,
|
||||||
|
// 窗口位置
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
transparent: true,
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0)',
|
||||||
|
alwaysOnTop: true,
|
||||||
|
resizable: true,
|
||||||
|
movable: true,
|
||||||
|
// 不在任务栏显示
|
||||||
|
skipTaskbar: true,
|
||||||
|
// 窗口不能最小化
|
||||||
|
minimizable: false,
|
||||||
|
// 窗口不能最大化
|
||||||
|
maximizable: false,
|
||||||
|
// 窗口不能进入全屏状态
|
||||||
|
fullscreenable: false
|
||||||
|
})
|
||||||
|
if (!this.win) return null
|
||||||
|
// 加载地址(开发环境用项目根目录,生产用打包后的相对路径)
|
||||||
|
this.win.loadFile(join(__dirname, '../main/src/web/lyric.html'))
|
||||||
|
// 窗口事件
|
||||||
|
this.event()
|
||||||
|
return this.win
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取窗口
|
||||||
|
* @returns BrowserWindow | null
|
||||||
|
*/
|
||||||
|
getWin(): BrowserWindow | null {
|
||||||
|
return this.win
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new LyricWindow()
|
||||||
@@ -207,14 +207,14 @@ const api = {
|
|||||||
// just add to the DOM global.
|
// just add to the DOM global.
|
||||||
if (process.contextIsolated) {
|
if (process.contextIsolated) {
|
||||||
try {
|
try {
|
||||||
contextBridge.exposeInMainWorld('electron', electronAPI)
|
contextBridge.exposeInMainWorld('electron', { ...electronAPI, ipcRenderer })
|
||||||
contextBridge.exposeInMainWorld('api', api)
|
contextBridge.exposeInMainWorld('api', api)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// @ts-ignore (define in dts)
|
// @ts-ignore (define in dts)
|
||||||
window.electron = electronAPI
|
window.electron = { ...electronAPI, ipcRenderer }
|
||||||
// @ts-ignore (define in dts)
|
// @ts-ignore (define in dts)
|
||||||
window.api = api
|
window.api = api
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/renderer/components.d.ts
vendored
3
src/renderer/components.d.ts
vendored
@@ -12,6 +12,7 @@ declare module 'vue' {
|
|||||||
AudioVisualizer: typeof import('./src/components/Play/AudioVisualizer.vue')['default']
|
AudioVisualizer: typeof import('./src/components/Play/AudioVisualizer.vue')['default']
|
||||||
ContextMenu: typeof import('./src/components/ContextMenu/ContextMenu.vue')['default']
|
ContextMenu: typeof import('./src/components/ContextMenu/ContextMenu.vue')['default']
|
||||||
Demo: typeof import('./src/components/ContextMenu/demo.vue')['default']
|
Demo: typeof import('./src/components/ContextMenu/demo.vue')['default']
|
||||||
|
DesktopLyricStyle: typeof import('./src/components/Settings/DesktopLyricStyle.vue')['default']
|
||||||
DirectorySettings: typeof import('./src/components/Settings/DirectorySettings.vue')['default']
|
DirectorySettings: typeof import('./src/components/Settings/DirectorySettings.vue')['default']
|
||||||
FloatBall: typeof import('./src/components/AI/FloatBall.vue')['default']
|
FloatBall: typeof import('./src/components/AI/FloatBall.vue')['default']
|
||||||
FullPlay: typeof import('./src/components/Play/FullPlay.vue')['default']
|
FullPlay: typeof import('./src/components/Play/FullPlay.vue')['default']
|
||||||
@@ -40,6 +41,7 @@ declare module 'vue' {
|
|||||||
TButton: typeof import('tdesign-vue-next')['Button']
|
TButton: typeof import('tdesign-vue-next')['Button']
|
||||||
TCard: typeof import('tdesign-vue-next')['Card']
|
TCard: typeof import('tdesign-vue-next')['Card']
|
||||||
TCheckbox: typeof import('tdesign-vue-next')['Checkbox']
|
TCheckbox: typeof import('tdesign-vue-next')['Checkbox']
|
||||||
|
TColorPicker: typeof import('tdesign-vue-next')['ColorPicker']
|
||||||
TContent: typeof import('tdesign-vue-next')['Content']
|
TContent: typeof import('tdesign-vue-next')['Content']
|
||||||
TDialog: typeof import('tdesign-vue-next')['Dialog']
|
TDialog: typeof import('tdesign-vue-next')['Dialog']
|
||||||
TDivider: typeof import('tdesign-vue-next')['Divider']
|
TDivider: typeof import('tdesign-vue-next')['Divider']
|
||||||
@@ -50,6 +52,7 @@ declare module 'vue' {
|
|||||||
TIcon: typeof import('tdesign-vue-next')['Icon']
|
TIcon: typeof import('tdesign-vue-next')['Icon']
|
||||||
TImage: typeof import('tdesign-vue-next')['Image']
|
TImage: typeof import('tdesign-vue-next')['Image']
|
||||||
TInput: typeof import('tdesign-vue-next')['Input']
|
TInput: typeof import('tdesign-vue-next')['Input']
|
||||||
|
TInputNumber: typeof import('tdesign-vue-next')['InputNumber']
|
||||||
TitleBarControls: typeof import('./src/components/TitleBarControls.vue')['default']
|
TitleBarControls: typeof import('./src/components/TitleBarControls.vue')['default']
|
||||||
TLayout: typeof import('tdesign-vue-next')['Layout']
|
TLayout: typeof import('tdesign-vue-next')['Layout']
|
||||||
TLoading: typeof import('tdesign-vue-next')['Loading']
|
TLoading: typeof import('tdesign-vue-next')['Loading']
|
||||||
|
|||||||
@@ -31,6 +31,15 @@ onMounted(() => {
|
|||||||
syncNaiveTheme()
|
syncNaiveTheme()
|
||||||
window.addEventListener('theme-changed', () => syncNaiveTheme())
|
window.addEventListener('theme-changed', () => syncNaiveTheme())
|
||||||
|
|
||||||
|
// 全局监听来自主进程的播放控制事件,确保路由切换也可响应
|
||||||
|
const forward = (name: string, val?: any) => {
|
||||||
|
window.dispatchEvent(new CustomEvent('global-music-control', { detail: { name, val } }))
|
||||||
|
}
|
||||||
|
window.electron?.ipcRenderer?.on?.('play', () => forward('play'))
|
||||||
|
window.electron?.ipcRenderer?.on?.('pause', () => forward('pause'))
|
||||||
|
window.electron?.ipcRenderer?.on?.('playPrev', () => forward('playPrev'))
|
||||||
|
window.electron?.ipcRenderer?.on?.('playNext', () => forward('playNext'))
|
||||||
|
|
||||||
// 应用启动后延迟3秒检查更新,避免影响启动速度
|
// 应用启动后延迟3秒检查更新,避免影响启动速度
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
checkForUpdates()
|
checkForUpdates()
|
||||||
|
|||||||
3
src/renderer/src/assets/icons/lyricOpen.svg
Normal file
3
src/renderer/src/assets/icons/lyricOpen.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg class="lyrics-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M192 240a112 112 0 0 1 111.616 102.784l0.384 9.216V832a16 16 0 0 0 12.352 15.552L320 848h66.816a48 48 0 0 1 6.528 95.552l-6.528 0.448H320a112 112 0 0 1-111.616-102.784L208 832V352a16 16 0 0 0-12.352-15.552L192 336H128a48 48 0 0 1-6.528-95.552L128 240h64z m640-157.568a112 112 0 0 1 111.616 102.848l0.384 9.152V832a112 112 0 0 1-102.784 111.616L832 944h-67.84a48 48 0 0 1-6.464-95.552l6.464-0.448H832a16 16 0 0 0 15.552-12.352L848 832V194.432a16 16 0 0 0-12.352-15.552L832 178.432H480a48 48 0 0 1-6.528-95.552l6.528-0.448H832z m-160 315.136c61.824 0 112 50.112 112 112v147.648a112 112 0 0 1-112 112h-128a112 112 0 0 1-112-112V509.568c0-61.888 50.176-112 112-112z m0 96h-128a16 16 0 0 0-16 16v147.648c0 8.832 7.168 16 16 16h128a16 16 0 0 0 16-16V509.568a16 16 0 0 0-16-16z m64-253.568a48 48 0 0 1 6.528 95.552l-6.528 0.448h-256a48 48 0 0 1-6.528-95.552L480 240h256zM256 82.432a48 48 0 0 1 6.528 95.616L256 178.432H128a48 48 0 0 1-6.528-95.552L128 82.432h128z" fill="currentColor"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -234,6 +234,8 @@
|
|||||||
--song-list-btn-bg-hover: var(--td-brand-color-light);
|
--song-list-btn-bg-hover: var(--td-brand-color-light);
|
||||||
--song-list-quality-bg: #fff7e6;
|
--song-list-quality-bg: #fff7e6;
|
||||||
--song-list-quality-color: #fa8c16;
|
--song-list-quality-color: #fa8c16;
|
||||||
|
--song-list-source-bg: #f3feff;
|
||||||
|
--song-list-source-color: #00d4e3;
|
||||||
|
|
||||||
/* Search 页面专用变量 - 亮色主题 */
|
/* Search 页面专用变量 - 亮色主题 */
|
||||||
--search-bg: var(--theme-bg-tertiary);
|
--search-bg: var(--theme-bg-tertiary);
|
||||||
@@ -597,6 +599,8 @@
|
|||||||
--song-list-btn-bg-hover: var(--td-brand-color-light);
|
--song-list-btn-bg-hover: var(--td-brand-color-light);
|
||||||
--song-list-quality-bg: #3a2a1a;
|
--song-list-quality-bg: #3a2a1a;
|
||||||
--song-list-quality-color: #fa8c16;
|
--song-list-quality-color: #fa8c16;
|
||||||
|
--song-list-source-bg: #343939;
|
||||||
|
--song-list-source-color: #00eeff;
|
||||||
|
|
||||||
/* Search 页面专用变量 - 暗色主题 */
|
/* Search 页面专用变量 - 暗色主题 */
|
||||||
--search-bg: var(--theme-bg-tertiary);
|
--search-bg: var(--theme-bg-tertiary);
|
||||||
|
|||||||
@@ -44,6 +44,9 @@
|
|||||||
<span v-if="song.types && song.types.length > 0" class="quality-tag">
|
<span v-if="song.types && song.types.length > 0" class="quality-tag">
|
||||||
{{ getQualityDisplayName(song.types[song.types.length - 1]) }}
|
{{ getQualityDisplayName(song.types[song.types.length - 1]) }}
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="song.source" class="source-tag">
|
||||||
|
{{ song.source }}
|
||||||
|
</span>
|
||||||
{{ song.singer }}
|
{{ song.singer }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -759,6 +762,14 @@ onUnmounted(() => {
|
|||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
.source-tag {
|
||||||
|
background: var(--song-list-source-bg);
|
||||||
|
color: var(--song-list-source-color);
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,12 +172,14 @@ watch(
|
|||||||
props.songInfo && 'source' in props.songInfo ? (props.songInfo as any).source : 'kg'
|
props.songInfo && 'source' in props.songInfo ? (props.songInfo as any).source : 'kg'
|
||||||
let parsedLyrics: LyricLine[] = []
|
let parsedLyrics: LyricLine[] = []
|
||||||
|
|
||||||
if (source === 'wy') {
|
if (source === 'wy' || source === 'tx') {
|
||||||
// 网易云:优先尝试 TTML
|
// 网易云 / QQ 音乐:优先尝试 TTML
|
||||||
try {
|
try {
|
||||||
const res = await (
|
const res = await (
|
||||||
await fetch(`https://amll.bikonoo.com/ncm-lyrics/${newId}.ttml`, {
|
await fetch(
|
||||||
signal: abort.signal
|
`https://amll-ttml-db.stevexmh.net/${source === 'wy' ? 'ncm' : 'qq'}/${newId}`,
|
||||||
|
{
|
||||||
|
signal: abort.signal
|
||||||
})
|
})
|
||||||
).text()
|
).text()
|
||||||
if (!active) return
|
if (!active) return
|
||||||
@@ -186,13 +188,13 @@ watch(
|
|||||||
} catch {
|
} catch {
|
||||||
// 回退到统一歌词 API
|
// 回退到统一歌词 API
|
||||||
const lyricData = await window.api.music.requestSdk('getLyric', {
|
const lyricData = await window.api.music.requestSdk('getLyric', {
|
||||||
source: 'wy',
|
source,
|
||||||
songInfo: _.cloneDeep(toRaw(props.songInfo)) as any
|
songInfo: _.cloneDeep(toRaw(props.songInfo)) as any
|
||||||
})
|
})
|
||||||
if (!active) return
|
if (!active) return
|
||||||
|
|
||||||
if (lyricData?.crlyric) {
|
if (lyricData?.crlyric) {
|
||||||
parsedLyrics = parseYrc(lyricData.crlyric)
|
parsedLyrics = parseCrLyricBySource(source, lyricData.crlyric)
|
||||||
} else if (lyricData?.lyric) {
|
} else if (lyricData?.lyric) {
|
||||||
parsedLyrics = parseLrc(lyricData.lyric)
|
parsedLyrics = parseLrc(lyricData.lyric)
|
||||||
}
|
}
|
||||||
@@ -227,6 +229,71 @@ watch(
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 桌面歌词联动:构建歌词负载、计算当前行并通过 IPC 推送
|
||||||
|
const buildLyricPayload = (lines: LyricLine[]) =>
|
||||||
|
(lines || []).map((l) => ({
|
||||||
|
content: (l.words || []).map((w) => w.word).join(''),
|
||||||
|
tran: l.translatedLyric || ''
|
||||||
|
}))
|
||||||
|
|
||||||
|
const lastLyricIndex = ref(-1)
|
||||||
|
const computeLyricIndex = (timeMs: number, lines: LyricLine[]) => {
|
||||||
|
if (!lines || lines.length === 0) return -1
|
||||||
|
const t = timeMs
|
||||||
|
const i = lines.findIndex((l) => t >= l.startTime && t < l.endTime)
|
||||||
|
if (i !== -1) return i
|
||||||
|
for (let j = lines.length - 1; j >= 0; j--) {
|
||||||
|
if (t >= lines[j].startTime) return j
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 歌词集合变化时,先推一次集合,index 为 -1(由窗口自行处理占位)
|
||||||
|
watch(
|
||||||
|
() => state.lyricLines,
|
||||||
|
(lines) => {
|
||||||
|
const payload = { index: -1, lyric: buildLyricPayload(lines) }
|
||||||
|
;(window as any)?.electron?.ipcRenderer?.send?.('play-lyric-change', payload)
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// 当前时间变化时,计算当前行并推送
|
||||||
|
watch(
|
||||||
|
() => state.currentTime,
|
||||||
|
(ms) => {
|
||||||
|
const idx = computeLyricIndex(ms, state.lyricLines)
|
||||||
|
if (idx !== lastLyricIndex.value) {
|
||||||
|
lastLyricIndex.value = idx
|
||||||
|
const payload = { index: idx, lyric: buildLyricPayload(state.lyricLines) }
|
||||||
|
;(window as any)?.electron?.ipcRenderer?.send?.('play-lyric-change', payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 播放状态推送(用于窗口播放/暂停按钮联动)
|
||||||
|
watch(
|
||||||
|
() => Audio.value.isPlay,
|
||||||
|
(playing) => {
|
||||||
|
;(window as any)?.electron?.ipcRenderer?.send?.('play-status-change', playing)
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// 歌曲标题推送
|
||||||
|
watch(
|
||||||
|
() => props.songInfo,
|
||||||
|
(info) => {
|
||||||
|
try {
|
||||||
|
const name = (info as any)?.name || ''
|
||||||
|
const artist = (info as any)?.singer || ''
|
||||||
|
const title = [name, artist].filter(Boolean).join(' - ')
|
||||||
|
if (title) (window as any)?.electron?.ipcRenderer?.send?.('play-song-change', title)
|
||||||
|
} catch {}
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
const bgRef = ref<BackgroundRenderRef | undefined>(undefined)
|
const bgRef = ref<BackgroundRenderRef | undefined>(undefined)
|
||||||
const lyricPlayerRef = ref<LyricPlayerRef | undefined>(undefined)
|
const lyricPlayerRef = ref<LyricPlayerRef | undefined>(undefined)
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
import mediaSessionController from '@renderer/utils/audio/useSmtc'
|
import mediaSessionController from '@renderer/utils/audio/useSmtc'
|
||||||
import defaultCoverImg from '/default-cover.png'
|
import defaultCoverImg from '/default-cover.png'
|
||||||
import { downloadSingleSong } from '@renderer/utils/audio/download'
|
import { downloadSingleSong } from '@renderer/utils/audio/download'
|
||||||
import { HeartIcon, DownloadIcon } from 'tdesign-icons-vue-next'
|
import { HeartIcon, DownloadIcon, CheckIcon, LockOnIcon } from 'tdesign-icons-vue-next'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { songListAPI } from '@renderer/api/songList'
|
import { songListAPI } from '@renderer/api/songList'
|
||||||
|
|
||||||
@@ -69,6 +69,70 @@ watch(
|
|||||||
)
|
)
|
||||||
onMounted(() => refreshLikeState())
|
onMounted(() => refreshLikeState())
|
||||||
const showFullPlay = ref(false)
|
const showFullPlay = ref(false)
|
||||||
|
// 桌面歌词开关与锁定状态
|
||||||
|
const desktopLyricOpen = ref(false)
|
||||||
|
const desktopLyricLocked = ref(false)
|
||||||
|
|
||||||
|
// 桌面歌词按钮逻辑:
|
||||||
|
// - 若未打开:打开桌面歌词
|
||||||
|
// - 若已打开且锁定:先解锁,不关闭
|
||||||
|
// - 若已打开且未锁定:关闭桌面歌词
|
||||||
|
const toggleDesktopLyric = async () => {
|
||||||
|
try {
|
||||||
|
if (!desktopLyricOpen.value) {
|
||||||
|
window.electron?.ipcRenderer?.send?.('change-desktop-lyric', true)
|
||||||
|
desktopLyricOpen.value = true
|
||||||
|
// 恢复最新锁定状态
|
||||||
|
const lock = await window.electron?.ipcRenderer?.invoke?.('get-lyric-lock-state')
|
||||||
|
desktopLyricLocked.value = !!lock
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 已打开
|
||||||
|
const lock = await window.electron?.ipcRenderer?.invoke?.('get-lyric-lock-state')
|
||||||
|
desktopLyricLocked.value = !!lock
|
||||||
|
if (desktopLyricLocked.value) {
|
||||||
|
// 先解锁,本次不关闭
|
||||||
|
window.electron?.ipcRenderer?.send?.('toogleDesktopLyricLock', false)
|
||||||
|
desktopLyricLocked.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 未锁定则关闭
|
||||||
|
window.electron?.ipcRenderer?.send?.('change-desktop-lyric', false)
|
||||||
|
desktopLyricOpen.value = false
|
||||||
|
} catch (e) {
|
||||||
|
console.error('切换桌面歌词失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听来自主进程的锁定状态广播
|
||||||
|
window.electron?.ipcRenderer?.on?.('toogleDesktopLyricLock', (_, lock) => {
|
||||||
|
desktopLyricLocked.value = !!lock
|
||||||
|
})
|
||||||
|
// 监听主进程通知关闭桌面歌词
|
||||||
|
window.electron?.ipcRenderer?.on?.('closeDesktopLyric', () => {
|
||||||
|
desktopLyricOpen.value = false
|
||||||
|
desktopLyricLocked.value = false
|
||||||
|
})
|
||||||
|
|
||||||
|
window.addEventListener('global-music-control', (e: any) => {
|
||||||
|
const name = e?.detail?.name
|
||||||
|
console.log(name);
|
||||||
|
switch (name) {
|
||||||
|
case 'play':
|
||||||
|
handlePlay()
|
||||||
|
break
|
||||||
|
case 'pause':
|
||||||
|
handlePause()
|
||||||
|
break
|
||||||
|
case 'playPrev':
|
||||||
|
playPrevious()
|
||||||
|
break
|
||||||
|
case 'playNext':
|
||||||
|
playNext()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
document.addEventListener('keydown', KeyEvent)
|
document.addEventListener('keydown', KeyEvent)
|
||||||
// 处理最小化右键的事件
|
// 处理最小化右键的事件
|
||||||
const removeMusicCtrlListener = window.api.onMusicCtrl(() => {
|
const removeMusicCtrlListener = window.api.onMusicCtrl(() => {
|
||||||
@@ -1091,6 +1155,29 @@ watch(showFullPlay, (val) => {
|
|||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 桌面歌词开关按钮 -->
|
||||||
|
<t-tooltip
|
||||||
|
:content="
|
||||||
|
desktopLyricOpen ? (desktopLyricLocked ? '解锁歌词' : '关闭桌面歌词') : '打开桌面歌词'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<t-button
|
||||||
|
class="control-btn lyric-btn"
|
||||||
|
shape="circle"
|
||||||
|
variant="text"
|
||||||
|
:disabled="!songInfo.songmid"
|
||||||
|
@click.stop="toggleDesktopLyric"
|
||||||
|
>
|
||||||
|
<SvgIcon name="lyricOpen" size="18"></SvgIcon>
|
||||||
|
<transition name="fade" mode="out-in">
|
||||||
|
<template v-if="desktopLyricOpen">
|
||||||
|
<LockOnIcon v-if="desktopLyricLocked" key="lock" class="lyric-lock" size="8" />
|
||||||
|
<CheckIcon v-else key="check" class="lyric-check" size="8" />
|
||||||
|
</template>
|
||||||
|
</transition>
|
||||||
|
</t-button>
|
||||||
|
</t-tooltip>
|
||||||
|
|
||||||
<!-- 播放列表按钮 -->
|
<!-- 播放列表按钮 -->
|
||||||
<t-tooltip content="播放列表">
|
<t-tooltip content="播放列表">
|
||||||
<n-badge :value="list.length" :max="99" color="#bbb">
|
<n-badge :value="list.length" :max="99" color="#bbb">
|
||||||
@@ -1444,6 +1531,7 @@ watch(showFullPlay, (val) => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
.iconfont {
|
.iconfont {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
@@ -1452,6 +1540,17 @@ watch(showFullPlay, (val) => {
|
|||||||
&:hover {
|
&:hover {
|
||||||
color: v-bind(hoverColor);
|
color: v-bind(hoverColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.lyric-btn .lyric-check,
|
||||||
|
&.lyric-btn .lyric-lock {
|
||||||
|
position: absolute;
|
||||||
|
right: -1px;
|
||||||
|
bottom: -1px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 0 2px #fff;
|
||||||
|
color: v-bind(maincolor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
233
src/renderer/src/components/Settings/DesktopLyricStyle.vue
Normal file
233
src/renderer/src/components/Settings/DesktopLyricStyle.vue
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
|
||||||
|
interface LyricOption {
|
||||||
|
fontSize: number
|
||||||
|
mainColor: string
|
||||||
|
shadowColor: string
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const option = ref<LyricOption>({
|
||||||
|
fontSize: 30,
|
||||||
|
mainColor: '#73BCFC',
|
||||||
|
shadowColor: 'rgba(255, 255, 255, 0.5)',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 800,
|
||||||
|
height: 180
|
||||||
|
})
|
||||||
|
|
||||||
|
const shadowRgb = ref<{ r: number; g: number; b: number }>({ r: 255, g: 255, b: 255 })
|
||||||
|
const mainHex = ref<string>('#73BCFC')
|
||||||
|
const shadowColorStr = ref<string>('rgba(255, 255, 255, 0.5')
|
||||||
|
|
||||||
|
const parseColorToRgb = (input: string) => {
|
||||||
|
const rgbaMatch = input?.match(/rgba?\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i)
|
||||||
|
if (rgbaMatch) {
|
||||||
|
return { r: Number(rgbaMatch[1]), g: Number(rgbaMatch[2]), b: Number(rgbaMatch[3]) }
|
||||||
|
}
|
||||||
|
const hexMatch = input?.match(/^#([0-9a-f]{6})$/i)
|
||||||
|
if (hexMatch) {
|
||||||
|
const hex = hexMatch[1]
|
||||||
|
return {
|
||||||
|
r: parseInt(hex.slice(0, 2), 16),
|
||||||
|
g: parseInt(hex.slice(2, 4), 16),
|
||||||
|
b: parseInt(hex.slice(4, 6), 16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { r: 255, g: 255, b: 255 }
|
||||||
|
}
|
||||||
|
const hexToRgb = (hex: string) => {
|
||||||
|
const m = hex?.match(/^#([0-9a-f]{6})$/i)
|
||||||
|
if (!m) return { r: 255, g: 255, b: 255 }
|
||||||
|
const h = m[1]
|
||||||
|
return {
|
||||||
|
r: parseInt(h.slice(0, 2), 16),
|
||||||
|
g: parseInt(h.slice(2, 4), 16),
|
||||||
|
b: parseInt(h.slice(4, 6), 16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const rgbToHex = (r: number, g: number, b: number) =>
|
||||||
|
`#${[r, g, b].map((v) => v.toString(16).padStart(2, '0')).join('')}`
|
||||||
|
const buildShadowRgba = () =>
|
||||||
|
`rgba(${shadowRgb.value.r}, ${shadowRgb.value.g}, ${shadowRgb.value.b}, 0.5)`
|
||||||
|
|
||||||
|
const onMainColorChange = (val: string) => {
|
||||||
|
mainHex.value = val
|
||||||
|
option.value.mainColor = val
|
||||||
|
}
|
||||||
|
const onShadowColorChange = (val: string) => {
|
||||||
|
shadowColorStr.value = val
|
||||||
|
option.value.shadowColor = val
|
||||||
|
}
|
||||||
|
|
||||||
|
const original = ref<LyricOption | null>(null)
|
||||||
|
|
||||||
|
const loadOption = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await window.electron.ipcRenderer.invoke('get-desktop-lyric-option')
|
||||||
|
if (res) {
|
||||||
|
option.value = { ...option.value, ...res }
|
||||||
|
original.value = { ...option.value }
|
||||||
|
mainHex.value = option.value.mainColor || '#73BCFC'
|
||||||
|
shadowColorStr.value = option.value.shadowColor || 'rgba(255,255,255,0.5)'
|
||||||
|
shadowRgb.value = parseColorToRgb(shadowColorStr.value)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('加载桌面歌词配置失败:', e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyOption = () => {
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
// 传入 callback=true 让桌面歌词窗口即时更新
|
||||||
|
const payload = { ...option.value, shadowColor: shadowColorStr.value }
|
||||||
|
window.electron.ipcRenderer.send('set-desktop-lyric-option', payload, true)
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => (saving.value = false), 200)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetOption = () => {
|
||||||
|
if (!original.value) return
|
||||||
|
option.value = { ...original.value }
|
||||||
|
applyOption()
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleDesktopLyric = (enabled: boolean) => {
|
||||||
|
window.electron.ipcRenderer.send('change-desktop-lyric', enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadOption()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="lyric-style">
|
||||||
|
<div class="header">
|
||||||
|
<h3>桌面歌词样式</h3>
|
||||||
|
<p>自定义桌面歌词的字体大小、颜色与阴影效果,并可预览与即时应用。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<div class="row">
|
||||||
|
<div class="field">
|
||||||
|
<label>字体大小(px)</label>
|
||||||
|
<t-input-number v-model="option.fontSize" :min="12" :max="96" :step="1" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>主颜色</label>
|
||||||
|
<t-color-picker
|
||||||
|
v-model="mainHex"
|
||||||
|
:color-modes="['monochrome']"
|
||||||
|
format="HEX"
|
||||||
|
@change="onMainColorChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>阴影颜色</label>
|
||||||
|
<t-color-picker
|
||||||
|
v-model="shadowColorStr"
|
||||||
|
:color-modes="['monochrome']"
|
||||||
|
format="RGBA"
|
||||||
|
:enable-alpha="true"
|
||||||
|
@change="onShadowColorChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="field">
|
||||||
|
<label>宽度</label>
|
||||||
|
<t-input-number v-model="option.width" :min="300" :max="1600" :step="10" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>高度</label>
|
||||||
|
<t-input-number v-model="option.height" :min="100" :max="600" :step="10" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<t-button :loading="loading" theme="default" variant="outline" @click="loadOption"
|
||||||
|
>刷新</t-button
|
||||||
|
>
|
||||||
|
<t-button :loading="saving" theme="primary" @click="applyOption">应用到桌面歌词</t-button>
|
||||||
|
<t-button theme="default" @click="resetOption">还原</t-button>
|
||||||
|
<t-switch @change="toggleDesktopLyric($event as boolean)">显示桌面歌词</t-switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="preview">
|
||||||
|
<div
|
||||||
|
class="preview-lyric"
|
||||||
|
:style="{
|
||||||
|
fontSize: option.fontSize + 'px',
|
||||||
|
color: mainHex,
|
||||||
|
textShadow: `0 0 6px ${shadowColorStr}`
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
这是桌面歌词预览
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.lyric-style {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.header h3 {
|
||||||
|
margin: 0 0 6px 0;
|
||||||
|
}
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.color-input {
|
||||||
|
width: 48px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid var(--td-border-level-1-color);
|
||||||
|
border-radius: var(--td-radius-small);
|
||||||
|
background: var(--td-bg-color-container);
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.preview {
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px dashed var(--td-border-level-1-color);
|
||||||
|
border-radius: var(--td-radius-medium);
|
||||||
|
background: var(--settings-preview-bg);
|
||||||
|
}
|
||||||
|
.preview-lyric {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -18,6 +18,7 @@ import DirectorySettings from '@renderer/components/Settings/DirectorySettings.v
|
|||||||
import MusicCache from '@renderer/components/Settings/MusicCache.vue'
|
import MusicCache from '@renderer/components/Settings/MusicCache.vue'
|
||||||
import AIFloatBallSettings from '@renderer/components/Settings/AIFloatBallSettings.vue'
|
import AIFloatBallSettings from '@renderer/components/Settings/AIFloatBallSettings.vue'
|
||||||
import ThemeSelector from '@renderer/components/ThemeSelector.vue'
|
import ThemeSelector from '@renderer/components/ThemeSelector.vue'
|
||||||
|
import DesktopLyricStyle from '@renderer/components/Settings/DesktopLyricStyle.vue'
|
||||||
import Versions from '@renderer/components/Versions.vue'
|
import Versions from '@renderer/components/Versions.vue'
|
||||||
import { useAutoUpdate } from '@renderer/composables/useAutoUpdate'
|
import { useAutoUpdate } from '@renderer/composables/useAutoUpdate'
|
||||||
import { playSetting as usePlaySetting } from '@renderer/store/playSetting'
|
import { playSetting as usePlaySetting } from '@renderer/store/playSetting'
|
||||||
@@ -419,6 +420,10 @@ const getTagOptionsStatus = () => {
|
|||||||
<h3>应用主题色</h3>
|
<h3>应用主题色</h3>
|
||||||
<ThemeSelector />
|
<ThemeSelector />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-group">
|
||||||
|
<DesktopLyricStyle />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- AI 功能设置 -->
|
<!-- AI 功能设置 -->
|
||||||
|
|||||||
541
src/web/lyric.html
Normal file
541
src/web/lyric.html
Normal file
@@ -0,0 +1,541 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>澜音 - 桌面歌词</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
user-select: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--font-size: 30;
|
||||||
|
--main-color: #73BCFC;
|
||||||
|
--shadow-color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--main-color);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
z-index: 0;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: move;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
&::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
.meta {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.lock-lyric {
|
||||||
|
cursor: none;
|
||||||
|
/* 鼠标穿透 */
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
* {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
.meta {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.9;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
transform 0.3s,
|
||||||
|
background-color 0.3s;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#song-artist {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 12px;
|
||||||
|
margin: 12px;
|
||||||
|
z-index: 1;
|
||||||
|
max-width: 100%;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
#lyric-text {
|
||||||
|
font-size: calc(var(--font-size) * 1px);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#lyric-tran {
|
||||||
|
font-size: calc(var(--font-size) * 1px - 5px);
|
||||||
|
margin-top: 8px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
padding: 0 4px;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-shadow: 0 0 4px var(--shadow-color);
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
/* animation: 15s wordsLoop linear infinite normal; */
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes wordsLoop {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0px);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div class="meta">
|
||||||
|
<span id="song-name">CeruMusic(澜音)</span>
|
||||||
|
<span id="song-artist">未知艺术家</span>
|
||||||
|
</div>
|
||||||
|
<div class="tools" id="tools">
|
||||||
|
<div id="show-app" class="item" title="打开应用">
|
||||||
|
<svg
|
||||||
|
width="1200"
|
||||||
|
height="1200"
|
||||||
|
viewBox="0 0 1200 1200"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<rect width="1200" height="1200" rx="379" fill="white" />
|
||||||
|
<path
|
||||||
|
d="M957.362 204.197C728.535 260.695 763.039 192.264 634.41 175.368C451.817 151.501 504.125 315.925 504.125 315.925L630.545 673.497C591.211 654.805 544.287 643.928 494.188 643.928C353.275 643.928 239 729.467 239 834.964C239 940.567 353.137 1026 494.188 1026C635.1 1026 749.375 940.461 749.375 834.964C749.375 832.218 749.237 829.473 749.099 826.727C749.513 825.988 749.789 825.143 750.065 824.087C757.932 789.449 634.272 348.345 634.272 348.345C634.272 348.345 764.971 401.886 860.89 351.936C971.163 294.699 964.953 202.402 957.362 204.197Z"
|
||||||
|
fill="url(#paint0_linear_4_16)"
|
||||||
|
stroke="#29293A"
|
||||||
|
stroke-opacity="0.23"
|
||||||
|
/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient
|
||||||
|
id="paint0_linear_4_16"
|
||||||
|
x1="678.412"
|
||||||
|
y1="-1151.29"
|
||||||
|
x2="796.511"
|
||||||
|
y2="832.071"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop offset="0.572115" stop-color="#B8F1ED" />
|
||||||
|
<stop offset="0.9999" stop-color="#B8F1CC" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div id="font-size-reduce" class="item" title="缩小字体">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M10.5 7h-2L3 21h2.2l1.1-3h6.2l1.1 3H16zm-3.4 9l2.4-6.3l2.4 6.3zM22 7h-8V5h8z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div id="font-size-add" class="item" title="放大字体">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M8.5 7h2L16 21h-2.4l-1.1-3H6.3l-1.1 3H3zm-1.4 9h4.8L9.5 9.7zM22 5v2h-3v3h-2V7h-3V5h3V2h2v3z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div id="play-prev" class="item" title="上一首">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M7 6c.55 0 1 .45 1 1v10c0 .55-.45 1-1 1s-1-.45-1-1V7c0-.55.45-1 1-1m3.66 6.82l5.77 4.07c.66.47 1.58-.01 1.58-.82V7.93c0-.81-.91-1.28-1.58-.82l-5.77 4.07a1 1 0 0 0 0 1.64"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<!-- 播放暂停 -->
|
||||||
|
<div id="pause" class="item hidden" title="暂停">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M8 19c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2s-2 .9-2 2v10c0 1.1.9 2 2 2m6-12v10c0 1.1.9 2 2 2s2-.9 2-2V7c0-1.1-.9-2-2-2s-2 .9-2 2"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div id="play" class="item" title="播放">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M8 6.82v10.36c0 .79.87 1.27 1.54.84l8.14-5.18a1 1 0 0 0 0-1.69L9.54 5.98A.998.998 0 0 0 8 6.82"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div id="play-next" class="item" title="下一首">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="m7.58 16.89l5.77-4.07c.56-.4.56-1.24 0-1.63L7.58 7.11C6.91 6.65 6 7.12 6 7.93v8.14c0 .81.91 1.28 1.58.82M16 7v10c0 .55.45 1 1 1s1-.45 1-1V7c0-.55-.45-1-1-1s-1 .45-1 1"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<!-- 锁定 -->
|
||||||
|
<div id="lock-lyric" class="item" title="锁定/解锁">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2M9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9zm9 14H6V10h12zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2s-2 .9-2 2s.9 2 2 2"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<!-- 关闭 -->
|
||||||
|
<div id="close-lyric" class="item" title="关闭">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M13.46 12L19 17.54V19h-1.46L12 13.46L6.46 19H5v-1.46L10.54 12L5 6.46V5h1.46L12 10.54L17.54 5H19v1.46z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main id="lyric-content">
|
||||||
|
<span id="lyric-text">该歌曲暂无歌词</span>
|
||||||
|
<span id="lyric-tran"></span>
|
||||||
|
</main>
|
||||||
|
<script>
|
||||||
|
class LyricsWindow {
|
||||||
|
constructor() {
|
||||||
|
// 获取元素
|
||||||
|
this.songNameDom = document.getElementById('song-name')
|
||||||
|
this.songArtistDom = document.getElementById('song-artist')
|
||||||
|
this.lyricContentDom = document.getElementById('lyric-content')
|
||||||
|
this.lyricTextDom = document.getElementById('lyric-text')
|
||||||
|
this.lyricTranDom = document.getElementById('lyric-tran')
|
||||||
|
this.pauseDom = document.getElementById('pause')
|
||||||
|
this.playDom = document.getElementById('play')
|
||||||
|
// 窗口位置
|
||||||
|
this.isDragging = false
|
||||||
|
this.startX = 0
|
||||||
|
this.startY = 0
|
||||||
|
this.startWinX = 0
|
||||||
|
this.startWinY = 0
|
||||||
|
this.winWidth = 0
|
||||||
|
this.winHeight = 0
|
||||||
|
// 临时变量
|
||||||
|
// this.lyricIndex = -1;
|
||||||
|
// 初始化
|
||||||
|
this.restoreOptions()
|
||||||
|
this.menuClick()
|
||||||
|
this.setupIPCListeners()
|
||||||
|
this.setupWindowDragListeners()
|
||||||
|
this.setupMutationObserver()
|
||||||
|
}
|
||||||
|
// 歌词切换动画
|
||||||
|
updateLyrics(content = '纯音乐,请欣赏', translation = '') {
|
||||||
|
// document.startViewTransition(() => {
|
||||||
|
// this.lyricTextDom.innerHTML = content;
|
||||||
|
// this.lyricTranDom.innerHTML = translation;
|
||||||
|
// });
|
||||||
|
this.lyricTextDom.innerHTML = content
|
||||||
|
this.lyricTranDom.innerHTML = translation
|
||||||
|
}
|
||||||
|
// 获取配置
|
||||||
|
async restoreOptions() {
|
||||||
|
try {
|
||||||
|
const defaultOptions = await window.electron.ipcRenderer.invoke(
|
||||||
|
'get-desktop-lyric-option'
|
||||||
|
)
|
||||||
|
if (defaultOptions) this.changeOptions(defaultOptions)
|
||||||
|
return defaultOptions
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to restore options:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 修改配置
|
||||||
|
changeOptions(options, callback = true) {
|
||||||
|
if (!options) return
|
||||||
|
const { fontSize, mainColor, shadowColor } = options
|
||||||
|
document.documentElement.style.setProperty('--font-size', fontSize)
|
||||||
|
document.documentElement.style.setProperty('--main-color', mainColor)
|
||||||
|
document.documentElement.style.setProperty('--shadow-color', shadowColor)
|
||||||
|
if (callback) window.electron.ipcRenderer.send('set-desktop-lyric-option', options)
|
||||||
|
}
|
||||||
|
// 菜单点击事件
|
||||||
|
menuClick() {
|
||||||
|
const toolsDom = document.getElementById('tools')
|
||||||
|
if (!toolsDom) return
|
||||||
|
// 菜单项点击
|
||||||
|
toolsDom.addEventListener('click', async (event) => {
|
||||||
|
const target = event.target.closest('div')
|
||||||
|
if (!target) return
|
||||||
|
console.log(target)
|
||||||
|
const id = target.id
|
||||||
|
if (!id) return
|
||||||
|
// 获取配置
|
||||||
|
const options = await this.restoreOptions()
|
||||||
|
switch (id) {
|
||||||
|
case 'show-app': {
|
||||||
|
window.electron.ipcRenderer.send('win-show')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'font-size-add': {
|
||||||
|
let fontSize = options.fontSize
|
||||||
|
if (fontSize < 60) {
|
||||||
|
fontSize++
|
||||||
|
this.changeOptions({ ...options, fontSize })
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'font-size-reduce': {
|
||||||
|
let fontSize = options.fontSize
|
||||||
|
if (fontSize > 10) {
|
||||||
|
fontSize--
|
||||||
|
this.changeOptions({ ...options, fontSize })
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'play': {
|
||||||
|
window.electron.ipcRenderer.send('send-main-event', 'play')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'pause': {
|
||||||
|
window.electron.ipcRenderer.send('send-main-event', 'pause')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'play-prev': {
|
||||||
|
window.electron.ipcRenderer.send('send-main-event', 'playPrev')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'play-next': {
|
||||||
|
window.electron.ipcRenderer.send('send-main-event', 'playNext')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'close-lyric': {
|
||||||
|
window.electron.ipcRenderer.send('closeDesktopLyric')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'lock-lyric': {
|
||||||
|
const locked = !document.body.classList.contains('lock-lyric')
|
||||||
|
document.body.classList.toggle('lock-lyric', locked)
|
||||||
|
window.electron.ipcRenderer.send('toogleDesktopLyricLock', locked)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 监听 IPC 事件
|
||||||
|
setupIPCListeners() {
|
||||||
|
window.electron.ipcRenderer.on('play-song-change', (_, title) => {
|
||||||
|
if (!title) return
|
||||||
|
const [songName, songArtist] = title.split(' - ')
|
||||||
|
this.songNameDom.innerHTML = songName
|
||||||
|
this.songArtistDom.innerHTML = songArtist
|
||||||
|
this.updateLyrics(title)
|
||||||
|
})
|
||||||
|
|
||||||
|
window.electron.ipcRenderer.on('play-lyric-change', (_, lyricData) => {
|
||||||
|
if (!lyricData) return
|
||||||
|
this.parsedLyricsData(lyricData)
|
||||||
|
})
|
||||||
|
|
||||||
|
window.electron.ipcRenderer.on('play-status-change', (_, status) => {
|
||||||
|
this.playDom.classList.toggle('hidden', status)
|
||||||
|
this.pauseDom.classList.toggle('hidden', !status)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 配置变化
|
||||||
|
window.electron.ipcRenderer.on('desktop-lyric-option-change', (_, options) => {
|
||||||
|
this.changeOptions(options, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 歌词锁定(仅更新样式,不再回传,避免事件循环)
|
||||||
|
window.electron.ipcRenderer.on('toogleDesktopLyricLock', (_, lock) => {
|
||||||
|
document.body.classList.toggle('lock-lyric', lock)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 解析歌词
|
||||||
|
parsedLyricsData(lyricData) {
|
||||||
|
if (!this.lyricContentDom || !this.lyricTextDom) return
|
||||||
|
const { index, lyric } = lyricData
|
||||||
|
// 更换文字
|
||||||
|
if (!lyric || index < 0) {
|
||||||
|
if (lyric.length === 0) this.updateLyrics()
|
||||||
|
} else {
|
||||||
|
const { content, tran } = lyric[index]
|
||||||
|
this.updateLyrics(content, tran || '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 拖拽窗口
|
||||||
|
setupWindowDragListeners() {
|
||||||
|
document.addEventListener('mousedown', this.startDrag.bind(this))
|
||||||
|
document.addEventListener('mousemove', this.dragWindow.bind(this))
|
||||||
|
document.addEventListener('mouseup', this.endDrag.bind(this))
|
||||||
|
}
|
||||||
|
// 开始拖拽
|
||||||
|
async startDrag(event) {
|
||||||
|
this.isDragging = true
|
||||||
|
const { screenX, screenY } = event
|
||||||
|
const {
|
||||||
|
x: winX,
|
||||||
|
y: winY,
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
} = await window.electron.ipcRenderer.invoke('get-window-bounds')
|
||||||
|
this.startX = screenX
|
||||||
|
this.startY = screenY
|
||||||
|
this.startWinX = winX
|
||||||
|
this.startWinY = winY
|
||||||
|
this.winWidth = width
|
||||||
|
this.winHeight = height
|
||||||
|
}
|
||||||
|
// 拖拽
|
||||||
|
async dragWindow(event) {
|
||||||
|
if (!this.isDragging) return
|
||||||
|
const { screenX, screenY } = event
|
||||||
|
let newWinX = this.startWinX + (screenX - this.startX)
|
||||||
|
let newWinY = this.startWinY + (screenY - this.startY)
|
||||||
|
const { width: screenWidth, height: screenHeight } =
|
||||||
|
await window.electron.ipcRenderer.invoke('get-screen-size')
|
||||||
|
newWinX = Math.max(0, Math.min(screenWidth - this.winWidth, newWinX))
|
||||||
|
newWinY = Math.max(0, Math.min(screenHeight - this.winHeight, newWinY))
|
||||||
|
window.electron.ipcRenderer.send(
|
||||||
|
'move-window',
|
||||||
|
newWinX,
|
||||||
|
newWinY,
|
||||||
|
this.winWidth,
|
||||||
|
this.winHeight
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// 结束拖拽
|
||||||
|
endDrag() {
|
||||||
|
this.isDragging = false
|
||||||
|
}
|
||||||
|
// 更新高度
|
||||||
|
updateWindowHeight() {
|
||||||
|
const bodyHeight = document.body.scrollHeight
|
||||||
|
window.electron.ipcRenderer.send('update-window-height', bodyHeight)
|
||||||
|
}
|
||||||
|
// 动态监听高度
|
||||||
|
setupMutationObserver() {
|
||||||
|
const observer = new MutationObserver(this.updateWindowHeight.bind(this))
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true, attributes: true })
|
||||||
|
this.updateWindowHeight()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new LyricsWindow()
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user