806 lines
24 KiB
JavaScript
806 lines
24 KiB
JavaScript
(function () {
|
||
const ControlType = {
|
||
mouse: 0,
|
||
keyboard: 1,
|
||
audio_capture: 2,
|
||
host_infomation: 3,
|
||
display_id: 4,
|
||
};
|
||
|
||
const MouseFlag = {
|
||
move: 0,
|
||
left_down: 1,
|
||
left_up: 2,
|
||
right_down: 3,
|
||
right_up: 4,
|
||
middle_down: 5,
|
||
middle_up: 6,
|
||
wheel_vertical: 7,
|
||
wheel_horizontal: 8,
|
||
};
|
||
|
||
const clamp01 = (value) => Math.max(0, Math.min(1, value));
|
||
const isTextInput = (el) => {
|
||
if (!el || !el.tagName) return false;
|
||
const tag = el.tagName.toLowerCase();
|
||
if (tag === "textarea") return true;
|
||
if (tag !== "input") return false;
|
||
const type = (el.getAttribute("type") || "text").toLowerCase();
|
||
return !["checkbox", "radio", "button", "submit", "reset"].includes(type);
|
||
};
|
||
|
||
class ControlManager {
|
||
constructor() {
|
||
this.dataChannel = null;
|
||
this.elements = {
|
||
video: document.getElementById("video"),
|
||
dataLog: document.getElementById("data-channel"),
|
||
mediaContainer: document.getElementById("media"),
|
||
videoContainer: document.getElementById("video-container"),
|
||
fullscreenBtn: document.getElementById("fullscreen-btn"),
|
||
realFullscreenBtn: document.getElementById("real-fullscreen-btn"),
|
||
virtualMouse: document.getElementById("virtual-mouse"),
|
||
virtualLeftBtn: document.getElementById("virtual-left-btn"),
|
||
virtualRightBtn: document.getElementById("virtual-right-btn"),
|
||
virtualWheel: document.getElementById("virtual-wheel"),
|
||
virtualTouchpad: document.getElementById("virtual-touchpad"),
|
||
virtualDragHandle: document.getElementById("virtual-mouse-drag-handle"),
|
||
};
|
||
|
||
this.state = {
|
||
pointerLocked: false,
|
||
normalizedPos: { x: 0.5, y: 0.5 },
|
||
lastPointerPos: null,
|
||
lastWheelAt: 0,
|
||
isFullscreen: false,
|
||
isRealFullscreen: false,
|
||
touchpadStart: null,
|
||
draggingVirtualMouse: false,
|
||
dragOffset: { x: 0, y: 0 },
|
||
pointerLockToastTimer: null,
|
||
videoRect: null,
|
||
};
|
||
|
||
this.virtualWheelTimer = null;
|
||
|
||
this.onPointerLockChange = this.onPointerLockChange.bind(this);
|
||
this.onPointerLockError = this.onPointerLockError.bind(this);
|
||
this.onPointerDown = this.onPointerDown.bind(this);
|
||
this.onPointerMove = this.onPointerMove.bind(this);
|
||
this.onPointerUp = this.onPointerUp.bind(this);
|
||
this.onPointerCancel = this.onPointerCancel.bind(this);
|
||
this.onWheel = this.onWheel.bind(this);
|
||
|
||
this.onTouchStartFallback = this.onTouchStartFallback.bind(this);
|
||
this.onTouchMoveFallback = this.onTouchMoveFallback.bind(this);
|
||
this.onTouchEndFallback = this.onTouchEndFallback.bind(this);
|
||
|
||
this.onVirtualWheelStart = this.onVirtualWheelStart.bind(this);
|
||
this.onVirtualWheelEnd = this.onVirtualWheelEnd.bind(this);
|
||
this.onTouchpadStart = this.onTouchpadStart.bind(this);
|
||
this.onTouchpadMove = this.onTouchpadMove.bind(this);
|
||
this.onTouchpadEnd = this.onTouchpadEnd.bind(this);
|
||
|
||
this.onDragHandleTouchStart = this.onDragHandleTouchStart.bind(this);
|
||
this.onDragHandleTouchMove = this.onDragHandleTouchMove.bind(this);
|
||
this.onDragHandleTouchEnd = this.onDragHandleTouchEnd.bind(this);
|
||
this.onDragHandleClick = this.onDragHandleClick.bind(this);
|
||
|
||
this.init();
|
||
}
|
||
|
||
init() {
|
||
const { video } = this.elements;
|
||
if (!video) {
|
||
console.warn("CrossDeskControl: video element not found");
|
||
return;
|
||
}
|
||
|
||
video.style.pointerEvents = "auto";
|
||
video.tabIndex = 0;
|
||
|
||
this.bindPointerLockEvents();
|
||
this.bindPointerListeners();
|
||
this.bindKeyboardListeners();
|
||
this.setupVirtualMouse();
|
||
this.setupFullscreenButtons();
|
||
}
|
||
|
||
setDataChannel(channel) {
|
||
this.dataChannel = channel;
|
||
}
|
||
|
||
isChannelOpen() {
|
||
return this.dataChannel && this.dataChannel.readyState === "open";
|
||
}
|
||
|
||
send(action) {
|
||
if (!this.isChannelOpen()) return false;
|
||
try {
|
||
const payload = JSON.stringify(action);
|
||
this.dataChannel.send(payload);
|
||
this.logDataChannel(payload);
|
||
return true;
|
||
} catch (err) {
|
||
console.error("CrossDeskControl: failed to send action", err);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
sendMouseAction({ x, y, flag, scroll = 0 }) {
|
||
const numericFlag =
|
||
typeof flag === "string" ? MouseFlag[flag] ?? MouseFlag.move : flag | 0;
|
||
|
||
const action = {
|
||
type: ControlType.mouse,
|
||
mouse: {
|
||
x: clamp01(x),
|
||
y: clamp01(y),
|
||
s: scroll | 0,
|
||
flag: numericFlag,
|
||
},
|
||
};
|
||
|
||
this.send(action);
|
||
}
|
||
|
||
sendKeyboardAction(keyValue, isDown) {
|
||
const action = {
|
||
type: ControlType.keyboard,
|
||
keyboard: {
|
||
key_value: keyValue | 0,
|
||
flag: isDown ? 0 : 1,
|
||
},
|
||
};
|
||
this.send(action);
|
||
}
|
||
|
||
sendAudioCapture(enabled) {
|
||
const action = {
|
||
type: ControlType.audio_capture,
|
||
audio_capture: !!enabled,
|
||
};
|
||
this.send(action);
|
||
}
|
||
|
||
sendDisplayId(id) {
|
||
const action = {
|
||
type: ControlType.display_id,
|
||
display_id: id | 0,
|
||
};
|
||
this.send(action);
|
||
}
|
||
|
||
sendRawMessage(raw) {
|
||
if (!this.isChannelOpen()) return false;
|
||
try {
|
||
this.dataChannel.send(raw);
|
||
this.logDataChannel(raw);
|
||
return true;
|
||
} catch (err) {
|
||
console.error("CrossDeskControl: failed to send raw message", err);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
logDataChannel(text) {
|
||
const { dataLog } = this.elements;
|
||
if (!dataLog) return;
|
||
dataLog.textContent += `> ${text}\n`;
|
||
dataLog.scrollTop = dataLog.scrollHeight;
|
||
}
|
||
|
||
bindPointerLockEvents() {
|
||
document.addEventListener("pointerlockchange", this.onPointerLockChange);
|
||
document.addEventListener("pointerlockerror", this.onPointerLockError);
|
||
document.addEventListener("keydown", (event) => {
|
||
if (event.ctrlKey && event.key === "Escape") {
|
||
document.exitPointerLock?.();
|
||
}
|
||
});
|
||
}
|
||
|
||
onPointerLockChange() {
|
||
this.state.pointerLocked = document.pointerLockElement === this.elements.video;
|
||
if (this.state.pointerLocked) {
|
||
this.state.videoRect = this.elements.video?.getBoundingClientRect() ?? null;
|
||
} else {
|
||
this.state.videoRect = null;
|
||
this.showPointerLockToast(
|
||
"已退出鼠标锁定,按 Esc 或点击视频重新锁定(释放可按 Ctrl+Esc)",
|
||
3000
|
||
);
|
||
}
|
||
if (this.elements.dataLog) {
|
||
this.elements.dataLog.textContent += `[pointerlock ${
|
||
this.state.pointerLocked ? "entered" : "exited"
|
||
}]\n`;
|
||
this.elements.dataLog.scrollTop = this.elements.dataLog.scrollHeight;
|
||
}
|
||
}
|
||
|
||
onPointerLockError() {
|
||
this.showPointerLockToast("鼠标锁定失败", 2500);
|
||
}
|
||
|
||
bindPointerListeners() {
|
||
const { video } = this.elements;
|
||
if (!video) return;
|
||
|
||
try {
|
||
video.style.touchAction = "none";
|
||
} catch (err) {}
|
||
|
||
video.addEventListener("pointerdown", this.onPointerDown, {
|
||
passive: false,
|
||
});
|
||
document.addEventListener("pointermove", this.onPointerMove, {
|
||
passive: false,
|
||
});
|
||
document.addEventListener("pointerup", this.onPointerUp, {
|
||
passive: false,
|
||
});
|
||
document.addEventListener("pointercancel", this.onPointerCancel);
|
||
video.addEventListener("wheel", this.onWheel, { passive: false });
|
||
|
||
if (!window.PointerEvent) {
|
||
video.addEventListener("touchstart", this.onTouchStartFallback, {
|
||
passive: false,
|
||
});
|
||
document.addEventListener("touchmove", this.onTouchMoveFallback, {
|
||
passive: false,
|
||
});
|
||
document.addEventListener("touchend", this.onTouchEndFallback, {
|
||
passive: false,
|
||
});
|
||
document.addEventListener("touchcancel", this.onTouchEndFallback, {
|
||
passive: false,
|
||
});
|
||
}
|
||
}
|
||
|
||
onPointerDown(event) {
|
||
const button = typeof event.button === "number" ? event.button : 0;
|
||
if (button < 0) return;
|
||
event.preventDefault?.();
|
||
|
||
this.state.lastPointerPos = { x: event.clientX, y: event.clientY };
|
||
this.ensureVideoRect();
|
||
if (this.state.videoRect && this.isInsideVideo(event.clientX, event.clientY)) {
|
||
this.updateNormalizedFromClient(event.clientX, event.clientY);
|
||
this.requestPointerLock();
|
||
}
|
||
|
||
this.elements.video?.setPointerCapture?.(event.pointerId ?? 0);
|
||
this.sendMouseAction({
|
||
x: this.state.normalizedPos.x,
|
||
y: this.state.normalizedPos.y,
|
||
flag: this.buttonToFlag(button, true),
|
||
});
|
||
}
|
||
|
||
onPointerMove(event) {
|
||
if (!this.state.pointerLocked && !this.state.lastPointerPos) return;
|
||
|
||
const movementX = this.state.pointerLocked
|
||
? event.movementX
|
||
: event.clientX - (this.state.lastPointerPos?.x ?? event.clientX);
|
||
const movementY = this.state.pointerLocked
|
||
? event.movementY
|
||
: event.clientY - (this.state.lastPointerPos?.y ?? event.clientY);
|
||
|
||
if (!this.state.pointerLocked) {
|
||
this.state.lastPointerPos = { x: event.clientX, y: event.clientY };
|
||
}
|
||
|
||
this.ensureVideoRect();
|
||
if (!this.state.videoRect) return;
|
||
|
||
if (this.state.pointerLocked) {
|
||
this.state.normalizedPos.x = clamp01(
|
||
this.state.normalizedPos.x + movementX / this.state.videoRect.width
|
||
);
|
||
this.state.normalizedPos.y = clamp01(
|
||
this.state.normalizedPos.y + movementY / this.state.videoRect.height
|
||
);
|
||
this.sendMouseAction({
|
||
x: this.state.normalizedPos.x,
|
||
y: this.state.normalizedPos.y,
|
||
flag: MouseFlag.move,
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (!this.isInsideVideo(event.clientX, event.clientY)) return;
|
||
const x = (event.clientX - this.state.videoRect.left) /
|
||
this.state.videoRect.width;
|
||
const y = (event.clientY - this.state.videoRect.top) /
|
||
this.state.videoRect.height;
|
||
this.state.normalizedPos = { x: clamp01(x), y: clamp01(y) };
|
||
this.sendMouseAction({
|
||
x: this.state.normalizedPos.x,
|
||
y: this.state.normalizedPos.y,
|
||
flag: MouseFlag.move,
|
||
});
|
||
}
|
||
|
||
onPointerUp(event) {
|
||
const button = typeof event.button === "number" ? event.button : 0;
|
||
this.elements.video?.releasePointerCapture?.(event.pointerId ?? 0);
|
||
this.state.lastPointerPos = null;
|
||
this.sendMouseAction({
|
||
x: this.state.normalizedPos.x,
|
||
y: this.state.normalizedPos.y,
|
||
flag: this.buttonToFlag(button, false),
|
||
});
|
||
}
|
||
|
||
onPointerCancel() {
|
||
this.state.lastPointerPos = null;
|
||
}
|
||
|
||
onWheel(event) {
|
||
const now = Date.now();
|
||
if (now - this.state.lastWheelAt < 50) return;
|
||
this.state.lastWheelAt = now;
|
||
|
||
this.ensureVideoRect();
|
||
if (!this.state.videoRect) return;
|
||
|
||
let coords = this.state.normalizedPos;
|
||
if (!this.state.pointerLocked) {
|
||
if (!this.isInsideVideo(event.clientX, event.clientY)) return;
|
||
coords = {
|
||
x: (event.clientX - this.state.videoRect.left) /
|
||
this.state.videoRect.width,
|
||
y: (event.clientY - this.state.videoRect.top) /
|
||
this.state.videoRect.height,
|
||
};
|
||
}
|
||
|
||
this.sendMouseAction({
|
||
x: coords.x,
|
||
y: coords.y,
|
||
flag: event.deltaY === 0 ? MouseFlag.wheel_horizontal : MouseFlag.wheel_vertical,
|
||
scroll: event.deltaY || event.deltaX,
|
||
});
|
||
event.preventDefault();
|
||
}
|
||
|
||
onTouchStartFallback(event) {
|
||
if (!event.touches?.length) return;
|
||
const touch = event.touches[0];
|
||
this.state.lastPointerPos = { x: touch.clientX, y: touch.clientY };
|
||
event.preventDefault();
|
||
this.onPointerDown({
|
||
button: 0,
|
||
clientX: touch.clientX,
|
||
clientY: touch.clientY,
|
||
});
|
||
}
|
||
|
||
onTouchMoveFallback(event) {
|
||
if (!this.state.lastPointerPos || !event.touches?.length) return;
|
||
const touch = event.touches[0];
|
||
event.preventDefault();
|
||
this.onPointerMove({
|
||
clientX: touch.clientX,
|
||
clientY: touch.clientY,
|
||
movementX: touch.clientX - this.state.lastPointerPos.x,
|
||
movementY: touch.clientY - this.state.lastPointerPos.y,
|
||
});
|
||
this.state.lastPointerPos = { x: touch.clientX, y: touch.clientY };
|
||
}
|
||
|
||
onTouchEndFallback(event) {
|
||
const touch = event.changedTouches?.[0];
|
||
this.onPointerUp({
|
||
button: 0,
|
||
clientX: touch?.clientX ?? 0,
|
||
clientY: touch?.clientY ?? 0,
|
||
});
|
||
this.state.lastPointerPos = null;
|
||
}
|
||
|
||
buttonToFlag(button, isDown) {
|
||
const mapping = {
|
||
0: { down: MouseFlag.left_down, up: MouseFlag.left_up },
|
||
1: { down: MouseFlag.middle_down, up: MouseFlag.middle_up },
|
||
2: { down: MouseFlag.right_down, up: MouseFlag.right_up },
|
||
};
|
||
const record = mapping[button] || mapping[0];
|
||
return isDown ? record.down : record.up;
|
||
}
|
||
|
||
requestPointerLock() {
|
||
try {
|
||
this.elements.video?.requestPointerLock?.();
|
||
} catch (err) {
|
||
console.warn("CrossDeskControl: requestPointerLock failed", err);
|
||
}
|
||
}
|
||
|
||
ensureVideoRect() {
|
||
const { video } = this.elements;
|
||
if (!video) return;
|
||
this.state.videoRect = video.getBoundingClientRect();
|
||
}
|
||
|
||
isInsideVideo(clientX, clientY) {
|
||
const rect = this.state.videoRect;
|
||
if (!rect) return false;
|
||
return (
|
||
clientX >= rect.left &&
|
||
clientX <= rect.right &&
|
||
clientY >= rect.top &&
|
||
clientY <= rect.bottom
|
||
);
|
||
}
|
||
|
||
updateNormalizedFromClient(clientX, clientY) {
|
||
if (!this.state.videoRect) return;
|
||
this.state.normalizedPos = {
|
||
x: clamp01((clientX - this.state.videoRect.left) / this.state.videoRect.width),
|
||
y: clamp01((clientY - this.state.videoRect.top) / this.state.videoRect.height),
|
||
};
|
||
}
|
||
|
||
bindKeyboardListeners() {
|
||
document.addEventListener("keydown", (event) => {
|
||
if (!this.isChannelOpen()) return;
|
||
if (event.repeat) return;
|
||
if (isTextInput(event.target)) return;
|
||
this.sendKeyboardAction(event.keyCode ?? 0, true);
|
||
});
|
||
|
||
document.addEventListener("keyup", (event) => {
|
||
if (!this.isChannelOpen()) return;
|
||
if (isTextInput(event.target)) return;
|
||
this.sendKeyboardAction(event.keyCode ?? 0, false);
|
||
});
|
||
}
|
||
|
||
setupVirtualMouse() {
|
||
const isDesktop = window.matchMedia(
|
||
"(hover: hover) and (pointer: fine)"
|
||
).matches;
|
||
|
||
if (isDesktop) {
|
||
if (this.elements.virtualMouse) {
|
||
this.elements.virtualMouse.style.pointerEvents = "none";
|
||
}
|
||
return;
|
||
}
|
||
|
||
this.elements.virtualLeftBtn?.addEventListener(
|
||
"touchstart",
|
||
(event) => {
|
||
event.preventDefault();
|
||
this.sendMouseAction({
|
||
x: this.state.normalizedPos.x,
|
||
y: this.state.normalizedPos.y,
|
||
flag: MouseFlag.left_down,
|
||
});
|
||
},
|
||
{ passive: false }
|
||
);
|
||
|
||
this.elements.virtualLeftBtn?.addEventListener(
|
||
"touchend",
|
||
(event) => {
|
||
event.preventDefault();
|
||
this.sendMouseAction({
|
||
x: this.state.normalizedPos.x,
|
||
y: this.state.normalizedPos.y,
|
||
flag: MouseFlag.left_up,
|
||
});
|
||
},
|
||
{ passive: false }
|
||
);
|
||
|
||
this.elements.virtualRightBtn?.addEventListener(
|
||
"touchstart",
|
||
(event) => {
|
||
event.preventDefault();
|
||
this.sendMouseAction({
|
||
x: this.state.normalizedPos.x,
|
||
y: this.state.normalizedPos.y,
|
||
flag: MouseFlag.right_down,
|
||
});
|
||
},
|
||
{ passive: false }
|
||
);
|
||
|
||
this.elements.virtualRightBtn?.addEventListener(
|
||
"touchend",
|
||
(event) => {
|
||
event.preventDefault();
|
||
this.sendMouseAction({
|
||
x: this.state.normalizedPos.x,
|
||
y: this.state.normalizedPos.y,
|
||
flag: MouseFlag.right_up,
|
||
});
|
||
},
|
||
{ passive: false }
|
||
);
|
||
|
||
this.elements.virtualWheel?.addEventListener(
|
||
"touchstart",
|
||
this.onVirtualWheelStart,
|
||
{ passive: false }
|
||
);
|
||
this.elements.virtualWheel?.addEventListener(
|
||
"touchend",
|
||
this.onVirtualWheelEnd,
|
||
{ passive: false }
|
||
);
|
||
this.elements.virtualWheel?.addEventListener(
|
||
"touchcancel",
|
||
this.onVirtualWheelEnd,
|
||
{ passive: false }
|
||
);
|
||
|
||
this.elements.virtualTouchpad?.addEventListener(
|
||
"touchstart",
|
||
this.onTouchpadStart,
|
||
{ passive: false }
|
||
);
|
||
this.elements.virtualTouchpad?.addEventListener(
|
||
"touchmove",
|
||
this.onTouchpadMove,
|
||
{ passive: false }
|
||
);
|
||
this.elements.virtualTouchpad?.addEventListener(
|
||
"touchend",
|
||
this.onTouchpadEnd,
|
||
{ passive: false }
|
||
);
|
||
this.elements.virtualTouchpad?.addEventListener(
|
||
"touchcancel",
|
||
this.onTouchpadEnd,
|
||
{ passive: false }
|
||
);
|
||
|
||
this.bindVirtualMouseDragging();
|
||
}
|
||
|
||
onVirtualWheelStart(event) {
|
||
event.preventDefault();
|
||
this.emitVirtualWheel();
|
||
this.virtualWheelTimer = setInterval(() => this.emitVirtualWheel(), 100);
|
||
}
|
||
|
||
onVirtualWheelEnd(event) {
|
||
event.preventDefault();
|
||
if (this.virtualWheelTimer) {
|
||
clearInterval(this.virtualWheelTimer);
|
||
this.virtualWheelTimer = null;
|
||
}
|
||
}
|
||
|
||
emitVirtualWheel() {
|
||
this.sendMouseAction({
|
||
x: this.state.normalizedPos.x,
|
||
y: this.state.normalizedPos.y,
|
||
flag: MouseFlag.wheel_vertical,
|
||
scroll: -20,
|
||
});
|
||
}
|
||
|
||
onTouchpadStart(event) {
|
||
const touch = event.touches?.[0];
|
||
if (!touch) return;
|
||
event.preventDefault();
|
||
this.state.touchpadStart = {
|
||
x: touch.clientX,
|
||
y: touch.clientY,
|
||
normalizedX: this.state.normalizedPos.x,
|
||
normalizedY: this.state.normalizedPos.y,
|
||
};
|
||
}
|
||
|
||
onTouchpadMove(event) {
|
||
const touch = event.touches?.[0];
|
||
if (!touch || !this.state.touchpadStart) return;
|
||
event.preventDefault();
|
||
|
||
this.ensureVideoRect();
|
||
if (!this.state.videoRect) return;
|
||
|
||
const sensitivity = 2;
|
||
const deltaX = touch.clientX - this.state.touchpadStart.x;
|
||
const deltaY = touch.clientY - this.state.touchpadStart.y;
|
||
|
||
const newX =
|
||
this.state.touchpadStart.normalizedX +
|
||
(deltaX / this.state.videoRect.width) * sensitivity;
|
||
const newY =
|
||
this.state.touchpadStart.normalizedY +
|
||
(deltaY / this.state.videoRect.height) * sensitivity;
|
||
|
||
this.state.normalizedPos = {
|
||
x: clamp01(newX),
|
||
y: clamp01(newY),
|
||
};
|
||
|
||
this.sendMouseAction({
|
||
x: this.state.normalizedPos.x,
|
||
y: this.state.normalizedPos.y,
|
||
flag: MouseFlag.move,
|
||
});
|
||
}
|
||
|
||
onTouchpadEnd(event) {
|
||
event.preventDefault();
|
||
this.state.touchpadStart = null;
|
||
}
|
||
|
||
bindVirtualMouseDragging() {
|
||
const { virtualMouse, virtualDragHandle, videoContainer } = this.elements;
|
||
if (!virtualMouse || !virtualDragHandle || !videoContainer) return;
|
||
|
||
virtualDragHandle.addEventListener("touchstart", this.onDragHandleTouchStart, {
|
||
passive: false,
|
||
});
|
||
virtualDragHandle.addEventListener("click", this.onDragHandleClick);
|
||
document.addEventListener("touchmove", this.onDragHandleTouchMove, {
|
||
passive: false,
|
||
});
|
||
document.addEventListener("touchend", this.onDragHandleTouchEnd, {
|
||
passive: false,
|
||
});
|
||
document.addEventListener("touchcancel", this.onDragHandleTouchEnd, {
|
||
passive: false,
|
||
});
|
||
}
|
||
|
||
onDragHandleTouchStart(event) {
|
||
const touch = event.touches?.[0];
|
||
if (!touch || !this.elements.virtualMouse) return;
|
||
event.preventDefault();
|
||
const rect = this.elements.virtualMouse.getBoundingClientRect();
|
||
this.state.draggingVirtualMouse = true;
|
||
this.state.dragOffset = {
|
||
x: touch.clientX - rect.left,
|
||
y: touch.clientY - rect.top,
|
||
};
|
||
}
|
||
|
||
onDragHandleTouchMove(event) {
|
||
if (!this.state.draggingVirtualMouse) return;
|
||
const touch = event.touches?.[0];
|
||
if (!touch || !this.elements.videoContainer || !this.elements.virtualMouse)
|
||
return;
|
||
event.preventDefault();
|
||
|
||
const containerRect = this.elements.videoContainer.getBoundingClientRect();
|
||
let newX = touch.clientX - this.state.dragOffset.x - containerRect.left;
|
||
let newY = touch.clientY - this.state.dragOffset.y - containerRect.top;
|
||
|
||
const maxX = Math.max(
|
||
0,
|
||
containerRect.width - this.elements.virtualMouse.offsetWidth
|
||
);
|
||
const maxY = Math.max(
|
||
0,
|
||
containerRect.height - this.elements.virtualMouse.offsetHeight
|
||
);
|
||
|
||
newX = Math.max(0, Math.min(newX, maxX));
|
||
newY = Math.max(0, Math.min(newY, maxY));
|
||
|
||
this.elements.virtualMouse.style.left = `${newX}px`;
|
||
this.elements.virtualMouse.style.top = `${newY}px`;
|
||
this.elements.virtualMouse.style.bottom = "auto";
|
||
this.elements.virtualMouse.style.transform = "none";
|
||
}
|
||
|
||
onDragHandleTouchEnd() {
|
||
this.state.draggingVirtualMouse = false;
|
||
}
|
||
|
||
onDragHandleClick(event) {
|
||
event.stopPropagation();
|
||
this.elements.virtualMouse?.classList.toggle("minimized");
|
||
}
|
||
|
||
setupFullscreenButtons() {
|
||
this.elements.fullscreenBtn?.addEventListener("click", () => {
|
||
const media = this.elements.mediaContainer;
|
||
if (!media) return;
|
||
this.state.isFullscreen = !this.state.isFullscreen;
|
||
media.classList.toggle("fullscreen", this.state.isFullscreen);
|
||
this.elements.fullscreenBtn.textContent = this.state.isFullscreen
|
||
? "退出全屏"
|
||
: "最大化";
|
||
this.ensureVideoRect();
|
||
});
|
||
|
||
this.elements.realFullscreenBtn?.addEventListener("click", () => {
|
||
const container = this.elements.videoContainer;
|
||
if (!container) return;
|
||
if (!this.state.isRealFullscreen) {
|
||
const request =
|
||
container.requestFullscreen ||
|
||
container.mozRequestFullScreen ||
|
||
container.webkitRequestFullscreen ||
|
||
container.msRequestFullscreen;
|
||
request?.call(container);
|
||
} else {
|
||
const exit =
|
||
document.exitFullscreen ||
|
||
document.mozCancelFullScreen ||
|
||
document.webkitExitFullscreen ||
|
||
document.msExitFullscreen;
|
||
exit?.call(document);
|
||
}
|
||
this.state.isRealFullscreen = !this.state.isRealFullscreen;
|
||
this.elements.realFullscreenBtn.textContent = this.state.isRealFullscreen
|
||
? "退出全屏"
|
||
: "全屏";
|
||
});
|
||
}
|
||
|
||
showPointerLockToast(text, duration = 2500) {
|
||
let toast = document.getElementById("pointerlock-toast");
|
||
if (!toast) {
|
||
toast = document.createElement("div");
|
||
toast.id = "pointerlock-toast";
|
||
Object.assign(toast.style, {
|
||
position: "fixed",
|
||
left: "50%",
|
||
bottom: "24px",
|
||
transform: "translateX(-50%)",
|
||
background: "rgba(0,0,0,0.75)",
|
||
color: "#fff",
|
||
padding: "8px 12px",
|
||
borderRadius: "6px",
|
||
fontSize: "13px",
|
||
zIndex: "9999",
|
||
pointerEvents: "none",
|
||
opacity: "1",
|
||
transition: "opacity 0.2s",
|
||
});
|
||
document.body.appendChild(toast);
|
||
}
|
||
toast.textContent = text;
|
||
toast.style.opacity = "1";
|
||
if (this.state.pointerLockToastTimer) {
|
||
clearTimeout(this.state.pointerLockToastTimer);
|
||
}
|
||
this.state.pointerLockToastTimer = setTimeout(() => {
|
||
toast.style.opacity = "0";
|
||
this.state.pointerLockToastTimer = null;
|
||
}, duration);
|
||
}
|
||
|
||
handleExternalMouseEvent(event) {
|
||
if (!event || !event.type) return;
|
||
switch (event.type) {
|
||
case "mousedown":
|
||
this.onPointerDown(event);
|
||
break;
|
||
case "mouseup":
|
||
this.onPointerUp(event);
|
||
break;
|
||
case "mousemove":
|
||
this.onPointerMove(event);
|
||
break;
|
||
case "wheel":
|
||
this.onWheel(event);
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
const control = new ControlManager();
|
||
|
||
window.CrossDeskControl = control;
|
||
window.sendRemoteActionAt = (x, y, flag, scroll) =>
|
||
control.sendMouseAction({ x, y, flag, scroll });
|
||
window.sendMouseEvent = (event) => control.handleExternalMouseEvent(event);
|
||
})();
|
||
|