diff --git a/crates/ericrfb-frontend/src/pages/console.ts b/crates/ericrfb-frontend/src/pages/console.ts index cc28a18..63bec83 100644 --- a/crates/ericrfb-frontend/src/pages/console.ts +++ b/crates/ericrfb-frontend/src/pages/console.ts @@ -155,12 +155,15 @@ function handleMessage(ev: MessageEvent) { // --------------------------------------------------------------------------- let portHotkeys: Record = {} +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 - const messages = parseHotkey(hotkey) - for (const msg of messages) { - ws.send(msg) - } + 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 = ` +
+
+
Switching port...
+
+
+ ` + 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) diff --git a/crates/ericrfb-frontend/src/style.css b/crates/ericrfb-frontend/src/style.css index 262b172..71bd7d3 100644 --- a/crates/ericrfb-frontend/src/style.css +++ b/crates/ericrfb-frontend/src/style.css @@ -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;