Files
CeruMusic/src/main/index.ts

347 lines
9.9 KiB
TypeScript
Raw Normal View History

import {
app,
shell,
BrowserWindow,
ipcMain,
screen,
Rectangle,
Display,
Tray,
Menu
} from 'electron'
import { configManager } from './services/ConfigManager'
2025-08-13 13:33:43 +08:00
import { join } from 'path'
2025-08-19 22:10:35 +08:00
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
2025-08-13 22:39:11 +08:00
import icon from '../../resources/logo.png?asset'
import path from 'node:path'
import InitEventServices from './events'
import lyricWindow from './windows/lyric-window'
import './events/musicCache'
import './events/songList'
import './events/directorySettings'
import './events/pluginNotice'
import initLyricIpc from './events/lyric'
// 获取单实例锁
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
// 如果没有获得锁,说明已经有实例在运行,退出当前实例
app.quit()
} else {
// 当第二个实例尝试启动时,聚焦到第一个实例的窗口
app.on('second-instance', () => {
// 如果有窗口存在,聚焦到该窗口
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore()
if (!mainWindow.isVisible()) mainWindow.show()
mainWindow.focus()
}
})
}
2025-08-19 21:33:43 +08:00
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
})
}
2025-08-16 11:34:37 +08:00
/**
*
*
* @param {BrowserWindow} win -
*/
function updateWindowMaxLimits(win: BrowserWindow | null): void {
if (!win) return
// 1. 获取窗口的当前边界 (bounds)
const currentBounds: Rectangle = win.getBounds()
// 2. 查找包含该边界的显示器
const currentDisplay: Display = screen.getDisplayMatching(currentBounds)
// 3. 获取该显示器的完整尺寸 (full screen size)
const { width: currentScreenWidth, height: currentScreenHeight } = currentDisplay.size
// 4. 应用新的最大尺寸限制
// 移除 maxWidth/maxHeight 上的硬限制,使其能够最大化到当前屏幕的尺寸。
// 注意:设置为 0, 0 意味着没有最小限制,我们只关注最大限制。
win.setMaximumSize(currentScreenWidth, currentScreenHeight)
}
2025-08-13 13:33:43 +08:00
function createWindow(): void {
// 获取保存的窗口位置和大小
const savedBounds = configManager.getWindowBounds()
// 默认窗口配置
const defaultOptions = {
2025-08-16 11:34:37 +08:00
width: 1100,
height: 750,
2025-09-05 20:58:20 +08:00
minWidth: 1100,
2025-08-16 11:34:37 +08:00
minHeight: 670,
2025-08-13 13:33:43 +08:00
show: false,
center: !savedBounds, // 如果有保存的位置,则不居中
2025-08-13 13:33:43 +08:00
autoHideMenuBar: true,
titleBarStyle: 'hidden' as const,
2025-08-13 13:33:43 +08:00
...(process.platform === 'linux' ? { icon } : {}),
2025-08-18 20:42:34 +08:00
icon: path.join(__dirname, '../../resources/logo.ico'),
2025-08-13 13:33:43 +08:00
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
webSecurity: false,
nodeIntegration: true,
contextIsolation: false,
backgroundThrottling: false
2025-08-13 13:33:43 +08:00
}
}
// 如果有保存的窗口位置和大小,则使用保存的值
if (savedBounds) {
Object.assign(defaultOptions, savedBounds)
}
// Create the browser window.
mainWindow = new BrowserWindow(defaultOptions)
2025-08-16 11:34:37 +08:00
if (process.platform == 'darwin') mainWindow.setWindowButtonVisibility(false)
2025-08-13 13:33:43 +08:00
// ⚠️ 关键修改 2: 监听 'moved' 事件,动态更新最大尺寸
mainWindow.on('moved', () => {
// 当窗口移动时,确保最大尺寸限制随屏幕变化
updateWindowMaxLimits(mainWindow)
if (mainWindow && !mainWindow.isMaximized() && !mainWindow.isFullScreen()) {
const bounds = mainWindow.getBounds()
configManager.saveWindowBounds(bounds)
}
})
// ⚠️ 关键修改 3: 窗口创建后立即应用一次最大尺寸限制
updateWindowMaxLimits(mainWindow)
mainWindow.on('resized', () => {
if (mainWindow && !mainWindow.isMaximized() && !mainWindow.isFullScreen()) {
const bounds = mainWindow.getBounds()
// 获取当前屏幕尺寸 (已在文件顶部导入 screen无需 require)
const currentDisplay = screen.getDisplayMatching(bounds)
// 使用 workAreaSize 避免窗口超出任务栏/Dock
const { width: screenWidth, height: screenHeight } = currentDisplay.workAreaSize
// 确保窗口不超过屏幕工作区域尺寸
let needResize = false
const newBounds = { ...bounds }
if (bounds.width > screenWidth) {
newBounds.width = screenWidth
needResize = true
}
if (bounds.height > screenHeight) {
newBounds.height = screenHeight
needResize = true
}
// 如果需要调整大小,应用新的尺寸
if (needResize) {
mainWindow.setBounds(newBounds)
}
configManager.saveWindowBounds(newBounds)
}
})
2025-08-13 13:33:43 +08:00
mainWindow.on('ready-to-show', () => {
2025-08-16 11:34:37 +08:00
mainWindow?.show()
})
2025-08-13 13:33:43 +08:00
mainWindow.webContents.setWindowOpenHandler((details) => {
2025-08-19 19:12:06 +08:00
shell.openExternal(details.url).then()
2025-08-13 13:33:43 +08:00
return { action: 'deny' }
})
InitEventServices(mainWindow)
2025-08-13 13:33:43 +08:00
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
2025-08-19 19:12:06 +08:00
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']).then()
2025-08-13 13:33:43 +08:00
} else {
2025-08-19 19:12:06 +08:00
mainWindow.loadFile(join(__dirname, '../renderer/index.html')).then()
2025-08-13 13:33:43 +08:00
}
}
2025-08-27 13:29:49 +08:00
import { registerAutoUpdateEvents, initAutoUpdateForWindow } from './events/autoUpdate'
2025-08-19 22:10:35 +08:00
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
2025-08-30 05:06:14 +08:00
app.whenReady().then(() => {
// Set app user model id for windows - 确保与 electron-builder.yml 中的 appId 一致
electronApp.setAppUserModelId('com.cerumusic.app')
// 在 Windows 上设置应用程序名称,帮助 SMTC 识别
if (process.platform === 'win32') {
app.setAppUserModelId('com.cerumusic.app')
// 设置应用程序名称
app.setName('澜音')
}
2025-08-19 22:10:35 +08:00
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
2025-08-13 13:33:43 +08:00
createWindow()
lyricWindow.create()
initLyricIpc(mainWindow)
// 仅在主进程初始化一次托盘
setupTray()
2025-08-27 13:29:49 +08:00
// 注册自动更新事件
registerAutoUpdateEvents()
ipcMain.on('startPing', () => {
if (ping) clearInterval(ping)
console.log('start-----开始')
startPing()
})
ipcMain.on('stopPing', () => {
clearInterval(ping)
})
2025-08-27 13:29:49 +08:00
// 初始化自动更新器
if (mainWindow) {
initAutoUpdateForWindow(mainWindow)
}
2025-08-19 22:10:35 +08:00
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
// 当所有窗口关闭时不退出应用,因为我们有系统托盘
app.on('window-all-closed', () => {
// 保持应用常驻,通过系统托盘管理
2025-08-13 13:33:43 +08:00
})
2025-08-19 22:10:35 +08:00
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
let ping: NodeJS.Timeout
function startPing() {
// 已迁移到 Howler不再使用 DOM <audio> 轮询。
// 如需主进程感知播放结束,请在渲染进程通过 IPC 主动通知。
if (ping) {
clearInterval(ping)
}
ping = setInterval(() => {
// 保留占位,避免调用方报错;不再做任何轮询。
// 可在此处监听自定义 IPC 事件以扩展行为。
clearInterval(ping)
}, 1000)
}