908 lines
25 KiB
JavaScript
908 lines
25 KiB
JavaScript
/**
|
||
* 窗口管理模块
|
||
* 负责创建、配置和管理主窗口
|
||
*/
|
||
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() {
|
||
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.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,
|
||
};
|