Files
ChatLab/electron/main/ipc/window.ts
T
2026-02-19 22:56:41 +08:00

210 lines
5.7 KiB
TypeScript
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.
/**
* 窗口和文件系统操作 IPC 处理器
*/
import { ipcMain, app, dialog, clipboard, shell, nativeTheme } from 'electron'
import * as fs from 'fs/promises'
import type { IpcContext } from './types'
import { simulateUpdateDialog, manualCheckForUpdates } from '../update'
import { t } from '../i18n'
type AppWithQuitFlag = typeof app & { isQuiting?: boolean }
// 通过类型扩展记录应用退出意图,避免使用 @ts-ignore。
const appWithQuitFlag = app as AppWithQuitFlag
/**
* 注册窗口和文件系统操作 IPC 处理器
*/
export function registerWindowHandlers(ctx: IpcContext): void {
const { win } = ctx
// ==================== 窗口操作 ====================
ipcMain.on('window-min', (ev) => {
ev.preventDefault()
win.minimize()
})
ipcMain.on('window-maxOrRestore', (ev) => {
const winSizeState = win.isMaximized()
if (winSizeState) {
win.restore()
} else {
win.maximize()
}
ev.reply('windowState', win.isMaximized())
})
ipcMain.on('window-restore', () => {
win.restore()
})
ipcMain.on('window-hide', () => {
win.hide()
})
ipcMain.on('window-close', () => {
win.close()
appWithQuitFlag.isQuiting = true
app.quit()
})
ipcMain.on('window-resize', (_, data) => {
if (data.resize) {
win.setResizable(true)
} else {
win.setSize(1180, 752)
win.setResizable(false)
}
})
ipcMain.on('open-devtools', () => {
win.webContents.openDevTools()
})
// 设置主题模式
ipcMain.on('window:setThemeSource', (_, mode: 'system' | 'light' | 'dark') => {
nativeTheme.themeSource = mode
// Windows 上动态更新 overlay 颜色以匹配主题
if (process.platform === 'win32' && win) {
const isDark = nativeTheme.shouldUseDarkColors
win.setTitleBarOverlay({
color: isDark ? '#111827' : '#f9fafb', // dark: gray-900, light: gray-50
symbolColor: isDark ? '#a1a1aa' : '#52525b', // dark: zinc-400, light: zinc-600
height: 32,
})
}
})
// ==================== 应用信息 ====================
ipcMain.handle('app:getVersion', () => {
return app.getVersion()
})
// 重启应用
ipcMain.handle('app:relaunch', () => {
app.relaunch()
app.quit()
})
// 获取远程配置(支持 JSON 和纯文本/Markdown
ipcMain.handle('app:fetchRemoteConfig', async (_, url: string) => {
try {
const response = await fetch(url)
const contentType = response.headers.get('content-type') || ''
// 根据 Content-Type 或 URL 后缀决定解析方式
const isJson = contentType.includes('application/json') || url.endsWith('.json')
if (isJson) {
const data = await response.json()
return { success: true, data }
} else {
// 纯文本/Markdown 等其他格式
const data = await response.text()
return { success: true, data }
}
} catch (error) {
return { success: false, error: String(error) }
}
})
// ==================== 更新检查 ====================
ipcMain.on('check-update', () => {
// 手动检查更新(即使是预发布版本也会提示)
manualCheckForUpdates()
})
// 模拟更新弹窗(仅开发模式使用)
ipcMain.on('simulate-update', () => {
if (!app.isPackaged) {
simulateUpdateDialog(win)
}
})
// ==================== 通用工具 ====================
ipcMain.handle('show-message', (event, args) => {
event.sender.send('show-message', args)
})
// 复制到剪贴板(文本)
ipcMain.handle('copyData', async (_, data) => {
try {
clipboard.writeText(data)
return true
} catch (error) {
console.error('Copy operation error:', error)
return false
}
})
// 复制图片到剪贴板(base64 data URL
ipcMain.handle('copyImage', async (_, dataUrl: string) => {
try {
// 从 data URL 中提取 base64 数据
const base64Data = dataUrl.replace(/^data:image\/\w+;base64,/, '')
const imageBuffer = Buffer.from(base64Data, 'base64')
// 使用 nativeImage 创建图片并写入剪贴板
const { nativeImage } = await import('electron')
const image = nativeImage.createFromBuffer(imageBuffer)
clipboard.writeImage(image)
return { success: true }
} catch (error) {
console.error('Image copy error:', error)
return { success: false, error: String(error) }
}
})
// ==================== 文件系统操作 ====================
// 选择文件夹
ipcMain.handle('selectDir', async (_, defaultPath = '') => {
try {
const { canceled, filePaths } = await dialog.showOpenDialog({
title: t('dialog.selectDirectory'),
defaultPath: defaultPath || app.getPath('documents'),
properties: ['openDirectory', 'createDirectory'],
buttonLabel: t('dialog.selectFolder'),
})
if (!canceled) {
return filePaths[0]
}
return null
} catch (err) {
console.error(t('dialog.selectFolderError'), err)
throw err
}
})
// 检查文件是否存在
ipcMain.handle('checkFileExist', async (_, filePath) => {
try {
await fs.access(filePath)
return true
} catch {
return false
}
})
// 在文件管理器中打开
ipcMain.handle('openInFolder', async (_, path) => {
try {
await fs.access(path)
await shell.showItemInFolder(path)
return true
} catch (error) {
console.error('Error opening directory:', error)
return false
}
})
// 显示打开对话框(通用)
ipcMain.handle('dialog:showOpenDialog', async (_, options) => {
try {
return await dialog.showOpenDialog(options)
} catch (error) {
console.error('Failed to show dialog:', error)
throw error
}
})
}