Files
lyroc/electron-app/modules/window-manager.js
2025-05-27 14:16:48 +08:00

876 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 窗口管理模块
* 负责创建、配置和管理主窗口
*/
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
};