From 72e8aa3c591ec38f53310a8755d7c37b4f7d283b Mon Sep 17 00:00:00 2001 From: yyhhyyyyyy Date: Tue, 12 Aug 2025 21:51:29 +0800 Subject: [PATCH 1/2] feat: add floating button position persistence with boundary validation --- src/main/presenter/configPresenter/index.ts | 12 ++++ .../FloatingButtonWindow.ts | 70 +++++++++++++++++++ .../floatingButtonPresenter/index.ts | 5 ++ .../floatingButtonPresenter/types.ts | 7 +- 4 files changed, 93 insertions(+), 1 deletion(-) diff --git a/src/main/presenter/configPresenter/index.ts b/src/main/presenter/configPresenter/index.ts index 4d248ce53..a9e445266 100644 --- a/src/main/presenter/configPresenter/index.ts +++ b/src/main/presenter/configPresenter/index.ts @@ -46,6 +46,7 @@ interface IAppSettings { copyWithCotEnabled?: boolean loggingEnabled?: boolean // 日志记录是否启用 floatingButtonEnabled?: boolean // 悬浮按钮是否启用 + floatingButtonPosition?: { x: number; y: number } // 悬浮按钮自定义位置 default_system_prompt?: string // 默认系统提示词 [key: string]: unknown // 允许任意键,使用unknown类型替代any } @@ -108,6 +109,7 @@ export class ConfigPresenter implements IConfigPresenter { copyWithCotEnabled: true, loggingEnabled: false, floatingButtonEnabled: false, + floatingButtonPosition: undefined, default_system_prompt: '', appVersion: this.currentAppVersion } @@ -818,6 +820,16 @@ export class ConfigPresenter implements IConfigPresenter { } } + // 获取悬浮按钮位置 + getFloatingButtonPosition(): { x: number; y: number } | null { + return this.getSetting<{ x: number; y: number }>('floatingButtonPosition') || null + } + + // 设置悬浮按钮位置 + setFloatingButtonPosition(position: { x: number; y: number }): void { + this.setSetting('floatingButtonPosition', position) + } + // ===================== MCP配置相关方法 ===================== // 获取MCP服务器配置 diff --git a/src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts b/src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts index f9cc1caac..e4f06cff8 100644 --- a/src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts +++ b/src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts @@ -3,6 +3,7 @@ import path from 'path' import { FloatingButtonConfig, FloatingButtonState } from './types' import logger from '../../../shared/logger' import { platform } from '@electron-toolkit/utils' +import { presenter } from '../index' export class FloatingButtonWindow { private window: BrowserWindow | null = null @@ -178,6 +179,11 @@ export class FloatingButtonWindow { * 计算悬浮按钮位置 */ private calculatePosition(): { x: number; y: number } { + if (this.config.position === 'custom' && this.config.customPosition) { + const validated = this.validatePosition(this.config.customPosition) + return validated + } + const primaryDisplay = screen.getPrimaryDisplay() const { workAreaSize } = primaryDisplay @@ -206,6 +212,51 @@ export class FloatingButtonWindow { return { x, y } } + /** + * 验证并修正位置,确保窗口不会完全移出可见区域 + */ + private validatePosition(position: { x: number; y: number }): { x: number; y: number } { + const displays = screen.getAllDisplays() + + // 检查位置是否在任何显示器的可见区域内 + let isVisible = false + for (const display of displays) { + const { + x: displayX, + y: displayY, + width: displayWidth, + height: displayHeight + } = display.workArea + const windowRight = position.x + this.config.size.width + const windowBottom = position.y + this.config.size.height + + // 检查窗口是否与显示器区域有交集(至少部分可见) + if ( + position.x < displayX + displayWidth && + windowRight > displayX && + position.y < displayY + displayHeight && + windowBottom > displayY + ) { + isVisible = true + break + } + } + + if (isVisible) { + return position + } + + // 如果窗口不可见,将其移动到主显示器的默认位置(右下角) + logger.warn('Floating button position is out of bounds, correcting to default position') + const primaryDisplay = screen.getPrimaryDisplay() + const { workArea } = primaryDisplay + + return { + x: workArea.width - this.config.size.width - 20, + y: workArea.height - this.config.size.height - 20 + } + } + /** * 设置窗口事件监听 */ @@ -226,9 +277,28 @@ export class FloatingButtonWindow { const bounds = this.window.getBounds() this.state.bounds.x = bounds.x this.state.bounds.y = bounds.y + this.savePosition({ x: bounds.x, y: bounds.y }) } }) // 注意:悬浮按钮点击事件的 IPC 处理器在主进程的 index.ts 中设置 } + + /** + * 保存当前位置到配置 + */ + private savePosition(position: { x: number; y: number }): void { + try { + if (this.savePositionTimer) { + clearTimeout(this.savePositionTimer) + } + this.savePositionTimer = setTimeout(() => { + presenter.configPresenter.setFloatingButtonPosition(position) + logger.debug(`Floating button position saved: ${position.x}, ${position.y}`) + }, 500) + } catch (error) { + logger.error('Failed to save floating button position:', error) + } + } + private savePositionTimer: NodeJS.Timeout | null = null } diff --git a/src/main/presenter/floatingButtonPresenter/index.ts b/src/main/presenter/floatingButtonPresenter/index.ts index 5f67bb09f..0c61cdb74 100644 --- a/src/main/presenter/floatingButtonPresenter/index.ts +++ b/src/main/presenter/floatingButtonPresenter/index.ts @@ -22,12 +22,17 @@ export class FloatingButtonPresenter { */ public async initialize(config?: Partial): Promise { const floatingButtonEnabled = this.configPresenter.getFloatingButtonEnabled() + const savedPosition = this.configPresenter.getFloatingButtonPosition() try { this.config = { ...this.config, ...config, enabled: floatingButtonEnabled } + if (savedPosition) { + this.config.position = 'custom' + this.config.customPosition = savedPosition + } if (!this.config.enabled) { console.log('FloatingButton is disabled, skipping window creation') diff --git a/src/main/presenter/floatingButtonPresenter/types.ts b/src/main/presenter/floatingButtonPresenter/types.ts index d6698b52a..c4a5a041d 100644 --- a/src/main/presenter/floatingButtonPresenter/types.ts +++ b/src/main/presenter/floatingButtonPresenter/types.ts @@ -2,12 +2,17 @@ export interface FloatingButtonConfig { /** 是否启用悬浮按钮 */ enabled: boolean /** 悬浮按钮位置 */ - position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' + position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'custom' /** 距离边缘的偏移量 */ offset: { x: number y: number } + /** 自定义位置 (当 position 为 'custom' 时使用) */ + customPosition?: { + x: number + y: number + } /** 悬浮按钮大小 */ size: { width: number From 20bb8831c37c6a29e83f609aacd4c7a1ea177a3f Mon Sep 17 00:00:00 2001 From: yyhhyyyyyy Date: Tue, 12 Aug 2025 22:17:54 +0800 Subject: [PATCH 2/2] feat: refactor floating button to use electron-window-state --- src/main/presenter/configPresenter/index.ts | 12 -- .../FloatingButtonWindow.ts | 115 +++--------------- .../floatingButtonPresenter/index.ts | 5 - .../floatingButtonPresenter/types.ts | 7 +- 4 files changed, 18 insertions(+), 121 deletions(-) diff --git a/src/main/presenter/configPresenter/index.ts b/src/main/presenter/configPresenter/index.ts index a9e445266..4d248ce53 100644 --- a/src/main/presenter/configPresenter/index.ts +++ b/src/main/presenter/configPresenter/index.ts @@ -46,7 +46,6 @@ interface IAppSettings { copyWithCotEnabled?: boolean loggingEnabled?: boolean // 日志记录是否启用 floatingButtonEnabled?: boolean // 悬浮按钮是否启用 - floatingButtonPosition?: { x: number; y: number } // 悬浮按钮自定义位置 default_system_prompt?: string // 默认系统提示词 [key: string]: unknown // 允许任意键,使用unknown类型替代any } @@ -109,7 +108,6 @@ export class ConfigPresenter implements IConfigPresenter { copyWithCotEnabled: true, loggingEnabled: false, floatingButtonEnabled: false, - floatingButtonPosition: undefined, default_system_prompt: '', appVersion: this.currentAppVersion } @@ -820,16 +818,6 @@ export class ConfigPresenter implements IConfigPresenter { } } - // 获取悬浮按钮位置 - getFloatingButtonPosition(): { x: number; y: number } | null { - return this.getSetting<{ x: number; y: number }>('floatingButtonPosition') || null - } - - // 设置悬浮按钮位置 - setFloatingButtonPosition(position: { x: number; y: number }): void { - this.setSetting('floatingButtonPosition', position) - } - // ===================== MCP配置相关方法 ===================== // 获取MCP服务器配置 diff --git a/src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts b/src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts index e4f06cff8..c45e05897 100644 --- a/src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts +++ b/src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts @@ -3,12 +3,13 @@ import path from 'path' import { FloatingButtonConfig, FloatingButtonState } from './types' import logger from '../../../shared/logger' import { platform } from '@electron-toolkit/utils' -import { presenter } from '../index' +import windowStateManager from 'electron-window-state' export class FloatingButtonWindow { private window: BrowserWindow | null = null private config: FloatingButtonConfig private state: FloatingButtonState + private windowState: any constructor(config: FloatingButtonConfig) { this.config = config @@ -21,6 +22,13 @@ export class FloatingButtonWindow { height: config.size.height } } + + // 初始化窗口状态管理器 + this.windowState = windowStateManager({ + file: 'floating-button-window-state.json', + defaultWidth: config.size.width, + defaultHeight: config.size.height + }) } /** @@ -32,16 +40,14 @@ export class FloatingButtonWindow { } try { - const position = this.calculatePosition() - // 根据环境选择正确的预加载脚本路径 const isDev = process.env.NODE_ENV === 'development' this.window = new BrowserWindow({ - width: this.config.size.width, - height: this.config.size.height, - x: position.x, - y: position.y, + x: this.windowState.x, + y: this.windowState.y, + width: this.windowState.width, + height: this.windowState.height, frame: false, transparent: platform.isMacOS, alwaysOnTop: this.config.alwaysOnTop, @@ -64,6 +70,7 @@ export class FloatingButtonWindow { sandbox: false // 禁用沙盒模式,确保预加载脚本能正常工作 } }) + this.windowState.manage(this.window) this.window.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }) this.window.setAlwaysOnTop(true, 'floating') // 设置窗口透明度 @@ -141,7 +148,7 @@ export class FloatingButtonWindow { } if (config.position || config.offset) { - const position = this.calculatePosition() + const position = this.getDefaultPosition() this.window.setPosition(position.x, position.y) this.state.bounds.x = position.x this.state.bounds.y = position.y @@ -176,78 +183,9 @@ export class FloatingButtonWindow { } /** - * 计算悬浮按钮位置 + * 获取默认位置(右下角) */ - private calculatePosition(): { x: number; y: number } { - if (this.config.position === 'custom' && this.config.customPosition) { - const validated = this.validatePosition(this.config.customPosition) - return validated - } - - const primaryDisplay = screen.getPrimaryDisplay() - const { workAreaSize } = primaryDisplay - - let x: number, y: number - - switch (this.config.position) { - case 'top-left': - x = this.config.offset.x - y = this.config.offset.y - break - case 'top-right': - x = workAreaSize.width - this.config.size.width - this.config.offset.x - y = this.config.offset.y - break - case 'bottom-left': - x = this.config.offset.x - y = workAreaSize.height - this.config.size.height - this.config.offset.y - break - case 'bottom-right': - default: - x = workAreaSize.width - this.config.size.width - this.config.offset.x - y = workAreaSize.height - this.config.size.height - this.config.offset.y - break - } - - return { x, y } - } - - /** - * 验证并修正位置,确保窗口不会完全移出可见区域 - */ - private validatePosition(position: { x: number; y: number }): { x: number; y: number } { - const displays = screen.getAllDisplays() - - // 检查位置是否在任何显示器的可见区域内 - let isVisible = false - for (const display of displays) { - const { - x: displayX, - y: displayY, - width: displayWidth, - height: displayHeight - } = display.workArea - const windowRight = position.x + this.config.size.width - const windowBottom = position.y + this.config.size.height - - // 检查窗口是否与显示器区域有交集(至少部分可见) - if ( - position.x < displayX + displayWidth && - windowRight > displayX && - position.y < displayY + displayHeight && - windowBottom > displayY - ) { - isVisible = true - break - } - } - - if (isVisible) { - return position - } - - // 如果窗口不可见,将其移动到主显示器的默认位置(右下角) - logger.warn('Floating button position is out of bounds, correcting to default position') + private getDefaultPosition(): { x: number; y: number } { const primaryDisplay = screen.getPrimaryDisplay() const { workArea } = primaryDisplay @@ -277,28 +215,9 @@ export class FloatingButtonWindow { const bounds = this.window.getBounds() this.state.bounds.x = bounds.x this.state.bounds.y = bounds.y - this.savePosition({ x: bounds.x, y: bounds.y }) } }) // 注意:悬浮按钮点击事件的 IPC 处理器在主进程的 index.ts 中设置 } - - /** - * 保存当前位置到配置 - */ - private savePosition(position: { x: number; y: number }): void { - try { - if (this.savePositionTimer) { - clearTimeout(this.savePositionTimer) - } - this.savePositionTimer = setTimeout(() => { - presenter.configPresenter.setFloatingButtonPosition(position) - logger.debug(`Floating button position saved: ${position.x}, ${position.y}`) - }, 500) - } catch (error) { - logger.error('Failed to save floating button position:', error) - } - } - private savePositionTimer: NodeJS.Timeout | null = null } diff --git a/src/main/presenter/floatingButtonPresenter/index.ts b/src/main/presenter/floatingButtonPresenter/index.ts index 0c61cdb74..5f67bb09f 100644 --- a/src/main/presenter/floatingButtonPresenter/index.ts +++ b/src/main/presenter/floatingButtonPresenter/index.ts @@ -22,17 +22,12 @@ export class FloatingButtonPresenter { */ public async initialize(config?: Partial): Promise { const floatingButtonEnabled = this.configPresenter.getFloatingButtonEnabled() - const savedPosition = this.configPresenter.getFloatingButtonPosition() try { this.config = { ...this.config, ...config, enabled: floatingButtonEnabled } - if (savedPosition) { - this.config.position = 'custom' - this.config.customPosition = savedPosition - } if (!this.config.enabled) { console.log('FloatingButton is disabled, skipping window creation') diff --git a/src/main/presenter/floatingButtonPresenter/types.ts b/src/main/presenter/floatingButtonPresenter/types.ts index c4a5a041d..d6698b52a 100644 --- a/src/main/presenter/floatingButtonPresenter/types.ts +++ b/src/main/presenter/floatingButtonPresenter/types.ts @@ -2,17 +2,12 @@ export interface FloatingButtonConfig { /** 是否启用悬浮按钮 */ enabled: boolean /** 悬浮按钮位置 */ - position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'custom' + position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' /** 距离边缘的偏移量 */ offset: { x: number y: number } - /** 自定义位置 (当 position 为 'custom' 时使用) */ - customPosition?: { - x: number - y: number - } /** 悬浮按钮大小 */ size: { width: number