Use native Ctrl+V paste and add media stream status diagnostics

This commit is contained in:
Jage9
2026-02-22 02:08:14 -05:00
parent 460ad08c02
commit 830ad199db
3 changed files with 26 additions and 22 deletions

View File

@@ -1,5 +1,5 @@
// Maintainer-controlled web client version. // Maintainer-controlled web client version.
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2) // 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. // Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
window.CHGRID_TIME_ZONE = "America/Detroit"; window.CHGRID_TIME_ZONE = "America/Detroit";

View File

@@ -131,13 +131,23 @@ type RadioSpatialConfig = {
export class RadioStationRuntime { export class RadioStationRuntime {
private readonly sharedRadioSources = new Map<string, SharedRadioSource>(); private readonly sharedRadioSources = new Map<string, SharedRadioSource>();
private readonly itemRadioOutputs = new Map<string, ItemRadioOutput>(); private readonly itemRadioOutputs = new Map<string, ItemRadioOutput>();
private readonly lastStreamStatusAt = new Map<string, number>();
private layerEnabled = true; private layerEnabled = true;
constructor( constructor(
private readonly audio: AudioEngine, private readonly audio: AudioEngine,
private readonly getSpatialConfig: (item: WorldItem) => RadioSpatialConfig, 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 { cleanup(itemId: string): void {
const output = this.itemRadioOutputs.get(itemId); const output = this.itemRadioOutputs.get(itemId);
if (!output) return; if (!output) return;
@@ -286,6 +296,18 @@ export class RadioStationRuntime {
element.crossOrigin = 'anonymous'; element.crossOrigin = 'anonymous';
element.loop = true; element.loop = true;
element.preload = 'none'; 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); const source = audioCtx.createMediaElementSource(element);
void element.play().catch(() => undefined); void element.play().catch(() => undefined);
const shared: SharedRadioSource = { const shared: SharedRadioSource = {

View File

@@ -178,7 +178,7 @@ let outputMode = localStorage.getItem(AUDIO_OUTPUT_MODE_STORAGE_KEY) === 'mono'
let connecting = false; let connecting = false;
const messageBuffer: string[] = []; const messageBuffer: string[] = [];
let messageCursor = -1; 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); const itemEmitRuntime = new ItemEmitRuntime(audio, resolveIncomingSoundUrl, getItemSpatialConfig);
let internalClipboardText = ''; let internalClipboardText = '';
let replaceTextOnNextType = false; let replaceTextOnNextType = false;
@@ -629,21 +629,6 @@ function pasteIntoActiveTextInput(raw: string): boolean {
return true; return true;
} }
async function handlePasteShortcut(): Promise<void> {
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 { function isTextEditingMode(mode: typeof state.mode): boolean {
return mode === 'nickname' || mode === 'chat' || mode === 'itemPropertyEdit'; return mode === 'nickname' || mode === 'chat' || mode === 'itemPropertyEdit';
} }
@@ -2398,7 +2383,8 @@ function setupInputHandlers(): void {
if (event.altKey) return; if (event.altKey) return;
if (event.ctrlKey && !isTextEditingMode(state.mode)) 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(); event.preventDefault();
} }
@@ -2420,10 +2406,6 @@ function setupInputHandlers(): void {
updateStatus('cut'); updateStatus('cut');
return; return;
} }
if (code === 'KeyV') {
void handlePasteShortcut();
return;
}
} }
if (isTypingKey(code) && state.keysPressed[code]) return; if (isTypingKey(code) && state.keysPressed[code]) return;