mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-04-25 13:22:56 +08:00
312 lines
9.6 KiB
TypeScript
312 lines
9.6 KiB
TypeScript
import { app, shell, BrowserWindow, protocol, nativeTheme } from 'electron'
|
||
import { join } from 'path'
|
||
import { optimizer, is, platform } from '@electron-toolkit/utils'
|
||
import { checkUpdate } from './update'
|
||
import mainIpcMain, { cleanup } from './ipcMain'
|
||
import { initAnalytics, trackDailyActive } from './analytics'
|
||
import { initProxy } from './network/proxy'
|
||
import { needsLegacyMigration, migrateFromLegacyDir, ensureAppDirs, cleanupPendingDeleteDir } from './paths'
|
||
import { migrateAllDatabases, checkMigrationNeeded } from './database/core'
|
||
import { initLocale } from './i18n'
|
||
|
||
type AppWithQuitFlag = typeof app & { isQuiting?: boolean }
|
||
// 统一通过扩展类型访问退出标记,避免使用 @ts-ignore。
|
||
const appWithQuitFlag = app as AppWithQuitFlag
|
||
|
||
class MainProcess {
|
||
mainWindow: BrowserWindow | null
|
||
constructor() {
|
||
// 主窗口
|
||
this.mainWindow = null
|
||
|
||
// 设置应用程序名称
|
||
if (process.platform === 'win32') app.setAppUserModelId(app.getName())
|
||
// 初始化
|
||
this.checkApp().then(async (lockObtained) => {
|
||
if (lockObtained) {
|
||
await this.init()
|
||
}
|
||
})
|
||
}
|
||
|
||
// 单例锁
|
||
async checkApp() {
|
||
if (!app.requestSingleInstanceLock()) {
|
||
app.quit()
|
||
// 未获得锁
|
||
return false
|
||
}
|
||
// 聚焦到当前程序
|
||
else {
|
||
app.on('second-instance', () => {
|
||
if (this.mainWindow) {
|
||
this.mainWindow.show()
|
||
if (this.mainWindow.isMinimized()) this.mainWindow.restore()
|
||
this.mainWindow.focus()
|
||
}
|
||
})
|
||
// 获得锁
|
||
return true
|
||
}
|
||
}
|
||
|
||
// 初始化程序
|
||
async init() {
|
||
initAnalytics()
|
||
|
||
// 清理上次切换目录后的旧数据目录
|
||
cleanupPendingDeleteDir()
|
||
|
||
// 执行数据目录迁移(从 Documents/ChatLab 迁移到 userData)
|
||
this.migrateDataIfNeeded()
|
||
|
||
// 确保应用目录存在
|
||
ensureAppDirs()
|
||
|
||
// 初始化主进程国际化(在 ensureAppDirs 之后,确保 settings 目录存在)
|
||
await initLocale()
|
||
|
||
// 执行数据库 schema 迁移(确保所有数据库在 Worker 查询前已是最新 schema)
|
||
this.migrateDatabasesIfNeeded()
|
||
|
||
initProxy() // 初始化代理配置
|
||
|
||
// 暂不注册自定义协议,避免触发系统 URL 协议关联提示
|
||
|
||
// 应用程序准备好之前注册
|
||
protocol.registerSchemesAsPrivileged([{ scheme: 'app', privileges: { secure: true, standard: true } }])
|
||
|
||
// 主应用程序事件
|
||
this.mainAppEvents()
|
||
}
|
||
|
||
// 从旧目录迁移数据(静默迁移)
|
||
migrateDataIfNeeded() {
|
||
if (needsLegacyMigration()) {
|
||
console.log('[Main] Legacy data migration needed, starting migration...')
|
||
const result = migrateFromLegacyDir()
|
||
if (result.success) {
|
||
console.log(`[Main] Migration completed. Migrated: ${result.migratedDirs.join(', ')}`)
|
||
} else {
|
||
console.error('[Main] Migration failed:', result.error)
|
||
}
|
||
} else {
|
||
console.log('[Main] No legacy data migration needed')
|
||
}
|
||
}
|
||
|
||
// 执行数据库 schema 迁移(静默迁移)
|
||
migrateDatabasesIfNeeded() {
|
||
try {
|
||
const { count } = checkMigrationNeeded()
|
||
if (count > 0) {
|
||
const result = migrateAllDatabases()
|
||
if (!result.success) {
|
||
console.error('[Main] Database schema migration failed:', result.error)
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('[Main] Error in migrateDatabasesIfNeeded:', error)
|
||
}
|
||
}
|
||
|
||
// 创建主窗口
|
||
async createWindow() {
|
||
// 平台差异化窗口配置
|
||
const windowOptions: Electron.BrowserWindowConstructorOptions = {
|
||
width: 1180,
|
||
height: 752,
|
||
minWidth: 1180,
|
||
minHeight: 752,
|
||
show: false,
|
||
autoHideMenuBar: true,
|
||
webPreferences: {
|
||
preload: join(__dirname, '../preload/index.js'),
|
||
sandbox: false,
|
||
devTools: true,
|
||
},
|
||
}
|
||
|
||
// macOS: 使用 hiddenInset 保留红绿灯按钮
|
||
// Windows: 使用 titleBarOverlay,在自定义标题栏区域右侧显示原生窗口按钮
|
||
// Linux: 使用自定义标题栏和自定义按钮
|
||
if (platform.isMacOS) {
|
||
windowOptions.titleBarStyle = 'hiddenInset'
|
||
} else if (platform.isWindows) {
|
||
// 保留系统框架,只隐藏标题栏内容,把内容区域顶到最上方
|
||
windowOptions.titleBarStyle = 'hidden'
|
||
// 获取当前主题状态
|
||
const isDark = nativeTheme.shouldUseDarkColors
|
||
windowOptions.titleBarOverlay = {
|
||
// 背景色与应用背景匹配,确保 hover 效果正确
|
||
color: isDark ? '#111827' : '#f9fafb', // dark: gray-900, light: gray-50
|
||
// 图标颜色适配主题
|
||
symbolColor: isDark ? '#a1a1aa' : '#52525b', // dark: zinc-400, light: zinc-600
|
||
height: 32,
|
||
}
|
||
} else {
|
||
// Linux 继续使用无边框 + 自定义按钮
|
||
windowOptions.frame = false
|
||
}
|
||
|
||
this.mainWindow = new BrowserWindow(windowOptions)
|
||
|
||
this.mainWindow.once('ready-to-show', () => {
|
||
this.mainWindow?.show()
|
||
|
||
// Windows 上根据当前主题设置 titleBarOverlay 颜色
|
||
if (platform.isWindows) {
|
||
const isDark = nativeTheme.shouldUseDarkColors
|
||
this.mainWindow?.setTitleBarOverlay({
|
||
color: isDark ? '#111827' : '#f9fafb', // dark: gray-900, light: gray-50
|
||
symbolColor: isDark ? '#a1a1aa' : '#52525b', // dark: zinc-400, light: zinc-600
|
||
height: 32,
|
||
})
|
||
|
||
// 监听主题变化,动态更新颜色
|
||
nativeTheme.on('updated', () => {
|
||
if (this.mainWindow && platform.isWindows) {
|
||
const isDark = nativeTheme.shouldUseDarkColors
|
||
this.mainWindow.setTitleBarOverlay({
|
||
color: isDark ? '#111827' : '#f9fafb', // dark: gray-900, light: gray-50
|
||
symbolColor: isDark ? '#a1a1aa' : '#52525b', // dark: zinc-400, light: zinc-600
|
||
height: 32,
|
||
})
|
||
}
|
||
})
|
||
}
|
||
})
|
||
|
||
// 主窗口事件
|
||
this.mainWindowEvents()
|
||
|
||
this.mainWindow.webContents.setWindowOpenHandler((details) => {
|
||
shell.openExternal(details.url)
|
||
return { action: 'deny' }
|
||
})
|
||
|
||
// HMR for renderer base on electron-vite cli.
|
||
// Load the remote URL for development or the local html file for production.
|
||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||
this.mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
||
} else {
|
||
this.mainWindow.loadFile(join(__dirname, '../../out/renderer/index.html'))
|
||
}
|
||
}
|
||
|
||
// 主应用程序事件
|
||
mainAppEvents() {
|
||
app.whenReady().then(async () => {
|
||
console.log('[Main] App is ready')
|
||
// 设置Windows应用程序用户模型id
|
||
if (process.platform === 'win32') app.setAppUserModelId(app.getName())
|
||
|
||
// 记录日活(用于统计操作系统版本、客户端版本,便于更好的适配客户端)
|
||
trackDailyActive()
|
||
|
||
// 创建主窗口
|
||
console.log('[Main] Creating window...')
|
||
await this.createWindow()
|
||
console.log('[Main] Window created')
|
||
|
||
// 检查更新逻辑
|
||
checkUpdate(this.mainWindow)
|
||
|
||
// 引入主进程ipcMain
|
||
if (this.mainWindow) {
|
||
console.log('[Main] Registering IPC handlers...')
|
||
mainIpcMain(this.mainWindow)
|
||
console.log('[Main] IPC handlers registered')
|
||
}
|
||
|
||
// 开发环境下 F12 打开控制台
|
||
app.on('browser-window-created', (_, window) => {
|
||
optimizer.watchWindowShortcuts(window)
|
||
})
|
||
|
||
app.on('activate', () => {
|
||
// 在 macOS 上,当单击 Dock 图标且没有其他窗口时,通常会重新创建窗口
|
||
if (BrowserWindow.getAllWindows().length === 0) {
|
||
this.createWindow()
|
||
return
|
||
}
|
||
|
||
if (platform.isMacOS) {
|
||
this.mainWindow?.show()
|
||
}
|
||
})
|
||
|
||
// 监听渲染进程崩溃
|
||
app.on('render-process-gone', (_event, w, d) => {
|
||
if (d.reason == 'crashed') {
|
||
w.reload()
|
||
}
|
||
// fs.appendFile(`./error-log-${+new Date()}.txt`, `${new Date()}渲染进程被杀死${d.reason}\n`)
|
||
})
|
||
|
||
// 自定义协议
|
||
app.on('open-url', (_, url) => {
|
||
console.log('Received custom protocol URL:', url)
|
||
})
|
||
|
||
// 当所有窗口都关闭时退出应用,macOS 除外
|
||
app.on('window-all-closed', () => {
|
||
if (!platform.isMacOS) {
|
||
app.quit()
|
||
}
|
||
})
|
||
|
||
// 只有显式调用quit才退出系统,区分MAC系统程序坞退出和点击X隐藏
|
||
app.on('before-quit', () => {
|
||
appWithQuitFlag.isQuiting = true
|
||
})
|
||
|
||
// 退出前清理资源
|
||
app.on('will-quit', () => {
|
||
cleanup()
|
||
})
|
||
})
|
||
}
|
||
|
||
// 主窗口事件
|
||
mainWindowEvents() {
|
||
if (!this.mainWindow) {
|
||
return
|
||
}
|
||
this.mainWindow.webContents.on('did-finish-load', () => {
|
||
setTimeout(() => {
|
||
if (this.mainWindow) {
|
||
this.mainWindow.webContents.send('app-started')
|
||
}
|
||
}, 500)
|
||
})
|
||
|
||
this.mainWindow.on('maximize', () => {
|
||
this.mainWindow?.webContents.send('windowState', true)
|
||
})
|
||
|
||
this.mainWindow.on('unmaximize', () => {
|
||
this.mainWindow?.webContents.send('windowState', false)
|
||
})
|
||
|
||
// 窗口关闭
|
||
this.mainWindow.on('close', (event) => {
|
||
if (platform.isMacOS) {
|
||
// macOS: 只有明确退出时才真正关闭,否则只隐藏窗口(符合 macOS 用户习惯)
|
||
if (!appWithQuitFlag.isQuiting) {
|
||
event.preventDefault()
|
||
this.mainWindow?.hide()
|
||
}
|
||
}
|
||
// Windows/Linux: 不阻止关闭,正常触发 window-all-closed → app.quit() → cleanup()
|
||
})
|
||
}
|
||
}
|
||
|
||
// 捕获未捕获的异常
|
||
process.on('uncaughtException', (error) => {
|
||
console.error('Uncaught Exception:', error)
|
||
})
|
||
|
||
new MainProcess()
|