feat: suspend input during port switch with overlay and countdown
When switching ports via the console dropdown: - Input (keyboard, mouse, wheel) is suspended immediately to prevent stray events from interfering with the OSCAR hotkey sequence - A semi-transparent overlay with spinner and countdown timer appears over the console canvas - Duration is calculated from the actual key_pause_duration setting multiplied by the number of * pause tokens in the port's hotkey, plus a 500ms buffer - Input resumes and overlay disappears when the timer expires Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -155,12 +155,15 @@ function handleMessage(ev: MessageEvent) {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let portHotkeys: Record<number, string> = {}
|
||||
let keyPauseDuration = 2000
|
||||
let inputSuspended = false
|
||||
|
||||
async function loadPortList() {
|
||||
try {
|
||||
const resp = await fetch('/api/kvm/ports')
|
||||
if (!resp.ok) return
|
||||
const data = await resp.json()
|
||||
keyPauseDuration = data.key_pause_duration || 2000
|
||||
const select = document.getElementById('port-select') as HTMLSelectElement
|
||||
if (!select) return
|
||||
select.innerHTML = ''
|
||||
@@ -182,19 +185,69 @@ async function loadPortList() {
|
||||
|
||||
function switchToPort(port: number) {
|
||||
const hotkey = portHotkeys[port]
|
||||
if (hotkey && ws?.readyState === WebSocket.OPEN) {
|
||||
// Send the hotkey sequence as key events over WS to the KVM
|
||||
if (!hotkey || ws?.readyState !== WebSocket.OPEN) return
|
||||
|
||||
// Count pause tokens to calculate total switch duration
|
||||
const pauseCount = (hotkey.match(/\*/g) || []).length
|
||||
const totalDelay = pauseCount * keyPauseDuration + 500 // +500ms buffer
|
||||
|
||||
// Suspend input and show overlay
|
||||
inputSuspended = true
|
||||
showSwitchOverlay(totalDelay)
|
||||
|
||||
// Send the hotkey sequence
|
||||
const messages = parseHotkey(hotkey)
|
||||
for (const msg of messages) {
|
||||
ws.send(msg)
|
||||
}
|
||||
}
|
||||
// Also update the Belkin's active port tracking
|
||||
|
||||
// Update Belkin's active port tracking
|
||||
fetch('/api/kvm/switch', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ port }),
|
||||
}).catch(() => {})
|
||||
|
||||
// Resume input after the switch completes
|
||||
setTimeout(() => {
|
||||
inputSuspended = false
|
||||
hideSwitchOverlay()
|
||||
document.getElementById('canvas')?.focus()
|
||||
}, totalDelay)
|
||||
}
|
||||
|
||||
function showSwitchOverlay(duration: number) {
|
||||
let overlay = document.getElementById('switch-overlay')
|
||||
if (!overlay) {
|
||||
overlay = document.createElement('div')
|
||||
overlay.id = 'switch-overlay'
|
||||
overlay.className = 'switch-overlay'
|
||||
document.querySelector('.console-wrap')?.appendChild(overlay)
|
||||
}
|
||||
overlay.innerHTML = `
|
||||
<div class="switch-overlay-content">
|
||||
<div class="switch-spinner"></div>
|
||||
<div>Switching port...</div>
|
||||
<div class="switch-timer" id="switch-timer"></div>
|
||||
</div>
|
||||
`
|
||||
overlay.hidden = false
|
||||
|
||||
// Countdown timer
|
||||
const start = Date.now()
|
||||
const timerEl = document.getElementById('switch-timer')
|
||||
const tick = () => {
|
||||
if (!timerEl || overlay!.hidden) return
|
||||
const remaining = Math.max(0, duration - (Date.now() - start))
|
||||
timerEl.textContent = `${(remaining / 1000).toFixed(1)}s`
|
||||
if (remaining > 0) requestAnimationFrame(tick)
|
||||
}
|
||||
tick()
|
||||
}
|
||||
|
||||
function hideSwitchOverlay() {
|
||||
const overlay = document.getElementById('switch-overlay')
|
||||
if (overlay) overlay.hidden = true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -206,18 +259,20 @@ function wireInputHandlers() {
|
||||
|
||||
canvas.addEventListener('keydown', (e) => {
|
||||
e.preventDefault()
|
||||
if (inputSuspended) return
|
||||
const sc = codeToScancode(e.code)
|
||||
if (sc !== undefined && ws?.readyState === WebSocket.OPEN) ws.send(makeKeyPress(sc))
|
||||
})
|
||||
|
||||
canvas.addEventListener('keyup', (e) => {
|
||||
e.preventDefault()
|
||||
if (inputSuspended) return
|
||||
const sc = codeToScancode(e.code)
|
||||
if (sc !== undefined && ws?.readyState === WebSocket.OPEN) ws.send(makeKeyRelease(sc))
|
||||
})
|
||||
|
||||
function sendPointer(e: MouseEvent) {
|
||||
if (ws?.readyState !== WebSocket.OPEN) return
|
||||
if (inputSuspended || ws?.readyState !== WebSocket.OPEN) return
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const x = Math.round((e.clientX - rect.left) * canvas.width / rect.width)
|
||||
const y = Math.round((e.clientY - rect.top) * canvas.height / rect.height)
|
||||
@@ -247,7 +302,7 @@ function wireInputHandlers() {
|
||||
canvas.addEventListener('contextmenu', (e) => e.preventDefault())
|
||||
canvas.addEventListener('wheel', (e) => {
|
||||
e.preventDefault()
|
||||
if (ws?.readyState !== WebSocket.OPEN) return
|
||||
if (inputSuspended || ws?.readyState !== WebSocket.OPEN) return
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const x = Math.round((e.clientX - rect.left) * canvas.width / rect.width)
|
||||
const y = Math.round((e.clientY - rect.top) * canvas.height / rect.height)
|
||||
|
||||
@@ -179,6 +179,48 @@ body {
|
||||
cursor: none;
|
||||
}
|
||||
|
||||
/* Port switch overlay */
|
||||
.switch-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.switch-overlay-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
color: #ccc;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.switch-spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid #444;
|
||||
border-top-color: #4a7c59;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.switch-timer {
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: #888;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* Console wrap needs position for overlay */
|
||||
.console-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Ports config page */
|
||||
.ports-page {
|
||||
padding: 1.5rem;
|
||||
|
||||
Reference in New Issue
Block a user