Skip to content

feat: Floating window #553

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jun 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ export default defineConfig({
alias: {
'@shared': resolve('src/shared')
}
},
build: {
rollupOptions: {
input: {
index: resolve('src/preload/index.ts'),
floating: resolve('src/preload/floating-preload.ts')
}
}
}
},
renderer: {
Expand Down Expand Up @@ -77,7 +85,8 @@ export default defineConfig({
rollupOptions: {
input: {
shell: resolve('src/renderer/shell/index.html'),
index: resolve('src/renderer/index.html')
index: resolve('src/renderer/index.html'),
floating: resolve('src/renderer/floating/index.html')
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/main/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,11 @@ export const TRAY_EVENTS = {
export const MEETING_EVENTS = {
INSTRUCTION: 'mcp:meeting-instruction', // 主进程向渲染进程发送指令
}

// 悬浮按钮相关事件
export const FLOATING_BUTTON_EVENTS = {
CLICKED: 'floating-button:clicked', // 悬浮按钮被点击
VISIBILITY_CHANGED: 'floating-button:visibility-changed', // 悬浮按钮显示状态改变
POSITION_CHANGED: 'floating-button:position-changed', // 悬浮按钮位置改变
ENABLED_CHANGED: 'floating-button:enabled-changed' // 悬浮按钮启用状态改变
}
103 changes: 54 additions & 49 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { ProxyMode, proxyConfig } from './presenter/proxyConfig'
import path from 'path'
import fs from 'fs'
import { eventBus } from './eventbus'
import { WINDOW_EVENTS, TRAY_EVENTS } from './events'
import { WINDOW_EVENTS, TRAY_EVENTS, FLOATING_BUTTON_EVENTS } from './events'
import { setLoggingEnabled } from '@shared/logger'
import { is } from '@electron-toolkit/utils' // 确保导入 is
import { handleShowHiddenWindow } from './utils'

// 设置应用命令行参数
app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required') // 允许视频自动播放
Expand All @@ -30,7 +31,7 @@ if (process.platform === 'darwin') {
presenter.deeplinkPresenter.init()

// 等待 Electron 初始化完成
app.whenReady().then(() => {
app.whenReady().then(async () => {
// Set app user model id for windows
electronApp.setAppUserModelId('com.wefonk.deepchat')

Expand All @@ -41,6 +42,9 @@ app.whenReady().then(() => {
// 初始化托盘图标和菜单,并存储 presenter 实例
presenter.setupTray()

// 立即进行基本初始化,不等待窗口ready-to-show事件
presenter.init()

// 从配置中读取代理设置并初始化
const proxyMode = presenter.configPresenter.getProxyMode() as ProxyMode
const customProxyUrl = presenter.configPresenter.getCustomProxyUrl()
Expand Down Expand Up @@ -80,16 +84,37 @@ app.whenReady().then(() => {

// 如果没有窗口,创建主窗口 (应用首次启动时)
if (presenter.windowPresenter.getAllWindows().length === 0) {
presenter.windowPresenter.createShellWindow({
initialTab: {
url: 'local://chat'
console.log('Main: Creating initial shell window on app startup')
try {
const windowId = await presenter.windowPresenter.createShellWindow({
initialTab: {
url: 'local://chat'
}
})
if (windowId) {
console.log(`Main: Initial shell window created successfully with ID: ${windowId}`)
} else {
console.error('Main: Failed to create initial shell window - returned null')
}
})
} catch (error) {
console.error('Main: Error creating initial shell window:', error)
}
} else {
console.log('Main: Shell windows already exist, skipping initial window creation')
}

// 注册全局快捷键
presenter.shortcutPresenter.registerShortcuts()

// 监听悬浮按钮配置变化事件
eventBus.on(FLOATING_BUTTON_EVENTS.ENABLED_CHANGED, async (enabled: boolean) => {
try {
await presenter.floatingButtonPresenter.setEnabled(enabled)
} catch (error) {
console.error('Failed to set floating button enabled state:', error)
}
})

// 托盘 检测更新
eventBus.on(TRAY_EVENTS.CHECK_FOR_UPDATES, () => {
const allWindows = presenter.windowPresenter.getAllWindows()
Expand All @@ -103,38 +128,8 @@ app.whenReady().then(() => {
presenter.upgradePresenter.checkUpdate()
})

// 监听显示/隐藏窗口事件 (从托盘或快捷键触发)
eventBus.on(TRAY_EVENTS.SHOW_HIDDEN_WINDOW, (trayClick: boolean) => {
const allWindows = presenter.windowPresenter.getAllWindows()
if (allWindows.length === 0) {
presenter.windowPresenter.createShellWindow({
initialTab: {
url: 'local://chat'
}
})
} else {
// 查找目标窗口 (焦点窗口或第一个窗口)
const targetWindow = presenter.windowPresenter.getFocusedWindow() || allWindows[0]

if (!targetWindow.isDestroyed()) {
// 逻辑: 如果窗口可见且不是从托盘点击触发,则隐藏;否则显示并置顶
if (targetWindow.isVisible() && !trayClick) {
presenter.windowPresenter.hide(targetWindow.id)
} else {
presenter.windowPresenter.show(targetWindow.id)
targetWindow.focus() // 确保窗口置顶
}
} else {
console.warn('Target window for SHOW_HIDDEN_WINDOW event is destroyed.') // 保持 warn
// 如果目标窗口已销毁,创建新窗口
presenter.windowPresenter.createShellWindow({
initialTab: {
url: 'local://chat'
}
})
}
}
})
// 监听显示/隐藏窗口事件 (从托盘或快捷键或悬浮窗口触发)
eventBus.on(TRAY_EVENTS.SHOW_HIDDEN_WINDOW, handleShowHiddenWindow)

// 监听浏览器窗口获得焦点事件
app.on('browser-window-focus', () => {
Expand Down Expand Up @@ -267,15 +262,23 @@ app.whenReady().then(() => {
})
}) // app.whenReady().then 结束

// 当所有窗口都关闭时,不退出应用。macOS 平台会保留在 Dock 中,Windows 会保留在托盘。
// 用户需要通过托盘菜单或 Cmd+Q 来真正退出应用。
// 因此移除 'window-all-closed' 事件监听
/*
// 当所有主窗口都关闭时的处理逻辑
// macOS 平台会保留在 Dock 中,Windows 会保留在托盘。
// 悬浮按钮窗口不计入主窗口数量
app.on('window-all-closed', () => {
presenter.destroy()
// trayPresenter.destroy() // <-- 已移动到 will-quit
// 检查是否还有非悬浮按钮的窗口
const mainWindows = presenter.windowPresenter.getAllWindows()

if (mainWindows.length === 0) {
// 只有悬浮按钮窗口时,在非 macOS 平台退出应用
if (process.platform !== 'darwin') {
console.log('main: All main windows closed on non-macOS platform, quitting app')
app.quit()
} else {
console.log('main: All main windows closed on macOS, keeping app running in dock')
}
}
})
*/

// 在应用即将退出时触发,适合进行最终的资源清理 (如销毁托盘)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand All @@ -298,9 +301,11 @@ app.on('will-quit', (_event) => {
})

// 在应用退出之前触发,早于 will-quit。通常不如 will-quit 适合资源清理。
// 移除在此处销毁托盘的逻辑。
// 在这里销毁悬浮按钮,确保应用能正常退出
app.on('before-quit', () => {
console.log('main: app before-quit event triggered.') // 保留关键日志
// presenter.destroy() // 如果需要在 will-quit 之前清理 presenter,可以保留
// trayPresenter.destroy() // <-- 从此处移除托盘销毁
try {
presenter.floatingButtonPresenter.destroy()
} catch (error) {
console.error('main: Error destroying floating button during before-quit:', error)
}
})
22 changes: 21 additions & 1 deletion src/main/presenter/configPresenter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { DEFAULT_PROVIDERS } from './providers'
import path from 'path'
import { app, nativeTheme, shell } from 'electron'
import fs from 'fs'
import { CONFIG_EVENTS, SYSTEM_EVENTS } from '@/events'
import { CONFIG_EVENTS, SYSTEM_EVENTS, FLOATING_BUTTON_EVENTS } from '@/events'
import { McpConfHelper, SYSTEM_INMEM_MCP_SERVERS } from './mcpConfHelper'
import { presenter } from '@/presenter'
import { compare } from 'compare-versions'
Expand All @@ -43,6 +43,7 @@ interface IAppSettings {
soundEnabled?: boolean // 音效是否启用
copyWithCotEnabled?: boolean
loggingEnabled?: boolean // 日志记录是否启用
floatingButtonEnabled?: boolean // 悬浮按钮是否启用
default_system_prompt?: string // 默认系统提示词
[key: string]: unknown // 允许任意键,使用unknown类型替代any
}
Expand Down Expand Up @@ -101,6 +102,7 @@ export class ConfigPresenter implements IConfigPresenter {
soundEnabled: false,
copyWithCotEnabled: true,
loggingEnabled: false,
floatingButtonEnabled: true,
default_system_prompt: '',
appVersion: this.currentAppVersion
}
Expand Down Expand Up @@ -741,6 +743,24 @@ export class ConfigPresenter implements IConfigPresenter {
eventBus.sendToRenderer(CONFIG_EVENTS.COPY_WITH_COT_CHANGED, SendTarget.ALL_WINDOWS, enabled)
}

// 获取悬浮按钮开关状态
getFloatingButtonEnabled(): boolean {
const value = this.getSetting<boolean>('floatingButtonEnabled') ?? false
return value === undefined || value === null ? false : value
}

// 设置悬浮按钮开关状态
setFloatingButtonEnabled(enabled: boolean): void {
this.setSetting('floatingButtonEnabled', enabled)
eventBus.sendToMain(FLOATING_BUTTON_EVENTS.ENABLED_CHANGED, enabled)

try {
presenter.floatingButtonPresenter.setEnabled(enabled)
} catch (error) {
console.error('Failed to directly call floatingButtonPresenter:', error)
}
}

// ===================== MCP配置相关方法 =====================

// 获取MCP服务器配置
Expand Down
Loading