mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Fixes the 51 P0–P2 findings from the full review of the i18n branch and the issues found while checking every new string on screen in ru and es. - Translate errors and statuses that still reached ru/es in English: feed and tile errors, provider statuses, the ALPR chip, the traffic mode chip, voice plan step labels, fire feed values, receiver bands, scene save reasons and the Cesium attribution link. - Show preset city names in the UI language (src/ui/placeNames.js); data and everything sent to the model stay English. - Share layer names between the layer panel, HUD and voice card (src/ui/layerNames.js). - Keep long ru/es strings whole: voice card error lines, analyst titles, the voice tier status, the Nepal evidence button, the ALPR retry chip. English layout is unchanged. - Ask before a language switch drops unsaved work. - Catalog fixes: glossary terms (пуск, пресет), gender agreement and consistent "peticiones" in es, fewer calques in both languages. - Fix English text that was itself wrong (e.g. "Exit cockpit to fly to a weather", raw tool ids in voice plan steps). Catalogs: es/ru 2535 messages each, complete. Co-Authored-By: Claude Opus 5.5 <[email protected]>
240 lines
8.1 KiB
JavaScript
240 lines
8.1 KiB
JavaScript
import { t, msg } from '../i18n/index.js';
|
|
import { bindRadioControls } from './radioBindings.js';
|
|
import { renderRadioState } from './radioPresentation.js';
|
|
|
|
/** Own Radio DOM, tuner interaction and subscription; receive playback and application actions. */
|
|
export class RadioControls {
|
|
constructor({ elements, radio, actions, canvas }) {
|
|
Object.assign(this, elements);
|
|
this.radio = radio;
|
|
this.actions = actions;
|
|
this.canvas = canvas;
|
|
this.destroyed = false;
|
|
this.listeners = new AbortController();
|
|
this._radioUnsubscribe = null;
|
|
this._radioState = null;
|
|
this._radioCategorySignature = '';
|
|
this._radioTunerStations = [];
|
|
this._radioTunerPool = [];
|
|
this._radioTunerDragging = false;
|
|
this._radioTunerDragStartSlot = 0;
|
|
this._radioTunerDragSnapshot = null;
|
|
this._radioTunerLastSlot = 0;
|
|
this._radioTunerDragDirection = 0;
|
|
this._radioTunerCoordinate = 0;
|
|
this._radioTunerPointerId = null;
|
|
this._radioTunerKeyboardKey = null;
|
|
this._radioTunerBandSignature = '';
|
|
this._radioTunerSelectedId = null;
|
|
this._radioTunerBandPinnedForNavigation = false;
|
|
this._refreshRadioTunerBand = null;
|
|
bindRadioControls.call(this);
|
|
}
|
|
|
|
listen(target, type, handler, options = {}) {
|
|
target?.addEventListener(type, handler, {
|
|
...options,
|
|
signal: this.listeners.signal,
|
|
});
|
|
}
|
|
|
|
connect() {
|
|
this._radioUnsubscribe?.();
|
|
this._radioUnsubscribe = null;
|
|
if (this.destroyed) return;
|
|
this._radioUnsubscribe = this.radio.subscribe?.((state) =>
|
|
this._renderRadioState(state),
|
|
);
|
|
}
|
|
|
|
_renderRadioState(state) {
|
|
renderRadioState.call(this, state);
|
|
}
|
|
|
|
/**
|
|
* Reveal the newly enabled directory and transport inside Context without
|
|
* moving focus, the page, or the globe. Only the expanded Enable path calls
|
|
* this helper.
|
|
* @param {HTMLElement} trigger Initiating Radio Enable button.
|
|
* @returns {Promise<boolean>} Whether the internal scroller moved.
|
|
*/
|
|
async _revealRadioControlsAfterExplicitEnable(trigger) {
|
|
const contextPanel = document.getElementById('global-context-panel');
|
|
const scroller =
|
|
(document.documentElement?.dataset.uiTheme === 'cyber' &&
|
|
contextPanel?.querySelector('.cyber-panel-body')) ||
|
|
contextPanel?.querySelector('.global-context-panel-inner');
|
|
const directory = this._radioPanel?.querySelector('.radio-directory-row');
|
|
const transport = this._radioPanel?.querySelector('.radio-transport');
|
|
if (
|
|
!contextPanel ||
|
|
contextPanel.classList.contains('collapsed') ||
|
|
!scroller ||
|
|
!directory ||
|
|
!transport ||
|
|
!this._radioState?.enabled
|
|
)
|
|
return false;
|
|
|
|
await new Promise((resolve) =>
|
|
requestAnimationFrame(() => requestAnimationFrame(resolve)),
|
|
);
|
|
if (this.destroyed || !this._radioState?.enabled || !trigger?.isConnected)
|
|
return false;
|
|
|
|
const viewport = scroller.getBoundingClientRect();
|
|
const directoryRect = directory.getBoundingClientRect();
|
|
const transportRect = transport.getBoundingClientRect();
|
|
const margin = 10;
|
|
const minimum =
|
|
scroller.scrollTop + transportRect.bottom - (viewport.bottom - margin);
|
|
const maximum =
|
|
scroller.scrollTop + directoryRect.top - (viewport.top + margin);
|
|
const desired =
|
|
minimum <= maximum
|
|
? Math.min(Math.max(scroller.scrollTop, minimum), maximum)
|
|
: minimum;
|
|
const next = Math.min(
|
|
Math.max(0, scroller.scrollHeight - scroller.clientHeight),
|
|
Math.max(0, desired),
|
|
);
|
|
if (Math.abs(next - scroller.scrollTop) < 1) return false;
|
|
const reducedMotion = window.matchMedia?.(
|
|
'(prefers-reduced-motion: reduce)',
|
|
)?.matches;
|
|
scroller.scrollTo({
|
|
top: next,
|
|
behavior: reducedMotion ? 'auto' : 'smooth',
|
|
});
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Bring the embedded Radio section into the expanded Context scroller.
|
|
* This never changes Radio power, playback, selection, or Context mode.
|
|
* @param {{focusTarget?: HTMLElement|null}} [options]
|
|
* @returns {Promise<boolean>} Whether the internal scroller moved.
|
|
*/
|
|
async _revealRadioPanelInsideContext({ focusTarget = null } = {}) {
|
|
const contextPanel = document.getElementById('global-context-panel');
|
|
const scroller =
|
|
(document.documentElement?.dataset.uiTheme === 'cyber' &&
|
|
contextPanel?.querySelector('.cyber-panel-body')) ||
|
|
contextPanel?.querySelector('.global-context-panel-inner');
|
|
if (
|
|
!contextPanel ||
|
|
contextPanel.classList.contains('collapsed') ||
|
|
!scroller ||
|
|
!this._radioPanel ||
|
|
this._radioPanel.classList.contains('collapsed')
|
|
)
|
|
return false;
|
|
|
|
await new Promise((resolve) =>
|
|
requestAnimationFrame(() => requestAnimationFrame(resolve)),
|
|
);
|
|
if (
|
|
this.destroyed ||
|
|
contextPanel.classList.contains('collapsed') ||
|
|
this._radioPanel.classList.contains('collapsed')
|
|
)
|
|
return false;
|
|
|
|
const viewport = scroller.getBoundingClientRect();
|
|
const radioRect = this._radioPanel.getBoundingClientRect();
|
|
const desired = scroller.scrollTop + radioRect.top - viewport.top - 10;
|
|
const next = Math.min(
|
|
Math.max(0, scroller.scrollHeight - scroller.clientHeight),
|
|
Math.max(0, desired),
|
|
);
|
|
const moved = Math.abs(next - scroller.scrollTop) >= 1;
|
|
if (moved) {
|
|
const reducedMotion = window.matchMedia?.(
|
|
'(prefers-reduced-motion: reduce)',
|
|
)?.matches;
|
|
scroller.scrollTo({
|
|
top: next,
|
|
behavior: reducedMotion ? 'auto' : 'smooth',
|
|
});
|
|
}
|
|
focusTarget?.focus?.({ preventScroll: true });
|
|
return moved;
|
|
}
|
|
|
|
/** Keep the launcher truthful for the active theme and Context route. */
|
|
_syncContextRadioLauncherState() {
|
|
if (this.destroyed || !this._contextRadioToggleBtn) return;
|
|
const contextPanel = globalThis.document?.getElementById(
|
|
'global-context-panel',
|
|
);
|
|
const contextExpanded = Boolean(
|
|
contextPanel && !contextPanel.classList.contains('collapsed'),
|
|
);
|
|
if (contextExpanded) {
|
|
const radioExpanded = Boolean(
|
|
this._radioPanel && !this._radioPanel.classList.contains('collapsed'),
|
|
);
|
|
this._contextRadioToggleBtn.setAttribute('aria-controls', 'radio-panel');
|
|
this._contextRadioToggleBtn.setAttribute(
|
|
'aria-expanded',
|
|
String(radioExpanded),
|
|
);
|
|
const label = t(
|
|
radioExpanded
|
|
? msg('Go to expanded Radio section')
|
|
: msg('Expand Radio section in Context'),
|
|
);
|
|
this._contextRadioToggleBtn.setAttribute('aria-label', label);
|
|
this._contextRadioToggleBtn.title = label;
|
|
return;
|
|
}
|
|
// This launcher toggles the compact player even when the detailed panel
|
|
// is open elsewhere. Its ARIA state must describe that same disclosure.
|
|
const compactOpen = Boolean(
|
|
this._contextRadioDock?.classList.contains('disclosure-open'),
|
|
);
|
|
this._contextRadioToggleBtn.setAttribute(
|
|
'aria-controls',
|
|
'context-radio-mini',
|
|
);
|
|
this._contextRadioToggleBtn.setAttribute(
|
|
'aria-expanded',
|
|
String(compactOpen),
|
|
);
|
|
const launcherLabel = t(
|
|
compactOpen
|
|
? msg('Close compact Radio controls')
|
|
: msg('Open compact Radio controls'),
|
|
);
|
|
this._contextRadioToggleBtn.setAttribute('aria-label', launcherLabel);
|
|
this._contextRadioToggleBtn.title = launcherLabel;
|
|
}
|
|
|
|
destroy() {
|
|
if (this.destroyed) return;
|
|
this.destroyed = true;
|
|
this.listeners.abort();
|
|
this._radioUnsubscribe?.();
|
|
this._radioUnsubscribe = null;
|
|
const pointer = this._radioTunerPointerId;
|
|
this._radioTunerPointerId = null;
|
|
this._radioTunerDragging = false;
|
|
this._radioTunerKeyboardKey = null;
|
|
this._radioTunerDragSnapshot = null;
|
|
this._radioTunerPool = [];
|
|
this._radioTunerStations = [];
|
|
this._radioTunerBandPinnedForNavigation = false;
|
|
try {
|
|
if (pointer !== null)
|
|
this._radioTunerSlider?.releasePointerCapture(pointer);
|
|
} catch {
|
|
/* capture already released */
|
|
}
|
|
this._radioTuner?.classList.remove('is-dragging', 'is-static');
|
|
document
|
|
.getElementById('title-bar')
|
|
?.classList.remove('radio-broadcasting');
|
|
this.radio.endTuning();
|
|
}
|
|
}
|