mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
- i18n scanner: lastError sink, no false sinks on defaults and ternaries - shared src/i18n/marker.js for modules that only mark strings - wind field messages move to wind/fields.js; CITY_POIS imports from cityPois.js - drop review IDs, change history and stale references from comments and docs Co-Authored-By: Claude Opus 5.5 <[email protected]>
261 lines
9.1 KiB
JavaScript
261 lines
9.1 KiB
JavaScript
import { t, translateKnown } from '../i18n/index.js';
|
||
import {
|
||
SPACE_MISSIONS_LAYER_BLOCK_REASON,
|
||
cockpitEntryAllowed,
|
||
isExplicitUserIntentOrigin,
|
||
parseContextLayerBlockReason,
|
||
recordContextRestoreExplicitChange,
|
||
recordContextSessionUserChange,
|
||
shouldExitContextForLayerChange,
|
||
spaceMissionEntryCancellationDisposition,
|
||
} from '../contextModePolicy.js';
|
||
import { localizeLayerName } from './layerNames.js';
|
||
|
||
/**
|
||
* A blocked-visibility reason in the UI language. Reasons stay English (voice
|
||
* and tools read them): the Space Missions refusal is rebuilt with the layer
|
||
* name localized, a known sentence is looked up, anything else passes.
|
||
*/
|
||
export function localizeVisibilityBlockReason(reason) {
|
||
if (typeof reason !== 'string' || !reason) return reason;
|
||
const blocked = parseContextLayerBlockReason(reason);
|
||
if (blocked)
|
||
return t(SPACE_MISSIONS_LAYER_BLOCK_REASON, {
|
||
layer: localizeLayerName(blocked.layer),
|
||
});
|
||
return translateKnown(reason);
|
||
}
|
||
|
||
export function _handleContextLayerChange(change) {
|
||
if (this.destroyed) return;
|
||
if (
|
||
change?.layerId === 'radio' &&
|
||
[
|
||
'visibility-transition',
|
||
'visibility',
|
||
'visibility-cancelled',
|
||
'visibility-failed',
|
||
].includes(change.type)
|
||
) {
|
||
this.actions.refreshRadio();
|
||
}
|
||
if (change?.type === 'visibility-transition') return;
|
||
// The effective mode must be read BEFORE the entering flag is cleared:
|
||
// the entry layer's own enable event is the one that clears it, and the
|
||
// session bookkeeping below needs to know a mode was being entered.
|
||
const effectiveContextMode = this._contextModeEntering || this._contextMode;
|
||
if (change?.type === 'visibility-cancelled') {
|
||
const cancellationDisposition = spaceMissionEntryCancellationDisposition({
|
||
change,
|
||
});
|
||
if (
|
||
this._contextModeDeferredEntryIntent?.layerId === change.layerId &&
|
||
this._contextModeDeferredEntryIntent.intentEpoch === change.intentEpoch
|
||
) {
|
||
if (cancellationDisposition !== 'replacement') {
|
||
this._contextModeDeferredEntryIntent = null;
|
||
}
|
||
}
|
||
if (cancellationDisposition === 'replacement') {
|
||
this._contextModeEntering = 'space-missions';
|
||
const entryIntent = this._contextModeEntryIntent;
|
||
if (
|
||
entryIntent?.generation === this._contextModeGeneration &&
|
||
entryIntent.layerId === change.layerId &&
|
||
entryIntent.intentEpoch === change.intentEpoch
|
||
) {
|
||
this._contextModeReplacementIntent = {
|
||
generation: entryIntent.generation,
|
||
layerId: change.layerId,
|
||
intentEpoch: change.successorIntentEpoch,
|
||
};
|
||
}
|
||
} else if (cancellationDisposition === 'restore') {
|
||
this._contextModeEntering = null;
|
||
this._contextModeEntryIntent = null;
|
||
this._contextModeReplacementIntent = null;
|
||
if (this._contextSessionSnapshot && !this._contextModeChanging) {
|
||
this._contextMode = null;
|
||
void this._trackContextLayerReaction(
|
||
this._runUserFacingContextAction(
|
||
async (notificationToken) => {
|
||
await this._restoreContextSessionAfterLayerSettles(
|
||
change.layerId,
|
||
{
|
||
notificationToken,
|
||
},
|
||
);
|
||
return true;
|
||
},
|
||
'Space Missions cancellation could not restore the previous layer state', // i18n-ignore: English message; the toast shows localizedMessage
|
||
{
|
||
localizedMessage: t(
|
||
'Space Missions cancellation could not restore the previous layer state',
|
||
),
|
||
},
|
||
),
|
||
);
|
||
}
|
||
}
|
||
this._syncContextModeButtons();
|
||
return;
|
||
}
|
||
if (
|
||
change?.layerId === 'rocket-launches' &&
|
||
['visibility', 'visibility-blocked', 'visibility-failed'].includes(
|
||
change.type,
|
||
)
|
||
) {
|
||
this._contextModeEntering = null;
|
||
}
|
||
if (change?.type === 'visibility-blocked') {
|
||
if (
|
||
!this._userFacingContextNotificationTokens.has(change.notificationToken)
|
||
) {
|
||
this.showToast(
|
||
localizeVisibilityBlockReason(change.reason) ||
|
||
t('That layer is unavailable in the current Context mode'),
|
||
);
|
||
}
|
||
this._syncContextModeButtons();
|
||
return;
|
||
}
|
||
if (change?.type === 'visibility-failed') {
|
||
const failureMessage = t('{layerId} could not {action} cleanly', {
|
||
layerId: change.layerId,
|
||
action: change.enabled ? t('start') : t('stop'),
|
||
});
|
||
// A failed direct Context-shell START has already had its siblings
|
||
// cleared by the visibility guard. Wait outside the synchronous manager
|
||
// notification for this queue to settle, then reconcile the complete
|
||
// snapshot, including an uncertain failed shell.
|
||
const needsDeferredShellRestore =
|
||
['military-awareness', 'rocket-launches'].includes(change.layerId) &&
|
||
change.enabled &&
|
||
this._contextSessionSnapshot &&
|
||
!this._contextModeChanging;
|
||
if (needsDeferredShellRestore) {
|
||
this._contextMode = null;
|
||
void this._trackContextLayerReaction(
|
||
this._runUserFacingContextAction(async (notificationToken) => {
|
||
await this._restoreContextSessionAfterLayerSettles(change.layerId, {
|
||
notificationToken,
|
||
});
|
||
// The wrapper owns failure announcements. On a successful rollback
|
||
// announce the original activation failure here so the same direct
|
||
// action still produces exactly one accessible notification.
|
||
this.showToast(failureMessage);
|
||
return true;
|
||
}, failureMessage),
|
||
);
|
||
} else if (
|
||
!this._userFacingContextNotificationTokens.has(change.notificationToken)
|
||
) {
|
||
this.showToast(failureMessage);
|
||
}
|
||
this._syncContextModeButtons();
|
||
return;
|
||
}
|
||
if (change?.type === 'visibility-will-change') {
|
||
// Explicit entry capture happens on the synchronous visibility-requested
|
||
// boundary. Keeping this later branch side-effect free prevents an
|
||
// awaited Clear/guard from replacing that authoritative pre-entry view.
|
||
return;
|
||
}
|
||
// Session bookkeeping must run BEFORE any exit path below: the exit
|
||
// handlers restore `snapshot ∪ userAdded`, so a stale entry here becomes
|
||
// a layer resurrected against the user's explicit disable.
|
||
recordContextSessionUserChange({
|
||
snapshot: this._contextSessionSnapshot,
|
||
change,
|
||
effectiveContextMode,
|
||
});
|
||
recordContextRestoreExplicitChange({
|
||
restoreState: this._contextRestoreState,
|
||
change,
|
||
});
|
||
if (
|
||
shouldExitContextForLayerChange({
|
||
contextMode: this._contextMode,
|
||
globalContextEnabled:
|
||
!!this._dataManager?.isEnabled('military-awareness'),
|
||
change,
|
||
})
|
||
) {
|
||
void this._trackContextLayerReaction(
|
||
this._runUserFacingContextAction((notificationToken) =>
|
||
this._deactivateContextForLayerChange({ notificationToken }),
|
||
),
|
||
);
|
||
return;
|
||
}
|
||
if (!this._contextModeChanging) {
|
||
if (change.layerId === 'military-awareness') {
|
||
// The coordinator remains manager-addressable for restoration and
|
||
// programmatic routes, but Contacts is selected only from the
|
||
// dedicated right-side Global Context chooser.
|
||
this._contextMode = change.enabled
|
||
? null
|
||
: this._contextMode === 'flights'
|
||
? null
|
||
: this._contextMode;
|
||
if (change.enabled) {
|
||
this._syncContextModeButtons();
|
||
} else if (this._contextSessionSnapshot) {
|
||
void this._trackContextLayerReaction(
|
||
this._runUserFacingContextAction((notificationToken) =>
|
||
this._deactivateContextForLayerChange({ notificationToken }),
|
||
),
|
||
);
|
||
}
|
||
} else if (change.layerId === 'rocket-launches') {
|
||
const ownsContextEntry =
|
||
isExplicitUserIntentOrigin(change.origin, change.layerId) ||
|
||
this._contextMode === 'space-missions' ||
|
||
effectiveContextMode === 'space-missions';
|
||
if (!ownsContextEntry) return;
|
||
this._contextMode = change.enabled
|
||
? 'space-missions'
|
||
: this._contextMode === 'space-missions'
|
||
? null
|
||
: this._contextMode;
|
||
if (change.enabled) {
|
||
this._syncContextModeButtons();
|
||
} else if (this._contextSessionSnapshot) {
|
||
void this._trackContextLayerReaction(
|
||
this._runUserFacingContextAction((notificationToken) =>
|
||
this._deactivateContextForLayerChange({ notificationToken }),
|
||
),
|
||
);
|
||
}
|
||
} else if (
|
||
this._contextMode === 'flights' &&
|
||
[
|
||
'flights',
|
||
'military',
|
||
'ais-live-vessels',
|
||
'military-installations',
|
||
].includes(change.layerId) &&
|
||
!change.enabled
|
||
) {
|
||
void this._trackContextLayerReaction(
|
||
this._runUserFacingContextAction((notificationToken) =>
|
||
this._deactivateContextForLayerChange({ notificationToken }),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
if (
|
||
this.cockpitView?.active &&
|
||
!cockpitEntryAllowed({
|
||
contextMode: this._contextMode,
|
||
contextModeChanging: this._contextModeChanging,
|
||
flightsEnabled: !!this._dataManager?.isEnabled('flights'),
|
||
militaryEnabled: !!this._dataManager?.isEnabled('military'),
|
||
})
|
||
) {
|
||
this.cockpitView.exit({ restoreTracking: false });
|
||
}
|
||
this._syncContextModeButtons();
|
||
}
|