Skip to content
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
1 change: 1 addition & 0 deletions src/main/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ export const MEETING_EVENTS = {
// 悬浮按钮相关事件
export const FLOATING_BUTTON_EVENTS = {
CLICKED: 'floating-button:clicked', // 悬浮按钮被点击
RIGHT_CLICKED: 'floating-button:right-clicked', // 悬浮按钮被右键点击
VISIBILITY_CHANGED: 'floating-button:visibility-changed', // 悬浮按钮显示状态改变
POSITION_CHANGED: 'floating-button:position-changed', // 悬浮按钮位置改变
ENABLED_CHANGED: 'floating-button:enabled-changed' // 悬浮按钮启用状态改变
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ export class FloatingButtonWindow {
return this.window !== null && !this.window.isDestroyed()
}

public getWindow(): BrowserWindow | null {
return this.window
}

/**
* 计算悬浮按钮位置
*/
Expand Down
122 changes: 116 additions & 6 deletions src/main/presenter/floatingButtonPresenter/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { FloatingButtonWindow } from './FloatingButtonWindow'
import { FloatingButtonConfig, FloatingButtonState, DEFAULT_FLOATING_BUTTON_CONFIG } from './types'
import { ConfigPresenter } from '../configPresenter'
import { ipcMain } from 'electron'
import { ipcMain, Menu, app } from 'electron'
import { FLOATING_BUTTON_EVENTS } from '@/events'
import { handleShowHiddenWindow } from '@/utils'
import { presenter } from '../index'

export class FloatingButtonPresenter {
private floatingWindow: FloatingButtonWindow | null = null
Expand Down Expand Up @@ -48,6 +48,7 @@ export class FloatingButtonPresenter {
this.config.enabled = false

ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.CLICKED)
ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.RIGHT_CLICKED)
if (this.floatingWindow) {
this.floatingWindow.destroy()
this.floatingWindow = null
Expand Down Expand Up @@ -107,12 +108,40 @@ export class FloatingButtonPresenter {
*/
private async createFloatingWindow(): Promise<void> {
ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.CLICKED)
ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.RIGHT_CLICKED)

ipcMain.on(FLOATING_BUTTON_EVENTS.CLICKED, () => {
ipcMain.on(FLOATING_BUTTON_EVENTS.CLICKED, async () => {
try {
// 触发内置事件处理器
handleShowHiddenWindow(true)
} catch {}
let floatingButtonPosition: { x: number; y: number; width: number; height: number } | null =
null
if (this.floatingWindow && this.floatingWindow.exists()) {
const buttonWindow = this.floatingWindow.getWindow()
if (buttonWindow && !buttonWindow.isDestroyed()) {
const bounds = buttonWindow.getBounds()
floatingButtonPosition = {
x: bounds.x,
y: bounds.y,
width: bounds.width,
height: bounds.height
}
}
}
if (floatingButtonPosition) {
await presenter.windowPresenter.toggleFloatingChatWindow(floatingButtonPosition)
} else {
await presenter.windowPresenter.toggleFloatingChatWindow()
}
} catch (error) {
console.error('Failed to handle floating button click:', error)
}
})
Comment on lines +113 to +137
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Pass-through of button bounds is good; consider multi-display correctness

You pass the floating button’s bounds to help position the chat window near the button. However, FloatingChatWindow.calculatePosition() uses screen.getPrimaryDisplay(), which can misplace the window when the button is on a non-primary monitor. Prefer deriving the display nearest the button center and using that display’s workArea.

Additional change needed in FloatingChatWindow (for reference):

- const primaryDisplay = screen.getPrimaryDisplay()
- const { workArea } = primaryDisplay
+ const buttonCenter = floatingButtonPosition
+  ? { x: floatingButtonPosition.x + floatingButtonPosition.width / 2,
+      y: floatingButtonPosition.y + floatingButtonPosition.height / 2 }
+  : undefined
+ const display = buttonCenter
+  ? screen.getDisplayNearestPoint(buttonCenter)
+  : screen.getPrimaryDisplay()
+ const { workArea } = display

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/presenter/floatingButtonPresenter/index.ts around lines 113 to 137,
the presenter passes button bounds but the downstream
FloatingChatWindow.calculatePosition() still uses screen.getPrimaryDisplay(),
which will misplace the chat on multi-monitor setups; update the code so the
display nearest the button center is used (use screen.getDisplayNearestPoint or
equivalent with the button center x/y) and pass that display’s workArea (or pass
the display object) into calculatePosition(), and modify
FloatingChatWindow.calculatePosition() to prefer the provided display/workArea
over screen.getPrimaryDisplay().


ipcMain.on(FLOATING_BUTTON_EVENTS.RIGHT_CLICKED, () => {
try {
this.showContextMenu()
} catch (error) {
console.error('Failed to handle floating button right click:', error)
}
})

if (!this.floatingWindow) {
Expand All @@ -122,5 +151,86 @@ export class FloatingButtonPresenter {

// 悬浮按钮创建后立即显示
this.floatingWindow.show()

this.preCreateFloatingChatWindow()
}

private preCreateFloatingChatWindow(): void {
try {
presenter.windowPresenter.createFloatingChatWindow().catch((error) => {
console.error('Failed to pre-create floating chat window:', error)
})
console.log('Started pre-creating floating chat window in background')
} catch (error) {
console.error('Error starting pre-creation of floating chat window:', error)
}
}

private showContextMenu(): void {
const template = [
{
label: '打开主窗口',
click: () => {
this.openMainWindow()
}
},
{
type: 'separator' as const
},
{
label: '退出应用',
click: () => {
this.exitApplication()
}
}
]

const contextMenu = Menu.buildFromTemplate(template)

if (this.floatingWindow && this.floatingWindow.exists()) {
const buttonWindow = this.floatingWindow.getWindow()
if (buttonWindow && !buttonWindow.isDestroyed()) {
contextMenu.popup({ window: buttonWindow })
return
}
}

const mainWindow = presenter.windowPresenter.mainWindow
if (mainWindow) {
contextMenu.popup({ window: mainWindow })
} else {
contextMenu.popup()
}
}
Comment on lines +169 to +204
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Localize context menu labels and reuse existing i18n infra

Hardcoded Chinese labels break localization. Reuse your i18n helpers (like getContextMenuLabels) or add keys for “Open main window” and “Exit application” to keep UX consistent.

Example:

- const template = [
-   { label: '打开主窗口', click: () => this.openMainWindow() },
-   { type: 'separator' as const },
-   { label: '退出应用', click: () => this.exitApplication() }
- ]
+ const lang = presenter?.configPresenter?.getLanguage?.() ?? 'en'
+ const labels = { openMain: /* t('menu.openMain') by lang */, exitApp: /* t('menu.exitApp') */ }
+ const template = [
+   { label: labels.openMain, click: () => this.openMainWindow() },
+   { type: 'separator' as const },
+   { label: labels.exitApp, click: () => this.exitApplication() }
+ ]

I can wire this to your existing i18n utilities on request.

Committable suggestion skipped: line range outside the PR's diff.


private openMainWindow(): void {
try {
const windowPresenter = presenter.windowPresenter
if (windowPresenter) {
const mainWindow = windowPresenter.mainWindow
if (mainWindow && !mainWindow.isDestroyed()) {
if (mainWindow.isMinimized()) {
mainWindow.restore()
}
mainWindow.show()
mainWindow.focus()
console.log('Main window opened from floating button context menu')
} else {
windowPresenter.createShellWindow({ initialTab: { url: 'local://chat' } })
console.log('Created new main window from floating button context menu')
}
}
} catch (error) {
console.error('Failed to open main window from floating button:', error)
}
}

private exitApplication(): void {
try {
console.log('Exiting application from floating button context menu')
app.quit()
} catch (error) {
console.error('Failed to exit application from floating button:', error)
}
}
Comment on lines +228 to 235
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Route app exit through WindowPresenter to set isQuitting before window closes

Direct app.quit() can race with close handlers that rely on isQuitting. Emit the FORCE_QUIT_APP event instead, which your WindowPresenter already handles to set isQuitting then quit.

-  private exitApplication(): void {
-    try {
-      console.log('Exiting application from floating button context menu')
-      app.quit()
-    } catch (error) {
-      console.error('Failed to exit application from floating button:', error)
-    }
-  }
+  private exitApplication(): void {
+    try {
+      console.log('Exiting application from floating button context menu')
+      // Ensure centralized quit path sets isQuitting before windows receive 'close'
+      const { eventBus } = require('@/eventbus')
+      const { WINDOW_EVENTS } = require('@/events')
+      eventBus.sendToMain(WINDOW_EVENTS.FORCE_QUIT_APP)
+    } catch (error) {
+      console.error('Failed to exit application from floating button:', error)
+    }
+  }

Note: add missing imports if your bundler supports them at top-level instead of require().

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private exitApplication(): void {
try {
console.log('Exiting application from floating button context menu')
app.quit()
} catch (error) {
console.error('Failed to exit application from floating button:', error)
}
}
private exitApplication(): void {
try {
console.log('Exiting application from floating button context menu')
// Ensure centralized quit path sets isQuitting before windows receive 'close'
const { eventBus } = require('@/eventbus')
const { WINDOW_EVENTS } = require('@/events')
eventBus.sendToMain(WINDOW_EVENTS.FORCE_QUIT_APP)
} catch (error) {
console.error('Failed to exit application from floating button:', error)
}
}
🤖 Prompt for AI Agents
In src/main/presenter/floatingButtonPresenter/index.ts around lines 228 to 235,
replace the direct app.quit() call with emitting the existing FORCE_QUIT_APP
event that WindowPresenter listens for so isQuitting is set before windows
close; keep the try/catch and logs but remove app.quit(), import the
FORCE_QUIT_APP constant (and the app/event emitter or WindowPresenter helper
used to emit it) at top-level, and call the emitter/WindowPresenter to emit
FORCE_QUIT_APP instead of invoking app.quit() directly.

}
36 changes: 36 additions & 0 deletions src/main/presenter/tabPresenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -942,4 +942,40 @@ export class TabPresenter implements ITabPresenter {
}
}
}

registerFloatingWindow(webContentsId: number, webContents: Electron.WebContents): void {
try {
console.log(`TabPresenter: Registering floating window as virtual tab, ID: ${webContentsId}`)
if (this.tabs.has(webContentsId)) {
console.warn(`TabPresenter: Tab ${webContentsId} already exists, skipping registration`)
return
}
const virtualView = {
webContents: webContents,
setVisible: () => {},
setBounds: () => {},
getBounds: () => ({ x: 0, y: 0, width: 400, height: 600 })
} as any
this.webContentsToTabId.set(webContentsId, webContentsId)
this.tabs.set(webContentsId, virtualView)
console.log(
`TabPresenter: Virtual tab registered successfully for floating window ${webContentsId}`
)
} catch (error) {
console.error('TabPresenter: Failed to register floating window:', error)
}
}

unregisterFloatingWindow(webContentsId: number): void {
try {
console.log(`TabPresenter: Unregistering floating window virtual tab, ID: ${webContentsId}`)
this.webContentsToTabId.delete(webContentsId)
this.tabs.delete(webContentsId)
console.log(
`TabPresenter: Virtual tab unregistered successfully for floating window ${webContentsId}`
)
} catch (error) {
console.error('TabPresenter: Failed to unregister floating window:', error)
}
}
Comment on lines +946 to +980
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid storing non-WebContentsView “virtual tabs” in Map<number, WebContentsView>

Using a plain object cast as any risks runtime/typing issues if later code assumes a real WebContentsView (e.g., addChildView, setBorderRadius). Recommend isolating virtual entries in a separate map or introducing a safe abstraction instead of widening via any.

Proposed direction:

  • Keep tabs: Map<number, WebContentsView> strictly for real views.
  • Add virtualTabs: Map<number, { webContents: Electron.WebContents }> for floating windows.
  • Update getTabIdByWebContentsId/getWindowIdByWebContentsId to consult both maps, or keep only webContentsToTabId as source of truth.
    This avoids accidental operations (attach/detach/bring-to-front/bounds updates) on non-view objects.
🤖 Prompt for AI Agents
In src/main/presenter/tabPresenter.ts around lines 946 to 980, the current
registerFloatingWindow/unregisterFloatingWindow functions insert a plain object
cast to any into tabs: Map<number, WebContentsView>, which can break code that
assumes real WebContentsView methods; instead create a new private virtualTabs:
Map<number, { webContents: Electron.WebContents }>, stop inserting virtual
entries into tabs, set webContentsToTabId as before to preserve lookup, and move
the virtual object into virtualTabs in registerFloatingWindow and remove it from
virtualTabs in unregisterFloatingWindow; also update any tab-lookup helpers
(getTabIdByWebContentsId/getWindowIdByWebContentsId) to consult
webContentsToTabId and then check tabs first and virtualTabs second (or rely
solely on webContentsToTabId) and ensure callers that operate on WebContentsView
guard against virtual entries so real view-only operations aren’t invoked on
virtualTabs.

}
Loading