Improve keyboard input user-experience and support Shift
This commit is contained in:
@@ -449,6 +449,15 @@ inline std::string GetWebPageHTML() {
|
|||||||
height: 100dvh !important;
|
height: 100dvh !important;
|
||||||
max-height: none !important;
|
max-height: none !important;
|
||||||
}
|
}
|
||||||
|
/* Portrait fullscreen: align canvas to top */
|
||||||
|
@media (orientation: portrait) {
|
||||||
|
#screen-page:fullscreen .canvas-container,
|
||||||
|
#screen-page:-webkit-full-screen .canvas-container,
|
||||||
|
#screen-page.pseudo-fullscreen .canvas-container {
|
||||||
|
align-items: flex-start !important;
|
||||||
|
padding-top: 56px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
#screen-page.pseudo-fullscreen #screen-canvas {
|
#screen-page.pseudo-fullscreen #screen-canvas {
|
||||||
max-width: 100vw !important; max-height: 100vh !important;
|
max-width: 100vw !important; max-height: 100vh !important;
|
||||||
max-height: 100dvh !important;
|
max-height: 100dvh !important;
|
||||||
@@ -554,6 +563,65 @@ inline std::string GetWebPageHTML() {
|
|||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
}
|
}
|
||||||
|
/* Utility class for hiding elements */
|
||||||
|
.ui-hidden { display: none !important; }
|
||||||
|
/* Quick control buttons - always visible on touch devices */
|
||||||
|
.quick-controls {
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
gap: 8px;
|
||||||
|
z-index: 100;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
.quick-controls.visible { display: flex; }
|
||||||
|
.quick-controls .qc-btn {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
background: rgba(0,0,0,0.6);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
.quick-controls .qc-btn:hover { background: rgba(0,0,0,0.8); }
|
||||||
|
.quick-controls .qc-btn:active { transform: scale(0.9); }
|
||||||
|
.quick-controls .qc-btn.active { background: rgba(52,199,89,0.9); }
|
||||||
|
.quick-controls .qc-btn:disabled { opacity: 0.4; }
|
||||||
|
/* Portrait: horizontal at top center */
|
||||||
|
@media (orientation: portrait) {
|
||||||
|
.quick-controls {
|
||||||
|
top: 4px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 4px;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.quick-controls .qc-btn {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
/* Portrait: align canvas to top, leave space for controls */
|
||||||
|
.canvas-container {
|
||||||
|
align-items: flex-start;
|
||||||
|
padding-top: 56px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Landscape: vertical at right center */
|
||||||
|
@media (orientation: landscape) {
|
||||||
|
.quick-controls {
|
||||||
|
right: 8px;
|
||||||
|
top: 8px;
|
||||||
|
transform: none;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
.cursor-overlay {
|
.cursor-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
@@ -567,6 +635,37 @@ inline std::string GetWebPageHTML() {
|
|||||||
transform-origin: 0 0;
|
transform-origin: 0 0;
|
||||||
}
|
}
|
||||||
.cursor-overlay.active { display: block; }
|
.cursor-overlay.active { display: block; }
|
||||||
|
/* Input shortcut bar - below canvas, portrait mode only */
|
||||||
|
.input-shortcuts {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: none;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 6px 4px;
|
||||||
|
z-index: 101;
|
||||||
|
}
|
||||||
|
.input-shortcuts.visible { display: flex; }
|
||||||
|
.input-shortcuts .shortcut-btn {
|
||||||
|
min-width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(128,128,128,0.5);
|
||||||
|
background: rgba(128,128,128,0.4);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.1s;
|
||||||
|
text-shadow: 0 0 2px #000, 0 0 4px #000, 1px 1px 1px #000;
|
||||||
|
}
|
||||||
|
.input-shortcuts .shortcut-btn:hover { background: rgba(128,128,128,0.5); }
|
||||||
|
.input-shortcuts .shortcut-btn:active { transform: scale(0.95); background: rgba(128,128,128,0.6); }
|
||||||
/* Mobile responsive */
|
/* Mobile responsive */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.page { padding: 10px; }
|
.page { padding: 10px; }
|
||||||
@@ -655,7 +754,26 @@ inline std::string GetWebPageHTML() {
|
|||||||
<button class="fullscreen-btn" onclick="toggleFullscreen()" title="Fullscreen (F11)">⛶</button>
|
<button class="fullscreen-btn" onclick="toggleFullscreen()" title="Fullscreen (F11)">⛶</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="canvas-container"><canvas id="screen-canvas"></canvas><div class="cursor-overlay" id="cursor-overlay"></div></div>
|
<div class="canvas-container">
|
||||||
|
<canvas id="screen-canvas"></canvas>
|
||||||
|
<div class="cursor-overlay" id="cursor-overlay"></div>
|
||||||
|
<div class="quick-controls" id="quick-controls">
|
||||||
|
<button class="qc-btn" id="qc-rdp" onclick="sendRdpReset()" title="RDP Reset" tabindex="-1">↻</button>
|
||||||
|
<button class="qc-btn" id="qc-keyboard" onclick="toggleKeyboard()" title="Keyboard" tabindex="-1">⌨</button>
|
||||||
|
<button class="qc-btn" id="qc-mouse" onclick="toggleControl()" title="Mouse" tabindex="-1">🖱</button>
|
||||||
|
<button class="qc-btn" id="qc-disconnect" onclick="disconnect()" title="Disconnect" tabindex="-1">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="input-shortcuts" id="input-shortcuts">
|
||||||
|
<button class="shortcut-btn" data-key="49" tabindex="-1">1</button>
|
||||||
|
<button class="shortcut-btn" data-key="50" tabindex="-1">2</button>
|
||||||
|
<button class="shortcut-btn" data-key="51" tabindex="-1">3</button>
|
||||||
|
<button class="shortcut-btn" data-key="52" tabindex="-1">4</button>
|
||||||
|
<button class="shortcut-btn" data-key="53" tabindex="-1">5</button>
|
||||||
|
<button class="shortcut-btn" data-key="190" tabindex="-1">.</button>
|
||||||
|
<button class="shortcut-btn" data-key="188" tabindex="-1">,</button>
|
||||||
|
<button class="shortcut-btn" data-key="191" data-shift="1" tabindex="-1">?</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="touch-indicator" id="touch-indicator"></div>
|
<div class="touch-indicator" id="touch-indicator"></div>
|
||||||
<button class="toolbar-toggle" id="toolbar-toggle" onclick="toggleFloatingToolbar()">•••</button>
|
<button class="toolbar-toggle" id="toolbar-toggle" onclick="toggleFloatingToolbar()">•••</button>
|
||||||
<div class="floating-toolbar" id="floating-toolbar">
|
<div class="floating-toolbar" id="floating-toolbar">
|
||||||
@@ -907,6 +1025,9 @@ inline std::string GetWebPageHTML() {
|
|||||||
ctx.fillStyle = '#000';
|
ctx.fillStyle = '#000';
|
||||||
ctx.fillRect(0, 0, width, height);
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
|
// Update input shortcuts position after canvas resize
|
||||||
|
requestAnimationFrame(updateInputShortcutsPosition);
|
||||||
|
|
||||||
// Set up vertical flip transform once (BMP is bottom-up)
|
// Set up vertical flip transform once (BMP is bottom-up)
|
||||||
ctx.setTransform(1, 0, 0, -1, 0, height);
|
ctx.setTransform(1, 0, 0, -1, 0, height);
|
||||||
if (decoder) { try { decoder.close(); } catch(e) {} }
|
if (decoder) { try { decoder.close(); } catch(e) {} }
|
||||||
@@ -1198,16 +1319,17 @@ inline std::string GetWebPageHTML() {
|
|||||||
|
|
||||||
function toggleFullscreen() {
|
function toggleFullscreen() {
|
||||||
const el = document.getElementById('screen-page');
|
const el = document.getElementById('screen-page');
|
||||||
const isFullscreen = document.fullscreenElement || document.webkitFullscreenElement;
|
const isFs = document.fullscreenElement || document.webkitFullscreenElement;
|
||||||
const isPseudo = el.classList.contains('pseudo-fullscreen');
|
const isPseudo = el.classList.contains('pseudo-fullscreen');
|
||||||
|
|
||||||
// Try native fullscreen first
|
// Try native fullscreen first
|
||||||
if (!isPseudo && (el.requestFullscreen || el.webkitRequestFullscreen)) {
|
if (!isPseudo && (el.requestFullscreen || el.webkitRequestFullscreen)) {
|
||||||
if (!isFullscreen) {
|
if (!isFs) {
|
||||||
(el.requestFullscreen || el.webkitRequestFullscreen).call(el).catch(() => {
|
(el.requestFullscreen || el.webkitRequestFullscreen).call(el).catch(() => {
|
||||||
// Native fullscreen failed (iOS), use pseudo-fullscreen
|
// Native fullscreen failed (iOS), use pseudo-fullscreen
|
||||||
el.classList.add('pseudo-fullscreen');
|
el.classList.add('pseudo-fullscreen');
|
||||||
window.scrollTo(0, 1);
|
window.scrollTo(0, 1);
|
||||||
|
updateUIForOrientation();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (document.exitFullscreen) document.exitFullscreen();
|
if (document.exitFullscreen) document.exitFullscreen();
|
||||||
@@ -1217,6 +1339,7 @@ inline std::string GetWebPageHTML() {
|
|||||||
// Toggle pseudo-fullscreen (iOS fallback)
|
// Toggle pseudo-fullscreen (iOS fallback)
|
||||||
el.classList.toggle('pseudo-fullscreen');
|
el.classList.toggle('pseudo-fullscreen');
|
||||||
if (el.classList.contains('pseudo-fullscreen')) window.scrollTo(0, 1);
|
if (el.classList.contains('pseudo-fullscreen')) window.scrollTo(0, 1);
|
||||||
|
updateUIForOrientation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)HTML";
|
)HTML";
|
||||||
@@ -1235,6 +1358,7 @@ inline std::string GetWebPageHTML() {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - lastTapTime < 300 && e.changedTouches.length === 1) {
|
if (now - lastTapTime < 300 && e.changedTouches.length === 1) {
|
||||||
el.classList.remove('pseudo-fullscreen');
|
el.classList.remove('pseudo-fullscreen');
|
||||||
|
updateUIForOrientation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
lastTapTime = now;
|
lastTapTime = now;
|
||||||
@@ -1254,14 +1378,25 @@ inline std::string GetWebPageHTML() {
|
|||||||
let toolbarVisible = false;
|
let toolbarVisible = false;
|
||||||
let toolbarHideTimer = null;
|
let toolbarHideTimer = null;
|
||||||
|
|
||||||
|
function isFullscreen() {
|
||||||
|
return !!(document.fullscreenElement || document.webkitFullscreenElement ||
|
||||||
|
document.getElementById('screen-page')?.classList.contains('pseudo-fullscreen'));
|
||||||
|
}
|
||||||
|
|
||||||
function toggleFloatingToolbar() {
|
function toggleFloatingToolbar() {
|
||||||
const toolbar = document.getElementById('floating-toolbar');
|
const toolbar = document.getElementById('floating-toolbar');
|
||||||
toolbarVisible = !toolbarVisible;
|
toolbarVisible = !toolbarVisible;
|
||||||
toolbar.classList.toggle('visible', toolbarVisible);
|
toolbar.classList.toggle('visible', toolbarVisible);
|
||||||
|
|
||||||
// Auto-hide after 4 seconds
|
// Clear any existing timer
|
||||||
if (toolbarHideTimer) clearTimeout(toolbarHideTimer);
|
if (toolbarHideTimer) {
|
||||||
if (toolbarVisible) {
|
clearTimeout(toolbarHideTimer);
|
||||||
|
toolbarHideTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only auto-hide when NOT in fullscreen (desktop behavior)
|
||||||
|
// In fullscreen (touch device), user must click again to close
|
||||||
|
if (toolbarVisible && !isFullscreen()) {
|
||||||
toolbarHideTimer = setTimeout(() => {
|
toolbarHideTimer = setTimeout(() => {
|
||||||
toolbarVisible = false;
|
toolbarVisible = false;
|
||||||
toolbar.classList.remove('visible');
|
toolbar.classList.remove('visible');
|
||||||
@@ -1269,6 +1404,88 @@ inline std::string GetWebPageHTML() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isLandscape() {
|
||||||
|
return window.innerWidth > window.innerHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cached DOM elements (initialized in window.onload)
|
||||||
|
let uiElements = null;
|
||||||
|
|
||||||
|
function initUIElements() {
|
||||||
|
uiElements = {
|
||||||
|
quickControls: document.getElementById('quick-controls'),
|
||||||
|
toolbarToggle: document.getElementById('toolbar-toggle'),
|
||||||
|
floatingToolbar: document.getElementById('floating-toolbar'),
|
||||||
|
btnRdpResetBar: document.getElementById('btn-rdp-reset-bar'),
|
||||||
|
btnMouseBar: document.getElementById('btn-mouse-bar'),
|
||||||
|
btnKeyboardBar: document.getElementById('btn-keyboard-bar'),
|
||||||
|
inputShortcuts: document.getElementById('input-shortcuts')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUIForOrientation() {
|
||||||
|
if (!isTouchDevice || !uiElements) return;
|
||||||
|
|
||||||
|
// Clear any pending toolbar hide timer when orientation/fullscreen changes
|
||||||
|
if (toolbarHideTimer) {
|
||||||
|
clearTimeout(toolbarHideTimer);
|
||||||
|
toolbarHideTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { quickControls, toolbarToggle, floatingToolbar,
|
||||||
|
btnRdpResetBar, btnMouseBar, btnKeyboardBar, inputShortcuts } = uiElements;
|
||||||
|
|
||||||
|
if (isLandscape()) {
|
||||||
|
// Landscape mode
|
||||||
|
quickControls.classList.remove('visible');
|
||||||
|
inputShortcuts.classList.remove('visible');
|
||||||
|
|
||||||
|
if (isFullscreen()) {
|
||||||
|
// Landscape fullscreen: show three-dot menu
|
||||||
|
toolbarToggle.classList.remove('ui-hidden');
|
||||||
|
btnRdpResetBar.classList.add('ui-hidden');
|
||||||
|
btnMouseBar.classList.add('ui-hidden');
|
||||||
|
btnKeyboardBar.classList.add('ui-hidden');
|
||||||
|
} else {
|
||||||
|
// Landscape non-fullscreen: show top toolbar buttons
|
||||||
|
toolbarToggle.classList.add('ui-hidden');
|
||||||
|
floatingToolbar.classList.remove('visible');
|
||||||
|
toolbarVisible = false;
|
||||||
|
btnRdpResetBar.classList.remove('ui-hidden');
|
||||||
|
btnMouseBar.classList.remove('ui-hidden');
|
||||||
|
btnKeyboardBar.classList.remove('ui-hidden');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Portrait mode: show quick controls
|
||||||
|
quickControls.classList.add('visible');
|
||||||
|
// Input shortcuts only visible when keyboard is open
|
||||||
|
const keyboardOpen = document.activeElement === mobileKeyboard;
|
||||||
|
inputShortcuts.classList.toggle('visible', keyboardOpen);
|
||||||
|
toolbarToggle.classList.add('ui-hidden');
|
||||||
|
floatingToolbar.classList.remove('visible');
|
||||||
|
toolbarVisible = false;
|
||||||
|
btnRdpResetBar.classList.add('ui-hidden');
|
||||||
|
btnMouseBar.classList.add('ui-hidden');
|
||||||
|
btnKeyboardBar.classList.add('ui-hidden');
|
||||||
|
}
|
||||||
|
// Position input shortcuts below canvas
|
||||||
|
updateInputShortcutsPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position input shortcuts bar below the canvas
|
||||||
|
function updateInputShortcutsPosition() {
|
||||||
|
if (!isTouchDevice || !uiElements || !uiElements.inputShortcuts) return;
|
||||||
|
if (isLandscape()) return; // Only for portrait mode
|
||||||
|
|
||||||
|
const canvas = document.getElementById('screen-canvas');
|
||||||
|
const shortcuts = uiElements.inputShortcuts;
|
||||||
|
if (!canvas || canvas.offsetHeight === 0) return;
|
||||||
|
|
||||||
|
// Position shortcuts 8px below canvas
|
||||||
|
const canvasBottom = canvas.offsetTop + canvas.offsetHeight;
|
||||||
|
shortcuts.style.top = (canvasBottom + 8) + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
function sendRdpReset() {
|
function sendRdpReset() {
|
||||||
if (ws && ws.readyState === WebSocket.OPEN && token) {
|
if (ws && ws.readyState === WebSocket.OPEN && token) {
|
||||||
ws.send(JSON.stringify({ cmd: 'rdp_reset', token }));
|
ws.send(JSON.stringify({ cmd: 'rdp_reset', token }));
|
||||||
@@ -1290,14 +1507,20 @@ inline std::string GetWebPageHTML() {
|
|||||||
controlEnabled = !controlEnabled;
|
controlEnabled = !controlEnabled;
|
||||||
// Update floating toolbar buttons
|
// Update floating toolbar buttons
|
||||||
const btnMouse = document.getElementById('btn-mouse');
|
const btnMouse = document.getElementById('btn-mouse');
|
||||||
const btnKeyboard = document.getElementById('btn-keyboard');
|
|
||||||
btnMouse.classList.toggle('active', controlEnabled);
|
btnMouse.classList.toggle('active', controlEnabled);
|
||||||
btnKeyboard.disabled = !controlEnabled;
|
|
||||||
// Update top toolbar buttons (sync state)
|
// Update top toolbar buttons (sync state)
|
||||||
const btnMouseBar = document.getElementById('btn-mouse-bar');
|
const btnMouseBar = document.getElementById('btn-mouse-bar');
|
||||||
const btnKeyboardBar = document.getElementById('btn-keyboard-bar');
|
|
||||||
btnMouseBar.classList.toggle('active', controlEnabled);
|
btnMouseBar.classList.toggle('active', controlEnabled);
|
||||||
btnKeyboardBar.disabled = !controlEnabled;
|
// Update quick controls buttons
|
||||||
|
const qcMouse = document.getElementById('qc-mouse');
|
||||||
|
if (qcMouse) qcMouse.classList.toggle('active', controlEnabled);
|
||||||
|
// Desktop only: keyboard requires mouse control enabled
|
||||||
|
if (!isTouchDevice) {
|
||||||
|
const btnKeyboard = document.getElementById('btn-keyboard');
|
||||||
|
const btnKeyboardBar = document.getElementById('btn-keyboard-bar');
|
||||||
|
if (btnKeyboard) btnKeyboard.disabled = !controlEnabled;
|
||||||
|
if (btnKeyboardBar) btnKeyboardBar.disabled = !controlEnabled;
|
||||||
|
}
|
||||||
// Cursor handling
|
// Cursor handling
|
||||||
const canvas = document.getElementById('screen-canvas');
|
const canvas = document.getElementById('screen-canvas');
|
||||||
const cursorOverlay = document.getElementById('cursor-overlay');
|
const cursorOverlay = document.getElementById('cursor-overlay');
|
||||||
@@ -1314,15 +1537,14 @@ inline std::string GetWebPageHTML() {
|
|||||||
const canvas = document.getElementById('screen-canvas');
|
const canvas = document.getElementById('screen-canvas');
|
||||||
const cursorOverlay = document.getElementById('cursor-overlay');
|
const cursorOverlay = document.getElementById('cursor-overlay');
|
||||||
|
|
||||||
// Get canvas base rect (without transform, use container)
|
// Get canvas base position (without transform, use container + offset)
|
||||||
|
// offsetLeft/offsetTop are layout positions, unaffected by CSS transform
|
||||||
const container = document.querySelector('.canvas-container');
|
const container = document.querySelector('.canvas-container');
|
||||||
const containerRect = container.getBoundingClientRect();
|
const containerRect = container.getBoundingClientRect();
|
||||||
|
|
||||||
// Calculate canvas position within container (centered)
|
|
||||||
const canvasDisplayWidth = canvas.offsetWidth;
|
const canvasDisplayWidth = canvas.offsetWidth;
|
||||||
const canvasDisplayHeight = canvas.offsetHeight;
|
const canvasDisplayHeight = canvas.offsetHeight;
|
||||||
const canvasLeft = containerRect.left + (containerRect.width - canvasDisplayWidth) / 2;
|
const canvasLeft = containerRect.left + canvas.offsetLeft;
|
||||||
const canvasTop = containerRect.top + (containerRect.height - canvasDisplayHeight) / 2;
|
const canvasTop = containerRect.top + canvas.offsetTop;
|
||||||
|
|
||||||
// Convert canvas coords to position on unzoomed canvas
|
// Convert canvas coords to position on unzoomed canvas
|
||||||
// Map [0, width-1] to [0, 1] for proper edge-to-edge display
|
// Map [0, width-1] to [0, 1] for proper edge-to-edge display
|
||||||
@@ -1354,13 +1576,13 @@ inline std::string GetWebPageHTML() {
|
|||||||
// Used to recalculate cursor position after zoom/pan
|
// Used to recalculate cursor position after zoom/pan
|
||||||
function screenToCanvas(screenX, screenY) {
|
function screenToCanvas(screenX, screenY) {
|
||||||
const canvas = document.getElementById('screen-canvas');
|
const canvas = document.getElementById('screen-canvas');
|
||||||
|
// Get canvas base position (without transform, use container + offset)
|
||||||
const container = document.querySelector('.canvas-container');
|
const container = document.querySelector('.canvas-container');
|
||||||
const containerRect = container.getBoundingClientRect();
|
const containerRect = container.getBoundingClientRect();
|
||||||
|
|
||||||
const canvasDisplayWidth = canvas.offsetWidth;
|
const canvasDisplayWidth = canvas.offsetWidth;
|
||||||
const canvasDisplayHeight = canvas.offsetHeight;
|
const canvasDisplayHeight = canvas.offsetHeight;
|
||||||
const canvasLeft = containerRect.left + (containerRect.width - canvasDisplayWidth) / 2;
|
const canvasLeft = containerRect.left + canvas.offsetLeft;
|
||||||
const canvasTop = containerRect.top + (containerRect.height - canvasDisplayHeight) / 2;
|
const canvasTop = containerRect.top + canvas.offsetTop;
|
||||||
|
|
||||||
// Reverse the transform chain
|
// Reverse the transform chain
|
||||||
const scaledX = screenX - canvasLeft;
|
const scaledX = screenX - canvasLeft;
|
||||||
@@ -1937,9 +2159,48 @@ inline std::string GetWebPageHTML() {
|
|||||||
touchState.touchCount = e.touches.length;
|
touchState.touchCount = e.touches.length;
|
||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
|
|
||||||
function toggleKeyboard() {
|
let lastKeyboardBtnTouch = 0; // Timestamp of last keyboard button touch
|
||||||
mobileKeyboard.focus();
|
let wasKeyboardFocused = false; // Capture focus state at touchstart (before blur)
|
||||||
|
|
||||||
|
function updateKeyboardButtons(active) {
|
||||||
|
// Update all keyboard buttons across different toolbars
|
||||||
|
const qcKeyboard = document.getElementById('qc-keyboard');
|
||||||
|
const btnKeyboard = document.getElementById('btn-keyboard');
|
||||||
|
const btnKeyboardBar = document.getElementById('btn-keyboard-bar');
|
||||||
|
if (qcKeyboard) qcKeyboard.classList.toggle('active', active);
|
||||||
|
if (btnKeyboard) btnKeyboard.classList.toggle('active', active);
|
||||||
|
if (btnKeyboardBar) btnKeyboardBar.classList.toggle('active', active);
|
||||||
|
// Show/hide input shortcuts based on keyboard state (portrait only)
|
||||||
|
const shortcuts = document.getElementById('input-shortcuts');
|
||||||
|
if (shortcuts && !isLandscape()) {
|
||||||
|
shortcuts.classList.toggle('visible', active);
|
||||||
|
if (active) updateInputShortcutsPosition();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleKeyboard() {
|
||||||
|
// Use focus state captured at touchstart (more reliable than button's active class)
|
||||||
|
const isOpen = wasKeyboardFocused;
|
||||||
|
wasKeyboardFocused = false; // Reset
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
mobileKeyboard.blur();
|
||||||
|
updateKeyboardButtons(false);
|
||||||
|
} else {
|
||||||
|
mobileKeyboard.focus();
|
||||||
|
updateKeyboardButtons(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mobileKeyboard.addEventListener('focus', function() {
|
||||||
|
updateKeyboardButtons(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
mobileKeyboard.addEventListener('blur', function() {
|
||||||
|
// Skip if keyboard button was touched within last 300ms
|
||||||
|
if (Date.now() - lastKeyboardBtnTouch < 300) return;
|
||||||
|
updateKeyboardButtons(false);
|
||||||
|
});
|
||||||
|
|
||||||
function sendRightClick() {
|
function sendRightClick() {
|
||||||
// Use cursor position (canvas coordinates), not touch position (screen coordinates)
|
// Use cursor position (canvas coordinates), not touch position (screen coordinates)
|
||||||
@@ -2029,9 +2290,32 @@ inline std::string GetWebPageHTML() {
|
|||||||
mobileKeyboard.addEventListener('input', function(e) {
|
mobileKeyboard.addEventListener('input', function(e) {
|
||||||
const char = e.data;
|
const char = e.data;
|
||||||
if (char) {
|
if (char) {
|
||||||
const keyCode = char.toUpperCase().charCodeAt(0);
|
// Check if character needs Shift key
|
||||||
|
const isUpperCase = char >= 'A' && char <= 'Z';
|
||||||
|
const shiftSymbols = '~!@#$%^&*()_+{}|:"<>?';
|
||||||
|
const needsShift = isUpperCase || shiftSymbols.includes(char);
|
||||||
|
|
||||||
|
// Map symbols to their base keys
|
||||||
|
const symbolMap = {
|
||||||
|
'~': 192, '!': 49, '@': 50, '#': 51, '$': 52, '%': 53,
|
||||||
|
'^': 54, '&': 55, '*': 56, '(': 57, ')': 48, '_': 189,
|
||||||
|
'+': 187, '{': 219, '}': 221, '|': 220, ':': 186,
|
||||||
|
'"': 222, '<': 188, '>': 190, '?': 191
|
||||||
|
};
|
||||||
|
|
||||||
|
let keyCode;
|
||||||
|
if (symbolMap[char]) {
|
||||||
|
keyCode = symbolMap[char];
|
||||||
|
} else {
|
||||||
|
keyCode = char.toUpperCase().charCodeAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send Shift down if needed
|
||||||
|
if (needsShift) sendKey(16, true); // VK_SHIFT = 16
|
||||||
sendKey(keyCode, true);
|
sendKey(keyCode, true);
|
||||||
sendKey(keyCode, false);
|
sendKey(keyCode, false);
|
||||||
|
// Send Shift up if needed
|
||||||
|
if (needsShift) sendKey(16, false);
|
||||||
}
|
}
|
||||||
mobileKeyboard.value = '';
|
mobileKeyboard.value = '';
|
||||||
});
|
});
|
||||||
@@ -2052,16 +2336,24 @@ inline std::string GetWebPageHTML() {
|
|||||||
function disconnect() {
|
function disconnect() {
|
||||||
// Reset control mode
|
// Reset control mode
|
||||||
controlEnabled = false;
|
controlEnabled = false;
|
||||||
|
// Reset keyboard state (blur event will update button state)
|
||||||
|
mobileKeyboard.blur();
|
||||||
// Reset floating toolbar buttons
|
// Reset floating toolbar buttons
|
||||||
const btnMouse = document.getElementById('btn-mouse');
|
const btnMouse = document.getElementById('btn-mouse');
|
||||||
const btnKeyboard = document.getElementById('btn-keyboard');
|
|
||||||
if (btnMouse) btnMouse.classList.remove('active');
|
if (btnMouse) btnMouse.classList.remove('active');
|
||||||
if (btnKeyboard) btnKeyboard.disabled = true;
|
|
||||||
// Reset top toolbar buttons
|
// Reset top toolbar buttons
|
||||||
const btnMouseBar = document.getElementById('btn-mouse-bar');
|
const btnMouseBar = document.getElementById('btn-mouse-bar');
|
||||||
const btnKeyboardBar = document.getElementById('btn-keyboard-bar');
|
|
||||||
if (btnMouseBar) btnMouseBar.classList.remove('active');
|
if (btnMouseBar) btnMouseBar.classList.remove('active');
|
||||||
|
// Desktop only: disable keyboard button
|
||||||
|
if (!isTouchDevice) {
|
||||||
|
const btnKeyboard = document.getElementById('btn-keyboard');
|
||||||
|
const btnKeyboardBar = document.getElementById('btn-keyboard-bar');
|
||||||
|
if (btnKeyboard) btnKeyboard.disabled = true;
|
||||||
if (btnKeyboardBar) btnKeyboardBar.disabled = true;
|
if (btnKeyboardBar) btnKeyboardBar.disabled = true;
|
||||||
|
}
|
||||||
|
// Reset quick control buttons
|
||||||
|
const qcMouse = document.getElementById('qc-mouse');
|
||||||
|
if (qcMouse) qcMouse.classList.remove('active');
|
||||||
document.getElementById('screen-canvas').style.cursor = 'default';
|
document.getElementById('screen-canvas').style.cursor = 'default';
|
||||||
document.getElementById('cursor-overlay').classList.remove('active');
|
document.getElementById('cursor-overlay').classList.remove('active');
|
||||||
|
|
||||||
@@ -2121,11 +2413,89 @@ inline std::string GetWebPageHTML() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.onload = function() {
|
window.onload = function() {
|
||||||
|
// Initialize cached DOM elements
|
||||||
|
initUIElements();
|
||||||
|
|
||||||
const compat = checkWebCodecs();
|
const compat = checkWebCodecs();
|
||||||
if (!compat.supported) {
|
if (!compat.supported) {
|
||||||
document.body.insertAdjacentHTML('afterbegin',
|
document.body.insertAdjacentHTML('afterbegin',
|
||||||
'<div class="compat-warning">Warning: Your browser may not support H264 decoding.</div>');
|
'<div class="compat-warning">Warning: Your browser may not support H264 decoding.</div>');
|
||||||
}
|
}
|
||||||
|
// On touch devices: setup UI based on orientation and fullscreen state
|
||||||
|
if (isTouchDevice) {
|
||||||
|
// Initial UI setup
|
||||||
|
updateUIForOrientation();
|
||||||
|
|
||||||
|
// Touch devices: keyboard is independent from mouse, enable keyboard buttons
|
||||||
|
const btnKeyboard = document.getElementById('btn-keyboard');
|
||||||
|
const btnKeyboardBar = document.getElementById('btn-keyboard-bar');
|
||||||
|
if (btnKeyboard) btnKeyboard.disabled = false;
|
||||||
|
if (btnKeyboardBar) btnKeyboardBar.disabled = false;
|
||||||
|
|
||||||
|
// Listen for orientation changes (with debounce)
|
||||||
|
let resizeTimer = null;
|
||||||
|
window.addEventListener('resize', function() {
|
||||||
|
if (resizeTimer) clearTimeout(resizeTimer);
|
||||||
|
resizeTimer = setTimeout(updateUIForOrientation, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for fullscreen changes
|
||||||
|
document.addEventListener('fullscreenchange', updateUIForOrientation);
|
||||||
|
document.addEventListener('webkitfullscreenchange', updateUIForOrientation);
|
||||||
|
|
||||||
|
// Input shortcut buttons event handlers
|
||||||
|
// Shortcuts only visible when keyboard is open, so no extra check needed
|
||||||
|
function sendShortcutKey(keyCode, isDown) {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN && token) {
|
||||||
|
ws.send(JSON.stringify({ cmd: 'key', token, keyCode, down: isDown, alt: false }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const shortcutBtns = document.querySelectorAll('.shortcut-btn');
|
||||||
|
shortcutBtns.forEach(function(btn) {
|
||||||
|
btn.addEventListener('touchstart', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const keyCode = parseInt(btn.dataset.key);
|
||||||
|
const needShift = btn.dataset.shift === '1';
|
||||||
|
if (needShift) sendShortcutKey(16, true); // Shift down
|
||||||
|
sendShortcutKey(keyCode, true);
|
||||||
|
sendShortcutKey(keyCode, false);
|
||||||
|
if (needShift) sendShortcutKey(16, false); // Shift up
|
||||||
|
});
|
||||||
|
btn.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
// Only handle click for non-touch (mouse)
|
||||||
|
if (!('ontouchstart' in window)) {
|
||||||
|
const keyCode = parseInt(btn.dataset.key);
|
||||||
|
const needShift = btn.dataset.shift === '1';
|
||||||
|
if (needShift) sendShortcutKey(16, true);
|
||||||
|
sendShortcutKey(keyCode, true);
|
||||||
|
sendShortcutKey(keyCode, false);
|
||||||
|
if (needShift) sendShortcutKey(16, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Desktop: hide keyboard buttons (physical keyboard available)
|
||||||
|
const btnKeyboard = document.getElementById('btn-keyboard');
|
||||||
|
const btnKeyboardBar = document.getElementById('btn-keyboard-bar');
|
||||||
|
if (btnKeyboard) btnKeyboard.classList.add('ui-hidden');
|
||||||
|
if (btnKeyboardBar) btnKeyboardBar.classList.add('ui-hidden');
|
||||||
|
}
|
||||||
|
// Keyboard buttons: capture focus state and prevent blur from updating button state
|
||||||
|
function bindKeyboardBtnEvents(btn) {
|
||||||
|
if (!btn) return;
|
||||||
|
btn.addEventListener('touchstart', function() {
|
||||||
|
lastKeyboardBtnTouch = Date.now();
|
||||||
|
wasKeyboardFocused = (document.activeElement === mobileKeyboard);
|
||||||
|
}, { passive: true });
|
||||||
|
btn.addEventListener('mousedown', function() {
|
||||||
|
lastKeyboardBtnTouch = Date.now();
|
||||||
|
wasKeyboardFocused = (document.activeElement === mobileKeyboard);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
bindKeyboardBtnEvents(document.getElementById('qc-keyboard'));
|
||||||
|
bindKeyboardBtnEvents(document.getElementById('btn-keyboard'));
|
||||||
|
bindKeyboardBtnEvents(document.getElementById('btn-keyboard-bar'));
|
||||||
// Restore token from sessionStorage
|
// Restore token from sessionStorage
|
||||||
token = sessionStorage.getItem('token');
|
token = sessionStorage.getItem('token');
|
||||||
connectWebSocket();
|
connectWebSocket();
|
||||||
|
|||||||
Reference in New Issue
Block a user