Files
gods-eye-view/src/layers/radio/presentation.js
T
Sameh Khamis b20c80e590 Switch language in place, without a reload
Choosing a language in the picker now applies it at once. The choice is
stored and ?lang is kept in step through replaceState, and the picker keeps
focus.

Code that writes translated text itself re-presents it on a switch:
onLocaleChange() lets an owner listen, removed at teardown, and status
writers hand over a function of the locale (voice cards, scene director,
SDR, draw hints, imagery split, map sources, Bhote Koshi media) so the
words are rebuilt rather than re-sent. The HUD, cockpit, radio, CCTV,
launch, awareness and credit texts follow the same hook, and
applyDocumentTranslations can be applied again without undoing text code
wrote since.

The unsaved-work registry and persistLocaleAndReload existed only to guard
the reload, so they go, with their catalog entries.
2026-11-01 16:47:39 -08:00

127 lines
4.1 KiB
JavaScript

import { getLocale } from '../../i18n/index.js';
export function createPresentation({
state: layerState,
services,
parts,
source,
}) {
/** Return the deeply immutable healthy catalog generation shared with tuner consumers. */
function getRadioAcceptedCatalogSnapshot() {
return layerState._acceptedCatalogSnapshot;
}
/** Build the station categories, labelled in the current locale. */
function buildCategories() {
layerState._categories = Object.freeze(
parts.categories
.buildRadioCategories(layerState._stations)
.map((category) => Object.freeze(category)),
);
layerState._categoriesLocale = getLocale();
}
/** Return an immutable snapshot consumed by the right-rail UI. */
function getRadioUIState() {
// Labels follow a language switch the next time the state is read.
if (
layerState._categories.length &&
layerState._categoriesLocale !== getLocale()
)
buildCategories();
const visible = parts.queries.visibleStations();
const selected = parts.queries.selectedStation();
return Object.freeze({
enabled: layerState._enabled,
loading: layerState._loading,
stale: layerState._stale,
degraded: layerState._degraded,
rejectedStationCount: layerState._rejectedStationCount,
error: layerState._error,
updatedAt: layerState._updatedAt,
filter: layerState._filter,
categories: layerState._categories,
acceptedCatalogGeneration: layerState._acceptedCatalogSnapshot.generation,
presentationActive: parts.interaction.radioPresentationAllowed(),
stationCount: layerState._stations.length,
filteredCount: visible.length,
selected,
selectedIndex: selected
? visible.findIndex((station) => station.id === selected.id)
: -1,
audioState: layerState._audioState,
audioError: layerState._audioError,
playingStationId: layerState._audioStationId,
volume: layerState._userVolume,
effectiveVolume:
layerState._audio?.volume ??
(layerState._voiceDucked ? 0 : layerState._userVolume),
voiceDucked: layerState._voiceDucked,
voiceRestoring: layerState._voiceRestoring,
tuningActive: layerState._tuningActive,
tuningStatic: layerState._tuningStatic,
tuningAwaitingStationId: layerState._tuningAwaitingStationId,
tuningPreviewStationId: layerState._tuningPreviewId,
tuningRestoredStationId:
layerState._cancelledTuningPresentationStation?.id || null,
tuningCatalogGeneration: layerState._tuningResolutionSnapshot.generation,
tuningUnavailableStationId: layerState._tuningUnavailableStationId,
});
}
function emitState() {
const snapshot = getRadioUIState();
for (const listener of layerState._listeners) {
try {
listener(snapshot);
} catch {
// A broken consumer must not break playback or rendering.
}
}
}
/** Subscribe to radio state; the current state is delivered immediately. */
function subscribeToRadio(listener) {
if (typeof listener !== 'function') return () => {};
layerState._listeners.add(listener);
listener(getRadioUIState());
return () => layerState._listeners.delete(listener);
}
/** Subscribe to explicit playback controls so voice handoffs cannot undo them. */
function subscribeToRadioPlaybackControls(listener) {
if (typeof listener !== 'function') return () => {};
layerState._playbackControlListeners.add(listener);
return () => layerState._playbackControlListeners.delete(listener);
}
function emitPlaybackControl(
action,
origin,
attemptId = layerState._activePlaybackAttempt?.id || null,
) {
const event = { action, origin, attemptId };
for (const listener of layerState._playbackControlListeners) {
try {
listener(event);
} catch {
// Playback controls must remain usable if an observer fails.
}
}
}
return {
getRadioAcceptedCatalogSnapshot,
getRadioUIState,
buildCategories,
emitState,
subscribeToRadio,
subscribeToRadioPlaybackControls,
emitPlaybackControl,
};
}