feat: 完成记忆窗口位置,最小化到托盘和托盘区图标
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { app, BrowserWindow, ipcMain, dialog, shell } from 'electron'
|
||||
import { app, BrowserWindow, ipcMain, dialog, shell, Tray, Menu, nativeImage } from 'electron'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import { spawn } from 'child_process'
|
||||
@@ -59,16 +59,232 @@ function restartAsAdmin(): void {
|
||||
}
|
||||
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
let tray: Tray | null = null
|
||||
let isQuitting = false
|
||||
let saveWindowStateTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
// 配置接口
|
||||
interface AppConfig {
|
||||
UI: {
|
||||
IfShowTray: boolean
|
||||
IfToTray: boolean
|
||||
location: string
|
||||
maximized: boolean
|
||||
size: string
|
||||
}
|
||||
Start: {
|
||||
IfMinimizeDirectly: boolean
|
||||
IfSelfStart: boolean
|
||||
}
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
const defaultConfig: AppConfig = {
|
||||
UI: {
|
||||
IfShowTray: false,
|
||||
IfToTray: false,
|
||||
location: '100,100',
|
||||
maximized: false,
|
||||
size: '1600,1000'
|
||||
},
|
||||
Start: {
|
||||
IfMinimizeDirectly: false,
|
||||
IfSelfStart: false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载配置
|
||||
function loadConfig(): AppConfig {
|
||||
try {
|
||||
const appRoot = getAppRoot()
|
||||
const configPath = path.join(appRoot, 'config', 'frontend_config.json')
|
||||
|
||||
if (fs.existsSync(configPath)) {
|
||||
const configData = fs.readFileSync(configPath, 'utf8')
|
||||
const config = JSON.parse(configData)
|
||||
return { ...defaultConfig, ...config }
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('加载配置失败:', error)
|
||||
}
|
||||
return defaultConfig
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
function saveConfig(config: AppConfig) {
|
||||
try {
|
||||
const appRoot = getAppRoot()
|
||||
const configDir = path.join(appRoot, 'config')
|
||||
const configPath = path.join(configDir, 'frontend_config.json')
|
||||
|
||||
if (!fs.existsSync(configDir)) {
|
||||
fs.mkdirSync(configDir, { recursive: true })
|
||||
}
|
||||
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8')
|
||||
} catch (error) {
|
||||
log.error('保存配置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建托盘
|
||||
function createTray() {
|
||||
if (tray) return
|
||||
|
||||
// 尝试多个可能的图标路径
|
||||
const iconPaths = [
|
||||
path.join(__dirname, '../public/AUTO-MAS.ico'),
|
||||
path.join(process.resourcesPath, 'assets/AUTO-MAS.ico'),
|
||||
path.join(app.getAppPath(), 'public/AUTO-MAS.ico'),
|
||||
path.join(app.getAppPath(), 'dist/AUTO-MAS.ico')
|
||||
]
|
||||
|
||||
let trayIcon
|
||||
|
||||
try {
|
||||
// 尝试加载图标
|
||||
for (const iconPath of iconPaths) {
|
||||
if (fs.existsSync(iconPath)) {
|
||||
trayIcon = nativeImage.createFromPath(iconPath)
|
||||
if (!trayIcon.isEmpty()) {
|
||||
log.info(`成功加载托盘图标: ${iconPath}`)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果所有路径都失败,创建一个默认图标
|
||||
if (!trayIcon || trayIcon.isEmpty()) {
|
||||
log.warn('无法加载托盘图标,使用默认图标')
|
||||
trayIcon = nativeImage.createEmpty()
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('加载托盘图标失败:', error)
|
||||
trayIcon = nativeImage.createEmpty()
|
||||
}
|
||||
|
||||
tray = new Tray(trayIcon)
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: '显示窗口',
|
||||
click: () => {
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMinimized()) {
|
||||
mainWindow.restore()
|
||||
}
|
||||
mainWindow.setSkipTaskbar(false) // 恢复任务栏图标
|
||||
mainWindow.show()
|
||||
mainWindow.focus()
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '隐藏窗口',
|
||||
click: () => {
|
||||
if (mainWindow) {
|
||||
const currentConfig = loadConfig()
|
||||
if (currentConfig.UI.IfToTray) {
|
||||
mainWindow.setSkipTaskbar(true) // 隐藏任务栏图标
|
||||
}
|
||||
mainWindow.hide()
|
||||
}
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: '退出',
|
||||
click: () => {
|
||||
isQuitting = true
|
||||
app.quit()
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
tray.setContextMenu(contextMenu)
|
||||
tray.setToolTip('AUTO_MAA')
|
||||
|
||||
// 双击托盘图标显示/隐藏窗口
|
||||
tray.on('double-click', () => {
|
||||
if (mainWindow) {
|
||||
const currentConfig = loadConfig()
|
||||
if (mainWindow.isVisible()) {
|
||||
if (currentConfig.UI.IfToTray) {
|
||||
mainWindow.setSkipTaskbar(true) // 隐藏任务栏图标
|
||||
}
|
||||
mainWindow.hide()
|
||||
} else {
|
||||
if (mainWindow.isMinimized()) {
|
||||
mainWindow.restore()
|
||||
}
|
||||
mainWindow.setSkipTaskbar(false) // 恢复任务栏图标
|
||||
mainWindow.show()
|
||||
mainWindow.focus()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 销毁托盘
|
||||
function destroyTray() {
|
||||
if (tray) {
|
||||
tray.destroy()
|
||||
tray = null
|
||||
}
|
||||
}
|
||||
|
||||
// 更新托盘状态
|
||||
function updateTrayVisibility(config: AppConfig) {
|
||||
// 根据需求逻辑判断是否应该显示托盘
|
||||
let shouldShowTray = false
|
||||
|
||||
if (config.UI.IfShowTray && config.UI.IfToTray) {
|
||||
// 勾选常驻显示托盘和最小化到托盘,就一直展示托盘
|
||||
shouldShowTray = true
|
||||
} else if (config.UI.IfShowTray && !config.UI.IfToTray) {
|
||||
// 勾选常驻显示托盘但没有最小化到托盘,就一直展示托盘
|
||||
shouldShowTray = true
|
||||
} else if (!config.UI.IfShowTray && config.UI.IfToTray) {
|
||||
// 没有常驻显示托盘但勾选最小化到托盘,有窗口时就只有窗口,最小化后任务栏消失,只有托盘
|
||||
shouldShowTray = !mainWindow || !mainWindow.isVisible()
|
||||
} else {
|
||||
// 没有常驻显示托盘也没有最小化到托盘,托盘一直不展示
|
||||
shouldShowTray = false
|
||||
}
|
||||
|
||||
// 特殊情况:如果没有窗口显示且没有托盘,强制显示托盘避免程序成为幽灵
|
||||
if (!shouldShowTray && (!mainWindow || !mainWindow.isVisible()) && !tray) {
|
||||
shouldShowTray = true
|
||||
log.warn('防幽灵机制:强制显示托盘图标')
|
||||
}
|
||||
|
||||
if (shouldShowTray && !tray) {
|
||||
createTray()
|
||||
log.info('托盘图标已创建')
|
||||
} else if (!shouldShowTray && tray) {
|
||||
destroyTray()
|
||||
log.info('托盘图标已销毁')
|
||||
}
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
log.info('开始创建主窗口')
|
||||
|
||||
const config = loadConfig()
|
||||
|
||||
// 解析窗口大小
|
||||
const [width, height] = config.UI.size.split(',').map(s => parseInt(s.trim()) || 1600)
|
||||
const [x, y] = config.UI.location.split(',').map(s => parseInt(s.trim()) || 100)
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1600,
|
||||
height: 1000,
|
||||
width: Math.max(width, 800),
|
||||
height: Math.max(height, 600),
|
||||
x,
|
||||
y,
|
||||
minWidth: 800,
|
||||
minHeight: 600,
|
||||
icon: path.join(__dirname, '../src/assets/AUTO-MAS.ico'),
|
||||
icon: path.join(__dirname, '../public/AUTO-MAS.ico'),
|
||||
frame: false, // 去掉系统标题栏
|
||||
titleBarStyle: 'hidden', // 隐藏标题栏
|
||||
webPreferences: {
|
||||
@@ -77,8 +293,14 @@ function createWindow() {
|
||||
contextIsolation: true,
|
||||
},
|
||||
autoHideMenuBar: true,
|
||||
show: !config.Start.IfMinimizeDirectly, // 根据配置决定是否直接显示
|
||||
})
|
||||
|
||||
// 如果配置为最大化,则最大化窗口
|
||||
if (config.UI.maximized) {
|
||||
mainWindow.maximize()
|
||||
}
|
||||
|
||||
mainWindow.setMenuBarVisibility(false)
|
||||
const devServer = process.env.VITE_DEV_SERVER_URL
|
||||
if (devServer) {
|
||||
@@ -90,11 +312,72 @@ function createWindow() {
|
||||
mainWindow.loadFile(indexHtmlPath)
|
||||
}
|
||||
|
||||
// 窗口事件处理
|
||||
mainWindow.on('close', (event) => {
|
||||
const currentConfig = loadConfig()
|
||||
|
||||
if (!isQuitting && currentConfig.UI.IfToTray) {
|
||||
// 如果启用了最小化到托盘,阻止关闭并隐藏窗口
|
||||
event.preventDefault()
|
||||
mainWindow?.hide()
|
||||
mainWindow?.setSkipTaskbar(true)
|
||||
|
||||
// 更新托盘状态
|
||||
updateTrayVisibility(currentConfig)
|
||||
|
||||
log.info('窗口已最小化到托盘,任务栏图标已隐藏')
|
||||
} else {
|
||||
// 保存窗口状态
|
||||
saveWindowState()
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
log.info('主窗口已关闭')
|
||||
mainWindow = null
|
||||
})
|
||||
|
||||
// 窗口最小化事件
|
||||
mainWindow.on('minimize', () => {
|
||||
const currentConfig = loadConfig()
|
||||
|
||||
if (currentConfig.UI.IfToTray) {
|
||||
// 如果启用了最小化到托盘,隐藏窗口并从任务栏移除
|
||||
mainWindow?.hide()
|
||||
mainWindow?.setSkipTaskbar(true)
|
||||
|
||||
// 更新托盘状态
|
||||
updateTrayVisibility(currentConfig)
|
||||
|
||||
log.info('窗口已最小化到托盘,任务栏图标已隐藏')
|
||||
}
|
||||
})
|
||||
|
||||
// 窗口显示/隐藏事件,用于更新托盘状态
|
||||
mainWindow.on('show', () => {
|
||||
const currentConfig = loadConfig()
|
||||
// 窗口显示时,恢复任务栏图标
|
||||
mainWindow?.setSkipTaskbar(false)
|
||||
updateTrayVisibility(currentConfig)
|
||||
log.info('窗口已显示,任务栏图标已恢复')
|
||||
})
|
||||
|
||||
mainWindow.on('hide', () => {
|
||||
const currentConfig = loadConfig()
|
||||
// 窗口隐藏时,根据配置决定是否隐藏任务栏图标
|
||||
if (currentConfig.UI.IfToTray) {
|
||||
mainWindow?.setSkipTaskbar(true)
|
||||
log.info('窗口已隐藏,任务栏图标已隐藏')
|
||||
}
|
||||
updateTrayVisibility(currentConfig)
|
||||
})
|
||||
|
||||
// 窗口移动和调整大小时保存状态
|
||||
mainWindow.on('moved', saveWindowState)
|
||||
mainWindow.on('resized', saveWindowState)
|
||||
mainWindow.on('maximize', saveWindowState)
|
||||
mainWindow.on('unmaximize', saveWindowState)
|
||||
|
||||
// 设置各个服务的主窗口引用
|
||||
if (mainWindow) {
|
||||
setDownloadMainWindow(mainWindow)
|
||||
@@ -102,6 +385,40 @@ function createWindow() {
|
||||
setGitMainWindow(mainWindow)
|
||||
log.info('主窗口创建完成,服务引用已设置')
|
||||
}
|
||||
|
||||
// 根据配置初始化托盘
|
||||
updateTrayVisibility(config)
|
||||
}
|
||||
|
||||
// 保存窗口状态(带防抖)
|
||||
function saveWindowState() {
|
||||
if (!mainWindow) return
|
||||
|
||||
// 清除之前的定时器
|
||||
if (saveWindowStateTimeout) {
|
||||
clearTimeout(saveWindowStateTimeout)
|
||||
}
|
||||
|
||||
// 设置新的定时器,500ms后保存
|
||||
saveWindowStateTimeout = setTimeout(() => {
|
||||
try {
|
||||
const config = loadConfig()
|
||||
const bounds = mainWindow!.getBounds()
|
||||
const isMaximized = mainWindow!.isMaximized()
|
||||
|
||||
// 只有在窗口不是最大化状态时才保存位置和大小
|
||||
if (!isMaximized) {
|
||||
config.UI.size = `${bounds.width},${bounds.height}`
|
||||
config.UI.location = `${bounds.x},${bounds.y}`
|
||||
}
|
||||
config.UI.maximized = isMaximized
|
||||
|
||||
saveConfig(config)
|
||||
log.info('窗口状态已保存')
|
||||
} catch (error) {
|
||||
log.error('保存窗口状态失败:', error)
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
// IPC处理函数
|
||||
@@ -158,7 +475,7 @@ ipcMain.handle('select-file', async (event, filters = []) => {
|
||||
})
|
||||
|
||||
// 在系统默认浏览器中打开URL
|
||||
ipcMain.handle('open-url', async (event, url: string) => {
|
||||
ipcMain.handle('open-url', async (_event, url: string) => {
|
||||
try {
|
||||
await shell.openExternal(url)
|
||||
return { success: true }
|
||||
@@ -180,7 +497,7 @@ ipcMain.handle('check-environment', async () => {
|
||||
})
|
||||
|
||||
// Python相关
|
||||
ipcMain.handle('download-python', async (event, mirror = 'tsinghua') => {
|
||||
ipcMain.handle('download-python', async (_event, mirror = 'tsinghua') => {
|
||||
const appRoot = getAppRoot()
|
||||
return downloadPython(appRoot, mirror)
|
||||
})
|
||||
@@ -190,7 +507,7 @@ ipcMain.handle('install-pip', async () => {
|
||||
return installPipPackage(appRoot)
|
||||
})
|
||||
|
||||
ipcMain.handle('install-dependencies', async (event, mirror = 'tsinghua') => {
|
||||
ipcMain.handle('install-dependencies', async (_event, mirror = 'tsinghua') => {
|
||||
const appRoot = getAppRoot()
|
||||
return installDependencies(appRoot, mirror)
|
||||
})
|
||||
@@ -208,7 +525,7 @@ ipcMain.handle('download-git', async () => {
|
||||
|
||||
ipcMain.handle(
|
||||
'clone-backend',
|
||||
async (event, repoUrl = 'https://github.com/DLmaster361/AUTO_MAA.git') => {
|
||||
async (_event, repoUrl = 'https://github.com/DLmaster361/AUTO_MAA.git') => {
|
||||
const appRoot = getAppRoot()
|
||||
return cloneBackend(appRoot, repoUrl)
|
||||
}
|
||||
@@ -216,14 +533,14 @@ ipcMain.handle(
|
||||
|
||||
ipcMain.handle(
|
||||
'update-backend',
|
||||
async (event, repoUrl = 'https://github.com/DLmaster361/AUTO_MAA.git') => {
|
||||
async (_event, repoUrl = 'https://github.com/DLmaster361/AUTO_MAA.git') => {
|
||||
const appRoot = getAppRoot()
|
||||
return cloneBackend(appRoot, repoUrl) // 使用相同的逻辑,会自动判断是pull还是clone
|
||||
}
|
||||
)
|
||||
|
||||
// 配置文件操作
|
||||
ipcMain.handle('save-config', async (event, config) => {
|
||||
ipcMain.handle('save-config', async (_event, config) => {
|
||||
try {
|
||||
const appRoot = getAppRoot()
|
||||
const configDir = path.join(appRoot, 'config')
|
||||
@@ -236,12 +553,36 @@ ipcMain.handle('save-config', async (event, config) => {
|
||||
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8')
|
||||
console.log(`配置已保存到: ${configPath}`)
|
||||
|
||||
// 如果是UI配置更新,需要更新托盘状态
|
||||
if (config.UI) {
|
||||
updateTrayVisibility(config)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存配置文件失败:', error)
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
// 新增:实时更新托盘状态的IPC处理器
|
||||
ipcMain.handle('update-tray-settings', async (_event, uiSettings) => {
|
||||
try {
|
||||
// 先更新配置文件
|
||||
const currentConfig = loadConfig()
|
||||
currentConfig.UI = { ...currentConfig.UI, ...uiSettings }
|
||||
saveConfig(currentConfig)
|
||||
|
||||
// 立即更新托盘状态
|
||||
updateTrayVisibility(currentConfig)
|
||||
|
||||
log.info('托盘设置已更新:', uiSettings)
|
||||
return true
|
||||
} catch (error) {
|
||||
log.error('更新托盘设置失败:', error)
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('load-config', async () => {
|
||||
try {
|
||||
const appRoot = getAppRoot()
|
||||
@@ -285,7 +626,7 @@ ipcMain.handle('get-log-path', async () => {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('get-log-files', async () => {
|
||||
ipcMain.handle('get-log-files', async (_event) => {
|
||||
try {
|
||||
return getLogFiles()
|
||||
} catch (error) {
|
||||
@@ -294,7 +635,7 @@ ipcMain.handle('get-log-files', async () => {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('get-logs', async (event, lines?: number, fileName?: string) => {
|
||||
ipcMain.handle('get-logs', async (_event, lines?: number, fileName?: string) => {
|
||||
try {
|
||||
let logFilePath: string
|
||||
|
||||
@@ -325,7 +666,7 @@ ipcMain.handle('get-logs', async (event, lines?: number, fileName?: string) => {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('clear-logs', async (event, fileName?: string) => {
|
||||
ipcMain.handle('clear-logs', async (_event, fileName?: string) => {
|
||||
try {
|
||||
let logFilePath: string
|
||||
|
||||
@@ -348,7 +689,7 @@ ipcMain.handle('clear-logs', async (event, fileName?: string) => {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('clean-old-logs', async (event, daysToKeep = 7) => {
|
||||
ipcMain.handle('clean-old-logs', async (_event, daysToKeep = 7) => {
|
||||
try {
|
||||
cleanOldLogs(daysToKeep)
|
||||
log.info(`已清理${daysToKeep}天前的旧日志文件`)
|
||||
@@ -359,7 +700,7 @@ ipcMain.handle('clean-old-logs', async (event, daysToKeep = 7) => {
|
||||
})
|
||||
|
||||
// 保留原有的日志操作方法以兼容现有代码
|
||||
ipcMain.handle('save-logs-to-file', async (event, logs: string) => {
|
||||
ipcMain.handle('save-logs-to-file', async (_event, logs: string) => {
|
||||
try {
|
||||
const appRoot = getAppRoot()
|
||||
const logsDir = path.join(appRoot, 'logs')
|
||||
@@ -423,17 +764,25 @@ app.on('second-instance', () => {
|
||||
|
||||
app.on('before-quit', async event => {
|
||||
// 只处理一次,避免多重触发
|
||||
event.preventDefault()
|
||||
log.info('应用准备退出')
|
||||
try {
|
||||
await stopBackend()
|
||||
log.info('后端服务已停止')
|
||||
} catch (e) {
|
||||
log.error('停止后端时出错:', e)
|
||||
console.error('停止后端时出错:', e)
|
||||
} finally {
|
||||
log.info('应用退出')
|
||||
app.exit(0)
|
||||
if (!isQuitting) {
|
||||
event.preventDefault()
|
||||
isQuitting = true
|
||||
|
||||
log.info('应用准备退出')
|
||||
|
||||
// 清理托盘
|
||||
destroyTray()
|
||||
|
||||
try {
|
||||
await stopBackend()
|
||||
log.info('后端服务已停止')
|
||||
} catch (e) {
|
||||
log.error('停止后端时出错:', e)
|
||||
console.error('停止后端时出错:', e)
|
||||
} finally {
|
||||
log.info('应用退出')
|
||||
app.exit(0)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -35,6 +35,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
saveConfig: (config: any) => ipcRenderer.invoke('save-config', config),
|
||||
loadConfig: () => ipcRenderer.invoke('load-config'),
|
||||
resetConfig: () => ipcRenderer.invoke('reset-config'),
|
||||
|
||||
// 托盘设置实时更新
|
||||
updateTraySettings: (uiSettings: any) => ipcRenderer.invoke('update-tray-settings', uiSettings),
|
||||
|
||||
// 日志文件操作
|
||||
getLogPath: () => ipcRenderer.invoke('get-log-path'),
|
||||
|
||||
Reference in New Issue
Block a user