fix bug for vnc stand mode
This commit is contained in:
parent
1ff8e1ce07
commit
f6cdaa8bce
@ -1,10 +1,8 @@
|
||||
// @ts-ignore - noVNC types are not available
|
||||
import RFB from "@novnc/novnc";
|
||||
// ui/src/ui/components/claw-computer-panel.ts
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { customElement, state, property } from "lit/decorators.js";
|
||||
import { createRef, ref, Ref } from "lit/directives/ref.js";
|
||||
// @ts-ignore - noVNC types are not available
|
||||
|
||||
// Compatible with both .default and non-.default versions
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@ -13,7 +11,7 @@ const RFBClass = (RFB as any).default || RFB;
|
||||
// RFB instance type definition
|
||||
interface RFBInstance {
|
||||
disconnect(): void;
|
||||
addEventListener(event: string, callback: (e?: unknown) => void): void;
|
||||
addEventListener(event: string, callback: (e: unknown) => void): void;
|
||||
scaleViewport: boolean;
|
||||
clipViewport: boolean;
|
||||
resizeSession: boolean;
|
||||
@ -30,9 +28,31 @@ export class ClawComputerPanel extends LitElement {
|
||||
@state() isConnected = false;
|
||||
@state() isFitted = true;
|
||||
|
||||
// Floating window state
|
||||
@state() private isFloating = false;
|
||||
@state() private dockedOffsetY = 0;
|
||||
@state() private dockedOffsetX = 0;
|
||||
@state() private floatingRect = { x: 0, y: 0, width: 600, height: 400 };
|
||||
|
||||
private rfb: RFBInstance | null = null;
|
||||
private screenRef: Ref<HTMLDivElement> = createRef<HTMLDivElement>();
|
||||
private autoConnectAttempted = false;
|
||||
private dragStart = { x: 0, y: 0 };
|
||||
private initialRect = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
offsetY: 0,
|
||||
offsetX: 0,
|
||||
containerHeight: 0,
|
||||
containerTop: 0,
|
||||
containerLeft: 0,
|
||||
containerWidth: 0,
|
||||
};
|
||||
private isDragging = false;
|
||||
private isResizing = false;
|
||||
private resizeEdge = "";
|
||||
private aspectRatio = 1;
|
||||
|
||||
@property({ type: Boolean }) enabled = false;
|
||||
|
||||
@ -55,13 +75,40 @@ export class ClawComputerPanel extends LitElement {
|
||||
background: var(--bg-accent);
|
||||
color: var(--text);
|
||||
font-family: system-ui, sans-serif;
|
||||
--vnc-border-color: var(--border);
|
||||
--vnc-window-bg: color-mix(in srgb, var(--bg-accent), black 10%);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:host {
|
||||
--vnc-border-color: color-mix(in srgb, var(--border), black 15%);
|
||||
--vnc-window-bg: color-mix(in srgb, var(--bg-accent), black 10%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:host {
|
||||
--vnc-border-color: color-mix(in srgb, var(--border), white 15%);
|
||||
--vnc-window-bg: color-mix(in srgb, var(--bg-accent), white 5%);
|
||||
}
|
||||
}
|
||||
|
||||
:host([theme="light"]) {
|
||||
--vnc-border-color: color-mix(in srgb, var(--border), black 15%);
|
||||
--vnc-window-bg: color-mix(in srgb, var(--bg-accent), black 10%);
|
||||
}
|
||||
:host([theme="dark"]) {
|
||||
--vnc-border-color: color-mix(in srgb, var(--border), white 15%);
|
||||
--vnc-window-bg: color-mix(in srgb, var(--bg-accent), white 5%);
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.screen-container {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
@ -73,46 +120,52 @@ export class ClawComputerPanel extends LitElement {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.screen {
|
||||
/* Remove fixed width/height to allow shrink-wrapping */
|
||||
width: auto;
|
||||
height: auto;
|
||||
/* Maximize but preserve aspect ratio via flex item behavior */
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
/* macOS-style window border wrapper */
|
||||
padding: 6px;
|
||||
padding-top: 36px; /* Extra space for title bar controls */
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--bg-accent),
|
||||
black 10%
|
||||
); /* Ensure darker than container */
|
||||
padding: 5.4px;
|
||||
padding-top: 32.4px;
|
||||
background: var(--vnc-window-bg);
|
||||
border-radius: 6px;
|
||||
box-shadow:
|
||||
0 0 0 1px var(--border),
|
||||
0 0 0 1px var(--vnc-border-color, var(--border)),
|
||||
0 20px 50px rgba(0, 0, 0, 0.4);
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.screen > *:not(.drag-handle):not(.window-controls):not(.resize-handle) {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.screen.dragging > *:not(.drag-handle):not(.window-controls):not(.resize-handle) {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
.screen.dragging canvas {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
/* Window controls (fake traffic lights) */
|
||||
.window-controls {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
top: 10.8px;
|
||||
left: 10.8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
z-index: 20;
|
||||
gap: 7.2px;
|
||||
z-index: 25;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.window-control {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
width: 10.8px;
|
||||
height: 10.8px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
@ -120,6 +173,7 @@ export class ClawComputerPanel extends LitElement {
|
||||
transform 0.1s,
|
||||
opacity 0.2s;
|
||||
}
|
||||
|
||||
.window-control:hover {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.1);
|
||||
@ -129,32 +183,29 @@ export class ClawComputerPanel extends LitElement {
|
||||
background-color: #27c93f;
|
||||
border-color: #1aab29;
|
||||
}
|
||||
|
||||
.window-control.close {
|
||||
background-color: #ff5f56;
|
||||
border-color: #e0443e;
|
||||
}
|
||||
.screen > div:not(.window-controls) {
|
||||
/* Fix for noVNC wrapper div */
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
background: transparent !important;
|
||||
|
||||
.window-control.minimize {
|
||||
background-color: #ffbd2e;
|
||||
}
|
||||
|
||||
.screen canvas {
|
||||
/* Force canvas to maintain aspect ratio within container */
|
||||
max-width: 100% !important;
|
||||
max-height: 100% !important;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
/* Remove default canvas outline/border */
|
||||
outline: none;
|
||||
display: block; /* Remove inline gap */
|
||||
display: block;
|
||||
margin: auto !important;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
.status-overlay {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
@ -166,20 +217,144 @@ export class ClawComputerPanel extends LitElement {
|
||||
color: var(--text);
|
||||
font-weight: 500;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
z-index: 100;
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.screen.floating {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
top: 0;
|
||||
left: 0;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 32.4px;
|
||||
cursor: grab;
|
||||
z-index: 20;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
background: transparent;
|
||||
z-index: 30;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.resize-handle.top {
|
||||
top: -5px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 10px;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.resize-handle.bottom {
|
||||
bottom: -5px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 10px;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.resize-handle.left {
|
||||
left: -5px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 10px;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.resize-handle.right {
|
||||
right: -5px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 10px;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.resize-handle.top-left {
|
||||
top: -5px;
|
||||
left: -5px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
cursor: nwse-resize;
|
||||
z-index: 35;
|
||||
}
|
||||
|
||||
.resize-handle.top-right {
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
cursor: nesw-resize;
|
||||
z-index: 35;
|
||||
}
|
||||
|
||||
.resize-handle.bottom-left {
|
||||
bottom: -5px;
|
||||
left: -5px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
cursor: nesw-resize;
|
||||
z-index: 35;
|
||||
}
|
||||
|
||||
.resize-handle.bottom-right {
|
||||
bottom: -5px;
|
||||
right: -5px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
cursor: nwse-resize;
|
||||
z-index: 35;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
const screenStyle = this.isFloating
|
||||
? `transform: translate(${this.floatingRect.x}px, ${this.floatingRect.y}px); width: ${this.floatingRect.width}px; height: ${this.floatingRect.height}px;`
|
||||
: `transform: translate(${this.dockedOffsetX}px, ${this.dockedOffsetY}px);`;
|
||||
|
||||
return html`
|
||||
<div class="container">
|
||||
${!this.isConnected ? html`<div class="status-overlay">${this.status}</div>` : null}
|
||||
<div class="screen-container">
|
||||
<div ${ref(this.screenRef)} class="screen">
|
||||
<div
|
||||
${ref(this.screenRef)}
|
||||
class="screen ${this.isFloating ? "floating" : ""}"
|
||||
style="${screenStyle}"
|
||||
>
|
||||
<div class="drag-handle" @mousedown=${this.handleDragStart}></div>
|
||||
${
|
||||
this.isFloating
|
||||
? html`
|
||||
<div class="resize-handle top" @mousedown=${(e: MouseEvent) => this.handleResizeStart(e, "top")}></div>
|
||||
<div class="resize-handle bottom" @mousedown=${(e: MouseEvent) => this.handleResizeStart(e, "bottom")}></div>
|
||||
<div class="resize-handle left" @mousedown=${(e: MouseEvent) => this.handleResizeStart(e, "left")}></div>
|
||||
<div class="resize-handle right" @mousedown=${(e: MouseEvent) => this.handleResizeStart(e, "right")}></div>
|
||||
<div class="resize-handle top-left" @mousedown=${(e: MouseEvent) => this.handleResizeStart(e, "top-left")}></div>
|
||||
<div class="resize-handle top-right" @mousedown=${(e: MouseEvent) => this.handleResizeStart(e, "top-right")}></div>
|
||||
<div class="resize-handle bottom-left" @mousedown=${(e: MouseEvent) => this.handleResizeStart(e, "bottom-left")}></div>
|
||||
<div class="resize-handle bottom-right" @mousedown=${(e: MouseEvent) => this.handleResizeStart(e, "bottom-right")}></div>
|
||||
`
|
||||
: null
|
||||
}
|
||||
<div class="window-controls">
|
||||
<div class="window-control close" @click=${this.handleClose}></div>
|
||||
<div class="window-control minimize"></div>
|
||||
<div class="window-control maximize" @click=${this.toggleFullscreen}></div>
|
||||
</div>
|
||||
</div>
|
||||
@ -189,22 +364,291 @@ export class ClawComputerPanel extends LitElement {
|
||||
}
|
||||
|
||||
private handleClose = () => {
|
||||
// Dispatch event to parent to close the panel
|
||||
this.dispatchEvent(new CustomEvent("close", { bubbles: true, composed: true }));
|
||||
};
|
||||
|
||||
private handleDragStart = (e: MouseEvent) => {
|
||||
if (this.isResizing) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// 先清理旧的监听器,防止重复添加
|
||||
this.cleanupDragListeners();
|
||||
|
||||
this.isDragging = true;
|
||||
this.dragStart = { x: e.clientX, y: e.clientY };
|
||||
|
||||
const screen = this.shadowRoot?.querySelector(".screen") as HTMLDivElement | null;
|
||||
if (screen) {
|
||||
// 拖拽过程中,暂时禁用 screen 内部所有元素的 pointer-events
|
||||
// 这样即使鼠标移动到 VNC 窗口内部,也不会被 canvas 捕获
|
||||
screen.classList.add("dragging");
|
||||
|
||||
const rect = screen.getBoundingClientRect();
|
||||
// 获取 claw-computer-panel 本身的尺寸
|
||||
const hostRect = this.getBoundingClientRect();
|
||||
|
||||
// 使用 offsetWidth/offsetHeight 获取实际尺寸
|
||||
const screenWidth = screen.offsetWidth;
|
||||
const screenHeight = screen.offsetHeight;
|
||||
const hostHeight = this.offsetHeight;
|
||||
|
||||
this.initialRect = {
|
||||
x: this.isFloating ? this.floatingRect.x : rect.left,
|
||||
y: this.isFloating ? this.floatingRect.y : rect.top,
|
||||
width: screenWidth,
|
||||
height: screenHeight,
|
||||
offsetY: this.dockedOffsetY,
|
||||
offsetX: this.dockedOffsetX,
|
||||
containerHeight: hostHeight,
|
||||
containerTop: hostRect.top,
|
||||
containerLeft: hostRect.left,
|
||||
containerWidth: this.offsetWidth,
|
||||
};
|
||||
|
||||
if (!this.isFloating) {
|
||||
this.floatingRect = {
|
||||
x: rect.left,
|
||||
y: rect.top,
|
||||
width: screenWidth,
|
||||
height: screenHeight,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("mousemove", this.handleDragMove, { capture: true, passive: false });
|
||||
window.addEventListener("mouseup", this.handleDragEnd, { capture: true });
|
||||
};
|
||||
|
||||
private handleDragMove = (e: MouseEvent) => {
|
||||
if (!this.isDragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 安全检查:如果鼠标按钮不再按下,就停止拖拽
|
||||
if (e.buttons !== 1) {
|
||||
this.cleanupDragListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const dx = e.clientX - this.dragStart.x;
|
||||
const dy = e.clientY - this.dragStart.y;
|
||||
|
||||
if (this.isFloating) {
|
||||
let newX = this.initialRect.x + dx;
|
||||
let newY = this.initialRect.y + dy;
|
||||
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
// 限制悬浮窗口不超出屏幕边界
|
||||
newX = Math.max(0, Math.min(newX, windowWidth - this.initialRect.width));
|
||||
newY = Math.max(0, Math.min(newY, windowHeight - this.initialRect.height));
|
||||
|
||||
this.floatingRect = {
|
||||
x: newX,
|
||||
y: newY,
|
||||
width: this.initialRect.width,
|
||||
height: this.initialRect.height,
|
||||
};
|
||||
} else {
|
||||
// 每次都直接获取当前尺寸
|
||||
const hostHeight = this.offsetHeight;
|
||||
const screen = this.shadowRoot?.querySelector(".screen") as HTMLDivElement | null;
|
||||
const screenHeight = screen?.offsetHeight || 0;
|
||||
|
||||
// 计算最高和最低不能超过多少
|
||||
// 当 screen 居中时,初始顶部位置是 (hostHeight - screenHeight) / 2
|
||||
const initialTop = (hostHeight - screenHeight) / 2;
|
||||
|
||||
// 最高:screen 顶部 = host 顶部 → 偏移量 = 0 - initialTop = -initialTop
|
||||
const maxUpOffset = -initialTop;
|
||||
|
||||
const maxDownOffset = hostHeight - screenHeight - initialTop;
|
||||
|
||||
let newOffsetY = this.initialRect.offsetY + dy;
|
||||
|
||||
// 使用计算出来的边界限制
|
||||
newOffsetY = Math.max(maxUpOffset, Math.min(newOffsetY, maxDownOffset));
|
||||
|
||||
this.dockedOffsetY = newOffsetY;
|
||||
|
||||
// 禁止左右移动
|
||||
this.dockedOffsetX = 0;
|
||||
}
|
||||
};
|
||||
|
||||
private cleanupDragListeners = () => {
|
||||
this.isDragging = false;
|
||||
|
||||
// 移除拖拽过程中添加的类,恢复 pointer-events
|
||||
const screen = this.shadowRoot?.querySelector(".screen") as HTMLDivElement | null;
|
||||
if (screen) {
|
||||
screen.classList.remove("dragging");
|
||||
}
|
||||
|
||||
try {
|
||||
window.removeEventListener("mousemove", this.handleDragMove, { capture: true });
|
||||
window.removeEventListener("mouseup", this.handleDragEnd, { capture: true });
|
||||
} catch (e) {
|
||||
console.error("Error removing drag event listeners:", e);
|
||||
}
|
||||
};
|
||||
|
||||
private handleDragEnd = () => {
|
||||
this.cleanupDragListeners();
|
||||
};
|
||||
|
||||
private cleanupResizeListeners = () => {
|
||||
this.isResizing = false;
|
||||
try {
|
||||
window.removeEventListener("mousemove", this.handleResizeMove);
|
||||
window.removeEventListener("mouseup", this.handleResizeEnd);
|
||||
} catch (e) {
|
||||
console.error("Error removing resize event listeners:", e);
|
||||
}
|
||||
};
|
||||
|
||||
private handleResizeStart = (e: MouseEvent, edge: string) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// 先清理旧的监听器,防止重复添加
|
||||
this.cleanupResizeListeners();
|
||||
|
||||
this.isResizing = true;
|
||||
this.resizeEdge = edge;
|
||||
this.dragStart = { x: e.clientX, y: e.clientY };
|
||||
this.initialRect = {
|
||||
x: this.floatingRect.x,
|
||||
y: this.floatingRect.y,
|
||||
width: this.floatingRect.width,
|
||||
height: this.floatingRect.height,
|
||||
offsetY: 0,
|
||||
offsetX: 0,
|
||||
containerHeight: 0,
|
||||
containerTop: 0,
|
||||
containerLeft: 0,
|
||||
containerWidth: 0,
|
||||
};
|
||||
this.aspectRatio = this.floatingRect.width / this.floatingRect.height;
|
||||
|
||||
window.addEventListener("mousemove", this.handleResizeMove);
|
||||
window.addEventListener("mouseup", this.handleResizeEnd);
|
||||
};
|
||||
|
||||
private handleResizeMove = (e: MouseEvent) => {
|
||||
if (!this.isResizing) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 安全检查:如果鼠标按钮不再按下,就停止调整大小
|
||||
if (e.buttons !== 1) {
|
||||
this.cleanupResizeListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const dx = e.clientX - this.dragStart.x;
|
||||
const dy = e.clientY - this.dragStart.y;
|
||||
|
||||
let { x, y, width, height } = this.initialRect;
|
||||
|
||||
if (this.resizeEdge.includes("right")) {
|
||||
width += dx;
|
||||
if (!this.resizeEdge.includes("top") && !this.resizeEdge.includes("bottom")) {
|
||||
height = width / this.aspectRatio;
|
||||
}
|
||||
}
|
||||
if (this.resizeEdge.includes("left")) {
|
||||
x += dx;
|
||||
width -= dx;
|
||||
if (!this.resizeEdge.includes("top") && !this.resizeEdge.includes("bottom")) {
|
||||
height = width / this.aspectRatio;
|
||||
}
|
||||
}
|
||||
if (this.resizeEdge.includes("bottom")) {
|
||||
height += dy;
|
||||
if (!this.resizeEdge.includes("left") && !this.resizeEdge.includes("right")) {
|
||||
width = height * this.aspectRatio;
|
||||
}
|
||||
}
|
||||
if (this.resizeEdge.includes("top")) {
|
||||
y += dy;
|
||||
height -= dy;
|
||||
if (!this.resizeEdge.includes("left") && !this.resizeEdge.includes("right")) {
|
||||
width = height * this.aspectRatio;
|
||||
}
|
||||
}
|
||||
|
||||
// 对角调整时的比例保持
|
||||
if (
|
||||
(this.resizeEdge.includes("left") || this.resizeEdge.includes("right")) &&
|
||||
(this.resizeEdge.includes("top") || this.resizeEdge.includes("bottom"))
|
||||
) {
|
||||
// 简单处理:基于宽度的变化来调整高度,或者基于高度调整宽度
|
||||
// 优先保持宽高比
|
||||
if (this.resizeEdge.includes("right")) {
|
||||
height = width / this.aspectRatio;
|
||||
} else {
|
||||
height = width / this.aspectRatio;
|
||||
}
|
||||
// 如果是 top,需要重新计算 y
|
||||
if (this.resizeEdge.includes("top")) {
|
||||
y = this.initialRect.y + (this.initialRect.height - height);
|
||||
}
|
||||
}
|
||||
|
||||
// 最小尺寸限制
|
||||
if (width < 200) {
|
||||
width = 200;
|
||||
height = width / this.aspectRatio;
|
||||
if (this.resizeEdge.includes("left")) {
|
||||
x = this.initialRect.x + (this.initialRect.width - width);
|
||||
}
|
||||
if (this.resizeEdge.includes("top")) {
|
||||
y = this.initialRect.y + (this.initialRect.height - height);
|
||||
}
|
||||
}
|
||||
if (height < 150) {
|
||||
height = 150;
|
||||
width = height * this.aspectRatio;
|
||||
if (this.resizeEdge.includes("left")) {
|
||||
x = this.initialRect.x + (this.initialRect.width - width);
|
||||
}
|
||||
if (this.resizeEdge.includes("top")) {
|
||||
y = this.initialRect.y + (this.initialRect.height - height);
|
||||
}
|
||||
}
|
||||
|
||||
this.floatingRect = { x, y, width, height };
|
||||
|
||||
if (this.rfb) {
|
||||
requestAnimationFrame(() => this.rfb?.resize?.());
|
||||
}
|
||||
};
|
||||
|
||||
private handleResizeEnd = () => {
|
||||
this.cleanupResizeListeners();
|
||||
setTimeout(() => this.rfb?.resize?.(), 50);
|
||||
};
|
||||
|
||||
private connect = async () => {
|
||||
let url =
|
||||
this.vncUrl || `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/vnc`;
|
||||
|
||||
// Append target configuration if available
|
||||
if (this.vncTarget) {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
urlObj.searchParams.set("target", this.vncTarget);
|
||||
url = urlObj.toString();
|
||||
} catch {
|
||||
// Fallback for non-standard WebSocket URLs if URL parsing fails
|
||||
if (url.includes("?")) {
|
||||
url += `&target=${encodeURIComponent(this.vncTarget)}`;
|
||||
} else {
|
||||
@ -218,11 +662,9 @@ export class ClawComputerPanel extends LitElement {
|
||||
}
|
||||
|
||||
this.status = "正在連接...";
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
let screen = this.screenRef.value;
|
||||
|
||||
if (!screen) {
|
||||
// Fallback: try to find the element via shadowRoot if ref failed
|
||||
screen = this.shadowRoot?.querySelector(".screen") as HTMLDivElement;
|
||||
}
|
||||
|
||||
@ -232,12 +674,10 @@ export class ClawComputerPanel extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear previous VNC canvas elements to prevent duplication
|
||||
const existingCanvases = screen.querySelectorAll("canvas");
|
||||
existingCanvases.forEach((canvas) => canvas.remove());
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const Constructor = RFBClass as new (
|
||||
target: HTMLElement,
|
||||
url: string,
|
||||
@ -250,10 +690,10 @@ export class ClawComputerPanel extends LitElement {
|
||||
clipViewport: true,
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
this.rfb.addEventListener("securityfailure", (e: CustomEvent) => {
|
||||
console.error("VNC security failure:", e.detail);
|
||||
this.status = `Security negotiation failed: ${e.detail.reason || "Unknown reason"}`;
|
||||
this.rfb.addEventListener("securityfailure", (e: unknown) => {
|
||||
const event = e as CustomEvent;
|
||||
console.error("VNC security failure:", event.detail);
|
||||
this.status = `Security negotiation failed: ${event.detail.reason || "Unknown reason"}`;
|
||||
});
|
||||
|
||||
if (this.rfb) {
|
||||
@ -305,18 +745,16 @@ export class ClawComputerPanel extends LitElement {
|
||||
}
|
||||
};
|
||||
|
||||
private handlePasswordKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
void this.connect();
|
||||
}
|
||||
private handleWindowBlur = () => {
|
||||
this.cleanupDragListeners();
|
||||
this.cleanupResizeListeners();
|
||||
};
|
||||
|
||||
firstUpdated() {
|
||||
window.addEventListener("resize", this.handleResize);
|
||||
window.addEventListener("blur", this.handleWindowBlur);
|
||||
|
||||
// Auto-connect if enabled and URL is configured
|
||||
if (this.enabled && this.vncUrl) {
|
||||
// Use setTimeout to ensure DOM is fully ready and to allow UI to render first
|
||||
setTimeout(() => {
|
||||
void this.connect();
|
||||
}, 100);
|
||||
@ -326,6 +764,9 @@ export class ClawComputerPanel extends LitElement {
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener("resize", this.handleResize);
|
||||
window.removeEventListener("blur", this.handleWindowBlur);
|
||||
this.cleanupDragListeners();
|
||||
this.cleanupResizeListeners();
|
||||
this.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user