/** * 窗口管理模块 * 负责创建、配置和管理主窗口 */ 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; // 存储右键菜单实例 // 设置默认语言 i18next.changeLanguage("zh"); // 窗口配置文件路径 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() { const res = [ { 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.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(); }, }, ]; if (isDev) { res.push({ label: i18next.t("menu.openDevTools"), click: () => { mainWindow.webContents.openDevTools(); searchWindow && searchWindow.webContents.openDevTools(); }, }); } return res; } /** * 更新右键菜单 */ 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.zh; // 如果消息是预定义的键,则使用翻译 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, };