Improve keyboard input user-experience and support Shift

This commit is contained in:
yuanyuanxiang
2026-04-22 00:05:38 +02:00
parent 01c7fc1c63
commit 011ec3d509

View File

@@ -449,6 +449,15 @@ inline std::string GetWebPageHTML() {
height: 100dvh !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 {
max-width: 100vw !important; max-height: 100vh !important;
max-height: 100dvh !important;
@@ -554,6 +563,65 @@ inline std::string GetWebPageHTML() {
transform: translate(-50%, -50%);
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 {
position: fixed;
width: 24px;
@@ -567,6 +635,37 @@ inline std::string GetWebPageHTML() {
transform-origin: 0 0;
}
.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 */
@media (max-width: 768px) {
.page { padding: 10px; }
@@ -655,7 +754,26 @@ inline std::string GetWebPageHTML() {
<button class="fullscreen-btn" onclick="toggleFullscreen()" title="Fullscreen (F11)">&#x26F6;</button>
</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">&#x21BB;</button>
<button class="qc-btn" id="qc-keyboard" onclick="toggleKeyboard()" title="Keyboard" tabindex="-1">&#x2328;</button>
<button class="qc-btn" id="qc-mouse" onclick="toggleControl()" title="Mouse" tabindex="-1">&#x1F5B1;</button>
<button class="qc-btn" id="qc-disconnect" onclick="disconnect()" title="Disconnect" tabindex="-1">&#x2715;</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>
<button class="toolbar-toggle" id="toolbar-toggle" onclick="toggleFloatingToolbar()">&#x2022;&#x2022;&#x2022;</button>
<div class="floating-toolbar" id="floating-toolbar">
@@ -907,6 +1025,9 @@ inline std::string GetWebPageHTML() {
ctx.fillStyle = '#000';
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)
ctx.setTransform(1, 0, 0, -1, 0, height);
if (decoder) { try { decoder.close(); } catch(e) {} }
@@ -1198,16 +1319,17 @@ inline std::string GetWebPageHTML() {
function toggleFullscreen() {
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');
// Try native fullscreen first
if (!isPseudo && (el.requestFullscreen || el.webkitRequestFullscreen)) {
if (!isFullscreen) {
if (!isFs) {
(el.requestFullscreen || el.webkitRequestFullscreen).call(el).catch(() => {
// Native fullscreen failed (iOS), use pseudo-fullscreen
el.classList.add('pseudo-fullscreen');
window.scrollTo(0, 1);
updateUIForOrientation();
});
} else {
if (document.exitFullscreen) document.exitFullscreen();
@@ -1217,6 +1339,7 @@ inline std::string GetWebPageHTML() {
// Toggle pseudo-fullscreen (iOS fallback)
el.classList.toggle('pseudo-fullscreen');
if (el.classList.contains('pseudo-fullscreen')) window.scrollTo(0, 1);
updateUIForOrientation();
}
}
)HTML";
@@ -1235,6 +1358,7 @@ inline std::string GetWebPageHTML() {
const now = Date.now();
if (now - lastTapTime < 300 && e.changedTouches.length === 1) {
el.classList.remove('pseudo-fullscreen');
updateUIForOrientation();
e.preventDefault();
}
lastTapTime = now;
@@ -1254,14 +1378,25 @@ inline std::string GetWebPageHTML() {
let toolbarVisible = false;
let toolbarHideTimer = null;
function isFullscreen() {
return !!(document.fullscreenElement || document.webkitFullscreenElement ||
document.getElementById('screen-page')?.classList.contains('pseudo-fullscreen'));
}
function toggleFloatingToolbar() {
const toolbar = document.getElementById('floating-toolbar');
toolbarVisible = !toolbarVisible;
toolbar.classList.toggle('visible', toolbarVisible);
// Auto-hide after 4 seconds
if (toolbarHideTimer) clearTimeout(toolbarHideTimer);
if (toolbarVisible) {
// Clear any existing timer
if (toolbarHideTimer) {
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(() => {
toolbarVisible = false;
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() {
if (ws && ws.readyState === WebSocket.OPEN && token) {
ws.send(JSON.stringify({ cmd: 'rdp_reset', token }));
@@ -1290,14 +1507,20 @@ inline std::string GetWebPageHTML() {
controlEnabled = !controlEnabled;
// Update floating toolbar buttons
const btnMouse = document.getElementById('btn-mouse');
const btnKeyboard = document.getElementById('btn-keyboard');
btnMouse.classList.toggle('active', controlEnabled);
btnKeyboard.disabled = !controlEnabled;
// Update top toolbar buttons (sync state)
const btnMouseBar = document.getElementById('btn-mouse-bar');
const btnKeyboardBar = document.getElementById('btn-keyboard-bar');
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
const canvas = document.getElementById('screen-canvas');
const cursorOverlay = document.getElementById('cursor-overlay');
@@ -1314,15 +1537,14 @@ inline std::string GetWebPageHTML() {
const canvas = document.getElementById('screen-canvas');
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 containerRect = container.getBoundingClientRect();
// Calculate canvas position within container (centered)
const canvasDisplayWidth = canvas.offsetWidth;
const canvasDisplayHeight = canvas.offsetHeight;
const canvasLeft = containerRect.left + (containerRect.width - canvasDisplayWidth) / 2;
const canvasTop = containerRect.top + (containerRect.height - canvasDisplayHeight) / 2;
const canvasLeft = containerRect.left + canvas.offsetLeft;
const canvasTop = containerRect.top + canvas.offsetTop;
// Convert canvas coords to position on unzoomed canvas
// 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
function screenToCanvas(screenX, screenY) {
const canvas = document.getElementById('screen-canvas');
// Get canvas base position (without transform, use container + offset)
const container = document.querySelector('.canvas-container');
const containerRect = container.getBoundingClientRect();
const canvasDisplayWidth = canvas.offsetWidth;
const canvasDisplayHeight = canvas.offsetHeight;
const canvasLeft = containerRect.left + (containerRect.width - canvasDisplayWidth) / 2;
const canvasTop = containerRect.top + (containerRect.height - canvasDisplayHeight) / 2;
const canvasLeft = containerRect.left + canvas.offsetLeft;
const canvasTop = containerRect.top + canvas.offsetTop;
// Reverse the transform chain
const scaledX = screenX - canvasLeft;
@@ -1937,9 +2159,48 @@ inline std::string GetWebPageHTML() {
touchState.touchCount = e.touches.length;
}, { passive: false });
function toggleKeyboard() {
mobileKeyboard.focus();
let lastKeyboardBtnTouch = 0; // Timestamp of last keyboard button touch
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() {
// Use cursor position (canvas coordinates), not touch position (screen coordinates)
@@ -2029,9 +2290,32 @@ inline std::string GetWebPageHTML() {
mobileKeyboard.addEventListener('input', function(e) {
const char = e.data;
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, false);
// Send Shift up if needed
if (needsShift) sendKey(16, false);
}
mobileKeyboard.value = '';
});
@@ -2052,16 +2336,24 @@ inline std::string GetWebPageHTML() {
function disconnect() {
// Reset control mode
controlEnabled = false;
// Reset keyboard state (blur event will update button state)
mobileKeyboard.blur();
// Reset floating toolbar buttons
const btnMouse = document.getElementById('btn-mouse');
const btnKeyboard = document.getElementById('btn-keyboard');
if (btnMouse) btnMouse.classList.remove('active');
if (btnKeyboard) btnKeyboard.disabled = true;
// Reset top toolbar buttons
const btnMouseBar = document.getElementById('btn-mouse-bar');
const btnKeyboardBar = document.getElementById('btn-keyboard-bar');
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;
}
// 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('cursor-overlay').classList.remove('active');
@@ -2121,11 +2413,89 @@ inline std::string GetWebPageHTML() {
});
window.onload = function() {
// Initialize cached DOM elements
initUIElements();
const compat = checkWebCodecs();
if (!compat.supported) {
document.body.insertAdjacentHTML('afterbegin',
'<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
token = sessionStorage.getItem('token');
connectWebSocket();