-
Notifications
You must be signed in to change notification settings - Fork 649
feat: add floating button position persistence with boundary validation #729
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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Enhance the validation logic and error handling. The position validation is good but could be improved:
Apply this improvement to the validation logic: 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 { 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
- // 检查窗口是否与显示器区域有交集(至少部分可见)
+ // Ensure at least 25% of the button is visible within the display
+ const minVisibleWidth = this.config.size.width * 0.25
+ const minVisibleHeight = this.config.size.height * 0.25
+ const visibleWidth = Math.min(windowRight, displayX + displayWidth) - Math.max(position.x, displayX)
+ const visibleHeight = Math.min(windowBottom, displayY + displayHeight) - Math.max(position.y, displayY)
+
if (
- position.x < displayX + displayWidth &&
- windowRight > displayX &&
- position.y < displayY + displayHeight &&
- windowBottom > displayY
+ visibleWidth >= minVisibleWidth && visibleHeight >= minVisibleHeight
) {
isVisible = true
break
}
}
if (isVisible) {
return position
}
- // 如果窗口不可见,将其移动到主显示器的默认位置(右下角)
+ // Move to primary display's bottom-right corner if position is invalid
logger.warn('Floating button position is out of bounds, correcting to default position')
const primaryDisplay = screen.getPrimaryDisplay()
- const { workArea } = primaryDisplay
+ const { x: workX, y: workY, width: workWidth, height: workHeight } = primaryDisplay.workArea
return {
- x: workArea.width - this.config.size.width - 20,
- y: workArea.height - this.config.size.height - 20
+ x: workX + workWidth - this.config.size.width - 20,
+ y: workY + workHeight - this.config.size.height - 20
}
}🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 设置窗口事件监听 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Improve the position saving implementation. The debounced saving is a good approach, but there are some improvements needed:
Move the timer property declaration and improve the implementation: + private savePositionTimer: NodeJS.Timeout | null = null
+
/**
* 保存当前位置到配置
*/
private savePosition(position: { x: number; y: number }): void {
try {
if (this.savePositionTimer) {
clearTimeout(this.savePositionTimer)
}
+
+ const SAVE_DEBOUNCE_DELAY = 500
this.savePositionTimer = setTimeout(() => {
presenter.configPresenter.setFloatingButtonPosition(position)
logger.debug(`Floating button position saved: ${position.x}, ${position.y}`)
- }, 500)
+ this.savePositionTimer = null
+ }, SAVE_DEBOUNCE_DELAY)
} catch (error) {
- logger.error('Failed to save floating button position:', error)
+ logger.error(`Failed to save floating button position (${position.x}, ${position.y}):`, error)
}
}
- private savePositionTimer: NodeJS.Timeout | null = null📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
建议直接使用目前已经存在的窗口状态管理器来实现该功能
import windowStateManager from 'electron-window-state' // 窗口状态管理器
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://github.com/BetaHuhn/electron-win-state
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
已使用 electron-window-state