Use native Ctrl+V paste and add media stream status diagnostics
This commit is contained in:
@@ -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";
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user