chore: bump version to 0.7.1

This commit is contained in:
ethan.chen
2025-05-27 14:16:48 +08:00
commit 63eda91fd6
103 changed files with 15868 additions and 0 deletions

View File

@@ -0,0 +1,875 @@
/**
* 窗口管理模块
* 负责创建、配置和管理主窗口
*/
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
};