/** * 窗口管理模块 * 负责创建、配置和管理主窗口 */ const { BrowserWindow, screen, Menu, ipcMain, shell } = require('electron'); const path = require('path'); const fs = require('fs'); const i18next = require('./i18n'); // 全局变量 let mainWindow = null; let unlockWindow = null; // 解锁窗口 let contextMenu = null; // 存储右键菜单实例 // 窗口配置文件路径 let configPath = null; /** * 初始化窗口配置 * @param {string} userDataPath 用户数据目录路径 */ function initConfig(userDataPath) { configPath = path.join(userDataPath, 'window-config.json'); } /** * 加载窗口配置 * @returns {Object|null} 窗口配置对象或null */ function loadWindowConfig() { try { if (fs.existsSync(configPath)) { const data = fs.readFileSync(configPath, 'utf8'); const config = JSON.parse(data); console.log('加载窗口配置成功:', config); // 如果配置中有语言设置,更新i18next if (config.language) { console.log('更新i18next语言:', config.language); i18next.changeLanguage(config.language); } return config; } } catch (error) { console.error('加载窗口配置失败:', error); } return null; } /** * 保存窗口配置 * @param {Object} bounds 窗口边界信息 */ function saveWindowConfig(bounds) { try { // 合并窗口位置和语言设置 const config = { ...bounds, language: i18next.language }; fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); console.log('保存窗口配置成功:', config); } catch (error) { console.error('保存窗口配置失败:', error); } } /** * 处理静态资源路径 * @param {string} htmlContent HTML内容 * @returns {string} 修正后的HTML内容 */ function fixStaticAssetsPaths(htmlContent) { // 将绝对路径(/assets/)替换为相对路径(./assets/) const fixedContent = htmlContent .replace(/src="\/assets\//g, 'src="./assets/') .replace(/href="\/assets\//g, 'href="./assets/') .replace(/href="\/favicon.svg"/g, 'href="./favicon.svg"'); console.log('已修复资源路径引用'); return fixedContent; } /** * 创建主窗口 * @param {string} frontendUrl 前端URL * @param {number} frontendPort 前端端口 * @param {number} backendPort 后端端口 * @param {boolean} isDev 是否为开发模式 * @returns {BrowserWindow} 创建的主窗口 */ function createWindow(frontendUrl, frontendPort, backendPort, isDev) { console.log('开始创建主窗口...'); // 加载窗口配置 const config = loadWindowConfig(); console.log('加载窗口配置成功:', config); // 提取窗口尺寸和位置 let windowWidth = 500; let windowHeight = 230; let x, y; let searchWindow = null; if (config) { windowWidth = config.width || windowWidth; windowHeight = config.height || windowHeight; x = config.x; y = config.y; } // 检查坐标是否有效 (针对多显示器情况) const displays = screen.getAllDisplays(); console.log(`系统有 ${displays.length} 个显示器`); let foundValidDisplay = false; if (x !== undefined && y !== undefined) { // 检查坐标是否在任一显示器范围内 for (const display of displays) { const { bounds } = display; if ( x >= bounds.x - windowWidth && x <= bounds.x + bounds.width && y >= bounds.y - windowHeight && y <= bounds.y + bounds.height ) { foundValidDisplay = true; break; } } // 如果坐标无效,使用主显示器中央 if (!foundValidDisplay) { const primaryDisplay = screen.getPrimaryDisplay(); x = Math.floor((primaryDisplay.workAreaSize.width - windowWidth) / 2); y = Math.floor((primaryDisplay.workAreaSize.height - windowHeight) / 2); } } else { // 如果没有保存的坐标,使用主显示器中央 const primaryDisplay = screen.getPrimaryDisplay(); x = Math.floor((primaryDisplay.workAreaSize.width - windowWidth) / 2); y = Math.floor((primaryDisplay.workAreaSize.height - windowHeight) / 2); } console.log(`设置窗口位置: x=${x}, y=${y}, 屏幕尺寸: ${screen.getPrimaryDisplay().workAreaSize.width}x${screen.getPrimaryDisplay().workAreaSize.height}`); // 创建浏览器窗口 mainWindow = new BrowserWindow({ width: windowWidth, height: windowHeight, x: x, y: y, frame: false, transparent: true, backgroundColor: '#00000000', hasShadow: false, // 禁用窗口阴影以防止透明边缘出现灰色 titleBarStyle: 'hidden', alwaysOnTop: true, show: false, // 初始不显示,等待加载完成后显示 webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, '..', 'preload.js'), } }); // 确保窗口在屏幕上可见,设置居中位置 mainWindow.setPosition(x, y); console.log(`设置窗口位置: x=${x}, y=${y}, 屏幕尺寸: ${screen.getPrimaryDisplay().workAreaSize.width}x${screen.getPrimaryDisplay().workAreaSize.height}`); console.log('窗口创建成功,现在设置窗口事件...'); // 确保窗口及其内容完全透明 mainWindow.webContents.on('did-finish-load', () => { // 设置背景色为透明 mainWindow.webContents.insertCSS(` html, body { background-color: transparent !important; background: transparent !important; } `); }); // 设置窗口永远在最上层,使用screen-saver级别确保在所有空间可见 mainWindow.setAlwaysOnTop(true, 'screen-saver', 1); // 在macOS上隐藏流量灯按钮 if (process.platform === 'darwin') { // 完全隐藏所有默认窗口控件 mainWindow.setWindowButtonVisibility(false); mainWindow.setMovable(true); // macOS 窗口设置 try { // 部分 Electron 版本可能不支持这个 API if (typeof mainWindow.setConstraints === 'function') { mainWindow.setConstraints({ minWidth: null, minHeight: null }); } } catch (e) { console.log('无法设置窗口约束,使用默认设置'); } } // 加载前端页面 if (isDev) { // 开发模式: 加载本地开发服务器 frontendUrl = `http://localhost:${frontendPort}/`; // 检查开发服务器是否在运行 const http = require('http'); const checkDevServer = () => { http.get(frontendUrl, (res) => { if (res.statusCode === 200) { mainWindow.loadURL(frontendUrl); } else { loadBuiltFiles(mainWindow); } }).on('error', () => { loadBuiltFiles(mainWindow); }); }; // 尝试连接开发服务器 checkDevServer(); } else { loadBuiltFiles(mainWindow); } // 设置后端URL global.backendUrl = `http://127.0.0.1:${backendPort}`; // 创建右键菜单 contextMenu = Menu.buildFromTemplate(getMenuTemplate()); // 添加右键菜单事件监听 mainWindow.webContents.on('context-menu', (e, params) => { e.preventDefault(); contextMenu.popup({ window: mainWindow }); }); // 阻止窗口导航到外部链接 mainWindow.webContents.on('will-navigate', (event, url) => { if (!url.startsWith(frontendUrl)) { event.preventDefault(); shell.openExternal(url); } }); // 创建关闭按钮window createUnlockWindow() return mainWindow; /** * 获取翻译后的菜单模板 * @returns {Array} 菜单模板数组 */ function getMenuTemplate() { return [ { label: i18next.t('menu.lockWindow'), click: () => { console.log('锁定窗口 - 启用点击穿透'); if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.blur() mainWindow.webContents.send('toggle-lock-window', true); showUnlockWindow() } } }, { type: 'separator' }, { label: i18next.t('menu.refresh'), click: () => { mainWindow.reload(); } }, { label: i18next.t('menu.openDevTools'), click: () => { mainWindow.webContents.openDevTools(); } }, { label: i18next.t('menu.language'), submenu: [ { label: i18next.t('menu.chinese'), click: () => { i18next.changeLanguage('zh'); mainWindow.webContents.send('change-language', 'zh'); searchWindow && searchWindow.webContents.send('change-language', 'zh'); updateContextMenu(); } }, { label: i18next.t('menu.english'), click: () => { i18next.changeLanguage('en'); mainWindow.webContents.send('change-language', 'en'); searchWindow && searchWindow.webContents.send('change-language', 'en'); updateContextMenu(); } } ] }, { label: i18next.t('menu.deleteLyrics'), click: () => { mainWindow.webContents.send('delete-current-lyrics'); } }, { label: i18next.t('menu.searchLyrics'), click: () => { console.log('右键菜单:点击搜索歌词'); const searchWindowManager = require('../windows/search-window'); searchWindow = searchWindowManager.createWindow(loadBuiltFiles); } }, { type: 'separator' }, { label: i18next.t('menu.quit'), click: () => { require('electron').app.quit(); } } ]; } /** * 更新右键菜单 */ function updateContextMenu() { if (mainWindow && !mainWindow.isDestroyed()) { contextMenu = Menu.buildFromTemplate(getMenuTemplate()); } } // 加载构建好的前端文件 function loadBuiltFiles(window, search) { // 修正前端资源路径,匹配package.json中的配置 const frontendDistPath = isDev ? path.join(__dirname, '..', '..', 'frontend', 'dist') : path.join(process.resourcesPath, 'app'); console.log(`加载已构建的前端文件: ${frontendDistPath}`); try { // 检查路径是否存在 if (!fs.existsSync(frontendDistPath)) { console.error(`前端资源路径不存在: ${frontendDistPath}`); // 尝试列出可能的路径 if (!isDev) { console.log('资源路径内容:'); console.log(fs.readdirSync(process.resourcesPath)); } } // 读取index.html文件内容 const indexPath = path.join(frontendDistPath, 'index.html'); if (!fs.existsSync(indexPath)) { console.error(`HTML文件不存在: ${indexPath}`); return; } let htmlContent = fs.readFileSync(indexPath, 'utf8'); // 修复资源路径 htmlContent = fixStaticAssetsPaths(htmlContent); // 将修复后的HTML内容写入临时文件 const tempIndexPath = path.join(frontendDistPath, '_fixed_index.html'); fs.writeFileSync(tempIndexPath, htmlContent, 'utf8'); console.log(`已创建临时HTML文件: ${tempIndexPath}`); // 加载修复后的HTML文件 window.loadFile(tempIndexPath, { search }); } catch (err) { console.error('加载前端文件失败:', err); } } } /** * 创建解锁窗口 * @returns {BrowserWindow} 创建的解锁窗口 */ function createUnlockWindow() { // 获取主窗口的位置和尺寸 let x, y; if (mainWindow && !mainWindow.isDestroyed()) { const mainBounds = mainWindow.getBounds(); // 将解锁按钮定位在歌词窗口上方 x = mainBounds.x + mainBounds.width / 2 - 24; y = mainBounds.y + 24; } else { // 如果主窗口不可用,用屏幕中心位置 const { width, height } = screen.getPrimaryDisplay().workAreaSize; x = Math.floor(width / 2 - 24); y = Math.floor(height / 2 - 24); } // 创建解锁窗口 unlockWindow = new BrowserWindow({ width: 48, height: 48, x: x, y: y, frame: false, show: false, resizable: false, transparent: true, alwaysOnTop: true, backgroundColor: '#00000000', webPreferences: { nodeIntegration: true, contextIsolation: false } }); // 加载HTML文件 unlockWindow.loadFile(path.join(__dirname, '..', 'unlock.html')); // 禁用右键菜单 unlockWindow.webContents.on('context-menu', e => { e.preventDefault(); }); // 设置窗口永远在最上层,使用screen-saver级别确保在所有空间可见 unlockWindow.setAlwaysOnTop(true, 'screen-saver', 1); // 在macOS上设置可见于所有工作区 if (process.platform === 'darwin') { unlockWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); } return unlockWindow; } function showUnlockWindow() { if (unlockWindow) { if (mainWindow && !mainWindow.isDestroyed()) { const mainBounds = mainWindow.getBounds(); // 将解锁按钮定位在歌词窗口上方 const x = mainBounds.x + mainBounds.width / 2 - 24; const y = mainBounds.y + 24; unlockWindow.setPosition(x, y); unlockWindow.showInactive() mainWindow.setIgnoreMouseEvents(true); } } } function hideUnlockWindow() { if (unlockWindow) { unlockWindow.hide() } } /** * 创建加载窗口 * @returns {BrowserWindow} 创建的加载窗口 */ function createLoadingWindow() { // 获取主显示器尺寸 const { width, height } = screen.getPrimaryDisplay().workAreaSize; // 创建加载窗口 loadingWindow = new BrowserWindow({ width: 320, height: 280, x: Math.floor((width - 320) / 2), y: Math.floor((height - 280) / 2), frame: false, resizable: false, transparent: true, alwaysOnTop: true, webPreferences: { nodeIntegration: true, contextIsolation: false } }); // 加载HTML文件 loadingWindow.loadFile(path.join(__dirname, '..', 'loading.html')); // 禁用右键菜单 loadingWindow.webContents.on('context-menu', e => { e.preventDefault(); }); // 窗口准备好时发送当前语言设置 loadingWindow.webContents.on('did-finish-load', () => { // 先尝试从配置中读取语言设置 const config = loadWindowConfig(); console.log('加载窗口配置:', config); const currentLang = config?.language || i18next.language; console.log('发送当前语言到加载窗口:', currentLang); loadingWindow.webContents.send('change-language', currentLang); }); return loadingWindow; } /** * 更新加载进度 * @param {number} progress 进度百分比 (0-100) * @param {string} message 状态消息 */ function updateLoadingProgress(progress, message) { if (loadingWindow && !loadingWindow.isDestroyed()) { // 获取当前语言的消息 const messages = { en: { initializing: 'Initializing application...', loadingBackend: 'Loading backend service...', allocatingPorts: 'Allocating ports...', loadingFrontend: 'Loading frontend resources...', connecting: 'Connecting to Apple Music...', ready: 'Ready', loadingLyrics: 'Loading lyrics...' }, zh: { initializing: '正在初始化应用...', loadingBackend: '正在加载后端服务...', allocatingPorts: '正在分配端口...', loadingFrontend: '正在加载前端资源...', connecting: '正在连接 Apple Music...', ready: '准备就绪', loadingLyrics: '正在加载歌词...' } }; // 获取当前语言 const currentLang = i18next.language; const t = messages[currentLang] || messages.en; // 如果消息是预定义的键,则使用翻译 const translatedMessage = t[message] || message; loadingWindow.webContents.send('update-progress', { progress, message: translatedMessage }); } } /** * 关闭加载窗口 */ function closeLoadingWindow() { if (loadingWindow && !loadingWindow.isDestroyed()) { // 设置最终进度为100% updateLoadingProgress(100, 'ready'); // 获取主窗口和加载窗口的位置和大小 const loadingBounds = loadingWindow.getBounds(); // 确保主窗口已创建 if (!mainWindow) { // 如果主窗口不存在,直接关闭加载窗口 loadingWindow.close(); loadingWindow = null; return; } const mainBounds = mainWindow.getBounds(); // 动画的总帧数和当前帧 const totalFrames = 40; // 动画时长约为40帧,大约600-800ms let currentFrame = 0; // 窗口属性的起始和结束值 const startProps = { x: loadingBounds.x, y: loadingBounds.y, width: loadingBounds.width, height: loadingBounds.height, opacity: 1 }; const endProps = { x: mainBounds.x, y: mainBounds.y, width: mainBounds.width, height: mainBounds.height, opacity: 0.3 }; // 创建动画计时器 const animationTimer = setInterval(() => { currentFrame++; // 使用缓动函数计算当前帧的属性值 const progress = easeOutCubic(currentFrame / totalFrames); // 计算当前帧的窗口位置和大小 const newBounds = { x: Math.round(startProps.x + (endProps.x - startProps.x) * progress), y: Math.round(startProps.y + (endProps.y - startProps.y) * progress), width: Math.round(startProps.width + (endProps.width - startProps.width) * progress), height: Math.round(startProps.height + (endProps.height - startProps.height) * progress) }; // 计算当前帧的不透明度 const newOpacity = startProps.opacity + (endProps.opacity - startProps.opacity) * progress; // 应用新的边界和不透明度 loadingWindow.setBounds(newBounds); loadingWindow.setOpacity(newOpacity); // 动画完成后执行淡出并显示主窗口 if (currentFrame >= totalFrames) { clearInterval(animationTimer); // 先显示主窗口,让它可以在背景中显示 if (mainWindow && !mainWindow.isVisible()) { mainWindow.show(); } // 执行额外的淡出动画 let fadeFrame = 0; const fadeTotalFrames = 20; // 约300ms的淡出时间 const fadeTimer = setInterval(() => { fadeFrame++; // 使用easeInCubic让淡出渐渐加速 const fadeProgress = fadeFrame / fadeTotalFrames; const fadeOpacity = Math.max(0, endProps.opacity * (1 - fadeProgress)); loadingWindow.setOpacity(fadeOpacity); if (fadeFrame >= fadeTotalFrames) { clearInterval(fadeTimer); loadingWindow.close(); loadingWindow = null; } }, 15); } }, 16); // 约60fps的刷新率 // 缓动函数,使动画更自然 function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); } } } /** * 设置窗口事件监听 */ function setupWindowEvents() { if (!mainWindow) { console.error('无法设置窗口事件:窗口未创建'); return; } // 使用轮询方法检测鼠标是否在窗口上 let isMouseOverWindow = false; let pollInterval = null; // 开始轮询检测鼠标位置 const startMouseDetection = () => { if (pollInterval) { clearInterval(pollInterval); } pollInterval = setInterval(() => { if (!mainWindow) { // 如果窗口已关闭,停止轮询 if (pollInterval) { clearInterval(pollInterval); pollInterval = null; } return; } try { // 获取鼠标位置 const mousePos = screen.getCursorScreenPoint(); // 获取窗口位置和大小 const bounds = mainWindow.getBounds(); // 检查鼠标是否在窗口内 const isInWindow = ( mousePos.x >= bounds.x && mousePos.x <= bounds.x + bounds.width && mousePos.y >= bounds.y && mousePos.y <= bounds.y + bounds.height ); // 状态变化时才发送事件和更新点击穿透 if (isInWindow !== isMouseOverWindow) { isMouseOverWindow = isInWindow; // 通知渲染进程 - 无论是开发还是生产模式都需要 if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('mouse-hover-change', isMouseOverWindow); } } } catch (err) { console.error('鼠标检测错误:', err); } }, 100); // 每100毫秒检测一次,可根据需要调整 }; // 创建窗口后立即开始检测 startMouseDetection(); // 监听窗口位置变化事件,保存新位置 mainWindow.on('moved', function() { if (!mainWindow) return; const bounds = mainWindow.getBounds(); saveWindowConfig(bounds); }); // 窗口关闭前保存位置 mainWindow.on('close', function() { if (!mainWindow) return; const bounds = mainWindow.getBounds(); saveWindowConfig(bounds); }); // 窗口关闭时清除轮询 mainWindow.on('closed', () => { if (pollInterval) { clearInterval(pollInterval); pollInterval = null; } }); // 确保窗口在所有空间都可见(macOS特有) if (process.platform === 'darwin') { // 设置窗口可见于所有工作区 mainWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); } } /** * 设置IPC通信 * @param {number} backendPort 后端服务端口 * @param {number} frontendPort 前端服务端口 */ function setupIPC(backendPort, frontendPort) { // IPC 处理函数,提供详细输出便于调试 ipcMain.handle('get-backend-url', () => { const url = `http://127.0.0.1:${backendPort}`; console.log(`向前端提供后端URL: ${url}`); return url; }); // 处理IPC事件:获取前端URL ipcMain.handle('get-frontend-url', () => { return `http://localhost:${frontendPort}`; }); // 监听窗口位置和大小变化 ipcMain.on('save-window-bounds', (event, bounds) => { saveWindowConfig(bounds); }); // 监听最小化窗口请求 ipcMain.on('minimize-window', () => { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.minimize(); } }); // 监听退出应用请求 ipcMain.on('quit-app', () => { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.close(); } }); // 监听获取端口请求 ipcMain.handle('get-ports', async () => { return { backendPort, frontendPort }; }); // 监听加载窗口完成事件 ipcMain.on('loading-finished', () => { closeLoadingWindow(); }); // 处理窗口拖动 - 使用系统原生拖动 ipcMain.on('start-window-drag', () => { if (process.platform === 'darwin') { // 使用BrowserWindow.startWindowDrag() API (Electron 14+) try { console.log('使用Electron原生窗口拖动API'); mainWindow.webContents.executeJavaScript('document.documentElement.style.cursor = "grab";') .catch(err => console.error('设置光标样式失败:', err)); // 使用较新的原生API if (typeof mainWindow.startWindowDrag === 'function') { mainWindow.startWindowDrag(); } else { // 回退到老方法 mainWindow.setMovable(true); mainWindow.moveTop(); } } catch (error) { console.error('窗口拖动错误:', error); } } }); // 处理窗口锁定状态设置 ipcMain.on('set-window-lock-state', (_, locked) => { console.log('设置窗口锁定状态:', locked); // 确保锁定状态下窗口始终忽略鼠标事件 if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.setIgnoreMouseEvents(locked); } }); // 处理锁定时close window的渲染 ipcMain.on('set-close-window-state', (_, showLock) => { console.log('设置关闭按钮', showLock); if (showLock) { // 展示解锁窗口 showUnlockWindow(); } else { // 关闭解锁窗口 hideUnlockWindow() } }) // 监听解锁窗口的解锁事件 ipcMain.on('unlock-window', () => { console.log('收到解锁窗口事件'); // 通知主窗口解锁 if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.setIgnoreMouseEvents(false); mainWindow.webContents.send('toggle-lock-window', false); } // 关闭解锁窗口 hideUnlockWindow() }); // 处理语言切换 ipcMain.on('change-language', (_, lang) => { console.log('收到语言切换请求:', lang); i18next.changeLanguage(lang); // 通知所有窗口语言已更改 BrowserWindow.getAllWindows().forEach(window => { if (!window.isDestroyed()) { window.webContents.send('change-language', lang); } }); // 更新右键菜单 updateContextMenu(); // 保存配置 if (mainWindow && !mainWindow.isDestroyed()) { const bounds = mainWindow.getBounds(); saveWindowConfig(bounds); } }); } module.exports = { initConfig, createWindow, createLoadingWindow, updateLoadingProgress, closeLoadingWindow, setupWindowEvents, setupIPC, mainWindow: () => mainWindow, saveWindowConfig };