mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
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.
127 lines
4.1 KiB
JavaScript
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,
|
|
};
|
|
}
|