diff --git a/src/main/events.ts b/src/main/events.ts index dae18ecaa..996df620e 100644 --- a/src/main/events.ts +++ b/src/main/events.ts @@ -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' // 悬浮按钮启用状态改变 diff --git a/src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts b/src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts index 41b7d4d5b..f9cc1caac 100644 --- a/src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts +++ b/src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts @@ -170,6 +170,10 @@ export class FloatingButtonWindow { return this.window !== null && !this.window.isDestroyed() } + public getWindow(): BrowserWindow | null { + return this.window + } + /** * 计算悬浮按钮位置 */ diff --git a/src/main/presenter/floatingButtonPresenter/index.ts b/src/main/presenter/floatingButtonPresenter/index.ts index a7b2833dd..5f67bb09f 100644 --- a/src/main/presenter/floatingButtonPresenter/index.ts +++ b/src/main/presenter/floatingButtonPresenter/index.ts @@ -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 @@ -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 @@ -107,12 +108,40 @@ export class FloatingButtonPresenter { */ private async createFloatingWindow(): Promise { 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) + } + }) + + 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) { @@ -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() + } + } + + 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) + } } } diff --git a/src/main/presenter/tabPresenter.ts b/src/main/presenter/tabPresenter.ts index 27053a0e7..037b56379 100644 --- a/src/main/presenter/tabPresenter.ts +++ b/src/main/presenter/tabPresenter.ts @@ -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) + } + } } diff --git a/src/main/presenter/windowPresenter/FloatingChatWindow.ts b/src/main/presenter/windowPresenter/FloatingChatWindow.ts new file mode 100644 index 000000000..b97233600 --- /dev/null +++ b/src/main/presenter/windowPresenter/FloatingChatWindow.ts @@ -0,0 +1,398 @@ +import { BrowserWindow, screen, nativeImage } from 'electron' +import path from 'path' +import logger from '../../../shared/logger' +import { platform, is } from '@electron-toolkit/utils' +import icon from '../../../../resources/icon.png?asset' +import iconWin from '../../../../resources/icon.ico?asset' +import { eventBus } from '../../eventbus' +import { TAB_EVENTS } from '../../events' +import { presenter } from '../' + +interface FloatingChatConfig { + size: { + width: number + height: number + } + minSize: { + width: number + height: number + } + opacity: number + alwaysOnTop: boolean +} + +interface FloatingButtonPosition { + x: number + y: number + width: number + height: number +} + +const DEFAULT_FLOATING_CHAT_CONFIG: FloatingChatConfig = { + size: { + width: 400, + height: 600 + }, + minSize: { + width: 350, + height: 450 + }, + opacity: 0.95, + alwaysOnTop: true +} + +export class FloatingChatWindow { + private window: BrowserWindow | null = null + private config: FloatingChatConfig + private isVisible: boolean = false + private shouldShowWhenReady: boolean = false + + constructor(config?: Partial) { + this.config = { + ...DEFAULT_FLOATING_CHAT_CONFIG, + ...config + } + } + + public async create(floatingButtonPosition?: FloatingButtonPosition): Promise { + if (this.window) { + return + } + + try { + const position = this.calculatePosition(floatingButtonPosition) + const iconFile = nativeImage.createFromPath(process.platform === 'win32' ? iconWin : icon) + const isDev = is.dev + + this.window = new BrowserWindow({ + width: this.config.size.width, + height: this.config.size.height, + minWidth: this.config.minSize.width, + minHeight: this.config.minSize.height, + x: position.x, + y: position.y, + frame: false, + transparent: true, + alwaysOnTop: this.config.alwaysOnTop, + skipTaskbar: true, + resizable: true, + minimizable: false, + maximizable: false, + closable: true, + show: false, + movable: true, + autoHideMenuBar: true, + icon: iconFile, + vibrancy: platform.isMacOS ? 'under-window' : undefined, + visualEffectState: platform.isMacOS ? 'followWindow' : undefined, + backgroundMaterial: platform.isWindows ? 'mica' : undefined, + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + preload: path.join(__dirname, '../preload/index.mjs'), + webSecurity: false, + devTools: isDev, + sandbox: false + } + }) + + this.window.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }) + this.window.setAlwaysOnTop(true, 'floating') + this.window.setOpacity(this.config.opacity) + this.setupWindowEvents() + this.registerVirtualTab() + + logger.info('FloatingChatWindow created successfully') + + this.loadPageContent() + .then(() => logger.info('FloatingChatWindow page content loaded')) + .catch((error) => logger.error('Failed to load FloatingChatWindow page content:', error)) + } catch (error) { + logger.error('Failed to create FloatingChatWindow:', error) + throw error + } + } + + public show(floatingButtonPosition?: FloatingButtonPosition): void { + if (!this.window) { + return + } + + if (floatingButtonPosition) { + const position = this.calculatePosition(floatingButtonPosition) + this.window.setPosition(position.x, position.y) + } + if (!this.window.isVisible()) { + if (this.window.webContents.isLoading() === false) { + this.window.show() + this.window.focus() + this.refreshWindowData() + } else { + this.window.show() + this.window.focus() + this.shouldShowWhenReady = true + this.window.webContents.once('did-finish-load', () => { + if (this.shouldShowWhenReady) { + this.refreshWindowData() + this.shouldShowWhenReady = false + } + }) + } + } else { + this.window.show() + this.window.focus() + this.refreshWindowData() + } + this.isVisible = true + logger.debug('FloatingChatWindow shown') + } + + public hide(): void { + if (!this.window) { + return + } + + this.window.hide() + this.isVisible = false + logger.debug('FloatingChatWindow hidden') + } + + public toggle(floatingButtonPosition?: FloatingButtonPosition): void { + if (this.isVisible) { + this.hide() + } else { + this.show(floatingButtonPosition) + } + } + + public destroy(): void { + if (this.window) { + this.unregisterVirtualTab() + try { + if (!this.window.isDestroyed()) { + this.window.destroy() + } + } catch (error) { + logger.error('Error destroying FloatingChatWindow:', error) + } + this.window = null + this.isVisible = false + logger.debug('FloatingChatWindow destroyed') + } + } + + public isShowing(): boolean { + return this.window !== null && !this.window.isDestroyed() && this.isVisible + } + + public getWindow(): BrowserWindow | null { + return this.window + } + + private refreshWindowData(): void { + if (this.window && !this.window.isDestroyed()) { + logger.debug('Refreshing floating window data') + setTimeout(() => { + if (this.window && !this.window.isDestroyed()) { + eventBus.sendToMain(TAB_EVENTS.RENDERER_TAB_READY, this.window.webContents.id) + } + }, 100) + } + } + + private registerVirtualTab(): void { + if (!this.window || this.window.isDestroyed()) { + return + } + + try { + const tabPresenter = presenter.tabPresenter + if (tabPresenter) { + const webContentsId = this.window.webContents.id + logger.info(`Registering virtual tab for floating window, WebContents ID: ${webContentsId}`) + tabPresenter.registerFloatingWindow(webContentsId, this.window.webContents) + } + } catch (error) { + logger.error('Failed to register virtual tab for floating window:', error) + } + } + + private unregisterVirtualTab(): void { + if (!this.window) { + return + } + + try { + const tabPresenter = presenter.tabPresenter + if (tabPresenter) { + const webContentsId = this.window.webContents.id + logger.info( + `Unregistering virtual tab for floating window, WebContents ID: ${webContentsId}` + ) + tabPresenter.unregisterFloatingWindow(webContentsId) + } + } catch (error) { + logger.error('Failed to unregister virtual tab for floating window:', error) + } + } + private calculatePosition(floatingButtonPosition?: FloatingButtonPosition): { + x: number + y: number + } { + const primaryDisplay = screen.getPrimaryDisplay() + const { workArea } = primaryDisplay + let x: number, y: number + + if (!floatingButtonPosition) { + x = workArea.x + workArea.width - this.config.size.width - 20 + y = workArea.y + workArea.height - this.config.size.height - 20 + return { x, y } + } + + const buttonX = floatingButtonPosition.x + const buttonY = floatingButtonPosition.y + const buttonWidth = floatingButtonPosition.width + const buttonHeight = floatingButtonPosition.height + const windowWidth = this.config.size.width + const windowHeight = this.config.size.height + const gap = 15 + const buttonCenterX = buttonX + buttonWidth / 2 + const buttonCenterY = buttonY + buttonHeight / 2 + const screenCenterX = workArea.x + workArea.width / 2 + const screenCenterY = workArea.y + workArea.height / 2 + + let positions: Array<{ x: number; y: number; priority: number }> = [] + if (buttonX + buttonWidth + gap + windowWidth <= workArea.x + workArea.width) { + positions.push({ + x: buttonX + buttonWidth + gap, + y: Math.max( + workArea.y, + Math.min( + buttonY + (buttonHeight - windowHeight) / 2, + workArea.y + workArea.height - windowHeight + ) + ), + priority: buttonCenterX < screenCenterX ? 1 : 3 + }) + } + + if (buttonX - gap - windowWidth >= workArea.x) { + positions.push({ + x: buttonX - gap - windowWidth, + y: Math.max( + workArea.y, + Math.min( + buttonY + (buttonHeight - windowHeight) / 2, + workArea.y + workArea.height - windowHeight + ) + ), + priority: buttonCenterX >= screenCenterX ? 1 : 3 + }) + } + + if (buttonY + buttonHeight + gap + windowHeight <= workArea.y + workArea.height) { + positions.push({ + x: Math.max( + workArea.x, + Math.min( + buttonX + (buttonWidth - windowWidth) / 2, + workArea.x + workArea.width - windowWidth + ) + ), + y: buttonY + buttonHeight + gap, + priority: buttonCenterY < screenCenterY ? 2 : 4 + }) + } + + if (buttonY - gap - windowHeight >= workArea.y) { + positions.push({ + x: Math.max( + workArea.x, + Math.min( + buttonX + (buttonWidth - windowWidth) / 2, + workArea.x + workArea.width - windowWidth + ) + ), + y: buttonY - gap - windowHeight, + priority: buttonCenterY >= screenCenterY ? 2 : 4 + }) + } + + if (positions.length === 0) { + x = workArea.x + workArea.width - windowWidth - 20 + y = workArea.y + workArea.height - windowHeight - 20 + } else { + positions.sort((a, b) => a.priority - b.priority) + x = positions[0].x + y = positions[0].y + } + x = Math.max(workArea.x + 10, Math.min(x, workArea.x + workArea.width - windowWidth - 10)) + y = Math.max(workArea.y + 10, Math.min(y, workArea.y + workArea.height - windowHeight - 10)) + return { x, y } + } + + private async loadPageContent(): Promise { + if (!this.window || this.window.isDestroyed()) { + throw new Error('Window is not available for page loading') + } + + const isDev = is.dev + if (isDev) { + await this.window.loadURL('http://localhost:5173/') + } else { + await this.window.loadFile(path.join(__dirname, '../renderer/index.html')) + } + + this.window.webContents.once('did-finish-load', () => { + logger.info('FloatingChatWindow did-finish-load, requesting fresh data') + setTimeout(async () => { + if (this.window && !this.window.isDestroyed()) { + logger.info(`Broadcasting thread list update for floating window`) + eventBus.sendToMain(TAB_EVENTS.RENDERER_TAB_READY, this.window.webContents.id) + } + }, 300) + }) + } + + private setupWindowEvents(): void { + if (!this.window) { + return + } + + this.window.on('ready-to-show', () => { + if (this.window && !this.window.isDestroyed()) { + if (this.shouldShowWhenReady) { + this.window.show() + this.window.focus() + this.shouldShowWhenReady = false + this.refreshWindowData() + } + } + }) + + this.window.on('close', (event) => { + const windowPresenter = presenter.windowPresenter + const isAppQuitting = windowPresenter?.isApplicationQuitting() || false + if (isAppQuitting) { + logger.info('App is quitting, allowing FloatingChatWindow to close normally') + return + } + event.preventDefault() + this.hide() + logger.debug('FloatingChatWindow close prevented, window hidden instead') + }) + + this.window.on('closed', () => { + this.window = null + this.isVisible = false + }) + + this.window.on('show', () => { + this.isVisible = true + }) + + this.window.on('hide', () => { + this.isVisible = false + }) + } +} diff --git a/src/main/presenter/windowPresenter/index.ts b/src/main/presenter/windowPresenter/index.ts index c35c25262..a05201bf7 100644 --- a/src/main/presenter/windowPresenter/index.ts +++ b/src/main/presenter/windowPresenter/index.ts @@ -13,6 +13,7 @@ import windowStateManager from 'electron-window-state' // 窗口状态管理器 import { SHORTCUT_EVENTS } from '@/events' // 快捷键事件常量 // TrayPresenter 在 main/index.ts 中全局管理,本 Presenter 不负责其生命周期 import { TabPresenter } from '../tabPresenter' // TabPresenter 类型 +import { FloatingChatWindow } from './FloatingChatWindow' // 悬浮对话窗口 /** * 窗口 Presenter,负责管理所有 BrowserWindow 实例及其生命周期。 @@ -38,6 +39,7 @@ export class WindowPresenter implements IWindowPresenter { hasInitialFocus: boolean } >() + private floatingChatWindow: FloatingChatWindow | null = null constructor(configPresenter: ConfigPresenter) { this.windows = new Map() @@ -57,6 +59,7 @@ export class WindowPresenter implements IWindowPresenter { app.on('before-quit', () => { console.log('App is quitting, setting isQuitting flag.') this.isQuitting = true + this.destroyFloatingChatWindow() }) // 监听快捷键事件:创建新窗口 @@ -171,16 +174,26 @@ export class WindowPresenter implements IWindowPresenter { * @param filePath 文件路径。 */ previewFile(filePath: string): void { - const window = this.mainWindow - if (window) { + let targetWindow = this.getFocusedWindow() + if (!targetWindow && this.floatingChatWindow && this.floatingChatWindow.isShowing()) { + const floatingWindow = this.floatingChatWindow.getWindow() + if (floatingWindow) { + targetWindow = floatingWindow + } + } + if (!targetWindow) { + targetWindow = this.mainWindow + } + + if (targetWindow && !targetWindow.isDestroyed()) { console.log(`Previewing file: ${filePath}`) if (process.platform === 'darwin') { - window.previewFile(filePath) + targetWindow.previewFile(filePath) } else { shell.openPath(filePath) // 使用系统默认应用打开 } } else { - console.warn('Cannot preview file, no valid main window found.') + console.warn('Cannot preview file, no valid window found.') } } @@ -487,6 +500,17 @@ export class WindowPresenter implements IWindowPresenter { console.warn(`Skipping sending message "${channel}" to destroyed window ${window.id}.`) } } + + if (this.floatingChatWindow && this.floatingChatWindow.isShowing()) { + const floatingWindow = this.floatingChatWindow.getWindow() + if (floatingWindow && !floatingWindow.isDestroyed()) { + try { + floatingWindow.webContents.send(channel, ...args) + } catch (error) { + console.error(`Error sending message "${channel}" to floating chat window:`, error) + } + } + } } /** @@ -1078,4 +1102,80 @@ export class WindowPresenter implements IWindowPresenter { return false // 过程中发生错误 } } + + public async createFloatingChatWindow(): Promise { + if (this.floatingChatWindow) { + console.log('FloatingChatWindow already exists') + return + } + + try { + this.floatingChatWindow = new FloatingChatWindow() + await this.floatingChatWindow.create() + console.log('FloatingChatWindow created successfully') + } catch (error) { + console.error('Failed to create FloatingChatWindow:', error) + this.floatingChatWindow = null + throw error + } + } + + public async showFloatingChatWindow(floatingButtonPosition?: { + x: number + y: number + width: number + height: number + }): Promise { + if (!this.floatingChatWindow) { + await this.createFloatingChatWindow() + } + + if (this.floatingChatWindow) { + this.floatingChatWindow.show(floatingButtonPosition) + console.log('FloatingChatWindow shown') + } + } + + public hideFloatingChatWindow(): void { + if (this.floatingChatWindow) { + this.floatingChatWindow.hide() + console.log('FloatingChatWindow hidden') + } + } + + public async toggleFloatingChatWindow(floatingButtonPosition?: { + x: number + y: number + width: number + height: number + }): Promise { + if (!this.floatingChatWindow) { + await this.createFloatingChatWindow() + } + + if (this.floatingChatWindow) { + this.floatingChatWindow.toggle(floatingButtonPosition) + console.log('FloatingChatWindow toggled') + } + } + + public destroyFloatingChatWindow(): void { + if (this.floatingChatWindow) { + this.floatingChatWindow.destroy() + this.floatingChatWindow = null + console.log('FloatingChatWindow destroyed') + } + } + + public isFloatingChatWindowVisible(): boolean { + return this.floatingChatWindow?.isShowing() || false + } + + public getFloatingChatWindow(): FloatingChatWindow | null { + return this.floatingChatWindow + } + + public isApplicationQuitting(): boolean { + return this.isQuitting + } } diff --git a/src/preload/floating-preload.ts b/src/preload/floating-preload.ts index 4639912b8..2839f9a9b 100644 --- a/src/preload/floating-preload.ts +++ b/src/preload/floating-preload.ts @@ -2,7 +2,8 @@ import { contextBridge, ipcRenderer } from 'electron' // 直接定义事件常量,避免路径解析问题 const FLOATING_BUTTON_EVENTS = { - CLICKED: 'floating-button:clicked' + CLICKED: 'floating-button:clicked', + RIGHT_CLICKED: 'floating-button:right-clicked' } as const // 定义悬浮按钮的 API @@ -16,6 +17,14 @@ const floatingButtonAPI = { } }, + onRightClick: () => { + try { + ipcRenderer.send(FLOATING_BUTTON_EVENTS.RIGHT_CLICKED) + } catch (error) { + console.error('FloatingPreload: Error sending right click IPC message:', error) + } + }, + // 监听来自主进程的事件 onConfigUpdate: (callback: (config: any) => void) => { ipcRenderer.on('floating-button-config-update', (_event, config) => { diff --git a/src/renderer/floating/FloatingButton.vue b/src/renderer/floating/FloatingButton.vue index 0806e973c..db4b0f9f7 100644 --- a/src/renderer/floating/FloatingButton.vue +++ b/src/renderer/floating/FloatingButton.vue @@ -7,6 +7,7 @@ class="w-15 h-15 rounded-full border-2 border-white/30 flex items-center justify-center cursor-pointer transition-all duration-300 relative overflow-hidden select-none floating-button no-drag" :class="{ 'floating-button-pulse': isPulsing }" @click="handleClick" + @contextmenu="handleRightClick" @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave" > @@ -48,6 +49,28 @@ const handleClick = () => { } } +const handleRightClick = (event: MouseEvent) => { + event.preventDefault() + if (floatingButton.value) { + floatingButton.value.style.transform = 'scale(0.9)' + setTimeout(() => { + if (floatingButton.value) { + floatingButton.value.style.transform = '' + } + }, 150) + } + + if (window.floatingButtonAPI) { + try { + window.floatingButtonAPI.onRightClick() + } catch (error) { + console.error('=== FloatingButton: Error calling onRightClick API ===:', error) + } + } else { + console.error('=== FloatingButton: floatingButtonAPI not available ===') + } +} + // 鼠标事件处理 const handleMouseEnter = () => { isPulsing.value = false diff --git a/src/renderer/floating/env.d.ts b/src/renderer/floating/env.d.ts index 6f624ddac..58ffc5d98 100644 --- a/src/renderer/floating/env.d.ts +++ b/src/renderer/floating/env.d.ts @@ -11,6 +11,7 @@ declare global { interface Window { floatingButtonAPI: { onClick: () => void + onRightClick: () => void onConfigUpdate: (callback: (config: any) => void) => void removeAllListeners: () => void } diff --git a/src/shared/presenter.d.ts b/src/shared/presenter.d.ts index e74ee929e..777b6177a 100644 --- a/src/shared/presenter.d.ts +++ b/src/shared/presenter.d.ts @@ -178,6 +178,7 @@ export interface IWindowPresenter { sendToWindow(windowId: number, channel: string, ...args: unknown[]): boolean sendToDefaultTab(channel: string, switchToTarget?: boolean, ...args: unknown[]): Promise closeWindow(windowId: number, forceClose?: boolean): Promise + isApplicationQuitting(): boolean } export interface ITabPresenter { @@ -214,6 +215,8 @@ export interface ITabPresenter { onRendererTabReady(tabId: number): Promise onRendererTabActivated(threadId: string): Promise isLastTabInWindow(tabId: number): Promise + registerFloatingWindow(webContentsId: number, webContents: Electron.WebContents): void + unregisterFloatingWindow(webContentsId: number): void resetTabToBlank(tabId: number): Promise }