From 830ad199db79226009a862e941295d32087302a6 Mon Sep 17 00:00:00 2001 From: Jage9 Date: Sun, 22 Feb 2026 02:08:14 -0500 Subject: [PATCH] Use native Ctrl+V paste and add media stream status diagnostics --- client/public/version.js | 2 +- client/src/audio/radioStationRuntime.ts | 22 ++++++++++++++++++++++ client/src/main.ts | 24 +++--------------------- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/client/public/version.js b/client/public/version.js index a95b614..3c75b78 100644 --- a/client/public/version.js +++ b/client/public/version.js @@ -1,5 +1,5 @@ // Maintainer-controlled web client version. // Format: YYYY.MM.DD Rn (example: 2026.02.20 R2) -window.CHGRID_WEB_VERSION = "2026.02.22 R137"; +window.CHGRID_WEB_VERSION = "2026.02.22 R138"; // Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid. window.CHGRID_TIME_ZONE = "America/Detroit"; diff --git a/client/src/audio/radioStationRuntime.ts b/client/src/audio/radioStationRuntime.ts index d838eba..af8cb3f 100644 --- a/client/src/audio/radioStationRuntime.ts +++ b/client/src/audio/radioStationRuntime.ts @@ -131,13 +131,23 @@ type RadioSpatialConfig = { export class RadioStationRuntime { private readonly sharedRadioSources = new Map(); private readonly itemRadioOutputs = new Map(); + private readonly lastStreamStatusAt = new Map(); private layerEnabled = true; constructor( private readonly audio: AudioEngine, private readonly getSpatialConfig: (item: WorldItem) => RadioSpatialConfig, + private readonly onStreamStatus?: (message: string) => void, ) {} + private reportStreamStatus(message: string, dedupeKey: string): void { + const now = Date.now(); + const lastAt = this.lastStreamStatusAt.get(dedupeKey) ?? 0; + if (now - lastAt < 3000) return; + this.lastStreamStatusAt.set(dedupeKey, now); + this.onStreamStatus?.(message); + } + cleanup(itemId: string): void { const output = this.itemRadioOutputs.get(itemId); if (!output) return; @@ -286,6 +296,18 @@ export class RadioStationRuntime { element.crossOrigin = 'anonymous'; element.loop = true; element.preload = 'none'; + element.addEventListener('error', () => { + this.reportStreamStatus( + `Media stream failed: ${streamUrl}`, + `error:${streamUrl}`, + ); + }); + element.addEventListener('canplay', () => { + this.reportStreamStatus( + `Media stream ready: ${streamUrl}`, + `ready:${streamUrl}`, + ); + }); const source = audioCtx.createMediaElementSource(element); void element.play().catch(() => undefined); const shared: SharedRadioSource = { diff --git a/client/src/main.ts b/client/src/main.ts index 49eea3d..1289137 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -178,7 +178,7 @@ let outputMode = localStorage.getItem(AUDIO_OUTPUT_MODE_STORAGE_KEY) === 'mono' let connecting = false; const messageBuffer: string[] = []; let messageCursor = -1; -const radioRuntime = new RadioStationRuntime(audio, getItemSpatialConfig); +const radioRuntime = new RadioStationRuntime(audio, getItemSpatialConfig, (message) => updateStatus(message)); const itemEmitRuntime = new ItemEmitRuntime(audio, resolveIncomingSoundUrl, getItemSpatialConfig); let internalClipboardText = ''; let replaceTextOnNextType = false; @@ -629,21 +629,6 @@ function pasteIntoActiveTextInput(raw: string): boolean { return true; } -async function handlePasteShortcut(): Promise { - let pasted = internalClipboardText; - try { - const clipboardText = await navigator.clipboard?.readText(); - if (typeof clipboardText === 'string') { - pasted = clipboardText; - internalClipboardText = clipboardText; - } - } catch { - // Clipboard read can fail without user gesture/permissions; fallback to internal clipboard. - } - if (!pasteIntoActiveTextInput(pasted)) return; - updateStatus('pasted'); -} - function isTextEditingMode(mode: typeof state.mode): boolean { return mode === 'nickname' || mode === 'chat' || mode === 'itemPropertyEdit'; } @@ -2398,7 +2383,8 @@ function setupInputHandlers(): void { if (event.altKey) return; if (event.ctrlKey && !isTextEditingMode(state.mode)) return; - if (state.mode !== 'normal' || !code.startsWith('Arrow')) { + const isNativePasteShortcut = event.ctrlKey && isTextEditingMode(state.mode) && code === 'KeyV'; + if ((state.mode !== 'normal' || !code.startsWith('Arrow')) && !isNativePasteShortcut) { event.preventDefault(); } @@ -2420,10 +2406,6 @@ function setupInputHandlers(): void { updateStatus('cut'); return; } - if (code === 'KeyV') { - void handlePasteShortcut(); - return; - } } if (isTypingKey(code) && state.keysPressed[code]) return;