mirror of
https://github.com/viarotel-org/escrcpy.git
synced 2024-11-23 23:21:02 +01:00
feat: ✨ Support edge hiding function
This commit is contained in:
parent
ae7859bac3
commit
14a81de211
@ -228,10 +228,11 @@ Windows 及 Linux 端内部集成了 Gnirehtet, 用于提供 PC 到安卓设
|
||||
23. 浮动操作栏 ✅
|
||||
24. 增强录制功能 ✅
|
||||
25. 启动APP(多线程) ✅
|
||||
26. 改进历史设备连接体验 🚧
|
||||
27. 文件管理支持上传目录及进度展示🚧
|
||||
28. 对设备进行分组 🚧
|
||||
29. 游戏键位映射 🚧
|
||||
26. 主窗口贴边隐藏 ✅
|
||||
27. 改进历史设备连接体验 🚧
|
||||
28. 文件管理支持上传目录及进度展示🚧
|
||||
29. 对设备进行分组 🚧
|
||||
30. 游戏键位映射 🚧
|
||||
|
||||
## 常见问题
|
||||
|
||||
|
@ -226,10 +226,11 @@ Refer to [scrcpy/doc/shortcuts](https://github.com/Genymobile/scrcpy/blob/master
|
||||
23. Floating control bar ✅
|
||||
24. Enhanced recording ✅
|
||||
25. Start APP(Multi-threaded) ✅
|
||||
26. Improved history device connection experience 🚧
|
||||
27. File management supports upload directory and progress display 🚧
|
||||
28. Device grouping 🚧
|
||||
29. Game key mapping 🚧
|
||||
26. Main window edge hidden ✅
|
||||
27. Improved history device connection experience 🚧
|
||||
28. File management supports upload directory and progress display 🚧
|
||||
29. Device grouping 🚧
|
||||
30. Game key mapping 🚧
|
||||
|
||||
## FAQ
|
||||
|
||||
|
599
electron/helpers/edger/index.js
Normal file
599
electron/helpers/edger/index.js
Normal file
@ -0,0 +1,599 @@
|
||||
import { screen } from 'electron'
|
||||
|
||||
export class Edger {
|
||||
constructor(window) {
|
||||
if (!window) {
|
||||
throw new Error('Window instance is required')
|
||||
}
|
||||
|
||||
this.window = window
|
||||
this.isHidden = false
|
||||
this.dockEdge = null
|
||||
this.originalBounds = null
|
||||
this.animationTimer = null
|
||||
this.showDebounceTimer = null
|
||||
this.hideDebounceTimer = null
|
||||
this.isDragging = false
|
||||
this.isAnimating = false
|
||||
this.lastMousePosition = null
|
||||
this.mouseMovementBuffer = []
|
||||
this.lastAnimationTime = 0
|
||||
|
||||
// Animation configs
|
||||
this.animationDuration = 300
|
||||
this.animationSteps = 30
|
||||
this.visiblePortion = 2
|
||||
this.mouseBufferSize = 5
|
||||
this.mouseVelocityThreshold = 50
|
||||
this.animationCooldown = 100
|
||||
|
||||
// Thresholds
|
||||
this.snapThreshold = 10
|
||||
this.undockThreshold = 20
|
||||
this.showHideThreshold = 50
|
||||
this.stablePositionThreshold = 3
|
||||
|
||||
// Window topping
|
||||
this.wasAlwaysOnTop = window.isAlwaysOnTop()
|
||||
this.handleWindowBlur = this.handleWindowBlur.bind(this)
|
||||
this.handleWindowFocus = this.handleWindowFocus.bind(this)
|
||||
|
||||
this.initialize()
|
||||
}
|
||||
|
||||
easeOutCubic(t) {
|
||||
return 1 - (1 - t) ** 3
|
||||
}
|
||||
|
||||
easeInCubic(t) {
|
||||
return t * t * t
|
||||
}
|
||||
|
||||
easeInOutCubic(t) {
|
||||
return t < 0.5
|
||||
? 4 * t * t * t
|
||||
: 1 - (-2 * t + 2) ** 3 / 2
|
||||
}
|
||||
|
||||
addBounceEffect(finalBounds) {
|
||||
const bounceSteps = 15
|
||||
const bounceDistance = 5
|
||||
let step = 0
|
||||
|
||||
const bounceBounds = { ...finalBounds }
|
||||
switch (this.dockEdge) {
|
||||
case 'right':
|
||||
bounceBounds.x -= bounceDistance
|
||||
break
|
||||
case 'left':
|
||||
bounceBounds.x += bounceDistance
|
||||
break
|
||||
case 'top':
|
||||
bounceBounds.y += bounceDistance
|
||||
break
|
||||
}
|
||||
|
||||
const bounceTimer = setInterval(() => {
|
||||
step++
|
||||
const progress = step / bounceSteps
|
||||
const easeProgress = this.easeInOutCubic(progress)
|
||||
|
||||
const currentBounds = {
|
||||
x: Math.round(bounceBounds.x + (finalBounds.x - bounceBounds.x) * easeProgress),
|
||||
y: Math.round(bounceBounds.y + (finalBounds.y - bounceBounds.y) * easeProgress),
|
||||
width: finalBounds.width,
|
||||
height: finalBounds.height,
|
||||
}
|
||||
|
||||
try {
|
||||
this.window.setBounds(currentBounds)
|
||||
}
|
||||
catch (err) {
|
||||
console.error('Failed to set window bounds during bounce:', err)
|
||||
clearInterval(bounceTimer)
|
||||
this.isAnimating = false
|
||||
return
|
||||
}
|
||||
|
||||
if (step >= bounceSteps) {
|
||||
clearInterval(bounceTimer)
|
||||
this.window.setBounds(finalBounds)
|
||||
this.isAnimating = false
|
||||
}
|
||||
}, 16)
|
||||
}
|
||||
|
||||
showWindow() {
|
||||
if (!this.isHidden || this.isAnimating)
|
||||
return
|
||||
clearTimeout(this.hideDebounceTimer)
|
||||
|
||||
if (this.showDebounceTimer)
|
||||
return
|
||||
|
||||
this.showDebounceTimer = setTimeout(() => {
|
||||
this.animateWindow(this.originalBounds, false)
|
||||
this.isHidden = false
|
||||
this.showDebounceTimer = null
|
||||
}, 100)
|
||||
}
|
||||
|
||||
hideWindow() {
|
||||
if (this.isHidden || this.isAnimating)
|
||||
return
|
||||
clearTimeout(this.showDebounceTimer)
|
||||
|
||||
if (this.hideDebounceTimer)
|
||||
return
|
||||
|
||||
this.hideDebounceTimer = setTimeout(() => {
|
||||
const hiddenBounds = this.getHiddenBounds()
|
||||
this.animateWindow(hiddenBounds, true)
|
||||
this.isHidden = true
|
||||
this.hideDebounceTimer = null
|
||||
}, 300)
|
||||
}
|
||||
|
||||
initialize() {
|
||||
// Track window movement
|
||||
this.window.on('move', () => {
|
||||
if (!this.isHidden) {
|
||||
this.checkEdgeSnap()
|
||||
}
|
||||
})
|
||||
|
||||
// Track drag start
|
||||
this.window.on('will-move', () => {
|
||||
this.isDragging = true
|
||||
if (this.dockEdge) {
|
||||
this.checkUndock()
|
||||
}
|
||||
})
|
||||
|
||||
// Track drag end
|
||||
this.window.on('moved', () => {
|
||||
this.isDragging = false
|
||||
})
|
||||
|
||||
// Track mouse position
|
||||
this.startMouseTracking()
|
||||
|
||||
// Add window focus event listening
|
||||
this.window.on('blur', this.handleWindowBlur)
|
||||
this.window.on('focus', this.handleWindowFocus)
|
||||
|
||||
// Check initial status
|
||||
if (this.window.isAlwaysOnTop()) {
|
||||
this.wasAlwaysOnTop = true
|
||||
}
|
||||
}
|
||||
|
||||
handleWindowBlur() {
|
||||
if (this.isHidden) {
|
||||
this.setAlwaysOnTop(true)
|
||||
}
|
||||
}
|
||||
|
||||
handleWindowFocus() {
|
||||
if (!this.isHidden && !this.wasAlwaysOnTop) {
|
||||
this.setAlwaysOnTop(false)
|
||||
}
|
||||
}
|
||||
|
||||
setAlwaysOnTop(value) {
|
||||
try {
|
||||
// 某些系统上可能需要特定的参数
|
||||
if (process.platform === 'darwin') {
|
||||
this.window.setAlwaysOnTop(value, 'floating')
|
||||
}
|
||||
else {
|
||||
this.window.setAlwaysOnTop(value)
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error('Failed to set always on top:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 改进获取隐藏位置的方法,确保窗口边缘始终可见
|
||||
getHiddenBounds() {
|
||||
const currentBounds = this.window.getBounds()
|
||||
const display = screen.getDisplayNearestPoint({
|
||||
x: currentBounds.x,
|
||||
y: currentBounds.y,
|
||||
})
|
||||
const screenBounds = display.workArea
|
||||
|
||||
let hiddenBounds = { ...currentBounds }
|
||||
const minVisiblePixels = 3 // 确保至少有3个像素可见
|
||||
|
||||
switch (this.dockEdge) {
|
||||
case 'right':
|
||||
hiddenBounds.x = screenBounds.x + screenBounds.width - minVisiblePixels
|
||||
break
|
||||
case 'left':
|
||||
hiddenBounds.x = screenBounds.x - currentBounds.width + minVisiblePixels
|
||||
break
|
||||
case 'top':
|
||||
hiddenBounds.y = screenBounds.y - currentBounds.height + minVisiblePixels
|
||||
break
|
||||
}
|
||||
|
||||
// 确保窗口不会完全隐藏
|
||||
hiddenBounds = {
|
||||
x: Math.round(hiddenBounds.x),
|
||||
y: Math.round(hiddenBounds.y),
|
||||
width: currentBounds.width,
|
||||
height: currentBounds.height,
|
||||
}
|
||||
|
||||
return hiddenBounds
|
||||
}
|
||||
|
||||
// 添加窗口位置恢复方法
|
||||
restoreWindowPosition() {
|
||||
if (!this.dockEdge || !this.originalBounds)
|
||||
return
|
||||
|
||||
const display = screen.getDisplayNearestPoint({
|
||||
x: this.originalBounds.x,
|
||||
y: this.originalBounds.y,
|
||||
})
|
||||
const screenBounds = display.workArea
|
||||
|
||||
// 确保窗口在屏幕范围内
|
||||
const restoredBounds = { ...this.originalBounds }
|
||||
|
||||
switch (this.dockEdge) {
|
||||
case 'right':
|
||||
restoredBounds.x = Math.min(
|
||||
restoredBounds.x,
|
||||
screenBounds.x + screenBounds.width - restoredBounds.width,
|
||||
)
|
||||
break
|
||||
case 'left':
|
||||
restoredBounds.x = Math.max(restoredBounds.x, screenBounds.x)
|
||||
break
|
||||
case 'top':
|
||||
restoredBounds.y = Math.max(restoredBounds.y, screenBounds.y)
|
||||
break
|
||||
}
|
||||
|
||||
this.window.setBounds(restoredBounds)
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.cleanupAnimation()
|
||||
if (this.showDebounceTimer) {
|
||||
clearTimeout(this.showDebounceTimer)
|
||||
}
|
||||
if (this.hideDebounceTimer) {
|
||||
clearTimeout(this.hideDebounceTimer)
|
||||
}
|
||||
|
||||
// 清理事件监听
|
||||
if (this.window) {
|
||||
this.window.removeListener('blur', this.handleWindowBlur)
|
||||
this.window.removeListener('focus', this.handleWindowFocus)
|
||||
this.window.removeAllListeners()
|
||||
|
||||
// 恢复原始置顶状态
|
||||
if (this.window.isAlwaysOnTop() !== this.wasAlwaysOnTop) {
|
||||
this.setAlwaysOnTop(this.wasAlwaysOnTop)
|
||||
}
|
||||
}
|
||||
|
||||
this.mouseMovementBuffer = []
|
||||
}
|
||||
|
||||
startMouseTracking() {
|
||||
const trackMouse = () => {
|
||||
if (!this.dockEdge)
|
||||
return
|
||||
|
||||
const currentTime = Date.now()
|
||||
const mousePos = screen.getCursorScreenPoint()
|
||||
|
||||
// Update mouse movement buffer
|
||||
this.updateMouseBuffer(mousePos, currentTime)
|
||||
|
||||
const windowBounds = this.window.getBounds()
|
||||
const display = screen.getDisplayNearestPoint(mousePos)
|
||||
const screenBounds = display.workArea
|
||||
|
||||
// Check that the mouse is stable
|
||||
if (this.isMouseStable()) {
|
||||
if (this.isMouseNearEdge(mousePos, windowBounds, screenBounds)) {
|
||||
this.showWindow()
|
||||
}
|
||||
else if (this.isMouseOutsideWindow(mousePos, windowBounds)) {
|
||||
this.hideWindow()
|
||||
}
|
||||
}
|
||||
|
||||
this.lastMousePosition = mousePos
|
||||
}
|
||||
|
||||
setInterval(trackMouse, 16)
|
||||
}
|
||||
|
||||
updateMouseBuffer(mousePos, currentTime) {
|
||||
this.mouseMovementBuffer.push({
|
||||
x: mousePos.x,
|
||||
y: mousePos.y,
|
||||
time: currentTime,
|
||||
})
|
||||
|
||||
if (this.mouseMovementBuffer.length > this.mouseBufferSize) {
|
||||
this.mouseMovementBuffer.shift()
|
||||
}
|
||||
}
|
||||
|
||||
isMouseStable() {
|
||||
if (this.mouseMovementBuffer.length < this.mouseBufferSize)
|
||||
return true
|
||||
|
||||
const recentMovements = this.mouseMovementBuffer.slice(-this.stablePositionThreshold)
|
||||
const firstPos = recentMovements[0]
|
||||
|
||||
return recentMovements.every(pos =>
|
||||
Math.abs(pos.x - firstPos.x) <= this.stablePositionThreshold
|
||||
&& Math.abs(pos.y - firstPos.y) <= this.stablePositionThreshold,
|
||||
)
|
||||
}
|
||||
|
||||
calculateMouseVelocity() {
|
||||
if (this.mouseMovementBuffer.length < 2)
|
||||
return 0
|
||||
|
||||
const latest = this.mouseMovementBuffer[this.mouseMovementBuffer.length - 1]
|
||||
const previous = this.mouseMovementBuffer[this.mouseMovementBuffer.length - 2]
|
||||
const timeDiff = latest.time - previous.time
|
||||
|
||||
if (timeDiff === 0)
|
||||
return 0
|
||||
|
||||
const distance = Math.sqrt(
|
||||
(latest.x - previous.x) ** 2
|
||||
+ (latest.y - previous.y) ** 2,
|
||||
)
|
||||
|
||||
return distance / timeDiff
|
||||
}
|
||||
|
||||
isMouseNearEdge(mousePos, windowBounds, screenBounds) {
|
||||
const threshold = this.snapThreshold
|
||||
|
||||
// Consider the offset of the window position
|
||||
const offset = 5
|
||||
const velocity = this.calculateMouseVelocity()
|
||||
|
||||
// If the mouse moves too fast, the display is not triggered
|
||||
if (velocity > this.mouseVelocityThreshold)
|
||||
return false
|
||||
|
||||
switch (this.dockEdge) {
|
||||
case 'right': {
|
||||
const rightEdge = screenBounds.x + screenBounds.width
|
||||
return mousePos.x >= rightEdge - threshold
|
||||
&& mousePos.y >= windowBounds.y - offset
|
||||
&& mousePos.y <= windowBounds.y + windowBounds.height + offset
|
||||
}
|
||||
case 'left': {
|
||||
return mousePos.x <= screenBounds.x + threshold
|
||||
&& mousePos.y >= windowBounds.y - offset
|
||||
&& mousePos.y <= windowBounds.y + windowBounds.height + offset
|
||||
}
|
||||
case 'top': {
|
||||
return mousePos.y <= screenBounds.y + threshold
|
||||
&& mousePos.x >= windowBounds.x - offset
|
||||
&& mousePos.x <= windowBounds.x + windowBounds.width + offset
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
isMouseOutsideWindow(mousePos, windowBounds) {
|
||||
const margin = 10 // Add edge tolerance
|
||||
return mousePos.x < windowBounds.x - margin
|
||||
|| mousePos.x > windowBounds.x + windowBounds.width + margin
|
||||
|| mousePos.y < windowBounds.y - margin
|
||||
|| mousePos.y > windowBounds.y + windowBounds.height + margin
|
||||
}
|
||||
|
||||
animateWindow(targetBounds, isHiding = false) {
|
||||
const currentTime = Date.now()
|
||||
if (currentTime - this.lastAnimationTime < this.animationCooldown) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.animationTimer) {
|
||||
clearInterval(this.animationTimer)
|
||||
}
|
||||
|
||||
if (this.isAnimating)
|
||||
return
|
||||
|
||||
this.isAnimating = true
|
||||
this.lastAnimationTime = currentTime
|
||||
|
||||
const startBounds = this.window.getBounds()
|
||||
// Make sure the start and destination positions are integers
|
||||
const sanitizedTargetBounds = {
|
||||
x: Math.round(targetBounds.x),
|
||||
y: Math.round(targetBounds.y),
|
||||
width: Math.round(targetBounds.width),
|
||||
height: Math.round(targetBounds.height),
|
||||
}
|
||||
|
||||
let step = 0
|
||||
let lastBounds = startBounds
|
||||
|
||||
const animate = () => {
|
||||
step++
|
||||
const progress = step / this.animationSteps
|
||||
const easeProgress = isHiding
|
||||
? this.easeInCubic(progress)
|
||||
: this.easeOutCubic(progress)
|
||||
|
||||
const currentBounds = {
|
||||
x: Math.round(startBounds.x + (sanitizedTargetBounds.x - startBounds.x) * easeProgress),
|
||||
y: Math.round(startBounds.y + (sanitizedTargetBounds.y - startBounds.y) * easeProgress),
|
||||
width: startBounds.width,
|
||||
height: startBounds.height,
|
||||
}
|
||||
|
||||
// Prevent the same location from being set repeatedly
|
||||
if (this.boundsEqual(currentBounds, lastBounds)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this.window.setBounds(currentBounds)
|
||||
lastBounds = currentBounds
|
||||
}
|
||||
catch (err) {
|
||||
console.error('Animation error:', err)
|
||||
this.cleanupAnimation()
|
||||
return
|
||||
}
|
||||
|
||||
if (step >= this.animationSteps) {
|
||||
this.window.setBounds(sanitizedTargetBounds)
|
||||
this.cleanupAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
this.animationTimer = setInterval(animate, this.animationDuration / this.animationSteps)
|
||||
}
|
||||
|
||||
boundsEqual(bounds1, bounds2) {
|
||||
return bounds1.x === bounds2.x
|
||||
&& bounds1.y === bounds2.y
|
||||
&& bounds1.width === bounds2.width
|
||||
&& bounds1.height === bounds2.height
|
||||
}
|
||||
|
||||
cleanupAnimation() {
|
||||
clearInterval(this.animationTimer)
|
||||
this.animationTimer = null
|
||||
this.isAnimating = false
|
||||
}
|
||||
|
||||
checkUndock() {
|
||||
if (!this.dockEdge || !this.isDragging)
|
||||
return
|
||||
|
||||
const windowBounds = this.window.getBounds()
|
||||
const display = screen.getDisplayNearestPoint({
|
||||
x: windowBounds.x,
|
||||
y: windowBounds.y,
|
||||
})
|
||||
const screenBounds = display.workArea
|
||||
|
||||
const distanceFromRight = Math.abs(windowBounds.x + windowBounds.width - screenBounds.x - screenBounds.width)
|
||||
const distanceFromLeft = Math.abs(windowBounds.x - screenBounds.x)
|
||||
const distanceFromTop = Math.abs(windowBounds.y - screenBounds.y)
|
||||
|
||||
let shouldUndock = false
|
||||
|
||||
switch (this.dockEdge) {
|
||||
case 'right':
|
||||
shouldUndock = distanceFromRight > this.undockThreshold
|
||||
break
|
||||
case 'left':
|
||||
shouldUndock = distanceFromLeft > this.undockThreshold
|
||||
break
|
||||
case 'top':
|
||||
shouldUndock = distanceFromTop > this.undockThreshold
|
||||
break
|
||||
}
|
||||
|
||||
if (shouldUndock) {
|
||||
this.undock()
|
||||
}
|
||||
}
|
||||
|
||||
undock() {
|
||||
this.dockEdge = null
|
||||
this.originalBounds = null
|
||||
this.isHidden = false
|
||||
if (this.animationTimer) {
|
||||
clearInterval(this.animationTimer)
|
||||
this.animationTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
checkEdgeSnap() {
|
||||
if (this.isDragging && this.dockEdge) {
|
||||
this.checkUndock()
|
||||
return
|
||||
}
|
||||
|
||||
const windowBounds = this.window.getBounds()
|
||||
const display = screen.getDisplayNearestPoint({
|
||||
x: windowBounds.x,
|
||||
y: windowBounds.y,
|
||||
})
|
||||
const screenBounds = display.workArea
|
||||
|
||||
const distanceFromRight = Math.abs(windowBounds.x + windowBounds.width - screenBounds.x - screenBounds.width)
|
||||
const distanceFromLeft = Math.abs(windowBounds.x - screenBounds.x)
|
||||
const distanceFromTop = Math.abs(windowBounds.y - screenBounds.y)
|
||||
|
||||
// Check right edge
|
||||
if (distanceFromRight < this.snapThreshold) {
|
||||
this.dockToEdge('right', windowBounds)
|
||||
}
|
||||
// Check left edge
|
||||
else if (distanceFromLeft < this.snapThreshold) {
|
||||
this.dockToEdge('left', windowBounds)
|
||||
}
|
||||
// Check top edge
|
||||
else if (distanceFromTop < this.snapThreshold) {
|
||||
this.dockToEdge('top', windowBounds)
|
||||
}
|
||||
}
|
||||
|
||||
dockToEdge(edge, bounds) {
|
||||
this.dockEdge = edge
|
||||
this.originalBounds = bounds
|
||||
|
||||
const display = screen.getDisplayNearestPoint({
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
})
|
||||
const screenBounds = display.workArea
|
||||
|
||||
// Snap to exact position
|
||||
switch (edge) {
|
||||
case 'right':
|
||||
this.window.setBounds({
|
||||
x: screenBounds.x + screenBounds.width - bounds.width,
|
||||
y: bounds.y,
|
||||
width: bounds.width,
|
||||
height: bounds.height,
|
||||
})
|
||||
break
|
||||
case 'left':
|
||||
this.window.setBounds({
|
||||
x: screenBounds.x,
|
||||
y: bounds.y,
|
||||
width: bounds.width,
|
||||
height: bounds.height,
|
||||
})
|
||||
break
|
||||
case 'top':
|
||||
this.window.setBounds({
|
||||
x: bounds.x,
|
||||
y: screenBounds.y,
|
||||
width: bounds.width,
|
||||
height: bounds.height,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Edger
|
@ -20,6 +20,8 @@ import control from '$control/electron/main.js'
|
||||
|
||||
import { loadPage } from './helpers/index.js'
|
||||
|
||||
import { Edger } from './helpers/edger/index.js'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
@ -76,6 +78,8 @@ function createWindow() {
|
||||
remote.enable(mainWindow.webContents)
|
||||
remote.initialize()
|
||||
|
||||
new Edger(mainWindow);
|
||||
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
mainWindow.show()
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user