chore: bump version to 0.7.1
This commit is contained in:
228
electron-app/modules/backend-service.js
Normal file
228
electron-app/modules/backend-service.js
Normal file
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* 后端服务管理模块
|
||||
* 负责启动、管理和监控Python后端服务
|
||||
*/
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const findProcess = require('find-process');
|
||||
const getPort = require('get-port');
|
||||
|
||||
// 存储Python进程引用
|
||||
let pythonProcess = null;
|
||||
let backendPort = 5000;
|
||||
let frontendPort = 5173;
|
||||
|
||||
/**
|
||||
* 获取可用的后端端口
|
||||
* @returns {Promise<number>} 可用端口号
|
||||
*/
|
||||
async function checkPort() {
|
||||
try {
|
||||
// 获取可用端口,首选5000
|
||||
backendPort = await getPort({ port: 5000 });
|
||||
console.log(`后端将使用端口: ${backendPort}`);
|
||||
return backendPort;
|
||||
} catch (err) {
|
||||
console.error('获取可用端口失败:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用的前端端口
|
||||
* @returns {Promise<number>} 可用端口号
|
||||
*/
|
||||
async function getFrontendPort() {
|
||||
try {
|
||||
// 获取完全随机的可用端口,不指定首选端口
|
||||
frontendPort = await getPort();
|
||||
console.log(`前端将使用随机端口: ${frontendPort}`);
|
||||
return frontendPort;
|
||||
} catch (err) {
|
||||
console.error('获取前端端口失败:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理已存在的Python进程
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function cleanupExistingProcesses() {
|
||||
try {
|
||||
const processList = await findProcess('port', 5000);
|
||||
|
||||
for (const proc of processList) {
|
||||
if (proc.name.includes('python')) {
|
||||
console.log(`杀死已存在的Python进程: PID ${proc.pid}`);
|
||||
process.kill(proc.pid, 'SIGKILL');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('清理进程失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动Python后端服务
|
||||
* @param {boolean} isDev 是否为开发模式
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function startPythonBackend(isDev) {
|
||||
try {
|
||||
// 在开发模式和生产模式下使用不同的路径
|
||||
let pythonPath;
|
||||
let scriptPath;
|
||||
|
||||
if (isDev) {
|
||||
// 开发模式:使用项目中的虚拟环境
|
||||
const venvPath = path.join(__dirname, '..', '..', 'backend', 'venv');
|
||||
pythonPath = process.platform === 'win32'
|
||||
? path.join(venvPath, 'Scripts', 'python.exe')
|
||||
: path.join(venvPath, 'bin', 'python3');
|
||||
scriptPath = path.join(__dirname, '..', '..', 'backend', 'main.py');
|
||||
} else {
|
||||
// 生产模式:使用打包的虚拟环境
|
||||
const venvPath = path.join(process.resourcesPath, 'backend', 'venv');
|
||||
pythonPath = process.platform === 'win32'
|
||||
? path.join(venvPath, 'Scripts', 'python.exe')
|
||||
: path.join(venvPath, 'bin', 'python3');
|
||||
scriptPath = path.join(process.resourcesPath, 'backend', 'main.py');
|
||||
}
|
||||
|
||||
console.log(`启动Python后端: ${scriptPath} 在端口 ${backendPort}`);
|
||||
console.log(`后端脚本路径存在: ${fs.existsSync(scriptPath)}`);
|
||||
console.log(`Python解释器路径: ${pythonPath}`);
|
||||
|
||||
// 检查Python解释器是否存在
|
||||
if (!fs.existsSync(pythonPath)) {
|
||||
throw new Error(`Python解释器不存在: ${pythonPath}`);
|
||||
}
|
||||
|
||||
// 打印Python脚本目录内容
|
||||
try {
|
||||
const scriptDir = path.dirname(scriptPath);
|
||||
console.log(`后端目录内容: ${fs.readdirSync(scriptDir).join(', ')}`);
|
||||
} catch (err) {
|
||||
console.error(`无法读取后端目录: ${err}`);
|
||||
}
|
||||
|
||||
// 启动子进程
|
||||
pythonProcess = spawn(pythonPath, [scriptPath], {
|
||||
env: { ...process.env, PORT: backendPort.toString() },
|
||||
stdio: 'pipe' // 确保可以读取标准输出和错误
|
||||
});
|
||||
|
||||
// 输出Python进程的日志
|
||||
pythonProcess.stdout.on('data', (data) => {
|
||||
const output = data.toString().trim();
|
||||
console.log(`Python后端输出: ${output}`);
|
||||
});
|
||||
|
||||
pythonProcess.stderr.on('data', (data) => {
|
||||
const message = data.toString().trim();
|
||||
// 判断是否是错误日志还是普通日志
|
||||
if (message.includes('ERROR') || message.includes('CRITICAL') || message.includes('WARN')) {
|
||||
console.error(`Python后端错误: ${message}`);
|
||||
} else {
|
||||
console.log(`Python后端日志: ${message}`);
|
||||
}
|
||||
});
|
||||
|
||||
pythonProcess.on('close', (code) => {
|
||||
console.log(`Python后端退出,退出码: ${code}`);
|
||||
// 如果应用仍在运行,尝试重启后端
|
||||
if (global.appReady && code !== 0) {
|
||||
console.log('尝试重启Python后端...');
|
||||
setTimeout(() => startPythonBackend(isDev), 1000);
|
||||
}
|
||||
});
|
||||
|
||||
// 等待后端启动 - 改为检测后端是否真正启动完成而非固定等待时间
|
||||
return new Promise((resolve, reject) => {
|
||||
// 后端启动超时时间,开发模式较短,生产模式较长
|
||||
const maxTimeout = isDev ? 15000 : 30000;
|
||||
const startTime = Date.now();
|
||||
let backendReady = false;
|
||||
|
||||
// 添加额外的日志解析以检测后端就绪状态
|
||||
pythonProcess.stdout.on('data', (data) => {
|
||||
const output = data.toString().trim();
|
||||
console.log(`Python后端输出: ${output}`);
|
||||
|
||||
// 检测后端就绪信号(uvicorn输出"Application startup complete"表示应用已启动)
|
||||
if (output.includes('Application startup complete') || output.includes('INFO: Application startup complete')) {
|
||||
console.log('检测到后端应用启动完成信号');
|
||||
backendReady = true;
|
||||
// 再等待短暂时间确保所有服务都初始化完成
|
||||
setTimeout(() => {
|
||||
console.log('后端已完全启动,准备创建前端窗口');
|
||||
resolve();
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// 实现HTTP测试以检测后端是否正常响应
|
||||
const testBackendConnection = () => {
|
||||
const http = require('http');
|
||||
const testUrl = `http://127.0.0.1:${backendPort}/`;
|
||||
|
||||
// 如果已经检测到后端就绪,不再继续测试
|
||||
if (backendReady) return;
|
||||
|
||||
// 检查是否超时
|
||||
if (Date.now() - startTime > maxTimeout) {
|
||||
console.warn(`后端启动超时(${maxTimeout}ms),将继续尝试启动前端`);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
http.get(testUrl, (res) => {
|
||||
if (res.statusCode === 200 || res.statusCode === 404) {
|
||||
// 404也表示服务器在运行,只是路径不存在
|
||||
console.log('通过HTTP检测确认后端已启动');
|
||||
backendReady = true;
|
||||
resolve();
|
||||
} else {
|
||||
console.log(`后端响应状态码: ${res.statusCode},继续等待...`);
|
||||
// 短时间后再次测试
|
||||
setTimeout(testBackendConnection, 1000);
|
||||
}
|
||||
}).on('error', (err) => {
|
||||
console.log(`后端连接测试失败: ${err.message}, 继续等待...`);
|
||||
// 短时间后再次测试
|
||||
setTimeout(testBackendConnection, 1000);
|
||||
});
|
||||
};
|
||||
|
||||
// 启动后端连接测试
|
||||
setTimeout(testBackendConnection, 1000);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('启动Python后端失败:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止Python后端
|
||||
*/
|
||||
function stopPythonBackend() {
|
||||
if (pythonProcess) {
|
||||
console.log('终止Python后端进程...');
|
||||
pythonProcess.kill();
|
||||
pythonProcess = null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkPort,
|
||||
getFrontendPort,
|
||||
cleanupExistingProcesses,
|
||||
startPythonBackend,
|
||||
stopPythonBackend,
|
||||
getBackendPort: () => backendPort,
|
||||
getFrontendPortNumber: () => frontendPort,
|
||||
getPythonProcess: () => pythonProcess
|
||||
};
|
||||
76
electron-app/modules/i18n.js
Normal file
76
electron-app/modules/i18n.js
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* @Date: 2025-05-23 13:26:08
|
||||
* @LastEditors: 陈子健
|
||||
* @LastEditTime: 2025-05-26 17:14:59
|
||||
* @FilePath: /mac-lyric-vue/electron-app/modules/i18n.js
|
||||
*/
|
||||
const i18next = require('i18next')
|
||||
const LanguageDetector = require('i18next-browser-languagedetector')
|
||||
|
||||
const resources = {
|
||||
en: {
|
||||
translation: {
|
||||
app: {
|
||||
title: 'lyroc',
|
||||
settings: 'Settings',
|
||||
language: 'Language',
|
||||
theme: 'Theme',
|
||||
about: 'About'
|
||||
},
|
||||
menu: {
|
||||
file: 'File',
|
||||
edit: 'Edit',
|
||||
view: 'View',
|
||||
help: 'Help',
|
||||
lockWindow: 'Lock Window',
|
||||
refresh: 'Refresh',
|
||||
openDevTools: 'Open DevTools',
|
||||
language: 'Language',
|
||||
chinese: 'Chinese',
|
||||
english: 'English',
|
||||
deleteLyrics: 'Delete Local Lyrics',
|
||||
searchLyrics: 'Search Lyrics',
|
||||
quit: 'Quit'
|
||||
}
|
||||
}
|
||||
},
|
||||
zh: {
|
||||
translation: {
|
||||
app: {
|
||||
title: 'Mac歌词',
|
||||
settings: '设置',
|
||||
language: '语言',
|
||||
theme: '主题',
|
||||
about: '关于'
|
||||
},
|
||||
menu: {
|
||||
file: '文件',
|
||||
edit: '编辑',
|
||||
view: '视图',
|
||||
help: '帮助',
|
||||
lockWindow: '锁定窗口',
|
||||
refresh: '刷新',
|
||||
openDevTools: '打开调试工具',
|
||||
language: '切换语言',
|
||||
chinese: '中文',
|
||||
english: '英文',
|
||||
deleteLyrics: '删除本地歌词',
|
||||
searchLyrics: '搜索歌词',
|
||||
quit: '退出'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i18next
|
||||
.use(LanguageDetector)
|
||||
.init({
|
||||
resources,
|
||||
fallbackLng: 'en',
|
||||
detection: {
|
||||
order: ['navigator', 'htmlTag'],
|
||||
caches: ['localStorage']
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = i18next
|
||||
73
electron-app/modules/logger.js
Normal file
73
electron-app/modules/logger.js
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 日志记录模块
|
||||
* 负责应用日志记录和输出重定向
|
||||
*/
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
/**
|
||||
* 设置应用日志系统
|
||||
* @param {string} userDataPath 用户数据目录路径
|
||||
* @returns {string} 日志文件路径
|
||||
*/
|
||||
function setupLogging(userDataPath) {
|
||||
const logPath = path.join(
|
||||
userDataPath,
|
||||
'logs',
|
||||
`app-${new Date().toISOString().replace(/:/g, '-')}.log`
|
||||
);
|
||||
|
||||
// 确保日志目录存在
|
||||
const logDir = path.dirname(logPath);
|
||||
if (!fs.existsSync(logDir)) {
|
||||
fs.mkdirSync(logDir, { recursive: true });
|
||||
}
|
||||
|
||||
console.log(`日志文件将保存在: ${logPath}`);
|
||||
|
||||
// 创建日志文件写入流
|
||||
const logStream = fs.createWriteStream(logPath, { flags: 'a' });
|
||||
|
||||
// 重定向console输出到文件
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleError = console.error;
|
||||
|
||||
console.log = function(...args) {
|
||||
const message = args.map(arg =>
|
||||
typeof arg === 'object' ? JSON.stringify(arg) : arg
|
||||
).join(' ');
|
||||
|
||||
const timestamp = new Date().toISOString();
|
||||
const logMessage = `[${timestamp}] [INFO] ${message}\n`;
|
||||
|
||||
logStream.write(logMessage);
|
||||
originalConsoleLog.apply(console, args);
|
||||
};
|
||||
|
||||
console.error = function(...args) {
|
||||
const message = args.map(arg =>
|
||||
typeof arg === 'object' ? JSON.stringify(arg) : arg
|
||||
).join(' ');
|
||||
|
||||
const timestamp = new Date().toISOString();
|
||||
const logMessage = `[${timestamp}] [ERROR] ${message}\n`;
|
||||
|
||||
logStream.write(logMessage);
|
||||
originalConsoleError.apply(console, args);
|
||||
};
|
||||
|
||||
// 记录未捕获的异常
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('未捕获的异常:', error);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('未处理的Promise拒绝:', reason);
|
||||
});
|
||||
|
||||
return logPath;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setupLogging
|
||||
};
|
||||
875
electron-app/modules/window-manager.js
Normal file
875
electron-app/modules/window-manager.js
Normal 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
|
||||
};
|
||||
Reference in New Issue
Block a user