mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-21 21:05:09 +00:00
* feat: pointer display and pointer screenshot tool * chore: fix ts issues * fix: use custom element with shadow dom for pointer overlay * fix: ts issues * Update apps/controller-ext/src/utils/PointerOverlay.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: Dani Akash <DaniAkash@users.noreply.github.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
143 lines
4.3 KiB
TypeScript
143 lines
4.3 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 BrowserOS
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
|
|
import { getBrowserOSAdapter } from '@/adapters/BrowserOSAdapter'
|
|
import { logger } from '@/utils/logger'
|
|
|
|
const POINTER_DISPLAY_DURATION_MS = 3000
|
|
const POINTER_DELAY_BEFORE_ACTION_MS = 500
|
|
|
|
/**
|
|
* PointerOverlay - Shows a visual mouse pointer overlay before actions
|
|
*
|
|
* Injects JavaScript to display a pointer arrow at the specified coordinates.
|
|
* The pointer auto-removes after POINTER_DISPLAY_DURATION_MS.
|
|
*
|
|
* biome-ignore lint/complexity/noStaticOnlyClass: class created before biome was setup
|
|
*/
|
|
export class PointerOverlay {
|
|
private static browserOS = getBrowserOSAdapter()
|
|
|
|
/**
|
|
* Show a pointer at the specified coordinates
|
|
* @param tabId - Tab to show pointer in
|
|
* @param x - X coordinate in viewport pixels
|
|
* @param y - Y coordinate in viewport pixels
|
|
* @param text - Optional label text (e.g., "Click", "Type: hello...")
|
|
*/
|
|
static async showPointer(
|
|
tabId: number,
|
|
x: number,
|
|
y: number,
|
|
text?: string,
|
|
): Promise<void> {
|
|
const pointerId = `browseros-pointer-${Date.now()}`
|
|
|
|
const textLabel = text
|
|
? `
|
|
var label = document.createElement('div');
|
|
label.style.cssText = 'position: absolute; top: 20px; left: 12px; background: rgba(0,0,0,0.9); color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-family: monospace; white-space: nowrap; box-shadow: 0 2px 4px rgba(0,0,0,0.5);';
|
|
label.textContent = '${text.replace(/[`$\\]/g, '\\$&').replace(/'/g, "\\'")}';
|
|
shadow.appendChild(label);
|
|
`
|
|
: ''
|
|
|
|
const script = `
|
|
(function() {
|
|
var existing = document.querySelector('browseros-pointer');
|
|
if (existing) existing.remove();
|
|
|
|
if (!customElements.get('browseros-pointer')) {
|
|
customElements.define('browseros-pointer', class extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this.attachShadow({ mode: 'open' });
|
|
}
|
|
});
|
|
}
|
|
|
|
var host = document.createElement('browseros-pointer');
|
|
host.id = '${pointerId}';
|
|
host.style.cssText = 'position: fixed; left: ${x}px; top: ${y}px; z-index: 2147483647; pointer-events: none;';
|
|
|
|
var shadow = host.shadowRoot;
|
|
|
|
var arrow = document.createElement('div');
|
|
arrow.style.cssText = 'width: 0; height: 0; border-style: solid; border-width: 0 12px 20px 12px; border-color: transparent transparent #FB6618 transparent; transform: translate(-3px, -3px) rotate(45deg); filter: drop-shadow(1px 1px 2px rgba(0,0,0,0.4));';
|
|
shadow.appendChild(arrow);
|
|
|
|
${textLabel}
|
|
|
|
document.body.appendChild(host);
|
|
|
|
setTimeout(function() {
|
|
var el = document.getElementById('${pointerId}');
|
|
if (el) el.remove();
|
|
}, ${POINTER_DISPLAY_DURATION_MS});
|
|
})();
|
|
`
|
|
|
|
try {
|
|
await PointerOverlay.browserOS.executeJavaScript(tabId, script)
|
|
logger.debug(
|
|
`[PointerOverlay] Showed pointer at (${x}, ${y}) in tab ${tabId}${text ? ` with label "${text}"` : ''}`,
|
|
)
|
|
} catch (error) {
|
|
logger.warn(
|
|
`[PointerOverlay] Failed to show pointer: ${error instanceof Error ? error.message : String(error)}`,
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show pointer and wait before action
|
|
* Returns after the delay so the action can proceed
|
|
*/
|
|
static async showPointerAndWait(
|
|
tabId: number,
|
|
x: number,
|
|
y: number,
|
|
text?: string,
|
|
): Promise<void> {
|
|
await PointerOverlay.showPointer(tabId, x, y, text)
|
|
await PointerOverlay.delay(POINTER_DELAY_BEFORE_ACTION_MS)
|
|
}
|
|
|
|
/**
|
|
* Calculate center coordinates from a rect
|
|
*/
|
|
static getCenterCoordinates(rect: {
|
|
x: number
|
|
y: number
|
|
width: number
|
|
height: number
|
|
}): { x: number; y: number } {
|
|
return {
|
|
x: Math.round(rect.x + rect.width / 2),
|
|
y: Math.round(rect.y + rect.height / 2),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate left-center coordinates (for type actions)
|
|
*/
|
|
static getLeftCenterCoordinates(rect: {
|
|
x: number
|
|
y: number
|
|
width: number
|
|
height: number
|
|
}): { x: number; y: number } {
|
|
return {
|
|
x: Math.round(rect.x + 10),
|
|
y: Math.round(rect.y + rect.height / 2),
|
|
}
|
|
}
|
|
|
|
private static delay(ms: number): Promise<void> {
|
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
}
|
|
}
|