mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
style: format Radio component boundary
This commit is contained in:
1 parent
7b9aca8597
commit
bd6cc5abe8
7 files changed
+943
-433
No files matched your search
+53
-163
@@ -10,47 +10,27 @@
|
|||||||
"src/data/localGeojsonCore.js",
|
"src/data/localGeojsonCore.js",
|
||||||
"src/data/localGeojsonLod.js"
|
"src/data/localGeojsonLod.js"
|
||||||
],
|
],
|
||||||
"external": [
|
"external": ["cesium"]
|
||||||
"cesium"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"application": {
|
"application": {
|
||||||
"exports": [
|
"exports": ["./application"],
|
||||||
"./application"
|
"modules": ["src/app/application.js"],
|
||||||
],
|
|
||||||
"modules": [
|
|
||||||
"src/app/application.js"
|
|
||||||
],
|
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
"viewer": {
|
"viewer": {
|
||||||
"exports": [
|
"exports": ["./application/viewer"],
|
||||||
"./application/viewer"
|
"modules": ["src/app/viewer.js"],
|
||||||
],
|
"external": ["cesium"]
|
||||||
"modules": [
|
|
||||||
"src/app/viewer.js"
|
|
||||||
],
|
|
||||||
"external": [
|
|
||||||
"cesium"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"vite-build": {
|
"vite-build": {
|
||||||
"runtime": "node",
|
"runtime": "node",
|
||||||
"exports": [
|
"exports": ["./build/vite"],
|
||||||
"./build/vite"
|
"modules": ["build/vite.js"],
|
||||||
],
|
"external": ["vite-plugin-cesium"]
|
||||||
"modules": [
|
|
||||||
"build/vite.js"
|
|
||||||
],
|
|
||||||
"external": [
|
|
||||||
"vite-plugin-cesium"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"live-providers": {
|
"live-providers": {
|
||||||
"runtime": "node",
|
"runtime": "node",
|
||||||
"exports": [
|
"exports": ["./server/providers/live"],
|
||||||
"./server/providers/live"
|
|
||||||
],
|
|
||||||
"modules": [
|
"modules": [
|
||||||
"server/providers/live.js",
|
"server/providers/live.js",
|
||||||
"server/providers/common/http.js",
|
"server/providers/common/http.js",
|
||||||
@@ -65,24 +45,16 @@
|
|||||||
"src/data/aisStreamAdapter.js",
|
"src/data/aisStreamAdapter.js",
|
||||||
"src/data/aisWatchdog.js"
|
"src/data/aisWatchdog.js"
|
||||||
],
|
],
|
||||||
"external": [
|
"external": ["ws"]
|
||||||
"ws"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"adsb-lol-source": {
|
"adsb-lol-source": {
|
||||||
"exports": [
|
"exports": ["./sources/adsb-lol"],
|
||||||
"./sources/adsb-lol"
|
"modules": ["src/data/adsbLolFallback.js"],
|
||||||
],
|
|
||||||
"modules": [
|
|
||||||
"src/data/adsbLolFallback.js"
|
|
||||||
],
|
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
"place-providers": {
|
"place-providers": {
|
||||||
"runtime": "node",
|
"runtime": "node",
|
||||||
"exports": [
|
"exports": ["./server/providers/places"],
|
||||||
"./server/providers/places"
|
|
||||||
],
|
|
||||||
"modules": [
|
"modules": [
|
||||||
"server/providers/places.js",
|
"server/providers/places.js",
|
||||||
"server/providers/places/google.js",
|
"server/providers/places/google.js",
|
||||||
@@ -97,19 +69,13 @@
|
|||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
"place-payloads": {
|
"place-payloads": {
|
||||||
"exports": [
|
"exports": ["./sources/places"],
|
||||||
"./sources/places"
|
"modules": ["src/data/placeProviderPayloads.js"],
|
||||||
],
|
|
||||||
"modules": [
|
|
||||||
"src/data/placeProviderPayloads.js"
|
|
||||||
],
|
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
"space-providers": {
|
"space-providers": {
|
||||||
"runtime": "node",
|
"runtime": "node",
|
||||||
"exports": [
|
"exports": ["./server/providers/space"],
|
||||||
"./server/providers/space"
|
|
||||||
],
|
|
||||||
"modules": [
|
"modules": [
|
||||||
"server/providers/space.js",
|
"server/providers/space.js",
|
||||||
"server/providers/space/celestrak.js",
|
"server/providers/space/celestrak.js",
|
||||||
@@ -120,19 +86,13 @@
|
|||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
"space-requests": {
|
"space-requests": {
|
||||||
"exports": [
|
"exports": ["./sources/space"],
|
||||||
"./sources/space"
|
"modules": ["src/data/spaceProviderRequests.js"],
|
||||||
],
|
|
||||||
"modules": [
|
|
||||||
"src/data/spaceProviderRequests.js"
|
|
||||||
],
|
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
"terrain-provider": {
|
"terrain-provider": {
|
||||||
"runtime": "node",
|
"runtime": "node",
|
||||||
"exports": [
|
"exports": ["./server/providers/terrain"],
|
||||||
"./server/providers/terrain"
|
|
||||||
],
|
|
||||||
"modules": [
|
"modules": [
|
||||||
"server/providers/terrain.js",
|
"server/providers/terrain.js",
|
||||||
"src/data/terrainHeightsProxy.js"
|
"src/data/terrainHeightsProxy.js"
|
||||||
@@ -140,50 +100,30 @@
|
|||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
"terrain-source": {
|
"terrain-source": {
|
||||||
"exports": [
|
"exports": ["./sources/terrain"],
|
||||||
"./sources/terrain"
|
"modules": ["src/data/terrainHeightsProxy.js"],
|
||||||
],
|
|
||||||
"modules": [
|
|
||||||
"src/data/terrainHeightsProxy.js"
|
|
||||||
],
|
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
"traffic-provider": {
|
"traffic-provider": {
|
||||||
"runtime": "node",
|
"runtime": "node",
|
||||||
"exports": [
|
"exports": ["./server/providers/traffic"],
|
||||||
"./server/providers/traffic"
|
"modules": ["server/providers/traffic.js", "src/data/tomtomTiles.js"],
|
||||||
],
|
|
||||||
"modules": [
|
|
||||||
"server/providers/traffic.js",
|
|
||||||
"src/data/tomtomTiles.js"
|
|
||||||
],
|
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
"traffic-source": {
|
"traffic-source": {
|
||||||
"exports": [
|
"exports": ["./sources/traffic"],
|
||||||
"./sources/traffic"
|
"modules": ["src/data/tomtomTiles.js"],
|
||||||
],
|
|
||||||
"modules": [
|
|
||||||
"src/data/tomtomTiles.js"
|
|
||||||
],
|
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
"firms-provider": {
|
"firms-provider": {
|
||||||
"runtime": "node",
|
"runtime": "node",
|
||||||
"exports": [
|
"exports": ["./server/providers/firms"],
|
||||||
"./server/providers/firms"
|
"modules": ["server/providers/firms.js", "src/data/firmsCsv.js"],
|
||||||
],
|
|
||||||
"modules": [
|
|
||||||
"server/providers/firms.js",
|
|
||||||
"src/data/firmsCsv.js"
|
|
||||||
],
|
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
"gbfs-provider": {
|
"gbfs-provider": {
|
||||||
"runtime": "node",
|
"runtime": "node",
|
||||||
"exports": [
|
"exports": ["./server/providers/gbfs"],
|
||||||
"./server/providers/gbfs"
|
|
||||||
],
|
|
||||||
"modules": [
|
"modules": [
|
||||||
"server/providers/gbfs.js",
|
"server/providers/gbfs.js",
|
||||||
"server/providers/common/http.js",
|
"server/providers/common/http.js",
|
||||||
@@ -192,19 +132,13 @@
|
|||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
"gbfs-source": {
|
"gbfs-source": {
|
||||||
"exports": [
|
"exports": ["./sources/gbfs"],
|
||||||
"./sources/gbfs"
|
"modules": ["src/data/gbfsSource.js"],
|
||||||
],
|
|
||||||
"modules": [
|
|
||||||
"src/data/gbfsSource.js"
|
|
||||||
],
|
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
"cctv-provider": {
|
"cctv-provider": {
|
||||||
"runtime": "node",
|
"runtime": "node",
|
||||||
"exports": [
|
"exports": ["./server/providers/cctv"],
|
||||||
"./server/providers/cctv"
|
|
||||||
],
|
|
||||||
"modules": [
|
"modules": [
|
||||||
"server/providers/cctv.js",
|
"server/providers/cctv.js",
|
||||||
"server/providers/cctv/catalog.js",
|
"server/providers/cctv/catalog.js",
|
||||||
@@ -221,9 +155,7 @@
|
|||||||
},
|
},
|
||||||
"radio-provider": {
|
"radio-provider": {
|
||||||
"runtime": "node",
|
"runtime": "node",
|
||||||
"exports": [
|
"exports": ["./server/providers/radio"],
|
||||||
"./server/providers/radio"
|
|
||||||
],
|
|
||||||
"modules": [
|
"modules": [
|
||||||
"server/providers/radio.js",
|
"server/providers/radio.js",
|
||||||
"server/providers/radio/catalog.js",
|
"server/providers/radio/catalog.js",
|
||||||
@@ -237,9 +169,7 @@
|
|||||||
},
|
},
|
||||||
"overpass-provider": {
|
"overpass-provider": {
|
||||||
"runtime": "node",
|
"runtime": "node",
|
||||||
"exports": [
|
"exports": ["./server/providers/overpass"],
|
||||||
"./server/providers/overpass"
|
|
||||||
],
|
|
||||||
"modules": [
|
"modules": [
|
||||||
"server/providers/common/geo.js",
|
"server/providers/common/geo.js",
|
||||||
"server/providers/common/http.js",
|
"server/providers/common/http.js",
|
||||||
@@ -258,9 +188,7 @@
|
|||||||
},
|
},
|
||||||
"military-installations-provider": {
|
"military-installations-provider": {
|
||||||
"runtime": "node",
|
"runtime": "node",
|
||||||
"exports": [
|
"exports": ["./server/providers/military-installations"],
|
||||||
"./server/providers/military-installations"
|
|
||||||
],
|
|
||||||
"modules": [
|
"modules": [
|
||||||
"server/providers/common/http.js",
|
"server/providers/common/http.js",
|
||||||
"server/providers/common/query.js",
|
"server/providers/common/query.js",
|
||||||
@@ -277,9 +205,7 @@
|
|||||||
},
|
},
|
||||||
"regional-provider": {
|
"regional-provider": {
|
||||||
"runtime": "node",
|
"runtime": "node",
|
||||||
"exports": [
|
"exports": ["./server/providers/regional"],
|
||||||
"./server/providers/regional"
|
|
||||||
],
|
|
||||||
"modules": [
|
"modules": [
|
||||||
"server/providers/common/http.js",
|
"server/providers/common/http.js",
|
||||||
"server/providers/common/query.js",
|
"server/providers/common/query.js",
|
||||||
@@ -298,9 +224,7 @@
|
|||||||
},
|
},
|
||||||
"openai-provider": {
|
"openai-provider": {
|
||||||
"runtime": "node",
|
"runtime": "node",
|
||||||
"exports": [
|
"exports": ["./server/providers/openai"],
|
||||||
"./server/providers/openai"
|
|
||||||
],
|
|
||||||
"modules": [
|
"modules": [
|
||||||
"server/providers/common/rate-limit.js",
|
"server/providers/common/rate-limit.js",
|
||||||
"server/providers/common/request.js",
|
"server/providers/common/request.js",
|
||||||
@@ -320,9 +244,7 @@
|
|||||||
},
|
},
|
||||||
"standalone-key-setup": {
|
"standalone-key-setup": {
|
||||||
"runtime": "node",
|
"runtime": "node",
|
||||||
"exports": [
|
"exports": ["./server/standalone/key-setup"],
|
||||||
"./server/standalone/key-setup"
|
|
||||||
],
|
|
||||||
"modules": [
|
"modules": [
|
||||||
"scripts/pinokio-environment.mjs",
|
"scripts/pinokio-environment.mjs",
|
||||||
"server/providers/common/source-root.js",
|
"server/providers/common/source-root.js",
|
||||||
@@ -333,9 +255,7 @@
|
|||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
"place-search": {
|
"place-search": {
|
||||||
"exports": [
|
"exports": ["./search"],
|
||||||
"./search"
|
|
||||||
],
|
|
||||||
"modules": [
|
"modules": [
|
||||||
"src/search/index.js",
|
"src/search/index.js",
|
||||||
"src/search/placeSearch.js",
|
"src/search/placeSearch.js",
|
||||||
@@ -345,27 +265,17 @@
|
|||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
"panel-disclosure": {
|
"panel-disclosure": {
|
||||||
"exports": [
|
"exports": ["./ui/panels"],
|
||||||
"./ui/panels"
|
"modules": ["src/ui/panelDisclosure.js"],
|
||||||
],
|
|
||||||
"modules": [
|
|
||||||
"src/ui/panelDisclosure.js"
|
|
||||||
],
|
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
"surface-keyboard": {
|
"surface-keyboard": {
|
||||||
"exports": [
|
"exports": ["./ui/surfaces"],
|
||||||
"./ui/surfaces"
|
"modules": ["src/ui/surfaceKeyboard.js"],
|
||||||
],
|
|
||||||
"modules": [
|
|
||||||
"src/ui/surfaceKeyboard.js"
|
|
||||||
],
|
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
"panel-rail-layout": {
|
"panel-rail-layout": {
|
||||||
"exports": [
|
"exports": ["./ui/layout"],
|
||||||
"./ui/layout"
|
|
||||||
],
|
|
||||||
"modules": [
|
"modules": [
|
||||||
"src/ui/panelRails.js",
|
"src/ui/panelRails.js",
|
||||||
"src/ui/leftPanelRail.js",
|
"src/ui/leftPanelRail.js",
|
||||||
@@ -377,9 +287,7 @@
|
|||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
"visual-input": {
|
"visual-input": {
|
||||||
"exports": [
|
"exports": ["./ui/input"],
|
||||||
"./ui/input"
|
|
||||||
],
|
|
||||||
"modules": [
|
"modules": [
|
||||||
"src/ui/visualInput.js",
|
"src/ui/visualInput.js",
|
||||||
"src/ui/applicationShortcuts.js",
|
"src/ui/applicationShortcuts.js",
|
||||||
@@ -388,19 +296,12 @@
|
|||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
"display-controls": {
|
"display-controls": {
|
||||||
"exports": [
|
"exports": ["./ui/display"],
|
||||||
"./ui/display"
|
"modules": ["src/ui/displayControls.js"],
|
||||||
],
|
|
||||||
"modules": [
|
|
||||||
"src/ui/displayControls.js"
|
|
||||||
],
|
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
"visual-effects": {
|
"visual-effects": {
|
||||||
"exports": [
|
"exports": ["./ui/effects", "./ui/effects/bloom"],
|
||||||
"./ui/effects",
|
|
||||||
"./ui/effects/bloom"
|
|
||||||
],
|
|
||||||
"modules": [
|
"modules": [
|
||||||
"src/ui/effects.js",
|
"src/ui/effects.js",
|
||||||
"src/ui/visualEffects.js",
|
"src/ui/visualEffects.js",
|
||||||
@@ -413,14 +314,10 @@
|
|||||||
"src/styles/noir.js",
|
"src/styles/noir.js",
|
||||||
"src/styles/snow.js"
|
"src/styles/snow.js"
|
||||||
],
|
],
|
||||||
"external": [
|
"external": ["cesium"]
|
||||||
"cesium"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"map-source-controls": {
|
"map-source-controls": {
|
||||||
"exports": [
|
"exports": ["./ui/maps"],
|
||||||
"./ui/maps"
|
|
||||||
],
|
|
||||||
"modules": [
|
"modules": [
|
||||||
"src/ui/mapSource.js",
|
"src/ui/mapSource.js",
|
||||||
"src/ui/mapSourceControls.js",
|
"src/ui/mapSourceControls.js",
|
||||||
@@ -430,10 +327,7 @@
|
|||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
"layer-panel": {
|
"layer-panel": {
|
||||||
"exports": [
|
"exports": ["./ui/layers", "./ui/layers/feedback"],
|
||||||
"./ui/layers",
|
|
||||||
"./ui/layers/feedback"
|
|
||||||
],
|
|
||||||
"modules": [
|
"modules": [
|
||||||
"src/ui/layers.js",
|
"src/ui/layers.js",
|
||||||
"src/ui/layerPanel.js",
|
"src/ui/layerPanel.js",
|
||||||
@@ -444,9 +338,7 @@
|
|||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
"location-controls": {
|
"location-controls": {
|
||||||
"exports": [
|
"exports": ["./ui/location"],
|
||||||
"./ui/location"
|
|
||||||
],
|
|
||||||
"modules": [
|
"modules": [
|
||||||
"src/ui/location.js",
|
"src/ui/location.js",
|
||||||
"src/ui/locationControls.js",
|
"src/ui/locationControls.js",
|
||||||
@@ -456,9 +348,7 @@
|
|||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
"radio-controls": {
|
"radio-controls": {
|
||||||
"exports": [
|
"exports": ["./ui/radio"],
|
||||||
"./ui/radio"
|
|
||||||
],
|
|
||||||
"modules": [
|
"modules": [
|
||||||
"src/ui/radio.js",
|
"src/ui/radio.js",
|
||||||
"src/ui/radioControls.js",
|
"src/ui/radioControls.js",
|
||||||
|
|||||||
+6
-1
@@ -1,2 +1,7 @@
|
|||||||
export { RadioControls } from './radioControls.js';
|
export { RadioControls } from './radioControls.js';
|
||||||
export { radioTunerSlot, radioTunerCommitSlot, radioTunerPointerPosition, buildRadioTunerTicks } from './radioTunerModel.js';
|
export {
|
||||||
|
radioTunerSlot,
|
||||||
|
radioTunerCommitSlot,
|
||||||
|
radioTunerPointerPosition,
|
||||||
|
buildRadioTunerTicks,
|
||||||
|
} from './radioTunerModel.js';
|
||||||
+459
-139
@@ -1,4 +1,9 @@
|
|||||||
import { radioTunerSlot, radioTunerCommitSlot, radioTunerPointerPosition, buildRadioTunerTicks } from './radioTunerModel.js';
|
import {
|
||||||
|
radioTunerSlot,
|
||||||
|
radioTunerCommitSlot,
|
||||||
|
radioTunerPointerPosition,
|
||||||
|
buildRadioTunerTicks,
|
||||||
|
} from './radioTunerModel.js';
|
||||||
|
|
||||||
/** Bind Radio input using a RadioControls receiver and supplied actions. */
|
/** Bind Radio input using a RadioControls receiver and supplied actions. */
|
||||||
export function bindRadioControls() {
|
export function bindRadioControls() {
|
||||||
@@ -9,41 +14,66 @@ export function bindRadioControls() {
|
|||||||
this._contextRadioDock?.classList.toggle('disclosure-open', open);
|
this._contextRadioDock?.classList.toggle('disclosure-open', open);
|
||||||
if (this._contextRadioMini) this._contextRadioMini.hidden = !open;
|
if (this._contextRadioMini) this._contextRadioMini.hidden = !open;
|
||||||
this._syncContextRadioLauncherState();
|
this._syncContextRadioLauncherState();
|
||||||
if (!open && returnFocus) this._contextRadioToggleBtn?.focus({ preventScroll: true });
|
if (!open && returnFocus)
|
||||||
|
this._contextRadioToggleBtn?.focus({ preventScroll: true });
|
||||||
};
|
};
|
||||||
this._setRadioDisclosure = setRadioDisclosure;
|
this._setRadioDisclosure = setRadioDisclosure;
|
||||||
const setCockpitDisclosure = (kind, expanded, { returnFocus = false } = {}) => {
|
const setCockpitDisclosure = (
|
||||||
|
kind,
|
||||||
|
expanded,
|
||||||
|
{ returnFocus = false } = {},
|
||||||
|
) => {
|
||||||
const displayOpen = kind === 'display' && Boolean(expanded);
|
const displayOpen = kind === 'display' && Boolean(expanded);
|
||||||
const radioOpen = kind === 'radio' && Boolean(expanded);
|
const radioOpen = kind === 'radio' && Boolean(expanded);
|
||||||
if (displayOpen || radioOpen) this.actions.setSignalCollapsed(true);
|
if (displayOpen || radioOpen) this.actions.setSignalCollapsed(true);
|
||||||
if (this._cockpitDisplayPanel) this._cockpitDisplayPanel.hidden = !displayOpen;
|
if (this._cockpitDisplayPanel)
|
||||||
|
this._cockpitDisplayPanel.hidden = !displayOpen;
|
||||||
if (this._cockpitRadioPanel) this._cockpitRadioPanel.hidden = !radioOpen;
|
if (this._cockpitRadioPanel) this._cockpitRadioPanel.hidden = !radioOpen;
|
||||||
this._cockpitDisplayToggleBtn?.closest('.cockpit-utility-control')
|
this._cockpitDisplayToggleBtn
|
||||||
|
?.closest('.cockpit-utility-control')
|
||||||
?.classList.toggle('is-expanded', displayOpen);
|
?.classList.toggle('is-expanded', displayOpen);
|
||||||
this._cockpitRadioToggleBtn?.closest('.cockpit-utility-control')
|
this._cockpitRadioToggleBtn
|
||||||
|
?.closest('.cockpit-utility-control')
|
||||||
?.classList.toggle('is-expanded', radioOpen);
|
?.classList.toggle('is-expanded', radioOpen);
|
||||||
this._cockpitDisplayToggleBtn?.setAttribute('aria-expanded', String(displayOpen));
|
this._cockpitDisplayToggleBtn?.setAttribute(
|
||||||
this._cockpitRadioToggleBtn?.setAttribute('aria-expanded', String(radioOpen));
|
'aria-expanded',
|
||||||
|
String(displayOpen),
|
||||||
|
);
|
||||||
|
this._cockpitRadioToggleBtn?.setAttribute(
|
||||||
|
'aria-expanded',
|
||||||
|
String(radioOpen),
|
||||||
|
);
|
||||||
if (displayOpen) this.actions.revealStyleParameters();
|
if (displayOpen) this.actions.revealStyleParameters();
|
||||||
if (this._cockpitDisplayToggleBtn) {
|
if (this._cockpitDisplayToggleBtn) {
|
||||||
const action = displayOpen ? 'Collapse' : 'Expand';
|
const action = displayOpen ? 'Collapse' : 'Expand';
|
||||||
this._cockpitDisplayToggleBtn.textContent = displayOpen ? '▶' : '◀';
|
this._cockpitDisplayToggleBtn.textContent = displayOpen ? '▶' : '◀';
|
||||||
this._cockpitDisplayToggleBtn.setAttribute('aria-label', `${action} Cockpit display options`);
|
this._cockpitDisplayToggleBtn.setAttribute(
|
||||||
|
'aria-label',
|
||||||
|
`${action} Cockpit display options`,
|
||||||
|
);
|
||||||
this._cockpitDisplayToggleBtn.title = `${action} Cockpit display options`;
|
this._cockpitDisplayToggleBtn.title = `${action} Cockpit display options`;
|
||||||
}
|
}
|
||||||
if (this._cockpitRadioToggleBtn) {
|
if (this._cockpitRadioToggleBtn) {
|
||||||
const action = radioOpen ? 'Collapse' : 'Expand';
|
const action = radioOpen ? 'Collapse' : 'Expand';
|
||||||
this._cockpitRadioToggleBtn.textContent = radioOpen ? '▶' : '◀';
|
this._cockpitRadioToggleBtn.textContent = radioOpen ? '▶' : '◀';
|
||||||
this._cockpitRadioToggleBtn.setAttribute('aria-label', `${action} Cockpit Radio controls`);
|
this._cockpitRadioToggleBtn.setAttribute(
|
||||||
|
'aria-label',
|
||||||
|
`${action} Cockpit Radio controls`,
|
||||||
|
);
|
||||||
this._cockpitRadioToggleBtn.title = `${action} Cockpit Radio controls`;
|
this._cockpitRadioToggleBtn.title = `${action} Cockpit Radio controls`;
|
||||||
}
|
}
|
||||||
if (!expanded && returnFocus) {
|
if (!expanded && returnFocus) {
|
||||||
(kind === 'display' ? this._cockpitDisplayToggleBtn : this._cockpitRadioToggleBtn)
|
(kind === 'display'
|
||||||
?.focus({ preventScroll: true });
|
? this._cockpitDisplayToggleBtn
|
||||||
|
: this._cockpitRadioToggleBtn
|
||||||
|
)?.focus({ preventScroll: true });
|
||||||
}
|
}
|
||||||
if (!displayOpen && !radioOpen
|
if (
|
||||||
&& this.actions.isCockpitActive()
|
!displayOpen &&
|
||||||
&& !this.actions.signalUserCollapsed()) {
|
!radioOpen &&
|
||||||
|
this.actions.isCockpitActive() &&
|
||||||
|
!this.actions.signalUserCollapsed()
|
||||||
|
) {
|
||||||
this.actions.setSignalCollapsed(false);
|
this.actions.setSignalCollapsed(false);
|
||||||
}
|
}
|
||||||
this.actions.layoutCockpit();
|
this.actions.layoutCockpit();
|
||||||
@@ -63,7 +93,8 @@ export function bindRadioControls() {
|
|||||||
tick.className = 'radio-tuner-tick';
|
tick.className = 'radio-tuner-tick';
|
||||||
scale.append(tick);
|
scale.append(tick);
|
||||||
}
|
}
|
||||||
while (scale.children.length > model.ticks.length) scale.lastElementChild?.remove();
|
while (scale.children.length > model.ticks.length)
|
||||||
|
scale.lastElementChild?.remove();
|
||||||
model.ticks.forEach((entry, index) => {
|
model.ticks.forEach((entry, index) => {
|
||||||
const tick = scale.children[index];
|
const tick = scale.children[index];
|
||||||
tick.style.left = `${entry.xPx}px`;
|
tick.style.left = `${entry.xPx}px`;
|
||||||
@@ -74,54 +105,98 @@ export function bindRadioControls() {
|
|||||||
scale.style.setProperty('--radio-tuner-tick-pitch', `${model.pitchPx}px`);
|
scale.style.setProperty('--radio-tuner-tick-pitch', `${model.pitchPx}px`);
|
||||||
return model;
|
return model;
|
||||||
};
|
};
|
||||||
const tunerPreview = ({ coordinate = this._radioTunerCoordinate, syncStatic = true, rotate = true } = {}) => {
|
const tunerPreview = ({
|
||||||
const slot = radioTunerSlot(this._radioTunerSlider?.value, this._radioTunerStations.length);
|
coordinate = this._radioTunerCoordinate,
|
||||||
const station = slot.locked ? this._radioTunerStations[slot.stationIndex] || null : null;
|
syncStatic = true,
|
||||||
const resolvedCoordinate = this._radioTunerStations.length <= 1
|
rotate = true,
|
||||||
|
} = {}) => {
|
||||||
|
const slot = radioTunerSlot(
|
||||||
|
this._radioTunerSlider?.value,
|
||||||
|
this._radioTunerStations.length,
|
||||||
|
);
|
||||||
|
const station = slot.locked
|
||||||
|
? this._radioTunerStations[slot.stationIndex] || null
|
||||||
|
: null;
|
||||||
|
const resolvedCoordinate =
|
||||||
|
this._radioTunerStations.length <= 1
|
||||||
? 0
|
? 0
|
||||||
: Math.min(this._radioTunerStations.length - 1, Math.max(0, Number(coordinate) || 0));
|
: Math.min(
|
||||||
const ratio = this._radioTunerStations.length === 1
|
this._radioTunerStations.length - 1,
|
||||||
|
Math.max(0, Number(coordinate) || 0),
|
||||||
|
);
|
||||||
|
const ratio =
|
||||||
|
this._radioTunerStations.length === 1
|
||||||
? 0.5
|
? 0.5
|
||||||
: resolvedCoordinate / Math.max(1, this._radioTunerStations.length - 1);
|
: resolvedCoordinate / Math.max(1, this._radioTunerStations.length - 1);
|
||||||
this._radioTunerCoordinate = resolvedCoordinate;
|
this._radioTunerCoordinate = resolvedCoordinate;
|
||||||
this._radioTuner?.style.setProperty('--radio-tuner-ratio', String(ratio));
|
this._radioTuner?.style.setProperty('--radio-tuner-ratio', String(ratio));
|
||||||
this._radioTuner?.classList.toggle('is-static', syncStatic ? false : Boolean(this._radioState?.tuningStatic));
|
this._radioTuner?.classList.toggle(
|
||||||
|
'is-static',
|
||||||
|
syncStatic ? false : Boolean(this._radioState?.tuningStatic),
|
||||||
|
);
|
||||||
syncTunerTape(resolvedCoordinate);
|
syncTunerTape(resolvedCoordinate);
|
||||||
if (this._radioTunerValue) {
|
if (this._radioTunerValue) {
|
||||||
this._radioTunerValue.textContent = station
|
this._radioTunerValue.textContent = station
|
||||||
? `CH ${String(slot.stationIndex + 1).padStart(2, '0')} / ${String(this._radioTunerStations.length).padStart(2, '0')}`
|
? `CH ${String(slot.stationIndex + 1).padStart(2, '0')} / ${String(this._radioTunerStations.length).padStart(2, '0')}`
|
||||||
: 'NO STATIONS';
|
: 'NO STATIONS';
|
||||||
}
|
}
|
||||||
if (this._radioTunerStation) this._radioTunerStation.textContent = station?.name || 'NO STATION AVAILABLE';
|
if (this._radioTunerStation)
|
||||||
|
this._radioTunerStation.textContent =
|
||||||
|
station?.name || 'NO STATION AVAILABLE';
|
||||||
if (this._radioTunerSlider) {
|
if (this._radioTunerSlider) {
|
||||||
this._radioTunerSlider.setAttribute('aria-valuetext', station
|
this._radioTunerSlider.setAttribute(
|
||||||
|
'aria-valuetext',
|
||||||
|
station
|
||||||
? `${station.name}, station ${slot.stationIndex + 1} of ${this._radioTunerStations.length}`
|
? `${station.name}, station ${slot.stationIndex + 1} of ${this._radioTunerStations.length}`
|
||||||
: 'No station available');
|
: 'No station available',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (syncStatic) this.radio.previewTuningStation(station?.id || null, { rotate });
|
if (syncStatic)
|
||||||
|
this.radio.previewTuningStation(station?.id || null, { rotate });
|
||||||
return station;
|
return station;
|
||||||
};
|
};
|
||||||
const setTunerDirectory = (pool) => {
|
const setTunerDirectory = (pool) => {
|
||||||
this._radioTunerPool = [...pool];
|
this._radioTunerPool = [...pool];
|
||||||
this._radioTunerStations = [...pool];
|
this._radioTunerStations = [...pool];
|
||||||
this._radioTunerBandSignature = this._radioTunerStations.map((station) => station.id).join('|');
|
this._radioTunerBandSignature = this._radioTunerStations
|
||||||
|
.map((station) => station.id)
|
||||||
|
.join('|');
|
||||||
if (this._radioTunerSlider) {
|
if (this._radioTunerSlider) {
|
||||||
this._radioTunerSlider.min = '0';
|
this._radioTunerSlider.min = '0';
|
||||||
this._radioTunerSlider.max = String(Math.max(0, this._radioTunerStations.length - 1));
|
this._radioTunerSlider.max = String(
|
||||||
|
Math.max(0, this._radioTunerStations.length - 1),
|
||||||
|
);
|
||||||
this._radioTunerSlider.step = '1';
|
this._radioTunerSlider.step = '1';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const refreshTunerBand = ({ force = false } = {}) => {
|
const refreshTunerBand = ({ force = false } = {}) => {
|
||||||
if (this._radioTunerDragging || this._radioTuner?.hidden || this._radioTunerSlider?.disabled) return false;
|
if (
|
||||||
|
this._radioTunerDragging ||
|
||||||
|
this._radioTuner?.hidden ||
|
||||||
|
this._radioTunerSlider?.disabled
|
||||||
|
)
|
||||||
|
return false;
|
||||||
const selectedId = this._radioState?.selected?.id || null;
|
const selectedId = this._radioState?.selected?.id || null;
|
||||||
const pool = this.radio.getTunerStations(750);
|
const pool = this.radio.getTunerStations(750);
|
||||||
const poolSignature = pool.map((station) => station.id).join('|');
|
const poolSignature = pool.map((station) => station.id).join('|');
|
||||||
const currentPoolSignature = this._radioTunerPool.map((station) => station.id).join('|');
|
const currentPoolSignature = this._radioTunerPool
|
||||||
if (!force && poolSignature === currentPoolSignature && selectedId === this._radioTunerSelectedId) return false;
|
.map((station) => station.id)
|
||||||
|
.join('|');
|
||||||
|
if (
|
||||||
|
!force &&
|
||||||
|
poolSignature === currentPoolSignature &&
|
||||||
|
selectedId === this._radioTunerSelectedId
|
||||||
|
)
|
||||||
|
return false;
|
||||||
setTunerDirectory(pool);
|
setTunerDirectory(pool);
|
||||||
this._radioTunerSelectedId = selectedId;
|
this._radioTunerSelectedId = selectedId;
|
||||||
const selectedPoolIndex = pool.findIndex((station) => station.id === selectedId);
|
const selectedPoolIndex = pool.findIndex(
|
||||||
const slot = radioTunerSlot(selectedPoolIndex >= 0 ? selectedPoolIndex : 0, this._radioTunerStations.length);
|
(station) => station.id === selectedId,
|
||||||
|
);
|
||||||
|
const slot = radioTunerSlot(
|
||||||
|
selectedPoolIndex >= 0 ? selectedPoolIndex : 0,
|
||||||
|
this._radioTunerStations.length,
|
||||||
|
);
|
||||||
this._radioTunerSlider.value = String(slot.slot);
|
this._radioTunerSlider.value = String(slot.slot);
|
||||||
this._radioTunerCoordinate = slot.stationIndex >= 0 ? slot.stationIndex : 0;
|
this._radioTunerCoordinate = slot.stationIndex >= 0 ? slot.stationIndex : 0;
|
||||||
tunerPreview({ coordinate: this._radioTunerCoordinate, syncStatic: false });
|
tunerPreview({ coordinate: this._radioTunerCoordinate, syncStatic: false });
|
||||||
@@ -129,16 +204,23 @@ export function bindRadioControls() {
|
|||||||
};
|
};
|
||||||
this._refreshRadioTunerBand = refreshTunerBand;
|
this._refreshRadioTunerBand = refreshTunerBand;
|
||||||
const beginTuner = () => {
|
const beginTuner = () => {
|
||||||
if (this._radioTunerDragging || this._radioTunerSlider?.disabled) return false;
|
if (this._radioTunerDragging || this._radioTunerSlider?.disabled)
|
||||||
|
return false;
|
||||||
refreshTunerBand();
|
refreshTunerBand();
|
||||||
if (!this._radioTunerStations.length || !this.radio.beginTuning()) return false;
|
if (!this._radioTunerStations.length || !this.radio.beginTuning())
|
||||||
|
return false;
|
||||||
// A tuner-owned camera preview must never replace the frozen directory.
|
// A tuner-owned camera preview must never replace the frozen directory.
|
||||||
// Only an explicit globe pointer/wheel gesture releases camera pinning.
|
// Only an explicit globe pointer/wheel gesture releases camera pinning.
|
||||||
this._radioTunerBandPinnedForNavigation = true;
|
this._radioTunerBandPinnedForNavigation = true;
|
||||||
this._radioTunerDragging = true;
|
this._radioTunerDragging = true;
|
||||||
this._radioTuner?.classList.add('is-dragging');
|
this._radioTuner?.classList.add('is-dragging');
|
||||||
const selectedIndex = this._radioTunerStations.findIndex((station) => station.id === this._radioState?.selected?.id);
|
const selectedIndex = this._radioTunerStations.findIndex(
|
||||||
const slot = radioTunerSlot(selectedIndex >= 0 ? selectedIndex : 0, this._radioTunerStations.length);
|
(station) => station.id === this._radioState?.selected?.id,
|
||||||
|
);
|
||||||
|
const slot = radioTunerSlot(
|
||||||
|
selectedIndex >= 0 ? selectedIndex : 0,
|
||||||
|
this._radioTunerStations.length,
|
||||||
|
);
|
||||||
this._radioTunerSlider.value = String(slot.slot);
|
this._radioTunerSlider.value = String(slot.slot);
|
||||||
this._radioTunerCoordinate = slot.stationIndex >= 0 ? slot.stationIndex : 0;
|
this._radioTunerCoordinate = slot.stationIndex >= 0 ? slot.stationIndex : 0;
|
||||||
this._radioTunerDragStartSlot = slot.slot;
|
this._radioTunerDragStartSlot = slot.slot;
|
||||||
@@ -163,7 +245,10 @@ export function bindRadioControls() {
|
|||||||
this._radioTunerBandSignature = dragSnapshot.bandSignature;
|
this._radioTunerBandSignature = dragSnapshot.bandSignature;
|
||||||
this._radioTunerSelectedId = dragSnapshot.selectedId;
|
this._radioTunerSelectedId = dragSnapshot.selectedId;
|
||||||
if (this._radioTunerSlider) {
|
if (this._radioTunerSlider) {
|
||||||
const startSlot = radioTunerSlot(dragSnapshot.slot, this._radioTunerStations.length);
|
const startSlot = radioTunerSlot(
|
||||||
|
dragSnapshot.slot,
|
||||||
|
this._radioTunerStations.length,
|
||||||
|
);
|
||||||
this._radioTunerSlider.max = String(startSlot.max);
|
this._radioTunerSlider.max = String(startSlot.max);
|
||||||
this._radioTunerSlider.value = String(startSlot.slot);
|
this._radioTunerSlider.value = String(startSlot.slot);
|
||||||
}
|
}
|
||||||
@@ -174,7 +259,10 @@ export function bindRadioControls() {
|
|||||||
this._radioTunerSlider.value = String(this._radioTunerDragStartSlot);
|
this._radioTunerSlider.value = String(this._radioTunerDragStartSlot);
|
||||||
this._radioTunerCoordinate = this._radioTunerDragStartSlot;
|
this._radioTunerCoordinate = this._radioTunerDragStartSlot;
|
||||||
}
|
}
|
||||||
let station = tunerPreview({ coordinate: this._radioTunerCoordinate, rotate: commit });
|
let station = tunerPreview({
|
||||||
|
coordinate: this._radioTunerCoordinate,
|
||||||
|
rotate: commit,
|
||||||
|
});
|
||||||
if (commit && !station && this._radioTunerSlider) {
|
if (commit && !station && this._radioTunerSlider) {
|
||||||
const snapped = radioTunerCommitSlot(
|
const snapped = radioTunerCommitSlot(
|
||||||
this._radioTunerSlider.value,
|
this._radioTunerSlider.value,
|
||||||
@@ -210,8 +298,10 @@ export function bindRadioControls() {
|
|||||||
if (result && !result.ok) {
|
if (result && !result.ok) {
|
||||||
this._radioTunerBandPinnedForNavigation = false;
|
this._radioTunerBandPinnedForNavigation = false;
|
||||||
if (result.reason === 'station-unavailable') {
|
if (result.reason === 'station-unavailable') {
|
||||||
if (this._radioTunerValue) this._radioTunerValue.textContent = 'OFF AIR';
|
if (this._radioTunerValue)
|
||||||
if (this._radioTunerStation) this._radioTunerStation.textContent = 'STATION UNAVAILABLE';
|
this._radioTunerValue.textContent = 'OFF AIR';
|
||||||
|
if (this._radioTunerStation)
|
||||||
|
this._radioTunerStation.textContent = 'STATION UNAVAILABLE';
|
||||||
this._radioTunerSlider?.setAttribute(
|
this._radioTunerSlider?.setAttribute(
|
||||||
'aria-valuetext',
|
'aria-valuetext',
|
||||||
'Station unavailable after directory refresh',
|
'Station unavailable after directory refresh',
|
||||||
@@ -221,7 +311,9 @@ export function bindRadioControls() {
|
|||||||
};
|
};
|
||||||
const cycleRadio = (direction, { rotate = true } = {}) => {
|
const cycleRadio = (direction, { rotate = true } = {}) => {
|
||||||
this._radioTunerBandPinnedForNavigation = true;
|
this._radioTunerBandPinnedForNavigation = true;
|
||||||
const pool = this._radioTunerPool.length ? this._radioTunerPool : this._radioTunerStations;
|
const pool = this._radioTunerPool.length
|
||||||
|
? this._radioTunerPool
|
||||||
|
: this._radioTunerStations;
|
||||||
const cycled = this.radio.cycleStation(direction, {
|
const cycled = this.radio.cycleStation(direction, {
|
||||||
rotate,
|
rotate,
|
||||||
stationIds: pool.map((station) => station.id),
|
stationIds: pool.map((station) => station.id),
|
||||||
@@ -241,105 +333,190 @@ export function bindRadioControls() {
|
|||||||
trigger.setAttribute('aria-busy', 'true');
|
trigger.setAttribute('aria-busy', 'true');
|
||||||
try {
|
try {
|
||||||
const toggled = await this.actions.runUserAction(
|
const toggled = await this.actions.runUserAction(
|
||||||
(notificationToken) => this.actions.setEnabled(enabling, {
|
(notificationToken) =>
|
||||||
|
this.actions.setEnabled(enabling, {
|
||||||
origin: 'user',
|
origin: 'user',
|
||||||
notificationToken,
|
notificationToken,
|
||||||
}),
|
}),
|
||||||
`Radio could not ${enabling ? 'start' : 'stop'} cleanly`,
|
`Radio could not ${enabling ? 'start' : 'stop'} cleanly`,
|
||||||
);
|
);
|
||||||
if (this.destroyed || toggled === false) return;
|
if (this.destroyed || toggled === false) return;
|
||||||
if (enabling && trigger === this._radioEnableBtn
|
if (
|
||||||
&& !document.getElementById('global-context-panel')?.classList.contains('collapsed')) {
|
enabling &&
|
||||||
this.actions.setPanelCollapsed('radio-panel', false, { explicit: true });
|
trigger === this._radioEnableBtn &&
|
||||||
|
!document
|
||||||
|
.getElementById('global-context-panel')
|
||||||
|
?.classList.contains('collapsed')
|
||||||
|
) {
|
||||||
|
this.actions.setPanelCollapsed('radio-panel', false, {
|
||||||
|
explicit: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (revealAfterEnable) await this._revealRadioControlsAfterExplicitEnable(trigger);
|
if (revealAfterEnable)
|
||||||
|
await this._revealRadioControlsAfterExplicitEnable(trigger);
|
||||||
} finally {
|
} finally {
|
||||||
if (!this.destroyed) {
|
if (!this.destroyed) {
|
||||||
trigger.setAttribute('aria-disabled', 'false');
|
trigger.setAttribute('aria-disabled', 'false');
|
||||||
trigger.setAttribute('aria-busy', 'false');
|
trigger.setAttribute('aria-busy', 'false');
|
||||||
if (revealAfterEnable && trigger.isConnected) trigger.focus({ preventScroll: true });
|
if (revealAfterEnable && trigger.isConnected)
|
||||||
|
trigger.focus({ preventScroll: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.listen(this._radioEnableBtn, 'click', () => void toggleRadio(this._radioEnableBtn));
|
this.listen(
|
||||||
this.listen(this._contextRadioMiniEnableBtn, 'click', () => void toggleRadio(this._contextRadioMiniEnableBtn));
|
this._radioEnableBtn,
|
||||||
this.listen(this._cockpitRadioEnableBtn, 'click', () => void toggleRadio(this._cockpitRadioEnableBtn));
|
'click',
|
||||||
|
() => void toggleRadio(this._radioEnableBtn),
|
||||||
|
);
|
||||||
|
this.listen(
|
||||||
|
this._contextRadioMiniEnableBtn,
|
||||||
|
'click',
|
||||||
|
() => void toggleRadio(this._contextRadioMiniEnableBtn),
|
||||||
|
);
|
||||||
|
this.listen(
|
||||||
|
this._cockpitRadioEnableBtn,
|
||||||
|
'click',
|
||||||
|
() => void toggleRadio(this._cockpitRadioEnableBtn),
|
||||||
|
);
|
||||||
this.listen(this._contextRadioToggleBtn, 'click', () => {
|
this.listen(this._contextRadioToggleBtn, 'click', () => {
|
||||||
const contextPanel = document.getElementById('global-context-panel');
|
const contextPanel = document.getElementById('global-context-panel');
|
||||||
if (contextPanel && !contextPanel.classList.contains('collapsed')) {
|
if (contextPanel && !contextPanel.classList.contains('collapsed')) {
|
||||||
setRadioDisclosure(false);
|
setRadioDisclosure(false);
|
||||||
this.actions.setPanelCollapsed('radio-panel', false, { explicit: true });
|
this.actions.setPanelCollapsed('radio-panel', false, { explicit: true });
|
||||||
void this._revealRadioPanelInsideContext({
|
void this._revealRadioPanelInsideContext({
|
||||||
focusTarget: this._radioPanel?.querySelector('[data-collapse-target="radio-panel"]'),
|
focusTarget: this._radioPanel?.querySelector(
|
||||||
|
'[data-collapse-target="radio-panel"]',
|
||||||
|
),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setRadioDisclosure(!this._contextRadioDock?.classList.contains('disclosure-open'));
|
setRadioDisclosure(
|
||||||
|
!this._contextRadioDock?.classList.contains('disclosure-open'),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
this.listen(this._contextRadioMiniCloseBtn, 'click', () => {
|
this.listen(this._contextRadioMiniCloseBtn, 'click', () => {
|
||||||
setRadioDisclosure(false, { returnFocus: true });
|
setRadioDisclosure(false, { returnFocus: true });
|
||||||
});
|
});
|
||||||
this.listen(this._contextRadioDetailsBtn, 'click', () => {
|
this.listen(this._contextRadioDetailsBtn, 'click', () => {
|
||||||
if (!this.actions.isCockpitActive()) this.actions.setPanelCollapsed('global-context-panel', false, { explicit: true });
|
if (!this.actions.isCockpitActive())
|
||||||
|
this.actions.setPanelCollapsed('global-context-panel', false, {
|
||||||
|
explicit: true,
|
||||||
|
});
|
||||||
this.actions.setPanelCollapsed('radio-panel', false, { explicit: true });
|
this.actions.setPanelCollapsed('radio-panel', false, { explicit: true });
|
||||||
setRadioDisclosure(false);
|
setRadioDisclosure(false);
|
||||||
this._radioEnableBtn?.focus({ preventScroll: true });
|
this._radioEnableBtn?.focus({ preventScroll: true });
|
||||||
});
|
});
|
||||||
this.listen(this._cockpitRadioToggleBtn, 'click', () => {
|
this.listen(this._cockpitRadioToggleBtn, 'click', () => {
|
||||||
const open = this._cockpitRadioToggleBtn.getAttribute('aria-expanded') === 'true';
|
const open =
|
||||||
|
this._cockpitRadioToggleBtn.getAttribute('aria-expanded') === 'true';
|
||||||
setCockpitDisclosure('radio', !open);
|
setCockpitDisclosure('radio', !open);
|
||||||
});
|
});
|
||||||
this.listen(document, 'pointerdown', (event) => {
|
this.listen(
|
||||||
if (!this._contextRadioDock?.classList.contains('disclosure-open')) return;
|
document,
|
||||||
|
'pointerdown',
|
||||||
|
(event) => {
|
||||||
|
if (!this._contextRadioDock?.classList.contains('disclosure-open'))
|
||||||
|
return;
|
||||||
if (event.target?.closest?.('#context-radio-dock')) return;
|
if (event.target?.closest?.('#context-radio-dock')) return;
|
||||||
setRadioDisclosure(false);
|
setRadioDisclosure(false);
|
||||||
}, tunerListenerOptions);
|
},
|
||||||
this.listen(document, 'pointerdown', (event) => {
|
tunerListenerOptions,
|
||||||
if (!this._cockpitUtilityControls || event.target?.closest?.('#cockpit-utility-controls')) return;
|
);
|
||||||
|
this.listen(
|
||||||
|
document,
|
||||||
|
'pointerdown',
|
||||||
|
(event) => {
|
||||||
|
if (
|
||||||
|
!this._cockpitUtilityControls ||
|
||||||
|
event.target?.closest?.('#cockpit-utility-controls')
|
||||||
|
)
|
||||||
|
return;
|
||||||
if (event.target?.closest?.('.cockpit-vision-controls')) return;
|
if (event.target?.closest?.('.cockpit-vision-controls')) return;
|
||||||
if (event.target?.closest?.('#left-panel-stack, #cockpit-context')) return;
|
if (event.target?.closest?.('#left-panel-stack, #cockpit-context'))
|
||||||
|
return;
|
||||||
setCockpitDisclosure('display', false);
|
setCockpitDisclosure('display', false);
|
||||||
setCockpitDisclosure('radio', false);
|
setCockpitDisclosure('radio', false);
|
||||||
}, tunerListenerOptions);
|
},
|
||||||
this.listen(document, 'keydown', (event) => {
|
tunerListenerOptions,
|
||||||
if (event.key !== 'Escape' || !this._contextRadioDock?.classList.contains('disclosure-open')) return;
|
);
|
||||||
|
this.listen(
|
||||||
|
document,
|
||||||
|
'keydown',
|
||||||
|
(event) => {
|
||||||
|
if (
|
||||||
|
event.key !== 'Escape' ||
|
||||||
|
!this._contextRadioDock?.classList.contains('disclosure-open')
|
||||||
|
)
|
||||||
|
return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
// Immediate: a plain stopPropagation() still lets every LATER listener on
|
// Immediate: a plain stopPropagation() still lets every LATER listener on
|
||||||
// this same document run, so closing the disclosure ALSO dismissed the
|
// this same document run, so closing the disclosure ALSO dismissed the
|
||||||
// first-run launcher — one key, two actions. Matches the cockpit
|
// first-run launcher — one key, two actions. Matches the cockpit
|
||||||
// disclosure handler directly below.
|
// disclosure handler directly below.
|
||||||
event.stopImmediatePropagation();
|
event.stopImmediatePropagation();
|
||||||
const escapedFromDisclosure = event.target === this._contextRadioToggleBtn
|
const escapedFromDisclosure =
|
||||||
|| this._contextRadioToggleBtn?.contains?.(event.target);
|
event.target === this._contextRadioToggleBtn ||
|
||||||
|
this._contextRadioToggleBtn?.contains?.(event.target);
|
||||||
setRadioDisclosure(false, { returnFocus: !escapedFromDisclosure });
|
setRadioDisclosure(false, { returnFocus: !escapedFromDisclosure });
|
||||||
if (escapedFromDisclosure) this._contextRadioToggleBtn?.blur?.();
|
if (escapedFromDisclosure) this._contextRadioToggleBtn?.blur?.();
|
||||||
}, { capture: true, });
|
},
|
||||||
this.listen(document, 'keydown', (event) => {
|
{ capture: true },
|
||||||
|
);
|
||||||
|
this.listen(
|
||||||
|
document,
|
||||||
|
'keydown',
|
||||||
|
(event) => {
|
||||||
if (event.key !== 'Escape') return;
|
if (event.key !== 'Escape') return;
|
||||||
const displayOpen = this._cockpitDisplayToggleBtn?.getAttribute('aria-expanded') === 'true';
|
const displayOpen =
|
||||||
const radioOpen = this._cockpitRadioToggleBtn?.getAttribute('aria-expanded') === 'true';
|
this._cockpitDisplayToggleBtn?.getAttribute('aria-expanded') === 'true';
|
||||||
|
const radioOpen =
|
||||||
|
this._cockpitRadioToggleBtn?.getAttribute('aria-expanded') === 'true';
|
||||||
if (!displayOpen && !radioOpen) return;
|
if (!displayOpen && !radioOpen) return;
|
||||||
const nestedPanel = event.target?.closest?.('.panel-collapsible:not(.collapsed), #param-slider-panel:not(.collapsed)');
|
const nestedPanel = event.target?.closest?.(
|
||||||
|
'.panel-collapsible:not(.collapsed), #param-slider-panel:not(.collapsed)',
|
||||||
|
);
|
||||||
if (nestedPanel) return;
|
if (nestedPanel) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopImmediatePropagation();
|
event.stopImmediatePropagation();
|
||||||
const kind = displayOpen ? 'display' : 'radio';
|
const kind = displayOpen ? 'display' : 'radio';
|
||||||
const disclosure = displayOpen ? this._cockpitDisplayToggleBtn : this._cockpitRadioToggleBtn;
|
const disclosure = displayOpen
|
||||||
const escapedFromDisclosure = event.target === disclosure || disclosure?.contains?.(event.target);
|
? this._cockpitDisplayToggleBtn
|
||||||
setCockpitDisclosure(kind, false, { returnFocus: !escapedFromDisclosure });
|
: this._cockpitRadioToggleBtn;
|
||||||
|
const escapedFromDisclosure =
|
||||||
|
event.target === disclosure || disclosure?.contains?.(event.target);
|
||||||
|
setCockpitDisclosure(kind, false, {
|
||||||
|
returnFocus: !escapedFromDisclosure,
|
||||||
|
});
|
||||||
if (escapedFromDisclosure) disclosure?.blur?.();
|
if (escapedFromDisclosure) disclosure?.blur?.();
|
||||||
}, { capture: true, });
|
},
|
||||||
this.listen(window, 'gev:cockpit-mode-changed', (event) => {
|
{ capture: true },
|
||||||
|
);
|
||||||
|
this.listen(
|
||||||
|
window,
|
||||||
|
'gev:cockpit-mode-changed',
|
||||||
|
(event) => {
|
||||||
if (event?.detail?.active) return;
|
if (event?.detail?.active) return;
|
||||||
setCockpitDisclosure('display', false);
|
setCockpitDisclosure('display', false);
|
||||||
setCockpitDisclosure('radio', false);
|
setCockpitDisclosure('radio', false);
|
||||||
}, tunerListenerOptions);
|
},
|
||||||
this.listen(window, 'gev:cockpit-signal-expanded', () => {
|
tunerListenerOptions,
|
||||||
|
);
|
||||||
|
this.listen(
|
||||||
|
window,
|
||||||
|
'gev:cockpit-signal-expanded',
|
||||||
|
() => {
|
||||||
setCockpitDisclosure('display', false);
|
setCockpitDisclosure('display', false);
|
||||||
}, tunerListenerOptions);
|
},
|
||||||
this.listen(window, 'gev:cockpit-context-expanded', () => {
|
tunerListenerOptions,
|
||||||
|
);
|
||||||
|
this.listen(
|
||||||
|
window,
|
||||||
|
'gev:cockpit-context-expanded',
|
||||||
|
() => {
|
||||||
this.actions.setPanelCollapsed('data-panel', true);
|
this.actions.setPanelCollapsed('data-panel', true);
|
||||||
}, tunerListenerOptions);
|
},
|
||||||
|
tunerListenerOptions,
|
||||||
|
);
|
||||||
this.listen(this._radioFilter, 'change', () => {
|
this.listen(this._radioFilter, 'change', () => {
|
||||||
const presentation = this.radio.getUIState();
|
const presentation = this.radio.getUIState();
|
||||||
if (!presentation.presentationActive) {
|
if (!presentation.presentationActive) {
|
||||||
@@ -347,9 +524,14 @@ export function bindRadioControls() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this._radioTunerDragging) finishTuner(false);
|
if (this._radioTunerDragging) finishTuner(false);
|
||||||
if (!this.actions.setParams({
|
if (
|
||||||
|
!this.actions.setParams(
|
||||||
|
{
|
||||||
filter: this._radioFilter.value,
|
filter: this._radioFilter.value,
|
||||||
}, { origin: 'user' })) {
|
},
|
||||||
|
{ origin: 'user' },
|
||||||
|
)
|
||||||
|
) {
|
||||||
this._radioFilter.value = this.radio.getUIState().filter;
|
this._radioFilter.value = this.radio.getUIState().filter;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -359,30 +541,51 @@ export function bindRadioControls() {
|
|||||||
});
|
});
|
||||||
this.listen(this._radioPrevBtn, 'click', () => cycleRadio(-1));
|
this.listen(this._radioPrevBtn, 'click', () => cycleRadio(-1));
|
||||||
this.listen(this._radioNextBtn, 'click', () => cycleRadio(1));
|
this.listen(this._radioNextBtn, 'click', () => cycleRadio(1));
|
||||||
this.listen(this._radioPlayBtn, 'click', () => void this.radio.togglePlayback({ origin: 'user' }));
|
this.listen(
|
||||||
this.listen(this._radioStopBtn, 'click', () => this.radio.stopPlayback({ origin: 'user' }));
|
this._radioPlayBtn,
|
||||||
|
'click',
|
||||||
|
() => void this.radio.togglePlayback({ origin: 'user' }),
|
||||||
|
);
|
||||||
|
this.listen(this._radioStopBtn, 'click', () =>
|
||||||
|
this.radio.stopPlayback({ origin: 'user' }),
|
||||||
|
);
|
||||||
this.listen(this._radioVolume, 'input', () => {
|
this.listen(this._radioVolume, 'input', () => {
|
||||||
const value = Number(this._radioVolume.value);
|
const value = Number(this._radioVolume.value);
|
||||||
if (this._radioVolumeValue) this._radioVolumeValue.textContent = `${value}%`;
|
if (this._radioVolumeValue)
|
||||||
|
this._radioVolumeValue.textContent = `${value}%`;
|
||||||
this.actions.setParams({ volume: value / 100 }, { origin: 'user' });
|
this.actions.setParams({ volume: value / 100 }, { origin: 'user' });
|
||||||
});
|
});
|
||||||
this.listen(this._contextRadioMiniPrevBtn, 'click', () => cycleRadio(-1));
|
this.listen(this._contextRadioMiniPrevBtn, 'click', () => cycleRadio(-1));
|
||||||
this.listen(this._contextRadioMiniNextBtn, 'click', () => cycleRadio(1));
|
this.listen(this._contextRadioMiniNextBtn, 'click', () => cycleRadio(1));
|
||||||
this.listen(this._contextRadioMiniPlayBtn, 'click', () => void this.radio.togglePlayback({ origin: 'user' }));
|
this.listen(
|
||||||
|
this._contextRadioMiniPlayBtn,
|
||||||
|
'click',
|
||||||
|
() => void this.radio.togglePlayback({ origin: 'user' }),
|
||||||
|
);
|
||||||
// Cockpit owns the Cesium camera even though it intentionally clears
|
// Cockpit owns the Cesium camera even though it intentionally clears
|
||||||
// viewer.trackedEntity. Station changes must never start the map-view
|
// viewer.trackedEntity. Station changes must never start the map-view
|
||||||
// rotation/fallback flights that would compete with its preUpdate pose.
|
// rotation/fallback flights that would compete with its preUpdate pose.
|
||||||
this.listen(this._cockpitRadioPrevBtn, 'click', () => cycleRadio(-1, { rotate: false }));
|
this.listen(this._cockpitRadioPrevBtn, 'click', () =>
|
||||||
this.listen(this._cockpitRadioNextBtn, 'click', () => cycleRadio(1, { rotate: false }));
|
cycleRadio(-1, { rotate: false }),
|
||||||
this.listen(this._cockpitRadioPlayBtn, 'click', () => void this.radio.togglePlayback({ origin: 'user' }));
|
);
|
||||||
|
this.listen(this._cockpitRadioNextBtn, 'click', () =>
|
||||||
|
cycleRadio(1, { rotate: false }),
|
||||||
|
);
|
||||||
|
this.listen(
|
||||||
|
this._cockpitRadioPlayBtn,
|
||||||
|
'click',
|
||||||
|
() => void this.radio.togglePlayback({ origin: 'user' }),
|
||||||
|
);
|
||||||
this.listen(this._contextRadioMiniVolume, 'input', () => {
|
this.listen(this._contextRadioMiniVolume, 'input', () => {
|
||||||
const value = Number(this._contextRadioMiniVolume.value);
|
const value = Number(this._contextRadioMiniVolume.value);
|
||||||
if (this._contextRadioMiniVolumeValue) this._contextRadioMiniVolumeValue.textContent = `${value}%`;
|
if (this._contextRadioMiniVolumeValue)
|
||||||
|
this._contextRadioMiniVolumeValue.textContent = `${value}%`;
|
||||||
this.actions.setParams({ volume: value / 100 }, { origin: 'user' });
|
this.actions.setParams({ volume: value / 100 }, { origin: 'user' });
|
||||||
});
|
});
|
||||||
this.listen(this._cockpitRadioVolume, 'input', () => {
|
this.listen(this._cockpitRadioVolume, 'input', () => {
|
||||||
const value = Number(this._cockpitRadioVolume.value);
|
const value = Number(this._cockpitRadioVolume.value);
|
||||||
if (this._cockpitRadioVolumeValue) this._cockpitRadioVolumeValue.textContent = `${value}%`;
|
if (this._cockpitRadioVolumeValue)
|
||||||
|
this._cockpitRadioVolumeValue.textContent = `${value}%`;
|
||||||
this.actions.setParams({ volume: value / 100 }, { origin: 'user' });
|
this.actions.setParams({ volume: value / 100 }, { origin: 'user' });
|
||||||
});
|
});
|
||||||
const updateTunerFromPointer = (event) => {
|
const updateTunerFromPointer = (event) => {
|
||||||
@@ -394,95 +597,212 @@ export function bindRadioControls() {
|
|||||||
rect.width,
|
rect.width,
|
||||||
this._radioTunerStations.length,
|
this._radioTunerStations.length,
|
||||||
);
|
);
|
||||||
if (position.stationIndex > this._radioTunerLastSlot) this._radioTunerDragDirection = 1;
|
if (position.stationIndex > this._radioTunerLastSlot)
|
||||||
else if (position.stationIndex < this._radioTunerLastSlot) this._radioTunerDragDirection = -1;
|
this._radioTunerDragDirection = 1;
|
||||||
|
else if (position.stationIndex < this._radioTunerLastSlot)
|
||||||
|
this._radioTunerDragDirection = -1;
|
||||||
this._radioTunerLastSlot = position.stationIndex;
|
this._radioTunerLastSlot = position.stationIndex;
|
||||||
this._radioTunerCoordinate = position.coordinate;
|
this._radioTunerCoordinate = position.coordinate;
|
||||||
this._radioTunerSlider.value = String(position.stationIndex);
|
this._radioTunerSlider.value = String(position.stationIndex);
|
||||||
tunerPreview({ coordinate: position.coordinate });
|
tunerPreview({ coordinate: position.coordinate });
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
this.listen(this._radioTunerSlider, 'pointerdown', (event) => {
|
this.listen(
|
||||||
|
this._radioTunerSlider,
|
||||||
|
'pointerdown',
|
||||||
|
(event) => {
|
||||||
if (!beginTuner()) return;
|
if (!beginTuner()) return;
|
||||||
this._radioTunerPointerId = event.pointerId;
|
this._radioTunerPointerId = event.pointerId;
|
||||||
this._radioTunerSlider.focus({ preventScroll: true });
|
this._radioTunerSlider.focus({ preventScroll: true });
|
||||||
try { this._radioTunerSlider.setPointerCapture(event.pointerId); } catch { /* capture is best effort */ }
|
try {
|
||||||
|
this._radioTunerSlider.setPointerCapture(event.pointerId);
|
||||||
|
} catch {
|
||||||
|
/* capture is best effort */
|
||||||
|
}
|
||||||
updateTunerFromPointer(event);
|
updateTunerFromPointer(event);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}, tunerListenerOptions);
|
},
|
||||||
this.listen(this._radioTunerSlider, 'pointermove', (event) => {
|
tunerListenerOptions,
|
||||||
if (!this._radioTunerDragging || this._radioTunerPointerId !== event.pointerId) return;
|
);
|
||||||
|
this.listen(
|
||||||
|
this._radioTunerSlider,
|
||||||
|
'pointermove',
|
||||||
|
(event) => {
|
||||||
|
if (
|
||||||
|
!this._radioTunerDragging ||
|
||||||
|
this._radioTunerPointerId !== event.pointerId
|
||||||
|
)
|
||||||
|
return;
|
||||||
updateTunerFromPointer(event);
|
updateTunerFromPointer(event);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}, tunerListenerOptions);
|
},
|
||||||
this.listen(this._radioTunerSlider, 'input', () => {
|
tunerListenerOptions,
|
||||||
if (this._radioTunerPointerId !== null || this._radioTunerKeyboardKey) return;
|
);
|
||||||
|
this.listen(
|
||||||
|
this._radioTunerSlider,
|
||||||
|
'input',
|
||||||
|
() => {
|
||||||
|
if (this._radioTunerPointerId !== null || this._radioTunerKeyboardKey)
|
||||||
|
return;
|
||||||
if (!this._radioTunerDragging && !beginTuner()) return;
|
if (!this._radioTunerDragging && !beginTuner()) return;
|
||||||
const inputSlot = radioTunerSlot(this._radioTunerSlider.value, this._radioTunerStations.length);
|
const inputSlot = radioTunerSlot(
|
||||||
if (inputSlot.slot > this._radioTunerLastSlot) this._radioTunerDragDirection = 1;
|
this._radioTunerSlider.value,
|
||||||
else if (inputSlot.slot < this._radioTunerLastSlot) this._radioTunerDragDirection = -1;
|
this._radioTunerStations.length,
|
||||||
|
);
|
||||||
|
if (inputSlot.slot > this._radioTunerLastSlot)
|
||||||
|
this._radioTunerDragDirection = 1;
|
||||||
|
else if (inputSlot.slot < this._radioTunerLastSlot)
|
||||||
|
this._radioTunerDragDirection = -1;
|
||||||
this._radioTunerLastSlot = inputSlot.slot;
|
this._radioTunerLastSlot = inputSlot.slot;
|
||||||
this._radioTunerCoordinate = inputSlot.stationIndex;
|
this._radioTunerCoordinate = inputSlot.stationIndex;
|
||||||
tunerPreview({ coordinate: this._radioTunerCoordinate });
|
tunerPreview({ coordinate: this._radioTunerCoordinate });
|
||||||
}, tunerListenerOptions);
|
},
|
||||||
this.listen(this._radioTunerSlider, 'change', () => {
|
tunerListenerOptions,
|
||||||
if (this._radioTunerPointerId === null && !this._radioTunerKeyboardKey) finishTuner(true);
|
);
|
||||||
}, tunerListenerOptions);
|
this.listen(
|
||||||
this.listen(this._radioTunerSlider, 'pointerup', (event) => {
|
this._radioTunerSlider,
|
||||||
|
'change',
|
||||||
|
() => {
|
||||||
|
if (this._radioTunerPointerId === null && !this._radioTunerKeyboardKey)
|
||||||
|
finishTuner(true);
|
||||||
|
},
|
||||||
|
tunerListenerOptions,
|
||||||
|
);
|
||||||
|
this.listen(
|
||||||
|
this._radioTunerSlider,
|
||||||
|
'pointerup',
|
||||||
|
(event) => {
|
||||||
if (this._radioTunerPointerId !== event.pointerId) return;
|
if (this._radioTunerPointerId !== event.pointerId) return;
|
||||||
updateTunerFromPointer(event);
|
updateTunerFromPointer(event);
|
||||||
finishTuner(true);
|
finishTuner(true);
|
||||||
try { this._radioTunerSlider.releasePointerCapture(event.pointerId); } catch { /* already released */ }
|
try {
|
||||||
|
this._radioTunerSlider.releasePointerCapture(event.pointerId);
|
||||||
|
} catch {
|
||||||
|
/* already released */
|
||||||
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}, tunerListenerOptions);
|
},
|
||||||
this.listen(this._radioTunerSlider, 'pointercancel', (event) => {
|
tunerListenerOptions,
|
||||||
if (this._radioTunerPointerId !== null && this._radioTunerPointerId !== event.pointerId) return;
|
);
|
||||||
try { this._radioTunerSlider.releasePointerCapture(event.pointerId); } catch { /* already released */ }
|
this.listen(
|
||||||
|
this._radioTunerSlider,
|
||||||
|
'pointercancel',
|
||||||
|
(event) => {
|
||||||
|
if (
|
||||||
|
this._radioTunerPointerId !== null &&
|
||||||
|
this._radioTunerPointerId !== event.pointerId
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
this._radioTunerSlider.releasePointerCapture(event.pointerId);
|
||||||
|
} catch {
|
||||||
|
/* already released */
|
||||||
|
}
|
||||||
finishTuner(false);
|
finishTuner(false);
|
||||||
}, tunerListenerOptions);
|
},
|
||||||
this.listen(this._radioTunerSlider, 'lostpointercapture', (event) => {
|
tunerListenerOptions,
|
||||||
if (this._radioTunerDragging && this._radioTunerPointerId === event.pointerId) finishTuner(false);
|
);
|
||||||
}, tunerListenerOptions);
|
this.listen(
|
||||||
this.listen(this._radioTunerSlider, 'keydown', (event) => {
|
this._radioTunerSlider,
|
||||||
|
'lostpointercapture',
|
||||||
|
(event) => {
|
||||||
|
if (
|
||||||
|
this._radioTunerDragging &&
|
||||||
|
this._radioTunerPointerId === event.pointerId
|
||||||
|
)
|
||||||
|
finishTuner(false);
|
||||||
|
},
|
||||||
|
tunerListenerOptions,
|
||||||
|
);
|
||||||
|
this.listen(
|
||||||
|
this._radioTunerSlider,
|
||||||
|
'keydown',
|
||||||
|
(event) => {
|
||||||
if (event.key === 'Escape' && this._radioTunerDragging) {
|
if (event.key === 'Escape' && this._radioTunerDragging) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
finishTuner(false);
|
finishTuner(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End', 'PageUp', 'PageDown'].includes(event.key)) return;
|
if (
|
||||||
|
![
|
||||||
|
'ArrowLeft',
|
||||||
|
'ArrowRight',
|
||||||
|
'Home',
|
||||||
|
'End',
|
||||||
|
'PageUp',
|
||||||
|
'PageDown',
|
||||||
|
].includes(event.key)
|
||||||
|
)
|
||||||
|
return;
|
||||||
if (!this._radioTunerDragging && !beginTuner()) return;
|
if (!this._radioTunerDragging && !beginTuner()) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this._radioTunerKeyboardKey = event.key;
|
this._radioTunerKeyboardKey = event.key;
|
||||||
const max = Math.max(0, this._radioTunerStations.length - 1);
|
const max = Math.max(0, this._radioTunerStations.length - 1);
|
||||||
const current = radioTunerSlot(this._radioTunerSlider.value, this._radioTunerStations.length).slot;
|
const current = radioTunerSlot(
|
||||||
|
this._radioTunerSlider.value,
|
||||||
|
this._radioTunerStations.length,
|
||||||
|
).slot;
|
||||||
const page = Math.max(1, Math.round(max / 10));
|
const page = Math.max(1, Math.round(max / 10));
|
||||||
const next = event.key === 'Home' ? 0
|
const next =
|
||||||
: event.key === 'End' ? max
|
event.key === 'Home'
|
||||||
: event.key === 'PageUp' ? current + page
|
? 0
|
||||||
: event.key === 'PageDown' ? current - page
|
: event.key === 'End'
|
||||||
|
? max
|
||||||
|
: event.key === 'PageUp'
|
||||||
|
? current + page
|
||||||
|
: event.key === 'PageDown'
|
||||||
|
? current - page
|
||||||
: current + (event.key === 'ArrowRight' ? 1 : -1);
|
: current + (event.key === 'ArrowRight' ? 1 : -1);
|
||||||
const slot = radioTunerSlot(next, this._radioTunerStations.length);
|
const slot = radioTunerSlot(next, this._radioTunerStations.length);
|
||||||
if (slot.slot > this._radioTunerLastSlot) this._radioTunerDragDirection = 1;
|
if (slot.slot > this._radioTunerLastSlot)
|
||||||
else if (slot.slot < this._radioTunerLastSlot) this._radioTunerDragDirection = -1;
|
this._radioTunerDragDirection = 1;
|
||||||
|
else if (slot.slot < this._radioTunerLastSlot)
|
||||||
|
this._radioTunerDragDirection = -1;
|
||||||
this._radioTunerLastSlot = slot.slot;
|
this._radioTunerLastSlot = slot.slot;
|
||||||
this._radioTunerCoordinate = slot.stationIndex;
|
this._radioTunerCoordinate = slot.stationIndex;
|
||||||
this._radioTunerSlider.value = String(slot.slot);
|
this._radioTunerSlider.value = String(slot.slot);
|
||||||
tunerPreview({ coordinate: this._radioTunerCoordinate });
|
tunerPreview({ coordinate: this._radioTunerCoordinate });
|
||||||
}, tunerListenerOptions);
|
},
|
||||||
this.listen(this._radioTunerSlider, 'keyup', (event) => {
|
tunerListenerOptions,
|
||||||
if (!this._radioTunerKeyboardKey || event.key !== this._radioTunerKeyboardKey) return;
|
);
|
||||||
|
this.listen(
|
||||||
|
this._radioTunerSlider,
|
||||||
|
'keyup',
|
||||||
|
(event) => {
|
||||||
|
if (
|
||||||
|
!this._radioTunerKeyboardKey ||
|
||||||
|
event.key !== this._radioTunerKeyboardKey
|
||||||
|
)
|
||||||
|
return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
finishTuner(true);
|
finishTuner(true);
|
||||||
}, tunerListenerOptions);
|
},
|
||||||
this.listen(this._radioTunerSlider, 'blur', () => finishTuner(true), tunerListenerOptions);
|
tunerListenerOptions,
|
||||||
|
);
|
||||||
|
this.listen(
|
||||||
|
this._radioTunerSlider,
|
||||||
|
'blur',
|
||||||
|
() => finishTuner(true),
|
||||||
|
tunerListenerOptions,
|
||||||
|
);
|
||||||
const releaseNavigationBand = () => {
|
const releaseNavigationBand = () => {
|
||||||
this._radioTunerBandPinnedForNavigation = false;
|
this._radioTunerBandPinnedForNavigation = false;
|
||||||
};
|
};
|
||||||
this.listen(this.canvas, 'pointerdown', releaseNavigationBand, tunerListenerOptions);
|
this.listen(
|
||||||
this.listen(this.canvas, 'wheel', releaseNavigationBand, tunerListenerOptions);
|
this.canvas,
|
||||||
|
'pointerdown',
|
||||||
|
releaseNavigationBand,
|
||||||
|
tunerListenerOptions,
|
||||||
|
);
|
||||||
|
this.listen(
|
||||||
|
this.canvas,
|
||||||
|
'wheel',
|
||||||
|
releaseNavigationBand,
|
||||||
|
tunerListenerOptions,
|
||||||
|
);
|
||||||
// The directory order is catalog/filter authority, not camera authority.
|
// The directory order is catalog/filter authority, not camera authority.
|
||||||
// Globe motion therefore never rebuilds or re-ranks the frequency band.
|
// Globe motion therefore never rebuilds or re-ranks the frequency band.
|
||||||
this._radioSelectedHandler = () => this.actions.setPanelCollapsed('radio-panel', false);
|
this._radioSelectedHandler = () =>
|
||||||
|
this.actions.setPanelCollapsed('radio-panel', false);
|
||||||
this.listen(document, 'gev:radio-selected', this._radioSelectedHandler);
|
this.listen(document, 'gev:radio-selected', this._radioSelectedHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
+98
-29
@@ -31,17 +31,24 @@ export class RadioControls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
listen(target, type, handler, options = {}) {
|
listen(target, type, handler, options = {}) {
|
||||||
target?.addEventListener(type, handler, { ...options, signal: this.listeners.signal });
|
target?.addEventListener(type, handler, {
|
||||||
|
...options,
|
||||||
|
signal: this.listeners.signal,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
this._radioUnsubscribe?.();
|
this._radioUnsubscribe?.();
|
||||||
this._radioUnsubscribe = null;
|
this._radioUnsubscribe = null;
|
||||||
if (this.destroyed) return;
|
if (this.destroyed) return;
|
||||||
this._radioUnsubscribe = this.radio.subscribe?.((state) => this._renderRadioState(state));
|
this._radioUnsubscribe = this.radio.subscribe?.((state) =>
|
||||||
|
this._renderRadioState(state),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderRadioState(state) { renderRadioState.call(this, state); }
|
_renderRadioState(state) {
|
||||||
|
renderRadioState.call(this, state);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reveal the newly enabled directory and transport inside Context without
|
* Reveal the newly enabled directory and transport inside Context without
|
||||||
@@ -55,19 +62,32 @@ export class RadioControls {
|
|||||||
const scroller = contextPanel?.querySelector('.global-context-panel-inner');
|
const scroller = contextPanel?.querySelector('.global-context-panel-inner');
|
||||||
const directory = this._radioPanel?.querySelector('.radio-directory-row');
|
const directory = this._radioPanel?.querySelector('.radio-directory-row');
|
||||||
const transport = this._radioPanel?.querySelector('.radio-transport');
|
const transport = this._radioPanel?.querySelector('.radio-transport');
|
||||||
if (!contextPanel || contextPanel.classList.contains('collapsed')
|
if (
|
||||||
|| !scroller || !directory || !transport || !this._radioState?.enabled) return false;
|
!contextPanel ||
|
||||||
|
contextPanel.classList.contains('collapsed') ||
|
||||||
|
!scroller ||
|
||||||
|
!directory ||
|
||||||
|
!transport ||
|
||||||
|
!this._radioState?.enabled
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
|
||||||
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
await new Promise((resolve) =>
|
||||||
if (this.destroyed || !this._radioState?.enabled || !trigger?.isConnected) return false;
|
requestAnimationFrame(() => requestAnimationFrame(resolve)),
|
||||||
|
);
|
||||||
|
if (this.destroyed || !this._radioState?.enabled || !trigger?.isConnected)
|
||||||
|
return false;
|
||||||
|
|
||||||
const viewport = scroller.getBoundingClientRect();
|
const viewport = scroller.getBoundingClientRect();
|
||||||
const directoryRect = directory.getBoundingClientRect();
|
const directoryRect = directory.getBoundingClientRect();
|
||||||
const transportRect = transport.getBoundingClientRect();
|
const transportRect = transport.getBoundingClientRect();
|
||||||
const margin = 10;
|
const margin = 10;
|
||||||
const minimum = scroller.scrollTop + transportRect.bottom - (viewport.bottom - margin);
|
const minimum =
|
||||||
const maximum = scroller.scrollTop + directoryRect.top - (viewport.top + margin);
|
scroller.scrollTop + transportRect.bottom - (viewport.bottom - margin);
|
||||||
const desired = minimum <= maximum
|
const maximum =
|
||||||
|
scroller.scrollTop + directoryRect.top - (viewport.top + margin);
|
||||||
|
const desired =
|
||||||
|
minimum <= maximum
|
||||||
? Math.min(Math.max(scroller.scrollTop, minimum), maximum)
|
? Math.min(Math.max(scroller.scrollTop, minimum), maximum)
|
||||||
: minimum;
|
: minimum;
|
||||||
const next = Math.min(
|
const next = Math.min(
|
||||||
@@ -75,8 +95,13 @@ export class RadioControls {
|
|||||||
Math.max(0, desired),
|
Math.max(0, desired),
|
||||||
);
|
);
|
||||||
if (Math.abs(next - scroller.scrollTop) < 1) return false;
|
if (Math.abs(next - scroller.scrollTop) < 1) return false;
|
||||||
const reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches;
|
const reducedMotion = window.matchMedia?.(
|
||||||
scroller.scrollTo({ top: next, behavior: reducedMotion ? 'auto' : 'smooth' });
|
'(prefers-reduced-motion: reduce)',
|
||||||
|
)?.matches;
|
||||||
|
scroller.scrollTo({
|
||||||
|
top: next,
|
||||||
|
behavior: reducedMotion ? 'auto' : 'smooth',
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,11 +114,24 @@ export class RadioControls {
|
|||||||
async _revealRadioPanelInsideContext({ focusTarget = null } = {}) {
|
async _revealRadioPanelInsideContext({ focusTarget = null } = {}) {
|
||||||
const contextPanel = document.getElementById('global-context-panel');
|
const contextPanel = document.getElementById('global-context-panel');
|
||||||
const scroller = contextPanel?.querySelector('.global-context-panel-inner');
|
const scroller = contextPanel?.querySelector('.global-context-panel-inner');
|
||||||
if (!contextPanel || contextPanel.classList.contains('collapsed')
|
if (
|
||||||
|| !scroller || !this._radioPanel || this._radioPanel.classList.contains('collapsed')) return false;
|
!contextPanel ||
|
||||||
|
contextPanel.classList.contains('collapsed') ||
|
||||||
|
!scroller ||
|
||||||
|
!this._radioPanel ||
|
||||||
|
this._radioPanel.classList.contains('collapsed')
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
|
||||||
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
await new Promise((resolve) =>
|
||||||
if (this.destroyed || contextPanel.classList.contains('collapsed') || this._radioPanel.classList.contains('collapsed')) return false;
|
requestAnimationFrame(() => requestAnimationFrame(resolve)),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
this.destroyed ||
|
||||||
|
contextPanel.classList.contains('collapsed') ||
|
||||||
|
this._radioPanel.classList.contains('collapsed')
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
|
||||||
const viewport = scroller.getBoundingClientRect();
|
const viewport = scroller.getBoundingClientRect();
|
||||||
const radioRect = this._radioPanel.getBoundingClientRect();
|
const radioRect = this._radioPanel.getBoundingClientRect();
|
||||||
@@ -104,8 +142,13 @@ export class RadioControls {
|
|||||||
);
|
);
|
||||||
const moved = Math.abs(next - scroller.scrollTop) >= 1;
|
const moved = Math.abs(next - scroller.scrollTop) >= 1;
|
||||||
if (moved) {
|
if (moved) {
|
||||||
const reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches;
|
const reducedMotion = window.matchMedia?.(
|
||||||
scroller.scrollTo({ top: next, behavior: reducedMotion ? 'auto' : 'smooth' });
|
'(prefers-reduced-motion: reduce)',
|
||||||
|
)?.matches;
|
||||||
|
scroller.scrollTo({
|
||||||
|
top: next,
|
||||||
|
behavior: reducedMotion ? 'auto' : 'smooth',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
focusTarget?.focus?.({ preventScroll: true });
|
focusTarget?.focus?.({ preventScroll: true });
|
||||||
return moved;
|
return moved;
|
||||||
@@ -115,25 +158,44 @@ export class RadioControls {
|
|||||||
_syncContextRadioLauncherState() {
|
_syncContextRadioLauncherState() {
|
||||||
if (!this._contextRadioToggleBtn) return;
|
if (!this._contextRadioToggleBtn) return;
|
||||||
const contextPanel = document.getElementById('global-context-panel');
|
const contextPanel = document.getElementById('global-context-panel');
|
||||||
const contextExpanded = Boolean(contextPanel && !contextPanel.classList.contains('collapsed'));
|
const contextExpanded = Boolean(
|
||||||
|
contextPanel && !contextPanel.classList.contains('collapsed'),
|
||||||
|
);
|
||||||
if (contextExpanded) {
|
if (contextExpanded) {
|
||||||
const radioExpanded = Boolean(this._radioPanel && !this._radioPanel.classList.contains('collapsed'));
|
const radioExpanded = Boolean(
|
||||||
|
this._radioPanel && !this._radioPanel.classList.contains('collapsed'),
|
||||||
|
);
|
||||||
this._contextRadioToggleBtn.setAttribute('aria-controls', 'radio-panel');
|
this._contextRadioToggleBtn.setAttribute('aria-controls', 'radio-panel');
|
||||||
this._contextRadioToggleBtn.setAttribute('aria-expanded', String(radioExpanded));
|
this._contextRadioToggleBtn.setAttribute(
|
||||||
const label = radioExpanded ? 'Go to expanded Radio section' : 'Expand Radio section in Context';
|
'aria-expanded',
|
||||||
|
String(radioExpanded),
|
||||||
|
);
|
||||||
|
const label = radioExpanded
|
||||||
|
? 'Go to expanded Radio section'
|
||||||
|
: 'Expand Radio section in Context';
|
||||||
this._contextRadioToggleBtn.setAttribute('aria-label', label);
|
this._contextRadioToggleBtn.setAttribute('aria-label', label);
|
||||||
this._contextRadioToggleBtn.title = label;
|
this._contextRadioToggleBtn.title = label;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const compactOpen = Boolean(this._contextRadioDock?.classList.contains('disclosure-open'));
|
const compactOpen = Boolean(
|
||||||
this._contextRadioToggleBtn.setAttribute('aria-controls', 'context-radio-mini');
|
this._contextRadioDock?.classList.contains('disclosure-open'),
|
||||||
this._contextRadioToggleBtn.setAttribute('aria-expanded', String(compactOpen));
|
);
|
||||||
|
this._contextRadioToggleBtn.setAttribute(
|
||||||
|
'aria-controls',
|
||||||
|
'context-radio-mini',
|
||||||
|
);
|
||||||
|
this._contextRadioToggleBtn.setAttribute(
|
||||||
|
'aria-expanded',
|
||||||
|
String(compactOpen),
|
||||||
|
);
|
||||||
const action = compactOpen ? 'Close' : 'Open';
|
const action = compactOpen ? 'Close' : 'Open';
|
||||||
this._contextRadioToggleBtn.setAttribute('aria-label', `${action} compact Radio controls`);
|
this._contextRadioToggleBtn.setAttribute(
|
||||||
|
'aria-label',
|
||||||
|
`${action} compact Radio controls`,
|
||||||
|
);
|
||||||
this._contextRadioToggleBtn.title = `${action} compact Radio controls`;
|
this._contextRadioToggleBtn.title = `${action} compact Radio controls`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
if (this.destroyed) return;
|
if (this.destroyed) return;
|
||||||
this.destroyed = true;
|
this.destroyed = true;
|
||||||
@@ -148,9 +210,16 @@ export class RadioControls {
|
|||||||
this._radioTunerPool = [];
|
this._radioTunerPool = [];
|
||||||
this._radioTunerStations = [];
|
this._radioTunerStations = [];
|
||||||
this._radioTunerBandPinnedForNavigation = false;
|
this._radioTunerBandPinnedForNavigation = false;
|
||||||
try { if (pointer !== null) this._radioTunerSlider?.releasePointerCapture(pointer); } catch { /* capture already released */ }
|
try {
|
||||||
|
if (pointer !== null)
|
||||||
|
this._radioTunerSlider?.releasePointerCapture(pointer);
|
||||||
|
} catch {
|
||||||
|
/* capture already released */
|
||||||
|
}
|
||||||
this._radioTuner?.classList.remove('is-dragging', 'is-static');
|
this._radioTuner?.classList.remove('is-dragging', 'is-static');
|
||||||
document.getElementById('title-bar')?.classList.remove('radio-broadcasting');
|
document
|
||||||
|
.getElementById('title-bar')
|
||||||
|
?.classList.remove('radio-broadcasting');
|
||||||
this.radio.endTuning();
|
this.radio.endTuning();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,13 +13,22 @@ class Element extends EventTarget {
|
|||||||
add: (...names) => names.forEach((n) => this.classes.add(n)),
|
add: (...names) => names.forEach((n) => this.classes.add(n)),
|
||||||
remove: (...names) => names.forEach((n) => this.classes.delete(n)),
|
remove: (...names) => names.forEach((n) => this.classes.delete(n)),
|
||||||
contains: (name) => this.classes.has(name),
|
contains: (name) => this.classes.has(name),
|
||||||
toggle: (name, enabled) => enabled ? this.classes.add(name) : this.classes.delete(name),
|
toggle: (name, enabled) =>
|
||||||
|
enabled ? this.classes.add(name) : this.classes.delete(name),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
setAttribute(name, value) { this.attrs.set(name, String(value)); }
|
setAttribute(name, value) {
|
||||||
getAttribute(name) { return this.attrs.get(name); }
|
this.attrs.set(name, String(value));
|
||||||
querySelector() { return null; }
|
}
|
||||||
focus() { this.focusCount++; }
|
getAttribute(name) {
|
||||||
|
return this.attrs.get(name);
|
||||||
|
}
|
||||||
|
querySelector() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
focus() {
|
||||||
|
this.focusCount++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function fixture() {
|
function fixture() {
|
||||||
@@ -34,25 +43,54 @@ function fixture() {
|
|||||||
const calls = [];
|
const calls = [];
|
||||||
const subscribers = [];
|
const subscribers = [];
|
||||||
const radio = {
|
const radio = {
|
||||||
subscribe(callback) { subscribers.push(callback); calls.push('subscribe'); return () => calls.push('unsubscribe'); },
|
subscribe(callback) {
|
||||||
endTuning() { calls.push('endTuning'); },
|
subscribers.push(callback);
|
||||||
cycleStation(direction) { calls.push(['cycle', direction]); return true; },
|
calls.push('subscribe');
|
||||||
|
return () => calls.push('unsubscribe');
|
||||||
|
},
|
||||||
|
endTuning() {
|
||||||
|
calls.push('endTuning');
|
||||||
|
},
|
||||||
|
cycleStation(direction) {
|
||||||
|
calls.push(['cycle', direction]);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const actions = {
|
const actions = {
|
||||||
isRegistered: () => true, isEnabled: () => false,
|
isRegistered: () => true,
|
||||||
|
isEnabled: () => false,
|
||||||
runUserAction: (operation) => operation(),
|
runUserAction: (operation) => operation(),
|
||||||
setEnabled: async () => true,
|
setEnabled: async () => true,
|
||||||
setPanelCollapsed: (...args) => calls.push(['panel', ...args]),
|
setPanelCollapsed: (...args) => calls.push(['panel', ...args]),
|
||||||
layoutCockpit() {}, isCockpitActive: () => false,
|
layoutCockpit() {},
|
||||||
scheduleLayout() {}, getLifecycle: () => null,
|
isCockpitActive: () => false,
|
||||||
|
scheduleLayout() {},
|
||||||
|
getLifecycle: () => null,
|
||||||
preservePanelStateDuringClear: () => false,
|
preservePanelStateDuringClear: () => false,
|
||||||
};
|
};
|
||||||
const controls = new RadioControls({
|
const controls = new RadioControls({
|
||||||
elements: { _radioPanel: panel, _radioEnableBtn: enable, _radioNextBtn: next },
|
elements: {
|
||||||
radio, actions, canvas: new Element(),
|
_radioPanel: panel,
|
||||||
|
_radioEnableBtn: enable,
|
||||||
|
_radioNextBtn: next,
|
||||||
|
},
|
||||||
|
radio,
|
||||||
|
actions,
|
||||||
|
canvas: new Element(),
|
||||||
});
|
});
|
||||||
return { controls, enable, next, actions, calls, subscribers, document,
|
return {
|
||||||
cleanup() { controls.destroy(); Object.assign(globalThis, prior); } };
|
controls,
|
||||||
|
enable,
|
||||||
|
next,
|
||||||
|
actions,
|
||||||
|
calls,
|
||||||
|
subscribers,
|
||||||
|
document,
|
||||||
|
cleanup() {
|
||||||
|
controls.destroy();
|
||||||
|
Object.assign(globalThis, prior);
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
test('Radio listeners and subscriptions are revoked once before tuning teardown', () => {
|
test('Radio listeners and subscriptions are revoked once before tuning teardown', () => {
|
||||||
@@ -61,7 +99,8 @@ test('Radio listeners and subscriptions are revoked once before tuning teardown'
|
|||||||
f.controls.connect();
|
f.controls.connect();
|
||||||
f.next.dispatchEvent(new Event('click'));
|
f.next.dispatchEvent(new Event('click'));
|
||||||
assert.deepEqual(f.calls, ['subscribe', ['cycle', 1]]);
|
assert.deepEqual(f.calls, ['subscribe', ['cycle', 1]]);
|
||||||
f.controls.destroy(); f.controls.destroy();
|
f.controls.destroy();
|
||||||
|
f.controls.destroy();
|
||||||
const snapshot = structuredClone(f.calls);
|
const snapshot = structuredClone(f.calls);
|
||||||
f.next.dispatchEvent(new Event('click'));
|
f.next.dispatchEvent(new Event('click'));
|
||||||
f.document.dispatchEvent(new Event('gev:radio-selected'));
|
f.document.dispatchEvent(new Event('gev:radio-selected'));
|
||||||
@@ -69,21 +108,28 @@ test('Radio listeners and subscriptions are revoked once before tuning teardown'
|
|||||||
f.controls.connect();
|
f.controls.connect();
|
||||||
assert.deepEqual(f.calls, snapshot);
|
assert.deepEqual(f.calls, snapshot);
|
||||||
assert.deepEqual(f.calls.slice(-2), ['unsubscribe', 'endTuning']);
|
assert.deepEqual(f.calls.slice(-2), ['unsubscribe', 'endTuning']);
|
||||||
} finally { f.cleanup(); }
|
} finally {
|
||||||
|
f.cleanup();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Radio reconnect removes its previous state subscription', () => {
|
test('Radio reconnect removes its previous state subscription', () => {
|
||||||
const f = fixture();
|
const f = fixture();
|
||||||
try {
|
try {
|
||||||
f.controls.connect(); f.controls.connect();
|
f.controls.connect();
|
||||||
|
f.controls.connect();
|
||||||
assert.deepEqual(f.calls, ['subscribe', 'unsubscribe', 'subscribe']);
|
assert.deepEqual(f.calls, ['subscribe', 'unsubscribe', 'subscribe']);
|
||||||
} finally { f.cleanup(); }
|
} finally {
|
||||||
|
f.cleanup();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a delayed Radio enable cannot reveal or refocus controls after destruction', async () => {
|
test('a delayed Radio enable cannot reveal or refocus controls after destruction', async () => {
|
||||||
const f = fixture();
|
const f = fixture();
|
||||||
let resolve;
|
let resolve;
|
||||||
const pending = new Promise((done) => { resolve = done; });
|
const pending = new Promise((done) => {
|
||||||
|
resolve = done;
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
f.actions.setEnabled = () => pending;
|
f.actions.setEnabled = () => pending;
|
||||||
f.enable.dispatchEvent(new Event('click'));
|
f.enable.dispatchEvent(new Event('click'));
|
||||||
@@ -91,26 +137,36 @@ test('a delayed Radio enable cannot reveal or refocus controls after destruction
|
|||||||
f.controls.destroy();
|
f.controls.destroy();
|
||||||
const attrs = new Map(f.enable.attrs);
|
const attrs = new Map(f.enable.attrs);
|
||||||
resolve(true);
|
resolve(true);
|
||||||
await pending; await new Promise((done) => setImmediate(done));
|
await pending;
|
||||||
|
await new Promise((done) => setImmediate(done));
|
||||||
assert.deepEqual(f.enable.attrs, attrs);
|
assert.deepEqual(f.enable.attrs, attrs);
|
||||||
assert.equal(f.enable.focusCount, 0);
|
assert.equal(f.enable.focusCount, 0);
|
||||||
assert.equal(f.calls.some((call) => Array.isArray(call) && call[0] === 'panel'), false);
|
assert.equal(
|
||||||
} finally { f.cleanup(); }
|
f.calls.some((call) => Array.isArray(call) && call[0] === 'panel'),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
f.cleanup();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('destruction releases an active tuner pointer after revoking listeners', () => {
|
test('destruction releases an active tuner pointer after revoking listeners', () => {
|
||||||
const f = fixture();
|
const f = fixture();
|
||||||
try {
|
try {
|
||||||
const released = [];
|
const released = [];
|
||||||
f.controls._radioTunerSlider = { releasePointerCapture(id) {
|
f.controls._radioTunerSlider = {
|
||||||
|
releasePointerCapture(id) {
|
||||||
assert.equal(f.controls.listeners.signal.aborted, true);
|
assert.equal(f.controls.listeners.signal.aborted, true);
|
||||||
released.push(id);
|
released.push(id);
|
||||||
} };
|
},
|
||||||
|
};
|
||||||
f.controls._radioTunerPointerId = 7;
|
f.controls._radioTunerPointerId = 7;
|
||||||
f.controls._radioTunerDragging = true;
|
f.controls._radioTunerDragging = true;
|
||||||
f.controls.destroy();
|
f.controls.destroy();
|
||||||
assert.deepEqual(released, [7]);
|
assert.deepEqual(released, [7]);
|
||||||
assert.equal(f.controls._radioTunerDragging, false);
|
assert.equal(f.controls._radioTunerDragging, false);
|
||||||
assert.equal(f.controls._radioTunerPointerId, null);
|
assert.equal(f.controls._radioTunerPointerId, null);
|
||||||
} finally { f.cleanup(); }
|
} finally {
|
||||||
|
f.cleanup();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
+185
-63
@@ -1,8 +1,9 @@
|
|||||||
/** Render Radio state without making playback or Context decisions. */
|
/** Render Radio state without making playback or Context decisions. */
|
||||||
export function renderRadioState(state) {
|
export function renderRadioState(state) {
|
||||||
if (this.destroyed || !state || !this._radioPanel) return;
|
if (this.destroyed || !state || !this._radioPanel) return;
|
||||||
const lifecycle = this.actions.getLifecycle() || null;
|
const lifecycle = this.actions.getLifecycle() || null;
|
||||||
const lifecycleState = lifecycle?.lifecycleState || (state.enabled ? 'enabled' : 'disabled');
|
const lifecycleState =
|
||||||
|
lifecycle?.lifecycleState || (state.enabled ? 'enabled' : 'disabled');
|
||||||
state = {
|
state = {
|
||||||
...state,
|
...state,
|
||||||
enabled: lifecycle ? lifecycle.enabled : state.enabled,
|
enabled: lifecycle ? lifecycle.enabled : state.enabled,
|
||||||
@@ -11,13 +12,16 @@ export function renderRadioState(state) {
|
|||||||
};
|
};
|
||||||
this._radioState = state;
|
this._radioState = state;
|
||||||
const enabled = Boolean(state.enabled);
|
const enabled = Boolean(state.enabled);
|
||||||
const transitioning = lifecycleState === 'enabling' || lifecycleState === 'disabling';
|
const transitioning =
|
||||||
|
lifecycleState === 'enabling' || lifecycleState === 'disabling';
|
||||||
const uncertain = Boolean(state.lifecycleUncertain);
|
const uncertain = Boolean(state.lifecycleUncertain);
|
||||||
const interactive = enabled && !transitioning && !uncertain;
|
const interactive = enabled && !transitioning && !uncertain;
|
||||||
const selected = state.selected || null;
|
const selected = state.selected || null;
|
||||||
const hasStations = state.filteredCount > 0;
|
const hasStations = state.filteredCount > 0;
|
||||||
const activePlayback = ['playing', 'buffering'].includes(state.audioState);
|
const activePlayback = ['playing', 'buffering'].includes(state.audioState);
|
||||||
document.getElementById('title-bar')?.classList.toggle('radio-broadcasting', state.audioState === 'playing');
|
document
|
||||||
|
.getElementById('title-bar')
|
||||||
|
?.classList.toggle('radio-broadcasting', state.audioState === 'playing');
|
||||||
this._radioPanel.classList.toggle('radio-enabled', enabled);
|
this._radioPanel.classList.toggle('radio-enabled', enabled);
|
||||||
this._radioPanel.classList.toggle('lifecycle-uncertain', uncertain);
|
this._radioPanel.classList.toggle('lifecycle-uncertain', uncertain);
|
||||||
this._contextRadioDock?.classList.toggle('active', enabled);
|
this._contextRadioDock?.classList.toggle('active', enabled);
|
||||||
@@ -29,17 +33,29 @@ export function renderRadioState(state) {
|
|||||||
if (this._radioLayerState) {
|
if (this._radioLayerState) {
|
||||||
this._radioLayerState.textContent = transitioning
|
this._radioLayerState.textContent = transitioning
|
||||||
? lifecycleState.toUpperCase()
|
? lifecycleState.toUpperCase()
|
||||||
: (uncertain ? 'UNCERTAIN' : (state.loading ? 'SYNC' : (enabled ? `${state.filteredCount}/${state.stationCount}` : 'OFF')));
|
: uncertain
|
||||||
|
? 'UNCERTAIN'
|
||||||
|
: state.loading
|
||||||
|
? 'SYNC'
|
||||||
|
: enabled
|
||||||
|
? `${state.filteredCount}/${state.stationCount}`
|
||||||
|
: 'OFF';
|
||||||
}
|
}
|
||||||
if (this._radioEnableBtn) {
|
if (this._radioEnableBtn) {
|
||||||
this._radioEnableBtn.classList.toggle('active', enabled);
|
this._radioEnableBtn.classList.toggle('active', enabled);
|
||||||
this._radioEnableBtn.setAttribute('aria-pressed', String(enabled));
|
this._radioEnableBtn.setAttribute('aria-pressed', String(enabled));
|
||||||
this._radioEnableBtn.textContent = transitioning
|
this._radioEnableBtn.textContent = transitioning
|
||||||
? lifecycleState.toUpperCase()
|
? lifecycleState.toUpperCase()
|
||||||
: (uncertain ? 'RECONCILE' : (enabled ? 'DISABLE' : 'ENABLE'));
|
: uncertain
|
||||||
|
? 'RECONCILE'
|
||||||
|
: enabled
|
||||||
|
? 'DISABLE'
|
||||||
|
: 'ENABLE';
|
||||||
this._radioEnableBtn.setAttribute(
|
this._radioEnableBtn.setAttribute(
|
||||||
'aria-label',
|
'aria-label',
|
||||||
uncertain ? 'Reconcile Radio — lifecycle uncertain' : `${enabled ? 'Disable' : 'Enable'} Radio`,
|
uncertain
|
||||||
|
? 'Reconcile Radio — lifecycle uncertain'
|
||||||
|
: `${enabled ? 'Disable' : 'Enable'} Radio`,
|
||||||
);
|
);
|
||||||
this._radioEnableBtn.disabled = false;
|
this._radioEnableBtn.disabled = false;
|
||||||
this._radioEnableBtn.setAttribute('aria-disabled', String(transitioning));
|
this._radioEnableBtn.setAttribute('aria-disabled', String(transitioning));
|
||||||
@@ -47,31 +63,58 @@ export function renderRadioState(state) {
|
|||||||
}
|
}
|
||||||
if (this._contextRadioMiniEnableBtn) {
|
if (this._contextRadioMiniEnableBtn) {
|
||||||
this._contextRadioMiniEnableBtn.classList.toggle('active', enabled);
|
this._contextRadioMiniEnableBtn.classList.toggle('active', enabled);
|
||||||
this._contextRadioMiniEnableBtn.setAttribute('aria-pressed', String(enabled));
|
this._contextRadioMiniEnableBtn.setAttribute(
|
||||||
|
'aria-pressed',
|
||||||
|
String(enabled),
|
||||||
|
);
|
||||||
this._contextRadioMiniEnableBtn.textContent = transitioning
|
this._contextRadioMiniEnableBtn.textContent = transitioning
|
||||||
? lifecycleState.toUpperCase()
|
? lifecycleState.toUpperCase()
|
||||||
: (uncertain ? 'RECONCILE' : (enabled ? 'DISABLE' : 'ENABLE'));
|
: uncertain
|
||||||
|
? 'RECONCILE'
|
||||||
|
: enabled
|
||||||
|
? 'DISABLE'
|
||||||
|
: 'ENABLE';
|
||||||
this._contextRadioMiniEnableBtn.setAttribute(
|
this._contextRadioMiniEnableBtn.setAttribute(
|
||||||
'aria-label',
|
'aria-label',
|
||||||
uncertain ? 'Reconcile Radio — lifecycle uncertain' : `${enabled ? 'Disable' : 'Enable'} Radio`,
|
uncertain
|
||||||
|
? 'Reconcile Radio — lifecycle uncertain'
|
||||||
|
: `${enabled ? 'Disable' : 'Enable'} Radio`,
|
||||||
);
|
);
|
||||||
this._contextRadioMiniEnableBtn.disabled = false;
|
this._contextRadioMiniEnableBtn.disabled = false;
|
||||||
this._contextRadioMiniEnableBtn.setAttribute('aria-disabled', String(transitioning));
|
this._contextRadioMiniEnableBtn.setAttribute(
|
||||||
this._contextRadioMiniEnableBtn.setAttribute('aria-busy', String(transitioning));
|
'aria-disabled',
|
||||||
|
String(transitioning),
|
||||||
|
);
|
||||||
|
this._contextRadioMiniEnableBtn.setAttribute(
|
||||||
|
'aria-busy',
|
||||||
|
String(transitioning),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (this._cockpitRadioEnableBtn) {
|
if (this._cockpitRadioEnableBtn) {
|
||||||
this._cockpitRadioEnableBtn.classList.toggle('active', enabled);
|
this._cockpitRadioEnableBtn.classList.toggle('active', enabled);
|
||||||
this._cockpitRadioEnableBtn.setAttribute('aria-pressed', String(enabled));
|
this._cockpitRadioEnableBtn.setAttribute('aria-pressed', String(enabled));
|
||||||
this._cockpitRadioEnableBtn.textContent = transitioning
|
this._cockpitRadioEnableBtn.textContent = transitioning
|
||||||
? lifecycleState.toUpperCase()
|
? lifecycleState.toUpperCase()
|
||||||
: (uncertain ? 'RECONCILE' : (enabled ? 'DISABLE' : 'ENABLE'));
|
: uncertain
|
||||||
|
? 'RECONCILE'
|
||||||
|
: enabled
|
||||||
|
? 'DISABLE'
|
||||||
|
: 'ENABLE';
|
||||||
this._cockpitRadioEnableBtn.setAttribute(
|
this._cockpitRadioEnableBtn.setAttribute(
|
||||||
'aria-label',
|
'aria-label',
|
||||||
uncertain ? 'Reconcile Radio — lifecycle uncertain' : `${enabled ? 'Disable' : 'Enable'} Radio`,
|
uncertain
|
||||||
|
? 'Reconcile Radio — lifecycle uncertain'
|
||||||
|
: `${enabled ? 'Disable' : 'Enable'} Radio`,
|
||||||
);
|
);
|
||||||
this._cockpitRadioEnableBtn.disabled = false;
|
this._cockpitRadioEnableBtn.disabled = false;
|
||||||
this._cockpitRadioEnableBtn.setAttribute('aria-disabled', String(transitioning));
|
this._cockpitRadioEnableBtn.setAttribute(
|
||||||
this._cockpitRadioEnableBtn.setAttribute('aria-busy', String(transitioning));
|
'aria-disabled',
|
||||||
|
String(transitioning),
|
||||||
|
);
|
||||||
|
this._cockpitRadioEnableBtn.setAttribute(
|
||||||
|
'aria-busy',
|
||||||
|
String(transitioning),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._radioFilter) {
|
if (this._radioFilter) {
|
||||||
@@ -80,19 +123,26 @@ export function renderRadioState(state) {
|
|||||||
.map((category) => `${category.id}:${category.count}:${category.color}`)
|
.map((category) => `${category.id}:${category.count}:${category.color}`)
|
||||||
.join('|');
|
.join('|');
|
||||||
if (categorySignature !== this._radioCategorySignature) {
|
if (categorySignature !== this._radioCategorySignature) {
|
||||||
this._radioFilter.replaceChildren(...state.categories.map((category) => {
|
this._radioFilter.replaceChildren(
|
||||||
|
...state.categories.map((category) => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.value = category.id;
|
option.value = category.id;
|
||||||
option.textContent = `● ${category.label} (${category.count})`;
|
option.textContent = `● ${category.label} (${category.count})`;
|
||||||
option.dataset.radioColor = category.color;
|
option.dataset.radioColor = category.color;
|
||||||
option.style.color = category.color;
|
option.style.color = category.color;
|
||||||
option.setAttribute('aria-label', `${category.label} (${category.count})`);
|
option.setAttribute(
|
||||||
|
'aria-label',
|
||||||
|
`${category.label} (${category.count})`,
|
||||||
|
);
|
||||||
return option;
|
return option;
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
this._radioCategorySignature = categorySignature;
|
this._radioCategorySignature = categorySignature;
|
||||||
}
|
}
|
||||||
this._radioFilter.value = prior;
|
this._radioFilter.value = prior;
|
||||||
const activeCategory = state.categories.find((category) => category.id === prior);
|
const activeCategory = state.categories.find(
|
||||||
|
(category) => category.id === prior,
|
||||||
|
);
|
||||||
this._radioFilter.style.color = activeCategory?.color || '';
|
this._radioFilter.style.color = activeCategory?.color || '';
|
||||||
this._radioFilter.disabled = !interactive || !state.stationCount;
|
this._radioFilter.disabled = !interactive || !state.stationCount;
|
||||||
}
|
}
|
||||||
@@ -101,8 +151,11 @@ export function renderRadioState(state) {
|
|||||||
if (this._radioTuner) this._radioTuner.hidden = !tunerAvailable;
|
if (this._radioTuner) this._radioTuner.hidden = !tunerAvailable;
|
||||||
if (this._radioTunerSlider) this._radioTunerSlider.disabled = !tunerAvailable;
|
if (this._radioTunerSlider) this._radioTunerSlider.disabled = !tunerAvailable;
|
||||||
if (this._radioTunerBandLabel) {
|
if (this._radioTunerBandLabel) {
|
||||||
const activeCategory = state.categories.find((category) => category.id === state.filter);
|
const activeCategory = state.categories.find(
|
||||||
this._radioTunerBandLabel.textContent = state.filter === 'all'
|
(category) => category.id === state.filter,
|
||||||
|
);
|
||||||
|
this._radioTunerBandLabel.textContent =
|
||||||
|
state.filter === 'all'
|
||||||
? 'DIRECTORY BAND'
|
? 'DIRECTORY BAND'
|
||||||
: `${String(activeCategory?.label || state.filter).toUpperCase()} BAND`;
|
: `${String(activeCategory?.label || state.filter).toUpperCase()} BAND`;
|
||||||
}
|
}
|
||||||
@@ -121,17 +174,30 @@ export function renderRadioState(state) {
|
|||||||
this._radioTunerSelectedId = null;
|
this._radioTunerSelectedId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._radioStationName) this._radioStationName.textContent = selected?.name || 'NO STATION SELECTED';
|
if (this._radioStationName)
|
||||||
|
this._radioStationName.textContent =
|
||||||
|
selected?.name || 'NO STATION SELECTED';
|
||||||
if (this._radioStationMeta) {
|
if (this._radioStationMeta) {
|
||||||
const place = selected ? [selected.state, selected.countryCode].filter(Boolean).join(' · ') : '';
|
const place = selected
|
||||||
const signal = selected ? [selected.codec, selected.bitrate ? `${selected.bitrate} kbps` : ''].filter(Boolean).join(' · ') : '';
|
? [selected.state, selected.countryCode].filter(Boolean).join(' · ')
|
||||||
|
: '';
|
||||||
|
const signal = selected
|
||||||
|
? [selected.codec, selected.bitrate ? `${selected.bitrate} kbps` : '']
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' · ')
|
||||||
|
: '';
|
||||||
this._radioStationMeta.textContent = selected
|
this._radioStationMeta.textContent = selected
|
||||||
? [place, signal].filter(Boolean).join(' / ') || 'Directory metadata only'
|
? [place, signal].filter(Boolean).join(' / ') ||
|
||||||
: (state.loading ? 'Loading station directory…' : 'Choose a globe marker or use next.');
|
'Directory metadata only'
|
||||||
|
: state.loading
|
||||||
|
? 'Loading station directory…'
|
||||||
|
: 'Choose a globe marker or use next.';
|
||||||
}
|
}
|
||||||
if (this._radioStationTags) {
|
if (this._radioStationTags) {
|
||||||
const tags = Array.isArray(selected?.tags) ? selected.tags.slice(0, 8) : [];
|
const tags = Array.isArray(selected?.tags) ? selected.tags.slice(0, 8) : [];
|
||||||
this._radioStationTags.textContent = tags.length ? `TAGS · ${tags.join(' · ')}` : '';
|
this._radioStationTags.textContent = tags.length
|
||||||
|
? `TAGS · ${tags.join(' · ')}`
|
||||||
|
: '';
|
||||||
}
|
}
|
||||||
if (this._radioStationHomepage) {
|
if (this._radioStationHomepage) {
|
||||||
const homepage = selected?.homepage || '';
|
const homepage = selected?.homepage || '';
|
||||||
@@ -140,72 +206,117 @@ export function renderRadioState(state) {
|
|||||||
else this._radioStationHomepage.removeAttribute('href');
|
else this._radioStationHomepage.removeAttribute('href');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._radioPrevBtn) this._radioPrevBtn.disabled = !interactive || !hasStations;
|
if (this._radioPrevBtn)
|
||||||
if (this._radioNextBtn) this._radioNextBtn.disabled = !interactive || !hasStations;
|
this._radioPrevBtn.disabled = !interactive || !hasStations;
|
||||||
if (this._contextRadioMiniPrevBtn) this._contextRadioMiniPrevBtn.disabled = !interactive || !hasStations;
|
if (this._radioNextBtn)
|
||||||
if (this._contextRadioMiniNextBtn) this._contextRadioMiniNextBtn.disabled = !interactive || !hasStations;
|
this._radioNextBtn.disabled = !interactive || !hasStations;
|
||||||
if (this._cockpitRadioPrevBtn) this._cockpitRadioPrevBtn.disabled = !interactive || !hasStations;
|
if (this._contextRadioMiniPrevBtn)
|
||||||
if (this._cockpitRadioNextBtn) this._cockpitRadioNextBtn.disabled = !interactive || !hasStations;
|
this._contextRadioMiniPrevBtn.disabled = !interactive || !hasStations;
|
||||||
|
if (this._contextRadioMiniNextBtn)
|
||||||
|
this._contextRadioMiniNextBtn.disabled = !interactive || !hasStations;
|
||||||
|
if (this._cockpitRadioPrevBtn)
|
||||||
|
this._cockpitRadioPrevBtn.disabled = !interactive || !hasStations;
|
||||||
|
if (this._cockpitRadioNextBtn)
|
||||||
|
this._cockpitRadioNextBtn.disabled = !interactive || !hasStations;
|
||||||
if (this._radioPlayBtn) {
|
if (this._radioPlayBtn) {
|
||||||
const action = activePlayback ? 'Pause' : (state.audioState === 'paused' ? 'Resume' : 'Play');
|
const action = activePlayback
|
||||||
|
? 'Pause'
|
||||||
|
: state.audioState === 'paused'
|
||||||
|
? 'Resume'
|
||||||
|
: 'Play';
|
||||||
this._radioPlayBtn.disabled = !interactive || !hasStations;
|
this._radioPlayBtn.disabled = !interactive || !hasStations;
|
||||||
this._radioPlayBtn.classList.toggle('active', activePlayback);
|
this._radioPlayBtn.classList.toggle('active', activePlayback);
|
||||||
this._radioPlayBtn.textContent = action.toUpperCase();
|
this._radioPlayBtn.textContent = action.toUpperCase();
|
||||||
this._radioPlayBtn.setAttribute('aria-label', `${action} ${selected ? 'selected' : 'nearest'} radio station`);
|
this._radioPlayBtn.setAttribute(
|
||||||
|
'aria-label',
|
||||||
|
`${action} ${selected ? 'selected' : 'nearest'} radio station`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (this._contextRadioMiniPlayBtn) {
|
if (this._contextRadioMiniPlayBtn) {
|
||||||
const action = activePlayback ? 'Pause' : (state.audioState === 'paused' ? 'Resume' : 'Play');
|
const action = activePlayback
|
||||||
|
? 'Pause'
|
||||||
|
: state.audioState === 'paused'
|
||||||
|
? 'Resume'
|
||||||
|
: 'Play';
|
||||||
this._contextRadioMiniPlayBtn.disabled = !interactive || !hasStations;
|
this._contextRadioMiniPlayBtn.disabled = !interactive || !hasStations;
|
||||||
this._contextRadioMiniPlayBtn.classList.toggle('active', activePlayback);
|
this._contextRadioMiniPlayBtn.classList.toggle('active', activePlayback);
|
||||||
this._contextRadioMiniPlayBtn.textContent = activePlayback ? 'Ⅱ' : '▶';
|
this._contextRadioMiniPlayBtn.textContent = activePlayback ? 'Ⅱ' : '▶';
|
||||||
this._contextRadioMiniPlayBtn.setAttribute('aria-label', `${action} ${selected ? 'selected' : 'nearest'} radio station`);
|
this._contextRadioMiniPlayBtn.setAttribute(
|
||||||
|
'aria-label',
|
||||||
|
`${action} ${selected ? 'selected' : 'nearest'} radio station`,
|
||||||
|
);
|
||||||
this._contextRadioMiniPlayBtn.title = action;
|
this._contextRadioMiniPlayBtn.title = action;
|
||||||
}
|
}
|
||||||
if (this._cockpitRadioPlayBtn) {
|
if (this._cockpitRadioPlayBtn) {
|
||||||
const action = activePlayback ? 'Pause' : (state.audioState === 'paused' ? 'Resume' : 'Play');
|
const action = activePlayback
|
||||||
|
? 'Pause'
|
||||||
|
: state.audioState === 'paused'
|
||||||
|
? 'Resume'
|
||||||
|
: 'Play';
|
||||||
this._cockpitRadioPlayBtn.disabled = !interactive || !hasStations;
|
this._cockpitRadioPlayBtn.disabled = !interactive || !hasStations;
|
||||||
this._cockpitRadioPlayBtn.classList.toggle('active', activePlayback);
|
this._cockpitRadioPlayBtn.classList.toggle('active', activePlayback);
|
||||||
this._cockpitRadioPlayBtn.textContent = activePlayback ? 'Ⅱ' : '▶';
|
this._cockpitRadioPlayBtn.textContent = activePlayback ? 'Ⅱ' : '▶';
|
||||||
this._cockpitRadioPlayBtn.setAttribute('aria-label', `${action} ${selected ? 'selected' : 'nearest'} radio station`);
|
this._cockpitRadioPlayBtn.setAttribute(
|
||||||
|
'aria-label',
|
||||||
|
`${action} ${selected ? 'selected' : 'nearest'} radio station`,
|
||||||
|
);
|
||||||
this._cockpitRadioPlayBtn.title = action;
|
this._cockpitRadioPlayBtn.title = action;
|
||||||
}
|
}
|
||||||
if (this._radioStopBtn) this._radioStopBtn.disabled = !interactive || state.audioState === 'stopped';
|
if (this._radioStopBtn)
|
||||||
|
this._radioStopBtn.disabled =
|
||||||
|
!interactive || state.audioState === 'stopped';
|
||||||
if (this._radioVolume) this._radioVolume.disabled = !interactive;
|
if (this._radioVolume) this._radioVolume.disabled = !interactive;
|
||||||
if (this._radioVolume && document.activeElement !== this._radioVolume) {
|
if (this._radioVolume && document.activeElement !== this._radioVolume) {
|
||||||
this._radioVolume.value = String(Math.round(state.volume * 100));
|
this._radioVolume.value = String(Math.round(state.volume * 100));
|
||||||
if (this._radioVolumeValue) this._radioVolumeValue.textContent = `${Math.round(state.volume * 100)}%`;
|
if (this._radioVolumeValue)
|
||||||
|
this._radioVolumeValue.textContent = `${Math.round(state.volume * 100)}%`;
|
||||||
}
|
}
|
||||||
if (this._contextRadioMiniVolume && document.activeElement !== this._contextRadioMiniVolume) {
|
if (
|
||||||
|
this._contextRadioMiniVolume &&
|
||||||
|
document.activeElement !== this._contextRadioMiniVolume
|
||||||
|
) {
|
||||||
this._contextRadioMiniVolume.value = String(Math.round(state.volume * 100));
|
this._contextRadioMiniVolume.value = String(Math.round(state.volume * 100));
|
||||||
}
|
}
|
||||||
if (this._contextRadioMiniVolume) this._contextRadioMiniVolume.disabled = !interactive;
|
if (this._contextRadioMiniVolume)
|
||||||
|
this._contextRadioMiniVolume.disabled = !interactive;
|
||||||
if (this._contextRadioMiniVolumeValue) {
|
if (this._contextRadioMiniVolumeValue) {
|
||||||
this._contextRadioMiniVolumeValue.textContent = `${Math.round(state.volume * 100)}%`;
|
this._contextRadioMiniVolumeValue.textContent = `${Math.round(state.volume * 100)}%`;
|
||||||
}
|
}
|
||||||
if (this._cockpitRadioVolume && document.activeElement !== this._cockpitRadioVolume) {
|
if (
|
||||||
|
this._cockpitRadioVolume &&
|
||||||
|
document.activeElement !== this._cockpitRadioVolume
|
||||||
|
) {
|
||||||
this._cockpitRadioVolume.value = String(Math.round(state.volume * 100));
|
this._cockpitRadioVolume.value = String(Math.round(state.volume * 100));
|
||||||
}
|
}
|
||||||
if (this._cockpitRadioVolume) this._cockpitRadioVolume.disabled = !interactive;
|
if (this._cockpitRadioVolume)
|
||||||
|
this._cockpitRadioVolume.disabled = !interactive;
|
||||||
if (this._cockpitRadioVolumeValue) {
|
if (this._cockpitRadioVolumeValue) {
|
||||||
this._cockpitRadioVolumeValue.textContent = `${Math.round(state.volume * 100)}%`;
|
this._cockpitRadioVolumeValue.textContent = `${Math.round(state.volume * 100)}%`;
|
||||||
}
|
}
|
||||||
if (this._contextRadioMiniStation) {
|
if (this._contextRadioMiniStation) {
|
||||||
this._contextRadioMiniStation.textContent = uncertain
|
this._contextRadioMiniStation.textContent = uncertain
|
||||||
? 'RADIO STATE UNCERTAIN'
|
? 'RADIO STATE UNCERTAIN'
|
||||||
: (selected?.name || (state.loading ? 'SYNCING DIRECTORY' : 'RADIO READY'));
|
: selected?.name || (state.loading ? 'SYNCING DIRECTORY' : 'RADIO READY');
|
||||||
}
|
}
|
||||||
if (this._cockpitRadioStation) {
|
if (this._cockpitRadioStation) {
|
||||||
this._cockpitRadioStation.textContent = uncertain
|
this._cockpitRadioStation.textContent = uncertain
|
||||||
? 'UNCERTAIN'
|
? 'UNCERTAIN'
|
||||||
: (selected?.name || (state.loading ? 'SYNCING' : 'READY'));
|
: selected?.name || (state.loading ? 'SYNCING' : 'READY');
|
||||||
}
|
}
|
||||||
if (this._radioPlaybackState) {
|
if (this._radioPlaybackState) {
|
||||||
const catalogSuffix = state.degraded
|
const catalogSuffix = state.degraded
|
||||||
? (state.stale ? ' · stale/degraded directory' : ' · degraded directory')
|
? state.stale
|
||||||
: (state.stale ? ' · stale directory' : '');
|
? ' · stale/degraded directory'
|
||||||
const outsideFilter = selected && state.selectedIndex < 0 ? ' · outside current filter' : '';
|
: ' · degraded directory'
|
||||||
|
: state.stale
|
||||||
|
? ' · stale directory'
|
||||||
|
: '';
|
||||||
|
const outsideFilter =
|
||||||
|
selected && state.selectedIndex < 0 ? ' · outside current filter' : '';
|
||||||
const messages = {
|
const messages = {
|
||||||
stopped: enabled ? 'Ready — playback starts only from your action' : 'Radio off',
|
stopped: enabled
|
||||||
|
? 'Ready — playback starts only from your action'
|
||||||
|
: 'Radio off',
|
||||||
loading: 'Connecting directly to broadcaster…',
|
loading: 'Connecting directly to broadcaster…',
|
||||||
buffering: 'Buffering broadcaster stream…',
|
buffering: 'Buffering broadcaster stream…',
|
||||||
playing: `Playing ${selected?.name || 'station'}`,
|
playing: `Playing ${selected?.name || 'station'}`,
|
||||||
@@ -214,32 +325,43 @@ export function renderRadioState(state) {
|
|||||||
};
|
};
|
||||||
const voiceSuffix = state.voiceDucked
|
const voiceSuffix = state.voiceDucked
|
||||||
? ' · muted during voice interaction'
|
? ' · muted during voice interaction'
|
||||||
: (state.voiceRestoring ? ' · restoring volume after voice' : '');
|
: state.voiceRestoring
|
||||||
|
? ' · restoring volume after voice'
|
||||||
|
: '';
|
||||||
const tuningSuffix = state.tuningAwaitingStationId
|
const tuningSuffix = state.tuningAwaitingStationId
|
||||||
? (state.audioState === 'error'
|
? state.audioState === 'error'
|
||||||
? ' · static indicates no broadcaster audio'
|
? ' · static indicates no broadcaster audio'
|
||||||
: ' · tuning static until broadcaster starts')
|
: ' · tuning static until broadcaster starts'
|
||||||
: '';
|
: '';
|
||||||
const unavailable = state.tuningUnavailableStationId
|
const unavailable = state.tuningUnavailableStationId
|
||||||
? 'Station unavailable after directory refresh — choose another channel'
|
? 'Station unavailable after directory refresh — choose another channel'
|
||||||
: null;
|
: null;
|
||||||
const lifecycleMessage = transitioning
|
const lifecycleMessage = transitioning
|
||||||
? (lifecycleState === 'enabling' ? 'Radio is enabling…' : 'Radio is disabling…')
|
? lifecycleState === 'enabling'
|
||||||
|
? 'Radio is enabling…'
|
||||||
|
: 'Radio is disabling…'
|
||||||
: null;
|
: null;
|
||||||
const uncertainMessage = uncertain
|
const uncertainMessage = uncertain
|
||||||
? 'Radio lifecycle is uncertain — use Enable or Disable to reconcile'
|
? 'Radio lifecycle is uncertain — use Enable or Disable to reconcile'
|
||||||
: null;
|
: null;
|
||||||
this._radioPlaybackState.textContent = `${uncertainMessage || unavailable || lifecycleMessage || state.error || messages[state.audioState] || 'Ready'}${tuningSuffix}${voiceSuffix}${catalogSuffix}${outsideFilter}`;
|
this._radioPlaybackState.textContent = `${uncertainMessage || unavailable || lifecycleMessage || state.error || messages[state.audioState] || 'Ready'}${tuningSuffix}${voiceSuffix}${catalogSuffix}${outsideFilter}`;
|
||||||
this._radioPlaybackState.classList.toggle('error', Boolean(uncertainMessage || unavailable || state.error || state.audioState === 'error'));
|
this._radioPlaybackState.classList.toggle(
|
||||||
|
'error',
|
||||||
|
Boolean(
|
||||||
|
uncertainMessage ||
|
||||||
|
unavailable ||
|
||||||
|
state.error ||
|
||||||
|
state.audioState === 'error',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!enabled
|
!enabled &&
|
||||||
&& !transitioning
|
!transitioning &&
|
||||||
&& !this.actions.preservePanelStateDuringClear()
|
!this.actions.preservePanelStateDuringClear() &&
|
||||||
&& !this._radioPanel.classList.contains('collapsed')
|
!this._radioPanel.classList.contains('collapsed')
|
||||||
) {
|
) {
|
||||||
this.actions.setPanelCollapsed('radio-panel', true);
|
this.actions.setPanelCollapsed('radio-panel', true);
|
||||||
}
|
}
|
||||||
this.actions.scheduleLayout();
|
this.actions.scheduleLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
+60
-12
@@ -1,7 +1,15 @@
|
|||||||
/** Map an integer tuner slot directly to one available directory station. */
|
/** Map an integer tuner slot directly to one available directory station. */
|
||||||
export function radioTunerSlot(value, stationCount) {
|
export function radioTunerSlot(value, stationCount) {
|
||||||
const count = Math.max(0, Math.floor(Number(stationCount) || 0));
|
const count = Math.max(0, Math.floor(Number(stationCount) || 0));
|
||||||
if (!count) return { slot: 0, max: 0, locked: false, stationIndex: -1, leftIndex: -1, rightIndex: -1 };
|
if (!count)
|
||||||
|
return {
|
||||||
|
slot: 0,
|
||||||
|
max: 0,
|
||||||
|
locked: false,
|
||||||
|
stationIndex: -1,
|
||||||
|
leftIndex: -1,
|
||||||
|
rightIndex: -1,
|
||||||
|
};
|
||||||
const max = Math.max(0, count - 1);
|
const max = Math.max(0, count - 1);
|
||||||
const slot = Math.min(max, Math.max(0, Math.round(Number(value) || 0)));
|
const slot = Math.min(max, Math.max(0, Math.round(Number(value) || 0)));
|
||||||
return {
|
return {
|
||||||
@@ -20,29 +28,49 @@ export function radioTunerCommitSlot(value, stationCount) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Map one pointer coordinate to continuous absolute directory progress. */
|
/** Map one pointer coordinate to continuous absolute directory progress. */
|
||||||
export function radioTunerPointerPosition(clientX, left, width, stationCount, insetPx = 7) {
|
export function radioTunerPointerPosition(
|
||||||
|
clientX,
|
||||||
|
left,
|
||||||
|
width,
|
||||||
|
stationCount,
|
||||||
|
insetPx = 7,
|
||||||
|
) {
|
||||||
const count = Math.max(0, Math.floor(Number(stationCount) || 0));
|
const count = Math.max(0, Math.floor(Number(stationCount) || 0));
|
||||||
if (!count) return { ratio: 0, coordinate: 0, stationIndex: -1 };
|
if (!count) return { ratio: 0, coordinate: 0, stationIndex: -1 };
|
||||||
if (count === 1) return { ratio: 0.5, coordinate: 0, stationIndex: 0 };
|
if (count === 1) return { ratio: 0.5, coordinate: 0, stationIndex: 0 };
|
||||||
const inset = Math.max(0, Number(insetPx) || 0);
|
const inset = Math.max(0, Number(insetPx) || 0);
|
||||||
const usableWidth = Math.max(1, (Number(width) || 0) - inset * 2);
|
const usableWidth = Math.max(1, (Number(width) || 0) - inset * 2);
|
||||||
const ratio = Math.min(1, Math.max(0, ((Number(clientX) || 0) - (Number(left) || 0) - inset) / usableWidth));
|
const ratio = Math.min(
|
||||||
|
1,
|
||||||
|
Math.max(
|
||||||
|
0,
|
||||||
|
((Number(clientX) || 0) - (Number(left) || 0) - inset) / usableWidth,
|
||||||
|
),
|
||||||
|
);
|
||||||
const coordinate = ratio * (count - 1);
|
const coordinate = ratio * (count - 1);
|
||||||
return {
|
return {
|
||||||
ratio,
|
ratio,
|
||||||
coordinate,
|
coordinate,
|
||||||
stationIndex: Math.min(count - 1, Math.max(0, Math.floor(coordinate + 0.5))),
|
stationIndex: Math.min(
|
||||||
|
count - 1,
|
||||||
|
Math.max(0, Math.floor(coordinate + 0.5)),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Build a bounded virtual tuner tape around one continuous directory coordinate. */
|
/** Build a bounded virtual tuner tape around one continuous directory coordinate. */
|
||||||
export function buildRadioTunerTicks(coordinate, stationCount, width, {
|
export function buildRadioTunerTicks(
|
||||||
|
coordinate,
|
||||||
|
stationCount,
|
||||||
|
width,
|
||||||
|
{
|
||||||
insetPx = 7,
|
insetPx = 7,
|
||||||
minPitchPx = 14,
|
minPitchPx = 14,
|
||||||
speedFactor = 5,
|
speedFactor = 5,
|
||||||
overscan = 2,
|
overscan = 2,
|
||||||
labelStep = 6,
|
labelStep = 6,
|
||||||
} = {}) {
|
} = {},
|
||||||
|
) {
|
||||||
const count = Math.max(0, Math.floor(Number(stationCount) || 0));
|
const count = Math.max(0, Math.floor(Number(stationCount) || 0));
|
||||||
const dialWidth = Math.max(0, Number(width) || 0);
|
const dialWidth = Math.max(0, Number(width) || 0);
|
||||||
const inset = Math.max(0, Number(insetPx) || 0);
|
const inset = Math.max(0, Number(insetPx) || 0);
|
||||||
@@ -51,7 +79,15 @@ export function buildRadioTunerTicks(coordinate, stationCount, width, {
|
|||||||
const value = Math.min(count - 1, Math.max(0, Number(coordinate) || 0));
|
const value = Math.min(count - 1, Math.max(0, Number(coordinate) || 0));
|
||||||
if (count === 1) {
|
if (count === 1) {
|
||||||
return {
|
return {
|
||||||
ticks: [{ stationIndex: 0, channel: 1, xPx: inset + usableWidth / 2, current: true, label: '01' }],
|
ticks: [
|
||||||
|
{
|
||||||
|
stationIndex: 0,
|
||||||
|
channel: 1,
|
||||||
|
xPx: inset + usableWidth / 2,
|
||||||
|
current: true,
|
||||||
|
label: '01',
|
||||||
|
},
|
||||||
|
],
|
||||||
needleX: inset + usableWidth / 2,
|
needleX: inset + usableWidth / 2,
|
||||||
pitchPx: Math.max(1, Number(minPitchPx) || 14),
|
pitchPx: Math.max(1, Number(minPitchPx) || 14),
|
||||||
ratio: 0.5,
|
ratio: 0.5,
|
||||||
@@ -64,16 +100,29 @@ export function buildRadioTunerTicks(coordinate, stationCount, width, {
|
|||||||
);
|
);
|
||||||
const needleX = inset + directoryStep * value;
|
const needleX = inset + directoryStep * value;
|
||||||
const overscanPx = pitchPx * Math.max(0, Number(overscan) || 0);
|
const overscanPx = pitchPx * Math.max(0, Number(overscan) || 0);
|
||||||
const first = Math.max(0, Math.ceil(value + (-overscanPx - needleX) / pitchPx));
|
const first = Math.max(
|
||||||
const last = Math.min(count - 1, Math.floor(value + (dialWidth + overscanPx - needleX) / pitchPx));
|
0,
|
||||||
const currentIndex = Math.min(count - 1, Math.max(0, Math.floor(value + 0.5)));
|
Math.ceil(value + (-overscanPx - needleX) / pitchPx),
|
||||||
|
);
|
||||||
|
const last = Math.min(
|
||||||
|
count - 1,
|
||||||
|
Math.floor(value + (dialWidth + overscanPx - needleX) / pitchPx),
|
||||||
|
);
|
||||||
|
const currentIndex = Math.min(
|
||||||
|
count - 1,
|
||||||
|
Math.max(0, Math.floor(value + 0.5)),
|
||||||
|
);
|
||||||
const majorEvery = Math.max(1, Math.floor(Number(labelStep) || 6));
|
const majorEvery = Math.max(1, Math.floor(Number(labelStep) || 6));
|
||||||
const labelWidth = Math.max(2, String(count).length);
|
const labelWidth = Math.max(2, String(count).length);
|
||||||
const ticks = [];
|
const ticks = [];
|
||||||
for (let stationIndex = first; stationIndex <= last; stationIndex += 1) {
|
for (let stationIndex = first; stationIndex <= last; stationIndex += 1) {
|
||||||
const channel = stationIndex + 1;
|
const channel = stationIndex + 1;
|
||||||
const current = stationIndex === currentIndex;
|
const current = stationIndex === currentIndex;
|
||||||
const labelled = current || stationIndex === 0 || stationIndex === count - 1 || channel % majorEvery === 0;
|
const labelled =
|
||||||
|
current ||
|
||||||
|
stationIndex === 0 ||
|
||||||
|
stationIndex === count - 1 ||
|
||||||
|
channel % majorEvery === 0;
|
||||||
ticks.push({
|
ticks.push({
|
||||||
stationIndex,
|
stationIndex,
|
||||||
channel,
|
channel,
|
||||||
@@ -84,4 +133,3 @@ export function buildRadioTunerTicks(coordinate, stationCount, width, {
|
|||||||
}
|
}
|
||||||
return { ticks, needleX, pitchPx, ratio: value / (count - 1) };
|
return { ticks, needleX, pitchPx, ratio: value / (count - 1) };
|
||||||
}
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user