mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
- Selections take the playback fallback as a `playFallback` option and set it after releasing a stale stream, instead of callers arming state that the release then had to save and restore. - Move the tuner `station-unavailable` release and the Pause label while a stream connects out of this branch: both were wrong on main already and go in a separate change. - Share the Radio layer test setup in src/testSupport/radioLayerHarness.mjs and fold overlapping Next/Previous tests (13 to 7). - One-line comments and a two-line changelog entry.
234 lines
7.7 KiB
JavaScript
234 lines
7.7 KiB
JavaScript
import {
|
|
RADIO_TUNER_STATION_LIMIT,
|
|
RADIO_TUNER_DIRECTORY_LIMIT,
|
|
EMPTY_ACCEPTED_CATALOG_SNAPSHOT,
|
|
} from './policy.js';
|
|
|
|
export function createTuning({ state: layerState, services, parts, source }) {
|
|
/** Bound one already ordered filtered directory without injecting outside selections. */
|
|
|
|
function buildRadioTunerBand(
|
|
rankedStations,
|
|
selected,
|
|
limit = RADIO_TUNER_STATION_LIMIT,
|
|
) {
|
|
const boundedLimit = Math.min(
|
|
RADIO_TUNER_DIRECTORY_LIMIT,
|
|
Math.max(1, Math.floor(Number(limit) || RADIO_TUNER_STATION_LIMIT)),
|
|
);
|
|
return (Array.isArray(rankedStations) ? rankedStations : []).slice(
|
|
0,
|
|
boundedLimit,
|
|
);
|
|
}
|
|
|
|
function captureRadioTuningResolutionSnapshot() {
|
|
return layerState._acceptedCatalogSnapshot;
|
|
}
|
|
|
|
/** Return the complete accepted filtered directory in stable catalog order. */
|
|
|
|
function getRadioTunerStations(limit = RADIO_TUNER_STATION_LIMIT) {
|
|
if (
|
|
!parts.interaction.radioPresentationAllowed() ||
|
|
!layerState._acceptedCatalogSnapshot.stations.length
|
|
)
|
|
return [];
|
|
return buildRadioTunerBand(
|
|
parts.queries.visibleStations(),
|
|
parts.queries.selectedStation(),
|
|
limit,
|
|
);
|
|
}
|
|
|
|
/** Begin a direct-manipulation tuning gesture and pause the current stream. */
|
|
|
|
function beginRadioTuning() {
|
|
if (
|
|
!parts.interaction.radioPresentationAllowed() ||
|
|
!parts.queries.visibleStations().length
|
|
)
|
|
return false;
|
|
const snapshot = captureRadioTuningResolutionSnapshot();
|
|
if (!Number.isSafeInteger(snapshot.generation) || !snapshot.stations.length)
|
|
return false;
|
|
layerState._tuningResolutionSnapshot = snapshot;
|
|
layerState._tuningStationById = new Map(
|
|
layerState._tuningResolutionSnapshot.stations.map((station) => [
|
|
station.id,
|
|
station,
|
|
]),
|
|
);
|
|
layerState._tuningUnavailableStationId = null;
|
|
layerState._tuningActive = true;
|
|
layerState._tuningStatic = false;
|
|
layerState._tuningAwaitingStationId = null;
|
|
layerState._cancelledTuningPresentationStation = null;
|
|
layerState._tuningStartStationId = layerState._selectedId;
|
|
layerState._tuningPreviewId = layerState._selectedId;
|
|
parts.tuningNoise.installTuningNoise();
|
|
parts.playback.pauseRadioPlayback({ origin: 'user' });
|
|
parts.tuningNoise.syncTuningNoiseGain();
|
|
parts.presentation.emitState();
|
|
return true;
|
|
}
|
|
|
|
/** Toggle low-volume synthesized static for tuner preview and stream handoff. */
|
|
|
|
function setRadioTuningStatic(active) {
|
|
if (
|
|
!parts.interaction.radioPresentationAllowed() ||
|
|
!layerState._tuningActive
|
|
)
|
|
return false;
|
|
const next = Boolean(active);
|
|
if (next === layerState._tuningStatic) return true;
|
|
layerState._tuningStatic = next;
|
|
parts.tuningNoise.syncTuningNoiseGain();
|
|
parts.presentation.emitState();
|
|
return true;
|
|
}
|
|
|
|
/** Preview a tuner station bracket and camera orientation without starting audio. */
|
|
|
|
function previewRadioTuningStation(id, { rotate = true } = {}) {
|
|
if (
|
|
!parts.interaction.radioPresentationAllowed() ||
|
|
!layerState._tuningActive
|
|
)
|
|
return false;
|
|
const station = parts.model.tuningResolutionStation(id);
|
|
const nextId = station?.id || null;
|
|
const changed = nextId !== layerState._tuningPreviewId;
|
|
layerState._tuningPreviewId = nextId;
|
|
layerState._tuningStatic = !station;
|
|
if (changed) {
|
|
parts.rendering.updateSelectionEntity();
|
|
if (station && rotate) {
|
|
const cameraState = parts.navigation.radioCameraState();
|
|
layerState._tuningCameraNavigation =
|
|
parts.navigation.beginRadioCameraNavigation(cameraState);
|
|
parts.navigation.rotateRadioStationIntoView(
|
|
station,
|
|
0.35,
|
|
cameraState,
|
|
layerState._tuningCameraNavigation,
|
|
);
|
|
} else if (rotate) {
|
|
layerState._tuningCameraNavigation = null;
|
|
parts.navigation.invalidateRadioCameraNavigation();
|
|
}
|
|
}
|
|
parts.tuningNoise.syncTuningNoiseGain();
|
|
parts.presentation.emitState();
|
|
return Boolean(station);
|
|
}
|
|
|
|
/** Finish a tuning gesture and release its synthesized noise source. */
|
|
|
|
function endRadioTuning() {
|
|
if (
|
|
!layerState._tuningActive &&
|
|
!layerState._tuningStatic &&
|
|
!layerState._tuningAwaitingStationId &&
|
|
!layerState._tuningNoiseSource &&
|
|
!layerState._cancelledTuningPresentationStation
|
|
)
|
|
return;
|
|
parts.tuningNoise.clearRadioTuningNoise();
|
|
}
|
|
|
|
/** Cancel a tuning gesture and restore its frozen start marker for presentation only. */
|
|
|
|
function cancelRadioTuning() {
|
|
if (!layerState._tuningActive) return false;
|
|
const restoredStation = parts.model.tuningResolutionStation(
|
|
layerState._tuningStartStationId,
|
|
);
|
|
parts.tuningNoise.clearRadioTuningNoise({ restoredStation });
|
|
return true;
|
|
}
|
|
|
|
/** Commit the exact frozen drag resolution or report that it is unavailable. */
|
|
|
|
function commitRadioTuningStation(id, { origin = 'programmatic' } = {}) {
|
|
const frozenStation = parts.model.tuningResolutionStation(id);
|
|
const currentStation = layerState._stationById.get(String(id)) || null;
|
|
const generation = layerState._tuningResolutionSnapshot.generation;
|
|
if (
|
|
!parts.interaction.radioPresentationAllowed() ||
|
|
!layerState._tuningActive ||
|
|
!frozenStation
|
|
) {
|
|
endRadioTuning();
|
|
return Object.freeze({
|
|
ok: false,
|
|
reason: 'not-tuning',
|
|
stationId: String(id || ''),
|
|
generation,
|
|
});
|
|
}
|
|
if (
|
|
!parts.model.radioStationResolutionMatches(frozenStation, currentStation)
|
|
) {
|
|
const stationId = frozenStation.id;
|
|
// A failed exact release owns the visible outcome. Do not let the previous
|
|
// selection—or changed metadata under the same ID—reappear between the
|
|
// frozen preview and the unavailable state.
|
|
layerState._selectedId = null;
|
|
layerState._selectionGeneration += 1;
|
|
parts.tuningNoise.clearRadioTuningNoise({ emit: false });
|
|
layerState._tuningUnavailableStationId = stationId;
|
|
parts.presentation.emitState();
|
|
return Object.freeze({
|
|
ok: false,
|
|
reason: 'station-unavailable',
|
|
stationId,
|
|
generation,
|
|
});
|
|
}
|
|
layerState._tuningActive = false;
|
|
layerState._tuningStatic = true;
|
|
layerState._tuningAwaitingStationId = frozenStation.id;
|
|
layerState._tuningPreviewId = null;
|
|
layerState._tuningStartStationId = null;
|
|
layerState._tuningUnavailableStationId = null;
|
|
const cameraNavigation = layerState._tuningCameraNavigation;
|
|
layerState._tuningCameraNavigation = null;
|
|
layerState._tuningResolutionSnapshot = EMPTY_ACCEPTED_CATALOG_SNAPSHOT;
|
|
layerState._tuningStationById = new Map();
|
|
// An exact tuner release owns its frozen target. A fallback armed by an
|
|
// earlier non-playing selection must never retarget this gesture if the
|
|
// broadcaster fails after release.
|
|
layerState._playFallbackId = null;
|
|
layerState._playFallbackFocus = null;
|
|
layerState._playFallbackOrigin = 'programmatic';
|
|
layerState._playFallbackAttemptId = null;
|
|
parts.tuningNoise.syncTuningNoiseGain();
|
|
parts.presentation.emitState();
|
|
const selected = parts.selection.selectRadioStation(frozenStation.id, {
|
|
autoplay: true,
|
|
focus: false,
|
|
origin,
|
|
cameraNavigation,
|
|
});
|
|
return Object.freeze({
|
|
ok: selected,
|
|
reason: selected ? null : 'station-unavailable',
|
|
stationId: frozenStation.id,
|
|
generation,
|
|
});
|
|
}
|
|
return {
|
|
buildRadioTunerBand,
|
|
captureRadioTuningResolutionSnapshot,
|
|
getRadioTunerStations,
|
|
beginRadioTuning,
|
|
setRadioTuningStatic,
|
|
previewRadioTuningStation,
|
|
endRadioTuning,
|
|
cancelRadioTuning,
|
|
commitRadioTuningStation,
|
|
};
|
|
}
|